aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Digitigrade/Job/ProcessIncomingPush.php6
-rw-r--r--Digitigrade/Model/Interaction.php20
-rw-r--r--Digitigrade/Model/Note.php37
-rw-r--r--Digitigrade/Model/UserAccount.php4
-rw-r--r--Digitigrade/Timeline/GlobalTimeline.php2
-rw-r--r--Digitigrade/Timeline/HomeTimeline.php26
-rw-r--r--Digitigrade/Timeline/HomeTimelineItem.php52
-rw-r--r--Digitigrade/Timeline/LocalTimeline.php2
-rw-r--r--Digitigrade/Timeline/ReasonReshared.php24
-rw-r--r--Digitigrade/Timeline/Timeline.php (renamed from Digitigrade/Timeline.php)4
-rw-r--r--Digitigrade/Timeline/TimelineIncludeable.php8
-rw-r--r--Digitigrade/Timeline/TimelineInclusionReason.php8
-rw-r--r--Digitigrade/Timeline/TimelineItem.php (renamed from Digitigrade/TimelineItem.php)2
-rw-r--r--Digitigrade/TimelineIncludeable.php6
-rw-r--r--Digitigrade/TimelineInclusionReason.php6
-rw-r--r--Digitigrade/TimelineInclusionReason/Reshared.php17
-rw-r--r--locale/en_GB.json11
-rw-r--r--migrations/20250115_173712_create_home_timeline.php18
-rw-r--r--routes/note_fragment.php3
-rw-r--r--routes/public_timelines.php15
-rw-r--r--routes/timelines.php48
-rw-r--r--templates/interaction_button.php3
-rw-r--r--templates/live_timeline_updater.php4
-rw-r--r--templates/reply_form.php6
-rw-r--r--templates/skeleton.php8
-rw-r--r--templates/timeline.php17
-rw-r--r--templates/write_note_form.php5
27 files changed, 286 insertions, 76 deletions
diff --git a/Digitigrade/Job/ProcessIncomingPush.php b/Digitigrade/Job/ProcessIncomingPush.php
index cd297ca..2d27f80 100644
--- a/Digitigrade/Job/ProcessIncomingPush.php
+++ b/Digitigrade/Job/ProcessIncomingPush.php
@@ -33,11 +33,13 @@ class ProcessIncomingPush extends Job {
break;
case 'note':
$log->info('importing note with uri ' . $this->object->self);
- Note::importFromReceivedObject($this->object);
+ $note = Note::importFromReceivedObject($this->object);
+ $note->processTimelineAdditions();
break;
case 'interaction':
$log->info('importing interaction with uri ' . $this->object->self);
- Interaction::importFromReceivedObject($this->object);
+ $interaction = Interaction::importFromReceivedObject($this->object);
+ $interaction->processTimelineAdditions();
break;
case 'extension':
throw new \RuntimeException('object type ' . $this->object->type . ' not yet implemented :(');
diff --git a/Digitigrade/Model/Interaction.php b/Digitigrade/Model/Interaction.php
index 4ca1a0c..ed4ef45 100644
--- a/Digitigrade/Model/Interaction.php
+++ b/Digitigrade/Model/Interaction.php
@@ -1,6 +1,10 @@
<?php
namespace Digitigrade\Model;
+use Digitigrade\Timeline\HomeTimelineItem;
+use Digitigrade\Timeline\ReasonReshared;
+use Digitigrade\Timeline\TimelineItem;
+
class Interaction extends PushableModel {
public ?int $id;
public string $uri;
@@ -57,6 +61,22 @@ class Interaction extends PushableModel {
return array_unique($instances);
}
+ public function processTimelineAdditions() {
+ // basically, if this is a reshare, we need to put a home timeline item
+ // for all followers who can see the shared note
+ if ($this->kind != InteractionKind::RESHARE)
+ return;
+ $reason = new ReasonReshared($this->author);
+ $item = new TimelineItem($this->target, $reason);
+
+ foreach ($this->target->getRelevantActors() as $actor) {
+ if (!$actor->isLocal)
+ return;
+ $user = UserAccount::findByLinkedActor($actor);
+ HomeTimelineItem::fromTimelineItem($item, $user)->save();
+ }
+ }
+
public function jsonSerialize(): array {
if ($this->deleted) {
return [
diff --git a/Digitigrade/Model/Note.php b/Digitigrade/Model/Note.php
index 71b25ae..7fb1221 100644
--- a/Digitigrade/Model/Note.php
+++ b/Digitigrade/Model/Note.php
@@ -2,8 +2,9 @@
namespace Digitigrade\Model;
use Digitigrade\Db;
-use Digitigrade\Job\PushObject;
-use Digitigrade\TimelineIncludeable;
+use Digitigrade\Timeline\HomeTimelineItem;
+use Digitigrade\Timeline\TimelineIncludeable;
+use Digitigrade\Timeline\TimelineItem;
class Note extends PushableModel implements TimelineIncludeable {
public ?int $id;
@@ -139,13 +140,19 @@ class Note extends PushableModel implements TimelineIncludeable {
return self::countWhere('in_reply_to = ? AND deleted = false', [$this->id]);
}
- protected function getRelevantServers(): array {
- $recipientActors = match ($this->privacy->scope) {
+ /**
+ * @return Actor[] actors who this note can be seen by, are mentioned, etc
+ */
+ public function getRelevantActors(): array {
+ return array_unique(array_merge(match ($this->privacy->scope) {
NotePrivacyScope::NONE => [],
NotePrivacyScope::MUTUALS => $this->author->findMutualFollows(),
default => $this->author->findFollowers()
- };
- $recipientActors = array_merge($recipientActors, $this->mentions, $this->privacy->alsoVisibleTo);
+ }, $this->mentions, $this->privacy->alsoVisibleTo, [$this->author]));
+ }
+
+ protected function getRelevantServers(): array {
+ $recipientActors = $this->getRelevantActors();
// reply-to and thread apex author should be covered by mentions but in case they're not:
if (isset($this->inReplyTo))
$recipientActors[] = $this->inReplyTo->author;
@@ -170,6 +177,24 @@ class Note extends PushableModel implements TimelineIncludeable {
render_template('note', ['note' => $this]);
}
+ public function toJsonReference(): mixed {
+ return $this->id;
+ }
+
+ public static function fromJsonReference(mixed $reference): self {
+ return self::find($reference);
+ }
+
+ public function processTimelineAdditions() {
+ $item = new TimelineItem($this);
+ foreach ($this->getRelevantActors() as $actor) {
+ if (!$actor->isLocal)
+ continue;
+ $user = UserAccount::findByLinkedActor($actor);
+ HomeTimelineItem::fromTimelineItem($item, $user)->save();
+ }
+ }
+
public function jsonSerialize(): array {
if ($this->deleted) {
return [
diff --git a/Digitigrade/Model/UserAccount.php b/Digitigrade/Model/UserAccount.php
index 72a9ce9..d0fa94b 100644
--- a/Digitigrade/Model/UserAccount.php
+++ b/Digitigrade/Model/UserAccount.php
@@ -59,6 +59,10 @@ class UserAccount extends Model {
return $user;
}
+ public static function findByLinkedActor(Actor $actor): ?self {
+ return self::findWhere('actor = ?', [$actor->id]);
+ }
+
public function verifyPassword(#[\SensitiveParameter] $attemptedPassword): bool {
return password_verify($attemptedPassword, $this->passwordHash);
}
diff --git a/Digitigrade/Timeline/GlobalTimeline.php b/Digitigrade/Timeline/GlobalTimeline.php
index 278b557..81eccfb 100644
--- a/Digitigrade/Timeline/GlobalTimeline.php
+++ b/Digitigrade/Timeline/GlobalTimeline.php
@@ -3,8 +3,6 @@ namespace Digitigrade\Timeline;
use Digitigrade\Db;
use Digitigrade\Model\Note;
-use Digitigrade\Timeline;
-use Digitigrade\TimelineItem;
class GlobalTimeline extends Timeline {
protected function getItems(int $limit, int $offset): array {
diff --git a/Digitigrade/Timeline/HomeTimeline.php b/Digitigrade/Timeline/HomeTimeline.php
new file mode 100644
index 0000000..c2baf3d
--- /dev/null
+++ b/Digitigrade/Timeline/HomeTimeline.php
@@ -0,0 +1,26 @@
+<?php
+namespace Digitigrade\Timeline;
+
+use Digitigrade\Model\UserAccount;
+
+class HomeTimeline extends Timeline {
+ private UserAccount $user;
+
+ public function __construct(UserAccount $user) {
+ $this->user = $user;
+ }
+
+ protected function getItems(int $limit, int $offset): array {
+ return array_map(
+ fn(HomeTimelineItem $hti) => $hti->toTimelineItem(),
+ HomeTimelineItem::findAllForUser($this->user, $limit, $offset)
+ );
+ }
+
+ public function getItemsSince(\DateTimeInterface $when, ?int $limit = null, int $offset = 0): array {
+ return array_map(
+ fn(HomeTimelineItem $hti) => $hti->toTimelineItem(),
+ HomeTimelineItem::findAllForUser($this->user, $limit, $offset, $when)
+ );
+ }
+} \ No newline at end of file
diff --git a/Digitigrade/Timeline/HomeTimelineItem.php b/Digitigrade/Timeline/HomeTimelineItem.php
new file mode 100644
index 0000000..cb2b6e4
--- /dev/null
+++ b/Digitigrade/Timeline/HomeTimelineItem.php
@@ -0,0 +1,52 @@
+<?php
+namespace Digitigrade\Timeline;
+
+use Digitigrade\Model;
+use Digitigrade\Model\UserAccount;
+
+class HomeTimelineItem extends Model {
+ public ?int $id;
+ public UserAccount $userAccount;
+ public ?string $reasonKind;
+ public ?string $reasonData;
+ public string $itemKind;
+ public string $itemData;
+ public \DateTimeImmutable $modified;
+
+ /**
+ * Creates a new home timeline entry for a particular user, but does not save it.
+ */
+ public static function fromTimelineItem(TimelineItem $item, UserAccount $user): self {
+ $hti = new self();
+ $hti->userAccount = $user;
+ $reason = $item->getReason();
+ if ($reason != null) {
+ $hti->reasonKind = $reason::class;
+ $hti->reasonData = json_encode($reason->toJsonReference());
+ }
+ $object = $item->getObject();
+ $hti->itemKind = $object::class;
+ $hti->itemData = json_encode($object->toJsonReference());
+ $hti->modified = new \DateTimeImmutable();
+ return $hti;
+ }
+
+ public function toTimelineItem(): TimelineItem {
+ $reason = null;
+ if (isset($this->reasonKind)) {
+ $reason = $this->reasonKind::fromJsonReference(json_decode($this->reasonData));
+ }
+ $object = $this->itemKind::fromJsonReference(json_decode($this->itemData));
+ return new TimelineItem($object, $reason);
+ }
+
+ public static function findAllForUser(UserAccount $user, ?int $limit = null, int $offset = 0, ?\DateTimeInterface $since = null) {
+ $where = 'user_account = ?';
+ $params = [$user->id];
+ if (isset($since)) {
+ $where .= ' AND modified > ?';
+ $params[] = $since->format('c');
+ }
+ return self::findAllWhere($where, $params, $limit, $offset, 'id DESC');
+ }
+} \ No newline at end of file
diff --git a/Digitigrade/Timeline/LocalTimeline.php b/Digitigrade/Timeline/LocalTimeline.php
index c20ada6..6129576 100644
--- a/Digitigrade/Timeline/LocalTimeline.php
+++ b/Digitigrade/Timeline/LocalTimeline.php
@@ -3,8 +3,6 @@ namespace Digitigrade\Timeline;
use Digitigrade\Db;
use Digitigrade\Model\Note;
-use Digitigrade\Timeline;
-use Digitigrade\TimelineItem;
class LocalTimeline extends Timeline {
protected function getItems(int $limit, int $offset): array {
diff --git a/Digitigrade/Timeline/ReasonReshared.php b/Digitigrade/Timeline/ReasonReshared.php
new file mode 100644
index 0000000..2ffe1b2
--- /dev/null
+++ b/Digitigrade/Timeline/ReasonReshared.php
@@ -0,0 +1,24 @@
+<?php
+namespace Digitigrade\Timeline;
+
+use Digitigrade\Model\Actor;
+
+class ReasonReshared extends TimelineInclusionReason {
+ private Actor $resharer;
+
+ public function __construct(Actor $resharer) {
+ $this->resharer = $resharer;
+ }
+
+ public function renderAsHtml() {
+ render_template('reason_reshared', ['actor' => $this->resharer]);
+ }
+
+ public function toJsonReference(): mixed {
+ return $this->resharer->id;
+ }
+
+ public static function fromJsonReference(mixed $reference): self {
+ return new self(Actor::find($reference));
+ }
+} \ No newline at end of file
diff --git a/Digitigrade/Timeline.php b/Digitigrade/Timeline/Timeline.php
index 64aabe1..b3def28 100644
--- a/Digitigrade/Timeline.php
+++ b/Digitigrade/Timeline/Timeline.php
@@ -1,7 +1,5 @@
<?php
-namespace Digitigrade;
-
-use Digitigrade\Model\Note;
+namespace Digitigrade\Timeline;
abstract class Timeline {
// TODO: currently making this number up. probably should be modifiable somewhere
diff --git a/Digitigrade/Timeline/TimelineIncludeable.php b/Digitigrade/Timeline/TimelineIncludeable.php
new file mode 100644
index 0000000..5406543
--- /dev/null
+++ b/Digitigrade/Timeline/TimelineIncludeable.php
@@ -0,0 +1,8 @@
+<?php
+namespace Digitigrade\Timeline;
+
+interface TimelineIncludeable {
+ public function renderAsHtml();
+ public function toJsonReference(): mixed;
+ public static function fromJsonReference(mixed $reference): self;
+} \ No newline at end of file
diff --git a/Digitigrade/Timeline/TimelineInclusionReason.php b/Digitigrade/Timeline/TimelineInclusionReason.php
new file mode 100644
index 0000000..6ffe76d
--- /dev/null
+++ b/Digitigrade/Timeline/TimelineInclusionReason.php
@@ -0,0 +1,8 @@
+<?php
+namespace Digitigrade\Timeline;
+
+abstract class TimelineInclusionReason {
+ abstract public function renderAsHtml();
+ abstract public function toJsonReference(): mixed;
+ abstract public static function fromJsonReference(mixed $reference): self;
+} \ No newline at end of file
diff --git a/Digitigrade/TimelineItem.php b/Digitigrade/Timeline/TimelineItem.php
index e59fbec..d6ba563 100644
--- a/Digitigrade/TimelineItem.php
+++ b/Digitigrade/Timeline/TimelineItem.php
@@ -1,5 +1,5 @@
<?php
-namespace Digitigrade;
+namespace Digitigrade\Timeline;
class TimelineItem {
private ?TimelineInclusionReason $reason;
diff --git a/Digitigrade/TimelineIncludeable.php b/Digitigrade/TimelineIncludeable.php
deleted file mode 100644
index 44433b4..0000000
--- a/Digitigrade/TimelineIncludeable.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-namespace Digitigrade;
-
-interface TimelineIncludeable {
- public function renderAsHtml();
-} \ No newline at end of file
diff --git a/Digitigrade/TimelineInclusionReason.php b/Digitigrade/TimelineInclusionReason.php
deleted file mode 100644
index e9840d1..0000000
--- a/Digitigrade/TimelineInclusionReason.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-namespace Digitigrade;
-
-abstract class TimelineInclusionReason {
- abstract public function renderAsHtml();
-} \ No newline at end of file
diff --git a/Digitigrade/TimelineInclusionReason/Reshared.php b/Digitigrade/TimelineInclusionReason/Reshared.php
deleted file mode 100644
index f185149..0000000
--- a/Digitigrade/TimelineInclusionReason/Reshared.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-namespace Digitigrade\TimelineInclusionReason;
-
-use Digitigrade\Model\Actor;
-use Digitigrade\TimelineInclusionReason;
-
-class Reshared extends TimelineInclusionReason {
- private Actor $resharer;
-
- public function __construct(Actor $resharer) {
- $this->resharer = $resharer;
- }
-
- public function renderAsHtml() {
- render_template('reason_reshared', ['actor' => $this->resharer]);
- }
-} \ No newline at end of file
diff --git a/locale/en_GB.json b/locale/en_GB.json
index d7860db..06da83e 100644
--- a/locale/en_GB.json
+++ b/locale/en_GB.json
@@ -10,18 +10,21 @@
"user.notes.placeholder": "This user hasn't posted anything yet.",
"timeline.global": "Global timeline",
"timeline.global.description": "This timeline shows all known public indexable notes.",
+ "timeline.global.shortName": "Global",
+ "timeline.local": "Local timeline",
+ "timeline.local.description": "This timeline shows all public indexable notes posted by users on this server.",
+ "timeline.local.shortName": "Local",
+ "timeline.home": "Home timeline",
+ "timeline.home.description": "This timeline shows all activity from users you follow.",
+ "timeline.home.shortName": "Home",
"note.action.like": "Like",
"note.action.dislike": "Dislike",
"note.action.reshare": "Reshare",
"note.action.reply": "Reply",
"note.info.replyTo": "In reply to %s",
- "timeline.local": "Local timeline",
- "timeline.local.description": "This timeline shows all public indexable notes posted by users on this server.",
"placeholder": "There's nothing here.",
"placeholder.moreItems": "…and %d more",
"placeholder.noMoreItems": "No more items.",
- "timeline.global.shortName": "Global",
- "timeline.local.shortName": "Local",
"digitigrade": "Digitigrade",
"note.resharedBy": "Reshared by %s",
"navigation.login": "Log in",
diff --git a/migrations/20250115_173712_create_home_timeline.php b/migrations/20250115_173712_create_home_timeline.php
new file mode 100644
index 0000000..becd1a9
--- /dev/null
+++ b/migrations/20250115_173712_create_home_timeline.php
@@ -0,0 +1,18 @@
+<?php
+
+use \Digitigrade\Db\Migrator;
+
+Migrator::getInstance()->register(20250115_173712, function (PDO $db) {
+ // items in each user's home timeline
+ $db->exec(<<<END
+ CREATE TABLE home_timeline_item (
+ id bigserial primary key,
+ user_account bigint not null references user_account,
+ reason_kind text,
+ reason_data jsonb,
+ item_kind text not null,
+ item_data jsonb not null,
+ modified timestamp with time zone not null
+ );
+ END);
+});
diff --git a/routes/note_fragment.php b/routes/note_fragment.php
index a992a61..4d8c1a6 100644
--- a/routes/note_fragment.php
+++ b/routes/note_fragment.php
@@ -22,6 +22,7 @@ Router::getInstance()->mount('/fragment/note/:id/:action', function (array $args
// we are adding a new interaction
$interaction = Interaction::create($user->actor, InteractionKind::from($action), $note);
$interaction->publish();
+ $interaction->processTimelineAdditions();
$active = true;
} else {
// we are removing the existing one
@@ -47,6 +48,7 @@ Router::getInstance()->mount('/fragment/note/:id/reply', function (array $args)
$reply->threadApex = $note->threadApex ?? $note;
$reply->save();
$reply->publish();
+ $reply->processTimelineAdditions();
}
render_template('reply_form', ['note' => $note]);
});
@@ -58,6 +60,7 @@ Router::getInstance()->mount('/fragment/note', function (array $args) {
// TODO: summary field, post language, privacy, etc
$note = Note::create($author->actor, $_POST['content'], 'unk');
$note->publish();
+ $note->processTimelineAdditions();
}
render_template('write_note_form');
}); \ No newline at end of file
diff --git a/routes/public_timelines.php b/routes/public_timelines.php
deleted file mode 100644
index b60f141..0000000
--- a/routes/public_timelines.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-use Digitigrade\Router;
-use Digitigrade\Timeline\GlobalTimeline;
-use Digitigrade\Timeline\LocalTimeline;
-
-Router::getInstance()->mount('/feed/global', function (array $args) {
- $items = (new GlobalTimeline())->getPage();
- render_template('timeline', ['kind' => 'global', 'items' => $items]);
-});
-
-Router::getInstance()->mount('/feed/local', function (array $args) {
- $items = (new LocalTimeline())->getPage();
- render_template('timeline', ['kind' => 'local', 'items' => $items]);
-}); \ No newline at end of file
diff --git a/routes/timelines.php b/routes/timelines.php
new file mode 100644
index 0000000..f70c0d6
--- /dev/null
+++ b/routes/timelines.php
@@ -0,0 +1,48 @@
+<?php
+
+use Digitigrade\HttpResponseStatus\BadRequest;
+use Digitigrade\HttpResponseStatus\TemporaryRedirect;
+use Digitigrade\Model\UserAccount;
+use Digitigrade\Router;
+use Digitigrade\Timeline\GlobalTimeline;
+use Digitigrade\Timeline\HomeTimeline;
+use Digitigrade\Timeline\LocalTimeline;
+
+Router::getInstance()->mount('/feed/global', function (array $args) {
+ $items = (new GlobalTimeline())->getPage();
+ render_template('timeline', ['kind' => 'global', 'items' => $items]);
+});
+
+Router::getInstance()->mount('/feed/local', function (array $args) {
+ $items = (new LocalTimeline())->getPage();
+ render_template('timeline', ['kind' => 'local', 'items' => $items]);
+});
+
+Router::getInstance()->mount('/feed/home', function (array $args) {
+ $user = UserAccount::findByCurrentSession();
+ if ($user == null) {
+ throw new TemporaryRedirect('/feed/global');
+ }
+ $items = (new HomeTimeline($user))->getPage();
+ render_template('timeline', ['kind' => 'home', 'items' => $items]);
+});
+
+Router::getInstance()->mount('/feed/home/partial', function (array $args) {
+ $user = UserAccount::requireByCurrentSession();
+ if (!isset($_GET['since'])) {
+ throw new BadRequest('get parameter `since` required');
+ }
+ $when = new \DateTimeImmutable($_GET['since']);
+ $items = (new HomeTimeline($user))->getItemsSince($when);
+
+ foreach ($items as $item) {
+ $item->getReason()?->renderAsHtml();
+ $item->getObject()->renderAsHtml();
+ }
+
+ render_template('live_timeline_updater', [
+ 'kind' => 'home',
+ 'isUpdate' => true,
+ 'since' => new \DateTimeImmutable()
+ ]);
+}); \ No newline at end of file
diff --git a/templates/interaction_button.php b/templates/interaction_button.php
index edd1a25..b3d072d 100644
--- a/templates/interaction_button.php
+++ b/templates/interaction_button.php
@@ -15,7 +15,8 @@ $id = "interactButton-$action-$note->id";
<div class="interactButtonContainer <?= $action . ($active ? ' active' : '') ?>">
<button title="<?= $tooltip ?>" class="icon material-symbols-outlined <?= $active ? 'active' : '' ?>"
hx-post="<?= "/fragment/note/$note->id/$action" ?>" hx-swap="outerHTML"
- hx-target="closest .interactButtonContainer" hx-disabled-elt="this" id="<?= $id ?>">
+ hx-target="closest .interactButtonContainer" hx-disabled-elt="this" id="<?= $id ?>"
+ _="on click wait 1s then trigger update on #liveTimelineUpdater">
<?= $icon ?>
</button>
<label for="<?= $id ?>" class="interactionCount"><?= $count ?? '' ?></label>
diff --git a/templates/live_timeline_updater.php b/templates/live_timeline_updater.php
new file mode 100644
index 0000000..366b417
--- /dev/null
+++ b/templates/live_timeline_updater.php
@@ -0,0 +1,4 @@
+<form hidden id="liveTimelineUpdater" hx-trigger="load delay:10s, update" hx-get="/feed/<?= $kind ?>/partial"
+ hx-target="next #timelineItems" hx-swap="afterbegin" <?= ($isUpdate ?? false) ? 'hx-swap-oob="true"' : '' ?>>
+ <input type="hidden" name="since" value="<?= $since->format('c') ?>">
+</form> \ No newline at end of file
diff --git a/templates/reply_form.php b/templates/reply_form.php
index f96eb5a..9a1d7e5 100644
--- a/templates/reply_form.php
+++ b/templates/reply_form.php
@@ -1,9 +1,9 @@
<form class="noteReplyForm" id="replyForm-<?= $note->id ?>"
- _="on submit send toggle to previous <button.reply/> on toggle or submit toggle @hidden" hidden
- hx-post="/fragment/note/<?= $note->id ?>/reply" hx-disabled-elt="find button" hx-swap="outerHTML">
+ _="on submit send toggle to previous <button.reply/> then wait 1s then trigger update on #liveTimelineUpdater on toggle or submit toggle @hidden"
+ hidden hx-post="/fragment/note/<?= $note->id ?>/reply" hx-disabled-elt="find button" hx-swap="outerHTML">
<div class="row">
<label for="replyContent-<?= $note->id ?>"><?= __('writeReply.label') ?></label>
- <textarea id="replyContent-<?= $note->id ?>" name="content" required></textarea>
+ <textarea id="replyContent-<?= $note->id ?>" name="content" required autocomplete="off"></textarea>
</div>
<div class="row">
<button class="primary"><?= __('writeReply.action') ?></button>
diff --git a/templates/skeleton.php b/templates/skeleton.php
index e678957..6eb6bd1 100644
--- a/templates/skeleton.php
+++ b/templates/skeleton.php
@@ -18,10 +18,14 @@ $user = Digitigrade\Model\UserAccount::findByCurrentSession();
<body hx-boost="true">
<header>
<nav>
- <?php call_template('navigation_links', ['links' => [
+ <?php
+ $links = $user == null ? [] : ['/feed/home' => __('timeline.home.shortName')];
+ $links = array_merge($links, [
'/feed/global' => __('timeline.global.shortName'),
'/feed/local' => __('timeline.local.shortName'),
- ]]); ?>
+ ]);
+ call_template('navigation_links', ['links' => $links]);
+ ?>
<span id="siteTitle"><?= __('digitigrade') ?></span>
<?php if ($user == null):
call_template('navigation_links', ['links' => [
diff --git a/templates/timeline.php b/templates/timeline.php
index 7ba2fbf..a65b4ce 100644
--- a/templates/timeline.php
+++ b/templates/timeline.php
@@ -1,11 +1,20 @@
<?php call_template('skeleton', ['pageTitle' => __("timeline.$kind")], function () {
global $items, $kind;
- call_template('alertbox', [
- 'variety' => 'info',
- 'message' => __("timeline.$kind.description")
- ]);
+ if ($kind != 'home') {
+ call_template('alertbox', [
+ 'variety' => 'info',
+ 'message' => __("timeline.$kind.description")
+ ]);
+ } else {
+ call_template('live_timeline_updater', [
+ 'kind' => 'home',
+ 'since' => new DateTimeImmutable()
+ ]);
+ }
+ echo '<div id="timelineItems">';
foreach ($items as $item) {
$item->getReason()?->renderAsHtml();
$item->getObject()->renderAsHtml();
}
+ echo '</div>';
}); \ No newline at end of file
diff --git a/templates/write_note_form.php b/templates/write_note_form.php
index 9b7c61c..c28dc12 100644
--- a/templates/write_note_form.php
+++ b/templates/write_note_form.php
@@ -1,7 +1,8 @@
-<form action="/todo" method="post" hx-post="/fragment/note" hx-swap="outerHTML" hx-disabled-elt="find button">
+<form action="/todo" method="post" hx-post="/fragment/note" hx-swap="outerHTML" hx-disabled-elt="find button"
+ _="on submit wait 1s then trigger update on #liveTimelineUpdater">
<div class="row">
<label for="noteContent"><?= __('writeNote.label') ?></label>
- <textarea name="content" id="noteContent" required></textarea>
+ <textarea name="content" id="noteContent" required autocomplete="off"></textarea>
</div>
<div class="row">
<button class="primary"><?= __('writeNote.action') ?></button>