mirror of
https://github.com/openvk/openvk
synced 2024-12-22 16:42:32 +03:00
Groups: Discussions
This commit adds discussions (forums) to groups, similar to how it was implemented in the original VK
This commit is contained in:
parent
64103ddbc5
commit
356c782f74
19 changed files with 750 additions and 9 deletions
86
Web/Models/Entities/Topic.php
Normal file
86
Web/Models/Entities/Topic.php
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace openvk\Web\Models\Entities;
|
||||||
|
use openvk\Web\Models\RowModel;
|
||||||
|
use openvk\Web\Models\Repositories\Clubs;
|
||||||
|
use openvk\Web\Util\DateTime;
|
||||||
|
|
||||||
|
class Topic extends Postable
|
||||||
|
{
|
||||||
|
protected $tableName = "topics";
|
||||||
|
protected $upperNodeReferenceColumnName = "group";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* May return fake owner (group), if flags are [1, (*)]
|
||||||
|
*
|
||||||
|
* @param bool $honourFlags - check flags
|
||||||
|
*/
|
||||||
|
function getOwner(bool $honourFlags = true, bool $real = false): RowModel
|
||||||
|
{
|
||||||
|
if($honourFlags && $this->isPostedOnBehalfOfGroup())
|
||||||
|
return $this->getClub();
|
||||||
|
|
||||||
|
return parent::getOwner($real);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClub(): Club
|
||||||
|
{
|
||||||
|
return (new Clubs)->get($this->getRecord()->group);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTitle(): string
|
||||||
|
{
|
||||||
|
return $this->getRecord()->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClosed(): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->getRecord()->closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPinned(): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->getRecord()->pinned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrettyId(): string
|
||||||
|
{
|
||||||
|
return $this->getRecord()->group . "_" . $this->getVirtualId();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPostedOnBehalfOfGroup(): bool
|
||||||
|
{
|
||||||
|
return ($this->getRecord()->flags & 0b10000000) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDeleted(): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->getRecord()->deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canBeModifiedBy(User $user): bool
|
||||||
|
{
|
||||||
|
return $this->getOwner(false)->getId() === $user->getId() || $this->club->canBeModifiedBy($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastComment(): ?Comment
|
||||||
|
{
|
||||||
|
$array = iterator_to_array($this->getLastComments(1));
|
||||||
|
return isset($array[0]) ? $array[0] : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdateTime(): DateTime
|
||||||
|
{
|
||||||
|
$lastComment = $this->getLastComment();
|
||||||
|
if(!is_null($lastComment))
|
||||||
|
return $lastComment->getPublicationTime();
|
||||||
|
else
|
||||||
|
return $this->getEditTime() ?? $this->getPublicationTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTopic(): void
|
||||||
|
{
|
||||||
|
$this->setDeleted(1);
|
||||||
|
$this->unwire();
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
}
|
73
Web/Models/Repositories/Topics.php
Normal file
73
Web/Models/Repositories/Topics.php
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace openvk\Web\Models\Repositories;
|
||||||
|
use openvk\Web\Models\Entities\Topic;
|
||||||
|
use openvk\Web\Models\Entities\Club;
|
||||||
|
use Nette\Database\Table\ActiveRow;
|
||||||
|
use Chandler\Database\DatabaseConnection;
|
||||||
|
|
||||||
|
class Topics
|
||||||
|
{
|
||||||
|
private $context;
|
||||||
|
private $topics;
|
||||||
|
|
||||||
|
function __construct()
|
||||||
|
{
|
||||||
|
$this->context = DatabaseConnection::i()->getContext();
|
||||||
|
$this->topics = $this->context->table("topics");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toTopic(?ActiveRow $ar): ?Topic
|
||||||
|
{
|
||||||
|
return is_null($ar) ? NULL : new Topic($ar);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(int $id): ?Topic
|
||||||
|
{
|
||||||
|
return $this->toTopic($this->topics->get($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTopicById(int $club, int $topic): ?Topic
|
||||||
|
{
|
||||||
|
return $this->toTopic($this->topics->where(["group" => $club, "virtual_id" => $topic, "deleted" => 0])->fetch());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClubTopics(Club $club, int $page = 1, ?int $perPage = NULL): \Traversable
|
||||||
|
{
|
||||||
|
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
|
||||||
|
|
||||||
|
// Get pinned topics first
|
||||||
|
$query = "SELECT `id` FROM `topics` WHERE `pinned` = 1 AND `group` = ? AND `deleted` = 0 UNION SELECT `id` FROM `topics` WHERE `pinned` = 0 AND `group` = ? AND `deleted` = 0";
|
||||||
|
$query .= " LIMIT " . $perPage . " OFFSET " . ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
foreach(DatabaseConnection::i()->getConnection()->query($query, $club->getId(), $club->getId()) as $topic) {
|
||||||
|
$topic = $this->get($topic->id);
|
||||||
|
if(!$topic) continue;
|
||||||
|
|
||||||
|
yield $topic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClubTopicsCount(Club $club): int
|
||||||
|
{
|
||||||
|
return sizeof($this->topics->where([
|
||||||
|
"group" => $club->getId(),
|
||||||
|
"deleted" => false
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function find(Club $club, string $query): \Traversable
|
||||||
|
{
|
||||||
|
return new Util\EntityStream("Topic", $this->topics->where("title LIKE ? AND group = ? AND deleted = 0", "%$query%", $club->getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastTopics(Club $club, ?int $count = NULL): \Traversable
|
||||||
|
{
|
||||||
|
$topics = $this->topics->where([
|
||||||
|
"group" => $club->getId(),
|
||||||
|
"deleted" => false
|
||||||
|
])->page(1, $count ?? OPENVK_DEFAULT_PER_PAGE)->order("created DESC");
|
||||||
|
|
||||||
|
foreach($topics as $topic)
|
||||||
|
yield $this->toTopic($topic);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
namespace openvk\Web\Presenters;
|
namespace openvk\Web\Presenters;
|
||||||
use openvk\Web\Models\Entities\{Comment, Photo, Video, User};
|
use openvk\Web\Models\Entities\{Comment, Photo, Video, User, Topic};
|
||||||
use openvk\Web\Models\Entities\Notifications\CommentNotification;
|
use openvk\Web\Models\Entities\Notifications\CommentNotification;
|
||||||
use openvk\Web\Models\Repositories\Comments;
|
use openvk\Web\Models\Repositories\Comments;
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ final class CommentPresenter extends OpenVKPresenter
|
||||||
"photos" => "openvk\\Web\\Models\\Repositories\\Photos",
|
"photos" => "openvk\\Web\\Models\\Repositories\\Photos",
|
||||||
"videos" => "openvk\\Web\\Models\\Repositories\\Videos",
|
"videos" => "openvk\\Web\\Models\\Repositories\\Videos",
|
||||||
"notes" => "openvk\\Web\\Models\\Repositories\\Notes",
|
"notes" => "openvk\\Web\\Models\\Repositories\\Notes",
|
||||||
|
"topics" => "openvk\\Web\\Models\\Repositories\\Topics",
|
||||||
];
|
];
|
||||||
|
|
||||||
function renderLike(int $id): void
|
function renderLike(int $id): void
|
||||||
|
@ -38,6 +39,9 @@ final class CommentPresenter extends OpenVKPresenter
|
||||||
$entity = $repo->get($eId);
|
$entity = $repo->get($eId);
|
||||||
if(!$entity) $this->notFound();
|
if(!$entity) $this->notFound();
|
||||||
|
|
||||||
|
if($entity instanceof Topic && $entity->isClosed())
|
||||||
|
$this->notFound();
|
||||||
|
|
||||||
$flags = 0;
|
$flags = 0;
|
||||||
if($this->postParam("as_group") === "on")
|
if($this->postParam("as_group") === "on")
|
||||||
$flags |= 0b10000000;
|
$flags |= 0b10000000;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
namespace openvk\Web\Presenters;
|
namespace openvk\Web\Presenters;
|
||||||
use openvk\Web\Models\Entities\{Club, Photo};
|
use openvk\Web\Models\Entities\{Club, Photo};
|
||||||
use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification;
|
use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification;
|
||||||
use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers};
|
use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics};
|
||||||
|
|
||||||
final class GroupPresenter extends OpenVKPresenter
|
final class GroupPresenter extends OpenVKPresenter
|
||||||
{
|
{
|
||||||
|
@ -28,6 +28,8 @@ final class GroupPresenter extends OpenVKPresenter
|
||||||
$this->template->club = $club;
|
$this->template->club = $club;
|
||||||
$this->template->albums = (new Albums)->getClubAlbums($club, 1, 3);
|
$this->template->albums = (new Albums)->getClubAlbums($club, 1, 3);
|
||||||
$this->template->albumsCount = (new Albums)->getClubAlbumsCount($club);
|
$this->template->albumsCount = (new Albums)->getClubAlbumsCount($club);
|
||||||
|
$this->template->topics = (new Topics)->getLastTopics($club, 3);
|
||||||
|
$this->template->topicsCount = (new Topics)->getClubTopicsCount($club);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,6 +206,7 @@ final class GroupPresenter extends OpenVKPresenter
|
||||||
$club->setShortcode(empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode"));
|
$club->setShortcode(empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode"));
|
||||||
$club->setWall(empty($this->postParam("wall")) ? 0 : 1);
|
$club->setWall(empty($this->postParam("wall")) ? 0 : 1);
|
||||||
$club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display"));
|
$club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display"));
|
||||||
|
$club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1);
|
||||||
|
|
||||||
$website = $this->postParam("website") ?? "";
|
$website = $this->postParam("website") ?? "";
|
||||||
if(empty($website))
|
if(empty($website))
|
||||||
|
|
191
Web/Presenters/TopicsPresenter.php
Normal file
191
Web/Presenters/TopicsPresenter.php
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace openvk\Web\Presenters;
|
||||||
|
use openvk\Web\Models\Entities\{Topic, Club, Comment, Photo, Video};
|
||||||
|
use openvk\Web\Models\Repositories\{Topics, Clubs};
|
||||||
|
|
||||||
|
final class TopicsPresenter extends OpenVKPresenter
|
||||||
|
{
|
||||||
|
private $topics;
|
||||||
|
private $clubs;
|
||||||
|
|
||||||
|
function __construct(Topics $topics, Clubs $clubs)
|
||||||
|
{
|
||||||
|
$this->topics = $topics;
|
||||||
|
$this->clubs = $clubs;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBoard(int $id): void
|
||||||
|
{
|
||||||
|
$this->assertUserLoggedIn();
|
||||||
|
|
||||||
|
$club = $this->clubs->get($id);
|
||||||
|
if(!$club)
|
||||||
|
$this->notFound();
|
||||||
|
|
||||||
|
$this->template->club = $club;
|
||||||
|
$page = (int) ($this->queryParam("p") ?? 1);
|
||||||
|
|
||||||
|
$query = $this->queryParam("query");
|
||||||
|
if($query) {
|
||||||
|
$results = $this->topics->find($club, $query);
|
||||||
|
$this->template->topics = $results->page($page);
|
||||||
|
$this->template->count = $results->size();
|
||||||
|
} else {
|
||||||
|
$this->template->topics = $this->topics->getClubTopics($club, $page);
|
||||||
|
$this->template->count = $this->topics->getClubTopicsCount($club);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->template->paginatorConf = (object) [
|
||||||
|
"count" => $this->template->count,
|
||||||
|
"page" => $page,
|
||||||
|
"amount" => NULL,
|
||||||
|
"perPage" => OPENVK_DEFAULT_PER_PAGE,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTopic(int $clubId, int $topicId): void
|
||||||
|
{
|
||||||
|
$this->assertUserLoggedIn();
|
||||||
|
|
||||||
|
$topic = $this->topics->getTopicById($clubId, $topicId);
|
||||||
|
if(!$topic)
|
||||||
|
$this->notFound();
|
||||||
|
|
||||||
|
$this->template->topic = $topic;
|
||||||
|
$this->template->club = $topic->getClub();
|
||||||
|
$this->template->count = $topic->getCommentsCount();
|
||||||
|
$this->template->page = (int) ($this->queryParam("p") ?? 1);
|
||||||
|
$this->template->comments = iterator_to_array($topic->getComments($this->template->page));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCreate(int $clubId): void
|
||||||
|
{
|
||||||
|
$this->assertUserLoggedIn();
|
||||||
|
|
||||||
|
$club = $this->clubs->get($clubId);
|
||||||
|
if(!$club)
|
||||||
|
$this->notFound();
|
||||||
|
|
||||||
|
if(!$club->isEveryoneCanCreateTopics() && !$club->canBeModifiedBy($this->user->identity))
|
||||||
|
$this->notFound();
|
||||||
|
|
||||||
|
|
||||||
|
if($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||||
|
$this->willExecuteWriteAction();
|
||||||
|
$title = $this->postParam("title");
|
||||||
|
|
||||||
|
if(!$title)
|
||||||
|
$this->flashFail("err", tr("failed_to_create_topic"), tr("no_title_specified"));
|
||||||
|
|
||||||
|
$flags = 0;
|
||||||
|
if($this->postParam("as_group") === "on")
|
||||||
|
$flags |= 0b10000000;
|
||||||
|
|
||||||
|
$topic = new Topic;
|
||||||
|
$topic->setGroup($club->getId());
|
||||||
|
$topic->setOwner($this->user->id);
|
||||||
|
$topic->setTitle(ovk_proc_strtr($title, 127));
|
||||||
|
$topic->setCreated(time());
|
||||||
|
$topic->setFlags($flags);
|
||||||
|
$topic->save();
|
||||||
|
|
||||||
|
// TODO move to trait
|
||||||
|
try {
|
||||||
|
$photo = NULL;
|
||||||
|
$video = NULL;
|
||||||
|
if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) {
|
||||||
|
$album = NULL;
|
||||||
|
if($wall > 0 && $wall === $this->user->id)
|
||||||
|
$album = (new Albums)->getUserWallAlbum($wallOwner);
|
||||||
|
|
||||||
|
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) {
|
||||||
|
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"]);
|
||||||
|
}
|
||||||
|
} catch(ISE $ex) {
|
||||||
|
$this->flash("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик.");
|
||||||
|
$this->redirect("/topic" . $topic->getPrettyId(), static::REDIRECT_TEMPORARY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!empty($this->postParam("text")) || $photo || $video) {
|
||||||
|
try {
|
||||||
|
$comment = new Comment;
|
||||||
|
$comment->setOwner($this->user->id);
|
||||||
|
$comment->setModel(get_class($topic));
|
||||||
|
$comment->setTarget($topic->getId());
|
||||||
|
$comment->setContent($this->postParam("text"));
|
||||||
|
$comment->setCreated(time());
|
||||||
|
$comment->setFlags($flags);
|
||||||
|
$comment->save();
|
||||||
|
} catch (\LengthException $ex) {
|
||||||
|
$this->flash("err", "Не удалось опубликовать комментарий", "Комментарий слишком большой.");
|
||||||
|
$this->redirect("/topic" . $topic->getPrettyId(), static::REDIRECT_TEMPORARY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!is_null($photo))
|
||||||
|
$comment->attach($photo);
|
||||||
|
|
||||||
|
if(!is_null($video))
|
||||||
|
$comment->attach($video);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->redirect("/topic" . $topic->getPrettyId(), static::REDIRECT_TEMPORARY);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->template->club = $club;
|
||||||
|
$this->template->graffiti = (bool) ovkGetQuirk("comments.allow-graffiti");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEdit(int $clubId, int $topicId): void
|
||||||
|
{
|
||||||
|
$this->assertUserLoggedIn();
|
||||||
|
|
||||||
|
$topic = $this->topics->getTopicById($clubId, $topicId);
|
||||||
|
if(!$topic)
|
||||||
|
$this->notFound();
|
||||||
|
|
||||||
|
if(!$topic->canBeModifiedBy($this->user->identity))
|
||||||
|
$this->notFound();
|
||||||
|
|
||||||
|
if($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||||
|
$this->willExecuteWriteAction();
|
||||||
|
$title = $this->postParam("title");
|
||||||
|
|
||||||
|
if(!$title)
|
||||||
|
$this->flashFail("err", tr("failed_to_change_topic"), tr("no_title_specified"));
|
||||||
|
|
||||||
|
$topic->setTitle(ovk_proc_strtr($title, 127));
|
||||||
|
$topic->setClosed(empty($this->postParam("close")) ? 0 : 1);
|
||||||
|
$topic->setPinned(empty($this->postParam("pin")) ? 0 : 1);
|
||||||
|
$topic->save();
|
||||||
|
|
||||||
|
$this->flash("succ", tr("changes_saved"), tr("topic_changes_saved_comment"));
|
||||||
|
$this->redirect("/topic" . $topic->getPrettyId(), static::REDIRECT_TEMPORARY);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->template->topic = $topic;
|
||||||
|
$this->template->club = $topic->getClub();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDelete(int $clubId, int $topicId): void
|
||||||
|
{
|
||||||
|
$this->assertUserLoggedIn();
|
||||||
|
$this->assertNoCSRF();
|
||||||
|
|
||||||
|
$topic = $this->topics->getTopicById($clubId, $topicId);
|
||||||
|
if(!$topic)
|
||||||
|
$this->notFound();
|
||||||
|
|
||||||
|
if(!$topic->canBeModifiedBy($this->user->identity))
|
||||||
|
$this->notFound();
|
||||||
|
|
||||||
|
$this->willExecuteWriteAction();
|
||||||
|
$topic->deleteTopic();
|
||||||
|
|
||||||
|
$this->redirect("/board" . $topic->getClub()->getId(), static::REDIRECT_TEMPORARY);
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,6 +77,14 @@
|
||||||
<input type="checkbox" name="wall" value="1" {if $club->canPost()}checked{/if}/> {_group_allow_post_for_everyone}
|
<input type="checkbox" name="wall" value="1" {if $club->canPost()}checked{/if}/> {_group_allow_post_for_everyone}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="120" valign="top">
|
||||||
|
<span class="nobold">{_discussions}: </span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="everyone_can_create_topics" value="1" n:attr="checked => $club->isEveryoneCanCreateTopics()" /> {_everyone_can_create_topics}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="120" valign="top">
|
<td width="120" valign="top">
|
||||||
<span class="nobold">{_group_administrators_list}: </span>
|
<span class="nobold">{_group_administrators_list}: </span>
|
||||||
|
|
|
@ -197,12 +197,31 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<b><a href="/album{$album->getPrettyId()}">{$album->getName()}</a></b><br>
|
<b><a href="/album{$album->getPrettyId()}">{$album->getName()}</a></b><br>
|
||||||
<span class="nobold">Обновлён {$album->getEditTime() ?? $album->getCreationTime()}</span>
|
<span class="nobold">{tr("updated_at", $album->getEditTime() ?? $album->getCreationTime())}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div n:if="$topicsCount > 0 || $club->isEveryoneCanCreateTopics() || $club->canBeModifiedBy($thisUser)">
|
||||||
|
<div class="content_title_expanded" onclick="hidePanel(this, {$topicsCount});">
|
||||||
|
{_discussions}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="content_subtitle">
|
||||||
|
{tr("topics", $topicsCount)}
|
||||||
|
<div style="float: right;">
|
||||||
|
<a href="/board{$club->getId()}">{_"all_title"}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div n:foreach="$topics as $topic" style="border-bottom: #e6e6e6 solid 1px; padding: 2px;">
|
||||||
|
<b><a href="/topic{$topic->getPrettyId()}">{$topic->getTitle()}</a></b><br>
|
||||||
|
<span class="nobold">{tr("updated_at", $topic->getUpdateTime())}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/block}
|
{/block}
|
||||||
|
|
61
Web/Presenters/templates/Topics/Board.xml
Normal file
61
Web/Presenters/templates/Topics/Board.xml
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{extends "../@listView.xml"}
|
||||||
|
{var iterator = iterator_to_array($topics)}
|
||||||
|
{var page = $paginatorConf->page}
|
||||||
|
|
||||||
|
{block title}{_discussions} {$club->getCanonicalName()}{/block}
|
||||||
|
|
||||||
|
{block header}
|
||||||
|
<a href="{$club->getURL()}">{$club->getCanonicalName()}</a> » {_discussions}
|
||||||
|
|
||||||
|
<div n:if="$club->isEveryoneCanCreateTopics() || $club->canBeModifiedBy($thisUser)" style="float: right;">
|
||||||
|
<a href="/board{$club->getId()}/create">{_create_topic}</a>
|
||||||
|
</div>
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block tabs}
|
||||||
|
<form style="margin-left: 12px;">
|
||||||
|
<input name="query" class="header_search_input" placeholder="{_"header_search"}" value="{$_GET['query'] ?? ''}" style="width: 90%" />
|
||||||
|
<input type="submit" class="button" value="{_"search_button"}" style="width: 7%" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p style="margin-left: 15px;">
|
||||||
|
<b>{tr("results", $count)}</b>
|
||||||
|
</p>
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block actions}
|
||||||
|
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{* BEGIN ELEMENTS DESCRIPTION *}
|
||||||
|
|
||||||
|
{block link|strip|stripHtml}
|
||||||
|
/topic{$x->getPrettyId()}
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block preview}
|
||||||
|
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block name}
|
||||||
|
{$x->getTitle()}
|
||||||
|
<div n:if="$x->isPinned()" class="pinned-mark"></div>
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block description}
|
||||||
|
<div style="float: left;">
|
||||||
|
{tr("messages", $x->getCommentsCount())}
|
||||||
|
</div>
|
||||||
|
{var lastComment = $x->getLastComment()}
|
||||||
|
<div n:if="$lastComment" class="avatar-list-item" style="float: right;">
|
||||||
|
<div class="avatar">
|
||||||
|
<a href="{$lastComment->getOwner()->getURL()}">
|
||||||
|
<img class="ava" src="{$lastComment->getOwner()->getAvatarUrl()}" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<a href="{$lastComment->getOwner()->getURL()}" class="title">{$lastComment->getOwner()->getCanonicalName()}</a>
|
||||||
|
<div class="subtitle">{_replied} {$lastComment->getPublicationTime()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/block}
|
96
Web/Presenters/templates/Topics/Create.xml
Normal file
96
Web/Presenters/templates/Topics/Create.xml
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
{extends "../@layout.xml"}
|
||||||
|
{block title}{_new_topic}{/block}
|
||||||
|
|
||||||
|
{block header}
|
||||||
|
<a href="{$club->getURL()}">{$club->getCanonicalName()}</a>
|
||||||
|
»
|
||||||
|
<a href="/board{$club->getId()}">{_discussions}</a>
|
||||||
|
»
|
||||||
|
{_new_topic}
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block content}
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
<table cellspacing="7" cellpadding="0" width="80%" border="0" align="center">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td width="120" valign="top">
|
||||||
|
<span class="nobold">{_title}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="title" style="width: 100%;" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="120" valign="top">
|
||||||
|
<span class="nobold">{_text}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<textarea id="wall-post-input1" name="text" style="width: 100%; resize: none;"></textarea>
|
||||||
|
<div n:if="$club->canBeModifiedBy($thisUser)" class="post-opts">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="as_group" onchange="onWallAsGroupClick(this)" /> {_post_as_group}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="post-buttons1">
|
||||||
|
<div class="post-upload">
|
||||||
|
{_attachment}: <span>(unknown)</span>
|
||||||
|
</div>
|
||||||
|
<input type="file" class="postFileSel" id="postFilePic" name="_pic_attachment" accept="image/*" style="display: none;" />
|
||||||
|
<input type="file" class="postFileSel" id="postFileVid" name="_vid_attachment" accept="video/*" style="display: none;" />
|
||||||
|
<br/>
|
||||||
|
<div style="float: right; display: flex; flex-direction: column;">
|
||||||
|
<a href="javascript:void(u('#post-buttons1 #wallAttachmentMenu').toggleClass('hidden'));">
|
||||||
|
{_attach}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div id="wallAttachmentMenu" class="hidden">
|
||||||
|
<a href="javascript:void(document.querySelector('#post-buttons1 input[name=_pic_attachment]').click());">
|
||||||
|
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-x-egon.png" />
|
||||||
|
{_attach_photo}
|
||||||
|
</a>
|
||||||
|
<a href="javascript:void(document.querySelector('#post-buttons1 input[name=_vid_attachment]').click());">
|
||||||
|
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-vnd.rn-realmedia.png" />
|
||||||
|
{_attach_video}
|
||||||
|
</a>
|
||||||
|
<a n:if="$graffiti ?? false" href="javascript:initGraffiti(1);">
|
||||||
|
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/draw-brush.png" />
|
||||||
|
{_draw_graffiti}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||||
|
<input type="submit" value="{_create_topic}" class="button" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(() => {
|
||||||
|
u("#post-buttons1 .postFileSel").on("change", function() {
|
||||||
|
handleUpload.bind(this, 1)();
|
||||||
|
});
|
||||||
|
|
||||||
|
setupWallPostInputHandlers(1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{if $graffiti}
|
||||||
|
{script "js/node_modules/react/dist/react-with-addons.min.js"}
|
||||||
|
{script "js/node_modules/react-dom/dist/react-dom.min.js"}
|
||||||
|
{script "js/vnd_literallycanvas.js"}
|
||||||
|
{css "js/node_modules/literallycanvas/lib/css/literallycanvas.css"}
|
||||||
|
{/if}
|
||||||
|
{/block}
|
53
Web/Presenters/templates/Topics/Edit.xml
Normal file
53
Web/Presenters/templates/Topics/Edit.xml
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{extends "../@layout.xml"}
|
||||||
|
{block title}{_edit_topic} "{$topic->getTitle()}"{/block}
|
||||||
|
|
||||||
|
{block header}
|
||||||
|
<a href="{$club->getURL()}">{$club->getCanonicalName()}</a>
|
||||||
|
»
|
||||||
|
<a href="/board{$club->getId()}">{_discussions}</a>
|
||||||
|
»
|
||||||
|
{_edit_topic}
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block content}
|
||||||
|
<div class="container_gray">
|
||||||
|
<b>{$topic->getTitle()}</b>
|
||||||
|
<br />
|
||||||
|
<a href="{$topic->getOwner()->getURL()}">{$topic->getOwner()->getCanonicalName()}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data" style="margin-top: 20px;">
|
||||||
|
<table cellspacing="7" cellpadding="0" width="80%" border="0" align="center">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td width="120" valign="top">
|
||||||
|
<span class="nobold">{_title}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="title" style="width: 100%;" value="{$topic->getTitle()}" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="120" valign="top">
|
||||||
|
<span class="nobold">{_topic_settings}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="pin" n:attr="checked => $topic->isPinned()" /> {_pin_topic}<br />
|
||||||
|
<input type="checkbox" name="close" n:attr="checked => $topic->isClosed()" /> {_close_topic}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a class="button" href="/topic{$topic->getPrettyId()}/delete?hash={urlencode($csrfToken)}">{_delete_topic}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||||
|
<input type="submit" value="{_save}" class="button" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||||
|
</form>
|
||||||
|
{/block}
|
25
Web/Presenters/templates/Topics/Topic.xml
Normal file
25
Web/Presenters/templates/Topics/Topic.xml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{extends "../@layout.xml"}
|
||||||
|
{block title}{_view_topic} "{$topic->getTitle()}"{/block}
|
||||||
|
|
||||||
|
{block header}
|
||||||
|
<a href="{$club->getURL()}">{$club->getCanonicalName()}</a>
|
||||||
|
»
|
||||||
|
<a href="/board{$club->getId()}">{_discussions}</a>
|
||||||
|
»
|
||||||
|
{_view_topic}
|
||||||
|
|
||||||
|
<div style="float: right;" n:if="$topic->canBeModifiedBy($thisUser)">
|
||||||
|
<a href="/topic{$club->getId()}_{$topic->getVirtualId()}/edit">{_edit_topic_action}</a>
|
||||||
|
</div>
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block content}
|
||||||
|
<div class="container_gray">
|
||||||
|
<b>{$topic->getTitle()}</b>
|
||||||
|
<br />
|
||||||
|
<a href="{$topic->getOwner()->getURL()}">{$topic->getOwner()->getCanonicalName()}</a>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
{include "../components/comments.xml", comments => $comments, count => $count, page => $page, model => "topics", club => $club, readOnly => $topic->isClosed(), parent => $topic}
|
||||||
|
</div>
|
||||||
|
{/block}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{extends "@default.xml"}
|
||||||
|
{var post = $notification->getModel(0)}
|
||||||
|
|
||||||
|
{block under}
|
||||||
|
{_nt_yours_adjective} <a href="/topic{$post->getPrettyId()}">{_nt_topic_instrumental}</a>
|
||||||
|
{/block}
|
|
@ -19,6 +19,7 @@ services:
|
||||||
- openvk\Web\Presenters\AdminPresenter
|
- openvk\Web\Presenters\AdminPresenter
|
||||||
- openvk\Web\Presenters\GiftsPresenter
|
- openvk\Web\Presenters\GiftsPresenter
|
||||||
- openvk\Web\Presenters\MessengerPresenter
|
- openvk\Web\Presenters\MessengerPresenter
|
||||||
|
- openvk\Web\Presenters\TopicsPresenter
|
||||||
- openvk\Web\Presenters\ThemepacksPresenter
|
- openvk\Web\Presenters\ThemepacksPresenter
|
||||||
- openvk\Web\Presenters\VKAPIPresenter
|
- openvk\Web\Presenters\VKAPIPresenter
|
||||||
- openvk\Web\Models\Repositories\Users
|
- openvk\Web\Models\Repositories\Users
|
||||||
|
@ -36,4 +37,5 @@ services:
|
||||||
- openvk\Web\Models\Repositories\IPs
|
- openvk\Web\Models\Repositories\IPs
|
||||||
- openvk\Web\Models\Repositories\Vouchers
|
- openvk\Web\Models\Repositories\Vouchers
|
||||||
- openvk\Web\Models\Repositories\Gifts
|
- openvk\Web\Models\Repositories\Gifts
|
||||||
|
- openvk\Web\Models\Repositories\Topics
|
||||||
- openvk\Web\Models\Repositories\ContentSearchRepository
|
- openvk\Web\Models\Repositories\ContentSearchRepository
|
||||||
|
|
|
@ -163,6 +163,16 @@ routes:
|
||||||
handler: "User->pinClub"
|
handler: "User->pinClub"
|
||||||
- url: "/groups_create"
|
- url: "/groups_create"
|
||||||
handler: "Group->create"
|
handler: "Group->create"
|
||||||
|
- url: "/board{num}"
|
||||||
|
handler: "Topics->board"
|
||||||
|
- url: "/board{num}/create"
|
||||||
|
handler: "Topics->create"
|
||||||
|
- url: "/topic{num}_{num}"
|
||||||
|
handler: "Topics->topic"
|
||||||
|
- url: "/topic{num}_{num}/edit"
|
||||||
|
handler: "Topics->edit"
|
||||||
|
- url: "/topic{num}_{num}/delete"
|
||||||
|
handler: "Topics->delete"
|
||||||
- url: "/audios{num}"
|
- url: "/audios{num}"
|
||||||
handler: "Audios->app"
|
handler: "Audios->app"
|
||||||
- url: "/audios{num}.json"
|
- url: "/audios{num}.json"
|
||||||
|
|
|
@ -1673,3 +1673,12 @@ body.scrolled .toTop:hover {
|
||||||
color: #7b7b7b;
|
color: #7b7b7b;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pinned-mark {
|
||||||
|
display: inline-block;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
background: url("/assets/packages/static/openvk/img/pin.png") no-repeat 0px 0px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
|
@ -17,5 +17,6 @@
|
||||||
"openvk\\Web\\Models\\Entities\\TicketComment":17,
|
"openvk\\Web\\Models\\Entities\\TicketComment":17,
|
||||||
"openvk\\Web\\Models\\Entities\\User":18,
|
"openvk\\Web\\Models\\Entities\\User":18,
|
||||||
"openvk\\Web\\Models\\Entities\\Video":19,
|
"openvk\\Web\\Models\\Entities\\Video":19,
|
||||||
"openvk\\Web\\Models\\Entities\\Gift":20
|
"openvk\\Web\\Models\\Entities\\Gift":20,
|
||||||
|
"openvk\\Web\\Models\\Entities\\Topic":21
|
||||||
}
|
}
|
||||||
|
|
22
install/sqls/00014-group-discussions.sql
Normal file
22
install/sqls/00014-group-discussions.sql
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
ALTER TABLE `groups` ADD COLUMN `everyone_can_create_topics` boolean NOT NULL AFTER `administrators_list_display`;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `topics` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL,
|
||||||
|
`group` bigint(20) unsigned NOT NULL,
|
||||||
|
`owner` bigint(20) unsigned NOT NULL,
|
||||||
|
`virtual_id` bigint(20) unsigned NOT NULL,
|
||||||
|
`created` bigint(20) unsigned NOT NULL,
|
||||||
|
`edited` bigint(20) unsigned DEFAULT NULL,
|
||||||
|
`title` varchar(128) COLLATE utf8mb4_unicode_520_ci NOT NULL,
|
||||||
|
`closed` boolean NOT NULL DEFAULT FALSE,
|
||||||
|
`pinned` boolean NOT NULL DEFAULT FALSE,
|
||||||
|
`flags` tinyint(3) unsigned DEFAULT NULL,
|
||||||
|
`deleted` tinyint(1) DEFAULT 0,
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `topics`
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `group` (`group`);
|
||||||
|
|
||||||
|
ALTER TABLE `topics`
|
||||||
|
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;
|
|
@ -484,6 +484,7 @@
|
||||||
"nt_post_instrumental" = "post";
|
"nt_post_instrumental" = "post";
|
||||||
"nt_note_instrumental" = "note";
|
"nt_note_instrumental" = "note";
|
||||||
"nt_photo_instrumental" = "photo";
|
"nt_photo_instrumental" = "photo";
|
||||||
|
"nt_topic_instrumental" = "topic";
|
||||||
|
|
||||||
/* Time */
|
/* Time */
|
||||||
|
|
||||||
|
@ -590,6 +591,39 @@
|
||||||
"banned_2" = "And the reason for this is simple: <b>$1</b>. Unfortunately, this time we had to block you forever.";
|
"banned_2" = "And the reason for this is simple: <b>$1</b>. Unfortunately, this time we had to block you forever.";
|
||||||
"banned_3" = "You can still <a href=\"/support?act=new\">write to the support</a> if you think there was an error or <a href=\"/logout?hash=$1\">logout</a>.";
|
"banned_3" = "You can still <a href=\"/support?act=new\">write to the support</a> if you think there was an error or <a href=\"/logout?hash=$1\">logout</a>.";
|
||||||
|
|
||||||
|
/* Discussions */
|
||||||
|
|
||||||
|
"discussions" = "Discussions";
|
||||||
|
|
||||||
|
"messages_one" = "One message";
|
||||||
|
"messages_other" = "$1 messages";
|
||||||
|
|
||||||
|
"replied" = "replied";
|
||||||
|
"create_topic" = "Create a topic";
|
||||||
|
|
||||||
|
"new_topic" = "New topic";
|
||||||
|
"title" = "Title";
|
||||||
|
"text" = "Text";
|
||||||
|
|
||||||
|
"view_topic" = "View topic";
|
||||||
|
"edit_topic_action" = "Edit topic";
|
||||||
|
"edit_topic" = "Edit topic";
|
||||||
|
"topic_settings" = "Topic settings";
|
||||||
|
"pin_topic" = "Pin topic";
|
||||||
|
"close_topic" = "Close topic";
|
||||||
|
"delete_topic" = "Delete topic";
|
||||||
|
|
||||||
|
"topics_one" = "One topic";
|
||||||
|
"topics_other" = "$1 topics";
|
||||||
|
|
||||||
|
"everyone_can_create_topics" = "Everyone can create topics";
|
||||||
|
|
||||||
|
"topic_changes_saved_comment" = "The updated title and settings will appear on the topic page.";
|
||||||
|
|
||||||
|
"failed_to_create_topic" = "Failed to create topic";
|
||||||
|
"failed_to_change_topic" = "Failed to change topic";
|
||||||
|
"no_title_specified" = "No title specified.";
|
||||||
|
|
||||||
/* Errors */
|
/* Errors */
|
||||||
|
|
||||||
"error_1" = "Incorrect query";
|
"error_1" = "Incorrect query";
|
||||||
|
|
|
@ -504,6 +504,7 @@
|
||||||
"nt_post_instrumental" = "постом";
|
"nt_post_instrumental" = "постом";
|
||||||
"nt_note_instrumental" = "заметкой";
|
"nt_note_instrumental" = "заметкой";
|
||||||
"nt_photo_instrumental" = "фотографией";
|
"nt_photo_instrumental" = "фотографией";
|
||||||
|
"nt_topic_instrumental" = "темой";
|
||||||
|
|
||||||
/* Time */
|
/* Time */
|
||||||
|
|
||||||
|
@ -615,6 +616,43 @@
|
||||||
"banned_2" = "А причина этому проста: <b>$1</b>. К сожалению, на этот раз нам пришлось заблокировать вас навсегда.";
|
"banned_2" = "А причина этому проста: <b>$1</b>. К сожалению, на этот раз нам пришлось заблокировать вас навсегда.";
|
||||||
"banned_3" = "Вы всё ещё можете <a href=\"/support?act=new\">написать в службу поддержки</a>, если считаете что произошла ошибка или <a href=\"/logout?hash=$1\">выйти</a>.";
|
"banned_3" = "Вы всё ещё можете <a href=\"/support?act=new\">написать в службу поддержки</a>, если считаете что произошла ошибка или <a href=\"/logout?hash=$1\">выйти</a>.";
|
||||||
|
|
||||||
|
/* Discussions */
|
||||||
|
|
||||||
|
"discussions" = "Обсуждения";
|
||||||
|
|
||||||
|
"messages_one" = "Одно сообщение";
|
||||||
|
"messages_few" = "$1 сообщения";
|
||||||
|
"messages_many" = "$1 сообщений";
|
||||||
|
"messages_other" = "$1 сообщений";
|
||||||
|
|
||||||
|
"replied" = "ответил";
|
||||||
|
"create_topic" = "Создать тему";
|
||||||
|
|
||||||
|
"new_topic" = "Новая тема";
|
||||||
|
"title" = "Заголовок";
|
||||||
|
"text" = "Текст";
|
||||||
|
|
||||||
|
"view_topic" = "Просмотр темы";
|
||||||
|
"edit_topic_action" = "Редактировать тему";
|
||||||
|
"edit_topic" = "Редактирование темы";
|
||||||
|
"topic_settings" = "Настройки темы";
|
||||||
|
"pin_topic" = "Закрепить тему";
|
||||||
|
"close_topic" = "Закрыть тему";
|
||||||
|
"delete_topic" = "Удалить тему";
|
||||||
|
|
||||||
|
"topics_one" = "Одна тема";
|
||||||
|
"topics_few" = "$1 темы";
|
||||||
|
"topics_many" = "$1 тема";
|
||||||
|
"topics_other" = "$1 тем";
|
||||||
|
|
||||||
|
"everyone_can_create_topics" = "Все могут создавать темы";
|
||||||
|
|
||||||
|
"topic_changes_saved_comment" = "Обновлённый заголовок и настройки появятся на странице с темой.";
|
||||||
|
|
||||||
|
"failed_to_create_topic" = "Не удалось создать тему";
|
||||||
|
"failed_to_change_topic" = "Не удалось изменить тему";
|
||||||
|
"no_title_specified" = "Заголовок не указан.";
|
||||||
|
|
||||||
/* Errors */
|
/* Errors */
|
||||||
|
|
||||||
"error_1" = "Некорректный запрос";
|
"error_1" = "Некорректный запрос";
|
||||||
|
|
Loading…
Reference in a new issue