aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Digitigrade/Model/Instance.php9
-rw-r--r--Digitigrade/Model/Note.php2
-rw-r--r--locale/en_GB.json6
-rw-r--r--routes/note.php19
-rw-r--r--routes/note_fragment.php27
-rw-r--r--static/form.css8
-rw-r--r--static/misc.css1
-rw-r--r--static/note.css16
-rw-r--r--templates/note.php33
-rw-r--r--templates/reply_form.php5
-rw-r--r--templates/write_note_form.php13
11 files changed, 122 insertions, 17 deletions
diff --git a/Digitigrade/Model/Instance.php b/Digitigrade/Model/Instance.php
index 6662d7e..5545849 100644
--- a/Digitigrade/Model/Instance.php
+++ b/Digitigrade/Model/Instance.php
@@ -4,6 +4,7 @@ namespace Digitigrade\Model;
use Digitigrade\Exception\AuthException;
use Digitigrade\Exception\EndpointMissingException;
use Digitigrade\GlobalConfig;
+use Digitigrade\HttpResponseStatus\Unauthorized;
use Digitigrade\Job\RefreshOutboundAuthToken;
class Instance extends FetchableModel {
@@ -80,6 +81,14 @@ class Instance extends FetchableModel {
return self::findByInboundAuthToken($token);
}
+ public static function requireByRequestHeaders(): self {
+ $inst = self::findByRequestHeaders();
+ if ($inst == null) {
+ throw new Unauthorized();
+ }
+ return $inst;
+ }
+
public static function findByDialbackSecret(string $secret): ?self {
$authRecord = InstanceAuth::findWhere('secret = ?', [$secret]);
if ($authRecord == null)
diff --git a/Digitigrade/Model/Note.php b/Digitigrade/Model/Note.php
index 7fb1221..64a2168 100644
--- a/Digitigrade/Model/Note.php
+++ b/Digitigrade/Model/Note.php
@@ -151,7 +151,7 @@ class Note extends PushableModel implements TimelineIncludeable {
}, $this->mentions, $this->privacy->alsoVisibleTo, [$this->author]));
}
- protected function getRelevantServers(): array {
+ public 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))
diff --git a/locale/en_GB.json b/locale/en_GB.json
index 06da83e..cb112f5 100644
--- a/locale/en_GB.json
+++ b/locale/en_GB.json
@@ -22,6 +22,10 @@
"note.action.reshare": "Reshare",
"note.action.reply": "Reply",
"note.info.replyTo": "In reply to %s",
+ "note.privacy.scope.none": "Private",
+ "note.privacy.scope.mutuals": "Mutuals only",
+ "note.privacy.scope.followers": "Followers only",
+ "note.privacy.scope.public": "Public",
"placeholder": "There's nothing here.",
"placeholder.moreItems": "…and %d more",
"placeholder.noMoreItems": "No more items.",
@@ -35,6 +39,8 @@
"login.action": "Log in",
"login.failed": "Invalid email address or password",
"writeNote.label": "Write a note",
+ "writeNote.summary.label": "Content warning (optional)",
+ "writeNote.scope.label": "Visibility",
"writeNote.action": "Post",
"writeReply.label": "Write a reply",
"writeReply.action": "Reply"
diff --git a/routes/note.php b/routes/note.php
index 8a6c9da..f1d6650 100644
--- a/routes/note.php
+++ b/routes/note.php
@@ -1,8 +1,12 @@
<?php
+use Digitigrade\HttpResponseStatus\Forbidden;
use Digitigrade\HttpResponseStatus\NotFound;
use Digitigrade\HttpResponseStatus\TemporaryRedirect;
+use Digitigrade\Model\Instance;
use Digitigrade\Model\Note;
+use Digitigrade\Model\NotePrivacyScope;
+use Digitigrade\Model\UserAccount;
use Digitigrade\Router;
Router::getInstance()->mount('/note/:id', function (array $args) {
@@ -16,6 +20,14 @@ Router::getInstance()->mount('/note/:id', function (array $args) {
if (isset($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'text/html')) {
throw new TemporaryRedirect('/@/' . $note->author->handle . "/note/$note->id");
}
+ // if it's not public we need to check whether the requesting instance is allowed to see it
+ if ($note->privacy->scope != NotePrivacyScope::PUBLIC ) {
+ $instance = Instance::requireByRequestHeaders();
+ $allowed = $note->getRelevantServers();
+ if (!in_array($instance, $allowed)) {
+ throw new Forbidden();
+ }
+ }
json_response($note);
});
@@ -28,5 +40,12 @@ Router::getInstance()->mount('/@/:handle/note/:id', function (array $args) {
if ($args['handle'] != $note->author->getFullHandle()) {
throw new TemporaryRedirect('/@/' . $note->author->getFullHandle() . "/note/$note->id");
}
+ // check the current user is allowed to see it
+ if ($note->privacy->scope != NotePrivacyScope::PUBLIC ) {
+ $user = UserAccount::requireByCurrentSession();
+ if (!in_array($user->actor, $note->getRelevantActors())) {
+ throw new Forbidden();
+ }
+ }
render_template('thread_page', ['note' => $note]);
});
diff --git a/routes/note_fragment.php b/routes/note_fragment.php
index 4d8c1a6..84f68c6 100644
--- a/routes/note_fragment.php
+++ b/routes/note_fragment.php
@@ -4,6 +4,7 @@ use Digitigrade\HttpResponseStatus\BadRequest;
use Digitigrade\Model\Interaction;
use Digitigrade\Model\InteractionKind;
use Digitigrade\Model\Note;
+use Digitigrade\Model\NotePrivacyScope;
use Digitigrade\Model\UserAccount;
use Digitigrade\Router;
@@ -42,8 +43,16 @@ Router::getInstance()->mount('/fragment/note/:id/reply', function (array $args)
// replying to a note
$author = UserAccount::requireByCurrentSession();
$note = Note::find($args['id']);
- if (isset($_POST['content'])) {
- $reply = Note::create($author->actor, $_POST['content'], 'unk');
+ if (isset($_POST['content']) && strlen(trim($_POST['content'])) > 0) {
+ $summary = trim($_POST['summary'] ?? '');
+ $summary = $summary == '' ? null : $summary;
+ $reply = Note::create(
+ $author->actor,
+ trim($_POST['content']),
+ 'unk',
+ $summary,
+ $note->privacy->scope
+ );
$reply->inReplyTo = $note;
$reply->threadApex = $note->threadApex ?? $note;
$reply->save();
@@ -56,9 +65,17 @@ Router::getInstance()->mount('/fragment/note/:id/reply', function (array $args)
Router::getInstance()->mount('/fragment/note', function (array $args) {
// posting a new note
$author = UserAccount::requireByCurrentSession();
- if (isset($_POST['content'])) {
- // TODO: summary field, post language, privacy, etc
- $note = Note::create($author->actor, $_POST['content'], 'unk');
+ if (isset($_POST['content']) && strlen(trim($_POST['content'])) > 0) {
+ // TODO: post language, other privacy options, etc
+ $summary = trim($_POST['summary'] ?? '');
+ $summary = $summary == '' ? null : $summary;
+ $note = Note::create(
+ $author->actor,
+ trim($_POST['content']),
+ 'unk',
+ $summary,
+ NotePrivacyScope::from($_POST['scope'])
+ );
$note->publish();
$note->processTimelineAdditions();
}
diff --git a/static/form.css b/static/form.css
index cb86003..0e0f978 100644
--- a/static/form.css
+++ b/static/form.css
@@ -9,13 +9,14 @@ form {
gap: 8px;
max-width: max-content;
- label:has(+ input, + textarea) {
+ label:has(+ input, + textarea, + select) {
display: block;
font-size: 10pt;
color: #555;
}
input:not([type="checkbox"]),
- textarea {
+ textarea,
+ select {
display: block;
border: 1px solid #3334;
border-radius: 8px;
@@ -25,7 +26,8 @@ form {
font-size: inherit;
color: inherit;
background: #fff;
- width: min(60vw, 24em, calc(100% - 16px));
+ width: min(60vw, 24em, 100%);
+ box-sizing: border-box;
}
textarea {
resize: vertical;
diff --git a/static/misc.css b/static/misc.css
index 2da35b6..3281def 100644
--- a/static/misc.css
+++ b/static/misc.css
@@ -56,6 +56,7 @@ a.icon {
span.inlineIcon {
font-size: inherit;
color: inherit;
+ user-select: none;
}
[hidden] {
diff --git a/static/note.css b/static/note.css
index fde4c62..cd9166c 100644
--- a/static/note.css
+++ b/static/note.css
@@ -12,6 +12,7 @@
.icon {
font-size: 24px;
+ user-select: none;
}
a {
text-decoration: none;
@@ -50,11 +51,21 @@ article.note {
.selected & {
font-size: 16pt;
}
+
+ summary {
+ border: 1px solid #5e380044;
+ border-radius: 8px;
+ padding: 8px 16px;
+ background: #ffda6144;
+ }
+ details[open] > summary {
+ margin-bottom: 8px;
+ }
}
.infoLine {
display: grid;
- grid-template-columns: max-content 1fr max-content;
+ grid-template-columns: max-content 1fr max-content max-content;
align-items: baseline;
margin-bottom: 4px;
gap: 8px;
@@ -75,6 +86,9 @@ article.note {
text-decoration: underline;
}
}
+ .privacyScope.inlineIcon {
+ align-self: center;
+ }
}
.replyInfo a {
diff --git a/templates/note.php b/templates/note.php
index 0a19475..0de2168 100644
--- a/templates/note.php
+++ b/templates/note.php
@@ -1,5 +1,6 @@
<?php
use Digitigrade\Model\InteractionKind;
+use Digitigrade\Model\NotePrivacyScope;
use Digitigrade\Model\UserAccount;
$user = UserAccount::findByCurrentSession();
@@ -26,18 +27,36 @@ if ($user != null) {
)
?>
</a>
+ <span class="privacyScope inlineIcon material-symbols-outlined" title="<?= match ($note->privacy->scope) {
+ NotePrivacyScope::NONE => __('note.privacy.scope.none'),
+ NotePrivacyScope::MUTUALS => __('note.privacy.scope.mutuals'),
+ NotePrivacyScope::FOLLOWERS => __('note.privacy.scope.followers'),
+ NotePrivacyScope::PUBLIC => __('note.privacy.scope.public')
+ } . '">' . match ($note->privacy->scope) {
+ NotePrivacyScope::NONE => 'lock',
+ NotePrivacyScope::MUTUALS => 'group',
+ NotePrivacyScope::FOLLOWERS => 'groups',
+ NotePrivacyScope::PUBLIC => 'public'
+ } ?></span>
</div>
<?php if (isset($note->inReplyTo)): ?>
- <div class="replyInfo">
- <a href="/@/<?= $note->inReplyTo->author->getFullHandle() ?>/note/<?= $note->inReplyTo->id ?>">
- <span class="material-symbols-outlined inlineIcon">reply</span>
- <?= sprintf(__('note.info.replyTo'),
- '<b>' . $note->inReplyTo->author->displayName) . '</b>' ?>
- </a>
+ <div class=" replyInfo">
+ <a href="/@/<?= $note->inReplyTo->author->getFullHandle() ?>/note/<?= $note->inReplyTo->id ?>">
+ <span class="material-symbols-outlined inlineIcon">reply</span>
+ <?= sprintf(__('note.info.replyTo'),
+ '<b>' . $note->inReplyTo->author->displayName) . '</b>' ?>
+ </a>
</div>
<?php endif; ?>
<div class="content">
- <?= $note->getFormattedContent('text/html') ?? htmlspecialchars($note->plainContent) ?>
+ <?php if (isset($note->summary)): ?>
+ <details>
+ <summary><?= htmlspecialchars($note->summary) ?></summary>
+ <?php endif; ?>
+ <?= $note->getFormattedContent('text/html') ?? htmlspecialchars($note->plainContent) ?>
+ <?php if (isset($note->summary)): ?>
+ </details>
+ <?php endif; ?>
</div>
<div class="buttons">
<?php foreach (['like', 'dislike', 'reshare'] as $action) {
diff --git a/templates/reply_form.php b/templates/reply_form.php
index 9a1d7e5..3434472 100644
--- a/templates/reply_form.php
+++ b/templates/reply_form.php
@@ -6,6 +6,11 @@
<textarea id="replyContent-<?= $note->id ?>" name="content" required autocomplete="off"></textarea>
</div>
<div class="row">
+ <label for="replySummary-<?= $note->id ?>"><?= __('writeNote.summary.label') ?></label>
+ <input type="text" id="replySummary-<?= $note->id ?>" name="summary" autocomplete="off"
+ value="<?= $note->summary ? "re: $note->summary" : '' ?>">
+ </div>
+ <div class="row">
<button class="primary"><?= __('writeReply.action') ?></button>
</div>
</form> \ No newline at end of file
diff --git a/templates/write_note_form.php b/templates/write_note_form.php
index c28dc12..b74de69 100644
--- a/templates/write_note_form.php
+++ b/templates/write_note_form.php
@@ -5,6 +5,19 @@
<textarea name="content" id="noteContent" required autocomplete="off"></textarea>
</div>
<div class="row">
+ <label for="noteSummary"><?= __('writeNote.summary.label') ?></label>
+ <input type="text" id="noteSummary" name="summary" autocomplete="off">
+ </div>
+ <div class="row">
+ <label for="noteScope"><?= __('writeNote.scope.label') ?></label>
+ <select name="scope" id="noteScope" required autocomplete="off">
+ <option value="none"><?= __('note.privacy.scope.none') ?></option>
+ <option value="mutuals"><?= __('note.privacy.scope.mutuals') ?></option>
+ <option value="followers"><?= __('note.privacy.scope.followers') ?></option>
+ <option value="public" selected><?= __('note.privacy.scope.public') ?></option>
+ </select>
+ </div>
+ <div class="row">
<button class="primary"><?= __('writeNote.action') ?></button>
</div>
</form> \ No newline at end of file