Редактирование постов только покруче (#979)

* Add editing posts

* Add checkboxes

* Add ctrl+enter + fix empty posts

* Fix funny bug
This commit is contained in:
lalka2018 2023-09-14 20:54:22 +03:00 committed by GitHub
parent 14d5caaf9f
commit 97a176c261
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 270 additions and 21 deletions

View file

@ -90,4 +90,12 @@ class Comment extends Post
{ {
return "/wall" . $this->getTarget()->getPrettyId() . "#_comment" . $this->getId(); return "/wall" . $this->getTarget()->getPrettyId() . "#_comment" . $this->getId();
} }
function canBeEditedBy(?User $user = NULL): bool
{
if(!$user)
return false;
return $user->getId() == $this->getOwner(false)->getId();
}
} }

View file

@ -246,5 +246,16 @@ class Post extends Postable
$this->save(); $this->save();
} }
function canBeEditedBy(?User $user = NULL): bool
{
if(!$user)
return false;
if($this->isDeactivationMessage() || $this->isUpdateAvatarMessage())
return false;
return $user->getId() == $this->getOwner(false)->getId();
}
use Traits\TRichText; use Traits\TRichText;
} }

View file

@ -3,7 +3,7 @@ namespace openvk\Web\Presenters;
use openvk\Web\Models\Exceptions\TooMuchOptionsException; use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User}; use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User};
use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification}; use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification};
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes}; use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Comments};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use Nette\InvalidStateException as ISE; use Nette\InvalidStateException as ISE;
use Bhaktaraz\RSSGenerator\Item; use Bhaktaraz\RSSGenerator\Item;
@ -498,4 +498,63 @@ final class WallPresenter extends OpenVKPresenter
# TODO localize message based on language and ?act=(un)pin # TODO localize message based on language and ?act=(un)pin
$this->flashFail("succ", tr("information_-1"), tr("changes_saved_comment")); $this->flashFail("succ", tr("information_-1"), tr("changes_saved_comment"));
} }
function renderEdit()
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
if($_SERVER["REQUEST_METHOD"] !== "POST")
$this->redirect("/id0");
if($this->postParam("type") == "post")
$post = $this->posts->get((int)$this->postParam("postid"));
else
$post = (new Comments)->get((int)$this->postParam("postid"));
if(!$post || $post->isDeleted())
$this->returnJson(["error" => "Invalid post"]);
if(!$post->canBeEditedBy($this->user->identity))
$this->returnJson(["error" => "Access denied"]);
$attachmentsCount = sizeof(iterator_to_array($post->getChildren()));
if(empty($this->postParam("newContent")) && $attachmentsCount < 1)
$this->returnJson(["error" => "Empty post"]);
$post->setEdited(time());
try {
$post->setContent($this->postParam("newContent"));
} catch(\LengthException $e) {
$this->returnJson(["error" => $e->getMessage()]);
}
if($this->postParam("type") === "post") {
$post->setNsfw($this->postParam("nsfw") == "true");
$flags = 0;
if($post->getTargetWall() < 0 && $post->getWallOwner()->canBeModifiedBy($this->user->identity)) {
if($this->postParam("fromgroup") == "true") {
$flags |= 0b10000000;
$post->setFlags($flags);
} else
$post->setFlags($flags);
}
}
$post->save(true);
$this->returnJson(["error" => "no",
"new_content" => $post->getText(),
"new_edited" => (string)$post->getEditTime(),
"nsfw" => $this->postParam("type") === "post" ? (int)$post->isExplicit() : 0,
"from_group" => $this->postParam("type") === "post" && $post->getTargetWall() < 0 ?
((int)$post->isPostedOnBehalfOfGroup()) : "false",
"author" => [
"name" => $post->getOwner()->getCanonicalName(),
"avatar" => $post->getOwner()->getAvatarUrl()
]]);
}
} }

View file

@ -34,6 +34,14 @@
{/if} {/if}
<a n:if="$canDelete ?? false" class="profile_link" style="display:block;width:96%;" href="/wall{$post->getPrettyId()}/delete">{_delete}</a> <a n:if="$canDelete ?? false" class="profile_link" style="display:block;width:96%;" href="/wall{$post->getPrettyId()}/delete">{_delete}</a>
<a
n:if="$thisUser->getChandlerUser()->can('access')->model('admin')->whichBelongsTo(NULL) AND $post->getEditTime()"
style="display:block;width:96%;"
class="profile_link"
href="/admin/logs?type=1&obj_type=Post&obj_id={$post->getId()}"
>
{_changes_history}
</a>
<a n:if="$canReport ?? false" class="profile_link" style="display:block;width:96%;" href="javascript:reportPost()">{_report}</a> <a n:if="$canReport ?? false" class="profile_link" style="display:block;width:96%;" href="javascript:reportPost()">{_report}</a>
</div> </div>
<script n:if="$canReport ?? false"> <script n:if="$canReport ?? false">

View file

@ -20,7 +20,7 @@
</div> </div>
<div class="post-content" id="{$comment->getId()}"> <div class="post-content" id="{$comment->getId()}">
<div class="text" id="text{$comment->getId()}"> <div class="text" id="text{$comment->getId()}">
{$comment->getText()|noescape} <span class="really_text">{$comment->getText()|noescape}</span>
<div n:ifcontent class="attachments_b"> <div n:ifcontent class="attachments_b">
<div class="attachment" n:foreach="$comment->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> <div class="attachment" n:foreach="$comment->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
@ -29,17 +29,17 @@
</div> </div>
</div> </div>
<div n:if="isset($thisUser) &&! ($compact ?? false)" class="post-menu"> <div n:if="isset($thisUser) &&! ($compact ?? false)" class="post-menu">
<a <a href="#_comment{$comment->getId()}" class="date">{$comment->getPublicationTime()}
href="{=$linkWithPost && get_class($comment->getTarget()) == 'openvk\Web\Models\Entities\Post' ? '/wall' . $comment->getTarget()->getPrettyId() : ''}#_comment{$comment->getId()}" <span n:if="$comment->getEditTime()" class="edited editedMark">({_edited_short})</span>
class="date"
>
{$comment->getPublicationTime()}
</a> </a>
{if !$timeOnly} {if !$timeOnly}
&nbsp;| &nbsp;|
{if $comment->canBeDeletedBy($thisUser)} {if $comment->canBeDeletedBy($thisUser)}
<a href="/comment{$comment->getId()}/delete">{_delete}</a>&nbsp;| <a href="/comment{$comment->getId()}/delete">{_delete}</a>&nbsp;|
{/if} {/if}
{if $comment->canBeEditedBy($thisUser)}
<a id="editPost" data-id="{$comment->getId()}">{_edit}</a>&nbsp;|
{/if}
<a class="comment-reply">{_reply}</a> <a class="comment-reply">{_reply}</a>
{if $thisUser->getId() != $comment->getOwner()->getId()} {if $thisUser->getId() != $comment->getOwner()->getId()}
{var $canReport = true} {var $canReport = true}

View file

@ -18,13 +18,13 @@
<tr> <tr>
<td width="54" valign="top"> <td width="54" valign="top">
<a href="{$author->getURL()}"> <a href="{$author->getURL()}">
<img src="{$author->getAvatarURL('miniscule')}" width="{if $compact}25{else}50{/if}" {if $compact}class="cCompactAvatars"{/if} /> <img src="{$author->getAvatarURL('miniscule')}" width="{if $compact}25{else}50{/if}" class="post-avatar {if $compact}cCompactAvatars{/if}" />
<span n:if="!$post->isPostedOnBehalfOfGroup() && !$compact && $author->isOnline()" class="post-online">{_online}</span> <span n:if="!$post->isPostedOnBehalfOfGroup() && !$compact && $author->isOnline()" class="post-online">{_online}</span>
</a> </a>
</td> </td>
<td width="100%" valign="top"> <td width="100%" valign="top">
<div class="post-author"> <div class="post-author">
<a href="{$author->getURL()}"><b>{$author->getCanonicalName()}</b></a> <a href="{$author->getURL()}"><b class="post-author-name">{$author->getCanonicalName()}</b></a>
<img n:if="$author->isVerified()" class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png"> <img n:if="$author->isVerified()" class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png">
{$post->isDeactivationMessage() ? ($author->isFemale() ? tr($deac . "_f") : tr($deac . "_m"))} {$post->isDeactivationMessage() ? ($author->isFemale() ? tr($deac . "_f") : tr($deac . "_m"))}
{$post->isUpdateAvatarMessage() && !$post->isPostedOnBehalfOfGroup() ? ($author->isFemale() ? tr("upd_f") : tr("upd_m"))} {$post->isUpdateAvatarMessage() && !$post->isPostedOnBehalfOfGroup() ? ($author->isFemale() ? tr("upd_f") : tr("upd_m"))}
@ -62,10 +62,17 @@
<a class="pin" href="/wall{$post->getPrettyId()}/pin?act=pin&hash={rawurlencode($csrfToken)}"></a> <a class="pin" href="/wall{$post->getPrettyId()}/pin?act=pin&hash={rawurlencode($csrfToken)}"></a>
{/if} {/if}
{/if} {/if}
{if $post->canBeEditedBy($thisUser) && !($forceNoEditLink ?? false) && $compact == false}
<a class="edit" id="editPost"
data-id="{$post->getId()}"
data-nsfw="{(int)$post->isExplicit()}"
{if $post->getTargetWall() < 0 && $post->getWallOwner()->canBeModifiedBy($thisUser)}data-fromgroup="{(int)$post->isPostedOnBehalfOfGroup()}"{/if}></a>
{/if}
</div> </div>
<div class="post-content" id="{$post->getPrettyId()}"> <div class="post-content" id="{$post->getPrettyId()}">
<div class="text" id="text{$post->getPrettyId()}"> <div class="text">
{$post->getText()|noescape} <span class="really_text">{$post->getText()|noescape}</span>
<div n:ifcontent class="attachments_b"> <div n:ifcontent class="attachments_b">
<div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
@ -88,7 +95,9 @@
</div> </div>
</div> </div>
<div class="post-menu" n:if="$compact == false"> <div class="post-menu" n:if="$compact == false">
<a href="/wall{$post->getPrettyId()}" class="date">{$post->getPublicationTime()}</a> <a href="/wall{$post->getPrettyId()}" class="date">{$post->getPublicationTime()}
<span n:if="$post->getEditTime()" class="edited editedMark">({_edited_short})</span>
</a>
<a n:if="!empty($platform)" class="client_app" data-app-tag="{$platform}" data-app-name="{$platformDetails['name']}" data-app-url="{$platformDetails['url']}" data-app-img="{$platformDetails['img']}"> <a n:if="!empty($platform)" class="client_app" data-app-tag="{$platform}" data-app-name="{$platformDetails['name']}" data-app-url="{$platformDetails['url']}" data-app-img="{$platformDetails['img']}">
<img src="/assets/packages/static/openvk/img/app_icons_mini/{$post->getPlatform(this)}.svg"> <img src="/assets/packages/static/openvk/img/app_icons_mini/{$post->getPlatform(this)}.svg">
</a> </a>

View file

@ -7,18 +7,20 @@
{var $deac = "post_deact_silent"} {var $deac = "post_deact_silent"}
{/if} {/if}
<table border="0" style="font-size: 11px;" n:class="post, $post->isExplicit() ? post-nsfw"> <table border="0" style="font-size: 11px;" n:class="post, $post->isExplicit() ? post-nsfw">
<tbody> <tbody>
<tr> <tr>
<td width="54" valign="top"> <td width="54" valign="top">
<a href="{$author->getURL()}"> <a href="{$author->getURL()}">
<img src="{$author->getAvatarURL('miniscule')}" width="50" /> <img src="{$author->getAvatarURL('miniscule')}" class="post-avatar" width="50" />
<span n:if="!$post->isPostedOnBehalfOfGroup() && !($compact ?? false) && $author->isOnline()" class="post-online">{_online}</span> <span n:if="!$post->isPostedOnBehalfOfGroup() && !($compact ?? false) && $author->isOnline()" class="post-online">{_online}</span>
</a> </a>
</td> </td>
<td width="100%" valign="top"> <td width="100%" valign="top">
<div class="post-author"> <div class="post-author">
<a href="{$author->getURL()}"><b>{$author->getCanonicalName()}</b></a> <a href="{$author->getURL()}"><b class="post-author-name">{$author->getCanonicalName()}</b></a>
<img n:if="$author->isVerified()" class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png"> <img n:if="$author->isVerified()" class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png">
{if $post->isDeactivationMessage()} {if $post->isDeactivationMessage()}
{$author->isFemale() ? tr($deac . "_f") : tr($deac . "_m")} {$author->isFemale() ? tr($deac . "_f") : tr($deac . "_m")}
@ -51,15 +53,17 @@
{/if} {/if}
<br/> <br/>
<a href="/wall{$post->getPrettyId()}" class="date"> <a href="/wall{$post->getPrettyId()}" class="date">
{$post->getPublicationTime()}{if $post->isPinned()}, {_pinned}{/if} {$post->getPublicationTime()} <span n:if="$post->getEditTime()" class="editedMark">({_edited_short})</span>{if $post->isPinned()}, {_pinned}{/if}
<a n:if="!empty($platform)" class="client_app" data-app-tag="{$platform}" data-app-name="{$platformDetails['name']}" data-app-url="{$platformDetails['url']}" data-app-img="{$platformDetails['img']}"> <a n:if="!empty($platform)" class="client_app" data-app-tag="{$platform}" data-app-name="{$platformDetails['name']}" data-app-url="{$platformDetails['url']}" data-app-img="{$platformDetails['img']}">
<img src="/assets/packages/static/openvk/img/app_icons_mini/{$post->getPlatform(this)}.svg"> <img src="/assets/packages/static/openvk/img/app_icons_mini/{$post->getPlatform(this)}.svg">
</a> </a>
</a> </a>
</div> </div>
<div class="post-content" id="{$post->getPrettyId()}"> <div class="post-content" id="{$post->getPrettyId()}">
<div class="text" id="text{$post->getPrettyId()}"> <div class="text">
{$post->getText()|noescape} {var $owner = $author->getId()}
<span class="really_text">{$post->getText()|noescape}</span>
<div n:ifcontent class="attachments_b"> <div n:ifcontent class="attachments_b">
<div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}"> <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
@ -87,6 +91,13 @@
{var $forceNoPinLink = true} {var $forceNoPinLink = true}
{/if} {/if}
{if !($forceNoEditLink ?? false) && $post->canBeEditedBy($thisUser)}
<a id="editPost"
data-id="{$post->getId()}"
data-nsfw="{(int)$post->isExplicit()}"
{if $post->getTargetWall() < 0 && $post->getWallOwner()->canBeModifiedBy($thisUser)}data-fromgroup="{(int)$post->isPostedOnBehalfOfGroup()}"{/if}>{_edit}</a> &nbsp;|&nbsp;
{/if}
{if !($forceNoDeleteLink ?? false) && $post->canBeDeletedBy($thisUser)} {if !($forceNoDeleteLink ?? false) && $post->canBeDeletedBy($thisUser)}
<a href="/wall{$post->getPrettyId()}/delete">{_delete}</a> &nbsp;|&nbsp; <a href="/wall{$post->getPrettyId()}/delete">{_delete}</a> &nbsp;|&nbsp;
{/if} {/if}

View file

@ -129,6 +129,8 @@ routes:
handler: "Wall->rss" handler: "Wall->rss"
- url: "/wall{num}/makePost" - url: "/wall{num}/makePost"
handler: "Wall->makePost" handler: "Wall->makePost"
- url: "/wall/edit"
handler: "Wall->edit"
- url: "/wall{num}_{num}" - url: "/wall{num}_{num}"
handler: "Wall->post" handler: "Wall->post"
- url: "/wall{num}_{num}/like" - url: "/wall{num}_{num}/like"

View file

@ -2702,6 +2702,10 @@ body.article .floating_sidebar, body.article .page_content {
font-size: 12px; font-size: 12px;
} }
.edited {
color: #9b9b9b;
}
.uploadedImage img { .uploadedImage img {
max-height: 76px; max-height: 76px;
object-fit: cover; object-fit: cover;
@ -2713,6 +2717,16 @@ body.article .floating_sidebar, body.article .page_content {
user-select: none; user-select: none;
} }
.editMenu.loading {
filter: opacity(0.5);
cursor: progress;
user-select: none;
}
.editMenu.loading * {
pointer-events: none;
}
.lagged * { .lagged * {
pointer-events: none; pointer-events: none;
} }
@ -2797,3 +2811,4 @@ body.article .floating_sidebar, body.article .page_content {
.smallFrame:hover { .smallFrame:hover {
background: #E9F0F1 !important; background: #E9F0F1 !important;
} }

View file

@ -110,10 +110,24 @@
transition-duration: 0.3s; transition-duration: 0.3s;
} }
.post-author .edit {
float: right;
height: 16px;
width: 16px;
overflow: auto;
background: url("/assets/packages/static/openvk/img/edit.png") no-repeat 0 0;
opacity: 0.1;
transition-duration: 0.3s;
}
.post-author .pin:hover { .post-author .pin:hover {
opacity: 0.4; opacity: 0.4;
} }
.post-author .edit:hover {
opacity: 0.4;
}
.expand_button { .expand_button {
background-color: #eee; background-color: #eee;
width: 100%; width: 100%;

BIN
Web/static/img/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

View file

@ -263,3 +263,110 @@ async function showArticle(note_id) {
u("body").removeClass("dimmed"); u("body").removeClass("dimmed");
u("body").addClass("article"); u("body").addClass("article");
} }
$(document).on("click", "#editPost", (e) => {
let post = e.currentTarget.closest("table")
let content = post.querySelector(".text")
let text = content.querySelector(".really_text")
if(content.querySelector("textarea") == null) {
content.insertAdjacentHTML("afterbegin", `
<div class="editMenu">
<div id="wall-post-input999">
<textarea id="new_content">${text.innerHTML.replace(/(<([^>]+)>)/gi, '')}</textarea>
<input type="button" class="button" value="${tr("save")}" id="endEditing">
<input type="button" class="button" value="${tr("cancel")}" id="cancelEditing">
</div>
${e.currentTarget.dataset.nsfw != null ? `
<div class="postOptions">
<label><input type="checkbox" id="nswfw" ${e.currentTarget.dataset.nsfw == 1 ? `checked` : ``}>${tr("contains_nsfw")}</label>
</div>
` : ``}
${e.currentTarget.dataset.fromgroup != null ? `
<div class="postOptions">
<label><input type="checkbox" id="fromgroup" ${e.currentTarget.dataset.fromgroup == 1 ? `checked` : ``}>${tr("post_as_group")}</label>
</div>
` : ``}
</div>
`)
u(content.querySelector("#cancelEditing")).on("click", () => {post.querySelector("#editPost").click()})
u(content.querySelector("#endEditing")).on("click", () => {
let nwcntnt = content.querySelector("#new_content").value
let type = "post"
if(post.classList.contains("comment")) {
type = "comment"
}
let xhr = new XMLHttpRequest()
xhr.open("POST", "/wall/edit")
xhr.onloadstart = () => {
content.querySelector(".editMenu").classList.add("loading")
}
xhr.onerror = () => {
MessageBox(tr("error"), "unknown error occured", [tr("ok")], [() => {Function.noop}])
}
xhr.ontimeout = () => {
MessageBox(tr("error"), "Try to refresh page", [tr("ok")], [() => {Function.noop}])
}
xhr.onload = () => {
let result = JSON.parse(xhr.responseText)
if(result.error == "no") {
post.querySelector("#editPost").click()
content.querySelector(".really_text").innerHTML = result.new_content
if(post.querySelector(".editedMark") == null) {
post.querySelector(".date").insertAdjacentHTML("beforeend", `
<span class="edited editedMark">(${tr("edited_short")})</span>
`)
}
if(e.currentTarget.dataset.nsfw != null) {
e.currentTarget.setAttribute("data-nsfw", result.nsfw)
if(result.nsfw == 0) {
post.classList.remove("post-nsfw")
} else {
post.classList.add("post-nsfw")
}
}
if(e.currentTarget.dataset.fromgroup != null) {
e.currentTarget.setAttribute("data-fromgroup", result.from_group)
}
post.querySelector(".post-avatar").setAttribute("src", result.author.avatar)
post.querySelector(".post-author-name").innerHTML = result.author.name
} else {
MessageBox(tr("error"), result.error, [tr("ok")], [Function.noop])
post.querySelector("#editPost").click()
}
}
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send("postid="+e.currentTarget.dataset.id+
"&newContent="+nwcntnt+
"&hash="+encodeURIComponent(u("meta[name=csrf]").attr("value"))+
"&type="+type+
"&nsfw="+(content.querySelector("#nswfw") != null ? content.querySelector("#nswfw").checked : 0)+
"&fromgroup="+(content.querySelector("#fromgroup") != null ? content.querySelector("#fromgroup").checked : 0))
})
u(".editMenu").on("keydown", (e) => {
if(e.ctrlKey && e.keyCode === 13)
content.querySelector("#endEditing").click()
});
text.style.display = "none"
setupWallPostInputHandlers(999)
} else {
u(content.querySelector(".editMenu")).remove()
text.style.display = "block"
}
})

View file

@ -214,6 +214,8 @@
"reply" = "Reply"; "reply" = "Reply";
"edited_short" = "edited";
/* Friends */ /* Friends */
"friends" = "Friends"; "friends" = "Friends";
@ -1149,6 +1151,7 @@
"warn_user_action" = "Warn user"; "warn_user_action" = "Warn user";
"ban_in_support_user_action" = "Ban in support"; "ban_in_support_user_action" = "Ban in support";
"unban_in_support_user_action" = "Unban in support"; "unban_in_support_user_action" = "Unban in support";
"changes_history" = "Editing history";
/* Admin panel */ /* Admin panel */

View file

@ -191,6 +191,7 @@
"version_incompatibility" = "Не удалось отобразить это вложение. Возможно, база данных несовместима с текущей версией OpenVK."; "version_incompatibility" = "Не удалось отобразить это вложение. Возможно, база данных несовместима с текущей версией OpenVK.";
"graffiti" = "Граффити"; "graffiti" = "Граффити";
"reply" = "Ответить"; "reply" = "Ответить";
"edited_short" = "ред.";
/* Friends */ /* Friends */
@ -1049,6 +1050,7 @@
"warn_user_action" = "Предупредить пользователя"; "warn_user_action" = "Предупредить пользователя";
"ban_in_support_user_action" = "Заблокировать в поддержке"; "ban_in_support_user_action" = "Заблокировать в поддержке";
"unban_in_support_user_action" = "Разблокировать в поддержке"; "unban_in_support_user_action" = "Разблокировать в поддержке";
"changes_history" = "История редактирования";
/* Admin panel */ /* Admin panel */