WIP|Allow to mark entries as read and unread

Add new read link to each entry. If entry was read the link will be
replaced with an unread link.
Clicking the link will mark the entry as read or unread.
Afterwards a redirect to referrer happens. If it is unkown, for whatever
reason, fallback to start.
Flash messages are added and rendered to let user know what happened.

Also respect already read entries when opening a feed.

WIP:

Still need to adjust entry listing for buckets.
Still need to add tests for new feature.

Still need to add new start page to show newest unread entries from all
buckets.
This commit is contained in:
Daniel Siepmann 2020-08-14 13:03:08 +02:00
parent bed87f8a10
commit a4e4f5da0d
Signed by: Daniel Siepmann
GPG key ID: 33D6629915560EF4
14 changed files with 146 additions and 11 deletions

View file

@ -3,6 +3,10 @@
--color-foreground: #D3D7CF; --color-foreground: #D3D7CF;
--color-blue-light: #9CD9F0; --color-blue-light: #9CD9F0;
--color-blue-dark: #72B3CC; --color-blue-dark: #72B3CC;
--color-green-light: #CDEE69;
--color-green-dark: #8EB33B;
--color-yellow-light: #FFE377;
--color-yellow-dark: #D0B03C;
--color-black-light: #5D5D5D; --color-black-light: #5D5D5D;
--color-black-dark: #000000; --color-black-dark: #000000;
--color-white-light: #F7F7F7; --color-white-light: #F7F7F7;

View file

@ -5,4 +5,5 @@ body > main {
} }
} }
@import 'content/flash';
@import 'content/entries'; @import 'content/entries';

View file

@ -0,0 +1,20 @@
body > aside {
ul {
list-style: none;
color: var(--color-black-dark);
margin: var(--spacing-elements);
li {
padding: var(--spacing-small-elements);
}
&.success {
background-color: var(--color-green-dark);
border: var(--color-green-light) solid var(--width-border-default);
}
&.notice {
background-color: var(--color-yellow-dark);
border: var(--color-yellow-light) solid var(--width-border-default);
}
}
}

View file

@ -17,3 +17,11 @@ feed:
entry: entry:
path: /entry/{slug} path: /entry/{slug}
controller: App\Controller\EntryController::show controller: App\Controller\EntryController::show
entry-mark-as-read:
path: /entry/{slug}/mark/read
controller: App\Controller\EntryController::read
entry-mark-as-un-read:
path: /entry/{slug}/mark/un-read
controller: App\Controller\EntryController::unRead

View file

@ -3,6 +3,10 @@
--color-foreground: #D3D7CF; --color-foreground: #D3D7CF;
--color-blue-light: #9CD9F0; --color-blue-light: #9CD9F0;
--color-blue-dark: #72B3CC; --color-blue-dark: #72B3CC;
--color-green-light: #CDEE69;
--color-green-dark: #8EB33B;
--color-yellow-light: #FFE377;
--color-yellow-dark: #D0B03C;
--color-black-light: #5D5D5D; --color-black-light: #5D5D5D;
--color-black-dark: #000000; --color-black-dark: #000000;
--color-white-light: #F7F7F7; --color-white-light: #F7F7F7;
@ -56,6 +60,19 @@ body > main {
body > main header h1 { body > main header h1 {
margin: 0; } margin: 0; }
body > aside ul {
list-style: none;
color: var(--color-black-dark);
margin: var(--spacing-elements); }
body > aside ul li {
padding: var(--spacing-small-elements); }
body > aside ul.success {
background-color: var(--color-green-dark);
border: var(--color-green-light) solid var(--width-border-default); }
body > aside ul.notice {
background-color: var(--color-yellow-dark);
border: var(--color-yellow-light) solid var(--width-border-default); }
article { article {
display: grid; display: grid;
grid-template-columns: 50em var(--width-sidebar-max); grid-template-columns: 50em var(--width-sidebar-max);

View file

@ -3,14 +3,64 @@
namespace App\Controller; namespace App\Controller;
use App\Entity\Entry; use App\Entity\Entry;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class EntryController extends AbstractController class EntryController extends AbstractController
{ {
public function show(Entry $entry) /**
* @var EntityManagerInterface
*/
private $entityManager;
public function __construct(
EntityManagerInterface $entityManager
) {
$this->entityManager = $entityManager;
}
public function show(Entry $entry): Response
{ {
return $this->render('entry/show.html.twig', [ return $this->render('entry/show.html.twig', [
'entry' => $entry, 'entry' => $entry,
]); ]);
} }
public function read(Entry $entry, Request $request): Response
{
if ($entry->wasRead()) {
$this->addFlash('notice', sprintf('Entry "%s" was already marked as read.', $entry->getName()));
} else {
$entry->markAsRead();
$this->entityManager->flush();
$this->addFlash('success', sprintf('Entry "%s" was marked as read.', $entry->getName()));
}
return $this->getRedirectResponseAfterModification($request);
}
public function unRead(Entry $entry, Request $request): Response
{
if ($entry->wasRead()) {
$entry->markAsUnRead();
$this->entityManager->flush();
$this->addFlash('success', sprintf('Entry "%s" was marked as un read.', $entry->getName()));
} else {
$this->addFlash('notice', sprintf('Entry "%s" was not yet marked as read.', $entry->getName()));
}
return $this->getRedirectResponseAfterModification($request);
}
private function getRedirectResponseAfterModification(Request $request): Response
{
$redirectTarget = $request->headers->get('referer');
if (is_string($redirectTarget)) {
return $this->redirect($redirectTarget, 307);
}
return $this->redirectToRoute('start', [], 307);
}
} }

View file

@ -5,6 +5,7 @@ namespace App\Entity;
use App\Repository\BucketRepository; use App\Repository\BucketRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
/** /**
@ -59,9 +60,6 @@ class Bucket
return $this->slug; return $this->slug;
} }
/**
* @return Collection|Feed[]
*/
public function getFeeds(): Collection public function getFeeds(): Collection
{ {
return $this->feeds; return $this->feeds;

View file

@ -128,9 +128,18 @@ class Entry
return $this->content; return $this->content;
} }
// TODO: Rename into is / was ? public function wasRead(): bool
public function getRead(): bool
{ {
return $this->read; return $this->read;
} }
public function markAsRead(): void
{
$this->read = true;
}
public function markAsUnRead(): void
{
$this->read = false;
}
} }

View file

@ -5,6 +5,7 @@ namespace App\Entity;
use App\Repository\FeedRepository; use App\Repository\FeedRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
/** /**
@ -100,4 +101,11 @@ class Feed
{ {
return $this->entries; return $this->entries;
} }
public function getUnreadEntries(): Collection
{
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('read', false));
return $this->entries->matching($criteria);
}
} }

View file

@ -12,6 +12,16 @@
<h1><a href="{{ path('start') }}">{{ app_name }}</a></h1> <h1><a href="{{ path('start') }}">{{ app_name }}</a></h1>
</header> </header>
<aside>
{% for label, messages in app.flashes %}
<ul class="{{ label }}">
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endfor %}
</aside>
<main> <main>
{% block body %}{% endblock %} {% block body %}{% endblock %}
</main> </main>

View file

@ -9,7 +9,7 @@
</header> </header>
{# TODO: collect newest entries of all feeds in controller #} {# TODO: collect newest entries of all feeds in controller #}
{{ include('feed/entries.html.twig', {entries: bucket.feeds.first.entries | slice(0, 10) }) }} {{ include('feed/entries.html.twig', {entries: bucket.feeds.first.unreadEntries | slice(0, 10) }) }}
<aside> <aside>
<h1>Feeds in this bucket</h1> <h1>Feeds in this bucket</h1>

View file

@ -28,5 +28,11 @@
<a href="{{ path('bucket', {slug: entry.feed.bucket.slug}) }}">{{ entry.feed.bucket.name }} bucket</a> <a href="{{ path('bucket', {slug: entry.feed.bucket.slug}) }}">{{ entry.feed.bucket.name }} bucket</a>
<a href="{{ path('feed', {slug: entry.feed.slug}) }}">{{ entry.feed.name }} feed</a> <a href="{{ path('feed', {slug: entry.feed.slug}) }}">{{ entry.feed.name }} feed</a>
{% if entry.wasRead %}
<a href="{{ path('entry-mark-as-un-read', {slug: entry.slug}) }}">Mark as unread</a>
{% else %}
<a href="{{ path('entry-mark-as-read', {slug: entry.slug}) }}">Mark as read</a>
{% endif %}
</nav> </nav>
</article> </article>

View file

@ -1,3 +1,7 @@
{% for entry in entries %} {% if entries %}
{% for entry in entries %}
{{ include('entry/_single.html.twig', {entry: entry, short: true}) }} {{ include('entry/_single.html.twig', {entry: entry, short: true}) }}
{% endfor %} {% endfor %}
{% else %}
No unread entries in this feed.
{% endif %}

View file

@ -21,6 +21,6 @@
</nav> </nav>
</header> </header>
{{ include('feed/entries.html.twig', {entries: feed.entries}) }} {{ include('feed/entries.html.twig', {entries: feed.unreadEntries}) }}
{% endblock %} {% endblock %}