feat: Add webmention counts and icons for replies, likes, and reposts.
- Add new SVG icons for the "reply", "like", and "repost" actions - Update webmention info display in note template to include counts and icons for replies, likes, and reposts - Add webmention counts to FrontPageController.php and modify queries in NotesController.php - Modify WebMentionsTableSeeder.php to change URLs, commentable ID, and add new WebMentions
This commit is contained in:
parent
5bc03f36d2
commit
92098a793e
15 changed files with 179 additions and 35 deletions
|
@ -20,9 +20,17 @@ class FrontPageController extends Controller
|
||||||
*/
|
*/
|
||||||
public function index(): Response|View
|
public function index(): Response|View
|
||||||
{
|
{
|
||||||
$notes = Note::latest()->with(['media', 'client', 'place'])->get();
|
$notes = Note::latest()->with(['media', 'client', 'place'])->withCount(['webmentions AS replies' => function ($query) {
|
||||||
|
$query->where('type', 'in-reply-to');
|
||||||
|
}])
|
||||||
|
->withCount(['webmentions AS likes' => function ($query) {
|
||||||
|
$query->where('type', 'like-of');
|
||||||
|
}])
|
||||||
|
->withCount(['webmentions AS reposts' => function ($query) {
|
||||||
|
$query->where('type', 'repost-of');
|
||||||
|
}])->get();
|
||||||
$articles = Article::latest()->get();
|
$articles = Article::latest()->get();
|
||||||
$bookmarks = Bookmark::latest()->get();
|
$bookmarks = Bookmark::latest()->with('tags')->get();
|
||||||
$likes = Like::latest()->get();
|
$likes = Like::latest()->get();
|
||||||
|
|
||||||
$items = collect($notes)
|
$items = collect($notes)
|
||||||
|
|
|
@ -26,8 +26,14 @@ class NotesController extends Controller
|
||||||
{
|
{
|
||||||
$notes = Note::latest()
|
$notes = Note::latest()
|
||||||
->with('place', 'media', 'client')
|
->with('place', 'media', 'client')
|
||||||
->withCount(['webmentions As replies' => function ($query) {
|
->withCount(['webmentions AS replies' => function ($query) {
|
||||||
$query->where('type', 'in-reply-to');
|
$query->where('type', 'in-reply-to');
|
||||||
|
}])
|
||||||
|
->withCount(['webmentions AS likes' => function ($query) {
|
||||||
|
$query->where('type', 'like-of');
|
||||||
|
}])
|
||||||
|
->withCount(['webmentions AS reposts' => function ($query) {
|
||||||
|
$query->where('type', 'repost-of');
|
||||||
}])->paginate(10);
|
}])->paginate(10);
|
||||||
|
|
||||||
return view('notes.index', compact('notes'));
|
return view('notes.index', compact('notes'));
|
||||||
|
@ -39,7 +45,16 @@ class NotesController extends Controller
|
||||||
public function show(string $urlId): View|JsonResponse|Response
|
public function show(string $urlId): View|JsonResponse|Response
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$note = Note::nb60($urlId)->with('webmentions')->firstOrFail();
|
$note = Note::nb60($urlId)->with('place', 'media', 'client')
|
||||||
|
->withCount(['webmentions AS replies' => function ($query) {
|
||||||
|
$query->where('type', 'in-reply-to');
|
||||||
|
}])
|
||||||
|
->withCount(['webmentions AS likes' => function ($query) {
|
||||||
|
$query->where('type', 'like-of');
|
||||||
|
}])
|
||||||
|
->withCount(['webmentions AS reposts' => function ($query) {
|
||||||
|
$query->where('type', 'repost-of');
|
||||||
|
}])->firstOrFail();
|
||||||
} catch (ModelNotFoundException $exception) {
|
} catch (ModelNotFoundException $exception) {
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,16 @@ class SearchController extends Controller
|
||||||
|
|
||||||
/** @var Note $note */
|
/** @var Note $note */
|
||||||
foreach ($notes as $note) {
|
foreach ($notes as $note) {
|
||||||
$note->load('place', 'media', 'client');
|
$note->load('place', 'media', 'client')
|
||||||
|
->loadCount(['webmentions AS replies' => function ($query) {
|
||||||
|
$query->where('type', 'in-reply-to');
|
||||||
|
}])
|
||||||
|
->loadCount(['webmentions AS likes' => function ($query) {
|
||||||
|
$query->where('type', 'like-of');
|
||||||
|
}])
|
||||||
|
->loadCount(['webmentions AS reposts' => function ($query) {
|
||||||
|
$query->where('type', 'repost-of');
|
||||||
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('search', compact('search', 'notes'));
|
return view('search', compact('search', 'notes'));
|
||||||
|
|
|
@ -145,7 +145,7 @@ class NotesTableSeeder extends Seeder
|
||||||
|
|
||||||
$now = Carbon::now()->subHours(6);
|
$now = Carbon::now()->subHours(6);
|
||||||
$noteWithTextLinkandEmoji = Note::create([
|
$noteWithTextLinkandEmoji = Note::create([
|
||||||
'note' => 'I love https://duckduckgo.com 💕', // there’s a two-heart emoji at the end of this
|
'note' => 'I love https://kagi.com 💕', // there’s a two-heart emoji at the end of this
|
||||||
'created_at' => $now,
|
'created_at' => $now,
|
||||||
]);
|
]);
|
||||||
DB::table('notes')
|
DB::table('notes')
|
||||||
|
|
|
@ -14,23 +14,32 @@ class WebMentionsTableSeeder extends Seeder
|
||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// WebMention Aaron
|
// WebMention reply Aaron
|
||||||
WebMention::create([
|
WebMention::create([
|
||||||
'source' => 'https://aaronpk.localhost/reply/1',
|
'source' => 'https://aaronpk.localhost/reply/1',
|
||||||
'target' => config('app.url') . '/notes/E',
|
'target' => config('app.url') . '/notes/Z',
|
||||||
'commentable_id' => '14',
|
'commentable_id' => '5',
|
||||||
'commentable_type' => 'App\Models\Note',
|
'commentable_type' => 'App\Models\Note',
|
||||||
'type' => 'in-reply-to',
|
'type' => 'in-reply-to',
|
||||||
'mf2' => '{"rels": [], "items": [{"type": ["h-entry"], "properties": {"url": ["https://aaronpk.localhost/reply/1"], "name": ["Hi too"], "author": [{"type": ["h-card"], "value": "Aaron Parecki", "properties": {"url": ["https://aaronpk.localhost"], "name": ["Aaron Parecki"], "photo": ["https://aaronparecki.com/images/profile.jpg"]}}], "content": [{"html": "Hi too", "value": "Hi too"}], "published": ["' . date(DATE_W3C) . '"], "in-reply-to": ["https://aaronpk.loclahost/reply/1", "' . config('app.url') .'/notes/E"]}}]}',
|
'mf2' => '{"rels": [], "items": [{"type": ["h-entry"], "properties": {"url": ["https://aaronpk.localhost/reply/1"], "name": ["Hi too"], "author": [{"type": ["h-card"], "value": "Aaron Parecki", "properties": {"url": ["https://aaronpk.localhost"], "name": ["Aaron Parecki"], "photo": ["https://aaronparecki.com/images/profile.jpg"]}}], "content": [{"html": "Hi too", "value": "Hi too"}], "published": ["' . date(DATE_W3C) . '"], "in-reply-to": ["https://aaronpk.loclahost/reply/1", "' . config('app.url') .'/notes/E"]}}]}',
|
||||||
]);
|
]);
|
||||||
// WebMention Tantek
|
// WebMention like Tantek
|
||||||
WebMention::create([
|
WebMention::create([
|
||||||
'source' => 'http://tantek.com/',
|
'source' => 'https://tantek.com/likes/1',
|
||||||
'target' => config('app.url') . '/notes/D',
|
'target' => config('app.url') . '/notes/G',
|
||||||
'commentable_id' => '13',
|
'commentable_id' => '16',
|
||||||
'commentable_type' => 'App\Models\Note',
|
'commentable_type' => 'App\Models\Note',
|
||||||
'type' => 'in-reply-to',
|
'type' => 'like-of',
|
||||||
'mf2' => '{"rels": [], "items": [{"type": ["h-entry"], "properties": {"url": ["http://tantek.com/"], "name": ["KUTGW"], "author": [{"type": ["h-card"], "value": "Tantek Celik", "properties": {"url": ["http://tantek.com/"], "name": ["Tantek Celik"]}}], "content": [{"html": "kutgw", "value": "kutgw"}], "published": ["' . date(DATE_W3C) . '"], "in-reply-to": ["' . config('app.url') . '/notes/D"]}}]}',
|
'mf2' => '{"rels": [], "items": [{"type": ["h-entry"], "properties": {"url": ["https://tantek.com/likes/1"], "name": ["KUTGW"], "author": [{"type": ["h-card"], "value": "Tantek Celik", "properties": {"url": ["https://tantek.com/"], "name": ["Tantek Celik"], "photo": ["https://tantek.com/photo.jpg"]}}], "content": [{"html": "kutgw", "value": "kutgw"}], "published": ["' . date(DATE_W3C) . '"], "u-like-of": ["' . config('app.url') . '/notes/G"]}}]}',
|
||||||
|
]);
|
||||||
|
// WebMention repost Barry
|
||||||
|
WebMention::create([
|
||||||
|
'source' => 'https://barryfrost.com/reposts/1',
|
||||||
|
'target' => config('app.url') . '/notes/C',
|
||||||
|
'commentable_id' => '12',
|
||||||
|
'commentable_type' => 'App\Models\Note',
|
||||||
|
'type' => 'repost-of',
|
||||||
|
'mf2' => '{"rels": [], "items": [{"type": ["h-entry"], "properties": {"url": ["https://barryfrost.com/reposts/1"], "name": ["Kagi is the best"], "author": [{"type": ["h-card"], "value": "Barry Frost", "properties": {"url": ["https://barryfrost.com/"], "name": ["Barry Frost"], "photo": ["https://barryfrost.com/barryfrost.jpg"]}}], "content": [{"html": "Kagi is the Best", "value": "Kagi is the Best"}], "published": ["' . date(DATE_W3C) . '"], "u-repost-of": ["' . config('app.url') . '/notes/C"]}}]}',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -4,3 +4,4 @@
|
||||||
@import url('colours.css');
|
@import url('colours.css');
|
||||||
@import url('code.css');
|
@import url('code.css');
|
||||||
@import url('content.css');
|
@import url('content.css');
|
||||||
|
@import url('notes.css');
|
||||||
|
|
|
@ -19,6 +19,15 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
|
& .replies,
|
||||||
|
& .likes,
|
||||||
|
& .reposts {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: .5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
& .syndication-links {
|
& .syndication-links {
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
|
|
||||||
|
@ -33,3 +42,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feather {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
stroke: currentcolor;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
38
resources/css/notes.css
Normal file
38
resources/css/notes.css
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
main {
|
||||||
|
& > .u-comment {
|
||||||
|
margin-block-start: 2rem;
|
||||||
|
margin-inline-start: 2rem;
|
||||||
|
border-inline-start: 1px solid var(--color-primary);
|
||||||
|
padding-inline-start: .5rem;
|
||||||
|
|
||||||
|
& .mini-h-card {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
& .u-photo {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-block-end: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .notes-subtitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .webmentions-author-list {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
& img {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
resources/views/icons/like.blade.php
Normal file
1
resources/views/icons/like.blade.php
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
|
After Width: | Height: | Size: 372 B |
1
resources/views/icons/reply.blade.php
Normal file
1
resources/views/icons/reply.blade.php
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-message-circle"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
After Width: | Height: | Size: 429 B |
1
resources/views/icons/repost.blade.php
Normal file
1
resources/views/icons/repost.blade.php
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-repeat"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>
|
After Width: | Height: | Size: 393 B |
|
@ -25,29 +25,36 @@
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
@if($note->webmentions->filter(function ($webmention) {
|
@if($note->webmentions->filter(function ($webmention) {
|
||||||
return ($webmention->type == 'like-of');
|
return ($webmention->type === 'like-of');
|
||||||
})->count() > 0) <h1 class="notes-subtitle">Likes</h1>
|
})->count() > 0)
|
||||||
@foreach($note->webmentions->filter(function ($webmention) {
|
<h1 class="notes-subtitle">Likes</h1>
|
||||||
return ($webmention->type == 'like-of');
|
<div class="webmentions-author-list">
|
||||||
}) as $like)
|
@foreach($note->webmentions->filter(function ($webmention) {
|
||||||
<a href="{{ $like['author']['properties']['url'][0] }}"><img src="{{ $like['author']['properties']['photo'][0] }}" alt="profile picture of {{ $like['author']['properties']['name'][0] }}" class="like-photo"></a>
|
return ($webmention->type === 'like-of');
|
||||||
@endforeach
|
}) as $like)
|
||||||
|
<a href="{{ $like['author']['properties']['url'][0] }}">
|
||||||
|
<img src="{{ $like['author']['properties']['photo'][0] }}" alt="profile picture of {{ $like['author']['properties']['name'][0] }}" class="like-photo">
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if($note->webmentions->filter(function ($webmention) {
|
@if($note->webmentions->filter(function ($webmention) {
|
||||||
|
return ($webmention->type === 'repost-of');
|
||||||
|
})->count() > 0)
|
||||||
|
<h1 class="notes-subtitle">Reposts</h1>
|
||||||
|
<div class="webmentions-author-list">
|
||||||
|
@foreach($note->webmentions->filter(function ($webmention) {
|
||||||
return ($webmention->type == 'repost-of');
|
return ($webmention->type == 'repost-of');
|
||||||
})->count() > 0) <h1 class="notes-subtitle">Reposts</h1>
|
}) as $repost)
|
||||||
@foreach($note->webmentions->filter(function ($webmention) {
|
<a href="{{ $repost['source'] }}">
|
||||||
return ($webmention->type == 'repost-of');
|
<img src="{{ $repost['author']['properties']['photo'][0] }}" alt="{{ $repost['author']['properties']['name'][0] }} reposted this at {{ $repost['published'] }}">
|
||||||
}) as $repost)
|
</a>
|
||||||
<p>
|
@endforeach
|
||||||
<a class="h-card vcard mini-h-card p-author" href="{{ $repost['author']['properties']['url'][0] }}">
|
</div>
|
||||||
<img src="{{ $repost['author']['properties']['photo'][0] }}" alt="profile picture of {{ $repost['author']['properties']['name'][0] }}" class="photo u-photo logo"> <span class="fn">{{ $repost['author']['properties']['name'][0] }}</span>
|
|
||||||
</a> reposted this at <a href="{{ $repost['source'] }}">{{ $repost['published'] }}</a>.
|
|
||||||
</p>
|
|
||||||
@endforeach
|
|
||||||
@endif
|
@endif
|
||||||
@stop
|
@stop
|
||||||
|
|
||||||
@section('scripts')
|
@section('scripts')
|
||||||
|
@parent
|
||||||
<link rel="stylesheet" href="/assets/highlight/zenburn.css">
|
<link rel="stylesheet" href="/assets/highlight/zenburn.css">
|
||||||
@stop
|
@stop
|
||||||
|
|
|
@ -40,6 +40,31 @@
|
||||||
in <span class="p-location h-adr">{!! $note->address !!}<data class="p-latitude" value="{{ $note->latitude }}"></data><data class="p-longitude" value="{{ $note->longitude }}"></data></span>
|
in <span class="p-location h-adr">{!! $note->address !!}<data class="p-latitude" value="{{ $note->latitude }}"></data><data class="p-longitude" value="{{ $note->longitude }}"></data></span>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
@if($note->replies > 0 || $note->likes > 0 || $note->reposts > 0)
|
||||||
|
<div class="webmention-info">
|
||||||
|
@if($note->replies > 0)
|
||||||
|
<div class="replies">
|
||||||
|
@include('icons.reply')
|
||||||
|
{{ $note->replies }}
|
||||||
|
<span class="sr-only">replies</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if($note->likes > 0)
|
||||||
|
<div class="likes">
|
||||||
|
@include('icons.like')
|
||||||
|
{{ $note->likes }}
|
||||||
|
<span class="sr-only">likes</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if($note->reposts > 0)
|
||||||
|
<div class="reposts">
|
||||||
|
@include('icons.repost')
|
||||||
|
{{ $note->reposts }}
|
||||||
|
<span class="sr-only">reposts</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
<div class="syndication-links">
|
<div class="syndication-links">
|
||||||
@if(
|
@if(
|
||||||
$note->tweet_id ||
|
$note->tweet_id ||
|
||||||
|
|
Loading…
Add table
Reference in a new issue