From 356c782f74e2f8b921da38371cc7d804f0db5a94 Mon Sep 17 00:00:00 2001 From: Maxim Leshchenko Date: Wed, 15 Dec 2021 00:27:17 +0200 Subject: [PATCH] Groups: Discussions This commit adds discussions (forums) to groups, similar to how it was implemented in the original VK --- Web/Models/Entities/Topic.php | 86 ++++++++ Web/Models/Repositories/Topics.php | 73 +++++++ Web/Presenters/CommentPresenter.php | 14 +- Web/Presenters/GroupPresenter.php | 5 +- Web/Presenters/TopicsPresenter.php | 191 ++++++++++++++++++ Web/Presenters/templates/Group/Edit.xml | 10 +- Web/Presenters/templates/Group/View.xml | 21 +- Web/Presenters/templates/Topics/Board.xml | 61 ++++++ Web/Presenters/templates/Topics/Create.xml | 96 +++++++++ Web/Presenters/templates/Topics/Edit.xml | 53 +++++ Web/Presenters/templates/Topics/Topic.xml | 25 +++ .../components/notifications/2/_21_18_.xml | 6 + Web/di.yml | 2 + Web/routes.yml | 10 + Web/static/css/style.css | 9 + data/modelCodes.json | 3 +- install/sqls/00014-group-discussions.sql | 22 ++ locales/en.strings | 34 ++++ locales/ru.strings | 38 ++++ 19 files changed, 750 insertions(+), 9 deletions(-) create mode 100644 Web/Models/Entities/Topic.php create mode 100644 Web/Models/Repositories/Topics.php create mode 100644 Web/Presenters/TopicsPresenter.php create mode 100644 Web/Presenters/templates/Topics/Board.xml create mode 100644 Web/Presenters/templates/Topics/Create.xml create mode 100644 Web/Presenters/templates/Topics/Edit.xml create mode 100644 Web/Presenters/templates/Topics/Topic.xml create mode 100644 Web/Presenters/templates/components/notifications/2/_21_18_.xml create mode 100644 install/sqls/00014-group-discussions.sql diff --git a/Web/Models/Entities/Topic.php b/Web/Models/Entities/Topic.php new file mode 100644 index 00000000..938eb383 --- /dev/null +++ b/Web/Models/Entities/Topic.php @@ -0,0 +1,86 @@ +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(); + } +} diff --git a/Web/Models/Repositories/Topics.php b/Web/Models/Repositories/Topics.php new file mode 100644 index 00000000..23b854d4 --- /dev/null +++ b/Web/Models/Repositories/Topics.php @@ -0,0 +1,73 @@ +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); + } +} diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index c904c295..c114ba5d 100644 --- a/Web/Presenters/CommentPresenter.php +++ b/Web/Presenters/CommentPresenter.php @@ -1,16 +1,17 @@ "openvk\\Web\\Models\\Repositories\\Posts", - "photos" => "openvk\\Web\\Models\\Repositories\\Photos", - "videos" => "openvk\\Web\\Models\\Repositories\\Videos", - "notes" => "openvk\\Web\\Models\\Repositories\\Notes", + "posts" => "openvk\\Web\\Models\\Repositories\\Posts", + "photos" => "openvk\\Web\\Models\\Repositories\\Photos", + "videos" => "openvk\\Web\\Models\\Repositories\\Videos", + "notes" => "openvk\\Web\\Models\\Repositories\\Notes", + "topics" => "openvk\\Web\\Models\\Repositories\\Topics", ]; function renderLike(int $id): void @@ -37,6 +38,9 @@ final class CommentPresenter extends OpenVKPresenter $repo = new $repoClass; $entity = $repo->get($eId); if(!$entity) $this->notFound(); + + if($entity instanceof Topic && $entity->isClosed()) + $this->notFound(); $flags = 0; if($this->postParam("as_group") === "on") diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index a253ea72..1a7b1932 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -2,7 +2,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{Club, Photo}; 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 { @@ -28,6 +28,8 @@ final class GroupPresenter extends OpenVKPresenter $this->template->club = $club; $this->template->albums = (new Albums)->getClubAlbums($club, 1, 3); $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->setWall(empty($this->postParam("wall")) ? 0 : 1); $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") ?? ""; if(empty($website)) diff --git a/Web/Presenters/TopicsPresenter.php b/Web/Presenters/TopicsPresenter.php new file mode 100644 index 00000000..de27fd38 --- /dev/null +++ b/Web/Presenters/TopicsPresenter.php @@ -0,0 +1,191 @@ +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); + } +} diff --git a/Web/Presenters/templates/Group/Edit.xml b/Web/Presenters/templates/Group/Edit.xml index 093d7aa1..ecf17393 100644 --- a/Web/Presenters/templates/Group/Edit.xml +++ b/Web/Presenters/templates/Group/Edit.xml @@ -69,7 +69,7 @@ - + {_wall}: @@ -77,6 +77,14 @@ canPost()}checked{/if}/> {_group_allow_post_for_everyone} + + + {_discussions}: + + + {_everyone_can_create_topics} + + {_group_administrators_list}: diff --git a/Web/Presenters/templates/Group/View.xml b/Web/Presenters/templates/Group/View.xml index 4f3ac113..7b885fcc 100644 --- a/Web/Presenters/templates/Group/View.xml +++ b/Web/Presenters/templates/Group/View.xml @@ -197,12 +197,31 @@
{$album->getName()}
- Обновлён {$album->getEditTime() ?? $album->getCreationTime()} + {tr("updated_at", $album->getEditTime() ?? $album->getCreationTime())}
+
+
+ {_discussions} +
+
+
+ {tr("topics", $topicsCount)} +
+ {_"all_title"} +
+
+
+
+ {$topic->getTitle()}
+ {tr("updated_at", $topic->getUpdateTime())} +
+
+
+
{/block} diff --git a/Web/Presenters/templates/Topics/Board.xml b/Web/Presenters/templates/Topics/Board.xml new file mode 100644 index 00000000..6d38ea20 --- /dev/null +++ b/Web/Presenters/templates/Topics/Board.xml @@ -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} + {$club->getCanonicalName()} » {_discussions} + +
+ {_create_topic} +
+{/block} + +{block tabs} +
+ + +
+ +

+ {tr("results", $count)} +

+{/block} + +{block actions} + +{/block} + +{* BEGIN ELEMENTS DESCRIPTION *} + +{block link|strip|stripHtml} + /topic{$x->getPrettyId()} +{/block} + +{block preview} + +{/block} + +{block name} + {$x->getTitle()} +
+{/block} + +{block description} +
+ {tr("messages", $x->getCommentsCount())} +
+ {var lastComment = $x->getLastComment()} +
+
+ + + +
+
+ {$lastComment->getOwner()->getCanonicalName()} +
{_replied} {$lastComment->getPublicationTime()}
+
+
+{/block} diff --git a/Web/Presenters/templates/Topics/Create.xml b/Web/Presenters/templates/Topics/Create.xml new file mode 100644 index 00000000..f68c4f6b --- /dev/null +++ b/Web/Presenters/templates/Topics/Create.xml @@ -0,0 +1,96 @@ +{extends "../@layout.xml"} +{block title}{_new_topic}{/block} + +{block header} + {$club->getCanonicalName()} + » + {_discussions} + » + {_new_topic} +{/block} + +{block content} +
+ + + + + + + + + + + + + + + +
+ {_title} + + +
+ {_text} + + +
+ +
+
+
+ {_attachment}: (unknown) +
+ + +
+ +
+
+ + + + +
+ + +
+ + + + {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} diff --git a/Web/Presenters/templates/Topics/Edit.xml b/Web/Presenters/templates/Topics/Edit.xml new file mode 100644 index 00000000..c40f28b2 --- /dev/null +++ b/Web/Presenters/templates/Topics/Edit.xml @@ -0,0 +1,53 @@ +{extends "../@layout.xml"} +{block title}{_edit_topic} "{$topic->getTitle()}"{/block} + +{block header} + {$club->getCanonicalName()} + » + {_discussions} + » + {_edit_topic} +{/block} + +{block content} +
+ {$topic->getTitle()} +
+ {$topic->getOwner()->getCanonicalName()} +
+ +
+ + + + + + + + + + + + + + + +
+ {_title} + + +
+ {_topic_settings} + + {_pin_topic}
+ {_close_topic} +
+ {_delete_topic} + + + +
+ + +
+{/block} diff --git a/Web/Presenters/templates/Topics/Topic.xml b/Web/Presenters/templates/Topics/Topic.xml new file mode 100644 index 00000000..bd4a71bb --- /dev/null +++ b/Web/Presenters/templates/Topics/Topic.xml @@ -0,0 +1,25 @@ +{extends "../@layout.xml"} +{block title}{_view_topic} "{$topic->getTitle()}"{/block} + +{block header} + {$club->getCanonicalName()} + » + {_discussions} + » + {_view_topic} + +
+ {_edit_topic_action} +
+{/block} + +{block content} +
+ {$topic->getTitle()} +
+ {$topic->getOwner()->getCanonicalName()} +
+
+ {include "../components/comments.xml", comments => $comments, count => $count, page => $page, model => "topics", club => $club, readOnly => $topic->isClosed(), parent => $topic} +
+{/block} diff --git a/Web/Presenters/templates/components/notifications/2/_21_18_.xml b/Web/Presenters/templates/components/notifications/2/_21_18_.xml new file mode 100644 index 00000000..eaf862f1 --- /dev/null +++ b/Web/Presenters/templates/components/notifications/2/_21_18_.xml @@ -0,0 +1,6 @@ +{extends "@default.xml"} +{var post = $notification->getModel(0)} + +{block under} + {_nt_yours_adjective} {_nt_topic_instrumental} +{/block} diff --git a/Web/di.yml b/Web/di.yml index ca1b857f..6fa75d2a 100644 --- a/Web/di.yml +++ b/Web/di.yml @@ -19,6 +19,7 @@ services: - openvk\Web\Presenters\AdminPresenter - openvk\Web\Presenters\GiftsPresenter - openvk\Web\Presenters\MessengerPresenter + - openvk\Web\Presenters\TopicsPresenter - openvk\Web\Presenters\ThemepacksPresenter - openvk\Web\Presenters\VKAPIPresenter - openvk\Web\Models\Repositories\Users @@ -36,4 +37,5 @@ services: - openvk\Web\Models\Repositories\IPs - openvk\Web\Models\Repositories\Vouchers - openvk\Web\Models\Repositories\Gifts + - openvk\Web\Models\Repositories\Topics - openvk\Web\Models\Repositories\ContentSearchRepository diff --git a/Web/routes.yml b/Web/routes.yml index 715a8a84..9fc3c7f6 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -163,6 +163,16 @@ routes: handler: "User->pinClub" - url: "/groups_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}" handler: "Audios->app" - url: "/audios{num}.json" diff --git a/Web/static/css/style.css b/Web/static/css/style.css index 26f4b546..692740b6 100644 --- a/Web/static/css/style.css +++ b/Web/static/css/style.css @@ -1673,3 +1673,12 @@ body.scrolled .toTop:hover { color: #7b7b7b; 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; +} diff --git a/data/modelCodes.json b/data/modelCodes.json index 6ac105ee..d27dd6d5 100644 --- a/data/modelCodes.json +++ b/data/modelCodes.json @@ -17,5 +17,6 @@ "openvk\\Web\\Models\\Entities\\TicketComment":17, "openvk\\Web\\Models\\Entities\\User":18, "openvk\\Web\\Models\\Entities\\Video":19, - "openvk\\Web\\Models\\Entities\\Gift":20 + "openvk\\Web\\Models\\Entities\\Gift":20, + "openvk\\Web\\Models\\Entities\\Topic":21 } diff --git a/install/sqls/00014-group-discussions.sql b/install/sqls/00014-group-discussions.sql new file mode 100644 index 00000000..3586406f --- /dev/null +++ b/install/sqls/00014-group-discussions.sql @@ -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; diff --git a/locales/en.strings b/locales/en.strings index 329365a9..403d8a81 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -484,6 +484,7 @@ "nt_post_instrumental" = "post"; "nt_note_instrumental" = "note"; "nt_photo_instrumental" = "photo"; +"nt_topic_instrumental" = "topic"; /* Time */ @@ -590,6 +591,39 @@ "banned_2" = "And the reason for this is simple: $1. Unfortunately, this time we had to block you forever."; "banned_3" = "You can still write to the support if you think there was an error or logout."; +/* 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 */ "error_1" = "Incorrect query"; diff --git a/locales/ru.strings b/locales/ru.strings index 6b73dbcf..880a095b 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -504,6 +504,7 @@ "nt_post_instrumental" = "постом"; "nt_note_instrumental" = "заметкой"; "nt_photo_instrumental" = "фотографией"; +"nt_topic_instrumental" = "темой"; /* Time */ @@ -615,6 +616,43 @@ "banned_2" = "А причина этому проста: $1. К сожалению, на этот раз нам пришлось заблокировать вас навсегда."; "banned_3" = "Вы всё ещё можете написать в службу поддержки, если считаете что произошла ошибка или выйти."; +/* 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 */ "error_1" = "Некорректный запрос";