add: pull to refresh
This commit is contained in:
parent
97d543415d
commit
9e63971cff
|
@ -1139,6 +1139,10 @@ impressumDescription: "In some countries, like germany, the inclusion of operato
|
|||
privacyPolicy: "Privacy Policy"
|
||||
privacyPolicyUrl: "Privacy Policy URL"
|
||||
tosAndPrivacyPolicy: "Terms of Service and Privacy Policy"
|
||||
releaseToRefresh: "Release to reload"
|
||||
refreshing: "Reloading"
|
||||
pullDownToRefresh: "Pull down to reload"
|
||||
disableWebSocket: "Disable realtime update on timeline"
|
||||
_announcement:
|
||||
forExistingUsers: "Existing users only"
|
||||
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
|
||||
|
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
@ -1142,6 +1142,10 @@ export interface Locale {
|
|||
"privacyPolicy": string;
|
||||
"privacyPolicyUrl": string;
|
||||
"tosAndPrivacyPolicy": string;
|
||||
"releaseToRefresh": string;
|
||||
"refreshing": string;
|
||||
"pullDownToRefresh": string;
|
||||
"disableWebSocket": string;
|
||||
"_announcement": {
|
||||
"forExistingUsers": string;
|
||||
"forExistingUsersDescription": string;
|
||||
|
|
|
@ -1139,6 +1139,10 @@ impressumDescription: "ドイツなどの一部の国と地域では表示が義
|
|||
privacyPolicy: "プライバシーポリシー"
|
||||
privacyPolicyUrl: "プライバシーポリシーURL"
|
||||
tosAndPrivacyPolicy: "利用規約・プライバシーポリシー"
|
||||
releaseToRefresh: "離してリロード"
|
||||
refreshing: "リロード中"
|
||||
pullDownToRefresh: "引っ張ってリロード"
|
||||
disableWebSocket: "タイムラインのリアルタイム更新を無効にする"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
|
|
|
@ -166,6 +166,8 @@ defineExpose({
|
|||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
overscroll-behavior: none;
|
||||
|
||||
min-height: 100%;
|
||||
background: var(--bg);
|
||||
|
||||
|
|
|
@ -101,6 +101,7 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'queue', count: number): void;
|
||||
(ev: 'status', error: boolean): void;
|
||||
}>();
|
||||
|
||||
let rootEl = $shallowRef<HTMLElement>();
|
||||
|
@ -192,6 +193,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();
|
||||
|
|
235
packages/frontend/src/components/MkPullToRefresh.vue
Normal file
235
packages/frontend/src/components/MkPullToRefresh.vue
Normal file
|
@ -0,0 +1,235 @@
|
|||
<template>
|
||||
<div ref="rootEl" :class="$style.root">
|
||||
<div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${currentHeight}px;`">
|
||||
<div :class="$style.contents">
|
||||
<i v-if="!isRefreshing" :class="['ti', 'ti-arrow-bar-to-down', { [$style.refresh]: isPullEnd }]"></i>
|
||||
<MkLoading v-else :em="true"/>
|
||||
<p v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</p>
|
||||
<p v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</p>
|
||||
<p v-else>{{ i18n.ts.pullDownToRefresh }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
let isPullStart = $ref(false);
|
||||
let isPullEnd = $ref(false);
|
||||
let isRefreshing = $ref(false);
|
||||
|
||||
const scrollStop = 10;
|
||||
const maxFrameSize = 75;
|
||||
const refreshFrameSize = 65;
|
||||
const fixTimeMs = 200;
|
||||
let currentHeight = $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<{
|
||||
(e: 'refresh'): void;
|
||||
}>();
|
||||
|
||||
const getScrollableParentElement = (node) => {
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.scrollHeight > node.clientHeight) {
|
||||
return node;
|
||||
}
|
||||
else {
|
||||
return getScrollableParentElement(node.parentNode);
|
||||
}
|
||||
};
|
||||
|
||||
const getScreenY = (event) => {
|
||||
if (supportPointerDesktop) {
|
||||
return event.screenY;
|
||||
}
|
||||
return event.touches[0].screenY;
|
||||
};
|
||||
|
||||
const moveStart = (e) => {
|
||||
if (!isPullStart && !isRefreshing && !disabled) {
|
||||
isPullStart = true;
|
||||
startScreenY = getScreenY(e);
|
||||
currentHeight = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const moveBySystem = (to: number): Promise<void> => new Promise(r => {
|
||||
const startHeight = currentHeight;
|
||||
const overHeight = currentHeight - to;
|
||||
if (overHeight < 1) {
|
||||
r();
|
||||
return;
|
||||
}
|
||||
const startTime = Date.now();
|
||||
let intervalId = setInterval(() => {
|
||||
const time = Date.now() - startTime;
|
||||
if (time > fixTimeMs) {
|
||||
currentHeight = to;
|
||||
clearInterval(intervalId);
|
||||
r();
|
||||
return;
|
||||
}
|
||||
const nextHeight = startHeight - (overHeight / fixTimeMs) * time;
|
||||
if (currentHeight < nextHeight) return;
|
||||
currentHeight = nextHeight;
|
||||
}, 1);
|
||||
});
|
||||
|
||||
const fixOverContent = async () => {
|
||||
if (currentHeight > refreshFrameSize) {
|
||||
await moveBySystem(refreshFrameSize);
|
||||
}
|
||||
};
|
||||
|
||||
const closeContent = async () => {
|
||||
if (currentHeight > 0) {
|
||||
await moveBySystem(0);
|
||||
}
|
||||
};
|
||||
|
||||
const moveEnd = () => {
|
||||
if (isPullStart && !isRefreshing) {
|
||||
startScreenY = null;
|
||||
if (isPullEnd) {
|
||||
isPullEnd = false;
|
||||
isRefreshing = true;
|
||||
fixOverContent().then(() => emits('refresh'));
|
||||
}
|
||||
else {
|
||||
closeContent().then(() => isPullStart = false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const moving = (e) => {
|
||||
if (!isPullStart || isRefreshing || disabled) return;
|
||||
|
||||
if (!scrollEl) {
|
||||
scrollEl = getScrollableParentElement(rootEl);
|
||||
}
|
||||
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? scrollStop : scrollStop + currentHeight)) {
|
||||
currentHeight = 0;
|
||||
isPullEnd = false;
|
||||
moveEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
if (startScreenY === null) {
|
||||
startScreenY = getScreenY(e);
|
||||
}
|
||||
const moveScreenY = getScreenY(e);
|
||||
|
||||
const moveHeight = moveScreenY - startScreenY!;
|
||||
if (moveHeight < 0) {
|
||||
currentHeight = 0;
|
||||
}
|
||||
else if (moveHeight >= maxFrameSize) {
|
||||
currentHeight = maxFrameSize;
|
||||
}
|
||||
else {
|
||||
currentHeight = moveHeight;
|
||||
}
|
||||
|
||||
isPullEnd = currentHeight >= refreshFrameSize;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
supportPointerDesktop = !!window.PointerEvent && deviceKind === 'desktop';
|
||||
|
||||
if (supportPointerDesktop) {
|
||||
rootEl.addEventListener('pointerdown', moveStart);
|
||||
rootEl.addEventListener('pointerup', moveEnd);
|
||||
rootEl.addEventListener('pointermove', moving, {passive: true});
|
||||
}
|
||||
else {
|
||||
rootEl.addEventListener('touchstart', moveStart);
|
||||
rootEl.addEventListener('touchend', moveEnd);
|
||||
rootEl.addEventListener('touchmove', moving, {passive: true});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* emit(refresh)が完了したことを知らせる関数
|
||||
*
|
||||
* タイムアウトがないのでこれを最終的に実行しないと出たままになる
|
||||
*/
|
||||
const refreshFinished = () => {
|
||||
closeContent().then(() => {
|
||||
isPullStart = false;
|
||||
isRefreshing = false;
|
||||
});
|
||||
};
|
||||
|
||||
const setDisabled = (value) => {
|
||||
disabled = value;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
refreshFinished,
|
||||
setDisabled,
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.frame {
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
|
||||
width: 100%;
|
||||
min-height: var(--frame-min-height, 0px);
|
||||
|
||||
box-shadow: inset 0px -7px 10px -10px rgba(0,0,0,.1);
|
||||
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;
|
||||
|
||||
> .contents {
|
||||
position: absolute;
|
||||
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
margin: 5px 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
margin: 5px 0;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
> i, > div {
|
||||
margin: 6px 0;
|
||||
}
|
||||
> i {
|
||||
transition: transform .25s;
|
||||
|
||||
&.refresh {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
> p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -4,12 +4,15 @@ 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 MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
import { useStream, reloadStream } from '@/stream.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { $i } from '@/account.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,29 +69,73 @@ let connection;
|
|||
let connection2;
|
||||
|
||||
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 === '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') {
|
||||
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 = {
|
||||
|
@ -95,12 +143,6 @@ if (props.src === 'antenna') {
|
|||
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 = {
|
||||
|
@ -108,68 +150,44 @@ if (props.src === 'antenna') {
|
|||
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);
|
||||
}
|
||||
};
|
||||
connection = stream.useChannel('main');
|
||||
connection.on('mention', onNote);
|
||||
} 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.disableWebSocket) {
|
||||
connectChannel();
|
||||
|
||||
onUnmounted(() => {
|
||||
connection.dispose();
|
||||
if (connection2) connection2.dispose();
|
||||
});
|
||||
connection.on('note', prepend);
|
||||
}
|
||||
|
||||
const pagination = {
|
||||
|
@ -178,19 +196,17 @@ const pagination = {
|
|||
params: query,
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
connection.dispose();
|
||||
if (connection2) connection2.dispose();
|
||||
});
|
||||
|
||||
const reloadTimeline = () => {
|
||||
const reloadTimeline = (fromPR = false) => {
|
||||
tlNotesCount = 0;
|
||||
|
||||
tlComponent.pagingComponent?.reload().then(() => {
|
||||
reloadStream();
|
||||
if (fromPR) prComponent.refreshFinished();
|
||||
});
|
||||
};
|
||||
|
||||
//const pullRefresh = () => reloadTimeline(true);
|
||||
|
||||
defineExpose({
|
||||
reloadTimeline
|
||||
});
|
||||
|
|
|
@ -150,7 +150,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="disableWebSocket">{{ i18n.ts.disableWebSocket ?? 'タイムラインのリアルタイム更新を無効にする' }}</MkSwitch>
|
||||
<MkSwitch v-model="disableWebSocket">{{ i18n.ts.disableWebSocket }}</MkSwitch>
|
||||
</div>
|
||||
<MkSelect v-model="serverDisconnectedBehavior">
|
||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||
|
|
|
@ -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,34 +140,36 @@ function focus(): void {
|
|||
tlComponent.focus();
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
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) => {
|
||||
os.popupMenu([{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRenotes,
|
||||
icon: 'ti ti-repeat',
|
||||
ref: $$(withRenotes),
|
||||
}, src === 'local' || src === 'social' ? {
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
ref: $$(withReplies),
|
||||
} : undefined, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.fileAttachedOnly,
|
||||
icon: 'ti ti-photo',
|
||||
ref: $$(onlyFiles),
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
}]);
|
||||
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) => {
|
||||
os.popupMenu([{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRenotes,
|
||||
icon: 'ti ti-repeat',
|
||||
ref: $$(withRenotes),
|
||||
}, src === 'local' || src === 'social' ? {
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
ref: $$(withReplies),
|
||||
} : undefined, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.fileAttachedOnly,
|
||||
icon: 'ti ti-photo',
|
||||
ref: $$(onlyFiles),
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
}
|
||||
]);
|
||||
|
||||
const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({
|
||||
key: 'list:' + l.id,
|
||||
|
|
|
@ -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 a new issue