diff options
| author | winter | 2025-01-16 20:21:42 +0000 |
|---|---|---|
| committer | winter | 2025-01-16 20:21:42 +0000 |
| commit | b1d6fbc4d740324d96d7fe2677fb15b9b59d20ea (patch) | |
| tree | ae2c996cc6a42b3a42437a7ec8812cd184644817 | |
| parent | 8de5608976dc8a73a400267601acb4c8e127de5a (diff) | |
follow requests
| -rw-r--r-- | Digitigrade/Model/Actor.php | 12 | ||||
| -rw-r--r-- | Digitigrade/Model/Session.php | 10 | ||||
| -rw-r--r-- | locale/en_GB.json | 10 | ||||
| -rw-r--r-- | routes/actor_profile_fragment.php | 18 | ||||
| -rw-r--r-- | routes/homepage.php | 7 | ||||
| -rw-r--r-- | routes/pending_follows.php | 10 | ||||
| -rw-r--r-- | routes/user_auth.php | 9 | ||||
| -rw-r--r-- | static/misc.css | 12 | ||||
| -rw-r--r-- | static/profile.css | 9 | ||||
| -rw-r--r-- | static/skeleton.css | 24 | ||||
| -rw-r--r-- | templates/actor_profile.php | 10 | ||||
| -rw-r--r-- | templates/actor_profile_pending_follow.php | 21 | ||||
| -rw-r--r-- | templates/follow_requests_page.php | 9 | ||||
| -rw-r--r-- | templates/side_navigation.php | 13 | ||||
| -rw-r--r-- | templates/skeleton.php | 11 | ||||
| -rw-r--r-- | templates/unread_indicator.php | 1 |
16 files changed, 181 insertions, 5 deletions
diff --git a/Digitigrade/Model/Actor.php b/Digitigrade/Model/Actor.php index 8a37b1c..d04c841 100644 --- a/Digitigrade/Model/Actor.php +++ b/Digitigrade/Model/Actor.php @@ -206,6 +206,18 @@ class Actor extends PushableModel implements RpcReceiver { } /** + * @return Actor[] a list of actors that would like to follow this actor + */ + public function findPendingFollowers(): array { + $relations = FollowRelation::findAllWithObject($this); + $relations = array_filter( + $relations, + fn(FollowRelation $rel) => $rel->status == FollowRelationStatus::PENDING + ); + return array_map(fn(FollowRelation $rel) => $rel->subject, $relations); + } + + /** * @return Actor[] a list of actors that both follow and are followed by this actor */ public function findMutualFollows(): array { diff --git a/Digitigrade/Model/Session.php b/Digitigrade/Model/Session.php index 31fb8bb..07827c0 100644 --- a/Digitigrade/Model/Session.php +++ b/Digitigrade/Model/Session.php @@ -40,4 +40,14 @@ class Session extends Model { 'secure' => GlobalConfig::getInstance()->isHttps() ]); } + + public function clearCookie() { + setcookie('digitigrade-session', '', [ + 'expires' => (new \DateTimeImmutable('1 year ago'))->getTimestamp(), + 'httponly' => true, + 'samesite' => 'Lax', + 'path' => '/', + 'secure' => GlobalConfig::getInstance()->isHttps() + ]); + } }
\ No newline at end of file diff --git a/locale/en_GB.json b/locale/en_GB.json index cb112f5..06ce2a2 100644 --- a/locale/en_GB.json +++ b/locale/en_GB.json @@ -7,6 +7,7 @@ "user.profile.requestToFollow.action": "Request to follow", "user.profile.unfollow.action": "Unfollow", "user.profile.followsYou": "Follows you", + "user.profile.pendingFollowsYou": "Wants to follow you", "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.", @@ -32,6 +33,8 @@ "digitigrade": "Digitigrade", "note.resharedBy": "Reshared by %s", "navigation.login": "Log in", + "navigation.logout": "Log out", + "navigation.followRequests": "Follow requests", "login.pageTitle": "Log in", "login.email": "Email address", "login.password": "Password", @@ -43,5 +46,10 @@ "writeNote.scope.label": "Visibility", "writeNote.action": "Post", "writeReply.label": "Write a reply", - "writeReply.action": "Reply" + "writeReply.action": "Reply", + "followRequests.pageTitle": "Pending follow requests", + "followRequests.accept.action": "Accept", + "followRequests.accepted": "Accepted!", + "followRequests.reject.action": "Reject", + "followRequests.rejected": "Rejected!" } diff --git a/routes/actor_profile_fragment.php b/routes/actor_profile_fragment.php index 9b864d9..a3be976 100644 --- a/routes/actor_profile_fragment.php +++ b/routes/actor_profile_fragment.php @@ -20,4 +20,22 @@ Router::getInstance()->mount('/fragment/actor/:id/followButton', function (array } render_template('actor_profile_follow_button', ['user' => $user, 'actor' => $actor]); +}); + +Router::getInstance()->mount('/fragment/actor/:id/acceptPending', function (array $args) { + $user = UserAccount::requireByCurrentSession(); + $actor = Actor::find($args['id']); + + $user->actor->acceptPendingFollowFrom($actor); + + render_template('actor_profile_pending_follow', ['response' => 'accepted']); +}); + +Router::getInstance()->mount('/fragment/actor/:id/rejectPending', function (array $args) { + $user = UserAccount::requireByCurrentSession(); + $actor = Actor::find($args['id']); + + $user->actor->rejectPendingFollowFrom($actor); + + render_template('actor_profile_pending_follow', ['response' => 'rejected']); });
\ No newline at end of file diff --git a/routes/homepage.php b/routes/homepage.php index db8ea72..c99dce4 100644 --- a/routes/homepage.php +++ b/routes/homepage.php @@ -2,12 +2,17 @@ use Digitigrade\HttpResponseStatus\NotFound; use Digitigrade\HttpResponseStatus\TemporaryRedirect; +use Digitigrade\Model\UserAccount; use Digitigrade\Router; Router::getInstance()->mount('/', function (array $args) { $accept = $_SERVER['HTTP_ACCEPT'] ?? ''; if (str_contains($accept, 'text/html') || str_contains($accept, '*/*')) { - throw new TemporaryRedirect('/feed/global'); + throw new TemporaryRedirect( + UserAccount::findByCurrentSession() == null + ? '/feed/global' + : '/feed/home' + ); } throw new NotFound(); });
\ No newline at end of file diff --git a/routes/pending_follows.php b/routes/pending_follows.php new file mode 100644 index 0000000..d3bd9da --- /dev/null +++ b/routes/pending_follows.php @@ -0,0 +1,10 @@ +<?php + +use Digitigrade\Model\UserAccount; +use Digitigrade\Router; + +Router::getInstance()->mount('/followrequests', function (array $args) { + $user = UserAccount::requireByCurrentSession(); + $actors = $user->actor->findPendingFollowers(); + render_template('follow_requests_page', ['actors' => $actors]); +});
\ No newline at end of file diff --git a/routes/user_auth.php b/routes/user_auth.php index afdf8d8..49e138b 100644 --- a/routes/user_auth.php +++ b/routes/user_auth.php @@ -29,4 +29,13 @@ Router::getInstance()->mount('/login', function (array $args) { } else { render_template('login_page', ['email' => $_POST['email'] ?? '', 'failed' => $failedVerification]); } +}); + +Router::getInstance()->mount('/logout', function (array $args) { + if (isset($_COOKIE['digitigrade-session'])) { + $session = Session::findByToken($_COOKIE['digitigrade-session']); + $session->clearCookie(); + $session->remove(); + } + throw new TemporaryRedirect('/'); });
\ No newline at end of file diff --git a/static/misc.css b/static/misc.css index 3281def..9a5c198 100644 --- a/static/misc.css +++ b/static/misc.css @@ -59,6 +59,18 @@ span.inlineIcon { user-select: none; } +span.unreadIndicator { + background: #3b005e; + color: #fff; + width: 24px; + height: 24px; + border-radius: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: bold; +} + [hidden] { display: none; } diff --git a/static/profile.css b/static/profile.css index f3f8ead..3912e82 100644 --- a/static/profile.css +++ b/static/profile.css @@ -26,9 +26,16 @@ .followInfo { display: grid; - justify-content: right; + justify-content: end; align-items: baseline; grid-auto-flow: column; gap: 16px; + + .pendingInfo { + display: grid; + align-items: baseline; + grid-auto-flow: column; + gap: 16px; + } } } diff --git a/static/skeleton.css b/static/skeleton.css index a7b7009..bd68c93 100644 --- a/static/skeleton.css +++ b/static/skeleton.css @@ -1,4 +1,3 @@ -html, body { background: #fdf5ff; color: #0f070f; @@ -11,6 +10,7 @@ body { justify-items: center; min-height: 100vh; width: 100%; + overflow-y: scroll; } header { @@ -75,6 +75,28 @@ nav { } } +ul.sideNavigation { + border: 1px solid #3334; + border-radius: 8px; + margin: 16px; + padding: 0; + background: #f8e9ff5e; + list-style: none; + > li a { + display: grid; + grid-template-columns: max-content 1fr max-content; + align-items: center; + justify-content: start; + padding: 16px; + gap: 16px; + color: inherit; + text-decoration: none; + } + > li:not(:last-child) { + border-bottom: 1px solid #3334; + } +} + main { width: min(1300px, 100%); display: grid; diff --git a/templates/actor_profile.php b/templates/actor_profile.php index c480d21..0dc3ccd 100644 --- a/templates/actor_profile.php +++ b/templates/actor_profile.php @@ -5,7 +5,13 @@ $user = UserAccount::findByCurrentSession(); ?> <div class="fullProfile"> <div class="basicInfo"> - <?php call_template('actor_avatar', ['actor' => $actor]); ?> + <?php if ($addLink ?? false): ?> + <a href="/@/<?= $actor->getFullHandle() ?>"> + <?php endif; ?> + <?php call_template('actor_avatar', ['actor' => $actor]); ?> + <?php if ($addLink ?? false): ?> + </a> + <?php endif; ?> <div> <div class="displayName"><?= $actor->displayName ?></div> <div class="handle">@<?= $actor->getFullHandle() ?></div> @@ -26,6 +32,8 @@ $user = UserAccount::findByCurrentSession(); <?php if (isset($user) && $actor->id != $user->actor->id): ?> <div class="followInfo"> + <?php if ($actor->followsPending($user->actor)) + call_template('actor_profile_pending_follow', ['subject' => $actor, 'object' => $user->actor]); ?> <?php if ($actor->follows($user->actor)): ?> <?= __('user.profile.followsYou') ?> <?php endif; ?> diff --git a/templates/actor_profile_pending_follow.php b/templates/actor_profile_pending_follow.php new file mode 100644 index 0000000..2f49fcd --- /dev/null +++ b/templates/actor_profile_pending_follow.php @@ -0,0 +1,21 @@ +<?php if (isset($response)): ?> + + <div class="pendingInfo"> + <?= __("followRequests.$response") ?> + </div> + +<?php else: ?> + + <div class="pendingInfo"> + <?= __('user.profile.pendingFollowsYou') ?> + <button class="primary" hx-post="/fragment/actor/<?= $subject->id ?>/acceptPending" hx-target="closest .pendingInfo" + hx-swap="outerHTML"> + <?= __('followRequests.accept.action') ?> + </button> + <button class="secondary" hx-post="/fragment/actor/<?= $subject->id ?>/rejectPending" + hx-target="closest .pendingInfo" hx-swap="outerHTML"> + <?= __('followRequests.reject.action') ?> + </button> + </div> + +<?php endif; ?>
\ No newline at end of file diff --git a/templates/follow_requests_page.php b/templates/follow_requests_page.php new file mode 100644 index 0000000..f93601c --- /dev/null +++ b/templates/follow_requests_page.php @@ -0,0 +1,9 @@ +<?php call_template('skeleton', ['pageTitle' => __('followRequests.pageTitle')], function () { + global $actors; + if (count($actors) == 0) { + call_template('placeholder_text'); + } + foreach ($actors as $actor) { + call_template('actor_profile', ['actor' => $actor, 'addLink' => true]); + } +});
\ No newline at end of file diff --git a/templates/side_navigation.php b/templates/side_navigation.php new file mode 100644 index 0000000..d9a2dfd --- /dev/null +++ b/templates/side_navigation.php @@ -0,0 +1,13 @@ +<ul class="sideNavigation"> + <?php foreach ($links as $entry): ?> + <li> + <a href="<?= $entry['href'] ?>"> + <div class="icon material-symbols-outlined"><?= $entry['icon'] ?></div> + <div><?= $entry['label'] ?></div> + <?php if (isset($entry['unreadCount']) && $entry['unreadCount'] > 0) { + call_template('unread_indicator', ['count' => $entry['unreadCount']]); + } ?> + </a> + </li> + <?php endforeach; ?> +</ul>
\ No newline at end of file diff --git a/templates/skeleton.php b/templates/skeleton.php index 6eb6bd1..fced6b8 100644 --- a/templates/skeleton.php +++ b/templates/skeleton.php @@ -1,4 +1,6 @@ <?php + +use Digitigrade\Model\FollowRelation; $user = Digitigrade\Model\UserAccount::findByCurrentSession(); ?> <!DOCTYPE html> @@ -44,6 +46,15 @@ $user = Digitigrade\Model\UserAccount::findByCurrentSession(); <?php if ($user != null) { call_template('write_note_form'); + call_template('side_navigation', ['links' => [ + [ + 'href' => '/followrequests', + 'icon' => 'person_add', + 'label' => __('navigation.followRequests'), + 'unreadCount' => FollowRelation::countWhere("object = ? AND status = 'pending'", [$user->actor->id]) + ], + ['href' => '/logout', 'icon' => 'logout', 'label' => __('navigation.logout')] + ]]); } ?> </section> diff --git a/templates/unread_indicator.php b/templates/unread_indicator.php new file mode 100644 index 0000000..bacacc3 --- /dev/null +++ b/templates/unread_indicator.php @@ -0,0 +1 @@ +<span class="unreadIndicator"><?= $count ?></span>
\ No newline at end of file |
