diff options
| -rw-r--r-- | locale/en_GB.json | 5 | ||||
| -rw-r--r-- | routes/note_fragment.php | 42 | ||||
| -rw-r--r-- | static/form.css | 15 | ||||
| -rw-r--r-- | static/note.css | 3 | ||||
| -rw-r--r-- | templates/note/editable.php | 30 | ||||
| -rw-r--r-- | templates/note/info_line.php | 28 | ||||
| -rw-r--r-- | templates/note/note.php | 47 |
7 files changed, 133 insertions, 37 deletions
diff --git a/locale/en_GB.json b/locale/en_GB.json index c0bd6a0..22cc291 100644 --- a/locale/en_GB.json +++ b/locale/en_GB.json @@ -39,13 +39,16 @@ "note.action.dislike": "Dislike", "note.action.reshare": "Reshare", "note.action.reply": "Reply", - "note.action.delete": "Delete note", + "note.action.delete": "Delete", "note.action.delete.confirmation": "Are you sure you want to permanently delete this note?", + "note.action.edit": "Edit", + "note.editForm.label": "Edit note", "note.openRemote": "Open on original instance", "note.openPage": "View page", "note.page.summaryMessage": "Published a new page: \"%s\"", "note.info.replyTo": "In reply to %s", "note.info.mentions": "Mentions %s", + "note.info.modified": "Last modified %s", "note.privacy.scope.none": "Private", "note.privacy.scope.mutuals": "Mutuals only", "note.privacy.scope.followers": "Followers only", diff --git a/routes/note_fragment.php b/routes/note_fragment.php index 94ebc07..9cc9143 100644 --- a/routes/note_fragment.php +++ b/routes/note_fragment.php @@ -14,16 +14,24 @@ use Digitigrade\Model\StorageObject; use Digitigrade\Model\UserAccount; use Digitigrade\PolicyManager; use Digitigrade\Router; -use Digitigrade\StorageProvider; use Digitigrade\StorageProvider\FilesystemStorage; Router::getInstance()->mount('/fragment/note/:id', function (array $args) { + $note = Note::find($args['id']); if ($_SERVER['REQUEST_METHOD'] != 'DELETE') { - throw new RuntimeException('method not implemented'); + // just render the note + // but 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('note/note', ['note' => Note::find($args['id'])]); + return; } // assuming we're deleting the note $user = UserAccount::requireByCurrentSession(); - $note = Note::find($args['id']); if ($note->author != $user->actor && !$user->isAdmin) { throw new Forbidden('you may not delete that note!'); } @@ -108,6 +116,34 @@ Router::getInstance()->mount('/fragment/note/:id/reply', function (array $args) render_template('note/reply_form', ['note' => $note]); }); +Router::getInstance()->mount('/fragment/note/:id/edit', function (array $args) { + // editing an existing note + $user = UserAccount::requireByCurrentSession(); + $note = Note::find($args['id']); + if ($note->author != $user->actor && !$user->isAdmin) { + throw new Forbidden('you may not edit that note!'); + } + if (!$note->author->isLocal) { + throw new BadRequest("can't edit a non-local note"); + } + + if (isset($_POST['content']) && strlen(trim($_POST['content'])) > 0) { + $note->summary = trim($_POST['summary'] ?? '') ?: null; + $content = trim($_POST['content']); + $note->plainContent = CommonMark::getInstance()->renderToPlainText($content); + $note->formattedContent = [ + 'text/markdown' => $content, + 'text/html' => CommonMark::getInstance()->renderToHtml($content) + ]; + $note->modified = new DateTimeImmutable(); + $note->save(); + $note->publish(); + render_template('note/note', ['note' => $note]); + } else { + render_template('note/editable', ['note' => $note]); + } +}); + Router::getInstance()->mount('/fragment/note', function (array $args) { // posting a new note $author = UserAccount::requireByCurrentSession(); diff --git a/static/form.css b/static/form.css index 6f474c1..bb8655d 100644 --- a/static/form.css +++ b/static/form.css @@ -61,7 +61,8 @@ form:not(.nopanel) { margin: 0; } - &.noteReplyForm { + &.noteReplyForm, + &.noteEditForm { background: none; border: none; padding: 0; @@ -76,8 +77,12 @@ form:not(.nopanel) { box-sizing: border-box; } } + &.noteEditForm { + margin-top: var(--spacing-single); + } &.noteReplyForm, + &.noteEditForm, &#writeNoteForm { details { summary { @@ -178,6 +183,14 @@ form:not(.nopanel) { font-size: var(--font-normal-size); } } + + .actionButtons { + display: grid; + justify-content: end; + align-items: baseline; + grid-auto-flow: column; + gap: var(--spacing-double); + } } input[type="checkbox"] { diff --git a/static/note.css b/static/note.css index a224508..6d3b215 100644 --- a/static/note.css +++ b/static/note.css @@ -148,7 +148,8 @@ article.note { } } - .replyInfo { + .replyInfo, + .modifiedInfo { font-size: var(--font-small-size); color: var(--clr-lesser-foreground); display: flex; diff --git a/templates/note/editable.php b/templates/note/editable.php new file mode 100644 index 0000000..7a864a9 --- /dev/null +++ b/templates/note/editable.php @@ -0,0 +1,30 @@ +<article class="note"> + <a href="/@/<?= $note->author->getFullHandle() ?>"> + <?php call_template('actor/avatar', ['actor' => $note->author]); ?> + </a> + + <div> + + <?php call_template('note/info_line', ['note' => $note]); ?> + + <form hx-post="/fragment/note/<?= $note->id ?>/edit" hx-target="closest .note" hx-swap="outerHTML" + class="noteEditForm"> + <div class="row summaryAndContent"> + <label for="noteContent-<?= $note->id ?>"><?= __('note.editForm.label') ?></label> + <input name="summary" type="text" placeholder="<?= __('writeNote.summary.label') ?>" + value="<?= htmlspecialchars($note->summary) ?>"> + <textarea id="noteContent-<?= $note->id ?>" name="content" required + placeholder="<?= __('writeNote.placeholder') ?>"><?= htmlspecialchars( + $note->formattedContent['text/markdown'] ?? $note->plainContent + ) ?></textarea> + <?php call_template('formatting_hint'); ?> + </div> + <div class="row actionButtons"> + <button type="button" class="secondary" + hx-get="/fragment/note/<?= $note->id ?>"><?= __('form.discardChanges') ?></button> + <button type="submit" class="primary"><?= __('form.saveChanges') ?></button> + </div> + </form> + + </div> +</article>
\ No newline at end of file diff --git a/templates/note/info_line.php b/templates/note/info_line.php new file mode 100644 index 0000000..acc85d7 --- /dev/null +++ b/templates/note/info_line.php @@ -0,0 +1,28 @@ +<?php + +use Digitigrade\Model\NotePrivacyScope; + +?> +<div class="infoLine"> + <a class="authorName" + href="/@/<?= htmlspecialchars($note->author->getFullHandle()) ?>"><?= htmlspecialchars($note->author->displayName) ?></a> + <span class="authorPronouns"> + <?php if (isset($note->author->pronouns) && trim($note->author->pronouns) != '') { + echo htmlspecialchars($note->author->pronouns); + } ?> + </span> + <a class="timestamp" href="<?= htmlspecialchars($note->getLocalUiHref()) ?>"> + <?= format_datetime($note->created) ?> + </a> + <span class="privacyScope inlineIcon material-symbols <?= match ($note->privacy->scope) { + NotePrivacyScope::NONE => 'lock', + NotePrivacyScope::MUTUALS => 'group', + NotePrivacyScope::FOLLOWERS => 'groups', + NotePrivacyScope::PUBLIC => 'public' + } ?>" 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') + } ?>"></span> +</div>
\ No newline at end of file diff --git a/templates/note/note.php b/templates/note/note.php index 7cef342..0d48272 100644 --- a/templates/note/note.php +++ b/templates/note/note.php @@ -3,7 +3,6 @@ use Digitigrade\Model\Actor; use Digitigrade\Model\InteractionKind; -use Digitigrade\Model\NotePrivacyScope; use Digitigrade\Model\UserAccount; use Digitigrade\UserSettings; @@ -17,7 +16,6 @@ if ($user != null) { $interactions = $note->getInteractionsWithAuthor($user->actor); $interKinds = array_map(fn($inter) => $inter->kind, $interactions); } -$pathToSelf = $note->getLocalUiHref(); ?> <article class="note <?= ($selected ?? false) ? 'selected' : '' ?>"> @@ -27,40 +25,14 @@ $pathToSelf = $note->getLocalUiHref(); <div> - <div class="infoLine"> - <a class="authorName" - href="/@/<?= htmlspecialchars($note->author->getFullHandle()) ?>"><?= htmlspecialchars($note->author->displayName) ?></a> - <span class="authorPronouns"> - <?php if (isset($note->author->pronouns) && trim($note->author->pronouns) != '') { - echo htmlspecialchars($note->author->pronouns); - } ?> - </span> - <a class="timestamp" href="<?= htmlspecialchars($pathToSelf) ?>"> - <?= format_datetime($note->created) - . (isset($note->modified) && $note->modified != $note->created - ? ' (' . format_datetime($note->modified) . ')' - : '' - ) ?> - </a> - <span class="privacyScope inlineIcon material-symbols <?= match ($note->privacy->scope) { - NotePrivacyScope::NONE => 'lock', - NotePrivacyScope::MUTUALS => 'group', - NotePrivacyScope::FOLLOWERS => 'groups', - NotePrivacyScope::PUBLIC => 'public' - } ?>" 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') - } ?>"></span> - </div> + <?php call_template('note/info_line', ['note' => $note]); ?> <?php if (isset($note->inReplyTo)): ?> <div class="replyInfo"> <span class="material-symbols reply inlineIcon"></span> <a href="/@/<?= htmlspecialchars($note->inReplyTo->author->getFullHandle()) ?>/note/<?= $note->inReplyTo->id ?>"> - <?= sprintf(__('note.info.replyTo'), + <?= __f('note.info.replyTo', '<b>' . htmlspecialchars($note->inReplyTo->author->displayName)) . '</b>' ?> </a> </div> @@ -68,7 +40,7 @@ $pathToSelf = $note->getLocalUiHref(); <div class="replyInfo"> <span class="material-symbols alternate-email inlineIcon"></span> <span> - <?= sprintf(__('note.info.mentions'), implode(', ', array_map( + <?= __f('note.info.mentions', implode(', ', array_map( fn(Actor $m) => '<b><a href="/@/' . htmlspecialchars($m->getFullHandle()) . '">' . htmlspecialchars($m->displayName) . '</a></b>', $note->mentions ))) ?> @@ -76,6 +48,13 @@ $pathToSelf = $note->getLocalUiHref(); </div> <?php endif; ?> + <?php if (isset($note->modified) && $note->modified != $note->created): ?> + <div class="modifiedInfo"> + <span class="material-symbols edit-outline inlineIcon"></span> + <span><?= __f('note.info.modified', format_datetime($note->modified)) ?></span> + </div> + <?php endif; ?> + <?php if (isset($note->summary)): ?> <details> <summary><?= htmlspecialchars($note->summary) ?></summary> @@ -151,6 +130,12 @@ $pathToSelf = $note->getLocalUiHref(); </li> <?php if ($note->author->isLocal && ($note->author == $user?->actor || $user?->isAdmin)): ?> <li> + <button hx-get="/fragment/note/<?= $note->id ?>/edit" hx-target="closest .note" hx-swap="outerHTML"> + <span class="material-symbols edit-outline"></span> + <span><?= __('note.action.edit') ?></span> + </button> + </li> + <li> <button hx-delete="/fragment/note/<?= $note->id ?>" hx-confirm="<?= __('note.action.delete.confirmation') ?>" hx-target="closest .note" hx-swap="delete"> |
