feat: Add customizable auto-update setting for feeds

An auto-update feature has been implemented, allowing users to enable or disable automatic display of new posts for feeds.
Additional settings include a less conspicuous update indicator and the ability to preserve scroll position after loading new posts.
These optional enhancements improve user control and customization.
Translations for these features have also been added for English, Japanese, and Russian.
This commit is contained in:
Алексей Ермолаев 2023-07-18 01:06:06 +03:00 committed by dotterian
parent d4aeacfae6
commit e20f213982
8 changed files with 152 additions and 9 deletions

View file

@ -570,6 +570,7 @@ serverLogs: "Server logs"
deleteAll: "Delete all"
showFixedPostForm: "Display the posting form at the top of the timeline"
newNoteRecived: "There are new posts"
newNotesCount: "There are {count} new posts"
sounds: "Sounds"
listen: "Listen"
none: "None"
@ -623,7 +624,10 @@ serviceworkerInfo: "Must be enabled for push notifications."
deletedNote: "Deleted post"
invisibleNote: "Invisible post"
enableInfiniteScroll: "Automatically load more"
visibility: "Visiblility"
disableAutoUpdate: "Disable feeds auto update"
lessObtrusiveFeedUpdates: "Less obtrusive feed updates indicator"
preserveScroll: "Maintain scroll position after loading a new posts"
visibility: "Visibility"
poll: "Poll"
useCw: "Hide content"
enablePlayer: "Open video player"

View file

@ -521,6 +521,7 @@ serverLogs: "サーバーログ"
deleteAll: "全て削除"
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
newNoteRecived: "新しい投稿があります"
newNotesCount: "新しい投稿は{count}件"
sounds: "サウンド"
listen: "聴く"
none: "なし"
@ -567,6 +568,9 @@ serviceworkerInfo: "プッシュ通知を行うには有効にする必要があ
deletedNote: "削除された投稿"
invisibleNote: "非公開の投稿"
enableInfiniteScroll: "自動でもっと見る"
disableAutoUpdate: "フィードの自動更新を無効にする"
lessObtrusiveFeedUpdates: "邪魔にならないフィード更新インジケータ"
preserveScroll: "新しい投稿を読み込んだ後、スクロール位置を維持する"
visibility: "公開範囲"
poll: "アンケート"
useCw: "内容を隠す"

View file

@ -539,6 +539,7 @@ serverLogs: "Журнал сервера"
deleteAll: "Удалить всё"
showFixedPostForm: "Показывать поле для ввода нового поста наверху ленты"
newNoteRecived: "Появился новый пост"
newNotesCount: "Новых постов: {count}"
sounds: "Звуки"
listen: "Слушать"
none: "Ничего"
@ -590,6 +591,9 @@ serviceworkerInfo: "Нужно включить, чтобы работали pus
deletedNote: "Удалённый пост"
invisibleNote: "Личное сообщение"
enableInfiniteScroll: "Включить бесконечную прокрутку"
disableAutoUpdate: "Отключить автообновление лент"
lessObtrusiveFeedUpdates: "Ненавязчивый индикатор обновлений в лентах"
preserveScroll: "Сохранение позиции прокрутки после загрузки новых сообщений"
visibility: "Видимость"
poll: "Опрос"
useCw: "Скрывать содержимое под предупреждением"

View file

@ -1,9 +1,11 @@
<script lang="ts">
import type { PropType } from "vue";
import { TransitionGroup, defineComponent, h } from "vue";
import { TransitionGroup, defineComponent, h, ref } from "vue";
import MkAd from "@/components/global/MkAd.vue";
import MkButton from "@/components/MkButton.vue";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import { getScrollContainer } from "@/scripts/scroll";
export default defineComponent({
props: {
@ -28,6 +30,11 @@ export default defineComponent({
required: false,
default: false,
},
noAutoupdate: {
type: Boolean,
required: false,
default: false,
},
ad: {
type: Boolean,
required: false,
@ -47,14 +54,77 @@ export default defineComponent({
if (props.items.length === 0) return;
const renderChildren = () =>
props.items.map((item, i) => {
let lastRenderedDate = ref(props.items[0].createdAt);
let newPostsCount = 0;
let lastTopPostId: null | string = null;
const scrollToLastTopPost = (element: HTMLElement) => {
if (lastTopPostId) {
const closestTimeline = element.closest(".sqadhkmv");
if (closestTimeline) {
const elem = closestTimeline.querySelector(
`[id="${lastTopPostId}"]`,
);
if (elem instanceof HTMLElement) {
setTimeout(
() => {
let scrollContainer:
| HTMLElement
| Window
| null = getScrollContainer(elem);
let prop = "scrollTop";
if (!scrollContainer) {
scrollContainer = window;
prop = "scrollY";
}
if (scrollContainer) {
const top =
elem.getBoundingClientRect().top +
scrollContainer[prop] -
80; /* minus 80 pixels to show part of lowest new post and so old post wouldn't end up under the header (probably scroll-margin/scroll-padding would be better) */
scrollContainer.scrollTo({
top,
});
}
},
defaultStore.state.animation ? 700 : 2,
);
}
}
}
};
const showNewPosts = (event: PointerEvent) => {
lastRenderedDate.value = props.items[0].createdAt;
if (
defaultStore.state.preserveScroll &&
event.target instanceof HTMLElement
) {
scrollToLastTopPost(event.target);
}
};
const renderChildren = () => {
newPostsCount = 0;
lastTopPostId = null;
let itemsToRender = props.items;
if (props.noAutoupdate) {
itemsToRender = itemsToRender.filter((item) => {
const filtered = item.createdAt <= lastRenderedDate.value;
if (!filtered) {
newPostsCount += 1;
}
return filtered;
});
}
const renderedItems = itemsToRender.map((item, i) => {
if (!slots || !slots.default) return;
const el = slots.default({
item,
})[0];
if (el.key == null && item.id) el.key = item.id;
if (!lastTopPostId) lastTopPostId = item.id;
if (
i !== props.items.length - 1 &&
@ -105,9 +175,25 @@ export default defineComponent({
}
}
});
return () =>
if (newPostsCount) {
renderedItems.unshift(
h(
MkButton,
{
primary: true,
onClick: showNewPosts,
},
i18n.t("newNotesCount", {
count: newPostsCount,
}),
),
);
}
return renderedItems;
};
return () => {
return h(
defaultStore.state.animation ? TransitionGroup : "div",
defaultStore.state.animation
? {
@ -122,6 +208,7 @@ export default defineComponent({
},
{ default: renderChildren },
);
};
},
});
</script>
@ -212,5 +299,9 @@ export default defineComponent({
}
}
}
> ._button {
margin-inline: auto;
}
}
</style>

View file

@ -20,6 +20,7 @@
:direction="pagination.reversed ? 'up' : 'down'"
:reversed="pagination.reversed"
:no-gap="noGap"
:no-autoupdate="noAutoupdate"
:ad="true"
class="notes"
>
@ -48,6 +49,7 @@ const tlEl = ref<HTMLElement>();
const props = defineProps<{
pagination: Paging;
noGap?: boolean;
noAutoupdate?: boolean;
}>();
const pagingComponent = ref<InstanceType<typeof MkPagination>>();

View file

@ -14,6 +14,7 @@
class="_buttonPrimary _shadow"
@click="tlComponent.scrollTop()"
:class="{ instant: !$store.state.animation }"
v-if="!$store.state.lessObtrusiveFeedUpdates"
>
{{ i18n.ts.newNoteRecived }}
<i class="ph-arrow-up ph-bold"></i>
@ -22,6 +23,7 @@
<XNotes
ref="tlComponent"
:no-gap="!$store.state.showGapBetweenNotesInTimeline"
:no-autoupdate="$store.state.disableAutoUpdate"
:pagination="pagination"
@queue="(x) => (queue = x)"
/>

View file

@ -40,6 +40,18 @@
<FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{
i18n.ts.enableInfiniteScroll
}}</FormSwitch>
<FormSwitch v-model="disableAutoUpdate" class="_formBlock">{{
i18n.ts.disableAutoUpdate
}}</FormSwitch>
<FormSwitch
v-if="defaultStore.state.disableAutoUpdate"
v-model="preserveScroll"
class="_formBlock"
>{{ i18n.ts.preserveScroll }}</FormSwitch
>
<FormSwitch v-model="lessObtrusiveFeedUpdates" class="_formBlock">{{
i18n.ts.lessObtrusiveFeedUpdates
}}</FormSwitch>
<FormSwitch
v-model="useReactionPickerForContextMenu"
class="_formBlock"
@ -335,6 +347,15 @@ const instanceTicker = computed(
const enableInfiniteScroll = computed(
defaultStore.makeGetterSetter("enableInfiniteScroll"),
);
const disableAutoUpdate = computed(
defaultStore.makeGetterSetter("disableAutoUpdate"),
);
const lessObtrusiveFeedUpdates = computed(
defaultStore.makeGetterSetter("lessObtrusiveFeedUpdates"),
);
const preserveScroll = computed(
defaultStore.makeGetterSetter("preserveScroll"),
);
const enterSendsMessage = computed(
defaultStore.makeGetterSetter("enterSendsMessage"),
);

View file

@ -226,6 +226,18 @@ export const defaultStore = markRaw(
where: "device",
default: true,
},
disableAutoUpdate: {
where: "device",
default: false,
},
lessObtrusiveFeedUpdates: {
where: "device",
default: false,
},
preserveScroll: {
where: "device",
default: false,
},
useReactionPickerForContextMenu: {
where: "device",
default: false,
@ -381,7 +393,10 @@ export class ColdDeviceStorage {
sound_channel: { type: "syuilo/square-pico", volume: 1 },
};
public static watchers = [];
public static watchers: Array<{
key: any;
callback: (value: any) => void;
}> = [];
public static get<T extends keyof typeof ColdDeviceStorage.default>(
key: T,