Merge remote-tracking branch 'misskey-original/develop' into develop
# Conflicts: # locales/index.d.ts # locales/ja-JP.yml # package.json # packages/backend/src/core/RoleService.ts # packages/backend/src/server/api/endpoints/notes/create.ts # packages/frontend/src/components/MkTimeline.vue # packages/frontend/src/const.ts # packages/frontend/src/pages/admin/roles.editor.vue # packages/frontend/src/pages/settings/general.vue # packages/frontend/src/pages/timeline.vue # packages/frontend/src/store.ts # packages/frontend/src/ui/_common_/stream-indicator.vue # packages/frontend/src/ui/universal.vue
This commit is contained in:
commit
9199a493f1
2
.github/workflows/api-misskey-js.yml
vendored
2
.github/workflows/api-misskey-js.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
4
.github/workflows/get-api-diff.yml
vendored
4
.github/workflows/get-api-diff.yml
vendored
|
@ -44,7 +44,7 @@ jobs:
|
||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
@ -126,7 +126,7 @@ jobs:
|
||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
|
@ -19,7 +19,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- uses: actions/setup-node@v3.8.1
|
- uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
@ -46,7 +46,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: 7
|
version: 7
|
||||||
run_install: false
|
run_install: false
|
||||||
- uses: actions/setup-node@v3.8.1
|
- uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
@ -72,7 +72,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: 7
|
version: 7
|
||||||
run_install: false
|
run_install: false
|
||||||
- uses: actions/setup-node@v3.8.1
|
- uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
2
.github/workflows/test-backend.yml
vendored
2
.github/workflows/test-backend.yml
vendored
|
@ -38,7 +38,7 @@ jobs:
|
||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
4
.github/workflows/test-frontend.yml
vendored
4
.github/workflows/test-frontend.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
@ -83,7 +83,7 @@ jobs:
|
||||||
version: 7
|
version: 7
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
2
.github/workflows/test-misskey-js.yml
vendored
2
.github/workflows/test-misskey-js.yml
vendored
|
@ -26,7 +26,7 @@ jobs:
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
|
|
||||||
- name: Setup Node.js ${{ matrix.node-version }}
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
2
.github/workflows/test-production.yml
vendored
2
.github/workflows/test-production.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v4.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -16,6 +16,10 @@
|
||||||
|
|
||||||
### General
|
### General
|
||||||
- Feat: アイコンデコレーション機能
|
- Feat: アイコンデコレーション機能
|
||||||
|
- サーバーで用意された画像をアイコンに重ねることができます
|
||||||
|
- 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png
|
||||||
|
- 最大でも黄色いエリア内にデコレーションを収めることを推奨します。
|
||||||
|
- 画像は512x512pxを推奨します。
|
||||||
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
|
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
|
||||||
- Enhance: ローカリゼーションの更新
|
- Enhance: ローカリゼーションの更新
|
||||||
- Enhance: 依存関係の更新
|
- Enhance: 依存関係の更新
|
||||||
|
@ -24,6 +28,9 @@
|
||||||
- Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました
|
- Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました
|
||||||
- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
|
- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
|
||||||
https://misskey-hub.net/docs/advanced/publish-on-your-website.html
|
https://misskey-hub.net/docs/advanced/publish-on-your-website.html
|
||||||
|
- Enhance: スワイプしてタイムラインを再読込できるように
|
||||||
|
- PCの場合は右上のボタンからでも再読込できます
|
||||||
|
- Enhance: タイムラインの自動更新を無効にできるように
|
||||||
- Enhance: コードのシンタックスハイライトエンジンをShikiに変更
|
- Enhance: コードのシンタックスハイライトエンジンをShikiに変更
|
||||||
- AiScriptのシンタックスハイライトに対応
|
- AiScriptのシンタックスハイライトに対応
|
||||||
- MFMでAiScriptをハイライトする場合、コードブロックの開始部分を ` ```is ` もしくは ` ```aiscript ` としてください
|
- MFMでAiScriptをハイライトする場合、コードブロックの開始部分を ` ```is ` もしくは ` ```aiscript ` としてください
|
||||||
|
@ -36,6 +43,8 @@
|
||||||
- Fix: 「検索」MFMにおいて一部の検索キーワードが正しく認識されない問題を修正
|
- Fix: 「検索」MFMにおいて一部の検索キーワードが正しく認識されない問題を修正
|
||||||
- Fix: 一部の言語でMisskey Webがクラッシュする問題を修正
|
- Fix: 一部の言語でMisskey Webがクラッシュする問題を修正
|
||||||
- Fix: チャンネルの作成・更新時に失敗した場合何も表示されない問題を修正 #11983
|
- Fix: チャンネルの作成・更新時に失敗した場合何も表示されない問題を修正 #11983
|
||||||
|
- Fix: 個人カードのemojiがバッテリーになっている問題を修正
|
||||||
|
- Fix: 標準テーマと同じIDを使用してインストールできてしまう問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: RedisへのTLのキャッシュをオフにできるように
|
- Enhance: RedisへのTLのキャッシュをオフにできるように
|
||||||
|
@ -48,7 +57,8 @@
|
||||||
- Fix: RedisへのTLキャッシュが有効の場合にHTL/LTL/STLが空になることがある問題を修正
|
- Fix: RedisへのTLキャッシュが有効の場合にHTL/LTL/STLが空になることがある問題を修正
|
||||||
- Fix: STLでフォローしていないチャンネルが取得される問題を修正
|
- Fix: STLでフォローしていないチャンネルが取得される問題を修正
|
||||||
- Fix: `hashtags/trend`にてRedisからトレンドの情報が取得できない際にInternal Server Errorになる問題を修正
|
- Fix: `hashtags/trend`にてRedisからトレンドの情報が取得できない際にInternal Server Errorになる問題を修正
|
||||||
- Fix: HTLをリロードまたは遡行したとき、フォローしているチャンネルのノートが含まれない問題を修正 #11765
|
- Fix: HTLをリロードまたは遡行したとき、フォローしているチャンネルのノートが含まれない問題を修正 #11765 #12181
|
||||||
|
- Fix: リノートをリノートできるのを修正
|
||||||
|
|
||||||
## 2023.10.2
|
## 2023.10.2
|
||||||
|
|
||||||
|
|
6
locales/index.d.ts
vendored
6
locales/index.d.ts
vendored
|
@ -1012,6 +1012,7 @@ export interface Locale {
|
||||||
"color": string;
|
"color": string;
|
||||||
"manageCustomEmojis": string;
|
"manageCustomEmojis": string;
|
||||||
"requestCustomEmojis": string;
|
"requestCustomEmojis": string;
|
||||||
|
"manageAvatarDecorations": string;
|
||||||
"youCannotCreateAnymore": string;
|
"youCannotCreateAnymore": string;
|
||||||
"cannotPerformTemporary": string;
|
"cannotPerformTemporary": string;
|
||||||
"cannotPerformTemporaryDescription": string;
|
"cannotPerformTemporaryDescription": string;
|
||||||
|
@ -1186,6 +1187,10 @@ export interface Locale {
|
||||||
"angle": string;
|
"angle": string;
|
||||||
"flip": string;
|
"flip": string;
|
||||||
"showAvatarDecorations": string;
|
"showAvatarDecorations": string;
|
||||||
|
"releaseToRefresh": string;
|
||||||
|
"refreshing": string;
|
||||||
|
"pullDownToRefresh": string;
|
||||||
|
"disableStreamingTimeline": string;
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
"forExistingUsers": string;
|
"forExistingUsers": string;
|
||||||
"forExistingUsersDescription": string;
|
"forExistingUsersDescription": string;
|
||||||
|
@ -1607,6 +1612,7 @@ export interface Locale {
|
||||||
"inviteExpirationTime": string;
|
"inviteExpirationTime": string;
|
||||||
"canManageCustomEmojis": string;
|
"canManageCustomEmojis": string;
|
||||||
"canRequestCustomEmojis": string;
|
"canRequestCustomEmojis": string;
|
||||||
|
"canManageAvatarDecorations": string;
|
||||||
"driveCapacity": string;
|
"driveCapacity": string;
|
||||||
"alwaysMarkNsfw": string;
|
"alwaysMarkNsfw": string;
|
||||||
"pinMax": string;
|
"pinMax": string;
|
||||||
|
|
|
@ -1009,6 +1009,7 @@ unassign: "アサインを解除"
|
||||||
color: "色"
|
color: "色"
|
||||||
manageCustomEmojis: "カスタム絵文字の管理"
|
manageCustomEmojis: "カスタム絵文字の管理"
|
||||||
requestCustomEmojis: "カスタム絵文字のリクエスト"
|
requestCustomEmojis: "カスタム絵文字のリクエスト"
|
||||||
|
manageAvatarDecorations: "アバターデコレーションの管理"
|
||||||
youCannotCreateAnymore: "これ以上作成することはできません。"
|
youCannotCreateAnymore: "これ以上作成することはできません。"
|
||||||
cannotPerformTemporary: "一時的に利用できません"
|
cannotPerformTemporary: "一時的に利用できません"
|
||||||
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
|
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
|
||||||
|
@ -1183,6 +1184,10 @@ detach: "外す"
|
||||||
angle: "角度"
|
angle: "角度"
|
||||||
flip: "反転"
|
flip: "反転"
|
||||||
showAvatarDecorations: "アイコンのデコレーションを表示"
|
showAvatarDecorations: "アイコンのデコレーションを表示"
|
||||||
|
releaseToRefresh: "離してリロード"
|
||||||
|
refreshing: "リロード中"
|
||||||
|
pullDownToRefresh: "引っ張ってリロード"
|
||||||
|
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
|
@ -1528,6 +1533,7 @@ _role:
|
||||||
inviteExpirationTime: "招待コードの有効期限"
|
inviteExpirationTime: "招待コードの有効期限"
|
||||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||||
canRequestCustomEmojis: "カスタム絵文字のリクエスト"
|
canRequestCustomEmojis: "カスタム絵文字のリクエスト"
|
||||||
|
canManageAvatarDecorations: "アバターデコレーションの管理"
|
||||||
driveCapacity: "ドライブ容量"
|
driveCapacity: "ドライブ容量"
|
||||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||||
pinMax: "ノートのピン留めの最大数"
|
pinMax: "ノートのピン留めの最大数"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2023.11.0-beta.5-prismisskey.1",
|
"version": "2023.11.0-beta.6-prismisskey.1",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { SearchService } from '@/core/SearchService.js';
|
import { SearchService } from '@/core/SearchService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteDeleteService {
|
export class NoteDeleteService {
|
||||||
|
@ -77,8 +78,8 @@ export class NoteDeleteService {
|
||||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
||||||
let renote: MiNote | null = null;
|
let renote: MiNote | null = null;
|
||||||
|
|
||||||
// if deletd note is renote
|
// if deleted note is renote
|
||||||
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
|
if (isPureRenote(note)) {
|
||||||
renote = await this.notesRepository.findOneBy({
|
renote = await this.notesRepository.findOneBy({
|
||||||
id: note.renoteId,
|
id: note.renoteId,
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,6 +34,7 @@ export type RolePolicies = {
|
||||||
inviteExpirationTime: number;
|
inviteExpirationTime: number;
|
||||||
canManageCustomEmojis: boolean;
|
canManageCustomEmojis: boolean;
|
||||||
canRequestCustomEmojis: boolean;
|
canRequestCustomEmojis: boolean;
|
||||||
|
canManageAvatarDecorations: boolean;
|
||||||
canSearchNotes: boolean;
|
canSearchNotes: boolean;
|
||||||
canUseTranslator: boolean;
|
canUseTranslator: boolean;
|
||||||
canHideAds: boolean;
|
canHideAds: boolean;
|
||||||
|
@ -60,6 +61,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
inviteLimitCycle: 60 * 24 * 7,
|
inviteLimitCycle: 60 * 24 * 7,
|
||||||
inviteExpirationTime: 0,
|
inviteExpirationTime: 0,
|
||||||
canManageCustomEmojis: false,
|
canManageCustomEmojis: false,
|
||||||
|
canManageAvatarDecorations: false,
|
||||||
canRequestCustomEmojis: false,
|
canRequestCustomEmojis: false,
|
||||||
canSearchNotes: false,
|
canSearchNotes: false,
|
||||||
canUseTranslator: true,
|
canUseTranslator: true,
|
||||||
|
@ -311,6 +313,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
||||||
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
||||||
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
||||||
|
canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)),
|
||||||
canRequestCustomEmojis: calc('canRequestCustomEmojis', vs => vs.some(v => v === true)),
|
canRequestCustomEmojis: calc('canRequestCustomEmojis', vs => vs.some(v => v === true)),
|
||||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||||
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
||||||
|
|
|
@ -319,9 +319,17 @@ export class ApPersonService implements OnModuleInit {
|
||||||
emojis,
|
emojis,
|
||||||
})) as MiRemoteUser;
|
})) as MiRemoteUser;
|
||||||
|
|
||||||
|
let _description: string | null = null;
|
||||||
|
|
||||||
|
if (person._misskey_summary) {
|
||||||
|
_description = truncate(person._misskey_summary, summaryLength);
|
||||||
|
} else if (person.summary) {
|
||||||
|
_description = this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag);
|
||||||
|
}
|
||||||
|
|
||||||
await transactionalEntityManager.save(new MiUserProfile({
|
await transactionalEntityManager.save(new MiUserProfile({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
description: person._misskey_summary ? truncate(person._misskey_summary, summaryLength) : person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
description: _description,
|
||||||
url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday?.[0] ?? null,
|
||||||
|
@ -487,10 +495,18 @@ export class ApPersonService implements OnModuleInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _description: string | null = null;
|
||||||
|
|
||||||
|
if (person._misskey_summary) {
|
||||||
|
_description = truncate(person._misskey_summary, summaryLength);
|
||||||
|
} else if (person.summary) {
|
||||||
|
_description = this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag);
|
||||||
|
}
|
||||||
|
|
||||||
await this.userProfilesRepository.update({ userId: exist.id }, {
|
await this.userProfilesRepository.update({ userId: exist.id }, {
|
||||||
url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
description: _description,
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday?.[0] ?? null,
|
||||||
location: person['vcard:Address'] ?? null,
|
location: person['vcard:Address'] ?? null,
|
||||||
});
|
});
|
||||||
|
|
10
packages/backend/src/misc/is-pure-renote.ts
Normal file
10
packages/backend/src/misc/is-pure-renote.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
|
||||||
|
export function isPureRenote(note: MiNote): note is MiNote & { renoteId: NonNullable<MiNote['renoteId']> } {
|
||||||
|
if (!note.renoteId) return false;
|
||||||
|
|
||||||
|
if (note.text) return false; // it's quoted with text
|
||||||
|
if (note.fileIds.length !== 0) return false; // it's quoted with files
|
||||||
|
if (note.hasPoll) return false; // it's quoted with poll
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IActivity } from '@/core/activitypub/type.js';
|
import { IActivity } from '@/core/activitypub/type.js';
|
||||||
|
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||||
import type { FindOptionsWhere } from 'typeorm';
|
import type { FindOptionsWhere } from 'typeorm';
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ export class ActivityPubServerService {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
private async packActivity(note: MiNote): Promise<any> {
|
private async packActivity(note: MiNote): Promise<any> {
|
||||||
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
|
if (isPureRenote(note)) {
|
||||||
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
|
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
|
||||||
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
|
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireRolePolicy: 'canManageAvatarDecorations',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -13,8 +13,7 @@ export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireRolePolicy: 'canManageAvatarDecorations',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireRolePolicy: 'canManageAvatarDecorations',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireRolePolicy: 'canManageAvatarDecorations',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
import {noteVisibilities} from "@/types.js";
|
import {noteVisibilities} from "@/types.js";
|
||||||
|
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
|
@ -222,7 +223,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
if (renote == null) {
|
if (renote == null) {
|
||||||
throw new ApiError(meta.errors.noSuchRenoteTarget);
|
throw new ApiError(meta.errors.noSuchRenoteTarget);
|
||||||
} else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) {
|
} else if (isPureRenote(renote)) {
|
||||||
throw new ApiError(meta.errors.cannotReRenote);
|
throw new ApiError(meta.errors.cannotReRenote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,7 +256,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
if (reply == null) {
|
if (reply == null) {
|
||||||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||||
} else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) {
|
} else if (isPureRenote(reply)) {
|
||||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -183,7 +183,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
const followingChannelIds = followingChannels.map(x => x.followeeId);
|
const followingChannelIds = followingChannels.map(x => x.followeeId);
|
||||||
query.andWhere(new Brackets(qb => {
|
query.andWhere(new Brackets(qb => {
|
||||||
qb
|
qb
|
||||||
.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds })
|
.where(new Brackets(qb2 => {
|
||||||
|
qb2
|
||||||
|
.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds })
|
||||||
|
.andWhere('note.channelId IS NULL');
|
||||||
|
}))
|
||||||
.orWhere('note.channelId IN (:...followingChannelIds)', { followingChannelIds });
|
.orWhere('note.channelId IN (:...followingChannelIds)', { followingChannelIds });
|
||||||
}));
|
}));
|
||||||
} else if (followees.length > 0) {
|
} else if (followees.length > 0) {
|
||||||
|
|
|
@ -67,6 +67,8 @@ export default abstract class Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract init(params: any): void;
|
public abstract init(params: any): void;
|
||||||
|
|
||||||
public dispose?(): void;
|
public dispose?(): void;
|
||||||
|
|
||||||
public onMessage?(type: string, body: any): void;
|
public onMessage?(type: string, body: any): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { common } from './common.js';
|
||||||
import { version, ui, lang, updateLocale } from '@/config.js';
|
import { version, ui, lang, updateLocale } from '@/config.js';
|
||||||
import { i18n, updateI18n } from '@/i18n.js';
|
import { i18n, updateI18n } from '@/i18n.js';
|
||||||
import { confirm, alert, post, popup, toast } from '@/os.js';
|
import { confirm, alert, post, popup, toast } from '@/os.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream, isReloading } from '@/stream.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
|
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
|
||||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||||
|
@ -39,6 +39,7 @@ export async function mainBoot() {
|
||||||
|
|
||||||
let reloadDialogShowing = false;
|
let reloadDialogShowing = false;
|
||||||
stream.on('_disconnected_', async () => {
|
stream.on('_disconnected_', async () => {
|
||||||
|
if (isReloading) return;
|
||||||
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
|
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
|
||||||
location.reload();
|
location.reload();
|
||||||
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
|
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
|
||||||
|
|
|
@ -166,6 +166,8 @@ defineExpose({
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
|
overscroll-behavior: none;
|
||||||
|
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,7 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'queue', count: number): void;
|
(ev: 'queue', count: number): void;
|
||||||
|
(ev: 'status', error: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let rootEl = $shallowRef<HTMLElement>();
|
let rootEl = $shallowRef<HTMLElement>();
|
||||||
|
@ -193,6 +194,11 @@ watch(queue, (a, b) => {
|
||||||
emit('queue', queue.value.size);
|
emit('queue', queue.value.size);
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
|
watch(error, (n, o) => {
|
||||||
|
if (n === o) return;
|
||||||
|
emit('status', n);
|
||||||
|
});
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
items.value = new Map();
|
items.value = new Map();
|
||||||
queue.value = new Map();
|
queue.value = new Map();
|
||||||
|
|
240
packages/frontend/src/components/MkPullToRefresh.vue
Normal file
240
packages/frontend/src/components/MkPullToRefresh.vue
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="rootEl">
|
||||||
|
<div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR))}px;`">
|
||||||
|
<div :class="$style.frameContent">
|
||||||
|
<MkLoading v-if="isRefreshing" :class="$style.loader" :em="true"/>
|
||||||
|
<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i>
|
||||||
|
<div :class="$style.text">
|
||||||
|
<template v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</template>
|
||||||
|
<template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template>
|
||||||
|
<template v-else>{{ i18n.ts.pullDownToRefresh }}</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="{ [$style.slotClip]: isPullStart }">
|
||||||
|
<slot/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, onUnmounted } from 'vue';
|
||||||
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
const SCROLL_STOP = 10;
|
||||||
|
const MAX_PULL_DISTANCE = Infinity;
|
||||||
|
const FIRE_THRESHOLD = 230;
|
||||||
|
const RELEASE_TRANSITION_DURATION = 200;
|
||||||
|
const PULL_BRAKE_BASE = 2;
|
||||||
|
const PULL_BRAKE_FACTOR = 200;
|
||||||
|
|
||||||
|
let isPullStart = $ref(false);
|
||||||
|
let isPullEnd = $ref(false);
|
||||||
|
let isRefreshing = $ref(false);
|
||||||
|
let pullDistance = $ref(0);
|
||||||
|
|
||||||
|
let supportPointerDesktop = false;
|
||||||
|
let startScreenY: number | null = null;
|
||||||
|
|
||||||
|
const rootEl = $shallowRef<HTMLDivElement>();
|
||||||
|
let scrollEl: HTMLElement | null = null;
|
||||||
|
|
||||||
|
let disabled = false;
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(ev: 'refresh'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function getScrollableParentElement(node) {
|
||||||
|
if (node == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.scrollHeight > node.clientHeight) {
|
||||||
|
return node;
|
||||||
|
} else {
|
||||||
|
return getScrollableParentElement(node.parentNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScreenY(event) {
|
||||||
|
if (supportPointerDesktop) {
|
||||||
|
return event.screenY;
|
||||||
|
}
|
||||||
|
return event.touches[0].screenY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveStart(event) {
|
||||||
|
if (!isPullStart && !isRefreshing && !disabled) {
|
||||||
|
isPullStart = true;
|
||||||
|
startScreenY = getScreenY(event);
|
||||||
|
pullDistance = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveBySystem(to: number): Promise<void> {
|
||||||
|
return new Promise(r => {
|
||||||
|
const startHeight = pullDistance;
|
||||||
|
const overHeight = pullDistance - to;
|
||||||
|
if (overHeight < 1) {
|
||||||
|
r();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const startTime = Date.now();
|
||||||
|
let intervalId = setInterval(() => {
|
||||||
|
const time = Date.now() - startTime;
|
||||||
|
if (time > RELEASE_TRANSITION_DURATION) {
|
||||||
|
pullDistance = to;
|
||||||
|
clearInterval(intervalId);
|
||||||
|
r();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextHeight = startHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time;
|
||||||
|
if (pullDistance < nextHeight) return;
|
||||||
|
pullDistance = nextHeight;
|
||||||
|
}, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fixOverContent() {
|
||||||
|
if (pullDistance > FIRE_THRESHOLD) {
|
||||||
|
await moveBySystem(FIRE_THRESHOLD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeContent() {
|
||||||
|
if (pullDistance > 0) {
|
||||||
|
await moveBySystem(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveEnd() {
|
||||||
|
if (isPullStart && !isRefreshing) {
|
||||||
|
startScreenY = null;
|
||||||
|
if (isPullEnd) {
|
||||||
|
isPullEnd = false;
|
||||||
|
isRefreshing = true;
|
||||||
|
fixOverContent().then(() => emits('refresh'));
|
||||||
|
} else {
|
||||||
|
closeContent().then(() => isPullStart = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moving(event) {
|
||||||
|
if (!isPullStart || isRefreshing || disabled) return;
|
||||||
|
|
||||||
|
if (!scrollEl) {
|
||||||
|
scrollEl = getScrollableParentElement(rootEl);
|
||||||
|
}
|
||||||
|
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) {
|
||||||
|
pullDistance = 0;
|
||||||
|
isPullEnd = false;
|
||||||
|
moveEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startScreenY === null) {
|
||||||
|
startScreenY = getScreenY(event);
|
||||||
|
}
|
||||||
|
const moveScreenY = getScreenY(event);
|
||||||
|
|
||||||
|
const moveHeight = moveScreenY - startScreenY!;
|
||||||
|
pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
|
||||||
|
|
||||||
|
isPullEnd = pullDistance >= FIRE_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* emit(refresh)が完了したことを知らせる関数
|
||||||
|
*
|
||||||
|
* タイムアウトがないのでこれを最終的に実行しないと出たままになる
|
||||||
|
*/
|
||||||
|
function refreshFinished() {
|
||||||
|
closeContent().then(() => {
|
||||||
|
isPullStart = false;
|
||||||
|
isRefreshing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDisabled(value) {
|
||||||
|
disabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// マウス操作でpull to refreshするのは不便そう
|
||||||
|
//supportPointerDesktop = !!window.PointerEvent && deviceKind === 'desktop';
|
||||||
|
|
||||||
|
if (supportPointerDesktop) {
|
||||||
|
rootEl.addEventListener('pointerdown', moveStart);
|
||||||
|
// ポインターの場合、ポップアップ系の動作をするとdownだけ発火されてupが発火されないため
|
||||||
|
window.addEventListener('pointerup', moveEnd);
|
||||||
|
rootEl.addEventListener('pointermove', moving, { passive: true });
|
||||||
|
} else {
|
||||||
|
rootEl.addEventListener('touchstart', moveStart);
|
||||||
|
rootEl.addEventListener('touchend', moveEnd);
|
||||||
|
rootEl.addEventListener('touchmove', moving, { passive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (supportPointerDesktop) window.removeEventListener('pointerup', moveEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
refreshFinished,
|
||||||
|
setDisabled,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.frame {
|
||||||
|
position: relative;
|
||||||
|
overflow: clip;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
min-height: var(--frame-min-height, 0px);
|
||||||
|
|
||||||
|
mask-image: linear-gradient(90deg, #000 0%, #000 80%, transparent);
|
||||||
|
-webkit-mask-image: -webkit-linear-gradient(90deg, #000 0%, #000 80%, transparent);
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frameContent {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin: 5px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
> .icon, > .loader {
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
transition: transform .25s;
|
||||||
|
|
||||||
|
&.refresh {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slotClip {
|
||||||
|
overflow-y: clip;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,13 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
|
<MkPullToRefresh ref="prComponent" @refresh="() => reloadTimeline(true)">
|
||||||
|
<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)" @status="prComponent.setDisabled($event)"/>
|
||||||
|
</MkPullToRefresh>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, provide, onUnmounted } from 'vue';
|
import { computed, provide, onUnmounted } from 'vue';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotes from '@/components/MkNotes.vue';
|
||||||
import { useStream } from '@/stream.js';
|
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||||
|
import { useStream, reloadStream } from '@/stream.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
|
@ -39,6 +42,7 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
provide('inChannel', computed(() => props.src === 'channel'));
|
provide('inChannel', computed(() => props.src === 'channel'));
|
||||||
|
|
||||||
|
const prComponent: InstanceType<typeof MkPullToRefresh> = $ref();
|
||||||
const tlComponent: InstanceType<typeof MkNotes> = $ref();
|
const tlComponent: InstanceType<typeof MkNotes> = $ref();
|
||||||
|
|
||||||
let tlNotesCount = 0;
|
let tlNotesCount = 0;
|
||||||
|
@ -65,29 +69,86 @@ let connection;
|
||||||
let connection2;
|
let connection2;
|
||||||
|
|
||||||
const stream = useStream();
|
const stream = useStream();
|
||||||
|
const connectChannel = () => {
|
||||||
|
if (props.src === 'antenna') {
|
||||||
|
connection = stream.useChannel('antenna', {
|
||||||
|
antennaId: props.antenna,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'home') {
|
||||||
|
connection = stream.useChannel('homeTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
});
|
||||||
|
connection2 = stream.useChannel('main');
|
||||||
|
} else if (props.src === 'local') {
|
||||||
|
connection = stream.useChannel('localTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'media') {
|
||||||
|
endpoint = 'notes/hybrid-timeline';
|
||||||
|
query = {
|
||||||
|
withFiles: true,
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withReplies: props.withReplies,
|
||||||
|
};
|
||||||
|
connection = stream.useChannel('hybridTimeline', {
|
||||||
|
withFiles: true,
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withReplies: props.withReplies,
|
||||||
|
});
|
||||||
|
connection.on('note', prepend);
|
||||||
|
} else if (props.src === 'social') {
|
||||||
|
connection = stream.useChannel('hybridTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withReplies: props.withReplies,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'global') {
|
||||||
|
connection = stream.useChannel('globalTimeline', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'mentions') {
|
||||||
|
connection = stream.useChannel('main');
|
||||||
|
connection.on('mention', prepend);
|
||||||
|
} else if (props.src === 'directs') {
|
||||||
|
const onNote = note => {
|
||||||
|
if (note.visibility === 'specified') {
|
||||||
|
prepend(note);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
connection = stream.useChannel('main');
|
||||||
|
connection.on('mention', onNote);
|
||||||
|
} else if (props.src === 'list') {
|
||||||
|
connection = stream.useChannel('userList', {
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
|
listId: props.list,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'channel') {
|
||||||
|
connection = stream.useChannel('channel', {
|
||||||
|
channelId: props.channel,
|
||||||
|
});
|
||||||
|
} else if (props.src === 'role') {
|
||||||
|
connection = stream.useChannel('roleTimeline', {
|
||||||
|
roleId: props.role,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (props.src !== 'directs' || props.src !== 'mentions') connection.on('note', prepend);
|
||||||
|
};
|
||||||
|
|
||||||
if (props.src === 'antenna') {
|
if (props.src === 'antenna') {
|
||||||
endpoint = 'antennas/notes';
|
endpoint = 'antennas/notes';
|
||||||
query = {
|
query = {
|
||||||
antennaId: props.antenna,
|
antennaId: props.antenna,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('antenna', {
|
|
||||||
antennaId: props.antenna,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
} else if (props.src === 'home') {
|
} else if (props.src === 'home') {
|
||||||
endpoint = 'notes/timeline';
|
endpoint = 'notes/timeline';
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('homeTimeline', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
|
|
||||||
connection2 = stream.useChannel('main');
|
|
||||||
} else if (props.src === 'local') {
|
} else if (props.src === 'local') {
|
||||||
endpoint = 'notes/local-timeline';
|
endpoint = 'notes/local-timeline';
|
||||||
query = {
|
query = {
|
||||||
|
@ -95,25 +156,6 @@ if (props.src === 'antenna') {
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('localTimeline', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
} else if (props.src === 'media') {
|
|
||||||
endpoint = 'notes/hybrid-timeline';
|
|
||||||
query = {
|
|
||||||
withFiles: true,
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withReplies: props.withReplies,
|
|
||||||
};
|
|
||||||
connection = stream.useChannel('hybridTimeline', {
|
|
||||||
withFiles: true,
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withReplies: props.withReplies,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
} else if (props.src === 'social') {
|
} else if (props.src === 'social') {
|
||||||
endpoint = 'notes/hybrid-timeline';
|
endpoint = 'notes/hybrid-timeline';
|
||||||
query = {
|
query = {
|
||||||
|
@ -121,68 +163,44 @@ if (props.src === 'antenna') {
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('hybridTimeline', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withReplies: props.withReplies,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
}else if (props.src === 'global') {
|
}else if (props.src === 'global') {
|
||||||
endpoint = 'notes/global-timeline';
|
endpoint = 'notes/global-timeline';
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('globalTimeline', {
|
|
||||||
withRenotes: props.withRenotes,
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
} else if (props.src === 'mentions') {
|
} else if (props.src === 'mentions') {
|
||||||
endpoint = 'notes/mentions';
|
endpoint = 'notes/mentions';
|
||||||
connection = stream.useChannel('main');
|
|
||||||
connection.on('mention', prepend);
|
|
||||||
} else if (props.src === 'directs') {
|
} else if (props.src === 'directs') {
|
||||||
endpoint = 'notes/mentions';
|
endpoint = 'notes/mentions';
|
||||||
query = {
|
query = {
|
||||||
visibility: 'specified',
|
visibility: 'specified',
|
||||||
};
|
};
|
||||||
const onNote = note => {
|
|
||||||
if (note.visibility === 'specified') {
|
|
||||||
prepend(note);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
connection = stream.useChannel('main');
|
|
||||||
connection.on('mention', onNote);
|
|
||||||
} else if (props.src === 'list') {
|
} else if (props.src === 'list') {
|
||||||
endpoint = 'notes/user-list-timeline';
|
endpoint = 'notes/user-list-timeline';
|
||||||
query = {
|
query = {
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
listId: props.list,
|
listId: props.list,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('userList', {
|
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
|
||||||
listId: props.list,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
} else if (props.src === 'channel') {
|
} else if (props.src === 'channel') {
|
||||||
endpoint = 'channels/timeline';
|
endpoint = 'channels/timeline';
|
||||||
query = {
|
query = {
|
||||||
channelId: props.channel,
|
channelId: props.channel,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('channel', {
|
|
||||||
channelId: props.channel,
|
|
||||||
});
|
|
||||||
connection.on('note', prepend);
|
|
||||||
} else if (props.src === 'role') {
|
} else if (props.src === 'role') {
|
||||||
endpoint = 'roles/notes';
|
endpoint = 'roles/notes';
|
||||||
query = {
|
query = {
|
||||||
roleId: props.role,
|
roleId: props.role,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('roleTimeline', {
|
}
|
||||||
roleId: props.role,
|
|
||||||
|
if (!defaultStore.state.disableStreamingTimeline) {
|
||||||
|
connectChannel();
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
connection.dispose();
|
||||||
|
if (connection2) connection2.dispose();
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
|
@ -191,9 +209,19 @@ const pagination = {
|
||||||
params: query,
|
params: query,
|
||||||
};
|
};
|
||||||
|
|
||||||
onUnmounted(() => {
|
const reloadTimeline = (fromPR = false) => {
|
||||||
connection.dispose();
|
tlNotesCount = 0;
|
||||||
if (connection2) connection2.dispose();
|
|
||||||
|
tlComponent.pagingComponent?.reload().then(() => {
|
||||||
|
reloadStream();
|
||||||
|
if (fromPR) prComponent.refreshFinished();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//const pullRefresh = () => reloadTimeline(true);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
reloadTimeline,
|
||||||
});
|
});
|
||||||
|
|
||||||
/* TODO
|
/* TODO
|
||||||
|
|
|
@ -68,6 +68,7 @@ export const ROLE_POLICIES = [
|
||||||
'inviteExpirationTime',
|
'inviteExpirationTime',
|
||||||
'canManageCustomEmojis',
|
'canManageCustomEmojis',
|
||||||
'canRequestCustomEmojis',
|
'canRequestCustomEmojis',
|
||||||
|
'canManageAvatarDecorations',
|
||||||
'canSearchNotes',
|
'canSearchNotes',
|
||||||
'canUseTranslator',
|
'canUseTranslator',
|
||||||
'canHideAds',
|
'canHideAds',
|
||||||
|
|
|
@ -1061,7 +1061,7 @@
|
||||||
["💰", "moneybag", 6],
|
["💰", "moneybag", 6],
|
||||||
["🪙", "coin", 6],
|
["🪙", "coin", 6],
|
||||||
["💳", "credit_card", 6],
|
["💳", "credit_card", 6],
|
||||||
["🪫", "identification_card", 6],
|
["🪪", "identification_card", 6],
|
||||||
["💎", "gem", 6],
|
["💎", "gem", 6],
|
||||||
["⚖", "balance_scale", 6],
|
["⚖", "balance_scale", 6],
|
||||||
["🧰", "toolbox", 6],
|
["🧰", "toolbox", 6],
|
||||||
|
|
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-model="ad.url" type="url">
|
<MkInput v-model="ad.url" type="url">
|
||||||
<template #label>URL</template>
|
<template #label>URL</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="ad.imageUrl">
|
<MkInput v-model="ad.imageUrl" type="url">
|
||||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkRadios v-model="ad.place">
|
<MkRadios v-model="ad.place">
|
||||||
|
|
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkTextarea v-model="announcement.text">
|
<MkTextarea v-model="announcement.text">
|
||||||
<template #label>{{ i18n.ts.text }}</template>
|
<template #label>{{ i18n.ts.text }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
<MkInput v-model="announcement.imageUrl">
|
<MkInput v-model="announcement.imageUrl" type="url">
|
||||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkRadios v-model="announcement.icon">
|
<MkRadios v-model="announcement.icon">
|
||||||
|
|
|
@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||||
<FormSuspense :p="init">
|
<FormSuspense :p="init">
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkInput v-model="iconUrl">
|
<MkInput v-model="iconUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
|
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="app192IconUrl">
|
<MkInput v-model="app192IconUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
|
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
|
||||||
<template #caption>
|
<template #caption>
|
||||||
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="app512IconUrl">
|
<MkInput v-model="app512IconUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
|
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
|
||||||
<template #caption>
|
<template #caption>
|
||||||
|
@ -37,27 +37,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="bannerUrl">
|
<MkInput v-model="bannerUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.bannerUrl }}</template>
|
<template #label>{{ i18n.ts.bannerUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="backgroundImageUrl">
|
<MkInput v-model="backgroundImageUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
|
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="notFoundImageUrl">
|
<MkInput v-model="notFoundImageUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.notFoundDescription }}</template>
|
<template #label>{{ i18n.ts.notFoundDescription }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="infoImageUrl">
|
<MkInput v-model="infoImageUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.nothing }}</template>
|
<template #label>{{ i18n.ts.nothing }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="serverErrorImageUrl">
|
<MkInput v-model="serverErrorImageUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.somethingHappened }}</template>
|
<template #label>{{ i18n.ts.somethingHappened }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
|
@ -132,7 +132,7 @@ const menuDef = $computed(() => [{
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-sparkles',
|
icon: 'ti ti-sparkles',
|
||||||
text: i18n.ts.avatarDecorations,
|
text: i18n.ts.avatarDecorations,
|
||||||
to: '/admin/avatar-decorations',
|
to: '/avatar-decorations',
|
||||||
active: currentPage?.route.name === 'avatarDecorations',
|
active: currentPage?.route.name === 'avatarDecorations',
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-whirl',
|
icon: 'ti ti-whirl',
|
||||||
|
|
|
@ -20,12 +20,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
||||||
|
|
||||||
<MkInput v-model="tosUrl">
|
<MkInput v-model="tosUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.tosUrl }}</template>
|
<template #label>{{ i18n.ts.tosUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="privacyPolicyUrl">
|
<MkInput v-model="privacyPolicyUrl" type="url">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
|
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
|
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
|
||||||
|
|
||||||
<template v-if="useObjectStorage">
|
<template v-if="useObjectStorage">
|
||||||
<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'">
|
<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'" type="url">
|
||||||
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
|
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
|
||||||
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
|
@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.color }}</template>
|
<template #label>{{ i18n.ts.color }}</template>
|
||||||
</MkColorInput>
|
</MkColorInput>
|
||||||
|
|
||||||
<MkInput v-model="role.iconUrl">
|
<MkInput v-model="role.iconUrl" type="url">
|
||||||
<template #label>{{ i18n.ts._role.iconUrl }}</template>
|
<template #label>{{ i18n.ts._role.iconUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
|
@ -339,6 +339,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageAvatarDecorations, 'canManageAvatarDecorations'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.canManageAvatarDecorations.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ role.policies.canManageAvatarDecorations.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canManageAvatarDecorations)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.canManageAvatarDecorations.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="role.policies.canManageAvatarDecorations.value" :disabled="role.policies.canManageAvatarDecorations.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkRange v-model="role.policies.canManageAvatarDecorations.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.canSearchNotes, 'canSearchNotes'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
|
|
@ -87,6 +87,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkInput>
|
</MkInput>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageAvatarDecorations, 'canManageAvatarDecorations'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template>
|
||||||
|
<template #suffix>{{ policies.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
<MkSwitch v-model="policies.canManageAvatarDecorations">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||||
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkInput>
|
</MkInput>
|
||||||
</FormSplit>
|
</FormSplit>
|
||||||
|
|
||||||
<MkInput v-model="impressumUrl">
|
<MkInput v-model="impressumUrl" type="url">
|
||||||
<template #label>{{ i18n.ts.impressumUrl }}</template>
|
<template #label>{{ i18n.ts.impressumUrl }}</template>
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ti ti-link"></i></template>
|
||||||
<template #caption>{{ i18n.ts.impressumDescription }}</template>
|
<template #caption>{{ i18n.ts.impressumDescription }}</template>
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :contentMax="900">
|
<MkSpacer :contentMax="900">
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
|
<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
|
||||||
|
@ -35,7 +35,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import XHeader from './_header_.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
|
@ -173,6 +173,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
|
<MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
|
||||||
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
|
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
|
||||||
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
|
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
<MkSelect v-model="serverDisconnectedBehavior">
|
<MkSelect v-model="serverDisconnectedBehavior">
|
||||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||||
|
@ -295,6 +296,7 @@ const showGlobalTimeline = computed(defaultStore.makeGetterSetter('showGlobalTim
|
||||||
const showVisibilityColor = computed(defaultStore.makeGetterSetter('showVisibilityColor'))
|
const showVisibilityColor = computed(defaultStore.makeGetterSetter('showVisibilityColor'))
|
||||||
const FeaturedOrNote = computed(defaultStore.makeGetterSetter('FeaturedOrNote'))
|
const FeaturedOrNote = computed(defaultStore.makeGetterSetter('FeaturedOrNote'))
|
||||||
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
|
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
|
||||||
|
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
|
||||||
|
|
||||||
watch(lang, () => {
|
watch(lang, () => {
|
||||||
miLocalStorage.setItem('lang', lang.value as string);
|
miLocalStorage.setItem('lang', lang.value as string);
|
||||||
|
@ -359,6 +361,7 @@ watch([
|
||||||
enableonlyAndWithSave,
|
enableonlyAndWithSave,
|
||||||
FeaturedOrNote,
|
FeaturedOrNote,
|
||||||
showGlobalTimeline
|
showGlobalTimeline
|
||||||
|
disableStreamingTimeline,
|
||||||
], async () => {
|
], async () => {
|
||||||
await reloadAsk();
|
await reloadAsk();
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,6 +44,7 @@ import { $i } from '@/account.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { antennasCache, userListsCache } from '@/cache.js';
|
import { antennasCache, userListsCache } from '@/cache.js';
|
||||||
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
|
|
||||||
provide('shouldOmitHeaderTitle', true);
|
provide('shouldOmitHeaderTitle', true);
|
||||||
|
|
||||||
|
@ -140,8 +141,15 @@ function focus(): void {
|
||||||
tlComponent.focus();
|
tlComponent.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => [{
|
const headerActions = $computed(() => [
|
||||||
icon: 'ti ti-dots',
|
...[deviceKind === 'desktop' ? {
|
||||||
|
icon: 'ti ti-refresh',
|
||||||
|
text: i18n.ts.reload,
|
||||||
|
handler: (ev) => {
|
||||||
|
console.log('called');
|
||||||
|
tlComponent.reloadTimeline();
|
||||||
|
},
|
||||||
|
} : {}], {icon: 'ti ti-dots',
|
||||||
text: i18n.ts.options,
|
text: i18n.ts.options,
|
||||||
handler: (ev) => {
|
handler: (ev) => {
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
|
|
|
@ -317,6 +317,10 @@ export const routes = [{
|
||||||
}, {
|
}, {
|
||||||
path: '/custom-emojis-manager',
|
path: '/custom-emojis-manager',
|
||||||
component: page(() => import('./pages/custom-emojis-manager.vue')),
|
component: page(() => import('./pages/custom-emojis-manager.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/avatar-decorations',
|
||||||
|
name: 'avatarDecorations',
|
||||||
|
component: page(() => import('./pages/avatar-decorations.vue')),
|
||||||
}, {
|
}, {
|
||||||
path: '/registry/keys/system/:path(*)?',
|
path: '/registry/keys/system/:path(*)?',
|
||||||
component: page(() => import('./pages/registry.keys.vue')),
|
component: page(() => import('./pages/registry.keys.vue')),
|
||||||
|
@ -354,7 +358,7 @@ export const routes = [{
|
||||||
}, {
|
}, {
|
||||||
path: '/avatar-decorations',
|
path: '/avatar-decorations',
|
||||||
name: 'avatarDecorations',
|
name: 'avatarDecorations',
|
||||||
component: page(() => import('./pages/admin/avatar-decorations.vue')),
|
component: page(() => import('./pages/avatar-decorations.vue')),
|
||||||
}, {
|
}, {
|
||||||
path: '/queue',
|
path: '/queue',
|
||||||
name: 'queue',
|
name: 'queue',
|
||||||
|
|
|
@ -464,6 +464,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
disableStreamingTimeline: {
|
||||||
|
where: 'device',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,9 @@ import { $i } from '@/account.js';
|
||||||
import { url } from '@/config.js';
|
import { url } from '@/config.js';
|
||||||
|
|
||||||
let stream: Misskey.Stream | null = null;
|
let stream: Misskey.Stream | null = null;
|
||||||
|
let timeoutHeartBeat: number | null = null;
|
||||||
|
|
||||||
|
export let isReloading: boolean = false;
|
||||||
|
|
||||||
export function useStream(): Misskey.Stream {
|
export function useStream(): Misskey.Stream {
|
||||||
if (stream) return stream;
|
if (stream) return stream;
|
||||||
|
@ -17,7 +20,20 @@ export function useStream(): Misskey.Stream {
|
||||||
token: $i.token,
|
token: $i.token,
|
||||||
} : null));
|
} : null));
|
||||||
|
|
||||||
window.setTimeout(heartbeat, 1000 * 60);
|
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reloadStream() {
|
||||||
|
if (!stream) return useStream();
|
||||||
|
if (timeoutHeartBeat) window.clearTimeout(timeoutHeartBeat);
|
||||||
|
isReloading = true;
|
||||||
|
|
||||||
|
stream.close();
|
||||||
|
stream.once('_connected_', () => isReloading = false);
|
||||||
|
stream.stream.reconnect();
|
||||||
|
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||||
|
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
@ -26,5 +42,5 @@ function heartbeat(): void {
|
||||||
if (stream != null && document.visibilityState === 'visible') {
|
if (stream != null && document.visibilityState === 'visible') {
|
||||||
stream.heartbeat();
|
stream.heartbeat();
|
||||||
}
|
}
|
||||||
window.setTimeout(heartbeat, 1000 * 60);
|
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Theme } from '@/scripts/theme.js';
|
import { Theme, getBuiltinThemes } from '@/scripts/theme.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { api } from '@/os.js';
|
import { api } from '@/os.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
@ -29,6 +29,10 @@ export async function fetchThemes(): Promise<void> {
|
||||||
|
|
||||||
export async function addTheme(theme: Theme): Promise<void> {
|
export async function addTheme(theme: Theme): Promise<void> {
|
||||||
if ($i == null) return;
|
if ($i == null) return;
|
||||||
|
const builtinThemes = await getBuiltinThemes();
|
||||||
|
if (builtinThemes.some(t => t.id === theme.id)) {
|
||||||
|
throw new Error('builtin theme');
|
||||||
|
}
|
||||||
await fetchThemes();
|
await fetchThemes();
|
||||||
const themes = getThemes().concat(theme);
|
const themes = getThemes().concat(theme);
|
||||||
await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
|
await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
|
||||||
|
|
|
@ -67,6 +67,11 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||||
to: '/custom-emojis-manager',
|
to: '/custom-emojis-manager',
|
||||||
text: i18n.ts.manageCustomEmojis,
|
text: i18n.ts.manageCustomEmojis,
|
||||||
icon: 'ti ti-icons',
|
icon: 'ti ti-icons',
|
||||||
|
} : undefined, ($i && ($i.isAdmin || $i.policies.canManageAvatarDecorations)) ? {
|
||||||
|
type: 'link',
|
||||||
|
to: '/avatar-decorations',
|
||||||
|
text: i18n.ts.manageAvatarDecorations,
|
||||||
|
icon: 'ti ti-sparkles',
|
||||||
} : undefined],
|
} : undefined],
|
||||||
}, null, (instance.impressumUrl) ? {
|
}, null, (instance.impressumUrl) ? {
|
||||||
text: i18n.ts.impressum,
|
text: i18n.ts.impressum,
|
||||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onUnmounted } from 'vue';
|
import { onUnmounted } from 'vue';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream, isReloading } from '@/stream.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
@ -27,6 +27,7 @@ let hasDisconnected = $ref(false);
|
||||||
let timeoutId = $ref<number>();
|
let timeoutId = $ref<number>();
|
||||||
|
|
||||||
function onDisconnected() {
|
function onDisconnected() {
|
||||||
|
if (isReloading) return;
|
||||||
window.clearTimeout(timeoutId);
|
window.clearTimeout(timeoutId);
|
||||||
timeoutId = window.setTimeout(() => {
|
timeoutId = window.setTimeout(() => {
|
||||||
hasDisconnected = true;
|
hasDisconnected = true;
|
||||||
|
|
|
@ -398,7 +398,7 @@ $widgets-hide-threshold: 1090px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: none;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,13 @@ module.exports = {
|
||||||
{ 'blankLine': 'always', 'prev': 'function', 'next': '*' },
|
{ 'blankLine': 'always', 'prev': 'function', 'next': '*' },
|
||||||
{ 'blankLine': 'always', 'prev': '*', 'next': 'function' },
|
{ 'blankLine': 'always', 'prev': '*', 'next': 'function' },
|
||||||
],
|
],
|
||||||
|
'lines-between-class-members': ['error', {
|
||||||
|
enforce: [{
|
||||||
|
blankLine: 'always',
|
||||||
|
prev: 'method',
|
||||||
|
next: '*',
|
||||||
|
}]
|
||||||
|
}],
|
||||||
'@typescript-eslint/func-call-spacing': ['error', 'never'],
|
'@typescript-eslint/func-call-spacing': ['error', 'never'],
|
||||||
'@typescript-eslint/no-explicit-any': ['warn'],
|
'@typescript-eslint/no-explicit-any': ['warn'],
|
||||||
'@typescript-eslint/no-unused-vars': ['warn'],
|
'@typescript-eslint/no-unused-vars': ['warn'],
|
||||||
|
|
Loading…
Reference in a new issue