Compare commits
38 Commits
041c9caf31
...
d13d354707
Author | SHA1 | Date | |
---|---|---|---|
d13d354707 | |||
|
70b2a8f72e | ||
|
c4f1ca2fd9 | ||
|
9d0f7eeb9c | ||
|
bc1fce9af6 | ||
|
5f12bc515d | ||
|
2f9c04b23b | ||
|
5c79d8db20 | ||
|
bc0c53b92b | ||
|
d6caa4d9c4 | ||
|
d894fc7f12 | ||
|
654821da00 | ||
|
f7d74b74e4 | ||
|
b9f4e00072 | ||
|
134408ced6 | ||
|
7e3e62e2de | ||
d1af6271b4 | |||
|
9e1b46f160 | ||
|
b353223941 | ||
|
54122a93dd | ||
|
7318200786 | ||
|
64abf28bb0 | ||
|
15388e7732 | ||
|
317cabe15c | ||
|
421d8fb2c2 | ||
|
285371c639 | ||
|
a312a744f9 | ||
|
cefff6cb92 | ||
|
09839ea5a4 | ||
|
07553f5f8e | ||
|
dd58eacac0 | ||
|
aaf560f747 | ||
|
1a44c33340 | ||
|
ccb1883e3e | ||
|
13e7b49f09 | ||
|
e56c372de7 | ||
|
694722c02f | ||
|
727ed9de55 |
@ -186,6 +186,9 @@ id: 'aidx'
|
|||||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||||
#outgoingAddressFamily: ipv4
|
#outgoingAddressFamily: ipv4
|
||||||
|
|
||||||
|
# Override the file URL rendering in ActivityPub (Object Storage file only)
|
||||||
|
#apFileBaseUrl: https://example.com/
|
||||||
|
|
||||||
# Proxy for HTTP/HTTPS
|
# Proxy for HTTP/HTTPS
|
||||||
#proxy: http://127.0.0.1:3128
|
#proxy: http://127.0.0.1:3128
|
||||||
|
|
||||||
|
@ -275,6 +275,9 @@ id: 'aidx'
|
|||||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||||
#outgoingAddressFamily: ipv4
|
#outgoingAddressFamily: ipv4
|
||||||
|
|
||||||
|
# Override the file URL rendering in ActivityPub (Object Storage file only)
|
||||||
|
#apFileBaseUrl: https://example.com/
|
||||||
|
|
||||||
# Proxy for HTTP/HTTPS
|
# Proxy for HTTP/HTTPS
|
||||||
#proxy: http://127.0.0.1:3128
|
#proxy: http://127.0.0.1:3128
|
||||||
|
|
||||||
|
@ -179,6 +179,9 @@ id: 'aidx'
|
|||||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||||
#outgoingAddressFamily: ipv4
|
#outgoingAddressFamily: ipv4
|
||||||
|
|
||||||
|
# Override the file URL rendering in ActivityPub (Object Storage file only)
|
||||||
|
#apFileBaseUrl: https://example.com/
|
||||||
|
|
||||||
# Proxy for HTTP/HTTPS
|
# Proxy for HTTP/HTTPS
|
||||||
#proxy: http://127.0.0.1:3128
|
#proxy: http://127.0.0.1:3128
|
||||||
|
|
||||||
|
25
.github/workflows/docker-develop.yml
vendored
@ -7,7 +7,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY_IMAGE: misskey/misskey
|
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# see https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
|
# see https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
|
||||||
@ -20,7 +20,8 @@ jobs:
|
|||||||
platform:
|
platform:
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
- linux/arm64
|
- linux/arm64
|
||||||
if: github.repository == 'misskey-dev/misskey'
|
outputs:
|
||||||
|
commit_sha: ${{ steps.git.outputs.commit_sha }}
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
@ -28,13 +29,17 @@ jobs:
|
|||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4.1.1
|
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
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
registry: ghcr.io
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push by digest
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@ -73,16 +78,18 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Login to Docker Hub
|
- name: Log in to Container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
registry: ghcr.io
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Create manifest list and push
|
- name: Create manifest list and push
|
||||||
working-directory: /tmp/digests
|
working-directory: /tmp/digests
|
||||||
run: |
|
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 ' *)
|
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||||
- name: Inspect image
|
- name: Inspect image
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:develop
|
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:develop
|
||||||
|
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{needs.build.outputs.commit_sha}}
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### General
|
### General
|
||||||
-
|
- Feat: コンテンツの表示にログインを必須にできるように
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
|
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
|
||||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/751)
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/751)
|
||||||
- Enhance: ドライブでソートができるように
|
- Enhance: ドライブでソートができるように
|
||||||
|
- Enhance: 投稿フォームでEscキーを押したときIME入力中ならフォームを閉じないように( #10866 )
|
||||||
|
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
|
||||||
|
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
|
||||||
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
|
||||||
|
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
-
|
||||||
|
@ -64,6 +64,22 @@ Thank you for your PR! Before creating a PR, please check the following:
|
|||||||
|
|
||||||
Thanks for your cooperation 🤗
|
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
|
## Reviewers guide
|
||||||
Be willing to comment on the good points and not just the things you want fixed 💯
|
Be willing to comment on the good points and not just the things you want fixed 💯
|
||||||
|
|
||||||
|
@ -200,6 +200,9 @@ id: "aidx"
|
|||||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||||
#outgoingAddressFamily: ipv4
|
#outgoingAddressFamily: ipv4
|
||||||
|
|
||||||
|
# Override the file URL rendering in ActivityPub (Object Storage file only)
|
||||||
|
#apFileBaseUrl: https://example.com/
|
||||||
|
|
||||||
# Proxy for HTTP/HTTPS
|
# Proxy for HTTP/HTTPS
|
||||||
#proxy: http://127.0.0.1:3128
|
#proxy: http://127.0.0.1:3128
|
||||||
|
|
||||||
|
@ -1294,6 +1294,7 @@ _abuseUserReport:
|
|||||||
accept: "Accept"
|
accept: "Accept"
|
||||||
reject: "Reject"
|
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."
|
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:
|
_delivery:
|
||||||
status: "Delivery status"
|
status: "Delivery status"
|
||||||
stop: "Suspended"
|
stop: "Suspended"
|
||||||
@ -1738,6 +1739,7 @@ _role:
|
|||||||
gtlAvailable: "Can view the global timeline"
|
gtlAvailable: "Can view the global timeline"
|
||||||
ltlAvailable: "Can view the local timeline"
|
ltlAvailable: "Can view the local timeline"
|
||||||
canPublicNote: "Can send public notes"
|
canPublicNote: "Can send public notes"
|
||||||
|
canEditNote: "Note editing"
|
||||||
mentionMax: "Maximum number of mentions in a note"
|
mentionMax: "Maximum number of mentions in a note"
|
||||||
canInvite: "Can create instance invite codes"
|
canInvite: "Can create instance invite codes"
|
||||||
inviteLimit: "Invite limit"
|
inviteLimit: "Invite limit"
|
||||||
|
40
locales/index.d.ts
vendored
@ -5130,6 +5130,10 @@ export interface Locale extends ILocale {
|
|||||||
* これ以上このクリップにノートを追加できません。
|
* これ以上このクリップにノートを追加できません。
|
||||||
*/
|
*/
|
||||||
"clipNoteLimitExceeded": string;
|
"clipNoteLimitExceeded": string;
|
||||||
|
/**
|
||||||
|
* 編集済み: {date} {time}
|
||||||
|
*/
|
||||||
|
"noteUpdatedAt": ParameterizedString<"date" | "time">;
|
||||||
/**
|
/**
|
||||||
* パフォーマンス
|
* パフォーマンス
|
||||||
*/
|
*/
|
||||||
@ -5190,6 +5194,32 @@ export interface Locale extends ILocale {
|
|||||||
* 名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。
|
* 名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。
|
||||||
*/
|
*/
|
||||||
"yourNameContainsProhibitedWordsDescription": string;
|
"yourNameContainsProhibitedWordsDescription": string;
|
||||||
|
/**
|
||||||
|
* 投稿者により、表示にはログインが必要と設定されています
|
||||||
|
*/
|
||||||
|
"thisContentsAreMarkedAsSigninRequiredByAuthor": string;
|
||||||
|
/**
|
||||||
|
* ロックダウン
|
||||||
|
*/
|
||||||
|
"lockdown": string;
|
||||||
|
"_accountSettings": {
|
||||||
|
/**
|
||||||
|
* コンテンツの表示にログインを必須にする
|
||||||
|
*/
|
||||||
|
"requireSigninToViewContents": string;
|
||||||
|
/**
|
||||||
|
* あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーから情報を収集されるのを防ぐ効果が期待できます。
|
||||||
|
*/
|
||||||
|
"requireSigninToViewContentsDescription1": string;
|
||||||
|
/**
|
||||||
|
* URLプレビュー(OGP)、Webページへの埋め込み、ノートの引用に対応していないサーバーからの表示も不可になります。
|
||||||
|
*/
|
||||||
|
"requireSigninToViewContentsDescription2": string;
|
||||||
|
/**
|
||||||
|
* リモートサーバーに連合されたコンテンツでは、これらの制限が適用されない場合があります。
|
||||||
|
*/
|
||||||
|
"requireSigninToViewContentsDescription3": string;
|
||||||
|
};
|
||||||
"_abuseUserReport": {
|
"_abuseUserReport": {
|
||||||
/**
|
/**
|
||||||
* 転送
|
* 転送
|
||||||
@ -6799,6 +6829,10 @@ export interface Locale extends ILocale {
|
|||||||
* パブリック投稿の許可
|
* パブリック投稿の許可
|
||||||
*/
|
*/
|
||||||
"canPublicNote": string;
|
"canPublicNote": string;
|
||||||
|
/**
|
||||||
|
* ノートの編集
|
||||||
|
*/
|
||||||
|
"canEditNote": string;
|
||||||
/**
|
/**
|
||||||
* ノート内の最大メンション数
|
* ノート内の最大メンション数
|
||||||
*/
|
*/
|
||||||
@ -9271,7 +9305,7 @@ export interface Locale extends ILocale {
|
|||||||
*/
|
*/
|
||||||
"youGotQuote": ParameterizedString<"name">;
|
"youGotQuote": ParameterizedString<"name">;
|
||||||
/**
|
/**
|
||||||
* {name}がRenoteしました
|
* {name}がリノートしました
|
||||||
*/
|
*/
|
||||||
"youRenoted": ParameterizedString<"name">;
|
"youRenoted": ParameterizedString<"name">;
|
||||||
/**
|
/**
|
||||||
@ -9376,7 +9410,7 @@ export interface Locale extends ILocale {
|
|||||||
*/
|
*/
|
||||||
"reply": string;
|
"reply": string;
|
||||||
/**
|
/**
|
||||||
* Renote
|
* リノート
|
||||||
*/
|
*/
|
||||||
"renote": string;
|
"renote": string;
|
||||||
/**
|
/**
|
||||||
@ -9434,7 +9468,7 @@ export interface Locale extends ILocale {
|
|||||||
*/
|
*/
|
||||||
"reply": string;
|
"reply": string;
|
||||||
/**
|
/**
|
||||||
* Renote
|
* リノート
|
||||||
*/
|
*/
|
||||||
"renote": string;
|
"renote": string;
|
||||||
};
|
};
|
||||||
|
@ -1278,6 +1278,7 @@ fromX: "{x}から"
|
|||||||
genEmbedCode: "埋め込みコードを生成"
|
genEmbedCode: "埋め込みコードを生成"
|
||||||
noteOfThisUser: "このユーザーのノート一覧"
|
noteOfThisUser: "このユーザーのノート一覧"
|
||||||
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
|
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
|
||||||
|
noteUpdatedAt: "編集済み: {date} {time}"
|
||||||
performance: "パフォーマンス"
|
performance: "パフォーマンス"
|
||||||
modified: "変更あり"
|
modified: "変更あり"
|
||||||
discard: "破棄"
|
discard: "破棄"
|
||||||
@ -1293,6 +1294,14 @@ prohibitedWordsForNameOfUser: "禁止ワード(ユーザーの名前)"
|
|||||||
prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。"
|
prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。"
|
||||||
yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています"
|
yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています"
|
||||||
yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。"
|
yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。"
|
||||||
|
thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示にはログインが必要と設定されています"
|
||||||
|
lockdown: "ロックダウン"
|
||||||
|
|
||||||
|
_accountSettings:
|
||||||
|
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
|
||||||
|
requireSigninToViewContentsDescription1: "あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーから情報を収集されるのを防ぐ効果が期待できます。"
|
||||||
|
requireSigninToViewContentsDescription2: "URLプレビュー(OGP)、Webページへの埋め込み、ノートの引用に対応していないサーバーからの表示も不可になります。"
|
||||||
|
requireSigninToViewContentsDescription3: "リモートサーバーに連合されたコンテンツでは、これらの制限が適用されない場合があります。"
|
||||||
|
|
||||||
_abuseUserReport:
|
_abuseUserReport:
|
||||||
forward: "転送"
|
forward: "転送"
|
||||||
@ -1757,6 +1766,7 @@ _role:
|
|||||||
gtlAvailable: "グローバルタイムラインの閲覧"
|
gtlAvailable: "グローバルタイムラインの閲覧"
|
||||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||||
canPublicNote: "パブリック投稿の許可"
|
canPublicNote: "パブリック投稿の許可"
|
||||||
|
canEditNote: "ノートの編集"
|
||||||
mentionMax: "ノート内の最大メンション数"
|
mentionMax: "ノート内の最大メンション数"
|
||||||
canInvite: "サーバー招待コードの発行"
|
canInvite: "サーバー招待コードの発行"
|
||||||
inviteLimit: "招待コードの作成可能数"
|
inviteLimit: "招待コードの作成可能数"
|
||||||
@ -2448,7 +2458,7 @@ _notification:
|
|||||||
youGotMention: "{name}からのメンション"
|
youGotMention: "{name}からのメンション"
|
||||||
youGotReply: "{name}からのリプライ"
|
youGotReply: "{name}からのリプライ"
|
||||||
youGotQuote: "{name}による引用"
|
youGotQuote: "{name}による引用"
|
||||||
youRenoted: "{name}がRenoteしました"
|
youRenoted: "{name}がリノートしました"
|
||||||
youWereFollowed: "フォローされました"
|
youWereFollowed: "フォローされました"
|
||||||
youReceivedFollowRequest: "フォローリクエストが来ました"
|
youReceivedFollowRequest: "フォローリクエストが来ました"
|
||||||
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
||||||
@ -2476,7 +2486,7 @@ _notification:
|
|||||||
follow: "フォロー"
|
follow: "フォロー"
|
||||||
mention: "メンション"
|
mention: "メンション"
|
||||||
reply: "リプライ"
|
reply: "リプライ"
|
||||||
renote: "Renote"
|
renote: "リノート"
|
||||||
quote: "引用"
|
quote: "引用"
|
||||||
reaction: "リアクション"
|
reaction: "リアクション"
|
||||||
pollEnded: "アンケートが終了"
|
pollEnded: "アンケートが終了"
|
||||||
@ -2492,7 +2502,7 @@ _notification:
|
|||||||
_actions:
|
_actions:
|
||||||
followBack: "フォローバック"
|
followBack: "フォローバック"
|
||||||
reply: "返信"
|
reply: "返信"
|
||||||
renote: "Renote"
|
renote: "リノート"
|
||||||
|
|
||||||
_deck:
|
_deck:
|
||||||
alwaysShowMainColumn: "常にメインカラムを表示"
|
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||||
|
@ -1271,6 +1271,7 @@ alwaysConfirmFollow: "팔로우일 때 항상 확인하기"
|
|||||||
inquiry: "문의하기"
|
inquiry: "문의하기"
|
||||||
tryAgain: "다시 시도해 주세요."
|
tryAgain: "다시 시도해 주세요."
|
||||||
confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인"
|
confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인"
|
||||||
|
noteUpdatedAt: "편집됨: {date} {time}"
|
||||||
sensitiveMediaRevealConfirm: "민감한 미디어입니다. 표시할까요?"
|
sensitiveMediaRevealConfirm: "민감한 미디어입니다. 표시할까요?"
|
||||||
createdLists: "만든 리스트"
|
createdLists: "만든 리스트"
|
||||||
createdAntennas: "만든 안테나"
|
createdAntennas: "만든 안테나"
|
||||||
@ -1745,6 +1746,7 @@ _role:
|
|||||||
gtlAvailable: "글로벌 타임라인 보이기"
|
gtlAvailable: "글로벌 타임라인 보이기"
|
||||||
ltlAvailable: "로컬 타임라인 보이기"
|
ltlAvailable: "로컬 타임라인 보이기"
|
||||||
canPublicNote: "공개 노트 허용"
|
canPublicNote: "공개 노트 허용"
|
||||||
|
canEditNote: "노트 편집 허용"
|
||||||
mentionMax: "노트에 넣을 수 있는 멘션 수"
|
mentionMax: "노트에 넣을 수 있는 멘션 수"
|
||||||
canInvite: "서버 초대 코드 발행"
|
canInvite: "서버 초대 코드 발행"
|
||||||
inviteLimit: "초대 한도"
|
inviteLimit: "초대 한도"
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2024.10.1",
|
"version": "2024.10.1",
|
||||||
|
"prefix": "sk",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/misskey-dev/misskey.git"
|
"url": "https://github.com/misskey-dev/misskey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.6.0",
|
"packageManager": "pnpm@9.7.1",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/frontend-shared",
|
"packages/frontend-shared",
|
||||||
"packages/frontend",
|
"packages/frontend",
|
||||||
@ -77,5 +78,9 @@
|
|||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tensorflow/tfjs-core": "4.4.0"
|
"@tensorflow/tfjs-core": "4.4.0"
|
||||||
|
},
|
||||||
|
"volta": {
|
||||||
|
"node": "20.16.0",
|
||||||
|
"pnpm": "9.7.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 751 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 14 KiB |
BIN
packages/backend/assets/icons/None-1024.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
packages/backend/assets/icons/None-512.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
packages/backend/assets/icons/None-769.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
packages/backend/assets/icons/iOS-1024.png
Normal file
After Width: | Height: | Size: 123 KiB |
BIN
packages/backend/assets/icons/iOS-512.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
packages/backend/assets/icons/iOS-769.png
Normal file
After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 123 KiB |
16
packages/backend/migration/1724072711475-NoteEdit.js
Normal 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"`);
|
||||||
|
}
|
||||||
|
}
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
@ -65,6 +65,8 @@ type Source = {
|
|||||||
|
|
||||||
setupPassword?: string;
|
setupPassword?: string;
|
||||||
|
|
||||||
|
apFileBaseUrl?: string;
|
||||||
|
|
||||||
proxy?: string;
|
proxy?: string;
|
||||||
proxySmtp?: string;
|
proxySmtp?: string;
|
||||||
proxyBypassHosts?: string[];
|
proxyBypassHosts?: string[];
|
||||||
@ -132,6 +134,7 @@ export type Config = {
|
|||||||
index: string;
|
index: string;
|
||||||
scope?: 'local' | 'global' | string[];
|
scope?: 'local' | 'global' | string[];
|
||||||
} | undefined;
|
} | undefined;
|
||||||
|
apFileBaseUrl: string | undefined;
|
||||||
proxy: string | undefined;
|
proxy: string | undefined;
|
||||||
proxySmtp: string | undefined;
|
proxySmtp: string | undefined;
|
||||||
proxyBypassHosts: string[] | undefined;
|
proxyBypassHosts: string[] | undefined;
|
||||||
@ -261,6 +264,7 @@ export function loadConfig(): Config {
|
|||||||
sentryForBackend: config.sentryForBackend,
|
sentryForBackend: config.sentryForBackend,
|
||||||
sentryForFrontend: config.sentryForFrontend,
|
sentryForFrontend: config.sentryForFrontend,
|
||||||
id: config.id,
|
id: config.id,
|
||||||
|
apFileBaseUrl: config.apFileBaseUrl,
|
||||||
proxy: config.proxy,
|
proxy: config.proxy,
|
||||||
proxySmtp: config.proxySmtp,
|
proxySmtp: config.proxySmtp,
|
||||||
proxyBypassHosts: config.proxyBypassHosts,
|
proxyBypassHosts: config.proxyBypassHosts,
|
||||||
|
@ -43,6 +43,7 @@ import { MetaService } from './MetaService.js';
|
|||||||
import { MfmService } from './MfmService.js';
|
import { MfmService } from './MfmService.js';
|
||||||
import { ModerationLogService } from './ModerationLogService.js';
|
import { ModerationLogService } from './ModerationLogService.js';
|
||||||
import { NoteCreateService } from './NoteCreateService.js';
|
import { NoteCreateService } from './NoteCreateService.js';
|
||||||
|
import { NoteUpdateService } from './NoteUpdateService.js';
|
||||||
import { NoteDeleteService } from './NoteDeleteService.js';
|
import { NoteDeleteService } from './NoteDeleteService.js';
|
||||||
import { NotePiningService } from './NotePiningService.js';
|
import { NotePiningService } from './NotePiningService.js';
|
||||||
import { NoteReadService } from './NoteReadService.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 $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService };
|
||||||
const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService };
|
const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService };
|
||||||
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
|
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
|
||||||
|
const $NoteUpdateService: Provider = { provide: 'NoteUpdateService', useExisting: NoteUpdateService };
|
||||||
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
|
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
|
||||||
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
|
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
|
||||||
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
|
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
|
||||||
@ -337,6 +339,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
MfmService,
|
MfmService,
|
||||||
ModerationLogService,
|
ModerationLogService,
|
||||||
NoteCreateService,
|
NoteCreateService,
|
||||||
|
NoteUpdateService,
|
||||||
NoteDeleteService,
|
NoteDeleteService,
|
||||||
NotePiningService,
|
NotePiningService,
|
||||||
NoteReadService,
|
NoteReadService,
|
||||||
@ -484,6 +487,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$MfmService,
|
$MfmService,
|
||||||
$ModerationLogService,
|
$ModerationLogService,
|
||||||
$NoteCreateService,
|
$NoteCreateService,
|
||||||
|
$NoteUpdateService,
|
||||||
$NoteDeleteService,
|
$NoteDeleteService,
|
||||||
$NotePiningService,
|
$NotePiningService,
|
||||||
$NoteReadService,
|
$NoteReadService,
|
||||||
@ -632,6 +636,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
MfmService,
|
MfmService,
|
||||||
ModerationLogService,
|
ModerationLogService,
|
||||||
NoteCreateService,
|
NoteCreateService,
|
||||||
|
NoteUpdateService,
|
||||||
NoteDeleteService,
|
NoteDeleteService,
|
||||||
NotePiningService,
|
NotePiningService,
|
||||||
NoteReadService,
|
NoteReadService,
|
||||||
@ -778,6 +783,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$MfmService,
|
$MfmService,
|
||||||
$ModerationLogService,
|
$ModerationLogService,
|
||||||
$NoteCreateService,
|
$NoteCreateService,
|
||||||
|
$NoteUpdateService,
|
||||||
$NoteDeleteService,
|
$NoteDeleteService,
|
||||||
$NotePiningService,
|
$NotePiningService,
|
||||||
$NoteReadService,
|
$NoteReadService,
|
||||||
|
@ -119,7 +119,11 @@ export interface NoteEventTypes {
|
|||||||
};
|
};
|
||||||
updated: {
|
updated: {
|
||||||
cw: string | null;
|
cw: string | null;
|
||||||
text: string;
|
text: string | null;
|
||||||
|
files: Packed<'DriveFile'>[];
|
||||||
|
fileIds: string[];
|
||||||
|
poll: any | null;
|
||||||
|
emojis: Record<string, string>;
|
||||||
};
|
};
|
||||||
reacted: {
|
reacted: {
|
||||||
reaction: string;
|
reaction: string;
|
||||||
|
@ -124,6 +124,7 @@ type MinimumUser = {
|
|||||||
|
|
||||||
type Option = {
|
type Option = {
|
||||||
createdAt?: Date | null;
|
createdAt?: Date | null;
|
||||||
|
updatedAt?: Date | null;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
reply?: MiNote | null;
|
reply?: MiNote | null;
|
||||||
|
309
packages/backend/src/core/NoteUpdateService.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -35,6 +35,7 @@ export type RolePolicies = {
|
|||||||
gtlAvailable: boolean;
|
gtlAvailable: boolean;
|
||||||
ltlAvailable: boolean;
|
ltlAvailable: boolean;
|
||||||
canPublicNote: boolean;
|
canPublicNote: boolean;
|
||||||
|
canEditNote: boolean;
|
||||||
mentionLimit: number;
|
mentionLimit: number;
|
||||||
canInvite: boolean;
|
canInvite: boolean;
|
||||||
inviteLimit: number;
|
inviteLimit: number;
|
||||||
@ -69,6 +70,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||||||
gtlAvailable: true,
|
gtlAvailable: true,
|
||||||
ltlAvailable: true,
|
ltlAvailable: true,
|
||||||
canPublicNote: true,
|
canPublicNote: true,
|
||||||
|
canEditNote: true,
|
||||||
mentionLimit: 20,
|
mentionLimit: 20,
|
||||||
canInvite: false,
|
canInvite: false,
|
||||||
inviteLimit: 0,
|
inviteLimit: 0,
|
||||||
@ -374,6 +376,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||||||
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
||||||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||||
canPublicNote: calc('canPublicNote', 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)),
|
mentionLimit: calc('mentionLimit', vs => Math.max(...vs)),
|
||||||
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
|
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
|
||||||
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
|
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
|
||||||
|
@ -83,6 +83,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
|||||||
isExplorable: true,
|
isExplorable: true,
|
||||||
isHibernated: false,
|
isHibernated: false,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
|
requireSigninToViewContents: false,
|
||||||
emojis: [],
|
emojis: [],
|
||||||
score: 0,
|
score: 0,
|
||||||
host: null,
|
host: null,
|
||||||
@ -134,6 +135,7 @@ function generateDummyNote(override?: Partial<MiNote>): MiNote {
|
|||||||
replyUserHost: null,
|
replyUserHost: null,
|
||||||
renoteUserId: null,
|
renoteUserId: null,
|
||||||
renoteUserHost: null,
|
renoteUserHost: null,
|
||||||
|
updatedAt: null,
|
||||||
...override,
|
...override,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import { NotePiningService } from '@/core/NotePiningService.js';
|
|||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
||||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||||
|
import { NoteUpdateService } from '@/core/NoteUpdateService.js';
|
||||||
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import { AppLockService } from '@/core/AppLockService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
@ -74,6 +75,7 @@ export class ApInboxService {
|
|||||||
private notePiningService: NotePiningService,
|
private notePiningService: NotePiningService,
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
private noteCreateService: NoteCreateService,
|
private noteCreateService: NoteCreateService,
|
||||||
|
private noteUpdateService: NoteUpdateService,
|
||||||
private noteDeleteService: NoteDeleteService,
|
private noteDeleteService: NoteDeleteService,
|
||||||
private appLockService: AppLockService,
|
private appLockService: AppLockService,
|
||||||
private apResolverService: ApResolverService,
|
private apResolverService: ApResolverService,
|
||||||
@ -751,11 +753,13 @@ export class ApInboxService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async update(actor: MiRemoteUser, activity: IUpdate): Promise<string> {
|
private async update(actor: MiRemoteUser, activity: IUpdate): Promise<string> {
|
||||||
|
const uri = getApId(activity);
|
||||||
|
|
||||||
if (actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
return 'skip: invalid actor';
|
return 'skip: invalid actor';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug('Update');
|
this.logger.debug(`Update: ${uri}`);
|
||||||
|
|
||||||
const resolver = this.apResolverService.createResolver();
|
const resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
@ -767,6 +771,9 @@ export class ApInboxService {
|
|||||||
if (isActor(object)) {
|
if (isActor(object)) {
|
||||||
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
||||||
return 'ok: Person updated';
|
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') {
|
} else if (getApType(object) === 'Question') {
|
||||||
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
|
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
|
||||||
return 'ok: Question updated';
|
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
|
@bindThis
|
||||||
private async move(actor: MiRemoteUser, activity: IMove): Promise<string> {
|
private async move(actor: MiRemoteUser, activity: IMove): Promise<string> {
|
||||||
// fetch the new and old accounts
|
// fetch the new and old accounts
|
||||||
|
@ -108,6 +108,7 @@ export class ApRendererService {
|
|||||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||||
type: 'Announce',
|
type: 'Announce',
|
||||||
published: this.idService.parse(note.id).date.toISOString(),
|
published: this.idService.parse(note.id).date.toISOString(),
|
||||||
|
updated: note.updatedAt?.toISOString() ?? undefined,
|
||||||
to,
|
to,
|
||||||
cc,
|
cc,
|
||||||
object,
|
object,
|
||||||
@ -164,7 +165,7 @@ export class ApRendererService {
|
|||||||
return {
|
return {
|
||||||
type: 'Document',
|
type: 'Document',
|
||||||
mediaType: file.webpublicType ?? file.type,
|
mediaType: file.webpublicType ?? file.type,
|
||||||
url: this.driveFileEntityService.getPublicUrl(file),
|
url: this.driveFileEntityService.getPublicUrl(file, undefined, true),
|
||||||
name: file.comment,
|
name: file.comment,
|
||||||
sensitive: file.isSensitive,
|
sensitive: file.isSensitive,
|
||||||
};
|
};
|
||||||
@ -244,7 +245,7 @@ export class ApRendererService {
|
|||||||
public renderImage(file: MiDriveFile): IApImage {
|
public renderImage(file: MiDriveFile): IApImage {
|
||||||
return {
|
return {
|
||||||
type: 'Image',
|
type: 'Image',
|
||||||
url: this.driveFileEntityService.getPublicUrl(file),
|
url: this.driveFileEntityService.getPublicUrl(file, undefined, true),
|
||||||
sensitive: file.isSensitive,
|
sensitive: file.isSensitive,
|
||||||
name: file.comment,
|
name: file.comment,
|
||||||
};
|
};
|
||||||
@ -438,6 +439,7 @@ export class ApRendererService {
|
|||||||
_misskey_quote: quote,
|
_misskey_quote: quote,
|
||||||
quoteUrl: quote,
|
quoteUrl: quote,
|
||||||
published: this.idService.parse(note.id).date.toISOString(),
|
published: this.idService.parse(note.id).date.toISOString(),
|
||||||
|
updated: note.updatedAt?.toISOString() ?? undefined,
|
||||||
to,
|
to,
|
||||||
cc,
|
cc,
|
||||||
inReplyTo,
|
inReplyTo,
|
||||||
@ -495,6 +497,7 @@ export class ApRendererService {
|
|||||||
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
||||||
_misskey_summary: profile.description,
|
_misskey_summary: profile.description,
|
||||||
_misskey_followedMessage: profile.followedMessage,
|
_misskey_followedMessage: profile.followedMessage,
|
||||||
|
_misskey_requireSigninToViewContents: user.requireSigninToViewContents,
|
||||||
icon: avatar ? this.renderImage(avatar) : null,
|
icon: avatar ? this.renderImage(avatar) : null,
|
||||||
image: banner ? this.renderImage(banner) : null,
|
image: banner ? this.renderImage(banner) : null,
|
||||||
tag,
|
tag,
|
||||||
|
@ -555,6 +555,7 @@ const extension_context_definition = {
|
|||||||
'_misskey_votes': 'misskey:_misskey_votes',
|
'_misskey_votes': 'misskey:_misskey_votes',
|
||||||
'_misskey_summary': 'misskey:_misskey_summary',
|
'_misskey_summary': 'misskey:_misskey_summary',
|
||||||
'_misskey_followedMessage': 'misskey:_misskey_followedMessage',
|
'_misskey_followedMessage': 'misskey:_misskey_followedMessage',
|
||||||
|
'_misskey_requireSigninToViewContents': 'misskey:_misskey_requireSigninToViewContents',
|
||||||
'isCat': 'misskey:isCat',
|
'isCat': 'misskey:isCat',
|
||||||
// vcard
|
// vcard
|
||||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { Config } from '@/config.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
@ -36,6 +36,7 @@ import { ApQuestionService } from './ApQuestionService.js';
|
|||||||
import { ApImageService } from './ApImageService.js';
|
import { ApImageService } from './ApImageService.js';
|
||||||
import type { Resolver } from '../ApResolverService.js';
|
import type { Resolver } from '../ApResolverService.js';
|
||||||
import type { IObject, IPost } from '../type.js';
|
import type { IObject, IPost } from '../type.js';
|
||||||
|
import { NoteUpdateService } from '@/core/NoteUpdateService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApNoteService {
|
export class ApNoteService {
|
||||||
@ -54,6 +55,9 @@ export class ApNoteService {
|
|||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
@Inject(DI.notesRepository)
|
||||||
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private apMfmService: ApMfmService,
|
private apMfmService: ApMfmService,
|
||||||
private apResolverService: ApResolverService,
|
private apResolverService: ApResolverService,
|
||||||
@ -70,6 +74,7 @@ export class ApNoteService {
|
|||||||
private appLockService: AppLockService,
|
private appLockService: AppLockService,
|
||||||
private pollService: PollService,
|
private pollService: PollService,
|
||||||
private noteCreateService: NoteCreateService,
|
private noteCreateService: NoteCreateService,
|
||||||
|
private noteUpdateService: NoteUpdateService,
|
||||||
private apDbResolverService: ApDbResolverService,
|
private apDbResolverService: ApDbResolverService,
|
||||||
private apLoggerService: ApLoggerService,
|
private apLoggerService: ApLoggerService,
|
||||||
) {
|
) {
|
||||||
@ -297,6 +302,7 @@ export class ApNoteService {
|
|||||||
try {
|
try {
|
||||||
return await this.noteCreateService.create(actor, {
|
return await this.noteCreateService.create(actor, {
|
||||||
createdAt: note.published ? new Date(note.published) : null,
|
createdAt: note.published ? new Date(note.published) : null,
|
||||||
|
updatedAt: note.updated ? new Date(note.updated) : null,
|
||||||
files,
|
files,
|
||||||
reply,
|
reply,
|
||||||
renote: quote,
|
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を解決します。
|
* Noteを解決します。
|
||||||
*
|
*
|
||||||
|
@ -256,12 +256,12 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
return {
|
return {
|
||||||
...( avatar ? {
|
...( avatar ? {
|
||||||
avatarId: avatar.id,
|
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,
|
avatarBlurhash: avatar.blurhash,
|
||||||
} : {}),
|
} : {}),
|
||||||
...( banner ? {
|
...( banner ? {
|
||||||
bannerId: banner.id,
|
bannerId: banner.id,
|
||||||
bannerUrl: banner.url ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
bannerUrl: banner.url ? this.driveFileEntityService.getPublicUrl(banner, undefined, true) : null,
|
||||||
bannerBlurhash: banner.blurhash,
|
bannerBlurhash: banner.blurhash,
|
||||||
} : {}),
|
} : {}),
|
||||||
};
|
};
|
||||||
@ -356,6 +356,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
tags,
|
tags,
|
||||||
isBot,
|
isBot,
|
||||||
isCat: (person as any).isCat === true,
|
isCat: (person as any).isCat === true,
|
||||||
|
requireSigninToViewContents: (person as any).requireSigninToViewContents === true,
|
||||||
emojis,
|
emojis,
|
||||||
})) as MiRemoteUser;
|
})) as MiRemoteUser;
|
||||||
|
|
||||||
|
@ -14,7 +14,9 @@ export interface IObject {
|
|||||||
summary?: string;
|
summary?: string;
|
||||||
_misskey_summary?: string;
|
_misskey_summary?: string;
|
||||||
_misskey_followedMessage?: string | null;
|
_misskey_followedMessage?: string | null;
|
||||||
|
_misskey_requireSigninToViewContents?: boolean;
|
||||||
published?: string;
|
published?: string;
|
||||||
|
updated?: string;
|
||||||
cc?: ApObject;
|
cc?: ApObject;
|
||||||
to?: ApObject;
|
to?: ApObject;
|
||||||
attributedTo?: ApObject;
|
attributedTo?: ApObject;
|
||||||
|
@ -108,7 +108,7 @@ export class DriveFileEntityService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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) {
|
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
||||||
return this.getProxiedUrl(file.uri, mode);
|
return this.getProxiedUrl(file.uri, mode);
|
||||||
@ -130,6 +130,16 @@ export class DriveFileEntityService {
|
|||||||
if (mode === 'avatar') {
|
if (mode === 'avatar') {
|
||||||
return this.getProxiedUrl(url, '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;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +149,10 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (packedNote.user.requireSigninToViewContents && meId == null) {
|
||||||
|
hide = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (hide) {
|
if (hide) {
|
||||||
packedNote.visibleUserIds = undefined;
|
packedNote.visibleUserIds = undefined;
|
||||||
packedNote.fileIds = [];
|
packedNote.fileIds = [];
|
||||||
@ -365,6 +369,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
const packed: Packed<'Note'> = await awaitAll({
|
const packed: Packed<'Note'> = await awaitAll({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
createdAt: this.idService.parse(note.id).date.toISOString(),
|
createdAt: this.idService.parse(note.id).date.toISOString(),
|
||||||
|
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
|
||||||
userId: note.userId,
|
userId: note.userId,
|
||||||
user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me),
|
user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me),
|
||||||
text: text,
|
text: text,
|
||||||
|
@ -490,6 +490,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
}))) : [],
|
}))) : [],
|
||||||
isBot: user.isBot,
|
isBot: user.isBot,
|
||||||
isCat: user.isCat,
|
isCat: user.isCat,
|
||||||
|
requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true,
|
||||||
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
||||||
name: instance.name,
|
name: instance.name,
|
||||||
softwareName: instance.softwareName,
|
softwareName: instance.softwareName,
|
||||||
|
@ -229,6 +229,11 @@ export class MiNote {
|
|||||||
comment: '[Denormalized]',
|
comment: '[Denormalized]',
|
||||||
})
|
})
|
||||||
public renoteUserHost: string | null;
|
public renoteUserHost: string | null;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
public updatedAt: Date | null;
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
constructor(data: Partial<MiNote>) {
|
constructor(data: Partial<MiNote>) {
|
||||||
|
@ -202,6 +202,11 @@ export class MiUser {
|
|||||||
})
|
})
|
||||||
public isHibernated: boolean;
|
public isHibernated: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public requireSigninToViewContents: boolean;
|
||||||
|
|
||||||
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
|
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -17,6 +17,11 @@ export const packedNoteSchema = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
deletedAt: {
|
deletedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
|
@ -180,6 +180,10 @@ export const packedRolePoliciesSchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
canEditNote: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
mentionLimit: {
|
mentionLimit: {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -115,6 +115,10 @@ export const packedUserLiteSchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
},
|
},
|
||||||
|
requireSigninToViewContents: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
},
|
||||||
instance: {
|
instance: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
|
@ -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_clips from './endpoints/notes/clips.js';
|
||||||
import * as ep___notes_conversation from './endpoints/notes/conversation.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_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_delete from './endpoints/notes/delete.js';
|
||||||
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.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';
|
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_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_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_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_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_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 };
|
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_clips,
|
||||||
$notes_conversation,
|
$notes_conversation,
|
||||||
$notes_create,
|
$notes_create,
|
||||||
|
$notes_update,
|
||||||
$notes_delete,
|
$notes_delete,
|
||||||
$notes_favorites_create,
|
$notes_favorites_create,
|
||||||
$notes_favorites_delete,
|
$notes_favorites_delete,
|
||||||
@ -1445,6 +1448,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||||||
$notes_clips,
|
$notes_clips,
|
||||||
$notes_conversation,
|
$notes_conversation,
|
||||||
$notes_create,
|
$notes_create,
|
||||||
|
$notes_update,
|
||||||
$notes_delete,
|
$notes_delete,
|
||||||
$notes_favorites_create,
|
$notes_favorites_create,
|
||||||
$notes_favorites_delete,
|
$notes_favorites_delete,
|
||||||
|
@ -39,6 +39,17 @@ export class GetterService {
|
|||||||
return note;
|
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
|
* Get user for API processing
|
||||||
*/
|
*/
|
||||||
|
@ -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_clips from './endpoints/notes/clips.js';
|
||||||
import * as ep___notes_conversation from './endpoints/notes/conversation.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_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_delete from './endpoints/notes/delete.js';
|
||||||
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.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';
|
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
||||||
@ -673,6 +674,7 @@ const eps = [
|
|||||||
['notes/clips', ep___notes_clips],
|
['notes/clips', ep___notes_clips],
|
||||||
['notes/conversation', ep___notes_conversation],
|
['notes/conversation', ep___notes_conversation],
|
||||||
['notes/create', ep___notes_create],
|
['notes/create', ep___notes_create],
|
||||||
|
['notes/update', ep___notes_update],
|
||||||
['notes/delete', ep___notes_delete],
|
['notes/delete', ep___notes_delete],
|
||||||
['notes/favorites/create', ep___notes_favorites_create],
|
['notes/favorites/create', ep___notes_favorites_create],
|
||||||
['notes/favorites/delete', ep___notes_favorites_delete],
|
['notes/favorites/delete', ep___notes_favorites_delete],
|
||||||
|
@ -179,6 +179,7 @@ export const paramDef = {
|
|||||||
autoAcceptFollowed: { type: 'boolean' },
|
autoAcceptFollowed: { type: 'boolean' },
|
||||||
noCrawle: { type: 'boolean' },
|
noCrawle: { type: 'boolean' },
|
||||||
preventAiLearning: { type: 'boolean' },
|
preventAiLearning: { type: 'boolean' },
|
||||||
|
requireSigninToViewContents: { type: 'boolean' },
|
||||||
isBot: { type: 'boolean' },
|
isBot: { type: 'boolean' },
|
||||||
isCat: { type: 'boolean' },
|
isCat: { type: 'boolean' },
|
||||||
injectFeaturedNote: { 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.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
|
||||||
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
|
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
|
||||||
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
|
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.isCat === 'boolean') updates.isCat = ps.isCat;
|
||||||
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
||||||
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
||||||
|
@ -26,6 +26,12 @@ export const meta = {
|
|||||||
code: 'NO_SUCH_NOTE',
|
code: 'NO_SUCH_NOTE',
|
||||||
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
|
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
signinRequired: {
|
||||||
|
message: 'Signin required.',
|
||||||
|
code: 'SIGNIN_REQUIRED',
|
||||||
|
id: '8e75455b-738c-471d-9f80-62693f33372e',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -44,11 +50,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
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);
|
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (note.user!.requireSigninToViewContents && me == null) {
|
||||||
|
throw new ApiError(meta.errors.signinRequired);
|
||||||
|
}
|
||||||
|
|
||||||
return await this.noteEntityService.pack(note, me, {
|
return await this.noteEntityService.pack(note, me, {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
|
165
packages/backend/src/server/api/endpoints/notes/update.ts
Normal 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),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -42,6 +42,12 @@ export const meta = {
|
|||||||
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
|
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
|
||||||
id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222',
|
id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
signinRequired: {
|
||||||
|
message: 'Signin required.',
|
||||||
|
code: 'SIGNIN_REQUIRED',
|
||||||
|
id: 'd1588a9e-4b4d-4c07-807f-16f1486577a2',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -141,27 +141,34 @@ export class ClientServerService {
|
|||||||
'name': this.meta.name || this.config.host,
|
'name': this.meta.name || this.config.host,
|
||||||
'start_url': '/',
|
'start_url': '/',
|
||||||
'display': 'standalone',
|
'display': 'standalone',
|
||||||
'background_color': '#313a42',
|
'background_color': '#1b1a25',
|
||||||
// 空文字列の場合右辺を使いたいため
|
// 空文字列の場合右辺を使いたいため
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
'theme_color': this.meta.themeColor || '#86b300',
|
'theme_color': this.meta.themeColor || '#b8b9f7',
|
||||||
'icons': [{
|
'icons': [{
|
||||||
// 空文字列の場合右辺を使いたいため
|
// 空文字列の場合右辺を使いたいため
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
'src': this.meta.app192IconUrl || '/static-assets/icons/192.png',
|
'src': '/static-assets/icons/None-512.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',
|
|
||||||
'sizes': '512x512',
|
'sizes': '512x512',
|
||||||
'type': 'image/png',
|
'type': 'image/png',
|
||||||
'purpose': 'maskable',
|
'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',
|
'src': '/static-assets/splash.png',
|
||||||
'sizes': '300x300',
|
'sizes': '1024x1024',
|
||||||
'type': 'image/png',
|
'type': 'image/png',
|
||||||
'purpose': 'any',
|
'purpose': 'any',
|
||||||
}],
|
}],
|
||||||
@ -191,7 +198,7 @@ export class ClientServerService {
|
|||||||
return {
|
return {
|
||||||
instanceName: meta.name ?? 'Misskey',
|
instanceName: meta.name ?? 'Misskey',
|
||||||
icon: meta.iconUrl,
|
icon: meta.iconUrl,
|
||||||
appleTouchIcon: meta.app512IconUrl,
|
appleTouchIcon: '/apple-touch-icon.png',
|
||||||
themeColor: meta.themeColor,
|
themeColor: meta.themeColor,
|
||||||
serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
|
serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
|
||||||
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.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) => {
|
fastify.get<{ Params: { note: string; } }>('/notes/:note', async (request, reply) => {
|
||||||
vary(reply.raw, 'Accept');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
||||||
const note = await this.notesRepository.findOneBy({
|
const note = await this.notesRepository.findOne({
|
||||||
|
where: {
|
||||||
id: request.params.note,
|
id: request.params.note,
|
||||||
visibility: In(['public', 'home']),
|
visibility: In(['public', 'home']),
|
||||||
|
},
|
||||||
|
relations: ['user'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (note) {
|
if (note && !note.user!.requireSigninToViewContents) {
|
||||||
const _note = await this.noteEntityService.pack(note);
|
const _note = await this.noteEntityService.pack(note);
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
|
||||||
reply.header('Cache-Control', 'public, max-age=15');
|
reply.header('Cache-Control', 'public, max-age=15');
|
||||||
|
@ -4,16 +4,24 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 {
|
html {
|
||||||
background: #ffb4e1;
|
background: rgb(27, 26, 37);
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
background: #dedede;
|
background: rgb(33, 32, 41);
|
||||||
}
|
}
|
||||||
main > .tabs {
|
main > .tabs {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
@ -169,7 +169,7 @@
|
|||||||
<p>Disable an adblocker / アドブロッカーを無効にする</p>
|
<p>Disable an adblocker / アドブロッカーを無効にする</p>
|
||||||
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
|
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
|
||||||
<p>(Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
|
<p>(Tor Browser) 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>
|
<summary>Other options / その他のオプション</summary>
|
||||||
<a href="/flush">
|
<a href="/flush">
|
||||||
<button class="button-small">
|
<button class="button-small">
|
||||||
@ -204,8 +204,12 @@
|
|||||||
<code>${details.toString()} ${JSON.stringify(details)}</code>`;
|
<code>${details.toString()} ${JSON.stringify(details)}</code>`;
|
||||||
errorsElement.appendChild(detailsElement);
|
errorsElement.appendChild(detailsElement);
|
||||||
addStyle(`
|
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,
|
#misskey_app,
|
||||||
@ -215,8 +219,8 @@
|
|||||||
|
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
background-color: #222;
|
background-color: rgb(27, 26, 37);
|
||||||
color: #dfddcc;
|
color: rgb(242, 238, 252);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@ -232,21 +236,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button-big {
|
.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;
|
line-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-big:hover {
|
.button-big:hover {
|
||||||
background: rgb(153, 204, 0);
|
background: rgb(230, 230, 252);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-small {
|
.button-small {
|
||||||
background: #444;
|
background: transparent;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-small:hover {
|
.button-small:hover {
|
||||||
background: #555;
|
background: rgba(184, 185, 247, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-label-big {
|
.button-label-big {
|
||||||
@ -257,7 +261,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button-label-small {
|
.button-label-small {
|
||||||
color: rgb(153, 204, 0);
|
color: rgba(184, 185, 247);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
@ -278,17 +282,23 @@
|
|||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body > details > summary {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
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 {
|
#errorInfo {
|
||||||
background: #333;
|
background: #272822;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
width: 40rem;
|
width: 40rem;
|
||||||
|
@ -4,16 +4,24 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 {
|
html {
|
||||||
background: #ffb4e1;
|
background: rgb(27, 26, 37);
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
background: #dedede;
|
background: rgb(33, 32, 41);
|
||||||
}
|
}
|
||||||
|
|
||||||
#tl > div {
|
#tl > div {
|
||||||
|
@ -3,24 +3,30 @@
|
|||||||
"name": "Misskey",
|
"name": "Misskey",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#313a42",
|
"background_color": "#1b1a25",
|
||||||
"theme_color": "#86b300",
|
"theme_color": "#b8b9f7",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/static-assets/icons/192.png",
|
"src": "/static-assets/icons/None-512.png",
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/static-assets/icons/512.png",
|
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable"
|
"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",
|
"src": "/static-assets/splash.png",
|
||||||
"sizes": "300x300",
|
"sizes": "1024x1024",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any"
|
"purpose": "any"
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,7 @@ describe('NoteCreateService', () => {
|
|||||||
replyUserHost: null,
|
replyUserHost: null,
|
||||||
renoteUserId: null,
|
renoteUserId: null,
|
||||||
renoteUserHost: null,
|
renoteUserHost: null,
|
||||||
|
updatedAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const poll: IPoll = {
|
const poll: IPoll = {
|
||||||
|
@ -43,6 +43,7 @@ const base: MiNote = {
|
|||||||
replyUserHost: null,
|
replyUserHost: null,
|
||||||
renoteUserId: null,
|
renoteUserId: null,
|
||||||
renoteUserHost: null,
|
renoteUserHost: null,
|
||||||
|
updatedAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('misc:is-renote', () => {
|
describe('misc:is-renote', () => {
|
||||||
|
@ -78,6 +78,7 @@ export const ROLE_POLICIES = [
|
|||||||
'gtlAvailable',
|
'gtlAvailable',
|
||||||
'ltlAvailable',
|
'ltlAvailable',
|
||||||
'canPublicNote',
|
'canPublicNote',
|
||||||
|
'canEditNote',
|
||||||
'mentionLimit',
|
'mentionLimit',
|
||||||
'canInvite',
|
'canInvite',
|
||||||
'inviteLimit',
|
'inviteLimit',
|
||||||
|
94
packages/frontend-shared/themes/d-byeolvit-noctiluca.json5
Normal 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',
|
||||||
|
}
|
92
packages/frontend-shared/themes/d-scone-color.json5
Normal 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',
|
||||||
|
}
|
19
packages/frontend-shared/themes/d-stella-r2.json5
Normal 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)',
|
||||||
|
},
|
||||||
|
}
|
94
packages/frontend-shared/themes/l-byeolvit-polaris.json5
Normal 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',
|
||||||
|
}
|
91
packages/frontend-shared/themes/l-scone-color.json5
Normal 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',
|
||||||
|
}
|
19
packages/frontend-shared/themes/l-stella-r2.json5
Normal 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)',
|
||||||
|
},
|
||||||
|
}
|
@ -18,6 +18,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordapp/twemoji": "15.1.0",
|
"@discordapp/twemoji": "15.1.0",
|
||||||
|
"@fontsource/jetbrains-mono": "^5.0.21",
|
||||||
"@github/webauthn-json": "2.1.1",
|
"@github/webauthn-json": "2.1.1",
|
||||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||||
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
||||||
@ -56,6 +57,8 @@
|
|||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"misskey-reversi": "workspace:*",
|
"misskey-reversi": "workspace:*",
|
||||||
"photoswipe": "5.4.4",
|
"photoswipe": "5.4.4",
|
||||||
|
"pretendard": "^1.3.9",
|
||||||
|
"pretendard-jp": "^1.3.9",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"rollup": "4.22.5",
|
"rollup": "4.22.5",
|
||||||
"sanitize-html": "2.13.1",
|
"sanitize-html": "2.13.1",
|
||||||
|
@ -5,12 +5,12 @@
|
|||||||
|
|
||||||
import { defineAsyncComponent, reactive, ref } from 'vue';
|
import { defineAsyncComponent, reactive, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
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 { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { miLocalStorage } from '@/local-storage.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 { del, get, set } from '@/scripts/idb-proxy.js';
|
||||||
import { apiUrl } from '@@/js/config.js';
|
|
||||||
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
|
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.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;
|
if (!$i) return;
|
||||||
for (const [key, value] of Object.entries(accountData)) {
|
for (const [key, value] of Object.entries(accountData)) {
|
||||||
$i[key] = value;
|
$i[key] = value;
|
||||||
|
@ -4,14 +4,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createApp, defineAsyncComponent, markRaw } from 'vue';
|
import { createApp, defineAsyncComponent, markRaw } from 'vue';
|
||||||
|
import { ui } from '@@/js/config.js';
|
||||||
import { common } from './common.js';
|
import { common } from './common.js';
|
||||||
import type * as Misskey from 'misskey-js';
|
import type * as Misskey from 'misskey-js';
|
||||||
import { ui } from '@@/js/config.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { alert, confirm, popup, post, toast } from '@/os.js';
|
import { alert, confirm, popup, post, toast } from '@/os.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import * as sound from '@/scripts/sound.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 { instance } from '@/instance.js';
|
||||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||||
@ -291,11 +291,11 @@ export async function mainBoot() {
|
|||||||
|
|
||||||
// 自分の情報が更新されたとき
|
// 自分の情報が更新されたとき
|
||||||
main.on('meUpdated', i => {
|
main.on('meUpdated', i => {
|
||||||
updateAccount(i);
|
updateAccountPartial(i);
|
||||||
});
|
});
|
||||||
|
|
||||||
main.on('readAllNotifications', () => {
|
main.on('readAllNotifications', () => {
|
||||||
updateAccount({
|
updateAccountPartial({
|
||||||
hasUnreadNotification: false,
|
hasUnreadNotification: false,
|
||||||
unreadNotificationsCount: 0,
|
unreadNotificationsCount: 0,
|
||||||
});
|
});
|
||||||
@ -303,39 +303,39 @@ export async function mainBoot() {
|
|||||||
|
|
||||||
main.on('unreadNotification', () => {
|
main.on('unreadNotification', () => {
|
||||||
const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
|
const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
|
||||||
updateAccount({
|
updateAccountPartial({
|
||||||
hasUnreadNotification: true,
|
hasUnreadNotification: true,
|
||||||
unreadNotificationsCount,
|
unreadNotificationsCount,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
main.on('unreadMention', () => {
|
main.on('unreadMention', () => {
|
||||||
updateAccount({ hasUnreadMentions: true });
|
updateAccountPartial({ hasUnreadMentions: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
main.on('readAllUnreadMentions', () => {
|
main.on('readAllUnreadMentions', () => {
|
||||||
updateAccount({ hasUnreadMentions: false });
|
updateAccountPartial({ hasUnreadMentions: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
main.on('unreadSpecifiedNote', () => {
|
main.on('unreadSpecifiedNote', () => {
|
||||||
updateAccount({ hasUnreadSpecifiedNotes: true });
|
updateAccountPartial({ hasUnreadSpecifiedNotes: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
main.on('readAllUnreadSpecifiedNotes', () => {
|
main.on('readAllUnreadSpecifiedNotes', () => {
|
||||||
updateAccount({ hasUnreadSpecifiedNotes: false });
|
updateAccountPartial({ hasUnreadSpecifiedNotes: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
main.on('readAllAntennas', () => {
|
main.on('readAllAntennas', () => {
|
||||||
updateAccount({ hasUnreadAntenna: false });
|
updateAccountPartial({ hasUnreadAntenna: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
main.on('unreadAntenna', () => {
|
main.on('unreadAntenna', () => {
|
||||||
updateAccount({ hasUnreadAntenna: true });
|
updateAccountPartial({ hasUnreadAntenna: true });
|
||||||
sound.playMisskeySfx('antenna');
|
sound.playMisskeySfx('antenna');
|
||||||
});
|
});
|
||||||
|
|
||||||
main.on('readAllAnnouncements', () => {
|
main.on('readAllAnnouncements', () => {
|
||||||
updateAccount({ hasUnreadAnnouncement: false });
|
updateAccountPartial({ hasUnreadAnnouncement: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 個人宛てお知らせが発行されたとき
|
// 個人宛てお知らせが発行されたとき
|
||||||
|
@ -29,7 +29,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i, updateAccount } from '@/account.js';
|
import { $i, updateAccountPartial } from '@/account.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
announcement: Misskey.entities.Announcement;
|
announcement: Misskey.entities.Announcement;
|
||||||
@ -51,7 +51,7 @@ async function ok() {
|
|||||||
|
|
||||||
modal.value?.close();
|
modal.value?.close();
|
||||||
misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
|
misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
|
||||||
updateAccount({
|
updateAccountPartial({
|
||||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
|
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -161,7 +161,7 @@ function openPostForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fontMonospace {
|
.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 {
|
.postForm {
|
||||||
|
@ -117,8 +117,8 @@ async function requestRender() {
|
|||||||
sitekey: props.sitekey,
|
sitekey: props.sitekey,
|
||||||
theme: defaultStore.state.darkMode ? 'dark' : 'light',
|
theme: defaultStore.state.darkMode ? 'dark' : 'light',
|
||||||
callback: callback,
|
callback: callback,
|
||||||
'expired-callback': callback,
|
'expired-callback': () => callback(undefined),
|
||||||
'error-callback': callback,
|
'error-callback': () => callback(undefined),
|
||||||
});
|
});
|
||||||
} else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) {
|
} else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) {
|
||||||
const { default: Widget } = await import('@mcaptcha/vanilla-glue');
|
const { default: Widget } = await import('@mcaptcha/vanilla-glue');
|
||||||
|
@ -78,7 +78,7 @@ watch(() => props.lang, (to) => {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--MI_THEME-divider);
|
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);
|
color: var(--shiki-fallback);
|
||||||
background-color: var(--shiki-fallback-bg);
|
background-color: var(--shiki-fallback-bg);
|
||||||
@ -90,7 +90,7 @@ watch(() => props.lang, (to) => {
|
|||||||
|
|
||||||
& pre,
|
& pre,
|
||||||
& code {
|
& code {
|
||||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
font-family: "JetBrains Mono", "Pretendard JP", Pretendard, Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ function copy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.codeBlockFallbackCode {
|
.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 {
|
.codePlaceholderRoot {
|
||||||
|
@ -163,7 +163,7 @@ watch(v, newValue => {
|
|||||||
color: var(--MI_THEME-fg);
|
color: var(--MI_THEME-fg);
|
||||||
border: solid 1px var(--MI_THEME-panel);
|
border: solid 1px var(--MI_THEME-panel);
|
||||||
transition: border-color 0.1s ease-out;
|
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 {
|
&:hover {
|
||||||
border-color: var(--MI_THEME-inputBorderHover) !important;
|
border-color: var(--MI_THEME-inputBorderHover) !important;
|
||||||
}
|
}
|
||||||
@ -207,7 +207,7 @@ watch(v, newValue => {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
font-size: 1em;
|
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 {
|
.textarea::selection {
|
||||||
|
@ -16,7 +16,7 @@ const props = defineProps<{
|
|||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.root {
|
.root {
|
||||||
display: inline-block;
|
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;
|
overflow-wrap: anywhere;
|
||||||
background: var(--MI_THEME-bg);
|
background: var(--MI_THEME-bg);
|
||||||
padding: .1em;
|
padding: .1em;
|
||||||
|
@ -37,13 +37,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { host } from '@@/js/config.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
import { host } from '@@/js/config.js';
|
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onClick() {
|
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;
|
wait.value = true;
|
||||||
|
|
||||||
|
@ -227,6 +227,7 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inTimeline = inject<boolean>('inTimeline', false);
|
const inTimeline = inject<boolean>('inTimeline', false);
|
||||||
|
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(false));
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject('inChannel', null);
|
||||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', 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 (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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,7 +420,7 @@ if (!props.mock) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renote(viaKeyboard = false) {
|
function renote(viaKeyboard = false) {
|
||||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
|
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
|
||||||
@ -429,7 +430,7 @@ function renote(viaKeyboard = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reply(): void {
|
function reply(): void {
|
||||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -442,7 +443,7 @@ function reply(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function react(): void {
|
function react(): void {
|
||||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
@ -563,7 +564,7 @@ function showRenoteMenu(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMyRenote) {
|
if (isMyRenote) {
|
||||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
os.popupMenu([
|
os.popupMenu([
|
||||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
|
@ -207,6 +207,7 @@ import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue';
|
|||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { isLink } from '@@/js/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
|
import { host } from '@@/js/config.js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
import MkReactionsViewer from '@/components/MkReactionsViewer.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 { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { host } from '@@/js/config.js';
|
|
||||||
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
|
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
@ -404,7 +404,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renote() {
|
function renote() {
|
||||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
|
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
|
||||||
@ -412,7 +412,7 @@ function renote() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reply(): void {
|
function reply(): void {
|
||||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
os.post({
|
os.post({
|
||||||
reply: appearNote.value,
|
reply: appearNote.value,
|
||||||
@ -423,7 +423,7 @@ function reply(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function react(): void {
|
function react(): void {
|
||||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
@ -499,7 +499,7 @@ async function clip(): Promise<void> {
|
|||||||
|
|
||||||
function showRenoteMenu(): void {
|
function showRenoteMenu(): void {
|
||||||
if (!isMyRenote) return;
|
if (!isMyRenote) return;
|
||||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: i18n.ts.unrenote,
|
text: i18n.ts.unrenote,
|
||||||
icon: 'ti ti-trash',
|
icon: 'ti ti-trash',
|
||||||
|
@ -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!"/>
|
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.info">
|
<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">
|
<div v-if="mock">
|
||||||
<MkTime :time="note.createdAt" colored/>
|
<MkTime :time="note.createdAt" colored/>
|
||||||
</div>
|
</div>
|
||||||
<MkA v-else :to="notePage(note)">
|
<MkA v-else :to="notePage(note)">
|
||||||
<MkTime :time="note.createdAt" colored/>
|
<MkTime :time="note.createdAt" colored/>
|
||||||
</MkA>
|
</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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
@ -5,11 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<MkAvatar :class="$style.avatar" :user="user"/>
|
|
||||||
<div :class="$style.main">
|
<div :class="$style.main">
|
||||||
<div :class="$style.header">
|
|
||||||
<MkUserName :user="user" :nowrap="true"/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<p v-if="useCw" :class="$style.cw">
|
<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;"/>
|
<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;
|
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 {
|
.main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@ -80,20 +66,4 @@ const props = defineProps<{
|
|||||||
overflow: clip;
|
overflow: clip;
|
||||||
text-overflow: ellipsis;
|
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>
|
</style>
|
||||||
|
@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
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 type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||||
import { sum } from '@/scripts/array.js';
|
import { sum } from '@/scripts/array.js';
|
||||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { host } from '@@/js/config.js';
|
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
noteId: string;
|
noteId: string;
|
||||||
@ -85,7 +85,7 @@ if (props.poll.expiresAt) {
|
|||||||
const vote = async (id) => {
|
const vote = async (id) => {
|
||||||
if (props.readOnly || closed.value || isVoted.value) return;
|
if (props.readOnly || closed.value || isVoted.value) return;
|
||||||
|
|
||||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
|
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
|
@ -65,10 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
|
<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 :class="[$style.textOuter, { [$style.withCw]: useCw }]">
|
||||||
<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
|
<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 v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||||
</div>
|
</div>
|
||||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
<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;
|
fixed?: boolean;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
freezeAfterPosted?: boolean;
|
freezeAfterPosted?: boolean;
|
||||||
|
updateMode?: boolean;
|
||||||
mock?: boolean;
|
mock?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
initialVisibleUsers: () => [],
|
initialVisibleUsers: () => [],
|
||||||
@ -201,6 +202,7 @@ const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'
|
|||||||
const imeText = ref('');
|
const imeText = ref('');
|
||||||
const showingOptions = ref(false);
|
const showingOptions = ref(false);
|
||||||
const textAreaReadOnly = ref(false);
|
const textAreaReadOnly = ref(false);
|
||||||
|
const justEndedComposition = ref(false);
|
||||||
|
|
||||||
const draftKey = computed((): string => {
|
const draftKey = computed((): string => {
|
||||||
let key = props.channel ? `channel:${props.channel.id}` : '';
|
let key = props.channel ? `channel:${props.channel.id}` : '';
|
||||||
@ -573,7 +575,13 @@ function clear() {
|
|||||||
function onKeydown(ev: KeyboardEvent) {
|
function onKeydown(ev: KeyboardEvent) {
|
||||||
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
|
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) {
|
function onCompositionUpdate(ev: CompositionEvent) {
|
||||||
@ -582,6 +590,7 @@ function onCompositionUpdate(ev: CompositionEvent) {
|
|||||||
|
|
||||||
function onCompositionEnd(ev: CompositionEvent) {
|
function onCompositionEnd(ev: CompositionEvent) {
|
||||||
imeText.value = '';
|
imeText.value = '';
|
||||||
|
justEndedComposition.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onPaste(ev: ClipboardEvent) {
|
async function onPaste(ev: ClipboardEvent) {
|
||||||
@ -707,6 +716,7 @@ function saveDraft() {
|
|||||||
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
|
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
|
||||||
quoteId: quoteId.value,
|
quoteId: quoteId.value,
|
||||||
reactionAcceptance: reactionAcceptance.value,
|
reactionAcceptance: reactionAcceptance.value,
|
||||||
|
noteId: props.updateMode ? props.initialNote?.id : undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -788,6 +798,7 @@ async function post(ev?: MouseEvent) {
|
|||||||
visibility: visibility.value,
|
visibility: visibility.value,
|
||||||
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
|
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
|
||||||
reactionAcceptance: reactionAcceptance.value,
|
reactionAcceptance: reactionAcceptance.value,
|
||||||
|
noteId: props.updateMode ? props.initialNote?.id : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') {
|
if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') {
|
||||||
@ -824,7 +835,7 @@ async function post(ev?: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
posting.value = true;
|
posting.value = true;
|
||||||
misskeyApi('notes/create', postData, token).then(() => {
|
misskeyApi(props.updateMode ? 'notes/update' : 'notes/create', postData, token).then(() => {
|
||||||
if (props.freezeAfterPosted) {
|
if (props.freezeAfterPosted) {
|
||||||
posted.value = true;
|
posted.value = true;
|
||||||
} else {
|
} else {
|
||||||
@ -1055,7 +1066,7 @@ defineExpose({
|
|||||||
|
|
||||||
&.modal {
|
&.modal {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 520px;
|
max-width: 750px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1198,7 +1209,7 @@ defineExpose({
|
|||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
padding: 16px 20px 0 20px;
|
padding: 16px 20px 0 20px;
|
||||||
min-height: 75px;
|
min-height: auto;
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background-size: auto auto;
|
background-size: auto auto;
|
||||||
|
@ -31,6 +31,7 @@ const props = withDefaults(defineProps<{
|
|||||||
instant?: boolean;
|
instant?: boolean;
|
||||||
fixed?: boolean;
|
fixed?: boolean;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
|
updateMode?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
initialLocalOnly: undefined,
|
initialLocalOnly: undefined,
|
||||||
});
|
});
|
||||||
|
@ -16,9 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
@keydown.space.enter="show"
|
@keydown.space.enter="show"
|
||||||
>
|
>
|
||||||
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
|
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
|
||||||
<select
|
<div
|
||||||
ref="inputEl"
|
ref="inputEl"
|
||||||
v-model="v"
|
|
||||||
v-adaptive-border
|
v-adaptive-border
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
:class="$style.inputCore"
|
:class="$style.inputCore"
|
||||||
@ -26,27 +25,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
:required="required"
|
:required="required"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@input="onInput"
|
|
||||||
@mousedown.prevent="() => {}"
|
@mousedown.prevent="() => {}"
|
||||||
@keydown.prevent="() => {}"
|
@keydown.prevent="() => {}"
|
||||||
>
|
>
|
||||||
|
<div style="pointer-events: none;">{{ currentValueText ?? '' }}</div>
|
||||||
|
<div style="display: none;">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</select>
|
</div>
|
||||||
|
</div>
|
||||||
<div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
|
<div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.caption"><slot name="caption"></slot></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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue';
|
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 { useInterval } from '@@/js/use-interval.js';
|
||||||
import { i18n } from '@/i18n.js';
|
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string | null;
|
modelValue: string | null;
|
||||||
@ -56,25 +53,20 @@ const props = defineProps<{
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
manualSave?: boolean;
|
|
||||||
small?: boolean;
|
small?: boolean;
|
||||||
large?: boolean;
|
large?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'changeByUser', value: string | null): void;
|
(ev: 'update:modelValue', value: string | number | null): void;
|
||||||
(ev: 'update:modelValue', value: string | null): void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
|
|
||||||
const { modelValue, autofocus } = toRefs(props);
|
const { modelValue, autofocus } = toRefs(props);
|
||||||
const v = ref(modelValue.value);
|
|
||||||
const focused = ref(false);
|
const focused = ref(false);
|
||||||
const opening = ref(false);
|
const opening = ref(false);
|
||||||
const changed = ref(false);
|
const currentValueText = ref<string | null>(null);
|
||||||
const invalid = ref(false);
|
|
||||||
const filled = computed(() => v.value !== '' && v.value != null);
|
|
||||||
const inputEl = ref<HTMLObjectElement | null>(null);
|
const inputEl = ref<HTMLObjectElement | null>(null);
|
||||||
const prefixEl = ref<HTMLElement | null>(null);
|
const prefixEl = ref<HTMLElement | null>(null);
|
||||||
const suffixEl = ref<HTMLElement | null>(null);
|
const suffixEl = ref<HTMLElement | null>(null);
|
||||||
@ -85,26 +77,6 @@ const height =
|
|||||||
36;
|
36;
|
||||||
|
|
||||||
const focus = () => container.value?.focus();
|
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になってしまうので、定期的に計算する
|
// 非表示状態だと要素の幅などは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() {
|
function show() {
|
||||||
if (opening.value) return;
|
if (opening.value) return;
|
||||||
focus();
|
focus();
|
||||||
@ -146,11 +143,9 @@ function show() {
|
|||||||
const pushOption = (option: VNode) => {
|
const pushOption = (option: VNode) => {
|
||||||
menu.push({
|
menu.push({
|
||||||
text: option.children as string,
|
text: option.children as string,
|
||||||
active: computed(() => v.value === option.props?.value),
|
active: computed(() => modelValue.value === option.props?.value),
|
||||||
action: () => {
|
action: () => {
|
||||||
v.value = option.props?.value;
|
emit('update:modelValue', option.props?.value);
|
||||||
changed.value = true;
|
|
||||||
emit('changeByUser', v.value);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -248,7 +243,8 @@ function show() {
|
|||||||
.inputCore {
|
.inputCore {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
height: v-bind("height + 'px'");
|
height: v-bind("height + 'px'");
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -38,6 +38,7 @@ const props = withDefaults(defineProps<{
|
|||||||
sound?: boolean;
|
sound?: boolean;
|
||||||
withRenotes?: boolean;
|
withRenotes?: boolean;
|
||||||
withReplies?: boolean;
|
withReplies?: boolean;
|
||||||
|
withSensitive?: boolean;
|
||||||
onlyFiles?: boolean;
|
onlyFiles?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
withRenotes: true,
|
withRenotes: true,
|
||||||
@ -51,6 +52,7 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
provide('inTimeline', true);
|
provide('inTimeline', true);
|
||||||
|
provide('tl_withSensitive', computed(() => props.withSensitive));
|
||||||
provide('inChannel', computed(() => props.src === 'channel'));
|
provide('inChannel', computed(() => props.src === 'channel'));
|
||||||
|
|
||||||
type TimelineQueryType = {
|
type TimelineQueryType = {
|
||||||
@ -248,6 +250,9 @@ function refreshEndpointAndChannel() {
|
|||||||
// IDが切り替わったら切り替え先のTLを表示させたい
|
// IDが切り替わったら切り替え先のTLを表示させたい
|
||||||
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
|
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
|
||||||
|
|
||||||
|
// withSensitiveはクライアントで完結する処理のため、単にリロードするだけでOK
|
||||||
|
watch(() => props.withSensitive, reloadTimeline);
|
||||||
|
|
||||||
// 初回表示用
|
// 初回表示用
|
||||||
refreshEndpointAndChannel();
|
refreshEndpointAndChannel();
|
||||||
|
|
||||||
|
@ -150,20 +150,6 @@ export const navbarItemDef = reactive({
|
|||||||
}], ev.currentTarget ?? ev.target);
|
}], 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: {
|
reload: {
|
||||||
title: i18n.ts.reload,
|
title: i18n.ts.reload,
|
||||||
icon: 'ti ti-refresh',
|
icon: 'ti ti-refresh',
|
||||||
|
@ -688,14 +688,16 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function post(props: Record<string, any> = {}): Promise<void> {
|
export function post(props: Record<string, any> = {}): Promise<void> {
|
||||||
pleaseLogin(undefined, (props.initialText || props.initialNote ? {
|
pleaseLogin({
|
||||||
|
openOnRemote: (props.initialText || props.initialNote ? {
|
||||||
type: 'share',
|
type: 'share',
|
||||||
params: {
|
params: {
|
||||||
text: props.initialText ?? props.initialNote.text,
|
text: props.initialText ?? props.initialNote.text,
|
||||||
visibility: props.initialVisibility ?? props.initialNote?.visibility,
|
visibility: props.initialVisibility ?? props.initialNote?.visibility,
|
||||||
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
|
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
|
||||||
},
|
},
|
||||||
} : undefined));
|
} : undefined),
|
||||||
|
});
|
||||||
|
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
|
@ -160,6 +160,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</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'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
|
||||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
@ -51,6 +51,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</MkFolder>
|
</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'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
|
||||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||||
<template #suffix>{{ policies.mentionLimit }}</template>
|
<template #suffix>{{ policies.mentionLimit }}</template>
|
||||||
|
@ -55,7 +55,7 @@ import * as os from '@/os.js';
|
|||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.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';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -90,7 +90,7 @@ async function read(target: Misskey.entities.Announcement): Promise<void> {
|
|||||||
target.isRead = true;
|
target.isRead = true;
|
||||||
await misskeyApi('i/read-announcement', { announcementId: target.id });
|
await misskeyApi('i/read-announcement', { announcementId: target.id });
|
||||||
if ($i) {
|
if ($i) {
|
||||||
updateAccount({
|
updateAccountPartial({
|
||||||
unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
|
unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ import * as os from '@/os.js';
|
|||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { $i, updateAccount } from '@/account.js';
|
import { $i, updateAccountPartial } from '@/account.js';
|
||||||
|
|
||||||
const paginationCurrent = {
|
const paginationCurrent = {
|
||||||
endpoint: 'announcements' as const,
|
endpoint: 'announcements' as const,
|
||||||
@ -94,7 +94,7 @@ async function read(target) {
|
|||||||
return a;
|
return a;
|
||||||
});
|
});
|
||||||
misskeyApi('i/read-announcement', { announcementId: target.id });
|
misskeyApi('i/read-announcement', { announcementId: target.id });
|
||||||
updateAccount({
|
updateAccountPartial({
|
||||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
|
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
if (props.showLoginPopup) {
|
if (props.showLoginPopup) {
|
||||||
pleaseLogin('/');
|
pleaseLogin({ path: '/' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerActions = computed(() => []);
|
const headerActions = computed(() => []);
|
||||||
|
@ -61,6 +61,7 @@ import { i18n } from '@/i18n.js';
|
|||||||
import { dateString } from '@/filters/date.js';
|
import { dateString } from '@/filters/date.js';
|
||||||
import MkClipPreview from '@/components/MkClipPreview.vue';
|
import MkClipPreview from '@/components/MkClipPreview.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
noteId: string;
|
noteId: string;
|
||||||
@ -128,6 +129,11 @@ function fetchNote() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
if (err.id === '8e75455b-738c-471d-9f80-62693f33372e') {
|
||||||
|
pleaseLogin({
|
||||||
|
message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor,
|
||||||
|
});
|
||||||
|
}
|
||||||
error.value = err;
|
error.value = err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ import FormSection from '@/components/form/section.vue';
|
|||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkLink from '@/components/MkLink.vue';
|
import MkLink from '@/components/MkLink.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { signinRequired, updateAccount } from '@/account.js';
|
import { signinRequired, updateAccountPartial } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
@ -123,7 +123,7 @@ async function unregisterTOTP(): Promise<void> {
|
|||||||
password: auth.result.password,
|
password: auth.result.password,
|
||||||
token: auth.result.token,
|
token: auth.result.token,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
updateAccount({
|
updateAccountPartial({
|
||||||
twoFactorEnabled: false,
|
twoFactorEnabled: false,
|
||||||
});
|
});
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -6,13 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template>
|
<template>
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkSelect v-model="type">
|
<MkSelect v-model="type">
|
||||||
<option value="all">{{ i18n.ts.all }}</option>
|
<option v-for="type in props.configurableTypes ?? notificationConfigTypes" :key="type" :value="type">{{ notificationConfigTypesI18nMap[type] }}</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>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
<MkSelect v-if="type === 'list'" v-model="userListId">
|
<MkSelect v-if="type === 'list'" v-model="userListId">
|
||||||
@ -21,31 +15,61 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
<div class="_buttons">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { ref } from 'vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
value: any;
|
value: NotificationConfig;
|
||||||
userLists: Misskey.entities.UserList[];
|
userLists: Misskey.entities.UserList[];
|
||||||
|
configurableTypes?: NotificationConfig['type'][]; // If not specified, all types are configurable
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
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 type = ref(props.value.type);
|
||||||
const userListId = ref(props.value.userListId);
|
const userListId = ref(props.value.type === 'list' ? props.value.userListId : null);
|
||||||
|
|
||||||
function save() {
|
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>
|
</script>
|
||||||
|
@ -22,7 +22,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
}}
|
}}
|
||||||
</template>
|
</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>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
@ -58,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { shallowRef, computed } from 'vue';
|
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 FormLink from '@/components/form/link.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
@ -73,7 +78,9 @@ import { notificationTypes } from '@@/js/const.js';
|
|||||||
|
|
||||||
const $i = signinRequired();
|
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 allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
|
||||||
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
|
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
|
||||||
@ -88,7 +95,7 @@ async function readAllNotifications() {
|
|||||||
await os.apiWithDialog('notifications/mark-all-as-read');
|
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', {
|
await os.apiWithDialog('i/update', {
|
||||||
notificationRecieveConfig: {
|
notificationRecieveConfig: {
|
||||||
...$i.notificationRecieveConfig,
|
...$i.notificationRecieveConfig,
|
||||||
|
@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #caption>{{ i18n.ts.noCrawleDescription }}</template>
|
<template #caption>{{ i18n.ts.noCrawleDescription }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkSwitch v-model="preventAiLearning" @update:modelValue="save()">
|
<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>
|
<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkSwitch v-model="isExplorable" @update:modelValue="save()">
|
<MkSwitch v-model="isExplorable" @update:modelValue="save()">
|
||||||
@ -44,6 +44,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
|
<template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
|
||||||
</MkSwitch>
|
</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>
|
<FormSection>
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch>
|
<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 noCrawle = ref($i.noCrawle);
|
||||||
const preventAiLearning = ref($i.preventAiLearning);
|
const preventAiLearning = ref($i.preventAiLearning);
|
||||||
const isExplorable = ref($i.isExplorable);
|
const isExplorable = ref($i.isExplorable);
|
||||||
|
const requireSigninToViewContents = ref($i.requireSigninToViewContents ?? false);
|
||||||
const hideOnlineStatus = ref($i.hideOnlineStatus);
|
const hideOnlineStatus = ref($i.hideOnlineStatus);
|
||||||
const publicReactions = ref($i.publicReactions);
|
const publicReactions = ref($i.publicReactions);
|
||||||
const followingVisibility = ref($i.followingVisibility);
|
const followingVisibility = ref($i.followingVisibility);
|
||||||
@ -107,6 +123,7 @@ function save() {
|
|||||||
noCrawle: !!noCrawle.value,
|
noCrawle: !!noCrawle.value,
|
||||||
preventAiLearning: !!preventAiLearning.value,
|
preventAiLearning: !!preventAiLearning.value,
|
||||||
isExplorable: !!isExplorable.value,
|
isExplorable: !!isExplorable.value,
|
||||||
|
requireSigninToViewContents: !!requireSigninToViewContents.value,
|
||||||
hideOnlineStatus: !!hideOnlineStatus.value,
|
hideOnlineStatus: !!hideOnlineStatus.value,
|
||||||
publicReactions: !!publicReactions.value,
|
publicReactions: !!publicReactions.value,
|
||||||
followingVisibility: followingVisibility.value,
|
followingVisibility: followingVisibility.value,
|
||||||
|
@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
:list="src.split(':')[1]"
|
:list="src.split(':')[1]"
|
||||||
:withRenotes="withRenotes"
|
:withRenotes="withRenotes"
|
||||||
:withReplies="withReplies"
|
:withReplies="withReplies"
|
||||||
|
:withSensitive="withSensitive"
|
||||||
:onlyFiles="onlyFiles"
|
:onlyFiles="onlyFiles"
|
||||||
:sound="true"
|
:sound="true"
|
||||||
@queue="queueUpdated"
|
@queue="queueUpdated"
|
||||||
@ -121,11 +122,6 @@ watch(src, () => {
|
|||||||
queue.value = 0;
|
queue.value = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(withSensitive, () => {
|
|
||||||
// これだけはクライアント側で完結する処理なので手動でリロード
|
|
||||||
tlComponent.value?.reloadTimeline();
|
|
||||||
});
|
|
||||||
|
|
||||||
function queueUpdated(q: number): void {
|
function queueUpdated(q: number): void {
|
||||||
queue.value = q;
|
queue.value = q;
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<img :src="misskeysvg" class="misskey"/>
|
<img :src="misskeysvg" class="misskey"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="emojis">
|
<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="🎉"/>
|
<MkEmoji :normal="true" :noStyle="true" emoji="🎉"/>
|
||||||
<MkEmoji :normal="true" :noStyle="true" emoji="🍮"/>
|
<MkEmoji :normal="true" :noStyle="true" emoji="🍮"/>
|
||||||
</div>
|
</div>
|
||||||
|