Merge branch 'develop' into pr/12085
This commit is contained in:
commit
6a8375b045
|
@ -14,7 +14,7 @@ jobs:
|
|||
- run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
@ -126,7 +126,7 @@ jobs:
|
|||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v3.8.1
|
||||
- uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
@ -46,7 +46,7 @@ jobs:
|
|||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v3.8.1
|
||||
- uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
@ -72,7 +72,7 @@ jobs:
|
|||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v3.8.1
|
||||
- uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -38,7 +38,7 @@ jobs:
|
|||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
@ -83,7 +83,7 @@ jobs:
|
|||
version: 7
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
- run: corepack enable
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.8.1
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -16,6 +16,10 @@
|
|||
|
||||
### General
|
||||
- Feat: アイコンデコレーション機能
|
||||
- サーバーで用意された画像をアイコンに重ねることができます
|
||||
- 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png
|
||||
- 最大でも黄色いエリア内にデコレーションを収めることを推奨します。
|
||||
- 画像は512x512pxを推奨します。
|
||||
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
|
||||
- Enhance: ローカリゼーションの更新
|
||||
- Enhance: 依存関係の更新
|
||||
|
@ -25,6 +29,9 @@
|
|||
- Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました
|
||||
- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
|
||||
https://misskey-hub.net/docs/advanced/publish-on-your-website.html
|
||||
- Enhance: スワイプしてタイムラインを再読込できるように
|
||||
- PCの場合は右上のボタンからでも再読込できます
|
||||
- Enhance: タイムラインの自動更新を無効にできるように
|
||||
- Enhance: コードのシンタックスハイライトエンジンをShikiに変更
|
||||
- AiScriptのシンタックスハイライトに対応
|
||||
- MFMでAiScriptをハイライトする場合、コードブロックの開始部分を ` ```is ` もしくは ` ```aiscript ` としてください
|
||||
|
@ -37,17 +44,22 @@
|
|||
- Fix: 「検索」MFMにおいて一部の検索キーワードが正しく認識されない問題を修正
|
||||
- Fix: 一部の言語でMisskey Webがクラッシュする問題を修正
|
||||
- Fix: チャンネルの作成・更新時に失敗した場合何も表示されない問題を修正 #11983
|
||||
- Fix: 個人カードのemojiがバッテリーになっている問題を修正
|
||||
- Fix: 標準テーマと同じIDを使用してインストールできてしまう問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: RedisへのTLのキャッシュをオフにできるように
|
||||
- Enhance: フォローしているチャンネルをフォロー解除した時(またはその逆)、タイムラインに反映される間隔を改善
|
||||
- Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました
|
||||
- 相手がMisskey v2023.11.0以降である必要があります
|
||||
- Fix: リストTLに自分のフォロワー限定投稿が含まれない問題を修正
|
||||
- Fix: ローカルタイムラインに投稿者自身の投稿への返信が含まれない問題を修正
|
||||
- Fix: 自分のフォローしているユーザーの自分のフォローしていないユーザーの visibility: followers な投稿への返信がストリーミングで流れてくる問題を修正
|
||||
- Fix: RedisへのTLキャッシュが有効の場合にHTL/LTL/STLが空になることがある問題を修正
|
||||
- Fix: STLでフォローしていないチャンネルが取得される問題を修正
|
||||
- Fix: `hashtags/trend`にてRedisからトレンドの情報が取得できない際にInternal Server Errorになる問題を修正
|
||||
- Fix: HTLをリロードまたは遡行したとき、フォローしているチャンネルのノートが含まれない問題を修正 #11765
|
||||
- Fix: HTLをリロードまたは遡行したとき、フォローしているチャンネルのノートが含まれない問題を修正 #11765 #12181
|
||||
- Fix: リノートをリノートできるのを修正
|
||||
|
||||
## 2023.10.2
|
||||
|
||||
|
|
|
@ -985,6 +985,7 @@ export interface Locale {
|
|||
"color": string;
|
||||
"manageCustomEmojis": string;
|
||||
"requestCustomEmojis": string;
|
||||
"manageAvatarDecorations": string;
|
||||
"youCannotCreateAnymore": string;
|
||||
"cannotPerformTemporary": string;
|
||||
"cannotPerformTemporaryDescription": string;
|
||||
|
@ -1028,7 +1029,7 @@ export interface Locale {
|
|||
"notesSearchNotAvailable": string;
|
||||
"license": string;
|
||||
"requestPending": string;
|
||||
"approve": string;
|
||||
"approval": string;
|
||||
"requestingEmojis": string;
|
||||
"emojiNameValidation": string;
|
||||
"unfavoriteConfirm": string;
|
||||
|
@ -1159,6 +1160,10 @@ export interface Locale {
|
|||
"angle": string;
|
||||
"flip": string;
|
||||
"showAvatarDecorations": string;
|
||||
"releaseToRefresh": string;
|
||||
"refreshing": string;
|
||||
"pullDownToRefresh": string;
|
||||
"disableStreamingTimeline": string;
|
||||
"_announcement": {
|
||||
"forExistingUsers": string;
|
||||
"forExistingUsersDescription": string;
|
||||
|
@ -1579,6 +1584,7 @@ export interface Locale {
|
|||
"inviteExpirationTime": string;
|
||||
"canManageCustomEmojis": string;
|
||||
"canRequestCustomEmojis": string;
|
||||
"canManageAvatarDecorations": string;
|
||||
"driveCapacity": string;
|
||||
"alwaysMarkNsfw": string;
|
||||
"pinMax": string;
|
||||
|
|
|
@ -982,6 +982,7 @@ unassign: "アサインを解除"
|
|||
color: "色"
|
||||
manageCustomEmojis: "カスタム絵文字の管理"
|
||||
requestCustomEmojis: "カスタム絵文字のリクエスト"
|
||||
manageAvatarDecorations: "アバターデコレーションの管理"
|
||||
youCannotCreateAnymore: "これ以上作成することはできません。"
|
||||
cannotPerformTemporary: "一時的に利用できません"
|
||||
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
|
||||
|
@ -1156,6 +1157,10 @@ detach: "外す"
|
|||
angle: "角度"
|
||||
flip: "反転"
|
||||
showAvatarDecorations: "アイコンのデコレーションを表示"
|
||||
releaseToRefresh: "離してリロード"
|
||||
refreshing: "リロード中"
|
||||
pullDownToRefresh: "引っ張ってリロード"
|
||||
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
|
@ -1500,6 +1505,7 @@ _role:
|
|||
inviteExpirationTime: "招待コードの有効期限"
|
||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||
canRequestCustomEmojis: "カスタム絵文字のリクエスト"
|
||||
canManageAvatarDecorations: "アバターデコレーションの管理"
|
||||
driveCapacity: "ドライブ容量"
|
||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||
pinMax: "ノートのピン留めの最大数"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2023.11.0-beta.4",
|
||||
"version": "2023.11.0-beta.6",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -24,6 +24,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { SearchService } from '@/core/SearchService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteDeleteService {
|
||||
|
@ -77,8 +78,8 @@ export class NoteDeleteService {
|
|||
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
||||
let renote: MiNote | null = null;
|
||||
|
||||
// if deletd note is renote
|
||||
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
|
||||
// if deleted note is renote
|
||||
if (isPureRenote(note)) {
|
||||
renote = await this.notesRepository.findOneBy({
|
||||
id: note.renoteId,
|
||||
});
|
||||
|
|
|
@ -33,6 +33,7 @@ export type RolePolicies = {
|
|||
inviteExpirationTime: number;
|
||||
canManageCustomEmojis: boolean;
|
||||
canRequestCustomEmojis: boolean;
|
||||
canManageAvatarDecorations: boolean;
|
||||
canSearchNotes: boolean;
|
||||
canUseTranslator: boolean;
|
||||
canHideAds: boolean;
|
||||
|
@ -59,6 +60,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
inviteExpirationTime: 0,
|
||||
canManageCustomEmojis: false,
|
||||
canRequestCustomEmojis: false,
|
||||
canManageAvatarDecorations: false,
|
||||
canSearchNotes: false,
|
||||
canUseTranslator: true,
|
||||
canHideAds: false,
|
||||
|
@ -309,6 +311,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
||||
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
||||
canRequestCustomEmojis: calc('canRequestCustomEmojis', vs => vs.some(v => v === true)),
|
||||
canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)),
|
||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||
|
|
|
@ -495,6 +495,7 @@ export class ApRendererService {
|
|||
preferredUsername: user.username,
|
||||
name: user.name,
|
||||
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
||||
_misskey_summary: profile.description,
|
||||
icon: avatar ? this.renderImage(avatar) : null,
|
||||
image: banner ? this.renderImage(banner) : null,
|
||||
tag,
|
||||
|
@ -644,6 +645,7 @@ export class ApRendererService {
|
|||
'_misskey_quote': 'misskey:_misskey_quote',
|
||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||
'_misskey_votes': 'misskey:_misskey_votes',
|
||||
'_misskey_summary': 'misskey:_misskey_summary',
|
||||
'isCat': 'misskey:isCat',
|
||||
// vcard
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
|
|
|
@ -319,9 +319,17 @@ export class ApPersonService implements OnModuleInit {
|
|||
emojis,
|
||||
})) 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({
|
||||
userId: user.id,
|
||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
||||
description: _description,
|
||||
url,
|
||||
fields,
|
||||
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 }, {
|
||||
url,
|
||||
fields,
|
||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
||||
description: _description,
|
||||
birthday: bday?.[0] ?? null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ export interface IObject {
|
|||
id?: string;
|
||||
name?: string | null;
|
||||
summary?: string;
|
||||
_misskey_summary?: string;
|
||||
published?: string;
|
||||
cc?: ApObject;
|
||||
to?: ApObject;
|
||||
|
|
|
@ -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 { bindThis } from '@/decorators.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 { FindOptionsWhere } from 'typeorm';
|
||||
|
||||
|
@ -88,7 +89,7 @@ export class ActivityPubServerService {
|
|||
*/
|
||||
@bindThis
|
||||
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 });
|
||||
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
|
@ -13,8 +13,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -16,7 +16,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -13,7 +13,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
|
||||
errors: {
|
||||
},
|
||||
|
|
|
@ -17,6 +17,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
@ -221,7 +222,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
if (renote == null) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -254,7 +255,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
if (reply == null) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -239,7 +239,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.where('note.channelId IN (:...followingChannelIds)', { followingChannelIds });
|
||||
qb.andWhere('note.channelId IS NULL');
|
||||
qb.orWhere('note.channelId IS NULL');
|
||||
}));
|
||||
} else {
|
||||
query.andWhere('note.channelId IS NULL');
|
||||
|
|
|
@ -183,7 +183,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const followingChannelIds = followingChannels.map(x => x.followeeId);
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where(new Brackets(qb2 => {
|
||||
qb2
|
||||
.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds })
|
||||
.andWhere('note.channelId IS NULL');
|
||||
}))
|
||||
.orWhere('note.channelId IN (:...followingChannelIds)', { followingChannelIds });
|
||||
}));
|
||||
} else if (followees.length > 0) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { common } from './common.js';
|
|||
import { version, ui, lang, updateLocale } from '@/config.js';
|
||||
import { i18n, updateI18n } from '@/i18n.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 { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||
|
@ -39,6 +39,7 @@ export async function mainBoot() {
|
|||
|
||||
let reloadDialogShowing = false;
|
||||
stream.on('_disconnected_', async () => {
|
||||
if (isReloading) return;
|
||||
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
|
||||
location.reload();
|
||||
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
|
||||
|
|
|
@ -166,6 +166,8 @@ defineExpose({
|
|||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
overscroll-behavior: none;
|
||||
|
||||
min-height: 100%;
|
||||
background: var(--bg);
|
||||
|
||||
|
|
|
@ -102,6 +102,7 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'queue', count: number): void;
|
||||
(ev: 'status', error: boolean): void;
|
||||
}>();
|
||||
|
||||
let rootEl = $shallowRef<HTMLElement>();
|
||||
|
@ -193,6 +194,11 @@ watch(queue, (a, b) => {
|
|||
emit('queue', queue.value.size);
|
||||
}, { deep: true });
|
||||
|
||||
watch(error, (n, o) => {
|
||||
if (n === o) return;
|
||||
emit('status', n);
|
||||
});
|
||||
|
||||
async function init(): Promise<void> {
|
||||
items.value = new Map();
|
||||
queue.value = new Map();
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, onUnmounted } from '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 { $i } from '@/account.js';
|
||||
import { instance } from '@/instance.js';
|
||||
|
@ -39,6 +42,7 @@ const emit = defineEmits<{
|
|||
|
||||
provide('inChannel', computed(() => props.src === 'channel'));
|
||||
|
||||
const prComponent: InstanceType<typeof MkPullToRefresh> = $ref();
|
||||
const tlComponent: InstanceType<typeof MkNotes> = $ref();
|
||||
|
||||
let tlNotesCount = 0;
|
||||
|
@ -65,75 +69,38 @@ let connection;
|
|||
let connection2;
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
const connectChannel = () => {
|
||||
if (props.src === 'antenna') {
|
||||
endpoint = 'antennas/notes';
|
||||
query = {
|
||||
antennaId: props.antenna,
|
||||
};
|
||||
connection = stream.useChannel('antenna', {
|
||||
antennaId: props.antenna,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
} else if (props.src === 'home') {
|
||||
endpoint = 'notes/timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
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') {
|
||||
endpoint = 'notes/local-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
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 === 'social') {
|
||||
endpoint = 'notes/hybrid-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
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') {
|
||||
endpoint = 'notes/global-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
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') {
|
||||
endpoint = 'notes/mentions';
|
||||
connection = stream.useChannel('main');
|
||||
connection.on('mention', prepend);
|
||||
} else if (props.src === 'directs') {
|
||||
endpoint = 'notes/mentions';
|
||||
query = {
|
||||
visibility: 'specified',
|
||||
};
|
||||
const onNote = note => {
|
||||
if (note.visibility === 'specified') {
|
||||
prepend(note);
|
||||
|
@ -141,35 +108,86 @@ if (props.src === 'antenna') {
|
|||
};
|
||||
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') {
|
||||
endpoint = 'antennas/notes';
|
||||
query = {
|
||||
antennaId: props.antenna,
|
||||
};
|
||||
} else if (props.src === 'home') {
|
||||
endpoint = 'notes/timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
} else if (props.src === 'local') {
|
||||
endpoint = 'notes/local-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
} else if (props.src === 'social') {
|
||||
endpoint = 'notes/hybrid-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
} else if (props.src === 'global') {
|
||||
endpoint = 'notes/global-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
} else if (props.src === 'mentions') {
|
||||
endpoint = 'notes/mentions';
|
||||
} else if (props.src === 'directs') {
|
||||
endpoint = 'notes/mentions';
|
||||
query = {
|
||||
visibility: 'specified',
|
||||
};
|
||||
} else if (props.src === 'list') {
|
||||
endpoint = 'notes/user-list-timeline';
|
||||
query = {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
};
|
||||
connection = stream.useChannel('userList', {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
} else if (props.src === 'channel') {
|
||||
endpoint = 'channels/timeline';
|
||||
query = {
|
||||
channelId: props.channel,
|
||||
};
|
||||
connection = stream.useChannel('channel', {
|
||||
channelId: props.channel,
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
} else if (props.src === 'role') {
|
||||
endpoint = 'roles/notes';
|
||||
query = {
|
||||
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 = {
|
||||
|
@ -178,9 +196,19 @@ const pagination = {
|
|||
params: query,
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
connection.dispose();
|
||||
if (connection2) connection2.dispose();
|
||||
const reloadTimeline = (fromPR = false) => {
|
||||
tlNotesCount = 0;
|
||||
|
||||
tlComponent.pagingComponent?.reload().then(() => {
|
||||
reloadStream();
|
||||
if (fromPR) prComponent.refreshFinished();
|
||||
});
|
||||
};
|
||||
|
||||
//const pullRefresh = () => reloadTimeline(true);
|
||||
|
||||
defineExpose({
|
||||
reloadTimeline,
|
||||
});
|
||||
|
||||
/* TODO
|
||||
|
|
|
@ -67,6 +67,7 @@ export const ROLE_POLICIES = [
|
|||
'inviteExpirationTime',
|
||||
'canManageCustomEmojis',
|
||||
'canRequestCustomEmojis',
|
||||
'canManageAvatarDecorations',
|
||||
'canSearchNotes',
|
||||
'canUseTranslator',
|
||||
'canHideAds',
|
||||
|
|
|
@ -1061,7 +1061,7 @@
|
|||
["💰", "moneybag", 6],
|
||||
["🪙", "coin", 6],
|
||||
["💳", "credit_card", 6],
|
||||
["🪫", "identification_card", 6],
|
||||
["🪪", "identification_card", 6],
|
||||
["💎", "gem", 6],
|
||||
["⚖", "balance_scale", 6],
|
||||
["🧰", "toolbox", 6],
|
||||
|
|
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInput v-model="ad.url" type="url">
|
||||
<template #label>URL</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.imageUrl">
|
||||
<MkInput v-model="ad.imageUrl" type="url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="ad.place">
|
||||
|
|
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTextarea v-model="announcement.text">
|
||||
<template #label>{{ i18n.ts.text }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="announcement.imageUrl">
|
||||
<MkInput v-model="announcement.imageUrl" type="url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="announcement.icon">
|
||||
|
|
|
@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="iconUrl">
|
||||
<MkInput v-model="iconUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="app192IconUrl">
|
||||
<MkInput v-model="app192IconUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
|
||||
<template #caption>
|
||||
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="app512IconUrl">
|
||||
<MkInput v-model="app512IconUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
|
||||
<template #caption>
|
||||
|
@ -37,27 +37,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="bannerUrl">
|
||||
<MkInput v-model="bannerUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.bannerUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="backgroundImageUrl">
|
||||
<MkInput v-model="backgroundImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="notFoundImageUrl">
|
||||
<MkInput v-model="notFoundImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.notFoundDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="infoImageUrl">
|
||||
<MkInput v-model="infoImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.nothing }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="serverErrorImageUrl">
|
||||
<MkInput v-model="serverErrorImageUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.somethingHappened }}</template>
|
||||
</MkInput>
|
||||
|
|
|
@ -118,7 +118,7 @@ const menuDef = $computed(() => [{
|
|||
}, {
|
||||
icon: 'ti ti-sparkles',
|
||||
text: i18n.ts.avatarDecorations,
|
||||
to: '/admin/avatar-decorations',
|
||||
to: '/avatar-decorations',
|
||||
active: currentPage?.route.name === 'avatarDecorations',
|
||||
}, {
|
||||
icon: 'ti ti-whirl',
|
||||
|
|
|
@ -20,12 +20,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<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 #label>{{ i18n.ts.tosUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="privacyPolicyUrl">
|
||||
<MkInput v-model="privacyPolicyUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
|
||||
</MkInput>
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
|
||||
|
||||
<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 #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
||||
</MkInput>
|
||||
|
|
|
@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.color }}</template>
|
||||
</MkColorInput>
|
||||
|
||||
<MkInput v-model="role.iconUrl">
|
||||
<MkInput v-model="role.iconUrl" type="url">
|
||||
<template #label>{{ i18n.ts._role.iconUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
|
@ -279,6 +279,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</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'])">
|
||||
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
||||
<template #suffix>
|
||||
|
|
|
@ -79,6 +79,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
</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'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
|
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
</FormSplit>
|
||||
|
||||
<MkInput v-model="impressumUrl">
|
||||
<MkInput v-model="impressumUrl" type="url">
|
||||
<template #label>{{ i18n.ts.impressumUrl }}</template>
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #caption>{{ i18n.ts.impressumDescription }}</template>
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps">
|
||||
<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>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
|
@ -151,6 +151,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
|
||||
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
|
||||
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
|
||||
<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
|
||||
</div>
|
||||
<MkSelect v-model="serverDisconnectedBehavior">
|
||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||
|
@ -253,6 +254,7 @@ const notificationPosition = computed(defaultStore.makeGetterSetter('notificatio
|
|||
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
|
||||
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
|
||||
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
|
||||
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
|
||||
|
||||
watch(lang, () => {
|
||||
miLocalStorage.setItem('lang', lang.value as string);
|
||||
|
@ -289,6 +291,7 @@ watch([
|
|||
reactionsDisplaySize,
|
||||
highlightSensitiveMedia,
|
||||
keepScreenOn,
|
||||
disableStreamingTimeline,
|
||||
], async () => {
|
||||
await reloadAsk();
|
||||
});
|
||||
|
|
|
@ -44,6 +44,7 @@ import { $i } from '@/account.js';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { antennasCache, userListsCache } from '@/cache.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
|
||||
|
@ -139,7 +140,15 @@ function focus(): void {
|
|||
tlComponent.focus();
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
const headerActions = $computed(() => [
|
||||
...[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,
|
||||
handler: (ev) => {
|
||||
|
@ -159,7 +168,8 @@ const headerActions = $computed(() => [{
|
|||
ref: $$(onlyFiles),
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
}]);
|
||||
}
|
||||
]);
|
||||
|
||||
const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({
|
||||
key: 'list:' + l.id,
|
||||
|
|
|
@ -313,6 +313,10 @@ export const routes = [{
|
|||
}, {
|
||||
path: '/custom-emojis-manager',
|
||||
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(*)?',
|
||||
component: page(() => import('./pages/registry.keys.vue')),
|
||||
|
@ -350,7 +354,7 @@ export const routes = [{
|
|||
}, {
|
||||
path: '/avatar-decorations',
|
||||
name: 'avatarDecorations',
|
||||
component: page(() => import('./pages/admin/avatar-decorations.vue')),
|
||||
component: page(() => import('./pages/avatar-decorations.vue')),
|
||||
}, {
|
||||
path: '/queue',
|
||||
name: 'queue',
|
||||
|
|
|
@ -369,6 +369,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'account',
|
||||
default: false,
|
||||
},
|
||||
disableStreamingTimeline: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
|
|
@ -9,6 +9,9 @@ import { $i } from '@/account.js';
|
|||
import { url } from '@/config.js';
|
||||
|
||||
let stream: Misskey.Stream | null = null;
|
||||
let timeoutHeartBeat: number | null = null;
|
||||
|
||||
export let isReloading: boolean = false;
|
||||
|
||||
export function useStream(): Misskey.Stream {
|
||||
if (stream) return stream;
|
||||
|
@ -17,7 +20,20 @@ export function useStream(): Misskey.Stream {
|
|||
token: $i.token,
|
||||
} : 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;
|
||||
}
|
||||
|
@ -26,5 +42,5 @@ function heartbeat(): void {
|
|||
if (stream != null && document.visibilityState === 'visible') {
|
||||
stream.heartbeat();
|
||||
}
|
||||
window.setTimeout(heartbeat, 1000 * 60);
|
||||
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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 { api } from '@/os.js';
|
||||
import { $i } from '@/account.js';
|
||||
|
@ -29,6 +29,10 @@ export async function fetchThemes(): Promise<void> {
|
|||
|
||||
export async function addTheme(theme: Theme): Promise<void> {
|
||||
if ($i == null) return;
|
||||
const builtinThemes = await getBuiltinThemes();
|
||||
if (builtinThemes.some(t => t.id === theme.id)) {
|
||||
throw new Error('builtin theme');
|
||||
}
|
||||
await fetchThemes();
|
||||
const themes = getThemes().concat(theme);
|
||||
await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
|
||||
|
|
|
@ -67,6 +67,11 @@ export function openInstanceMenu(ev: MouseEvent) {
|
|||
to: '/custom-emojis-manager',
|
||||
text: i18n.ts.manageCustomEmojis,
|
||||
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],
|
||||
}, null, (instance.impressumUrl) ? {
|
||||
text: i18n.ts.impressum,
|
||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted } from 'vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { useStream, isReloading } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -26,6 +26,7 @@ const zIndex = os.claimZIndex('high');
|
|||
let hasDisconnected = $ref(false);
|
||||
|
||||
function onDisconnected() {
|
||||
if (isReloading) return;
|
||||
hasDisconnected = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -319,7 +319,7 @@ $widgets-hide-threshold: 1090px;
|
|||
min-width: 0;
|
||||
overflow: auto;
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior: contain;
|
||||
overscroll-behavior: none;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue