Skip to content

ff-220 Improved GUI of the "mark read/unread" operation. Added "undo mark as read" button #314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jan 15, 2025
Merged
1 change: 1 addition & 0 deletions changes/unreleased.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@

- ff-219 — Fixed sending User-Agent header in requests.
- ff-220 — Improved GUI of the "mark read/unread" operation. Added "undo mark as read" button.
2 changes: 1 addition & 1 deletion ffun/ffun/api/http_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ async def api_get_entries_by_ids(
) -> entities.GetEntriesByIdsResponse:
# TODO: check if belongs to user

if len(request.ids) > 10:
if len(request.ids) > settings.max_entries_details_requests:
# TODO: better error processing
raise fastapi.HTTPException(status_code=400, detail="Too many ids")

Expand Down
1 change: 1 addition & 0 deletions ffun/ffun/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Settings(BaseSettings):
max_returned_entries: int = 10000
max_feeds_suggestions_for_site: int = 100
max_entries_suggestions_for_site: int = 3
max_entries_details_requests: int = 100

model_config = pydantic_settings.SettingsConfigDict(env_prefix="FFUN_API_")

Expand Down
52 changes: 40 additions & 12 deletions site/src/components/EntryForList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,30 @@
<div
ref="entryTop"
class="flex text-lg">
<div class="flex-shrink-0 w-8 text-right pr-1">
<div :class="['flex-shrink-0', 'text-right', {'ml-8': isRead}]">
<input-marker
class="w-7 mr-2"
:marker="e.Marker.Read"
:entry-id="entryId">
<template v-slot:marked>
<span
class="text-green-700 no-underline"
title="Mark as unread">
<i class="ti ti-chevrons-left" />
</span>
</template>

<template v-slot:unmarked>
<span
class="text-orange-700 no-underline"
title="Mark as read">
<i class="ti ti-chevrons-right" />
</span>
</template>
</input-marker>
</div>

<div class="flex-shrink-0 w-8 text-center pr-1">
<value-score
:value="entry.score"
:entry-id="entry.id" />
Expand All @@ -14,15 +37,6 @@
class="w-5 h-5 align-text-bottom mx-1 inline" />
</div>

<div class="flex-shrink-0 text-right">
<input-marker
class="w-7 mr-2"
:marker="e.Marker.Read"
:entry-id="entryId"
on-text="read"
off-text="new" />
</div>

<div class="flex-grow">
<a
:href="entry.url"
Expand Down Expand Up @@ -73,7 +87,7 @@

<script lang="ts" setup>
import _ from "lodash";
import {computed, ref, useTemplateRef} from "vue";
import {computed, ref, useTemplateRef, onMounted} from "vue";
import type * as t from "@/logic/types";
import * as events from "@/logic/events";
import * as e from "@/logic/enums";
Expand Down Expand Up @@ -156,11 +170,25 @@
await entriesStore.displayEntry({entryId: entry.value.id});

if (topElement.value) {
topElement.value.scrollIntoView({behavior: "instant"});
const rect = topElement.value.getBoundingClientRect();

const isVisible =
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth);

if (!isVisible) {
topElement.value.scrollIntoView({behavior: "instant"});
}
}
}
} else {
await newsLinkOpenedEvent();
}
}

onMounted(() => {
entriesStore.requestFullEntry({entryId: properties.entryId});
});
</script>
28 changes: 8 additions & 20 deletions site/src/inputs/Marker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,25 @@
<template v-if="hasMarker">
<a
href="#"
class="marked"
@click.prevent="unmark()"
>{{ onText }}</a
>
@click.prevent="unmark()">
<slot name="marked" />
</a>
</template>

<template v-else>
<a
href="#"
class="unmarked"
@click.prevent="mark()"
>{{ offText }}</a
>
@click.prevent="mark()">
<slot name="unmarked" />
</a>
</template>
</div>
</template>

<script lang="ts" setup>
import {computed, ref} from "vue";
import * as api from "@/logic/api";
import type * as e from "@/logic/enums";
import * as e from "@/logic/enums";
import type * as t from "@/logic/types";
import {useEntriesStore} from "@/stores/entries";

Expand All @@ -32,8 +30,6 @@
const properties = defineProps<{
marker: e.Marker;
entryId: t.EntryId;
onText: string;
offText: string;
}>();

const hasMarker = computed(() => {
Expand All @@ -55,12 +51,4 @@
}
</script>

<style scoped>
.marked {
@apply text-green-700 no-underline;
}

.unmarked {
@apply text-orange-700 font-bold no-underline;
}
</style>
<style scoped></style>
35 changes: 32 additions & 3 deletions site/src/stores/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export const useEntriesStore = defineStore("entriesStore", () => {
const entries = ref<{[key: t.EntryId]: t.Entry}>({});
const requestedEntries = ref<{[key: t.EntryId]: boolean}>({});
const displayedEntryId = ref<t.EntryId | null>(null);
const readHistory = ref<t.EntryId[]>([]);

const canUndoMarkRead = computed(() => readHistory.value.length > 0);

function registerEntry(entry: t.Entry) {
if (entry.id in entries.value) {
Expand Down Expand Up @@ -82,19 +85,33 @@ export const useEntriesStore = defineStore("entriesStore", () => {
requestedEntriesTimer.start();

async function setMarker({entryId, marker}: {entryId: t.EntryId; marker: e.Marker}) {
await api.setMarker({entryId: entryId, marker: marker});
if (marker === e.Marker.Read) {
readHistory.value.push(entryId);
}

// This code must be before the actual API request
// to guarantee smooth UI transition to the new state
// otherwise the UI will be updated two times which leads to flickering
if (entryId in entries.value) {
entries.value[entryId].setMarker(marker);
}

await api.setMarker({entryId: entryId, marker: marker});
}

async function removeMarker({entryId, marker}: {entryId: t.EntryId; marker: e.Marker}) {
await api.removeMarker({entryId: entryId, marker: marker});
if (marker === e.Marker.Read) {
_.pull(readHistory.value, entryId);

hideEntry({entryId: entryId});
}

// This code must be before the actual API request, see comment above
if (entryId in entries.value) {
entries.value[entryId].removeMarker(marker);
}

await api.removeMarker({entryId: entryId, marker: marker});
}

async function displayEntry({entryId}: {entryId: t.EntryId}) {
Expand All @@ -118,6 +135,16 @@ export const useEntriesStore = defineStore("entriesStore", () => {
}
}

function undoMarkRead() {
if (readHistory.value.length === 0) {
return;
}

const entryId = readHistory.value.pop() as t.EntryId;

removeMarker({entryId: entryId, marker: e.Marker.Read});
}

return {
entries,
requestFullEntry,
Expand All @@ -126,6 +153,8 @@ export const useEntriesStore = defineStore("entriesStore", () => {
loadedEntriesReport,
displayedEntryId,
displayEntry,
hideEntry
hideEntry,
undoMarkRead,
canUndoMarkRead
};
});
2 changes: 1 addition & 1 deletion site/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
}

.ffun-form-button {
@apply ffun-input-common border-blue-300 disabled:bg-blue-700/75 py-1;
@apply ffun-input-common border-blue-300 disabled:bg-blue-100/25 py-1;
}

.ffun-file-button {
Expand Down
3 changes: 2 additions & 1 deletion site/src/values/Score.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<div
class="inline-block cursor-pointer font-semibold text-purple-700"
title="Click to see score details"
class="inline-block cursor-pointer text-purple-700"
@click.prevent="onClick()">
{{ value }}
</div>
Expand Down
11 changes: 10 additions & 1 deletion site/src/views/NewsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@
</template>

<template #side-menu-item-3>
Show read:
Show read

<config-flag
style="min-width: 2.5rem"
v-model:flag="globalSettings.showRead"
on-text="no"
off-text="yes" />

<button
class="ffun-form-button py-0 ml-1"
title='Undo last "mark read" operation'
:disabled="!entriesStore.canUndoMarkRead"
@click="entriesStore.undoMarkRead()">
</button>
</template>

<template #side-footer>
Expand Down