Compare commits
3 Commits
c5235a7b2f
...
1af98b690b
Author | SHA1 | Date |
---|---|---|
|
1af98b690b | |
|
d25af911cf | |
|
df1a3742dd |
|
@ -4,11 +4,18 @@
|
||||||
-
|
-
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
-
|
- Feat: マウスでもタイムラインを引っ張って更新できるように
|
||||||
|
- アクセシビリティ設定からオフにすることもできます
|
||||||
|
- Enhance: タイムラインのパフォーマンスを向上
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775`
|
- Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775`
|
||||||
- Enhance: 連合先のソフトウェア及びバージョン名により配信停止を行えるように `#15727`
|
- Enhance: 連合先のソフトウェア及びバージョン名により配信停止を行えるように `#15727`
|
||||||
|
- Enhance: 2025.4.1 で追加されたインデックスの再生成をノートの追加しながら行えるようになりました。 `#15915`
|
||||||
|
- `MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY` 環境変数を `1` にセットしていると、巨大なテーブルの既存のカラムに関するインデックス再生成が`CREATE INDEX CONCURRENTLY`を使用するようになりました。
|
||||||
|
- 複数のサーバープロセスをクラスタリングしているサーバーにおいて、一部のプロセスが起動している状態でこのオプションを有効にしてマイグレーションすることにより、ダウンタイムを削減することができます。
|
||||||
|
- ただし、このオプションを有効にする場合、インデックスの作成にかかる時間が倍~3倍以上になることがあります。
|
||||||
|
- また、大きなインスタンスである場合にはインデックスの作成に失敗し、複数回再試行する必要がある可能性があります。
|
||||||
|
|
||||||
## 2025.4.1
|
## 2025.4.1
|
||||||
|
|
||||||
|
|
|
@ -5709,6 +5709,10 @@ export interface Locale extends ILocale {
|
||||||
* デバイス間でインストールしたテーマを同期
|
* デバイス間でインストールしたテーマを同期
|
||||||
*/
|
*/
|
||||||
"enableSyncThemesBetweenDevices": string;
|
"enableSyncThemesBetweenDevices": string;
|
||||||
|
/**
|
||||||
|
* ひっぱって更新
|
||||||
|
*/
|
||||||
|
"enablePullToRefresh": string;
|
||||||
"_chat": {
|
"_chat": {
|
||||||
/**
|
/**
|
||||||
* 送信者の名前を表示
|
* 送信者の名前を表示
|
||||||
|
|
|
@ -1427,6 +1427,7 @@ _settings:
|
||||||
ifOn: "オンのとき"
|
ifOn: "オンのとき"
|
||||||
ifOff: "オフのとき"
|
ifOff: "オフのとき"
|
||||||
enableSyncThemesBetweenDevices: "デバイス間でインストールしたテーマを同期"
|
enableSyncThemesBetweenDevices: "デバイス間でインストールしたテーマを同期"
|
||||||
|
enablePullToRefresh: "ひっぱって更新"
|
||||||
|
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "送信者の名前を表示"
|
showSenderName: "送信者の名前を表示"
|
||||||
|
|
|
@ -3,11 +3,25 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { isConcurrentIndexMigrationEnabled } from "./js/migration-config.js";
|
||||||
|
|
||||||
export class CompositeNoteIndex1745378064470 {
|
export class CompositeNoteIndex1745378064470 {
|
||||||
name = 'CompositeNoteIndex1745378064470';
|
name = 'CompositeNoteIndex1745378064470';
|
||||||
|
transaction = isConcurrentIndexMigrationEnabled() ? false : undefined;
|
||||||
|
|
||||||
async up(queryRunner) {
|
async up(queryRunner) {
|
||||||
await queryRunner.query(`CREATE INDEX "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`);
|
const concurrently = isConcurrentIndexMigrationEnabled();
|
||||||
|
|
||||||
|
if (concurrently) {
|
||||||
|
const hasValidIndex = await queryRunner.query(`SELECT indisvalid FROM pg_index INNER JOIN pg_class ON pg_index.indexrelid = pg_class.oid WHERE pg_class.relname = 'IDX_724b311e6f883751f261ebe378'`);
|
||||||
|
if (!hasValidIndex || hasValidIndex[0].indisvalid !== true) {
|
||||||
|
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
|
||||||
|
await queryRunner.query(`CREATE INDEX CONCURRENTLY "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`);
|
||||||
|
}
|
||||||
|
|
||||||
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_5b87d9d19127bd5d92026017a7"`);
|
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_5b87d9d19127bd5d92026017a7"`);
|
||||||
// Flush all cached Linear Scan Plans and redo statistics for composite index
|
// Flush all cached Linear Scan Plans and redo statistics for composite index
|
||||||
// this is important for Postgres to learn that even in highly complex queries, using this index first can reduce the result set significantly
|
// this is important for Postgres to learn that even in highly complex queries, using this index first can reduce the result set significantly
|
||||||
|
@ -15,7 +29,8 @@ export class CompositeNoteIndex1745378064470 {
|
||||||
}
|
}
|
||||||
|
|
||||||
async down(queryRunner) {
|
async down(queryRunner) {
|
||||||
|
const mayConcurrently = isConcurrentIndexMigrationEnabled() ? 'CONCURRENTLY' : '';
|
||||||
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
|
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
|
||||||
await queryRunner.query(`CREATE INDEX "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`);
|
await queryRunner.query(`CREATE INDEX ${mayConcurrently} "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function isConcurrentIndexMigrationEnabled() {
|
||||||
|
return process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { loadConfig } from './built/config.js';
|
import { loadConfig } from './built/config.js';
|
||||||
import { entities } from './built/postgres.js';
|
import { entities } from './built/postgres.js';
|
||||||
|
import { isConcurrentIndexMigrationEnabled } from "./migration/js/migration-config.js";
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
|
@ -14,4 +15,5 @@ export default new DataSource({
|
||||||
extra: config.db.extra,
|
extra: config.db.extra,
|
||||||
entities: entities,
|
entities: entities,
|
||||||
migrations: ['migration/*.js'],
|
migrations: ['migration/*.js'],
|
||||||
|
migrationsTransactionMode: isConcurrentIndexMigrationEnabled() ? 'each' : 'all',
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,8 +24,13 @@ const $config: Provider = {
|
||||||
const $db: Provider = {
|
const $db: Provider = {
|
||||||
provide: DI.db,
|
provide: DI.db,
|
||||||
useFactory: async (config) => {
|
useFactory: async (config) => {
|
||||||
const db = createPostgresDataSource(config);
|
try {
|
||||||
return await db.initialize();
|
const db = createPostgresDataSource(config);
|
||||||
|
return await db.initialize();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,16 @@ import { MiUser } from './User.js';
|
||||||
import { MiChannel } from './Channel.js';
|
import { MiChannel } from './Channel.js';
|
||||||
import type { MiDriveFile } from './DriveFile.js';
|
import type { MiDriveFile } from './DriveFile.js';
|
||||||
|
|
||||||
|
// Note: When you create a new index for existing column of this table,
|
||||||
|
// it might be better to index concurrently under isConcurrentIndexMigrationEnabled flag
|
||||||
|
// by editing generated migration file since this table is very large,
|
||||||
|
// and it will make a long lock to create index in most cases.
|
||||||
|
// Please note that `CREATE INDEX CONCURRENTLY` is not supported in transaction,
|
||||||
|
// so you need to set `transaction = false` in migration if isConcurrentIndexMigrationEnabled() is true.
|
||||||
|
// Please refer 1745378064470-composite-note-index.js for example.
|
||||||
|
// You should not use `@Index({ concurrent: true })` decorator because database initialization for test will fail
|
||||||
|
// because it will always run CREATE INDEX in transaction based on decorators.
|
||||||
|
// Not appending `{ concurrent: true }` to `@Index` will not cause any problem in production,
|
||||||
@Index(['userId', 'id'])
|
@Index(['userId', 'id'])
|
||||||
@Entity('note')
|
@Entity('note')
|
||||||
export class MiNote {
|
export class MiNote {
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkPullToRefresh :refresher="() => reload()">
|
<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reload()">
|
||||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
|
@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</MkPullToRefresh>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
|
@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="rootEl">
|
<div ref="rootEl">
|
||||||
<div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR))}px;`">
|
<div v-if="isPulling" :class="$style.frame" :style="`--frame-min-height: ${pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR))}px;`">
|
||||||
<div :class="$style.frameContent">
|
<div :class="$style.frameContent">
|
||||||
<MkLoading v-if="isRefreshing" :class="$style.loader" :em="true"/>
|
<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>
|
<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPulledEnough }]"></i>
|
||||||
<div :class="$style.text">
|
<div :class="$style.text">
|
||||||
<template v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</template>
|
<template v-if="isPulledEnough">{{ i18n.ts.releaseToRefresh }}</template>
|
||||||
<template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template>
|
<template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template>
|
||||||
<template v-else>{{ i18n.ts.pullDownToRefresh }}</template>
|
<template v-else>{{ i18n.ts.pullDownToRefresh }}</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,19 +34,16 @@ const RELEASE_TRANSITION_DURATION = 200;
|
||||||
const PULL_BRAKE_BASE = 1.5;
|
const PULL_BRAKE_BASE = 1.5;
|
||||||
const PULL_BRAKE_FACTOR = 170;
|
const PULL_BRAKE_FACTOR = 170;
|
||||||
|
|
||||||
const isPullStart = ref(false);
|
const isPulling = ref(false);
|
||||||
const isPullEnd = ref(false);
|
const isPulledEnough = ref(false);
|
||||||
const isRefreshing = ref(false);
|
const isRefreshing = ref(false);
|
||||||
const pullDistance = ref(0);
|
const pullDistance = ref(0);
|
||||||
|
|
||||||
let supportPointerDesktop = false;
|
|
||||||
let startScreenY: number | null = null;
|
let startScreenY: number | null = null;
|
||||||
|
|
||||||
const rootEl = useTemplateRef('rootEl');
|
const rootEl = useTemplateRef('rootEl');
|
||||||
let scrollEl: HTMLElement | null = null;
|
let scrollEl: HTMLElement | null = null;
|
||||||
|
|
||||||
let disabled = false;
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
refresher: () => Promise<void>;
|
refresher: () => Promise<void>;
|
||||||
}>(), {
|
}>(), {
|
||||||
|
@ -57,18 +54,51 @@ const emit = defineEmits<{
|
||||||
(ev: 'refresh'): void;
|
(ev: 'refresh'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function getScreenY(event) {
|
function getScreenY(event: TouchEvent | MouseEvent | PointerEvent): number {
|
||||||
if (supportPointerDesktop) {
|
if (event.touches && event.touches[0] && event.touches[0].screenY != null) {
|
||||||
|
return event.touches[0].screenY;
|
||||||
|
} else {
|
||||||
return event.screenY;
|
return event.screenY;
|
||||||
}
|
}
|
||||||
return event.touches[0].screenY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveStart(event) {
|
// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
|
||||||
if (!isPullStart.value && !isRefreshing.value && !disabled) {
|
function lockDownScroll() {
|
||||||
isPullStart.value = true;
|
scrollEl!.style.touchAction = 'pan-x pan-down pinch-zoom';
|
||||||
startScreenY = getScreenY(event);
|
scrollEl!.style.overscrollBehavior = 'none';
|
||||||
pullDistance.value = 0;
|
}
|
||||||
|
|
||||||
|
function unlockDownScroll() {
|
||||||
|
scrollEl!.style.touchAction = 'auto';
|
||||||
|
scrollEl!.style.overscrollBehavior = 'contain';
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveStart(event: PointerEvent) {
|
||||||
|
const scrollPos = scrollEl!.scrollTop;
|
||||||
|
if (scrollPos === 0) {
|
||||||
|
lockDownScroll();
|
||||||
|
if (!isPulling.value && !isRefreshing.value) {
|
||||||
|
isPulling.value = true;
|
||||||
|
startScreenY = getScreenY(event);
|
||||||
|
pullDistance.value = 0;
|
||||||
|
|
||||||
|
// タッチデバイスでPointerEventを使うとなんか挙動がおかしいので、TouchEventとMouseEventを使い分ける
|
||||||
|
if (event.pointerType === 'mouse') {
|
||||||
|
window.addEventListener('mousemove', moving, { passive: true });
|
||||||
|
window.addEventListener('mouseup', () => {
|
||||||
|
window.removeEventListener('mousemove', moving);
|
||||||
|
onPullRelease();
|
||||||
|
}, { passive: true, once: true });
|
||||||
|
} else {
|
||||||
|
window.addEventListener('touchmove', moving, { passive: true });
|
||||||
|
window.addEventListener('touchend', () => {
|
||||||
|
window.removeEventListener('touchmove', moving);
|
||||||
|
onPullRelease();
|
||||||
|
}, { passive: true, once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unlockDownScroll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,31 +138,39 @@ async function closeContent() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveEnd() {
|
function onPullRelease() {
|
||||||
if (isPullStart.value && !isRefreshing.value) {
|
window.document.body.removeAttribute('inert');
|
||||||
startScreenY = null;
|
startScreenY = null;
|
||||||
if (isPullEnd.value) {
|
if (isPulledEnough.value) {
|
||||||
isPullEnd.value = false;
|
isPulledEnough.value = false;
|
||||||
isRefreshing.value = true;
|
isRefreshing.value = true;
|
||||||
fixOverContent().then(() => {
|
fixOverContent().then(() => {
|
||||||
emit('refresh');
|
emit('refresh');
|
||||||
props.refresher().then(() => {
|
props.refresher().then(() => {
|
||||||
refreshFinished();
|
refreshFinished();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else {
|
});
|
||||||
closeContent().then(() => isPullStart.value = false);
|
} else {
|
||||||
}
|
closeContent().then(() => isPulling.value = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function moving(event: TouchEvent | PointerEvent) {
|
function toggleScrollLockOnTouchEnd() {
|
||||||
if (!isPullStart.value || isRefreshing.value || disabled) return;
|
const scrollPos = scrollEl!.scrollTop;
|
||||||
|
if (scrollPos === 0) {
|
||||||
|
lockDownScroll();
|
||||||
|
} else {
|
||||||
|
unlockDownScroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value) || isHorizontalSwipeSwiping.value) {
|
function moving(event: MouseEvent | TouchEvent) {
|
||||||
|
if (!isPulling.value || isRefreshing.value) return;
|
||||||
|
|
||||||
|
if ((scrollEl?.scrollTop ?? 0) > SCROLL_STOP + pullDistance.value || isHorizontalSwipeSwiping.value) {
|
||||||
pullDistance.value = 0;
|
pullDistance.value = 0;
|
||||||
isPullEnd.value = false;
|
isPulledEnough.value = false;
|
||||||
moveEnd();
|
onPullRelease();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,15 +182,12 @@ function moving(event: TouchEvent | PointerEvent) {
|
||||||
const moveHeight = moveScreenY - startScreenY!;
|
const moveHeight = moveScreenY - startScreenY!;
|
||||||
pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
|
pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
|
||||||
|
|
||||||
if (pullDistance.value > 0) {
|
// マウスでのpull時、画面上のテキスト選択が発生して画面がスクロールされたりするのを防ぐ
|
||||||
if (event.cancelable) event.preventDefault();
|
if (pullDistance.value > 3) { // ある程度遊びを持たせないと通常のクリックでも発火しクリックできなくなる
|
||||||
|
window.document.body.setAttribute('inert', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pullDistance.value > SCROLL_STOP) {
|
isPulledEnough.value = pullDistance.value >= FIRE_THRESHOLD;
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -162,61 +197,23 @@ function moving(event: TouchEvent | PointerEvent) {
|
||||||
*/
|
*/
|
||||||
function refreshFinished() {
|
function refreshFinished() {
|
||||||
closeContent().then(() => {
|
closeContent().then(() => {
|
||||||
isPullStart.value = false;
|
isPulling.value = false;
|
||||||
isRefreshing.value = false;
|
isRefreshing.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDisabled(value) {
|
|
||||||
disabled = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onScrollContainerScroll() {
|
|
||||||
const scrollPos = scrollEl!.scrollTop;
|
|
||||||
|
|
||||||
// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
|
|
||||||
if (scrollPos === 0) {
|
|
||||||
scrollEl!.style.touchAction = 'pan-x pan-down pinch-zoom';
|
|
||||||
registerEventListenersForReadyToPull();
|
|
||||||
} else {
|
|
||||||
scrollEl!.style.touchAction = 'auto';
|
|
||||||
unregisterEventListenersForReadyToPull();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerEventListenersForReadyToPull() {
|
|
||||||
if (rootEl.value == null) return;
|
|
||||||
rootEl.value.addEventListener('touchstart', moveStart, { passive: true });
|
|
||||||
rootEl.value.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
|
|
||||||
}
|
|
||||||
|
|
||||||
function unregisterEventListenersForReadyToPull() {
|
|
||||||
if (rootEl.value == null) return;
|
|
||||||
rootEl.value.removeEventListener('touchstart', moveStart);
|
|
||||||
rootEl.value.removeEventListener('touchmove', moving);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (rootEl.value == null) return;
|
if (rootEl.value == null) return;
|
||||||
|
|
||||||
scrollEl = getScrollContainer(rootEl.value);
|
scrollEl = getScrollContainer(rootEl.value);
|
||||||
if (scrollEl == null) return;
|
|
||||||
|
|
||||||
scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true });
|
rootEl.value.addEventListener('pointerdown', moveStart, { passive: true });
|
||||||
|
rootEl.value.addEventListener('touchend', toggleScrollLockOnTouchEnd, { passive: true });
|
||||||
rootEl.value.addEventListener('touchend', moveEnd, { passive: true });
|
|
||||||
|
|
||||||
registerEventListenersForReadyToPull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (scrollEl) scrollEl.removeEventListener('scroll', onScrollContainerScroll);
|
rootEl.value.removeEventListener('pointerdown', moveStart);
|
||||||
|
rootEl.value.removeEventListener('touchend', toggleScrollLockOnTouchEnd);
|
||||||
unregisterEventListenersForReadyToPull();
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
setDisabled,
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkPullToRefresh ref="prComponent" :refresher="() => reloadTimeline()">
|
<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reloadTimeline()">
|
||||||
<MkPagination v-if="paginationQuery" ref="pagingComponent" :pagination="paginationQuery" @queue="emit('queue', $event)" @status="prComponent?.setDisabled($event)">
|
<MkPagination v-if="paginationQuery" ref="pagingComponent" :pagination="paginationQuery" @queue="emit('queue', $event)">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img :src="infoImageUrl" draggable="false"/>
|
<img :src="infoImageUrl" draggable="false"/>
|
||||||
|
@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</MkPullToRefresh>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -93,7 +93,6 @@ type TimelineQueryType = {
|
||||||
roleId?: string
|
roleId?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
const prComponent = useTemplateRef('prComponent');
|
|
||||||
const pagingComponent = useTemplateRef('pagingComponent');
|
const pagingComponent = useTemplateRef('pagingComponent');
|
||||||
|
|
||||||
let tlNotesCount = 0;
|
let tlNotesCount = 0;
|
||||||
|
|
|
@ -471,6 +471,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkPreferenceContainer>
|
</MkPreferenceContainer>
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
|
<SearchMarker :keywords="['swipe', 'pull', 'refresh']">
|
||||||
|
<MkPreferenceContainer k="enablePullToRefresh">
|
||||||
|
<MkSwitch v-model="enablePullToRefresh">
|
||||||
|
<template #label><SearchLabel>{{ i18n.ts._settings.enablePullToRefresh }}</SearchLabel></template>
|
||||||
|
</MkSwitch>
|
||||||
|
</MkPreferenceContainer>
|
||||||
|
</SearchMarker>
|
||||||
|
|
||||||
<SearchMarker :keywords="['keep', 'screen', 'display', 'on']">
|
<SearchMarker :keywords="['keep', 'screen', 'display', 'on']">
|
||||||
<MkPreferenceContainer k="keepScreenOn">
|
<MkPreferenceContainer k="keepScreenOn">
|
||||||
<MkSwitch v-model="keepScreenOn">
|
<MkSwitch v-model="keepScreenOn">
|
||||||
|
@ -800,6 +808,7 @@ const animatedMfm = prefer.model('animatedMfm');
|
||||||
const disableShowingAnimatedImages = prefer.model('disableShowingAnimatedImages');
|
const disableShowingAnimatedImages = prefer.model('disableShowingAnimatedImages');
|
||||||
const keepScreenOn = prefer.model('keepScreenOn');
|
const keepScreenOn = prefer.model('keepScreenOn');
|
||||||
const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe');
|
const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe');
|
||||||
|
const enablePullToRefresh = prefer.model('enablePullToRefresh');
|
||||||
const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer');
|
const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer');
|
||||||
const contextMenu = prefer.model('contextMenu');
|
const contextMenu = prefer.model('contextMenu');
|
||||||
const menuStyle = prefer.model('menuStyle');
|
const menuStyle = prefer.model('menuStyle');
|
||||||
|
@ -857,6 +866,8 @@ watch([
|
||||||
fontSize,
|
fontSize,
|
||||||
useSystemFont,
|
useSystemFont,
|
||||||
makeEveryTextElementsSelectable,
|
makeEveryTextElementsSelectable,
|
||||||
|
enableHorizontalSwipe,
|
||||||
|
enablePullToRefresh,
|
||||||
], async () => {
|
], async () => {
|
||||||
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
|
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
|
||||||
});
|
});
|
||||||
|
|
|
@ -300,6 +300,9 @@ export const PREF_DEF = {
|
||||||
enableHorizontalSwipe: {
|
enableHorizontalSwipe: {
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
enablePullToRefresh: {
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
useNativeUiForVideoAudioPlayer: {
|
useNativeUiForVideoAudioPlayer: {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -39,6 +39,7 @@ import type { PageMetadata } from '@/page.js';
|
||||||
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
|
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
|
||||||
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
|
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
|
||||||
import XTitlebar from '@/ui/_common_/titlebar.vue';
|
import XTitlebar from '@/ui/_common_/titlebar.vue';
|
||||||
|
import XSidebar from '@/ui/_common_/navbar.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
|
@ -51,7 +52,6 @@ import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
|
||||||
const XWidgets = defineAsyncComponent(() => import('./_common_/widgets.vue'));
|
const XWidgets = defineAsyncComponent(() => import('./_common_/widgets.vue'));
|
||||||
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
|
|
||||||
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
||||||
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
|
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue