From bddfbdc3682a1c08235537e5a3a52da3db66e0e9 Mon Sep 17 00:00:00 2001
From: veselcraft <veselcraft@icloud.com>
Date: Tue, 15 Aug 2023 00:59:57 +0300
Subject: [PATCH 01/26] Reports: Fix 500 error while trying to delete any
 non-text publication

---
 Web/Models/Entities/Report.php | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/Web/Models/Entities/Report.php b/Web/Models/Entities/Report.php
index 4070952e..09c838ef 100644
--- a/Web/Models/Entities/Report.php
+++ b/Web/Models/Entities/Report.php
@@ -96,8 +96,13 @@ class Report extends RowModel
     {
         if ($this->getContentType() !== "user") {
             $pubTime = $this->getContentObject()->getPublicationTime();
-            $name = $this->getContentObject()->getName();
-            $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $pubTime ($name) был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать.");
+            if (method_exists($this->getContentObject(), "getName")) {
+                $name = $this->getContentObject()->getName();
+                $placeholder = "$pubTime ($name)";
+            } else {
+                $placeholder = "$pubTime";
+            }
+            $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $placeholder был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать.");
             $this->getContentObject()->delete($this->getContentType() !== "app");
         }
 

From a2c5896fa1702476bcee31958e01821c61aec546 Mon Sep 17 00:00:00 2001
From: veselcraft <veselcraft@icloud.com>
Date: Tue, 15 Aug 2023 01:10:49 +0300
Subject: [PATCH 02/26] Reports: Fix 500 error while trying to delete group's
 publication

---
 Web/Models/Entities/Report.php | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/Web/Models/Entities/Report.php b/Web/Models/Entities/Report.php
index 09c838ef..d449a2e8 100644
--- a/Web/Models/Entities/Report.php
+++ b/Web/Models/Entities/Report.php
@@ -3,6 +3,7 @@ namespace openvk\Web\Models\Entities;
 use openvk\Web\Util\DateTime;
 use Nette\Database\Table\ActiveRow;
 use openvk\Web\Models\RowModel;
+use openvk\Web\Models\Entities\Club;
 use Chandler\Database\DatabaseConnection;
 use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Users, Posts, Photos, Videos, Clubs};
 use Chandler\Database\DatabaseConnection as DB;
@@ -102,7 +103,13 @@ class Report extends RowModel
             } else {
                 $placeholder = "$pubTime";
             }
-            $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $placeholder был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать.");
+
+            if ($this->getAuthor() instanceof Club) {
+                $name = $this->getAuthor()->getName();
+                $this->getAuthor()->getOwner()->adminNotify("Ваш контент, который опубликовали $placeholder в созданной вами группе \"$name\" был удалён модераторами инстанса. За повторные или серьёзные нарушения группу могут заблокировать.");
+            } else {
+                $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $placeholder был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать.");
+            }
             $this->getContentObject()->delete($this->getContentType() !== "app");
         }
 

From 1174ddfa4f7112519c27ba73b86175b6b738babd Mon Sep 17 00:00:00 2001
From: n1rwana <aydashkin@vk.com>
Date: Tue, 15 Aug 2023 02:12:48 +0300
Subject: [PATCH 03/26] =?UTF-8?q?[Reports]=20=D0=92=D0=BE=D0=B7=D0=BC?=
 =?UTF-8?q?=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D0=B5=D1=80?=
 =?UTF-8?q?=D0=B5=D0=B9=D1=82=D0=B8=20=D0=BA=20=D0=BF=D0=BE=D1=81=D1=82?=
 =?UTF-8?q?=D1=83=20=D0=B8=D0=B7=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD?=
 =?UTF-8?q?=D1=82=D0=B0=D1=80=D0=B8=D1=8F=20(#965)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

ПОРНО
---
 Web/Presenters/templates/Report/ViewContent.xml | 2 +-
 Web/Presenters/templates/components/comment.xml | 7 ++++++-
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/Web/Presenters/templates/Report/ViewContent.xml b/Web/Presenters/templates/Report/ViewContent.xml
index 3928495f..249e6b38 100644
--- a/Web/Presenters/templates/Report/ViewContent.xml
+++ b/Web/Presenters/templates/Report/ViewContent.xml
@@ -16,7 +16,7 @@
         {elseif $type == "group" || $type == "user"}
             {include "../components/group.xml", group => $object, isUser => $type == "user"}
         {elseif $type == "comment"}
-            {include "../components/comment.xml", comment => $object, timeOnly => true}
+            {include "../components/comment.xml", comment => $object, timeOnly => true, linkWithPost => true}
         {elseif $type == "note"}
             {include "./content/note.xml", note => $object}
         {elseif $type == "app"}
diff --git a/Web/Presenters/templates/components/comment.xml b/Web/Presenters/templates/components/comment.xml
index 5067c6c8..714893d1 100644
--- a/Web/Presenters/templates/components/comment.xml
+++ b/Web/Presenters/templates/components/comment.xml
@@ -29,7 +29,12 @@
                         </div>
                     </div>
                     <div n:if="isset($thisUser) &&! ($compact ?? false)" class="post-menu">
-                        <a href="#_comment{$comment->getId()}" class="date">{$comment->getPublicationTime()}</a>
+                        <a
+                            href="{=$linkWithPost && get_class($comment->getTarget()) == 'openvk\Web\Models\Entities\Post' ? '/wall' . $comment->getTarget()->getPrettyId() : ''}#_comment{$comment->getId()}"
+                            class="date"
+                        >
+                            {$comment->getPublicationTime()}
+                        </a>
                         {if !$timeOnly}
                             &nbsp;|
                             {if $comment->canBeDeletedBy($thisUser)}

From c2b6db1b8a3103720ad9217041999f7eb759ba29 Mon Sep 17 00:00:00 2001
From: veselcraft <veselcraft@icloud.com>
Date: Tue, 15 Aug 2023 02:39:48 +0300
Subject: [PATCH 04/26] Global: Add underline while cursor hovering to the
 clickable counters in the left menu

---
 Web/Presenters/templates/@layout.xml | 2 +-
 Web/static/css/main.css              | 4 ++++
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml
index c3d561b2..f20169d6 100644
--- a/Web/Presenters/templates/@layout.xml
+++ b/Web/Presenters/templates/@layout.xml
@@ -176,7 +176,7 @@
                             <a href="{$thisUser->getURL()}" class="link" title="{_my_page} [Alt+Shift+.]" accesskey=".">{_my_page}</a>
                             <a href="/friends{$thisUser->getId()}" class="link">{_my_friends}
                                 <object type="internal/link" n:if="$thisUser->getFollowersCount() > 0">
-                                    <a href="/friends{$thisUser->getId()}?act=incoming">
+                                    <a href="/friends{$thisUser->getId()}?act=incoming" class="linkunderline">
                                        (<b>{$thisUser->getFollowersCount()}</b>)
                                     </a>
                                 </object>
diff --git a/Web/static/css/main.css b/Web/static/css/main.css
index c6d22a04..55484f13 100644
--- a/Web/static/css/main.css
+++ b/Web/static/css/main.css
@@ -29,6 +29,10 @@ a {
     cursor: pointer;
 }
 
+.linkunderline:hover {
+    text-decoration: underline;
+}
+
 p {
     margin: 5px 0;
 }

From 69d0739ef155c7a959b1e7f361e42bf04533167e Mon Sep 17 00:00:00 2001
From: lalka2018 <99399973+lalka2016@users.noreply.github.com>
Date: Sun, 20 Aug 2023 13:55:41 +0300
Subject: [PATCH 05/26] dghnryjtyj (#972)

---
 VKAPI/Handlers/Notes.php     | 2 +-
 Web/Models/Entities/Note.php | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/VKAPI/Handlers/Notes.php b/VKAPI/Handlers/Notes.php
index ce26baae..7c9c9fec 100644
--- a/VKAPI/Handlers/Notes.php
+++ b/VKAPI/Handlers/Notes.php
@@ -211,7 +211,7 @@ final class Notes extends VKAPIRequestHandler
                 $items = [];
     
                 $note = (new NotesRepo)->getNoteById((int)$id[0], (int)$id[1]);
-                if($note) {
+                if($note && !$note->isDeleted()) {
                     $nodez->notes[] = $note->toVkApiStruct();
                 }
             }
diff --git a/Web/Models/Entities/Note.php b/Web/Models/Entities/Note.php
index 37d9ac29..83082bf3 100644
--- a/Web/Models/Entities/Note.php
+++ b/Web/Models/Entities/Note.php
@@ -124,7 +124,7 @@ class Note extends Postable
         $res = (object) [];
 
         $res->type          = "note";
-        $res->id            = $this->getId();
+        $res->id            = $this->getVirtualId();
         $res->owner_id      = $this->getOwner()->getId();
         $res->title         = $this->getName();
         $res->text          = $this->getText();

From 0b7a2e1eda9e1b88935418680ff7afad7d82289e Mon Sep 17 00:00:00 2001
From: lvl <90154880+apeeh@users.noreply.github.com>
Date: Mon, 21 Aug 2023 13:44:56 +0400
Subject: [PATCH 06/26] Update hy.strings (#973)

it's been a while
---
 locales/hy.strings | 573 ++++++++++++++++++++++++++++++++++++---------
 1 file changed, 459 insertions(+), 114 deletions(-)

diff --git a/locales/hy.strings b/locales/hy.strings
index de11783f..09a8e344 100644
--- a/locales/hy.strings
+++ b/locales/hy.strings
@@ -17,6 +17,7 @@
 "password" = "Գաղտնաբառ";
 "registration" = "Գրանցում";
 "forgot_password" = "Մոռացե՞լ եք գաղտնաբառը";
+
 "checkbox_in_registration" = "Ես համաձայն եմ <a href='/privacy'>կոնֆիդենցիալության քաղաքականությանն</a> ու <a href='/terms'>կայքի կանոնադրությանը</a>։";
 "checkbox_in_registration_unchecked" = "Դուք պետք է համաձայնվեք պայմանների հետ նախքան գրանցվելը։";
 
@@ -54,8 +55,11 @@
 "register_meta_desc" = "Գրանցվեք $1 -ում հենց հիմա՛";
 "register_referer_meta_title" = "$1 -ն հրավիրում է ձեզ դեպի $2";
 "register_referer_meta_desc" = "Միացե՛ք $1 -ին և բազմաթիվ օգտատերերին $2 -ու՛մ";
+"registration_welcome_1" = "յուրահատուկ գործընկերների որոնման գործիք է, հիմնված ՎԿոնտակտե–ի կառուցվաշքի վրա։";
+"registration_welcome_2" = "Մենք ցանկանում ենք, որպեսզի Ձեր ընկերները, դասարանցիները, հարևանները և գործընկերները միշտ մնան կապի մեջ։";
 
 "users" = "Օգտատերեր";
+"other_fields" = "Այլ դաշտեր";
 
 /* Profile information */
 
@@ -75,18 +79,20 @@
 "female" = "իգական";
 "description" = "Նկարագրություն";
 "save" = "Պահպանել";
-"main_information" = "Հիմնական ինֆորմացիա";
+"main_information" = "Հիմնական տեղեկություն";
+"additional_information" = "Հավելյալ տեղեկություն";
 "nickname" = "Մականուն";
 "online" = "Օնլայն";
 "was_online" = "եղել է ցանցում";
 "was_online_m" = "եղել է ցանցում";
 "was_online_f" = "եղել է ցանցում";
 "all_title" = "Բոլորը";
-"information" = "Ինֆորմացիա";
+"information" = "Տեղեկություն";
 "status" = "Կարգավիճակ";
-"no_information_provided" = "Ինֆորմացիան բացակայում է";
+"no_information_provided" = "Տեղեկությունը բացակայում է";
 "deceased_person" = "Վախճանված";
 "none" = "բացակայում է";
+"desc_none" = "առանց նկարագրության";
 "send" = "ուղարկել";
 
 "years_zero" = "Զրո տարեկան";
@@ -98,7 +104,7 @@
 "show_my_birthday" = "Ցույց տալ ծննդյան օրը";
 "show_only_month_and_day" = "Ցուցադրել միայն ամիսն ու օրը";
 
-"relationship" = "Ընտանեկան դրություն";
+"relationship" = "Կարգավիճակ";
 
 "relationship_0" = "Ընտրված չէ";
 "relationship_1" = "Չամուսնացած եմ";
@@ -128,7 +134,7 @@
 "politViews_8" = "Ուլտրակոնսերվատիվ";
 "politViews_9" = "Լիբերտարիանական";
 
-"contact_information" = "Կոնտակտային ինֆորմացիա";
+"contact_information" = "Կոնտակտային տեղեկատվություն";
 
 "email" = "Էլեկտրոնային հասցե";
 "phone" = "Հեռախոս";
@@ -137,7 +143,7 @@
 "city" = "Քաղաք";
 "address" = "Հասցե";
 
-"personal_information" = "Անձնական ինֆորմացիա";
+"personal_information" = "Անձնական տեղեկատվություն";
 
 "interests" = "Հետաքրքրություններ";
 "favorite_music" = "Սիրված երգ";
@@ -150,7 +156,7 @@
 "updated_at" = "Թարմացված է $1";
 
 "user_banned" = "Ցավո՛ք, մենք ստիպված կասեցրել ենք <b>$1</b>-ի էջը։";
-"user_banned_comment" = "Մոդերատորի մեկնաբանությունը․";
+"user_banned_comment" = "Մոդերատորի մեկնաբանությունը.";
 
 /* Wall */
 
@@ -163,6 +169,9 @@
 "post_deact_f" = "ջնջել է էջը հետևյալ բառերով.";
 "post_deact_silent_m" = "սուս ու փուս ջնջել է էջը։";
 "post_deact_silent_f" = "սուս ու փուս ջնջել է էջը։";
+"post_on_your_wall" = "Ձեր պատին";
+"post_on_group_wall" = "$1 –ին";
+"post_on_user_wall" = "$1 –ի պատին";
 "wall" = "Պատ";
 "post" = "Գրություն";
 "write" = "Գրել";
@@ -194,9 +203,9 @@
 "attachment" = "Հավելում";
 "post_as_group" = "Խմբի անունից";
 "comment_as_group" = "Մեկնաբանել խմբի անունից";
-"add_signature" = "Հեղինակի ստորագրություն";
+"add_signature" = "Ավելացնել ստորագրություն";
 "contains_nsfw" = "Պարունակում է NSFW մատերիալ";
-"nsfw_warning" = "Այս պոստը կարող է պարունակել 18+ մատերիալ";
+"nsfw_warning" = "Այս գրությունը կարող է պարունակել 18+ մատերիալ";
 "report" = "Բողոքարկել";
 "attach" = "Ամրացնել";
 "attach_photo" = "Ամրացնել նկար";
@@ -214,47 +223,37 @@
 /* Friends */
 
 "friends" = "Ընկերներ";
-"followers" = "Բաժանորդներ";
-"follower" = "Բաժանորդ";
+"followers" = "Հետևորդներ";
+"follower" = "Հետևորդ";
 "friends_add" = "Ավելացնել դեպի ընկերներ";
 "friends_delete" = "Հեռացնել ընկերներից";
 "friends_reject" = "Չեղարկել հայտը";
 "friends_accept" = "Ընդունել հայտը";
 "send_message" = "Ուղարկել նամակ";
-"incoming_req" = "Բաժանորդներ";
-"outcoming_req" = "Հայցեր";
+"incoming_req" = "Սպասվող";
+"outcoming_req" = "Արտագնա";
 "req" = "Հայցեր";
 "friends_online" = "Ընկերները ցանցում";
 "all_friends" = "Բոլոր ընկերները"; 
 
 "req_zero" = "Ոչ մի հայտ չի գտնվել...";
 "req_one" = "Գտնվեց մեկ հայտ";
-"req_few" = "Գտնվեց $1 հայտ";
-"req_many" = "Գտնվեց $1 հայտ";
 "req_other" = "Գտնվեց $1 հայտ";
 
 "friends_zero" = "Ոչ մի ընկեր չկա";
 "friends_one" = "$1 ընկեր";
-"friends_few" = "$1 ընկեր";
-"friends_many" = "$1 հատ ընկեր";
 "friends_other" = "$1 հատ ընկեր";
 
 "friends_list_zero" = "Դուք դեռ չունեք ընկերներ";
 "friends_list_one" = "Դուք ունեք մեկ ընկեր";
-"friends_list_few" = "Դուք ունեք $1 ընկեր";
-"friends_list_many" = "Դուք ունեք $1 ընկեր";
 "friends_list_other" = "Դուք ունեք $1 ընկեր";
 
-"followers_zero" = "Ոչ մի բաժանորդ չունեք";
-"followers_one" = "$1 բաժանորդ";
-"followers_few" = "$1 բաժանորդ";
-"followers_many" = "$1 բաժանորդ";
-"followers_other" = "$1 բաժանորդ";
+"followers_zero" = "Ոչ մի հետևորդ չունեք";
+"followers_one" = "$1 հետևորդ";
+"followers_other" = "$1 հետևորդ";
 
 "subscriptions_zero" = "Ոչ մեկի վրա չեք բաժանորդագրվել";
 "subscriptions_one" = "$1 բաժանորդագրություն";
-"subscriptions_few" = "$1 բաժանորդագրություն";
-"subscriptions_many" = "$1 բաժանորդագրություն";
 "subscriptions_other" = "$1 բաժանորդագրություն";
 
 "friends_list_online_zero" = "Դուք դեռ չունեք ցանցի մեջ գտնվող ընկերներ";
@@ -269,7 +268,7 @@
 "subscribe" = "Բաժանորդագրվել";
 "unsubscribe" = "Հետ բաժանորդագրվել";
 "subscriptions" = "Բաժանորդագրություններ";
-"join_community" = "Մտնել խումբ";
+"join_community" = "Միանալ խմբին";
 "leave_community" = "Լքել խումբը";
 "check_community" = "Դիտել խումբը";
 "min_6_community" = "Անվանումը չպետք է լինի 6 նշից պակաս";
@@ -279,20 +278,19 @@
 "create_group" = "Ստեղծել խումբ";
 "group_managers" = "Ղեկավարություն";
 "group_type" = "Խմբի տեսակ";
-"group_type_open" = "Սա բաց խումբ է․ այստեղ ամեն ոք կարող է մտնել։";
+"group_type_open" = "Սա բաց խումբ է․ ամեն ոք կարող է միանալ իրեն։";
 "group_type_closed" = "Սա փակ խումբ է․ այստեղ միանալու համար անհրաժեշտ է հայտ թողնել։";
-"creator" = "Ստեղծող";
+"creator" = "Հեղինակ";
 "administrators" = "Ադմինիստրատորներ";
-"add_to_left_menu" = "Ավելացնել դեպի ձախ մենյու";
-"remove_from_left_menu" = "Ջնջել ձախ մենյուից";
-"all_followers" = "Բոլոր բաժանորդները";
+"add_to_left_menu" = "Ավելացնել ձախ մենյույում";
+"remove_from_left_menu" = "Հեռացնել ձախ մենյուից";
+"all_followers" = "Բոլոր հետևորդները";
 "only_administrators" = "Միայն ադմինիստրատորները";
-"website" = "Վեբկայք";
+"website" = "Կայք";
 "managed" = "Կառավարվում է";
 "size" = "Չափ";
 
 "administrators_one" = "$1 ադմինիստրատոր";
-"administrators_few" = "$1 ադմինիստրատոր";
 "administrators_other" = "$1 ադմինիստրատոր";
 
 "role" = "Դեր";
@@ -301,30 +299,26 @@
 "promote_to_owner" = "Դարձնել տեր";
 "devote" = "Հետ բողոքարկել";
 "set_comment" = "Փոփոխել մեկնաբանությունը";
-"hidden_yes" = "Թաքցված է";
-"hidden_no" = "Թաքցված չէ";
+"hidden_yes" = "Թաքնված է";
+"hidden_no" = "Թաքնված չէ";
 "group_allow_post_for_everyone" = "Թույլատրել հրապարակել բոլորին";
 "group_hide_from_global_feed" = "Չցույց տալ հրապարակությունները ընդհանուր լրահոսում։";
-"statistics" = "Ստատիստիկա";
+"statistics" = "Վիճակագրություն";
 "group_administrators_list" = "Ադմինների ցուցակ";
-"group_display_only_creator" = "Ցույց տալ միայն խմբի ստեղծողին";
+"group_display_only_creator" = "Ցուցադրել միայն խմբի ստեղծողին";
 "group_display_all_administrators" = "Ցուցադրել բոլոր ադմինիստրատորներին";
 "group_dont_display_administrators_list" = "Ոչ մեկին ցույց չտալ";
 
-"group_changeowner_modal_title" = "Օգտատերի իրավասությունների փոխանցում";
-"group_changeowner_modal_text" = "<b>Ուշադրությու՛ն։</b> Դուք փոխանցում եք խմբի բոլոր իրավունքները $1-ին։ Այս գործողությունը անդառնալի է։ Դուք էլի կմնաք ադմինիստրատոր, բայց հեշտությամբ դա ձեզնից կարող են խլել։";
-"group_owner_setted" = "Նոր տերը ($1) նշանակված է $2 միությունում։ Ձեզ տրված են ադմինիստրատորի իրավասություններ։ Եթե ուզում եք հետ բերել իրավասությունները, <a href='/support?act=new'>գրե՛ք կայքի տեխնիկական աջակցությանը</a>։";
+"group_changeowner_modal_title" = "Սեփականատիրոջ իրավասությունների փոխանցում";
+"group_changeowner_modal_text" = "<b>Ուշադրությու՛ն։</b> Դուք փոխանցում եք խմբի բոլոր իրավունքները $1-ին։ Այս գործողությունը անդառնալի է։ Դուք էլի կմնաք ադմինիստրատոր, բայց հեշտությամբ այդ դերը ձեզնից կարող են խլել։";
+"group_owner_setted" = "Նոր տերը ($1) նշանակված է $2 հանրությունում։ Ձեզ տրված են ադմինիստրատորի իրավասություններ։ Եթե ուզում եք հետ բերել իրավասությունները, <a href='/support?act=new'>գրե՛ք կայքի տեխնիկական աջակցությանը</a>։";
 
 "participants_zero" = "Ոչ մի մասնակից";
 "participants_one" = "Միայն մեկ մասնակից";
-"participants_few" = "$1 մասնակից";
-"participants_many" = "$1 հատ մասնակից";
 "participants_other" = "$1 հատ մասնակից";
 
 "groups_zero" = "Ոչ մի խումբ";
 "groups_one" = "Մեկ խումբ";
-"groups_few" = "$1 խումբ";
-"groups_many" = "$1 խումբ";
 "groups_other" = "$1 խումբ";
 
 "groups_list_zero" = "Դուք չկաք գեթ ոչ մի խմբում";
@@ -333,12 +327,10 @@
 
 "meetings_zero" = "Ոչ մի հանդիպում";
 "meetings_one" = "Մեկ հանդիպում";
-"meetings_few" = "$1 հանդիպում";
-"meetings_many" = "$1 հանդիպում";
 "meetings_other" = "$1 հանդիպում";
 
 "open_new_group" = "Նոր խումբ բացել";
-"open_group_desc" = "Չե՞ք կարող խումբ գտնել, բացեք ձերը․․․";
+"open_group_desc" = "Չե՞ք կարողանում գտնել ճիշտ խումբը, բացե՛ք ձերը․․․";
 "search_group" = "Խմբի որոնում";
 "search_by_groups" = "Որոնում ըստ խմբերի";
 "search_group_desc" = "Այստեղ դուք կարող եք փնտրել խմբեր և ընտրել ձեզ ամենահարմարը․․․";
@@ -361,16 +353,36 @@
 
 "albums_zero" = "Ոչ մի ալբոմ չկա";
 "albums_one" = "Մեկ ալբոմ";
-"albums_few" = "$1 ալբոմ";
-"albums_many" = "$1 ալբոմ";
 "albums_other" = "$1 ալբոմ";
 
 "albums_list_zero" = "Դուք ոչ մի ալբոմ չունեք";
 "albums_list_one" = "Դուք ունեք մեկ ալբոմ";
-"albums_list_few" = "Դուք ունեք $1 ալբոմ";
-"albums_list_many" = "Դուք ունեք $1 ալբոմ";
 "albums_list_other" = "Դուք ունեք $1 ալբոմ";
 
+"add_image" = "Ավելացնել պատկեր";
+"add_image_group" = "Վերբեռնել պատկեր";
+"upload_new_picture" = "Ավելացնել նոր պատկեր";
+"uploading_new_image" = "Վերբեռնվում է նոր պատկերը․․․";
+"friends_avatar" = "Այն ավելի կհեշտացնի ձեր ընկերներին ճանաչել Ձեզ, եթե տեղադրեք ձեր իրական լուսանկարը։";
+"groups_avatar" = "Լավ պատկերը կարող է Ձեր խումբը ավելի ճանաչելի դարձնել։";
+"formats_avatar" = "Դուք կարող եք վերբեռնել պատկեր միայն JPG, GIF կամ PNG ֆորմատով։";
+"troubles_avatar" = "Եթե դժվարանում եք տեղադրելուց, փորձե՛ք ընտրել ավելի փոքր նկար։";
+"webcam_avatar" = "Եթե Ձեր համակարգիչը ունի վեբ–տեսախցիկ, Դուք կարող եք <a href='javascript:'>վերցնել նկար</a>։";
+
+"update_avatar_notification" = "Պրոֆիլի պատկերը փոփոխված է";
+"update_avatar_description" = "Սեղմեք դիտելու համար";
+
+"deleting_avatar" = "Պատկերը ջնջվում է";
+"deleting_avatar_sure" = "Դուք վստա՞հ եք որ ցանկանում եք ջնջել պատկերը։";
+
+"deleted_avatar_notification" = "Պատկերը հաջողությամբ ջնջվել է";
+
+"save_changes" = "Պահպանել փոփոխությունները";
+
+"upd_m" = "թարմացրել է իր պրոֆիլի պատկերը";
+"upd_f" = "թարմացրել է իր պրոֆիլի պատկերը";
+"upd_g" = "թարմացրել է իր խմբի պատկերը";
+
 /* Notes */
 
 "notes" = "Նշումներ";
@@ -380,26 +392,35 @@
 "create_note" = "Ստեղծել նշում";
 "edit_note" = "Խմբագրել նշումը";
 "actions" = "Գործողություններ";
+
+"edited" = "Խմբագրված է";
+
+"notes_zero" = "Ոչ մի նշում";
+"notes_one" = "Մեկ նշում";
+"notes_other" = "$1 նշում";
 "notes_start_screen" = "Նշումների շնորհիվ Դուք կարող եք կիսվել ընկերների հետ տարբեր իրադարձություններով, և իմանալ թե ինչ է կատարվում իրենց մոտ։";
 "note_preview" = "Նախադիտում";
 "note_preview_warn" = "Սա ընդամենը նախադիտում է";
 "note_preview_warn_details" = "Պահպանելուց հետո նշումները կարող են այլ տեսք ունենալ։ Ու մեկ էլ, այդքան հաճախ նախադիտում մի արեք։";
 "note_preview_empty_err" = "Ինչու՞ նախադիտել նշումը առանց վերնագրի կամ բովանդակության։";
 
-"edited" = "Խմբագրված է";
-
-"notes_zero" = "Ոչ մի նշում";
-"notes_one" = "Մեկ նշում";
-"notes_few" = "$1 նշում";
-"notes_many" = "$1 նշում";
-"notes_other" = "$1 նշում";
-
 "notes_list_zero" = "Ոչ մի նշում չի գտնվել";
 "notes_list_one" = "Գտնվեց մեկ նշում";
-"notes_list_few" = "Գտնվեց $1 նշում";
-"notes_list_many" = "Գտնվեց $1 նշում";
 "notes_list_other" = "Գտնվեց $1 նշում";
 
+"select_note" = "Ընտրվում է նշումը";
+"no_notes" = "Դուք չունե՛ք ոչ մի նշում";
+
+"error_attaching_note" = "Խնդիր առաջացավ նշումը ընտրելիս";
+
+"select_or_create_new" = "Ընտրե՛ք առկա նշումներից, կամ <a href='/notes/create'>ստեղծե՛ք նորը</a>";
+
+"notes_closed" = "Դուք չե՛ք կարող ամրացնել նշումը հրապարակությանը, քանզի միայն Դուք եք տեսնում նրանք։<br>  Դուք կարող եք փոխել դա <a href=\"/settings?act=privacy\">կարգավորումներում</a>։";
+"do_not_attach_note" = "Չամրացնել նշում";
+
+/* Notes: Article Viewer */
+"aw_legacy_ui" = "Հին ինտերֆեյս";
+
 /* Menus */
 
 "edit_button" = "խմբ.";
@@ -415,8 +436,10 @@
 "my_settings" = "Իմ կարգավորումները";
 "bug_tracker" = "Բագ–թրեքեր";
 
+"menu_settings" = "Կարգավորումներ";
 "menu_login" = "Մուտք";
 "menu_registration" = "Գրանցում";
+
 "menu_help" = "Օգնություն";
 
 "menu_logout" = "Դուրս գալ";
@@ -436,7 +459,7 @@
 "left_menu_donate" = "Աջակցել";
 
 "footer_about_instance" = "հոսքի մասին";
-"footer_rules" = "կանոնները";
+"footer_rules" = "կանոններ";
 "footer_blog" = "բլոգ";
 "footer_help" = "օգնություն";
 "footer_developers" = "մշակողներին";
@@ -452,9 +475,9 @@
 "interface" = "Արտաքին տեսք";
 "security" = "Անվտանգություն";
 
-"profile_picture" = "Էջի նկար";
+"profile_picture" = "Էջի պատկեր";
 
-"picture" = "Նկար";
+"picture" = "Պատկեր";
 
 "change_password" = "Փոխել գաղտնաբառը";
 "old_password" = "Հին գաղտնաբառը";
@@ -473,13 +496,17 @@
 "apply_style_for_this_device" = "Հաստատել տեսքը միայն այս սարքի համար";
 
 "search_for_groups" = "Խմբերի որոնում";
-"search_for_people" = "Մարդկանց որոնում";
+"search_for_users" = "Մարդկանց որոնում";
+"search_for_posts" = "Հրապարակումների որոնում";
+"search_for_comments" = "Մեկնաբանությունների որոնում";
+"search_for_videos" = "Վիդեոների որոնում";
+"search_for_apps" = "Հավելվածների որոնում";
+"search_for_notes" = "Նշումների որոնում";
+"search_for_audios" = "Երաժշտության որոնում";
 "search_button" = "Որոնել";
 "search_placeholder" = "Գրեք ցանկացած անուն, անվանում կամ բառ";
 "results_zero" = "Ոչ մի արդյունք";
 "results_one" = "Մեկ արդյունք";
-"results_few" = "$1 արդյունք";
-"results_many" = "$1 արդյունք";
 "results_other" = "$1 արդյունք";
 
 "privacy_setting_access_page" = "Ով կարող է տեսնել ինտերնետում իմ էջը";
@@ -504,9 +531,9 @@
 "your_email_address" = "Ձեր էլեկտրոնային հասցեն";
 "your_page_address" = "Ձեր էջի հասցեն";
 "page_address" = "Էջի հասցեն";
-"current_email_address" = "Ներկայիս հասցեն";
-"new_email_address" = "Նոր հասցեն";
-"save_email_address" = "Պահպանել հասցեն";
+"current_email_address" = "Ներկայիս էլեկտրոնային հասցեն";
+"new_email_address" = "Նոր էլեկտրոնային հասցեն";
+"save_email_address" = "Պահպանել էլեկտրոնային հասցեն";
 "page_id" = "Էջի ID–ն";
 "you_can_also" = "Դուք նաև կարող եք";
 "delete_your_page" = "ջնջել ձեր էջը";
@@ -518,13 +545,14 @@
 "ui_settings_rating_show" = "Ցուցադրել";
 "ui_settings_rating_hide" = "Թաքցնել";
 "ui_settings_nsfw_content" = "NSFW-կոնտենտ";
-"ui_settings_nsfw_content_dont_show" = "Ցույց չտա՛լ գլոբալ ժապավենում";
+"ui_settings_nsfw_content_dont_show" = "Ցույց չտա՛լ ընդհանուր ժապավենում";
 "ui_settings_nsfw_content_blur" = "Միայն ներծծել";
 "ui_settings_nsfw_content_show" = "Ցույց տալ";
-"ui_settings_view_of_posts" = "Փոսթերի տեսակ";
+"ui_settings_view_of_posts" = "Հրապարակումների տեսք";
 "ui_settings_view_of_posts_old" = "Հին";
 "ui_settings_view_of_posts_microblog" = "Միկրոբլոգ";
 "ui_settings_main_page" = "Գլխավոր էջ";
+"ui_settings_sessions" = "Այցելություններ";
 
 "additional_links" = "Հավելյալ հղումներ";
 "ad_poster" = "Գովազդային վահանակ";
@@ -547,7 +575,7 @@
 "profile_deactivate_reason_5_text" = "Ինձ այստեղ շան տեղ դնող չկա ու ես տխրում եմ։ Դուք կզղջաք որ ես հեռացա...";
 "profile_deactivate_reason_6" = "Այլ պատճառ";
 
-"profile_deactivated_msg" = "Ձեր էջը <b>ջնջված է</b>։<br/><br/>Եթե Դուք ուզենաք նորից օգտվել Ձեր էջով, կարող եք <a href='/settings/reactivate'>ապաակտիվացնել այն</a> մինչև $1:";
+"profile_deactivated_msg" = "Ձեր էջը <b>ջնջված է</b>։<br/><br/>Եթե Դուք ուզենաք նորից օգտվել կայքով, ապա կարող եք <a href='/settings/reactivate'>ապաակտիվացնել այն</a> մինչև $1:";
 "profile_deactivated_status" = "Էջը ջնջված է";
 "profile_deactivated_info" = "Օգտատիրոջ էջը հեռացվել է։<br/>Մանրամասն տեղեկատվությունը բացակայում է։";
 
@@ -557,6 +585,18 @@
 "end_all_sessions_description" = "Եթե ցանկանում եք դուրս գալ $1–ից ամեն դեվայսից, սեղմե՛ք ներքևի կոճակը";
 
 "end_all_sessions_done" = "Բոլոր սեսսիաները նետված են, ներառյալ բջջային հավելվածները";
+"backdrop_short" = "Ետնապատկեր";
+"backdrop" = "Էջի ետնապատկեր";
+"backdrop_desc" = "Դուք կարող եք տեղադրել երկու նկար, որպես Ձեր պրոֆիլի կամ խմբի նախանկար։ Նրանք կցուցադրվեն էջի ձախ և աջ ծայրերում։ Այս հարմարանքի շնորհիվ Դուք կարող եք ավելացնել հավելյալ անհատականություն Ձեր պրոֆիլին։";
+"backdrop_warn" = "Նկարները կկազմակերպվեն ըստ վերևի դասակարգման։ Իրենց բարձրությունը ավտոմատ կընդլայնվի, ու նրանք կզբաղեցնեն էկրանի բարձրության 100%-ը, նաև կավելացվի մեջտեղում լղոզում։ Այն անհնար կլինի փոխարինել փոխարինել ետնապատկերը OpenVK-ի հիմնական ինտերֆեյսով կամ ավելացնել աուդիո։";
+"backdrop_about_adding" = "Դուք կարող եք ավելացնել միայն մեկ նկար, կախված դիզայնից, վերջնական արդյունքը կարող է տգեղ տեսք ունենալ։ Դուք նաև կարող եք փոխել միայն մեկ նկար. եթե արդեն ունեք երկու տեղադրված նկար և ուզում եք փոխել մեկը – վերբեռնեք միայն մեկ անգամ, ուսի մյուսները չեն ջնջվի։ Որպեսզի ջնջեք երկու նկարները, սեղմե՛ք ներքևի կոճակը, դուք չե՛ք կարող ջնջել նկարները առանձին։";
+"backdrop_save" = "Պահպանել ետնապակներները";
+"backdrop_remove" = "Ջնջել բոլոր ետնապակներները";
+"backdrop_error_title" = "Խնդիր առաջացավ ՝ ետնապակներները պահպանելիս";
+"backdrop_error_no_media" = "Նկարները վնասված են կամ լիարժեք չեն տեղադրվել";
+"backdrop_succ" = "Ետնապատկերի կարգավորումները պահպանված են";
+"backdrop_succ_rem" = "Ետնապատկերները ջնջվեցին";
+"backdrop_succ_desc" = "Օգտատերերը կտեսնեն փոփոխությունները 5 րոպեյվա ընթացքում";
 "browse" = "Վերանայում";
 
 /* Two-factor authentication */
@@ -612,11 +652,9 @@
 
 "videos_zero" = "Ոչ մի տեսանյութ չկա";
 "videos_one" = "Մեկ տեսանյութ";
-"videos_few" = "$1 տեսանյութ";
-"videos_many" = "$1 տեսանյութ";
 "videos_other" = "$1 տեսանյութ";
 
-"view_video" = "Դիտում";
+"view_video" = "Դիտել";
 
 /* Notifications */
 
@@ -649,26 +687,34 @@
 "nt_photo_instrumental" = "նկարով";
 "nt_topic_instrumental" = "թեմայով";
 
+"nt_you_were_mentioned_u" = "Ձեզ նշել է օգտատերը";
+"nt_you_were_mentioned_g" = "Ձեզ նշել է խումբը";
+"nt_mention_in_post_or_comms" = "իր քննարկման թեմաներից մեկում";
+"nt_mention_in_photo" = "այս նկարի քննարկմանը";
+"nt_mention_in_video" = "այս վիդեոյի քննարկմանը";
+"nt_mention_in_note" = "այս նշման քննարկմանը";
+"nt_mention_in_topic" = "այս քննարկմանը";
+
 /* Time */
 
-"time_at_sp" = " -ում ";
+"time_at_sp" = " ՝ ";
 "time_just_now" = "հենց նոր";
 "time_exactly_five_minutes_ago" = "ուղիղ հինգ րոպե առաջ";
 "time_minutes_ago" = "$1 րոպե առաջ";
 "time_today" = "այսօր";
 "time_yesterday" = "երեկ";
 
-"points" = "Ձայն";
+"points" = "Ձայներ";
 "points_count" = "ձայն";
 "on_your_account" = "ձեր հաշվում";
-"top_up_your_account" = "Լիցքավորել բալանսը";
+"top_up_your_account" = "Ստանալ ավելին";
 "you_still_have_x_points" = "Դուք ունեք <b>$1</b> չօգտագործված ձայն։";
 
 "vouchers" = "Վաուչերներ";
 "have_voucher" = "Կա վաուչեր";
 "voucher_token" = "Վաուչերի կոդ";
 "voucher_activators" = "Օգտագործվածները";
-"voucher_explanation" = "Գրե՛ք վաուչերի սերիական համարը։ Սովորաբար այն նշված է լինում կտրոնի կամ նամակի մեջ։";
+"voucher_explanation" = "Ներմուծե՛ք վաուչերի սերիական համարը։ Սովորաբար այն նշված է լինում կտրոնի կամ նամակի մեջ։";
 "voucher_explanation_ex" = "Ուշադրություն դարձրե՛ք, որ վաուչերները կարող են սպառվել և օգտագործվել միայն մեկ անգամ։";
 "invalid_voucher" = "Անվավեր վաուչեր";
 "voucher_bad" = "Հնարավոր է, դուք ներմուծել եք սխալ վաուչեր, արդեն օգտագործել եք այն կամ էլ այն սպառվել է։";
@@ -677,14 +723,12 @@
 "redeem" = "Ակտիվացնել վաուչերը";
 "deactivate" = "Դեակտիվացնել";
 "usages_total" = "Օգտագործումների քանակ";
-"usages_left" = "Մնացին օգտագործումներ";
+"usages_left" = "Մնաց օգտագործելու";
 
 "points_transfer_dialog_header_1" = "Դուք կարող եք ուղարկել ձայները և նվերների մի մասը այլ մարդուն։";
-"points_transfer_dialog_header_2" = "Ձեր ներկայիս բալանսը․ ";
+"points_transfer_dialog_header_2" = "Ձեր ներկայիս բալանսը.";
 
 "points_amount_one" = "Մեկ ձայն";
-"points_amount_few" = "$1 ձայն";
-"points_amount_many" = "$1 ձայն";
 "points_amount_other" = "$1 ձայն";
 
 "transfer_poins" = "Ձայների փոխանցում";
@@ -732,13 +776,9 @@
 "gifts" = "Նվերներ";
 "gifts_zero" = "Նվերներ չկան";
 "gifts_one" = "Մեկ նվեր";
-"gifts_few" = "$1 նվեր";
-"gifts_many" = "$1 նվեր";
 "gifts_other" = "$1 նվեր";
 "gifts_left" = "Մնաց $1 նվեր";
 "gifts_left_one" = "Մնաց մեկ նվեր";
-"gifts_left_few" = "$1 նվեր մնաց";
-"gifts_left_many" = "$1 նվեր մնաց";
 "gifts_left_other" = "$1 նվեր մնաց";
 
 "send_gift" = "Ուղարկել նվեր";
@@ -753,8 +793,6 @@
 "coins" = "Ձայներ";
 "coins_zero" = "0 ձայն";
 "coins_one" = "Մեկ ձայն";
-"coins_few" = "$1 ձայն";
-"coins_many" = "$1 ձայն";
 "coins_other" = "$1 ձայն";
 
 "users_gifts" = "Նվերներ";
@@ -838,7 +876,7 @@
 "support_status_0" = "Հարցը դիտարկման տակ է";
 "support_status_1" = "Կա պատասխան";
 "support_status_2" = "Փակ է";
-"support_greeting_hi" = "Բարև ձեզ, $1!";
+"support_greeting_hi" = "Բարև՜ Ձեզ, $1";
 "support_greeting_regards" = "Հարգանքով, <br/>$1 -ի աջակցման թիմ։";
 
 "support_faq" = "Հաճախ տրվող հարցեր";
@@ -860,6 +898,12 @@
 
 "fast_answers" = "Արագ պատասխաններ";
 
+"ignore_report" = "Արհամարել զեկույցը";
+"report_number" = "Զեկույց #";
+"list_of_reports" = "Զեկույցների ցանկ";
+"text_of_the_post" = "Հրապարակման տեքստ";
+"today" = "այսօր";
+
 "comment" = "Մեկնաբանություն";
 "sender" = "Ուղարկող";
 
@@ -874,6 +918,13 @@
 "banned_in_support_1" = "Կներե՛ք, <b>$1</b>, բայց հիմա Ձեզ թույլատրված չէ դիմումներ ստեղծել։";
 "banned_in_support_2" = "Դրա պատճառաբանությունը սա է․ <b>$1</b>։ Ցավո՛ք, այդ հնարավորությունը մենք Ձեզնից վերցրել ենք առհավետ։";
 
+"you_can_close_this_ticket_1" = "Եթե չունեք այլատիպ հարցեր, ապա կարող եք ";
+"you_can_close_this_ticket_2" = "փակել այս դիմումը";
+"agent_profile_created_1" = "Պրոֆիլը ստեղծված է";
+"agent_profile_created_2" = "Հիմա օգտատերերը կարող են տեսնել Ձեր անհատականեցված անունը և ավատարը ՝ սովորականների փոխարեն։";
+"agent_profile_edited" = "Պրոֆիլը ստեղծված է";
+"agent_profile" = "Իմ Գործակալի քարտը";
+
 /* Invite */
 
 "invite" = "Հրավիրել";
@@ -914,19 +965,47 @@
 "messages_error_1" = "Նամակը չի ուղարկվել";
 "messages_error_1_description" = "Այս նամակը ուղարկելու ժամանակ տեղի է ունեցել ընդհանրացված սխալ...";
 
+/* Polls */
+"poll" = "Քվեարկություն";
+"create_poll" = "Ստեղծել քվեարկություն";
+"poll_title" = "Հարց տալ";
+"poll_add_option" = "Ավելացնել ընտրություն...";
+"poll_anonymous" = "Գաղտնի քվեարկումներ";
+"poll_multiple" = "Տարբեր պատասխաններ";
+"poll_locked" = "Վիկտորինայի ռեժիմ (առանց վերանայման)";
+"poll_edit_expires" = "Սպառվում է. ";
+"poll_edit_expires_days" = "օր";
+"poll_editor_tips" = "Backspace խփելը դատարկ ընտրությունը ջնջելու է։ Օգտագործե՛ք Tab/Enter դատարկ ընտրությանը, որպեսզի այն ավելի արագ ստեղծեք։";
+"poll_embed" = "Ներկառուցված կոդ";
+
+"poll_voter_count_zero" = "Եղեք <b>առաջի՛ն</b> քվեարկողը";
+"poll_voter_count_one" = "<b>Միայն մեկ</b> օգտատեր է քվեարկել";
+"poll_voter_count_few" = "Քվեարկեց <b>$1</b> հոգի";
+"poll_voter_count_many" = "Քվեարկեց <b>$1</b> հոգի";
+"poll_voter_count_other" = "Քվեարկեց <b>$1</b> հոգի";
+
+"poll_voters_list" = "Քվեարկողներ";
+
+"poll_anon" = "Գաղտնի";
+"poll_public" = "Հանրային";
+"poll_multi" = "տարբեր ընտրություններ";
+"poll_lock" = "առանց հետ կանչելու";
+"poll_until" = "մինչև $1";
+
+"poll_err_to_much_options" = "Չափից շատ տարբերակ է տրված։";
+"poll_err_anonymous" = "Չի լինում տեսնել քվեարկողների ցանկը. քվեարկությունը գաղտնի է";
+"cast_vote" = "Քվեարկե՛լ";
+"retract_vote" = "Չեղարկել իմ քվեարկումը";
+
 /* Discussions */
 
 "discussions" = "Քննարկումներ";
 
 "messages_one" = "Մեկ նամակ";
-"messages_few" = "$1 նամակ";
-"messages_many" = "$1 նամակ";
 "messages_other" = "$1 նամակ";
 
 "topic_messages_count_zero" = "Թեմայում նամակ չկա";
 "topic_messages_count_one" = "Թեմայում մեկ նամակ է";
-"topic_messages_count_few" = "Թեմայում $1 նամակ կա";
-"topic_messages_count_many" = "Թեմայում $1 նամակ կա";
 "topic_messages_count_other" = "Թեմայում $1 նամակ կա";
 
 "replied" = "պատասխանել է";
@@ -945,11 +1024,9 @@
 "delete_topic" = "Ջնջել թեման";
 
 "topics_one" = "Մեկ թեմա";
-"topics_few" = "$1 թեմա";
-"topics_many" = "$1 թեմա";
 "topics_other" = "$1 թեմա";
 
-"created" = "Ստեղծված է";
+"created" = "Ստեղծվել է";
 
 "everyone_can_create_topics" = "Բոլորը կարող են թեմաներ սարքել";
 "display_list_of_topics_above_wall" = "Ցուցադրել պատի տակ թեմաների ցուցակը";
@@ -978,8 +1055,10 @@
 "error_upload_failed" = "Չհաջողվեց վերբեռնել նկարը";
 "error_old_password" = "Հին գաղտնաբառը չի համընկնում";
 "error_new_password" = "Նոր գաղտնաբառերը չեն համընկնում";
+"error_weak_password" = "Գաղտնաբառը այդքան էլ խիստ չէ։ Այն առնվազն պետք է պարունակի 8 նիշ, մեկ մեծատառ տառ և մեկ թիվ։";
 "error_shorturl_incorrect" = "Կարճ հասցեն ունի սխալ ֆորմատ";
-"error_repost_fail" = "Չհաջողվեց կիսվել գրության հետ";
+"error_repost_fail" = "Չհաջողվեց կիսվել գրությունով";
+"error_data_too_big" = "'$1' ատտրիբուտը պետք է առնվազն լինի $2 –ը $3 –ի չափ երկար";
 
 "forbidden" = "Հասանելիության սխալ";
 "forbidden_comment" = "Այս օգտատիրոջ գաղտնիության կարգավորումները ձեզ թույլ չեն տալիս դիտել օգտատերի էջը։";
@@ -1015,7 +1094,7 @@
 "suspicious_registration_attempt_comment" = "Դուք մի տեսակ փորձել եք սխալ տեղից գրանցվել։";
 
 "rate_limit_error" = "Հե՛յ, կարող ա՞ խառնել ես։";
-"rate_limit_error_comment" = "Ա՛յ $1, չի՛ կարելի այսքան հաճախ սպամ հրապարակել։ Հո դու Կառլենը չե՞ս։ Բացառության կոդ․ $2։";
+"rate_limit_error_comment" = "Ա՛յ $1 ջան, չի՛ կարելի այսքան հաճախ սպամել։ Հո դու Գրիգորիսը չե՞ս։ Բացառության կոդ․ $2։";
 
 "not_enough_permissions" = "Այդքան իրավասություն չկա";
 "not_enough_permissions_comment" = "Դուք բավական իրավասություն չունեք այս գործողությունը կատարելու համար։";
@@ -1047,7 +1126,7 @@
 
 /* Admin panel */
 
-"admin" = "Ադմին-վահանակ";
+"admin" = "Ադմինի վահանակ";
 
 "admin_ownerid" = "Օգտատիրոջ ID";
 "admin_author" = "Հեղինակ";
@@ -1124,7 +1203,7 @@
 "admin_banned_links" = "Արգելափակված հղումներ";
 "admin_banned_link" = "Հղում";
 "admin_banned_domain" = "Դոմեն";
-"admin_banned_link_description" = "Պրոտոկոլով (https://example.com/)";
+"admin_banned_link_description" = "Պրոտոկոլով (https://example.am/)";
 "admin_banned_link_regexp" = "Ռեգուլյար արտահայտություն";
 "admin_banned_link_regexp_description" = "Տեղադրվում է վերոնշյալ դոմենից հետո։ Մի լրացրե՛ք, եթե ցանկանում եք արգելափակել ամբողջ դոմենը";
 "admin_banned_link_reason" = "Պատճառ";
@@ -1132,6 +1211,15 @@
 "admin_banned_link_not_specified" = "Հղումը նշված չէ";
 "admin_banned_link_not_found" = "Հղումը չի գտնվել";
 
+"logs_adding" = "Ստեղծում";
+"logs_editing" = "Խմբագրում";
+"logs_removing" = "Ջնջում";
+"logs_restoring" = "Վերականգնում";
+"logs_added" = "ստեղծվել է";
+"logs_edited" = "խմբագրվել է";
+"logs_removed" = "ջնջվել է";
+"logs_restored" = "վերականգնվել է";
+
 /* Paginator (deprecated) */
 
 "paginator_back" = "Հետ";
@@ -1150,29 +1238,20 @@
 "instance_links" = "Հոսքերի հղումներ․";
 
 "about_users_one" = "<b>Մեկ</b> օգտատեր";
-"about_users_few" = "<b>$1</b> օգտատեր";
-"about_users_many" = "<b>$1</b> օգտատեր";
 "about_users_other" = "<b>$1</b> օգտատեր";
 
 "about_online_users_one" = "<b>Մեկ</b> օգտատեր է ցանցի մեջ";
-"about_online_users_few" = "<b>$1</b> օգտատեր է ցանցի մեջ";
-"about_online_users_many" = "<b>$1</b> օգտատեր է ցանցի մեջ";
 "about_online_users_other" = "<b>$1</b> օգտատեր է ցանցի մեջ";
 
 "about_active_users_one" = "<b>Մեկ</b> ակտիվ օգտատեր";
-"about_active_users_few" = "<b>$1</b> ակտիվ օգտատեր";
-"about_active_users_many" = "<b>$1</b> ակտիվ օգտատեր";
 "about_active_users_other" = "<b>$1</b> ակտիվ օգտատեր";
 
 "about_groups_one" = "<b>Մեկ</b> խումբ";
-"about_groups_few" = "<b>$1</b> խումբ";
-"about_groups_many" = "<b>$1</b> խումբ";
 "about_groups_other" = "<b>$1</b> խումբ";
 
 "about_wall_posts_one" = "<b>Մեկ</b> գրություն պատերի վրա";
-"about_wall_posts_few" = "<b>$1</b> գրություն պատերի վրա";
-"about_wall_posts_many" = "<b>$1</b> գրություն պատերի վրա";
 "about_wall_posts_other" = "<b>$1</b> գրություն պատերի վրա";
+
 "about_watch_rules" = "տես <a href='$1'>այստեղ</a>․";
 
 /* Dialogs */
@@ -1191,10 +1270,11 @@
 /* User alerts */
 
 "user_alert_scam" = "Այս հաշվի վրա բազմաթիվ բողոքներ են եկել խարդախության հետ կապված։ Խնդրվում է զգույշ լինել, հատկապես եթե Ձեզնից փորձեն գումար խնդրել և շորթել։";
+"user_may_not_reply" = "Այս օգտատերը կարող է Ձեզ չպատասխանել, ձեր անվտանգության կարգավորումների պատճառով։  <a href='/settings?act=privacy'>Բացել անվտանգության կարգավորումները</a>";
 
 /* Cookies pop-up */
 
-"cookies_popup_content" = "Cookie բառը անգլերենից նշանակում է թխվածքաբլիթ, իսկ թխվածքաբլիթը համեղ է։ Մեր կայքը չի ուտում թխվածք, բայց օգտագործում է այն ուղղակի սեսսիան կողմնորոշելու համար։ Ավելի մանրամասն կարող եք ծանոթանալ մեր <a href='/privacy'>գաղտնիության քաղաքականությանը</a> հավելյալ ինֆորմացիայի համար։";
+"cookies_popup_content" = "Cookie բառը թարգմանաբար նշանակում է թխվածքաբլիթ, իսկ թխվածքաբլիթը լավ բան է։ Մեր կայքը չի ուտում թխվածք, բայց օգտագործում է այն ՝ այցելությունը կողմնորոշելու համար։ Ավելի մանրամասն կարող եք ծանոթանալ մեր <a href='/privacy'>գաղտնիության քաղաքականությանը</a> հավելյալ ինֆորմացիայի համար։";
 "cookies_popup_agree" = "Համաձայն եմ";
 
 /* Away */
@@ -1206,6 +1286,42 @@
 "url_is_banned_title" = "Հղում դեպի կասկածելի կայք";
 "url_is_banned_proceed" = "Անցնել հղումով";
 
+"recently" = "Վերջերս";
+
+/* Helpdesk */
+
+"helpdesk" = "Աջակցում";
+"helpdesk_agent" = "Աջակցման գործակալ";
+"helpdesk_agent_card" = "Գործակալի քարտ";
+"helpdesk_positive_answers" = "դրական պատասխաններ";
+"helpdesk_negative_answers" = "բացասական պատասխաններ";
+"helpdesk_all_answers" = "բոլոր պատասխանները";
+"helpdesk_showing_name" = "Ցուցադրվող անունը";
+"helpdesk_show_number" = "Ցույց տալ թիվը";
+"helpdesk_avatar_url" = "Ավատարի հղումը";
+
+/* Chandler */
+
+"c_user_removed_from_group" = "Այս օգտատերը հեռացվել է խմբից";
+"c_permission_removed_from_group" = "Թույլտվությունը հեռացվել է խմբից";
+"c_group_removed" = "Խումբը ջնջվել է";
+"c_groups" = "Chandler–ի Խմբեր";
+"c_users" = "Chandler–ի Օգտատերեր";
+"c_group_permissions" = "Իրավասություններ";
+"c_group_members" = "Մասնակիցներ";
+"c_model" = "Մոդել";
+"c_permission" = "Իրավասություն";
+"c_permissions" = "Իրավասություններ";
+"c_color" = "Գույն";
+"add" = "Ավելացնել";
+"c_edit_groups" = "Խմբագրել Խմբերը";
+"c_user_is_not_in_group" = "Օգտատիրոջ և խմբի հանդեպ հարաբերությունները չգտնվեցին։";
+"c_permission_not_found" = "Իրավասության և խմբի հանդեպ հարաբերությունները չգտնվեցին։";
+"c_group_not_found" = "Խումբը չգտնվե՛ց։";
+"c_user_is_already_in_group" = "Այս օգտատերը արդեն խմբի անդամ է։";
+"c_add_to_group" = "Ավելացնել խմբին";
+"c_remove_from_group" = "Հեռացնել խմբից";
+
 /* Maintenance */
 
 "global_maintenance" = "Տեխնիկական աշխատանքներ";
@@ -1214,3 +1330,232 @@
 "undergoing_section_maintenance" = "Ցավոք սրտի, <b>$1</b> բաժինը ժամանակավորապես անհասանելի է։ Մենք արդեն աշխատում ենք խնդիրները շտկելու ուղղությամբ։ Խնդրում ենք այցելել մի քիչ ուշ։";
 
 "topics" = "Թեմաներ";
+
+
+/* Tutorial */
+
+"tour_title" = "Կայքի ճամփորդություն";
+"reg_title" = "Գրանցում";
+"ifnotlike_title" = " &quot;Ի՞նչ եթե ինձ այս կայքը դուր չի գալիս։&quot; ";
+"tour_promo" = "Ինչ է Ձեզ սպասվում գրանցումից հետո";
+
+"reg_text" = "<a href='/reg'>Օգտատիրոջ գրանցումը</a> լրիվ անվճար է և տևում է երկու րոպեյից ոչ ավել։";
+"ifnotlike_text" = "Դուք միշտ կարող եք ջնջել Ձեր հաշիվը";
+
+
+"tour_next" = "Հաջորդը →";
+"tour_reg" = "Գրանցում →";
+
+
+"tour_section_1" = "Սկիզբ";
+"tour_section_2" = "Պրոֆիլ";
+"tour_section_3" = "Նկարներ";
+"tour_section_4" = "Որոնում";
+"tour_section_5" = "Վիդեոներ";
+"tour_section_6" = "Աուդիոներ";
+"tour_section_7" = "Հիմնական լուրերի ժապավեն";
+"tour_section_8" = "Ընդհանուր լուրերի ժապավեն";
+"tour_section_9" = "Խմբեր";
+"tour_section_10" = "Իրադարձություններ";
+"tour_section_11" = "Թեմաներ";
+"tour_section_12" = "Անհատականացում";
+"tour_section_13" = "Պրոմոկոդեր";
+"tour_section_14" = "Հեռախոսի տարբերակ";
+
+
+"tour_section_1_title_1" = "Որտեղի՞ց սկսել";
+"tour_section_1_text_1" = "Օգտատիրոջ գրանցումը ամենաառաջին քայլն է այստեղ Ձեր ճանապարհը սկսելու համար։";
+"tour_section_1_text_2" = "Գրանցվելու համար պետք է ունենալ էլ. հասցե և գաղտնաբառ։";
+"tour_section_1_text_3" = "<b>Հիշե՛ք.</b> Ձեր էլ. հասցեն կօգտագործվի կայք մուտք գործելու համար։ Դուք նաև կունենաք լիիրավ իրավունք չնշել Ձեր ազգանունը գրանցվելիս։ Եթե հանկարծ կորցնեք մուտք գործելու գաղտնաբառը, միշտ կարող եք օգտվել <a href='/restore'>վերականգնման էջից</a>։";
+"tour_section_1_bottom_text_1" = "Գրանցվելով այս կայքում, Դուք համաձայնվում եք <a href='/terms'>կայքի կանոններին</a> և <a href='/privacy'>գաղտնիության քաղաքականությանը</a>։";
+
+"tour_section_2_title_1" = "Ձեր պրոֆիլը";
+"tour_section_2_text_1_1" = "Գրանցվելուց հետո Դուք ավտոմատ կերպով կվերահղվեք դեպի <b>ձեր</b> էջը:";
+"tour_section_2_text_1_2" = "Դուք կարող եք խմբագրել այն որտեղ և երբ ցանկանաք։";
+"tour_section_2_text_1_3" = "<b>Ակնարկ.</b> Որպեսզի Ձեր պրոֆիլը թույն ու ներկայանալի տեսք ունենա, կարող եք այն լրացնել տեղեկությամբ, կամ էլ տեղադրել լուսանկար, որը օրինակի համար ցույց է տալիս Ձեր վերաբերմունքը Կարգին Հաղորդմանը։";
+"tour_section_2_bottom_text_1" = "Դուք եք միայն որոշում ինչքան տեղեկություն կարող են իմանալ Ձեր մասին ընկերները։";
+"tour_section_2_title_2" = "Տեղադրել սեփական անվտանգության կարգավորումները։";
+"tour_section_2_text_2_1" = "Դուք կարող եք սահմանել թե ինչ տեսակի տեղեկություն կարող է երևալ Ձեր էջում։";
+"tour_section_2_text_2_2" = "Դուք իրավունք ունեք բլոկավորել հսանելիությունը Ձեր էջին որոնողական համակարգերից ու չգրանցված օգտատերերից։";
+"tour_section_2_text_2_3" = "<b>Հիշե՛ք.</b> հետագայում անվտանգության կարգավորումները կընդլայնվեն։";
+"tour_section_2_title_3" = "Պրոֆիլի URL";
+"tour_section_2_text_3_1" = "էջը գրանցելուն պես Դուք ստանում եք անձնական ID, ասենք ՝ <b>@id12345</b>";
+"tour_section_2_text_3_2" = "<b>Սովորական ID-ն</b>, որը ստացվում է գրանցումից հետո, <b>մնում է անփոփոխ</b>";
+"tour_section_2_text_3_3" = "Բայց Ձեր էջի կարգավորումներում Դուք կարող եք տեղադրել անձնական հասցեն, և այն <b>կարող է փոխվել</b> երբ կամենաք";
+"tour_section_2_text_3_4" = "<b>Ակնարկ.</b> Դուք կարող եք վերցնել ցանկացած հասցե, որը առնվազն 5 նիշանի է։ Փորձե՛ք վերցնել թույն URL :Ճ";
+"tour_section_2_bottom_text_2" = "<i>Ցանկացած կարճ հասցե լատինատառ փոքրատառ տառերով սպասարկվում է; այն կարող է պարունակել թվեր (ոչ սկզբում), կետեր և ընդգծումնր (ոչ սկզբում կամ վերջում)</i>";
+"tour_section_2_title_4" = "Պատ";
+
+
+"tour_section_3_title_1" = "Կիսվե՛ք Ձեր կյանքի պահերով";
+"tour_section_3_text_1" = "&quot;Լուսանկարների&quot; բաժինը հասանելի է Ձեզ անմիջապես գրանցումից հետո";
+"tour_section_3_text_2" = "Դուք կարող եք դիտել օգտատիրոջ ալբոմները կամ էլ ստեղծել ձերը";
+"tour_section_3_text_3" = "Հասանելիություն տալ բոլոր ալբոմներին ուրիշներին, ինչը կառավարվում է Ձեր էջի անվտանգության կարգավորումներով";
+"tour_section_3_bottom_text_1" = "Դուք կարող եք ստեղծել անսահմանափակ քանակությամբ ալբոմներ, ճամփորդության կամ հանգստի համար, կամ էլ զուտ մեմերի համար";
+
+
+"tour_section_4_title_1" = "Որոնում";
+"tour_section_4_text_1" = "&quot;Որոնման&quot; բաժինը թույլ է տալիս փնտրել մարդկանց և խմբերը։";
+"tour_section_4_text_2" = "Կայքի հենց այս բաժինը ժամանակի ընթացքում ընդլայնվում է";
+"tour_section_4_text_3" = "Որպեսզի սկսեք որոնելը, Դուք պետք է իմանաք օգտատիրոջ անունը (կամ ազգանունը), և եթե Ձեզ հետաքրքիր ա խումբ ճարելը, ապա պետք է ճշտել իր անունը։";
+"tour_section_4_title_2" = "Արագ որոնում";
+"tour_section_4_text_4" = "Եթե ուզում եք խնայել ժամանակ, որոնման բարը միշտ հասանելի է կայքի վերնամասում";
+
+
+"tour_section_5_title_1" = "Վերբեռնե՛ք և կիսվե՛ք վիդեոներով ընկերների հետ";
+"tour_section_5_text_1" = "Դուք կարող եք տեղադրել անսահմանափակ վիդեոներ և կարճ հոլովակներ";
+"tour_section_5_text_2" = "&quot;Վիդեոների&quot; բաժինը ղեկավարվում է անվտանգության կարգավորումներով";
+"tour_section_5_bottom_text_1" = "Վիդեոները կարող են տեղադրվել անցնելով &quot;Վիդեոների&quot; բաժինը, ուղղակի կցելով դրանք պատին.";
+"tour_section_5_title_2" = "YouTube-ից վիդեոների ներկրում";
+"tour_section_5_text_3" = "Ուղիղ տեղադրումից բացի, նաև կարող եք ամրացնել Ձեր սիրելի YouTube–յան վիդեոների հղումները";
+
+
+"tour_section_6_title_1" = "Աուդիոների բաժինը, որը հլը չկա բհահահսհդ xDDD հորս արևևև";
+"tour_section_6_text_1" = "Ինչպես ասվում էր Կարգին Հաղորդումում. «ապե մի քիչ էլ պահի ստե բան չի երևում»։ Վատ չէր լինի պատմել այս բաժնի մասին, բայց Վրիսկա ախպերը բեսամթ ալարել ա էս սարքել (նենց լավ ա էլի :Ճ):";
+
+
+"tour_section_7_title_1" = "Հետևե՛ք թե ինչ են գրում Ձեր ընկերներ";
+"tour_section_7_text_1" = "&quot;Լուրերի&quot; բաժինը բաժանվում է երկու տիպի. հիմնական և ընդհանուր ժապավենների";
+"tour_section_7_text_2" = "Հիմնական ժապավենը ցուցադրում է Ձեր ընկերների ու խմբերի նորությունները";
+"tour_section_7_bottom_text_1" = "Մենք չենք սարքում Ձեր լուրերի ժապավենը։ <b>Դուք եք ստեղծում այն</b>։";
+
+
+"tour_section_8_title_1" = "Հետևե՛ք այստեղ քննարկվող թեմաներին";
+"tour_section_8_text_1" = "Ընդհանուր ժապավենը ցուցադրում է բոլոր օգտատերերի և խմբերի թեմաները";
+"tour_section_8_text_2" = "Այս բաժինը խորհուրդ չի տրվում դիտել նյարդայիններին, հղիներին ու Վարդան Ղուկասյանին լսողներին";
+"tour_section_8_bottom_text_1" = "Ընդհանուր ժապավենի տեսքը չի տարբերվում հիմնականից";
+"tour_section_8_bottom_text_2" = "Ժապավենը ունի տարատեսակ կոնտենտ, սովորական նկարներից և վիդեոներից մինչև գաղտնի գրառումներ և քվեարկություններ";
+
+
+"tour_section_9_title_1" = "Ստեղծե՛ք խմբե՛ր";
+"tour_section_9_text_1" = "Կայքը արդեն վաղուց ունի հազարավոր խմբեր նվիրված տարբեր թեմաներին և երկրպագումներին";
+"tour_section_9_text_2" = "Դուք կարող եք միանալ ցանկացած խմբին, եթե չեք գտնում Ձեր ուզածը, կարող եք ստեղծել այն";
+"tour_section_9_text_3" = "Ամեն խումբ ունի իր վիքի էջերը, ալբոմները, հղումները և քննարկումները";
+"tour_section_9_title_2" = "Կառավարեք խումբը ընկերների հետ";
+"tour_section_9_text_2_1" = "Կառավարեք խումբը &quot;Խմբագրել Խումբը&quot; բաժնում հանրության ավատարի ներքո";
+"tour_section_9_text_2_2" = "Ստեղծեք Ձեր ադմինիստրատորների ու մոդերատորների թիմը, ում Դուք վստահում եք";
+"tour_section_9_text_2_3" = "Դուք կարող եք թաքցնել ադմինիստրատորին, և նա չի երևա Ձեր խմբի ոչ մի անկյունում";
+"tour_section_9_bottom_text_1" = "&quot;Իմ Խմբերը&quot; բաժինը կայքի ձախ մենյույում է գտնվում";
+"tour_section_9_bottom_text_2" = "Խմբի օրինակ";
+"tour_section_9_bottom_text_3" = "Խմբերը իրական կազմակերպույուններ են, որոնց մասնակիցները ցանկանում են մնալ կապի հետ իրենց լսարանի հետ";
+
+
+"tour_section_10_title_1" = "Վա՛յ";
+"tour_section_10_text_1" = "Այս բաժնում էլ լավ կլիներ սարքել ծանոթություն, սակայն այն դեռ սարքվում է։ Եկե՛ք սիրուն ձևերով շրջանցեք այն և առաջ գնանք...";
+
+
+"tour_section_11_title_1" = "Տեսքեր";
+"tour_section_11_text_1" = "Գրանցվելուց հետո Ձեր էջում կիրառվում է սովորական տեսքը";
+"tour_section_11_text_2" = "Որոշ նորեկները կարող է չսիրեն լռելյայն տեսքը, քանի որ այն անգամ հնության զգացում է տալիս";
+"tour_section_11_text_3" = "<b>Բայց հլը հո՛պ.</b> Դուք կարող եք անգամ ստեղծել Ձեր տեսքը ՝ կարդալով <a href='https://docs.openvk.uk/'>դոկումենտացիան</a>, կամ էլ ընտրել եղածներից մեկը";
+"tour_section_11_bottom_text_1" = "Տեսքերի ցանկը հասանելի է &quot;Իմ Կարգավորումներ&quot; –ի &quot;Ինտերֆեյս&quot; բաժնում;";
+"tour_section_11_wordart" = "<img src='https://openvk.uk/assets/packages/static/openvk/img/tour/wordart_en.png' width='65%'>";
+
+"tour_section_12_title_1" = "Պրոֆիլ և խմբի ետնապատկերներ";
+"tour_section_12_text_1" = "Դուք կարող եք երկու ետնապատկեր տեղադրել";
+"tour_section_12_text_2" = "Նրանք կցուցադրվեն Ձեր էջի ծայրամասերում";
+"tour_section_12_text_3" = "<b>Ակնարկ.</b> նախքան ետնապատկեր տեղադրելը, փորձե՛ք էքսպերիմենտներ անել իր հետ. փոխել գույնը, հայելու էֆֆեկտ դնել կամ մի բան անել";
+"tour_section_12_title_2" = "Ավատարներ";
+"tour_section_12_text_2_1" = "Դուք կարող եք դնել տարբեր կարգավորումներ ավատարը տեսնելու համար. սովորական, շրջանաձև կամ քառակուսի (1:1)";
+"tour_section_12_text_2_2" = "Այս կարգավորումները տեսանելի են միայն Ձեզ";
+"tour_section_12_title_3" = "Ձախ մենյույի փոփոխումը";
+"tour_section_12_text_3_1" = "Եթե պետք է, կարող եք թաքցնել էջում որոշակի բաժինները";
+"tour_section_12_text_3_2" = "<b>Հիշե՛ք.</b> Հիմնական բաժինները (Իմ Էջը, Իմ Ընկերները, Իմ Պատասխանները, Իմ Կարգավորումները) չի լինի թաքցնել";
+"tour_section_12_title_4" = "Գրառումների դիտարկում";
+"tour_section_12_text_4_1" = "Եթե հոգնել եք պատի հին դիզայնից որը վաղեմի հայտնի ՎԿոնտակտե–ին էր հարիր, ապա միշտ էլ կարող եք փոխել այն սարքելով միկրոբլոգ";
+"tour_section_12_text_4_2" = "Գրառումների տեսքը կարող է փոփոխվել երկու տարբերակի միջև ՝ ցանկացած ժամանակ";
+"tour_section_12_text_4_3" = "<b>Հաշվի առեք</b>, որ եթե հին դիզայնն եք ընտրել, վերջին մեկնաբանությունները չեն ցուցադրվի";
+"tour_section_12_bottom_text_1" = "Ետնապատկերի կարգավորման էջ";
+"tour_section_12_bottom_text_2" = "Ետնապատկերներով էջերի օրինակներ";
+"tour_section_12_bottom_text_3" = "Այս հարմարանքով կարող եք ավելի շատ անհատականացնել Ձեր էջը";
+"tour_section_12_bottom_text_4" = "Հին տեսք";
+"tour_section_12_bottom_text_5" = "Միկրոբլոգ";
+
+
+"tour_section_13_title_1" = "Պրոմոկոդեր";
+"tour_section_13_text_1" = "OpenVK–ն ունի պրոմոկոդերի համակարգ, որը ավելացնում է որոշակի արժույթ (գնահատման տոկոսներ, ձայներ և այլն)";
+"tour_section_13_text_2" = "Որոշակի կուպոններ ստեղծվում են տոների ժամանակ։ Դրանք հայթհայթելու համար հետևե՛ք <a href='https://t.me/openvk'>OpenVK–ի Telegram–յան ալիքին</a>";
+"tour_section_13_text_3" = "Պրոմոկոդն ակտիվացնելուց հետո սահմանված արժույթը անմիջապես կփոխանցվի Ձեզ";
+"tour_section_13_text_4" = "<b>Հիշե՛ք.</b> Բոլոր պրոմոկոդերի ակտիվացման ժամկետը խիստ սահմանափակ է";
+"tour_section_13_bottom_text_1" = "Պրոմոկոդերն ունեն 24 տառ ու թիվ";
+"tour_section_13_bottom_text_2" = "Հաջողված ակտիվացիա (խոսքի ՝ մրցանակաբաշխում ենք 100 ձայնով)";
+"tour_section_13_bottom_text_3" = "<b>Ուշադի՛ր.</b> Պրոմոկոդի ակտիվացումից հետո կրկին չեք կարող այն օգտագործել";
+
+"tour_section_14_title_1" = "Հեռախոսի տարբերակ";
+"tour_section_14_text_1" = "Այս պահին կայքի հեռախոսի վերսիան դեռևս չկա, սակայն գոյություն ունի հավելվածը Android-ի համար";
+"tour_section_14_text_2" = "OpenVK Legacy–ն OpenVK-ի ռետրո հավելվածն է, որը իմիտացնում է ՎԿոնտակտե–ի 2013թ. դիզայնը";
+"tour_section_14_text_3" = "Մինիմալ սպասարկվող տարբերակը Android 2.1 Eclair–ն է, որը անգամ վաղ 2010–ականների սարքերի վրա է աշխատում";
+
+"tour_section_14_title_2" = "Որտեղի՞ց ես կարող եմ ներբեռնել այն";
+"tour_section_14_text_2_1" = "Ռելիզային տարբերակները տեղադրվում են F-Droid–ի ռեպոզիտորիայում";
+"tour_section_14_text_2_2" = "Եթե Դուք բետա թեստավորող եք, հավելվածի նոր տարբերակները հրապարակվում են առանձին թարմացումների ալիքում";
+"tour_section_14_text_2_3" = "<b>Հաշվի՛ առեք.</b> Հավելվածը կարող է ունենալ բագեր և խնդիրներ, որոնց մասին խնդրվում է հայտնել <a href='/app'>հավելվածի պաշտոնական խմբում</a>";
+
+"tour_section_14_bottom_text_1" = "Էկրանի նկարներ";
+"tour_section_14_bottom_text_2" = "Սա ավարտում է կայքի ճամփորդությունը։ Եթե ցանկանում եք փորձարկել հեռախոսի հավելվածը, ստեղծել Ձեր խումբը, հրավիրել ընկերներին կամ նորերին գտնել, կամ էլ ուղղակի հավես ժամանակ անցկացնել, Դուք կարող եք անել դա հենց հիմա փոքրիկ <a href='/reg'>գրանցում անելով</a>";
+"tour_section_14_bottom_text_3" = "Սա ավարտում է կայքի ճամփորդությունը";
+
+/* Search */
+
+"s_people" = "Մարդիկ";
+"s_groups" = "Ակումբներ";
+"s_events" = "Իրադարձություններ";
+"s_apps" = "Հավելվածներ";
+"s_questions" = "Հարցեր";
+"s_notes" = "Նշումներ";
+"s_themes" = "Տեսքեր";
+"s_posts" = "Գրառումներ";
+"s_comments" = "Մեկնաբանություններ";
+"s_videos" = "Վիդեոներ";
+"s_audios" = "Երաժշտություն";
+"s_by_people" = "մարդկանց համար";
+"s_by_groups" = "խմբերի համար";
+"s_by_posts" = "գրառումների համար";
+"s_by_comments" = "մեկնաբանությունների համար";
+"s_by_videos" = "վիդեոների համար";
+"s_by_apps" = "հավելվածների համար";
+"s_by_audios" = "երգերի համար";
+
+"s_order_by" = "Դասավորել ըստ...";
+
+"s_order_by_id" = "Ըստ ID-ի";
+"s_order_by_name" = "Ըստ անվան";
+"s_order_by_random" = "Ըստ պատահականության";
+"s_order_by_rating" = "Ըստ վարկանիշի";
+"s_order_invert" = "Շրջել";
+
+"s_by_date" = "Ըստ ամսաթվի";
+"s_registered_before" = "Գրանցված մինչև";
+"s_registered_after" = "Գրանցված հետո";
+"s_date_before" = "Առաջ";
+"s_date_after" = "Հետո";
+
+"s_main" = "Հիմնական";
+
+"s_now_on_site" = "հիմա կայքում";
+"s_with_photo" = "նկարով";
+"s_only_in_names" = "միայն անուններում";
+
+"s_any" = "ցանկացած";
+"reset" = "Վերականգնել";
+
+"closed_group_post" = "Սա մասնավոր խմբի գրառում է";
+"deleted_target_comment" = "Այս մեկնաբանությունը ջնջված գրառման տակ է եղել";
+
+"no_results" = "Արդյունք չկա";
+
+/* Mobile */
+"mobile_friends" = "Ընկերներ";
+"mobile_photos" = "Նկարներ";
+"mobile_videos" = "Վիդեոներ";
+"mobile_messages" = "Նամակներ";
+"mobile_notes" = "Գրառումներ";
+"mobile_groups" = "Խմբեր";
+"mobile_search" = "Որոնում";
+"mobile_settings" = "Կարգավորումներ";
+"mobile_desktop_version" = "Համակարգչի տարբերակ";
+"mobile_log_out" = "Դուրս գալ";
+"mobile_menu" = "Մենյու";
+"mobile_like" = "Հավանել";
+"mobile_user_info_hide" = "Թաքցնել";
+"mobile_user_info_show_details" = "Ցույց տալ մանրամասն";

From 4c0deec5afc35ab5ed36ee9ee2b79142e89124db Mon Sep 17 00:00:00 2001
From: lalka2018 <99399973+lalka2016@users.noreply.github.com>
Date: Mon, 21 Aug 2023 12:45:27 +0300
Subject: [PATCH 07/26] r (#971)

---
 Web/Presenters/templates/components/video.xml | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/Web/Presenters/templates/components/video.xml b/Web/Presenters/templates/components/video.xml
index 33961c4f..f568e942 100644
--- a/Web/Presenters/templates/components/video.xml
+++ b/Web/Presenters/templates/components/video.xml
@@ -5,8 +5,10 @@
             <td valign="top">
                 <div class="video-preview">
                     <a href="/video{$video->getPrettyId()}">
-                        <img src="{$video->getThumbnailURL()}"
-                        style="max-width: 170px; max-height: 127px; margin: auto;" >
+                        <div class="video-preview">
+                            <img src="{$video->getThumbnailURL()}"
+                            style="max-width: 170px; max-height: 127px; margin: auto;" >
+                        </div>
                     </a>
                 </div>
             </td>
@@ -33,5 +35,5 @@
             </td>
         </tr>
     </tbody>
-    </table
+</table>
 {/block}

From 0d66c8e9d677ea29bc46ed3422885725c82a053f Mon Sep 17 00:00:00 2001
From: n1rwana <aydashkin@vk.com>
Date: Mon, 21 Aug 2023 12:47:25 +0300
Subject: [PATCH 08/26] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?=
 =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0?=
 =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B0?=
 =?UTF-8?q?=20=D1=81=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86?=
 =?UTF-8?q?=D0=B8=D0=B5=D0=B9=20=D0=BE=20=D0=B4=D0=B5=D0=B0=D0=BA=D1=82?=
 =?UTF-8?q?=D0=B8=D0=B2=D0=B0=D1=86=D0=B8=D0=B8=20(#966)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Web/Presenters/templates/@layout.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml
index f20169d6..3d724cf5 100644
--- a/Web/Presenters/templates/@layout.xml
+++ b/Web/Presenters/templates/@layout.xml
@@ -301,7 +301,7 @@
                     </div>
                 </div>
                 {ifset $thisUser}
-                    {if !$thisUser->isBanned()}
+                    {if !$thisUser->isBanned() && !$thisUser->isDeleted()}
                         </div>
                     {/if}
                 {/ifset}

From e433e46b36251359c9543267521609ce45030c31 Mon Sep 17 00:00:00 2001
From: n1rwana <aydashkin@vk.com>
Date: Mon, 21 Aug 2023 12:47:51 +0300
Subject: [PATCH 09/26] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?=
 =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=80=D0=B5=D0=B4=D0=B8=D1=80=D0=B5=D0=BA?=
 =?UTF-8?q?=D1=82=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D1=83=D0=B4=D0=B0?=
 =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B0=D0=B2=D0=B0=D1=82=D0=B0?=
 =?UTF-8?q?=D1=80=D0=BA=D0=B8=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D1=81?=
 =?UTF-8?q?=D1=82=D0=B2=D0=B0=20(#967)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Web/Presenters/PhotosPresenter.php | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php
index 345b2c60..9d64ba20 100644
--- a/Web/Presenters/PhotosPresenter.php
+++ b/Web/Presenters/PhotosPresenter.php
@@ -1,6 +1,6 @@
 <?php declare(strict_types=1);
 namespace openvk\Web\Presenters;
-use openvk\Web\Models\Entities\{Club, Photo, Album};
+use openvk\Web\Models\Entities\{Club, Photo, Album, User};
 use openvk\Web\Models\Repositories\{Photos, Albums, Users, Clubs};
 use Nette\InvalidStateException as ISE;
 
@@ -292,11 +292,13 @@ final class PhotosPresenter extends OpenVKPresenter
         if(!$photo) $this->notFound();
         if(is_null($this->user) || $this->user->id != $ownerId)
             $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
-        
+
+        $redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId;
+
         $photo->isolate();
         $photo->delete();
         
         $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена.");
-        $this->redirect("/id0");
+        $this->redirect($redirect);
     }
 }

From 245f8690c681f6327b137ce4423d043ebe8cbe29 Mon Sep 17 00:00:00 2001
From: n1rwana <aydashkin@vk.com>
Date: Sat, 26 Aug 2023 13:14:25 +0300
Subject: [PATCH 10/26] =?UTF-8?q?[noSpam]=20=D0=9F=D0=B0=D1=80=D0=B0=D0=BC?=
 =?UTF-8?q?=D0=B5=D1=82=D1=80=D1=8B=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B8=D1=80?=
 =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B8=20(#960)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Параметры блокировки

* Защита от SQLi и доработка поиска

* Фикс блокировки, если дата разблокировки не указана
---
 Web/Presenters/AdminPresenter.php         |   2 +-
 Web/Presenters/NoSpamPresenter.php        | 299 +++++++++++-----------
 Web/Presenters/templates/NoSpam/Index.xml |  39 ++-
 3 files changed, 191 insertions(+), 149 deletions(-)

diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php
index f5c40bcc..37fb50e8 100644
--- a/Web/Presenters/AdminPresenter.php
+++ b/Web/Presenters/AdminPresenter.php
@@ -363,7 +363,7 @@ final class AdminPresenter extends OpenVKPresenter
         if (str_contains($this->queryParam("reason"), "*"))
             exit(json_encode([ "error" => "Incorrect reason" ]));
 
-        $unban_time = strtotime($this->queryParam("date")) ?: NULL;
+        $unban_time = strtotime($this->queryParam("date")) ?: "permanent";
 
         $user = $this->users->get($id);
         if(!$user)
diff --git a/Web/Presenters/NoSpamPresenter.php b/Web/Presenters/NoSpamPresenter.php
index 1560ba63..8164d05e 100644
--- a/Web/Presenters/NoSpamPresenter.php
+++ b/Web/Presenters/NoSpamPresenter.php
@@ -177,26 +177,25 @@ final class NoSpamPresenter extends OpenVKPresenter
                 if ($conditions) {
                     $logs = $db->query("SELECT * FROM `ChandlerLogs` $whereStart $conditions GROUP BY `object_id`, `object_model`");
 
-                    if (!$where) {
-                        foreach ($logs as $log) {
-                            $log = (new Logs)->get($log->id);
-                            $response[] = $log->getObject()->unwrap();
-                        }
-                    } else {
-                        foreach ($logs as $log) {
-                            $log = (new Logs)->get($log->id);
-                            $object = $log->getObject()->unwrap();
+                    foreach ($logs as $log) {
+                        $log = (new Logs)->get($log->id);
+                        $object = $log->getObject()->unwrap();
 
-                            if (!$object) continue;
+                        if (!$object) continue;
+                        if ($where) {
                             if (str_starts_with($where, " AND")) {
                                 $where = substr_replace($where, "", 0, strlen(" AND"));
                             }
 
-                            foreach ($db->query("SELECT * FROM `$table` WHERE $where")->fetchAll() as $o) {
-                                if ($object->id === $o["id"]) {
+                            $a = $db->query("SELECT * FROM `$table` WHERE $where")->fetchAll();
+                            foreach ($a as $o) {
+                                if ($object->id == $o["id"]) {
                                     $response[] = $object;
                                 }
                             }
+
+                        } else {
+                            $response[] = $object;
                         }
                     }
                 }
@@ -206,70 +205,72 @@ final class NoSpamPresenter extends OpenVKPresenter
         }
 
         try {
-        $response = [];
-        $processed = 0;
+            $response = [];
+            $processed = 0;
 
-        $where = $this->postParam("where");
-        $ip = $this->postParam("ip");
-        $useragent = $this->postParam("useragent");
-        $searchTerm = $this->postParam("q");
-        $ts = (int)$this->postParam("ts");
-        $te = (int)$this->postParam("te");
-        $user = $this->postParam("user");
+            $where = $this->postParam("where");
+            $ip = addslashes($this->postParam("ip"));
+            $useragent = addslashes($this->postParam("useragent"));
+            $searchTerm = addslashes($this->postParam("q"));
+            $ts = (int)$this->postParam("ts");
+            $te = (int)$this->postParam("te");
+            $user = addslashes($this->postParam("user"));
 
-        if (!$ip && !$useragent && !$searchTerm && !$ts && !$te && !$where && !$searchTerm && !$user)
-            $this->returnJson(["success" => false, "error" => "Нет запроса. Заполните поле \"подстрока\" или введите запрос \"WHERE\" в поле под ним."]);
-
-        $models = explode(",", $this->postParam("models"));
-
-        foreach ($models as $_model) {
-            $model_name = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $_model;
-            if (!class_exists($model_name)) {
-                continue;
+            if ($where) {
+                $where = explode(";", $where)[0];
             }
 
-            $model = new $model_name;
+            if (!$ip && !$useragent && !$searchTerm && !$ts && !$te && !$where && !$searchTerm && !$user)
+                $this->returnJson(["success" => false, "error" => "Нет запроса. Заполните поле \"подстрока\" или введите запрос \"WHERE\" в поле под ним."]);
 
-            $c = new \ReflectionClass($model_name);
-            if ($c->isAbstract() || $c->getName() == NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") {
-                continue;
-            }
+            $models = explode(",", $this->postParam("models"));
 
-            $db = DatabaseConnection::i()->getContext();
-            $table = $model->getTableName();
-            $columns = $db->getStructure()->getColumns($table);
-
-            if ($searchTerm) {
-                $conditions = [];
-                $need_deleted = false;
-                foreach ($columns as $column) {
-                    if ($column["name"] == "deleted") {
-                        $need_deleted = true;
-                    } else {
-                        $conditions[] = "`$column[name]` REGEXP '$searchTerm'";
-                    }
+            foreach ($models as $_model) {
+                $model_name = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $_model;
+                if (!class_exists($model_name)) {
+                    continue;
                 }
-                $conditions = implode(" OR ", $conditions);
 
-                $where = ($this->postParam("where") ? " AND ($conditions)" : "($conditions)");
-                if ($need_deleted) $where .= " AND (`deleted` = 0)";
-            }
+                $model = new $model_name;
 
-            $rows = [];
-            if ($ip || $useragent || $ts || $te || $user) {
-                $rows = searchByAdditionalParams($table, $where, $ip, $useragent, $ts, $te, $user);
-            }
+                $c = new \ReflectionClass($model_name);
+                if ($c->isAbstract() || $c->getName() == NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") {
+                    continue;
+                }
 
-            if (count($rows) === 0) {
-                if (!$searchTerm) {
-                    if (str_starts_with($where, " AND")) {
-                        if ($searchTerm && !$this->postParam("where")) {
-                            $where = substr_replace($where, "", 0, strlen(" AND"));
+                $db = DatabaseConnection::i()->getContext();
+                $table = $model->getTableName();
+                $columns = $db->getStructure()->getColumns($table);
+
+                if ($searchTerm) {
+                    $conditions = [];
+                    $need_deleted = false;
+                    foreach ($columns as $column) {
+                        if ($column["name"] == "deleted") {
+                            $need_deleted = true;
                         } else {
-                            $where = "(" . $this->postParam("where") . ")" . $where;
+                            $conditions[] = "`$column[name]` REGEXP '$searchTerm'";
                         }
                     }
+                    $conditions = implode(" OR ", $conditions);
 
+                    $where = ($this->postParam("where") ? " AND ($conditions)" : "($conditions)");
+                    if ($need_deleted) $where .= " AND (`deleted` = 0)";
+                }
+
+                $rows = [];
+
+                if (str_starts_with($where, " AND")) {
+                    if ($searchTerm && !$this->postParam("where")) {
+                        $where = substr_replace($where, "", 0, strlen(" AND"));
+                    } else {
+                        $where = "(" . $this->postParam("where") . ")" . $where;
+                    }
+                }
+
+                if ($ip || $useragent || $ts || $te || $user) {
+                    $rows = searchByAdditionalParams($table, $where, $ip, $useragent, $ts, $te, $user);
+                } else {
                     if (!$where) {
                         $rows = [];
                     } else {
@@ -277,99 +278,105 @@ final class NoSpamPresenter extends OpenVKPresenter
                         $rows = $result->fetchAll();
                     }
                 }
-            }
 
-            if (!in_array((int)$this->postParam("ban"), [1, 2, 3])) {
-                foreach ($rows as $key => $object) {
-                    $object = (array)$object;
-                    $_obj = [];
-                    foreach ($object as $key => $value) {
-                        foreach ($columns as $column) {
-                            if ($column["name"] === $key && in_array(strtoupper($column["nativetype"]), ["BLOB", "BINARY", "VARBINARY", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB"])) {
-                                $value = "[BINARY]";
-                                break;
-                            }
-                        }
-
-                        $_obj[$key] = $value;
-                        $_obj["__model_name"] = $_model;
-                    }
-                    $response[] = $_obj;
-                }
-            } else {
-                $ids = [];
-
-                foreach ($rows as $object) {
-                    $object = new $model_name($db->table($table)->get($object->id));
-                    if (!$object) continue;
-                    $ids[] = $object->getId();
-                }
-
-                $log = new NoSpamLog;
-                $log->setUser($this->user->id);
-                $log->setModel($_model);
-                if ($searchTerm) {
-                    $log->setRegex($searchTerm);
-                } else {
-                    $log->setRequest($where);
-                }
-                $log->setBan_Type((int)$this->postParam("ban"));
-                $log->setCount(count($rows));
-                $log->setTime(time());
-                $log->setItems(implode(",", $ids));
-                $log->save();
-
-                $banned_ids = [];
-                foreach ($rows as $object) {
-                    $object = new $model_name($db->table($table)->get($object->id));
-                    if (!$object) continue;
-
-                    $owner = NULL;
-                    $methods = ["getOwner", "getUser", "getRecipient", "getInitiator"];
-
-                    if (method_exists($object, "ban")) {
-                        $owner = $object;
-                    } else {
-                        foreach ($methods as $method) {
-                            if (method_exists($object, $method)) {
-                                $owner = $object->$method();
-                                break;
-                            }
-                        }
-                    }
-
-                    if ($owner instanceof User && $owner->getId() === $this->user->id) {
-                        if (count($rows) === 1) {
-                            $this->returnJson(["success" => false, "error" => "\"Производственная травма\" — Вы не можете блокировать или удалять свой же контент"]);
-                        } else {
-                            continue;
-                        }
-                    }
-
-                    if (in_array((int)$this->postParam("ban"), [2, 3])) {
-                        if ($owner) {
-                            $_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId());
-                            if (!in_array($_id, $banned_ids)) {
-                                if ($owner instanceof User) {
-                                    $owner->ban("**content-noSpamTemplate-" . $log->getId() . "**", false, time() + $owner->getNewBanTime(), $this->user->id);
-                                } else {
-                                    $owner->ban("Подозрительная активность");
+                if (!in_array((int)$this->postParam("ban"), [1, 2, 3])) {
+                    foreach ($rows as $key => $object) {
+                        $object = (array)$object;
+                        $_obj = [];
+                        foreach ($object as $key => $value) {
+                            foreach ($columns as $column) {
+                                if ($column["name"] === $key && in_array(strtoupper($column["nativetype"]), ["BLOB", "BINARY", "VARBINARY", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB"])) {
+                                    $value = "[BINARY]";
+                                    break;
                                 }
-
-                                $banned_ids[] = $_id;
                             }
+
+                            $_obj[$key] = $value;
+                            $_obj["__model_name"] = $_model;
                         }
+                        $response[] = $_obj;
+                    }
+                } else {
+                    $ids = [];
+
+                    foreach ($rows as $object) {
+                        $object = new $model_name($db->table($table)->get($object->id));
+                        if (!$object) continue;
+                        $ids[] = $object->getId();
                     }
 
-                    if (in_array((int)$this->postParam("ban"), [1, 3]))
-                        $object->delete();
+                    $log = new NoSpamLog;
+                    $log->setUser($this->user->id);
+                    $log->setModel($_model);
+                    if ($searchTerm) {
+                        $log->setRegex($searchTerm);
+                    } else {
+                        $log->setRequest($where);
+                    }
+                    $log->setBan_Type((int)$this->postParam("ban"));
+                    $log->setCount(count($rows));
+                    $log->setTime(time());
+                    $log->setItems(implode(",", $ids));
+                    $log->save();
+
+                    $banned_ids = [];
+                    foreach ($rows as $object) {
+                        $object = new $model_name($db->table($table)->get($object->id));
+                        if (!$object) continue;
+
+                        $owner = NULL;
+                        $methods = ["getOwner", "getUser", "getRecipient", "getInitiator"];
+
+                        if (method_exists($object, "ban")) {
+                            $owner = $object;
+                        } else {
+                            foreach ($methods as $method) {
+                                if (method_exists($object, $method)) {
+                                    $owner = $object->$method();
+                                    break;
+                                }
+                            }
+                        }
+
+                        if ($owner instanceof User && $owner->getId() === $this->user->id) {
+                            if (count($rows) === 1) {
+                                $this->returnJson(["success" => false, "error" => "\"Производственная травма\" — Вы не можете блокировать или удалять свой же контент"]);
+                            } else {
+                                continue;
+                            }
+                        }
+
+                        if (in_array((int)$this->postParam("ban"), [2, 3])) {
+                            $reason = mb_strlen(trim($this->postParam("ban_reason"))) > 0 ? addslashes($this->postParam("ban_reason")) : ("**content-noSpamTemplate-" . $log->getId() . "**");
+                            $is_forever = (string)$this->postParam("is_forever") === "true";
+                            $unban_time = $is_forever ? 0 : (int)$this->postParam("unban_time") ?? NULL;
+
+                            if ($owner) {
+                                $_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId());
+                                if (!in_array($_id, $banned_ids)) {
+                                    if ($owner instanceof User) {
+                                        if (!$unban_time && !$is_forever)
+                                            $unban_time = time() + $owner->getNewBanTime();
+
+                                        $owner->ban($reason, false, $unban_time, $this->user->id);
+                                    } else {
+                                        $owner->ban("Подозрительная активность");
+                                    }
+
+                                    $banned_ids[] = $_id;
+                                }
+                            }
+                        }
+
+                        if (in_array((int)$this->postParam("ban"), [1, 3]))
+                            $object->delete();
+                    }
+
+                    $processed++;
                 }
-
-                $processed++;
             }
-        }
 
-        $this->returnJson(["success" => true, "processed" => $processed, "count" => count($response), "list" => $response]);
+            $this->returnJson(["success" => true, "processed" => $processed, "count" => count($response), "list" => $response]);
         } catch (\Throwable $e) {
             $this->returnJson(["success" => false, "error" => $e->getMessage()]);
         }
diff --git a/Web/Presenters/templates/NoSpam/Index.xml b/Web/Presenters/templates/NoSpam/Index.xml
index 746f86d5..89305c5b 100644
--- a/Web/Presenters/templates/NoSpam/Index.xml
+++ b/Web/Presenters/templates/NoSpam/Index.xml
@@ -106,13 +106,31 @@
                             <span class="nobold">Параметры блокировки:</span>
                         </td>
                         <td>
-                            <select name="ban_type" id="noSpam-ban-type">
+                            <select name="ban_type" id="noSpam-ban-type" style="width: 140px;">
                                 <option value="1">Только откат</option>
                                 <option value="2">Только блокировка</option>
                                 <option value="3">Откат и блокировка</option>
                             </select>
                         </td>
                     </tr>
+                    <tr class="banSettings" style="width: 129px; border-top: 1px solid #ECECEC; display: none;">
+                        <td>
+                            <span class="nobold">Причина:</span>
+                        </td>
+                        <td>
+                            <input type="text" name="ban-reason" id="ban-reason" style="width: 140px;" />
+                        </td>
+                    </tr>
+                    <tr class="banSettings" style="width: 129px; border-top: 1px solid #ECECEC; display: none;">
+                        <td>
+                            <span class="nobold">До:</span>
+                        </td>
+                        <td>
+                            <input type="datetime-local" name="unban-time" id="unban-time" style="width: 140px;" />
+                            <br />
+                            <input type="checkbox" name="is_forever" id="is-forever" /> навсегда
+                        </td>
+                    </tr>
                 </tbody>
             </table>
             <div style="border-top: 1px solid #ECECEC; margin: 8px 0;"/>
@@ -158,7 +176,6 @@
             $("#noSpam-results-loader").show();
             $("#noSpam-loader").show();
 
-
             let models = [];
             $(".model").each(function (i) {
                 let name = $(this).val();
@@ -178,6 +195,10 @@
             let ts = $("#ts").val() ? Math.floor(new Date($("#ts").val()).getTime() / 1000) : null;
             let te = $("#te").val() ? Math.floor(new Date($("#te").val()).getTime() / 1000) : null;
             let user = $("#user").val();
+            let ban_reason = $("#ban-reason").val();
+            let unban_time = $("#unban-time").val() ? Math.floor(new Date($("#unban-time").val()).getTime() / 1000) : null;
+            let is_forever = $("#is-forever").prop('checked');
+            console.log(ban_reason, unban_time, is_forever);
 
             await $.ajax({
                 type: "POST",
@@ -193,6 +214,9 @@
                     ts: ts,
                     te: te,
                     user: user,
+                    ban_reason: ban_reason,
+                    unban_time: unban_time,
+                    is_forever: is_forever,
                     hash: {=$csrfToken}
                 },
                 success: (response) => {
@@ -277,6 +301,17 @@
             selectChange(e.target.value);
         })
 
+        $("#noSpam-ban-type").change(async (e) => {
+            if (e.target.value > 1) {
+                $(".banSettings").show();
+            } else {
+                $("#ban-reason").val(null);
+                $("#unban-time").val(null);
+                $("#is-forever").prop('checked', false);
+                $(".banSettings").hide();
+            }
+        });
+
         $("#add-model").on("click", () => {
             console.log($(".model").length);
             $("#models-list").append(`

From 14d5caaf9f5e0a4517bda5ba71165890b087c8a3 Mon Sep 17 00:00:00 2001
From: lalka2018 <99399973+lalka2016@users.noreply.github.com>
Date: Thu, 14 Sep 2023 20:36:29 +0300
Subject: [PATCH 11/26] Photos: AJAX support (#980)

* aj

* Drag'n'drop

* add good view
---
 Web/Presenters/PhotosPresenter.php            |  94 ++++++---
 Web/Presenters/templates/Photos/EditAlbum.xml |   9 +
 .../templates/Photos/UploadPhoto.xml          |  77 +++++---
 Web/static/css/main.css                       |  98 +++++++++-
 Web/static/js/al_photos.js                    | 179 ++++++++++++++++++
 locales/en.strings                            |  21 ++
 locales/ru.strings                            |  21 ++
 7 files changed, 442 insertions(+), 57 deletions(-)
 create mode 100644 Web/static/js/al_photos.js

diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php
index 9d64ba20..11026d00 100644
--- a/Web/Presenters/PhotosPresenter.php
+++ b/Web/Presenters/PhotosPresenter.php
@@ -27,7 +27,7 @@ final class PhotosPresenter extends OpenVKPresenter
             if(!$user) $this->notFound();
             if (!$user->getPrivacyPermission('photos.read', $this->user->identity ?? NULL))
                 $this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
-            $this->template->albums  = $this->albums->getUserAlbums($user, $this->queryParam("p") ?? 1);
+            $this->template->albums  = $this->albums->getUserAlbums($user, (int)($this->queryParam("p") ?? 1));
             $this->template->count   = $this->albums->getUserAlbumsCount($user);
             $this->template->owner   = $user;
             $this->template->canEdit = false;
@@ -36,7 +36,7 @@ final class PhotosPresenter extends OpenVKPresenter
         } else {
             $club = (new Clubs)->get(abs($owner));
             if(!$club) $this->notFound();
-            $this->template->albums  = $this->albums->getClubAlbums($club, $this->queryParam("p") ?? 1);
+            $this->template->albums  = $this->albums->getClubAlbums($club, (int)($this->queryParam("p") ?? 1));
             $this->template->count   = $this->albums->getClubAlbumsCount($club);
             $this->template->owner   = $club;
             $this->template->canEdit = false;
@@ -46,7 +46,7 @@ final class PhotosPresenter extends OpenVKPresenter
         
         $this->template->paginatorConf = (object) [
             "count"   => $this->template->count,
-            "page"    => $this->queryParam("p") ?? 1,
+            "page"    => (int)($this->queryParam("p") ?? 1),
             "amount"  => NULL,
             "perPage" => OPENVK_DEFAULT_PER_PAGE,
         ];
@@ -147,7 +147,7 @@ final class PhotosPresenter extends OpenVKPresenter
         $this->template->photos = iterator_to_array( $album->getPhotos( (int) ($this->queryParam("p") ?? 1), 20) );
         $this->template->paginatorConf = (object) [
             "count"   => $album->getPhotosCount(),
-            "page"    => $this->queryParam("p") ?? 1,
+            "page"    => (int)($this->queryParam("p") ?? 1),
             "amount"  => sizeof($this->template->photos),
             "perPage" => 20,
             "atBottom" => true
@@ -221,39 +221,74 @@ final class PhotosPresenter extends OpenVKPresenter
     function renderUploadPhoto(): void
     {
         $this->assertUserLoggedIn();
-        $this->willExecuteWriteAction();
+        $this->willExecuteWriteAction(true);
         
         if(is_null($this->queryParam("album")))
-            $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в <b>DELETED</b>.");
+            $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED.", 500, true);
         
         [$owner, $id] = explode("_", $this->queryParam("album"));
         $album = $this->albums->get((int) $id);
         if(!$album)
-            $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в <b>DELETED</b>.");
+            $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED.", 500, true);
         if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity))
-            $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
+            $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.", 500, true);
         
         if($_SERVER["REQUEST_METHOD"] === "POST") {
-            if(!isset($_FILES["blob"]))
-                $this->flashFail("err", "Нету фотографии", "Выберите файл.");
-            
-            try {
-                $photo = new Photo;
-                $photo->setOwner($this->user->id);
-                $photo->setDescription($this->postParam("desc"));
-                $photo->setFile($_FILES["blob"]);
-                $photo->setCreated(time());
-                $photo->save();
-            } catch(ISE $ex) {
-                $name = $album->getName();
-                $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в <b>$name</b>.");
-            }
-            
-            $album->addPhoto($photo);
-            $album->setEdited(time());
-            $album->save();
+            if($this->queryParam("act") == "finish") {
+                $result = json_decode($this->postParam("photos"), true);
+                
+                foreach($result as $photoId => $description) {
+                    $phot = $this->photos->get($photoId);
 
-            $this->redirect("/photo" . $photo->getPrettyId() . "?from=album" . $album->getId());
+                    if(!$phot || $phot->isDeleted() || $phot->getOwner()->getId() != $this->user->id)
+                        continue;
+                    
+                    if(iconv_strlen($description) > 255)
+                        $this->flashFail("err", tr("error"), tr("description_too_long"), 500, true);
+
+                    $phot->setDescription($description);
+                    $phot->save();
+
+                    $album = $phot->getAlbum();
+                }
+
+                $this->returnJson(["success" => true,
+                                    "album"  => $album->getId(),
+                                    "owner"  => $album->getOwner() instanceof User ? $album->getOwner()->getId() : $album->getOwner()->getId() * -1]);
+            }
+
+            if(!isset($_FILES))
+                $this->flashFail("err", "Нету фотографии", "Выберите файл.", 500, true);
+            
+            $photos = [];
+            for($i = 0; $i < $this->postParam("count"); $i++) {
+                try {
+                    $photo = new Photo;
+                    $photo->setOwner($this->user->id);
+                    $photo->setDescription("");
+                    $photo->setFile($_FILES["photo_".$i]);
+                    $photo->setCreated(time());
+                    $photo->save();
+
+                    $photos[] = [
+                        "url"   => $photo->getURLBySizeId("tiny"),
+                        "id"    => $photo->getId(),
+                        "vid"   => $photo->getVirtualId(),
+                        "owner" => $photo->getOwner()->getId(),
+                        "link"  => $photo->getURL()
+                    ];
+                } catch(ISE $ex) {
+                    $name = $album->getName();
+                    $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в $name.", 500, true);
+                }
+
+                $album->addPhoto($photo);
+                $album->setEdited(time());
+                $album->save();
+            }
+
+            $this->returnJson(["success" => true,
+                "photos" => $photos]);
         } else {
             $this->template->album = $album;
         }
@@ -285,7 +320,7 @@ final class PhotosPresenter extends OpenVKPresenter
     function renderDeletePhoto(int $ownerId, int $photoId): void
     {
         $this->assertUserLoggedIn();
-        $this->willExecuteWriteAction();
+        $this->willExecuteWriteAction($_SERVER["REQUEST_METHOD"] === "POST");
         $this->assertNoCSRF();
         
         $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId);
@@ -298,6 +333,9 @@ final class PhotosPresenter extends OpenVKPresenter
         $photo->isolate();
         $photo->delete();
         
+        if($_SERVER["REQUEST_METHOD"] === "POST")
+            $this->returnJson(["success" => true]);
+
         $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена.");
         $this->redirect($redirect);
     }
diff --git a/Web/Presenters/templates/Photos/EditAlbum.xml b/Web/Presenters/templates/Photos/EditAlbum.xml
index 271d1034..a10b5c7f 100644
--- a/Web/Presenters/templates/Photos/EditAlbum.xml
+++ b/Web/Presenters/templates/Photos/EditAlbum.xml
@@ -14,6 +14,15 @@
 {/block}
 
 {block content}
+    <div class="tabs">
+        <div id="activetabs" class="tab">
+            <a id="act_tab_a" href="/album{$album->getPrettyId()}/edit">{_edit_album}</a>
+        </div>
+        <div class="tab">
+            <a href="/photos/upload?album={$album->getPrettyId()}">{_add_photos}</a>
+        </div>
+    </div>
+
     <form method="post" enctype="multipart/form-data">
       <table cellspacing="6">
         <tbody>
diff --git a/Web/Presenters/templates/Photos/UploadPhoto.xml b/Web/Presenters/templates/Photos/UploadPhoto.xml
index 9876e5b9..6ea987cb 100644
--- a/Web/Presenters/templates/Photos/UploadPhoto.xml
+++ b/Web/Presenters/templates/Photos/UploadPhoto.xml
@@ -12,32 +12,53 @@
 {/block}
 
 {block content}
-    <form action="/photos/upload?album={$album->getPrettyId()}" method="post" enctype="multipart/form-data">
-      <table cellspacing="6">
-        <tbody>
-          <tr>
-            <td width="120" valign="top"><span class="nobold">{_description}:</span></td>
-            <td><textarea style="margin: 0px; height: 50px; width: 159px; resize: none;" name="desc"></textarea></td>
-          </tr>
-          <tr>
-            <td width="120" valign="top"><span class="nobold">{_photo}:</span></td>
-            <td>
-              <label class="button" style="">{_browse}
-                <input type="file" id="blob" name="blob" style="display: none;" onchange="filename.innerHTML=blob.files[0].name" />
-              </label>
-              <div id="filename" style="margin-top: 10px;"></div>
-            </td>
-          </tr>
-          <tr>
-            <td width="120" valign="top"></td>
-            <td>
-                <input type="hidden" name="hash" value="{$csrfToken}" />
-                <input type="submit" class="button" name="submit" value="Загрузить" />
-            </td>
-          </tr>
-        </tbody>
-      </table>
-      
-      <input n:ifset="$_GET['album']" type="hidden" name="album" value="{$_GET['album']}" />
-    </form>
+    <div class="tabs">
+        <div class="tab">
+            <a href="/album{$album->getPrettyId()}/edit">{_edit_album}</a>
+        </div>
+        <div id="activetabs" class="tab">
+            <a id="act_tab_a" href="#">{_add_photos}</a>
+        </div>
+    </div>
+
+    <input type="file" accept=".jpg,.png,.gif" name="files[]" multiple class="button" id="uploadButton" style="display:none">
+
+    <div class="container_gray" style="height: 344px;">
+        <div class="insertThere"></div>
+        <div class="whiteBox" style="display: block;">
+            <div class="boxContent">
+                <h4>{_uploading_photos_from_computer}</h4>
+
+                <div class="limits" style="margin-top:17px">
+                    <b style="color:#45688E">{_admin_limits}</b>
+                    <ul class="blueList" style="margin-left: -25px;margin-top: 1px;">
+                        <li>{_supported_formats}</li>
+                        <li>{_max_load_photos}</li>
+                    </ul>
+
+                    <div style="text-align: center;padding-top: 4px;" class="insertAgain">
+                        <input type="button" class="button" id="fakeButton" onclick="uploadButton.click()" value="{_upload_picts}">
+                    </div>
+
+                    <div class="tipping" style="margin-top: 19px;">
+                        <span style="line-height: 15px"><b>{_tip}</b>: {_tip_ctrl}</span>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="insertPhotos" id="photos" style="margin-top: 9px;padding-bottom: 12px;"></div>
+
+        <input type="button" class="button" style="display:none;margin-left: auto;margin-right: auto;" id="endUploading" value="{_end_uploading}">
+    </div>
+
+    <input n:ifset="$_GET['album']" type="hidden" id="album" value="{$_GET['album']}" />
+
+    <script>
+        uploadButton.value = ''
+    </script>
+{/block}
+
+{block bodyScripts}
+    {script "js/al_photos.js"}
 {/block}
diff --git a/Web/static/css/main.css b/Web/static/css/main.css
index 55484f13..f05d0a2b 100644
--- a/Web/static/css/main.css
+++ b/Web/static/css/main.css
@@ -2700,4 +2700,100 @@ body.article .floating_sidebar, body.article .page_content {
     position: absolute;
     right: 22px;
     font-size: 12px;
-}
\ No newline at end of file
+}
+
+.uploadedImage img {
+    max-height: 76px;
+    object-fit: cover;
+}
+
+.lagged {
+    filter: opacity(0.5);
+    cursor: progress;
+    user-select: none;
+}
+
+.lagged * {
+    pointer-events: none;
+}
+
+.button.dragged {
+    background: #c4c4c4 !important;
+    border-color: #c4c4c4 !important;
+    color: black !important;
+}
+
+.whiteBox {
+    background: white;
+    width: 421px;
+    margin-left: auto;
+    margin-right: auto;
+    border: 1px solid #E8E8E8;
+    margin-top: 7%;
+    height: 231px;
+}
+
+.boxContent {
+    padding: 24px 38px;
+}
+
+.blueList {
+    list-style-type: none;
+}
+
+.blueList li {
+    color: black;
+    font-size: 11px;
+    padding-top: 7px;
+}
+
+.blueList li::before {
+    content: " ";
+    width: 5px;
+    height: 5px;
+    display: inline-block;
+    vertical-align: bottom;
+    background-color: #73889C;
+    margin: 3px;
+    margin-left: 2px;
+    margin-right: 7px;
+}
+
+.insertedPhoto {
+    background: white;
+    border: 1px solid #E8E7EA;
+    padding: 10px;
+    height: 100px;
+    margin-top: 6px;
+}
+
+.uploadedImage {
+    float: right;
+    display: flex;
+    flex-direction: column;
+}
+
+.uploadedImageDescription {
+    width: 449px;
+}
+
+.uploadedImageDescription textarea {
+    width: 84%;
+    height: 86px;
+}
+
+.smallFrame {
+    border: 1px solid #E1E3E5;
+    background: #F0F0F0;
+    height: 33px;
+    text-align: center;
+    cursor: pointer;
+}
+
+.smallFrame .smallBtn {
+    margin-top: 10px;
+}
+
+.smallFrame:hover {
+    background: #E9F0F1 !important;
+}
diff --git a/Web/static/js/al_photos.js b/Web/static/js/al_photos.js
new file mode 100644
index 00000000..59965c09
--- /dev/null
+++ b/Web/static/js/al_photos.js
@@ -0,0 +1,179 @@
+$(document).on("change", "#uploadButton", (e) => {
+    let iterator = 0
+
+    if(e.currentTarget.files.length > 10) {
+        MessageBox(tr("error"), tr("too_many_pictures"), [tr("ok")], [() => {Function.noop}])
+        return;
+    }
+
+    if(document.querySelector(".whiteBox").style.display == "block") {
+        document.querySelector(".whiteBox").style.display = "none"
+        document.querySelector(".insertThere").append(document.getElementById("fakeButton"));
+    }
+
+    let photos = new FormData()
+    for(file of e.currentTarget.files) {
+        photos.append("photo_"+iterator, file)
+        iterator += 1
+    }
+
+    photos.append("count", e.currentTarget.files.length)
+    photos.append("hash", u("meta[name=csrf]").attr("value"))
+
+    let xhr = new XMLHttpRequest()
+    xhr.open("POST", "/photos/upload?album="+document.getElementById("album").value)
+
+    xhr.onloadstart = () => {
+        document.querySelector(".insertPhotos").insertAdjacentHTML("beforeend", `<img id="loader" src="/assets/packages/static/openvk/img/loading_mini.gif">`)
+    }
+
+    xhr.onload = () => {
+        let result = JSON.parse(xhr.responseText)
+
+        if(result.success) {
+            u("#loader").remove()
+            let photosArr = result.photos
+
+            for(photo of photosArr) {
+                let table = document.querySelector(".insertPhotos")
+
+                table.insertAdjacentHTML("beforeend", `
+                <div id="photo" class="insertedPhoto" data-id="${photo.id}">
+                    <div class="uploadedImageDescription" style="float: left;">
+                        <span style="color: #464646;position: absolute;">${tr("description")}:</span>
+                        <textarea style="margin-left: 62px; resize: none;" maxlength="255"></textarea>
+                    </div>
+                    <div class="uploadedImage">
+                        <a href="${photo.link}" target="_blank"><img width="125" src="${photo.url}"></a>
+                        <a class="profile_link" style="width: 125px;" id="deletePhoto" data-id="${photo.vid}" data-owner="${photo.owner}">${tr("delete")}</a>
+                        <!--<div class="smallFrame" style="margin-top: 6px;">
+                            <div class="smallBtn">${tr("album_poster")}</div>
+                        </div>-->
+                    </div>
+                </div>
+                `)
+            }
+
+            document.getElementById("endUploading").style.display = "block"
+        } else {
+            u("#loader").remove()
+            MessageBox(tr("error"), escapeHtml(result.flash.message) ?? tr("error_uploading_photo"), [tr("ok")], [() => {Function.noop}])
+        }
+    }
+
+    xhr.send(photos)
+})
+
+$(document).on("click", "#endUploading", (e) => {
+    let table = document.querySelector("#photos")
+    let data  = new FormData()
+    let arr   = new Map();
+    for(el of table.querySelectorAll("div#photo")) {
+        arr.set(el.dataset.id, el.querySelector("textarea").value)
+    }
+
+    data.append("photos", JSON.stringify(Object.fromEntries(arr)))
+    data.append("hash", u("meta[name=csrf]").attr("value"))
+
+    let xhr = new XMLHttpRequest()
+    // в самом вк на каждое изменение описания отправляется свой запрос, но тут мы экономим запросы
+    xhr.open("POST", "/photos/upload?act=finish&album="+document.getElementById("album").value)
+
+    xhr.onloadstart = () => {
+        e.currentTarget.setAttribute("disabled", "disabled")
+    }
+
+    xhr.onerror = () => {
+        MessageBox(tr("error"), tr("error_uploading_photo"), [tr("ok")], [() => {Function.noop}])
+    }
+
+    xhr.onload = () => {
+        let result = JSON.parse(xhr.responseText)
+
+        if(!result.success) {
+            MessageBox(tr("error"), escapeHtml(result.flash.message), [tr("ok")], [() => {Function.noop}])
+        } else {
+            document.querySelector(".page_content .insertPhotos").innerHTML = ""
+            document.getElementById("endUploading").style.display = "none"
+    
+            NewNotification(tr("photos_successfully_uploaded"), tr("click_to_go_to_album"), null, () => {window.location.assign(`/album${result.owner}_${result.album}`)})
+            
+            document.querySelector(".whiteBox").style.display = "block"
+            document.querySelector(".insertAgain").append(document.getElementById("fakeButton"))
+        }
+
+        e.currentTarget.removeAttribute("disabled")
+    }
+
+    xhr.send(data)
+})
+
+$(document).on("click", "#deletePhoto", (e) => {
+    let data  = new FormData()
+    data.append("hash", u("meta[name=csrf]").attr("value"))
+
+    let xhr = new XMLHttpRequest()
+    xhr.open("POST", `/photo${e.currentTarget.dataset.owner}_${e.currentTarget.dataset.id}/delete`)
+
+    xhr.onloadstart = () => {
+        e.currentTarget.closest("div#photo").classList.add("lagged")
+    }
+
+    xhr.onerror = () => {
+        MessageBox(tr("error"), tr("unknown_error"), [tr("ok")], [() => {Function.noop}])
+    }
+
+    xhr.onload = () => {
+        u(e.currentTarget.closest("div#photo")).remove()
+
+        if(document.querySelectorAll("div#photo").length < 1) {
+            document.getElementById("endUploading").style.display = "none"
+            document.querySelector(".whiteBox").style.display = "block"
+            document.querySelector(".insertAgain").append(document.getElementById("fakeButton"))
+        }
+    }
+
+    xhr.send(data)
+})
+
+$(document).on("dragover drop", (e) => {
+    e.preventDefault()
+
+    return false;
+})
+
+$(document).on("dragover", (e) => {
+    e.preventDefault()
+    document.querySelector("#fakeButton").classList.add("dragged")
+    document.querySelector("#fakeButton").value = tr("drag_files_here")
+})
+
+$(document).on("dragleave", (e) => {
+    e.preventDefault()
+    document.querySelector("#fakeButton").classList.remove("dragged")
+    document.querySelector("#fakeButton").value = tr("upload_picts")
+})
+
+$("#fakeButton").on("drop", (e) => {
+    e.originalEvent.dataTransfer.dropEffect = 'move';
+    e.preventDefault()
+
+    $(document).trigger("dragleave")
+
+    let files = e.originalEvent.dataTransfer.files
+
+    for(const file of files) {
+        if(!file.type.startsWith('image/')) {
+            MessageBox(tr("error"), tr("only_images_accepted", escapeHtml(file.name)), [tr("ok")], [() => {Function.noop}])
+            return;
+        }
+
+        if(file.size > 5 * 1024 * 1024) {
+            MessageBox(tr("error"), tr("max_filesize", 5), [tr("ok")], [() => {Function.noop}])
+            return;
+        }
+    }
+
+    document.getElementById("uploadButton").files = files
+    u("#uploadButton").trigger("change")
+})
diff --git a/locales/en.strings b/locales/en.strings
index 487d2156..780da8b3 100644
--- a/locales/en.strings
+++ b/locales/en.strings
@@ -381,6 +381,25 @@
 "upd_f" = "updated her profile picture";
 "upd_g" = "updated group's picture";
 
+"add_photos" = "Add photos";
+"upload_picts" = "Upload photos";
+"end_uploading" = "Finish uploading";
+"photos_successfully_uploaded" = "Photos successfully uploaded";
+"click_to_go_to_album" = "Click here to go to album.";
+"error_uploading_photo" = "Error when uploading photo";
+"too_many_pictures" = "No more than 10 pictures";
+
+"drag_files_here" = "Drag files here";
+"only_images_accepted" = "File \"$1\" is not an image";
+"max_filesize" = "Max filesize is $1 MB";
+
+"uploading_photos_from_computer" = "Uploading photos from Your computer";
+"supported_formats" = "Supported file formats: JPG, PNG and GIF.";
+"max_load_photos" = "You can upload up to 10 photos at a time.";
+"tip" = "Tip";
+"tip_ctrl" = "to select multiple photos at once, hold down the Ctrl key when selecting files in Windows or the CMD key in Mac OS.";
+"album_poster" = "Album poster";
+
 /* Notes */
 
 "notes" = "Notes";
@@ -1066,6 +1085,7 @@
 "error_data_too_big" = "Attribute '$1' must be at most $2 $3 long";
 
 "forbidden" = "Access error";
+"unknown_error" = "Unknown error";
 "forbidden_comment" = "This user's privacy settings do not allow you to look at his page.";
 
 "changes_saved" = "Changes saved";
@@ -1117,6 +1137,7 @@
 "media_file_corrupted_or_too_large" = "The media content file is corrupted or too large.";
 "post_is_empty_or_too_big" = "The post is empty or too big.";
 "post_is_too_big" = "The post is too big.";
+"description_too_long" = "Description is too long.";
 
 /* Admin actions */
 
diff --git a/locales/ru.strings b/locales/ru.strings
index dc101e2b..2364175a 100644
--- a/locales/ru.strings
+++ b/locales/ru.strings
@@ -364,6 +364,25 @@
 "upd_f" = "обновила фотографию на своей странице";
 "upd_g" = "обновило фотографию группы";
 
+"add_photos" = "Добавить фотографии";
+"upload_picts" = "Загрузить фотографии";
+"end_uploading" = "Завершить загрузку";
+"photos_successfully_uploaded" = "Фотографии успешно загружены";
+"click_to_go_to_album" = "Нажмите, чтобы перейти к альбому.";
+"error_uploading_photo" = "Не удалось загрузить фотографию";
+"too_many_pictures" = "Не больше 10 фотографий";
+
+"drag_files_here" = "Перетащите файлы сюда";
+"only_images_accepted" = "Файл \"$1\" не является изображением";
+"max_filesize" = "Максимальный размер файла — $1 мегабайт";
+
+"uploading_photos_from_computer" = "Загрузка фотографий с Вашего компьютера";
+"supported_formats" = "Поддерживаемые форматы файлов: JPG, PNG и GIF.";
+"max_load_photos" = "Вы можете загружать до 10 фотографий за один раз.";
+"tip" = "Подсказка";
+"tip_ctrl" = "для того, чтобы выбрать сразу несколько фотографий, удерживайте клавишу Ctrl при выборе файлов в ОС Windows или клавишу CMD в Mac OS.";
+"album_poster" = "Обложка альбома";
+
 /* Notes */
 
 "notes" = "Заметки";
@@ -982,6 +1001,7 @@
 "error_repost_fail" = "Не удалось поделиться записью";
 "error_data_too_big" = "Аттрибут '$1' не может быть длиннее $2 $3";
 "forbidden" = "Ошибка доступа";
+"unknown_error" = "Неизвестная ошибка";
 "forbidden_comment" = "Настройки приватности этого пользователя не разрешают вам смотреть на его страницу.";
 "changes_saved" = "Изменения сохранены";
 "changes_saved_comment" = "Новые данные появятся на вашей странице";
@@ -1017,6 +1037,7 @@
 "media_file_corrupted_or_too_large" = "Файл медиаконтента повреждён или слишком велик.";
 "post_is_empty_or_too_big" = "Пост пустой или слишком большой.";
 "post_is_too_big" = "Пост слишком большой.";
+"description_too_long" = "Описание слишком длинное.";
 
 /* Admin actions */
 

From 97a176c261e0813abe9a3942ec71294f2d187eb6 Mon Sep 17 00:00:00 2001
From: lalka2018 <99399973+lalka2016@users.noreply.github.com>
Date: Thu, 14 Sep 2023 20:54:22 +0300
Subject: [PATCH 12/26] =?UTF-8?q?=D0=A0=D0=B5=D0=B4=D0=B0=D0=BA=D1=82?=
 =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE?=
 =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B2=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE?=
 =?UTF-8?q?=20=D0=BF=D0=BE=D0=BA=D1=80=D1=83=D1=87=D0=B5=20(#979)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Add editing posts

* Add checkboxes

* Add ctrl+enter + fix empty posts

* Fix funny bug
---
 Web/Models/Entities/Comment.php               |   8 ++
 Web/Models/Entities/Post.php                  |  11 ++
 Web/Presenters/WallPresenter.php              |  61 +++++++++-
 Web/Presenters/templates/Wall/Post.xml        |   8 ++
 .../templates/components/comment.xml          |  12 +-
 .../components/post/microblogpost.xml         |  23 ++--
 .../templates/components/post/oldpost.xml     |  23 +++-
 Web/routes.yml                                |   2 +
 Web/static/css/main.css                       |  15 +++
 Web/static/css/microblog.css                  |  14 +++
 Web/static/img/edit.png                       | Bin 0 -> 571 bytes
 Web/static/js/al_wall.js                      | 109 +++++++++++++++++-
 locales/en.strings                            |   3 +
 locales/ru.strings                            |   2 +
 14 files changed, 270 insertions(+), 21 deletions(-)
 create mode 100644 Web/static/img/edit.png

diff --git a/Web/Models/Entities/Comment.php b/Web/Models/Entities/Comment.php
index d813d6be..d64a2763 100644
--- a/Web/Models/Entities/Comment.php
+++ b/Web/Models/Entities/Comment.php
@@ -90,4 +90,12 @@ class Comment extends Post
     {
         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();
+    }
 }
diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php
index 42941901..2d323a8e 100644
--- a/Web/Models/Entities/Post.php
+++ b/Web/Models/Entities/Post.php
@@ -245,6 +245,17 @@ class Post extends Postable
         $this->unwire();
         $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;
 }
diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php
index 3e115ec7..09392bc3 100644
--- a/Web/Presenters/WallPresenter.php
+++ b/Web/Presenters/WallPresenter.php
@@ -3,7 +3,7 @@ namespace openvk\Web\Presenters;
 use openvk\Web\Models\Exceptions\TooMuchOptionsException;
 use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User};
 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 Nette\InvalidStateException as ISE;
 use Bhaktaraz\RSSGenerator\Item;
@@ -498,4 +498,63 @@ final class WallPresenter extends OpenVKPresenter
         # TODO localize message based on language and ?act=(un)pin
         $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()
+                        ]]);
+    }
 }
diff --git a/Web/Presenters/templates/Wall/Post.xml b/Web/Presenters/templates/Wall/Post.xml
index 575c7bba..8ac11bb7 100644
--- a/Web/Presenters/templates/Wall/Post.xml
+++ b/Web/Presenters/templates/Wall/Post.xml
@@ -34,6 +34,14 @@
         {/if}
         
         <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>
     </div>
     <script n:if="$canReport ?? false">
diff --git a/Web/Presenters/templates/components/comment.xml b/Web/Presenters/templates/components/comment.xml
index 714893d1..fb8fb244 100644
--- a/Web/Presenters/templates/components/comment.xml
+++ b/Web/Presenters/templates/components/comment.xml
@@ -20,7 +20,7 @@
                 </div>
                 <div class="post-content" id="{$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 class="attachment" n:foreach="$comment->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
@@ -29,17 +29,17 @@
                         </div>
                     </div>
                     <div n:if="isset($thisUser) &&! ($compact ?? false)" class="post-menu">
-                        <a
-                            href="{=$linkWithPost && get_class($comment->getTarget()) == 'openvk\Web\Models\Entities\Post' ? '/wall' . $comment->getTarget()->getPrettyId() : ''}#_comment{$comment->getId()}"
-                            class="date"
-                        >
-                            {$comment->getPublicationTime()}
+                        <a href="#_comment{$comment->getId()}" class="date">{$comment->getPublicationTime()} 
+                            <span n:if="$comment->getEditTime()" class="edited editedMark">({_edited_short})</span>
                         </a>
                         {if !$timeOnly}
                             &nbsp;|
                             {if $comment->canBeDeletedBy($thisUser)}
                                 <a href="/comment{$comment->getId()}/delete">{_delete}</a>&nbsp;|
                             {/if}
+                            {if $comment->canBeEditedBy($thisUser)}
+                                <a id="editPost" data-id="{$comment->getId()}">{_edit}</a>&nbsp;|
+                            {/if}
                             <a class="comment-reply">{_reply}</a>
                             {if $thisUser->getId() != $comment->getOwner()->getId()}
                                 {var $canReport = true}
diff --git a/Web/Presenters/templates/components/post/microblogpost.xml b/Web/Presenters/templates/components/post/microblogpost.xml
index 98a41d72..c8cd2a12 100644
--- a/Web/Presenters/templates/components/post/microblogpost.xml
+++ b/Web/Presenters/templates/components/post/microblogpost.xml
@@ -18,13 +18,13 @@
         <tr>
             <td width="54" valign="top">
                 <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>
                 </a>
             </td>
             <td width="100%" valign="top">
                 <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">
                     {$post->isDeactivationMessage() ? ($author->isFemale() ? tr($deac . "_f") : tr($deac . "_m"))}
                     {$post->isUpdateAvatarMessage() && !$post->isPostedOnBehalfOfGroup() ? ($author->isFemale() ? tr("upd_f") : tr("upd_m"))}
@@ -62,11 +62,18 @@
                             <a class="pin" href="/wall{$post->getPrettyId()}/pin?act=pin&hash={rawurlencode($csrfToken)}"></a>
                         {/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 class="post-content" id="{$post->getPrettyId()}">
-                    <div class="text" id="text{$post->getPrettyId()}">
-                        {$post->getText()|noescape}
-                        
+                    <div class="text">
+                        <span class="really_text">{$post->getText()|noescape}</span>
+
                         <div n:ifcontent class="attachments_b">
                             <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
                                 {include "../attachment.xml", attachment => $attachment}
@@ -88,13 +95,15 @@
                     </div>
                 </div>
                 <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']}">
                         <img src="/assets/packages/static/openvk/img/app_icons_mini/{$post->getPlatform(this)}.svg">
                     </a>
                     {if isset($thisUser)}
                         &nbsp;
-                        
+
                         <a n:if="!($forceNoCommentsLink ?? false) && $commentsCount == 0" href="javascript:expand_comment_textarea({$commentTextAreaId})">{_comment}</a>
                         
                         <div class="like_wrap">
diff --git a/Web/Presenters/templates/components/post/oldpost.xml b/Web/Presenters/templates/components/post/oldpost.xml
index c893e289..ad7896f2 100644
--- a/Web/Presenters/templates/components/post/oldpost.xml
+++ b/Web/Presenters/templates/components/post/oldpost.xml
@@ -7,18 +7,20 @@
     {var $deac = "post_deact_silent"}
 {/if}
 
+
+
 <table border="0" style="font-size: 11px;" n:class="post, $post->isExplicit() ? post-nsfw">
     <tbody>
         <tr>
             <td width="54" valign="top">
                 <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>
                 </a>
             </td>
             <td width="100%" valign="top">
                 <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">
                     {if $post->isDeactivationMessage()}
                         {$author->isFemale() ? tr($deac . "_f") : tr($deac . "_m")}
@@ -51,16 +53,18 @@
                     {/if}
                     <br/>
                     <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']}">
                             <img src="/assets/packages/static/openvk/img/app_icons_mini/{$post->getPlatform(this)}.svg">
                         </a>
                     </a>
                 </div>
                 <div class="post-content" id="{$post->getPrettyId()}">
-                    <div class="text" id="text{$post->getPrettyId()}">
-                        {$post->getText()|noescape}
-                        
+                    <div class="text">
+                        {var $owner = $author->getId()}
+
+                        <span class="really_text">{$post->getText()|noescape}</span>
+
                         <div n:ifcontent class="attachments_b">
                             <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
                                 {include "../attachment.xml", attachment => $attachment}
@@ -87,6 +91,13 @@
                         {var $forceNoPinLink = true}
                     {/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)}
                         <a href="/wall{$post->getPrettyId()}/delete">{_delete}</a> &nbsp;|&nbsp;
                     {/if}
diff --git a/Web/routes.yml b/Web/routes.yml
index effa68eb..bc95f44a 100644
--- a/Web/routes.yml
+++ b/Web/routes.yml
@@ -129,6 +129,8 @@ routes:
       handler: "Wall->rss"
     - url: "/wall{num}/makePost"
       handler: "Wall->makePost"
+    - url: "/wall/edit"
+      handler: "Wall->edit"
     - url: "/wall{num}_{num}"
       handler: "Wall->post"
     - url: "/wall{num}_{num}/like"
diff --git a/Web/static/css/main.css b/Web/static/css/main.css
index f05d0a2b..46ad74b0 100644
--- a/Web/static/css/main.css
+++ b/Web/static/css/main.css
@@ -2702,6 +2702,10 @@ body.article .floating_sidebar, body.article .page_content {
     font-size: 12px;
 }
 
+.edited {
+    color: #9b9b9b;
+}
+
 .uploadedImage img {
     max-height: 76px;
     object-fit: cover;
@@ -2713,6 +2717,16 @@ body.article .floating_sidebar, body.article .page_content {
     user-select: none;
 }
 
+.editMenu.loading {
+    filter: opacity(0.5);
+    cursor: progress;
+    user-select: none;
+}
+
+.editMenu.loading * {
+    pointer-events: none;
+}
+
 .lagged * {
     pointer-events: none;
 }
@@ -2797,3 +2811,4 @@ body.article .floating_sidebar, body.article .page_content {
 .smallFrame:hover {
     background: #E9F0F1 !important;
 }
+
diff --git a/Web/static/css/microblog.css b/Web/static/css/microblog.css
index bf5d0d53..503af42a 100644
--- a/Web/static/css/microblog.css
+++ b/Web/static/css/microblog.css
@@ -110,10 +110,24 @@
     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 {
     opacity: 0.4;
 }
 
+.post-author .edit:hover {
+    opacity: 0.4;
+}
+
 .expand_button {
     background-color: #eee;
     width: 100%;
diff --git a/Web/static/img/edit.png b/Web/static/img/edit.png
new file mode 100644
index 0000000000000000000000000000000000000000..b3474d0f7a5dbe1221e549119fbfaf687899c81c
GIT binary patch
literal 571
zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s3?%0jwTl2LmUKs7M+SzC^Add=zXSOS$sR$z
z3=Hi83=BO&3=DsP>_$5VhRD|pj6(bj3^vag7$nc_I;mvDz`&@O>FgZf>Flf!P?VpR
znUl)Epm9DqA;GAiq_8MeC?Vm*S#3?OqVk}Qj-H?d`|}5m9XP|l)_L+MYab({okCe$
zU7Qv(+mEu|MnRYOYpgSmI!Hb6($mmlyj0(CLqm0|#mvUWiwq~u>FH}SbQzyx+~hiG
z)4?~q3T1&F6O1R+vYdG4-M(Rc!wpY?Z^8n*IbLuo{O0zkwVHAE2%{PMp=pd8y@RwJ
zz8P6O7Y`9jU6wvYLn10FO3Px|6ZNH9Ea76)mpl{Gu6U;UTwL^txazbe%NVXY-C<{I
z5@=l@aG4=%%Y~E$Mhr)pTfSNf$n5J$N*74tm=F^c!j@`eBq{t%`OFOEg9je$pVHX)
zl;7#hlIe|!jeN|^|3g}4%u-}-Zm?rMyw8&1z%l>7`IDZq00UMfz$e5tyLaP(^Y<RV
z{7^n^dv4#R?q!EQeEwcFW9R*6?`}SNy>ib*pu!G5kvt&9QxfDC{2u`rgzld^2NdTl
z@Q5r1MxrnXGcwGYBLNh&@pN$vkqCD^<tWymz;on6g6o8x|7(x>)uw$FcNF!V-xzl9
z&fhP|%evS1TkKu5E?ecON{RZ>ro66*%ai2pUuW(YIr-xq&>RL&S3j3^P6<r_()Qi*

literal 0
HcmV?d00001

diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js
index bb349c14..9b54adce 100644
--- a/Web/static/js/al_wall.js
+++ b/Web/static/js/al_wall.js
@@ -262,4 +262,111 @@ async function showArticle(note_id) {
     u("#articleText").html(`<h1 class="articleView_nameHeading">${note.title}</h1>` + note.html);
     u("body").removeClass("dimmed");
     u("body").addClass("article");
-}
\ No newline at end of file
+}
+
+$(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"
+    }
+})
diff --git a/locales/en.strings b/locales/en.strings
index 780da8b3..f6e325d5 100644
--- a/locales/en.strings
+++ b/locales/en.strings
@@ -214,6 +214,8 @@
 
 "reply" = "Reply";
 
+"edited_short" = "edited";
+
 /* Friends */
 
 "friends" = "Friends";
@@ -1149,6 +1151,7 @@
 "warn_user_action" = "Warn user";
 "ban_in_support_user_action" = "Ban in support";
 "unban_in_support_user_action" = "Unban in support";
+"changes_history" = "Editing history";
 
 /* Admin panel */
 
diff --git a/locales/ru.strings b/locales/ru.strings
index 2364175a..b66a5404 100644
--- a/locales/ru.strings
+++ b/locales/ru.strings
@@ -191,6 +191,7 @@
 "version_incompatibility" = "Не удалось отобразить это вложение. Возможно, база данных несовместима с текущей версией OpenVK.";
 "graffiti" = "Граффити";
 "reply" = "Ответить";
+"edited_short" = "ред.";
 
 /* Friends */
 
@@ -1049,6 +1050,7 @@
 "warn_user_action" = "Предупредить пользователя";
 "ban_in_support_user_action" = "Заблокировать в поддержке";
 "unban_in_support_user_action" = "Разблокировать в поддержке";
+"changes_history" = "История редактирования";
 
 /* Admin panel */
 

From 06f324f98cf985631e3aeb833eb9087e399fbf28 Mon Sep 17 00:00:00 2001
From: lalka2018 <99399973+lalka2016@users.noreply.github.com>
Date: Sat, 16 Sep 2023 19:14:23 +0300
Subject: [PATCH 13/26] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=D1=8B=20=D0=B4?=
 =?UTF-8?q?=D0=BB=D1=8F=20#980=20=D0=B8=20#979=20(#982)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Что я должен здесь сказать?

* playerock

* Copypaste
---
 Web/Models/Entities/Post.php                  |  3 +++
 Web/Models/Entities/Postable.php              |  4 +--
 Web/Presenters/WallPresenter.php              |  1 +
 .../templates/Photos/UploadPhoto.xml          | 10 ++++---
 .../templates/components/comment.xml          |  6 ++---
 .../components/post/microblogpost.xml         |  2 +-
 .../templates/components/post/oldpost.xml     |  2 +-
 Web/static/js/al_photos.js                    | 27 ++++++++++++++++---
 Web/static/js/al_wall.js                      |  3 ++-
 9 files changed, 43 insertions(+), 15 deletions(-)

diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php
index 2d323a8e..8c38c567 100644
--- a/Web/Models/Entities/Post.php
+++ b/Web/Models/Entities/Post.php
@@ -254,6 +254,9 @@ class Post extends Postable
         if($this->isDeactivationMessage() || $this->isUpdateAvatarMessage())
             return false;
 
+        if($this->getTargetWall() > 0)
+            return $this->getPublicationTime()->timestamp() + WEEK > time();
+
         return $user->getId() == $this->getOwner(false)->getId();
     }
     
diff --git a/Web/Models/Entities/Postable.php b/Web/Models/Entities/Postable.php
index 23981cc1..ffbf480c 100644
--- a/Web/Models/Entities/Postable.php
+++ b/Web/Models/Entities/Postable.php
@@ -167,9 +167,9 @@ abstract class Postable extends Attachable
                 $this->stateChanges("created", time());
             
             $this->stateChanges("virtual_id", $pCount + 1);
-        } else {
+        } /*else {
             $this->stateChanges("edited", time());
-        }
+        }*/
         
         parent::save();
     }
diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php
index 09392bc3..96b61b48 100644
--- a/Web/Presenters/WallPresenter.php
+++ b/Web/Presenters/WallPresenter.php
@@ -552,6 +552,7 @@ final class WallPresenter extends OpenVKPresenter
                         "nsfw"        => $this->postParam("type") === "post" ? (int)$post->isExplicit() : 0,
                         "from_group"  => $this->postParam("type") === "post" && $post->getTargetWall() < 0 ?
                         ((int)$post->isPostedOnBehalfOfGroup()) : "false",
+                        "new_text"    => $post->getText(false),
                         "author"      => [
                             "name"    => $post->getOwner()->getCanonicalName(),
                             "avatar"  => $post->getOwner()->getAvatarUrl()
diff --git a/Web/Presenters/templates/Photos/UploadPhoto.xml b/Web/Presenters/templates/Photos/UploadPhoto.xml
index 6ea987cb..ab81d3aa 100644
--- a/Web/Presenters/templates/Photos/UploadPhoto.xml
+++ b/Web/Presenters/templates/Photos/UploadPhoto.xml
@@ -2,9 +2,13 @@
 {block title}{_upload_photo}{/block}
 
 {block header}
-    <a href="{$thisUser->getURL()}">{$thisUser->getCanonicalName()}</a>
+    <a href="{$album->getOwner()->getURL()}">{$album->getOwner()->getCanonicalName()}</a>
     » 
-    <a href="/albums{$thisUser->getId()}">{_albums}</a>
+    {if $album->getOwner() instanceof openvk\Web\Models\Entities\Club}
+        <a href="/albums{$album->getOwner()->getId() * -1}">{_albums}</a>
+    {else}
+        <a href="/albums{$album->getOwner()->getId()}">{_albums}</a>
+    {/if}
     » 
     <a href="/album{$album->getPrettyId()}">{$album->getName()}</a>
     » 
@@ -23,7 +27,7 @@
 
     <input type="file" accept=".jpg,.png,.gif" name="files[]" multiple class="button" id="uploadButton" style="display:none">
 
-    <div class="container_gray" style="height: 344px;">
+    <div class="container_gray" style="min-height: 344px;">
         <div class="insertThere"></div>
         <div class="whiteBox" style="display: block;">
             <div class="boxContent">
diff --git a/Web/Presenters/templates/components/comment.xml b/Web/Presenters/templates/components/comment.xml
index fb8fb244..69238417 100644
--- a/Web/Presenters/templates/components/comment.xml
+++ b/Web/Presenters/templates/components/comment.xml
@@ -8,19 +8,19 @@
         <tr>
             <td width="30" valign="top">
                 <a href="{$author->getURL()}">
-                    <img src="{$author->getAvatarURL('miniscule')}" width="30" class="cCompactAvatars" />
+                    <img src="{$author->getAvatarURL('miniscule')}" width="30" class="cCompactAvatars post-avatar" />
                 </a>
             </td>
             <td width="100%" valign="top">
                 <div class="post-author">
-                    <a href="{$author->getURL()}"><b>
+                    <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"><br/>
                 </div>
                 <div class="post-content" id="{$comment->getId()}">
                     <div class="text" id="text{$comment->getId()}">
-                        <span class="really_text">{$comment->getText()|noescape}</span>
+                        <span data-text="{$comment->getText(false)}" class="really_text">{$comment->getText()|noescape}</span>
                         
                         <div n:ifcontent class="attachments_b">
                             <div class="attachment" n:foreach="$comment->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
diff --git a/Web/Presenters/templates/components/post/microblogpost.xml b/Web/Presenters/templates/components/post/microblogpost.xml
index c8cd2a12..6b0444c7 100644
--- a/Web/Presenters/templates/components/post/microblogpost.xml
+++ b/Web/Presenters/templates/components/post/microblogpost.xml
@@ -72,7 +72,7 @@
                 </div>
                 <div class="post-content" id="{$post->getPrettyId()}">
                     <div class="text">
-                        <span class="really_text">{$post->getText()|noescape}</span>
+                        <span data-text="{$post->getText(false)}" class="really_text">{$post->getText()|noescape}</span>
 
                         <div n:ifcontent class="attachments_b">
                             <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
diff --git a/Web/Presenters/templates/components/post/oldpost.xml b/Web/Presenters/templates/components/post/oldpost.xml
index ad7896f2..6e0b4c53 100644
--- a/Web/Presenters/templates/components/post/oldpost.xml
+++ b/Web/Presenters/templates/components/post/oldpost.xml
@@ -63,7 +63,7 @@
                     <div class="text">
                         {var $owner = $author->getId()}
 
-                        <span class="really_text">{$post->getText()|noescape}</span>
+                        <span data-text="{$post->getText(false)}" class="really_text">{$post->getText()|noescape}</span>
 
                         <div n:ifcontent class="attachments_b">
                             <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
diff --git a/Web/static/js/al_photos.js b/Web/static/js/al_photos.js
index 59965c09..b4632d60 100644
--- a/Web/static/js/al_photos.js
+++ b/Web/static/js/al_photos.js
@@ -6,6 +6,18 @@ $(document).on("change", "#uploadButton", (e) => {
         return;
     }
 
+    for(const file of e.currentTarget.files) {
+        if(!file.type.startsWith('image/')) {
+            MessageBox(tr("error"), tr("only_images_accepted", escapeHtml(file.name)), [tr("ok")], [() => {Function.noop}])
+            return;
+        }
+
+        if(file.size > 5 * 1024 * 1024) {
+            MessageBox(tr("error"), tr("max_filesize", 5), [tr("ok")], [() => {Function.noop}])
+            return;
+        }
+    }
+
     if(document.querySelector(".whiteBox").style.display == "block") {
         document.querySelector(".whiteBox").style.display = "none"
         document.querySelector(".insertThere").append(document.getElementById("fakeButton"));
@@ -142,23 +154,23 @@ $(document).on("dragover drop", (e) => {
     return false;
 })
 
-$(document).on("dragover", (e) => {
+$(".container_gray").on("dragover", (e) => {
     e.preventDefault()
     document.querySelector("#fakeButton").classList.add("dragged")
     document.querySelector("#fakeButton").value = tr("drag_files_here")
 })
 
-$(document).on("dragleave", (e) => {
+$(".container_gray").on("dragleave", (e) => {
     e.preventDefault()
     document.querySelector("#fakeButton").classList.remove("dragged")
     document.querySelector("#fakeButton").value = tr("upload_picts")
 })
 
-$("#fakeButton").on("drop", (e) => {
+$(".container_gray").on("drop", (e) => {
     e.originalEvent.dataTransfer.dropEffect = 'move';
     e.preventDefault()
 
-    $(document).trigger("dragleave")
+    $(".container_gray").trigger("dragleave")
 
     let files = e.originalEvent.dataTransfer.files
 
@@ -177,3 +189,10 @@ $("#fakeButton").on("drop", (e) => {
     document.getElementById("uploadButton").files = files
     u("#uploadButton").trigger("change")
 })
+
+u(".container_gray").on("paste", (e) => {
+    if(e.clipboardData.files.length > 0 && e.clipboardData.files.length < 10) {
+        document.getElementById("uploadButton").files = e.clipboardData.files;
+        u("#uploadButton").trigger("change")
+    }
+})
diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js
index 9b54adce..4c8bb933 100644
--- a/Web/static/js/al_wall.js
+++ b/Web/static/js/al_wall.js
@@ -273,7 +273,7 @@ $(document).on("click", "#editPost", (e) => {
         content.insertAdjacentHTML("afterbegin", `
             <div class="editMenu">
                 <div id="wall-post-input999"> 
-                    <textarea id="new_content">${text.innerHTML.replace(/(<([^>]+)>)/gi, '')}</textarea>
+                    <textarea id="new_content">${text.dataset.text}</textarea>
                     <input type="button" class="button" value="${tr("save")}" id="endEditing">
                     <input type="button" class="button" value="${tr("cancel")}" id="cancelEditing">
                 </div>
@@ -343,6 +343,7 @@ $(document).on("click", "#editPost", (e) => {
 
                     post.querySelector(".post-avatar").setAttribute("src", result.author.avatar)
                     post.querySelector(".post-author-name").innerHTML = result.author.name
+                    post.querySelector(".really_text").setAttribute("data-text", result.new_text)
                 } else {
                     MessageBox(tr("error"), result.error, [tr("ok")], [Function.noop])
                     post.querySelector("#editPost").click()

From 293993653441ef367880a590035a02df9a84854c Mon Sep 17 00:00:00 2001
From: lalka2018 <99399973+lalka2016@users.noreply.github.com>
Date: Sat, 16 Sep 2023 19:51:36 +0300
Subject: [PATCH 14/26] Update Post.php (#983)

---
 Web/Models/Entities/Post.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php
index 8c38c567..6d0fe8cf 100644
--- a/Web/Models/Entities/Post.php
+++ b/Web/Models/Entities/Post.php
@@ -255,7 +255,7 @@ class Post extends Postable
             return false;
 
         if($this->getTargetWall() > 0)
-            return $this->getPublicationTime()->timestamp() + WEEK > time();
+            return $this->getPublicationTime()->timestamp() + WEEK > time() && $user->getId() == $this->getOwner(false)->getId();
 
         return $user->getId() == $this->getOwner(false)->getId();
     }

From 0ef413a5b9554b0742bb16cf8ece92dbfb39f511 Mon Sep 17 00:00:00 2001
From: n1rwana <aydashkin@vk.com>
Date: Sun, 17 Sep 2023 00:56:36 +0300
Subject: [PATCH 15/26] Ability to hide "My applications" from the menu (#937)

---
 Web/Models/Entities/User.php               |  2 ++
 Web/Presenters/UserPresenter.php           |  1 +
 Web/Presenters/templates/@layout.xml       |  2 +-
 Web/Presenters/templates/User/Settings.xml | 10 ++++++++++
 4 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php
index e5b8da06..aaf00ec9 100644
--- a/Web/Models/Entities/User.php
+++ b/Web/Models/Entities/User.php
@@ -462,6 +462,7 @@ class User extends RowModel
                 "news",
                 "links",
                 "poster",
+                "apps"
             ],
         ])->get($id);
     }
@@ -1026,6 +1027,7 @@ class User extends RowModel
                 "news",
                 "links",
                 "poster",
+                "apps"
             ],
         ])->set($id, (int) $status)->toInteger();
 
diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php
index 9cfa3654..46aa7e92 100644
--- a/Web/Presenters/UserPresenter.php
+++ b/Web/Presenters/UserPresenter.php
@@ -481,6 +481,7 @@ final class UserPresenter extends OpenVKPresenter
                     "menu_novajoj"   => "news",
                     "menu_ligiloj"   => "links",
                     "menu_standardo" => "poster",
+                    "menu_aplikoj"   => "apps"
                 ];
                 foreach($settings as $checkbox => $setting)
                     $user->setLeftMenuItemStatus($setting, $this->checkbox($checkbox));
diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml
index 3d724cf5..f37532a7 100644
--- a/Web/Presenters/templates/@layout.xml
+++ b/Web/Presenters/templates/@layout.xml
@@ -196,7 +196,7 @@
                                     (<b>{$thisUser->getNotificationsCount()}</b>)
                                 {/if}
                             </a>
-                            <a href="/apps?act=installed" class="link">{_my_apps}</a>
+                            <a n:if="$thisUser->getLeftMenuItemStatus('apps')" href="/apps?act=installed" class="link">{_my_apps}</a>
                             <a href="/settings" class="link">{_my_settings}</a>
                             
                             {var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)}
diff --git a/Web/Presenters/templates/User/Settings.xml b/Web/Presenters/templates/User/Settings.xml
index e61f900d..9d9a2b25 100644
--- a/Web/Presenters/templates/User/Settings.xml
+++ b/Web/Presenters/templates/User/Settings.xml
@@ -650,6 +650,16 @@
                         <td>
                             <span class="nobold">{_my_feed}</span>
                         </td>
+                    </tr><tr>
+                        <td width="120" valign="top" align="right" align="right">
+                            <input
+                                  n:attr="checked => $user->getLeftMenuItemStatus('apps')"
+                                  type="checkbox"
+                                  name="menu_aplikoj" />
+                        </td>
+                        <td>
+                            <span class="nobold">{_my_apps}</span>
+                        </td>
                     </tr><tr n:if="sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0">
                         <td width="120" valign="top" align="right" align="right">
                             <input

From 468eba80bdd4d8931280a229f7db3e4a162c058a Mon Sep 17 00:00:00 2001
From: lalka2018 <99399973+lalka2016@users.noreply.github.com>
Date: Sun, 17 Sep 2023 16:22:59 +0300
Subject: [PATCH 16/26] Locales: Make more strings translatable (#961)

---
 Web/Models/Entities/Club.php                  |   6 +-
 Web/Presenters/AdminPresenter.php             |   8 +-
 Web/Presenters/CommentPresenter.php           |  18 +-
 Web/Presenters/GiftsPresenter.php             |  10 +-
 Web/Presenters/GroupPresenter.php             |  30 +--
 Web/Presenters/NotesPresenter.php             |   6 +-
 Web/Presenters/PhotosPresenter.php            |  28 +--
 Web/Presenters/ReportPresenter.php            |  12 +-
 Web/Presenters/TopicsPresenter.php            |   4 +-
 Web/Presenters/UserPresenter.php              |  10 +-
 Web/Presenters/VideosPresenter.php            |  16 +-
 Web/Presenters/WallPresenter.php              |   2 +-
 Web/Presenters/templates/@layout.xml          |   5 +-
 Web/Presenters/templates/About/BB.xml         |   8 +-
 Web/Presenters/templates/About/Help.xml       |   2 +-
 Web/Presenters/templates/About/Sandbox.xml    |   2 +-
 .../templates/Admin/BansHistory.xml           |  18 +-
 Web/Presenters/templates/Admin/Logs.xml       |  36 +--
 Web/Presenters/templates/Apps/Play.xml        |  14 +-
 Web/Presenters/templates/Away/View.xml        |   4 +-
 Web/Presenters/templates/Group/Followers.xml  |   4 +-
 Web/Presenters/templates/Group/Statistics.xml |   8 +-
 Web/Presenters/templates/Group/View.xml       |  16 +-
 Web/Presenters/templates/NoSpam/Index.xml     |  50 ++--
 Web/Presenters/templates/NoSpam/Tabs.xml      |   6 +-
 Web/Presenters/templates/NoSpam/Templates.xml |  20 +-
 Web/Presenters/templates/Notes/Create.xml     |   2 +-
 Web/Presenters/templates/Notes/Edit.xml       |   2 +-
 Web/Presenters/templates/Photos/Album.xml     |   5 +-
 Web/Presenters/templates/Photos/AlbumList.xml |   2 +-
 Web/Presenters/templates/Photos/EditAlbum.xml |   2 +-
 Web/Presenters/templates/Photos/EditPhoto.xml |   2 +-
 Web/Presenters/templates/Photos/Photo.xml     |  13 +-
 .../templates/Photos/UnlinkPhoto.xml          |  10 +-
 Web/Presenters/templates/User/Edit.xml        |   4 +-
 Web/Presenters/templates/User/Settings.xml    |   8 +-
 Web/Presenters/templates/User/VerifyPhone.xml |   6 +-
 Web/Presenters/templates/User/View.xml        |  16 +-
 Web/Presenters/templates/User/banned.xml      |   6 +-
 Web/Presenters/templates/Videos/Edit.xml      |   6 +-
 Web/Presenters/templates/Videos/View.xml      |  14 +-
 Web/Presenters/templates/Wall/Post.xml        |  12 +-
 .../templates/components/comment.xml          |  14 +-
 .../components/notifications/9601/_20_18_.xml |   2 +-
 .../components/post/microblogpost.xml         |   2 +-
 .../templates/components/post/oldpost.xml     |   2 +-
 locales/en.strings                            | 228 ++++++++++++++++++
 locales/ru.strings                            | 226 +++++++++++++++++
 48 files changed, 688 insertions(+), 239 deletions(-)

diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php
index 31485129..fbdc503b 100644
--- a/Web/Models/Entities/Club.php
+++ b/Web/Models/Entities/Club.php
@@ -224,7 +224,7 @@ class Club extends RowModel
                     "shape" => "spline",
                     "color" => "#597da3",
                 ],
-                "name" => $unique ? "Полный охват" : "Все просмотры",
+                "name" => $unique ? tr("full_coverage") : tr("all_views"),
             ],
             "subs"  => [
                 "x" => array_reverse(range(1, 7)),
@@ -235,7 +235,7 @@ class Club extends RowModel
                     "color" => "#b05c91",
                 ],
                 "fill" => "tozeroy",
-                "name" => $unique ? "Охват подписчиков" : "Просмотры подписчиков",
+                "name" => $unique ? tr("subs_coverage") : tr("subs_views"),
             ],
             "viral" => [
                 "x" => array_reverse(range(1, 7)),
@@ -246,7 +246,7 @@ class Club extends RowModel
                     "color" => "#4d9fab",
                 ],
                 "fill" => "tozeroy",
-                "name" => $unique ? "Виральный охват" : "Виральные просмотры",
+                "name" => $unique ? tr("viral_coverage") : tr("viral_views"),
             ],
         ];
     }
diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php
index 37fb50e8..14fbbc74 100644
--- a/Web/Presenters/AdminPresenter.php
+++ b/Web/Presenters/AdminPresenter.php
@@ -283,7 +283,7 @@ final class AdminPresenter extends OpenVKPresenter
                     $this->notFound();
                 
                 $gift->delete();
-                $this->flashFail("succ", "Gift moved successfully", "This gift will now be in <b>Recycle Bin</b>.");
+                $this->flashFail("succ", tr("admin_gift_moved_successfully"), tr("admin_gift_moved_to_recycle"));
                 break;
             case "copy":
             case "move":
@@ -302,7 +302,7 @@ final class AdminPresenter extends OpenVKPresenter
                 $catTo->addGift($gift);
                 
                 $name = $catTo->getName();
-                $this->flash("succ", "Gift moved successfully", "This gift will now be in <b>$name</b>.");
+                $this->flash("succ", tr("admin_gift_moved_successfully"), "This gift will now be in <b>$name</b>.");
                 $this->redirect("/admin/gifts/" . $catTo->getSlug() . "." . $catTo->getId() . "/");
                 break;
             default:
@@ -333,10 +333,10 @@ final class AdminPresenter extends OpenVKPresenter
                 $gift->setUsages((int) $this->postParam("usages"));
                 if(isset($_FILES["pic"]) && $_FILES["pic"]["error"] === UPLOAD_ERR_OK) {
                     if(!$gift->setImage($_FILES["pic"]["tmp_name"]))
-                        $this->flashFail("err", "Не удалось сохранить подарок", "Изображение подарка кривое.");
+                        $this->flashFail("err", tr("error_when_saving_gift"), tr("error_when_saving_gift_bad_image"));
                 } else if($gen) {
                     # If there's no gift pic but it's newly created
-                    $this->flashFail("err", "Не удалось сохранить подарок", "Пожалуйста, загрузите изображение подарка.");
+                    $this->flashFail("err", tr("error_when_saving_gift"), tr("error_when_saving_gift_no_image"));
                 }
                 
                 $gift->save();
diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php
index cb0efd0d..29c54c78 100644
--- a/Web/Presenters/CommentPresenter.php
+++ b/Web/Presenters/CommentPresenter.php
@@ -55,7 +55,7 @@ final class CommentPresenter extends OpenVKPresenter
             $this->flashFail("err", tr("error"), tr("forbidden"));
 
         if($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading'])
-            $this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator.");
+            $this->flashFail("err", tr("error"), tr("video_uploads_disabled"));
         
         $flags = 0;
         if($this->postParam("as_group") === "on" && !is_null($club) && $club->canBeModifiedBy($this->user->identity))
@@ -66,7 +66,7 @@ final class CommentPresenter extends OpenVKPresenter
             try {
                 $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"]);
             } catch(ISE $ex) {
-                $this->flashFail("err", "Не удалось опубликовать пост", "Файл изображения повреждён, слишком велик или одна сторона изображения в разы больше другой.");
+                $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_when_publishing_comment_description"));
             }
         }
         
@@ -86,11 +86,11 @@ final class CommentPresenter extends OpenVKPresenter
                 $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]);
             }
         } catch(ISE $ex) {
-            $this->flashFail("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик.");
+            $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_file_too_big"));
         }
         
         if(empty($this->postParam("text")) && !$photo && !$video)
-            $this->flashFail("err", "Не удалось опубликовать комментарий", "Комментарий пустой или слишком большой.");
+            $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty"));
         
         try {
             $comment = new Comment;
@@ -102,7 +102,7 @@ final class CommentPresenter extends OpenVKPresenter
             $comment->setFlags($flags);
             $comment->save();
         } catch (\LengthException $ex) {
-            $this->flashFail("err", "Не удалось опубликовать комментарий", "Комментарий слишком большой.");
+            $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_too_big"));
         }
         
         if(!is_null($photo))
@@ -124,7 +124,7 @@ final class CommentPresenter extends OpenVKPresenter
             if($mentionee instanceof User)
                 (new MentionNotification($mentionee, $entity, $comment->getOwner(), strip_tags($comment->getText())))->emit();
         
-        $this->flashFail("succ", "Комментарий добавлен", "Ваш комментарий появится на странице.");
+        $this->flashFail("succ", tr("comment_is_added"), tr("comment_is_added_desc"));
     }
     
     function renderDeleteComment(int $id): void
@@ -135,15 +135,15 @@ final class CommentPresenter extends OpenVKPresenter
         $comment = (new Comments)->get($id);
         if(!$comment) $this->notFound();
         if(!$comment->canBeDeletedBy($this->user->identity))
-            $this->throwError(403, "Forbidden", "У вас недостаточно прав чтобы редактировать этот ресурс.");
+            $this->throwError(403, "Forbidden", tr("error_access_denied"));
         if ($comment->getTarget() instanceof Post && $comment->getTarget()->getWallOwner()->isBanned())
             $this->flashFail("err", tr("error"), tr("forbidden"));
 
         $comment->delete();
         $this->flashFail(
             "succ",
-            "Успешно",
-            "Этот комментарий больше не будет показыватся.<br/><a href='/al_comments/spam?$id'>Отметить как спам</a>?"
+            tr("success"),
+            tr("comment_will_not_appear")
         );
     }
 }
diff --git a/Web/Presenters/GiftsPresenter.php b/Web/Presenters/GiftsPresenter.php
index 8f59bdcb..71480540 100644
--- a/Web/Presenters/GiftsPresenter.php
+++ b/Web/Presenters/GiftsPresenter.php
@@ -49,7 +49,7 @@ final class GiftsPresenter extends OpenVKPresenter
         $user = $this->users->get((int) ($this->queryParam("user") ?? 0));
         $cat  = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0));
         if(!$user || !$cat)
-            $this->flashFail("err", "Не удалось подарить", "Пользователь или набор не существуют.");
+            $this->flashFail("err", tr("error_when_gifting"), tr("error_user_not_exists"));
         
         $this->template->page = $page = (int) ($this->queryParam("p") ?? 1);
         $gifts = $cat->getGifts($page, null, $this->template->count);
@@ -66,14 +66,14 @@ final class GiftsPresenter extends OpenVKPresenter
         $gift = $this->gifts->get((int) ($this->queryParam("elid") ?? 0));
         $cat  = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0));
         if(!$user || !$cat || !$gift || !$cat->hasGift($gift))
-            $this->flashFail("err", "Не удалось подарить", "Не удалось подтвердить права на подарок.");
+            $this->flashFail("err", tr("error_when_gifting"), tr("error_no_rights_gifts"));
         
         if(!$gift->canUse($this->user->identity))
-            $this->flashFail("err", "Не удалось подарить", "У вас больше не осталось таких подарков.");
+            $this->flashFail("err", tr("error_when_gifting"), tr("error_no_more_gifts"));
         
         $coinsLeft = $this->user->identity->getCoins() - $gift->getPrice();
         if($coinsLeft < 0)
-            $this->flashFail("err", "Не удалось подарить", "Ору нищ не пук.");
+            $this->flashFail("err", tr("error_when_gifting"), tr("error_no_money"));
         
         $this->template->_template = "Gifts/Confirm.xml";
         if($_SERVER["REQUEST_METHOD"] !== "POST") {
@@ -91,7 +91,7 @@ final class GiftsPresenter extends OpenVKPresenter
         $user->gift($this->user->identity, $gift, $comment, !is_null($this->postParam("anonymous")));
         $gift->used();
         
-        $this->flash("succ", "Подарок отправлен", "Вы отправили подарок <b>" . $user->getFirstName() . "</b> за " . $gift->getPrice() . " голосов.");
+        $this->flash("succ", tr("gift_sent"), tr("gift_sent_desc", $user->getFirstName(), $gift->getPrice()));
         $this->redirect($user->getURL());
     }
     
diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php
index d8fbcb79..d3a46fd5 100644
--- a/Web/Presenters/GroupPresenter.php
+++ b/Web/Presenters/GroupPresenter.php
@@ -54,7 +54,7 @@ final class GroupPresenter extends OpenVKPresenter
                     $club->save();
                 } catch(\PDOException $ex) {
                     if($ex->getCode() == 23000)
-                        $this->flashFail("err", "Ошибка", "Произошла ошибка на стороне сервера. Обратитесь к системному администратору.");
+                        $this->flashFail("err", tr("error"), tr("error_on_server_side"));
                     else
                         throw $ex;
                 }
@@ -62,7 +62,7 @@ final class GroupPresenter extends OpenVKPresenter
                 $club->toggleSubscription($this->user->identity);
                 $this->redirect("/club" . $club->getId());
             }else{
-                $this->flashFail("err", "Ошибка", "Вы не ввели название группы.");
+                $this->flashFail("err", tr("error"), tr("error_no_group_name"));
             }
         }
     }
@@ -132,7 +132,7 @@ final class GroupPresenter extends OpenVKPresenter
             $this->notFound();
         
         if(!$club->canBeModifiedBy($this->user->identity ?? NULL))
-            $this->flashFail("err", "Ошибка доступа", "У вас недостаточно прав, чтобы изменять этот ресурс.");
+            $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
 
         if(!is_null($hidden)) {
             if($club->getOwner()->getId() == $user->getId()) {
@@ -150,9 +150,9 @@ final class GroupPresenter extends OpenVKPresenter
             }
 
             if($hidden) {
-                $this->flashFail("succ", "Операция успешна", "Теперь " . $user->getCanonicalName() . " будет показываться как обычный подписчик всем кроме других администраторов");
+                $this->flashFail("succ", tr("success_action"), tr("x_is_now_hidden", $user->getCanonicalName()));
             } else {
-                $this->flashFail("succ", "Операция успешна", "Теперь все будут знать про то что " . $user->getCanonicalName() . " - администратор");
+                $this->flashFail("succ", tr("success_action"), tr("x_is_now_showed", $user->getCanonicalName()));
             }
         } elseif($removeComment) {
             if($club->getOwner()->getId() == $user->getId()) {
@@ -164,11 +164,11 @@ final class GroupPresenter extends OpenVKPresenter
                 $manager->save();
             }
 
-            $this->flashFail("succ", "Операция успешна", "Комментарий к администратору удален");
+            $this->flashFail("succ", tr("success_action"), tr("comment_is_deleted"));
         } elseif($comment) {
             if(mb_strlen($comment) > 36) {
                 $commentLength = (string) mb_strlen($comment);
-                $this->flashFail("err", "Ошибка", "Комментарий слишком длинный ($commentLength символов вместо 36 символов)");
+                $this->flashFail("err", tr("error"), tr("comment_is_too_long", $commentLength));
             }
 
             if($club->getOwner()->getId() == $user->getId()) {
@@ -180,16 +180,16 @@ final class GroupPresenter extends OpenVKPresenter
                 $manager->save();
             }
 
-            $this->flashFail("succ", "Операция успешна", "Комментарий к администратору изменён");
+            $this->flashFail("succ", tr("success_action"), tr("comment_is_changed"));
         }else{
             if($club->canBeModifiedBy($user)) {
                 $club->removeManager($user);
-                $this->flashFail("succ", "Операция успешна", $user->getCanonicalName() . " более не администратор.");
+                $this->flashFail("succ", tr("success_action"), tr("x_no_more_admin", $user->getCanonicalName()));
             } else {
                 $club->addManager($user);
                 
                 (new ClubModeratorNotification($user, $club, $this->user->identity))->emit();
-                $this->flashFail("succ", "Операция успешна", $user->getCanonicalName() . " назначен(а) администратором.");
+                $this->flashFail("succ", tr("success_action"), tr("x_is_admin", $user->getCanonicalName()));
             }
         }
         
@@ -245,7 +245,7 @@ final class GroupPresenter extends OpenVKPresenter
                     (new Albums)->getClubAvatarAlbum($club)->addPhoto($photo);
                 } catch(ISE $ex) {
                     $name = $album->getName();
-                    $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию.");
+                    $this->flashFail("err", tr("error"), tr("error_when_uploading_photo"));
                 }
             }
             
@@ -253,12 +253,12 @@ final class GroupPresenter extends OpenVKPresenter
                 $club->save();
             } catch(\PDOException $ex) {
                 if($ex->getCode() == 23000)
-                    $this->flashFail("err", "Ошибка", "Произошла ошибка на стороне сервера. Обратитесь к системному администратору.");
+                    $this->flashFail("err", tr("error"), tr("error_on_server_side"));
                 else
                     throw $ex;
             }
             
-            $this->flash("succ", "Изменения сохранены", "Новые данные появятся в вашей группе.");
+            $this->flash("succ", tr("changes_saved"), tr("new_changes_desc"));
         }
     }
     
@@ -298,7 +298,7 @@ final class GroupPresenter extends OpenVKPresenter
 
             } catch(ISE $ex) {
                 $name = $album->getName();
-                $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию.");
+                $this->flashFail("err", tr("error"), tr("error_when_uploading_photo"));
             }
         }
         $this->returnJson([
@@ -350,7 +350,7 @@ final class GroupPresenter extends OpenVKPresenter
         $this->assertUserLoggedIn();
         
         if(!eventdb())
-            $this->flashFail("err", "Ошибка подключения", "Не удалось подключится к службе телеметрии.");
+            $this->flashFail("err", tr("connection_error"), tr("connection_error_desc"));
         
         $club = $this->clubs->get($id);
         if(!$club->canBeModifiedBy($this->user->identity))
diff --git a/Web/Presenters/NotesPresenter.php b/Web/Presenters/NotesPresenter.php
index 50437ad7..4b71c8b1 100644
--- a/Web/Presenters/NotesPresenter.php
+++ b/Web/Presenters/NotesPresenter.php
@@ -107,7 +107,7 @@ final class NotesPresenter extends OpenVKPresenter
         if(!$note || $note->getOwner()->getId() !== $owner || $note->isDeleted())
             $this->notFound();
         if(is_null($this->user) || !$note->canBeModifiedBy($this->user->identity))
-            $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
+            $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
         $this->template->note = $note;
             
         if($_SERVER["REQUEST_METHOD"] === "POST") {
@@ -135,11 +135,11 @@ final class NotesPresenter extends OpenVKPresenter
         if(!$note) $this->notFound();
         if($note->getOwner()->getId() . "_" . $note->getId() !== $owner . "_" . $id || $note->isDeleted()) $this->notFound();
         if(is_null($this->user) || !$note->canBeModifiedBy($this->user->identity))
-            $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
+            $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
         
         $name = $note->getName();
         $note->delete();
-        $this->flash("succ", "Заметка удалена", "Заметка \"$name\" была успешно удалена.");
+        $this->flash("succ", tr("note_is_deleted"), tr("note_x_is_now_deleted", $name));
         $this->redirect("/notes" . $this->user->id);
     }
 }
diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php
index 11026d00..bef984b7 100644
--- a/Web/Presenters/PhotosPresenter.php
+++ b/Web/Presenters/PhotosPresenter.php
@@ -94,7 +94,7 @@ final class PhotosPresenter extends OpenVKPresenter
         if(!$album) $this->notFound();
         if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound();
         if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity) || $album->isDeleted())
-            $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
+            $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
         $this->template->album = $album;
         
         if($_SERVER["REQUEST_METHOD"] === "POST") {
@@ -106,7 +106,7 @@ final class PhotosPresenter extends OpenVKPresenter
             $album->setEdited(time());
             $album->save();
             
-            $this->flash("succ", "Изменения сохранены", "Новые данные приняты.");
+            $this->flash("succ", tr("changes_saved"), tr("new_data_accepted"));
         }
     }
     
@@ -120,13 +120,13 @@ final class PhotosPresenter extends OpenVKPresenter
         if(!$album) $this->notFound();
         if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound();
         if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity))
-            $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
+            $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
         
         $name  = $album->getName();
         $owner = $album->getOwner();
         $album->delete();
 
-        $this->flash("succ", "Альбом удалён", "Альбом $name был успешно удалён.");
+        $this->flash("succ", tr("album_is_deleted"), tr("album_x_is_deleted", $name));
         $this->redirect("/albums" . ($owner instanceof Club ? "-" : "") . $owner->getId());
     }
     
@@ -205,13 +205,13 @@ final class PhotosPresenter extends OpenVKPresenter
         $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId);
         if(!$photo) $this->notFound();
         if(is_null($this->user) || $this->user->id != $ownerId)
-            $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
+            $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
         
         if($_SERVER["REQUEST_METHOD"] === "POST") {
             $photo->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc"));
             $photo->save();
             
-            $this->flash("succ", "Изменения сохранены", "Обновлённое описание появится на странице с фоткой.");
+            $this->flash("succ", tr("changes_saved"), tr("new_description_will_appear"));
             $this->redirect("/photo" . $photo->getPrettyId());
         } 
         
@@ -224,14 +224,14 @@ final class PhotosPresenter extends OpenVKPresenter
         $this->willExecuteWriteAction(true);
         
         if(is_null($this->queryParam("album")))
-            $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED.", 500, true);
+            $this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true);
         
         [$owner, $id] = explode("_", $this->queryParam("album"));
         $album = $this->albums->get((int) $id);
         if(!$album)
-            $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED.", 500, true);
+            $this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true);
         if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity))
-            $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.", 500, true);
+            $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), 500, true);
         
         if($_SERVER["REQUEST_METHOD"] === "POST") {
             if($this->queryParam("act") == "finish") {
@@ -258,7 +258,7 @@ final class PhotosPresenter extends OpenVKPresenter
             }
 
             if(!isset($_FILES))
-                $this->flashFail("err", "Нету фотографии", "Выберите файл.", 500, true);
+                $this->flashFail("err", tr("no_photo"), tr("select_file"), 500, true);
             
             $photos = [];
             for($i = 0; $i < $this->postParam("count"); $i++) {
@@ -304,7 +304,7 @@ final class PhotosPresenter extends OpenVKPresenter
         if(!$album || !$photo) $this->notFound();
         if(!$album->hasPhoto($photo)) $this->notFound();
         if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity))
-            $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
+            $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
         
         if($_SERVER["REQUEST_METHOD"] === "POST") {
             $this->assertNoCSRF();
@@ -312,7 +312,7 @@ final class PhotosPresenter extends OpenVKPresenter
             $album->setEdited(time());
             $album->save();
             
-            $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена.");
+            $this->flash("succ", tr("photo_is_deleted"), tr("photo_is_deleted_desc"));
             $this->redirect("/album" . $album->getPrettyId());
         }
     }
@@ -326,7 +326,7 @@ final class PhotosPresenter extends OpenVKPresenter
         $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId);
         if(!$photo) $this->notFound();
         if(is_null($this->user) || $this->user->id != $ownerId)
-            $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
+            $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
 
         $redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId;
 
@@ -336,7 +336,7 @@ final class PhotosPresenter extends OpenVKPresenter
         if($_SERVER["REQUEST_METHOD"] === "POST")
             $this->returnJson(["success" => true]);
 
-        $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена.");
+        $this->flash("succ", tr("photo_is_deleted"), tr("photo_is_deleted_desc"));
         $this->redirect($redirect);
     }
 }
diff --git a/Web/Presenters/ReportPresenter.php b/Web/Presenters/ReportPresenter.php
index 68d27861..a87154c8 100644
--- a/Web/Presenters/ReportPresenter.php
+++ b/Web/Presenters/ReportPresenter.php
@@ -118,22 +118,22 @@ final class ReportPresenter extends OpenVKPresenter
             $report->deleteContent();
             $report->banUser($this->user->identity->getId());
 
-            $this->flash("suc", "Смэрть...", "Пользователь успешно забанен.");
+            $this->flash("suc", tr("death"), tr("user_successfully_banned"));
         } else if ($this->postParam("delete")) {
             $report->deleteContent();
 
-            $this->flash("suc", "Нехай живе!", "Контент удалён, а пользователю прилетело предупреждение.");
+            $this->flash("suc", tr("nehay"), tr("content_is_deleted"));
         } else if ($this->postParam("ignore")) {
             $report->delete();
 
-            $this->flash("suc", "Нехай живе!", "Жалоба проигнорирована.");
+            $this->flash("suc", tr("nehay"), tr("report_is_ignored"));
         } else if ($this->postParam("banClubOwner") || $this->postParam("banClub")) {
             if ($report->getContentType() !== "group")
-                $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
+                $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
 
             $club = $report->getContentObject();
             if (!$club || $club->isBanned())
-                $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
+                $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
 
             if ($this->postParam("banClubOwner")) {
                 $club->getOwner()->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**", false, $club->getOwner()->getNewBanTime(), $this->user->identity->getId());
@@ -143,7 +143,7 @@ final class ReportPresenter extends OpenVKPresenter
 
             $report->delete();
 
-            $this->flash("suc", "Смэрть...", ($this->postParam("banClubOwner") ? "Создатель сообщества успешно забанен." : "Сообщество успешно забанено"));
+            $this->flash("suc", tr("death"), ($this->postParam("banClubOwner") ? tr("group_owner_is_banned") : tr("group_is_banned")));
         }
 
         $this->redirect("/scumfeed");
diff --git a/Web/Presenters/TopicsPresenter.php b/Web/Presenters/TopicsPresenter.php
index e7b08ac3..92d67e84 100644
--- a/Web/Presenters/TopicsPresenter.php
+++ b/Web/Presenters/TopicsPresenter.php
@@ -111,7 +111,7 @@ final class TopicsPresenter extends OpenVKPresenter
                     $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]);
                 }
             } catch(ISE $ex) {
-                $this->flash("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик.");
+                $this->flash("err", tr("error_when_publishing_comment"), tr("error_comment_file_too_big"));
                 $this->redirect("/topic" . $topic->getPrettyId());
             }
             
@@ -126,7 +126,7 @@ final class TopicsPresenter extends OpenVKPresenter
                     $comment->setFlags($flags);
                     $comment->save();
                 } catch (\LengthException $ex) {
-                    $this->flash("err", "Не удалось опубликовать комментарий", "Комментарий слишком большой.");
+                    $this->flash("err", tr("error_when_publishing_comment"), tr("error_comment_too_big"));
                     $this->redirect("/topic" . $topic->getPrettyId());
                 }
                 
diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php
index 46aa7e92..51ddc6aa 100644
--- a/Web/Presenters/UserPresenter.php
+++ b/Web/Presenters/UserPresenter.php
@@ -72,7 +72,7 @@ final class UserPresenter extends OpenVKPresenter
         if(!is_null($this->user)) {
             if($this->template->mode !== "friends" && $this->user->id !== $id) {
                 $name = $user->getFullName();
-                $this->flash("err", "Ошибка доступа", "Вы не можете просматривать полный список подписок $name.");
+                $this->flash("err", tr("error_access_denied_short"), tr("error_viewing_subs", $name));
                 
                 $this->redirect($user->getURL());
             }
@@ -107,11 +107,11 @@ final class UserPresenter extends OpenVKPresenter
             $this->notFound();
 
         if(!$club->canBeModifiedBy($this->user->identity ?? NULL))
-            $this->flashFail("err", "Ошибка доступа", "У вас недостаточно прав, чтобы изменять этот ресурс.", NULL, true);
+            $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), NULL, true);
 
         $isClubPinned = $this->user->identity->isClubPinned($club);
         if(!$isClubPinned && $this->user->identity->getPinnedClubCount() > 10)
-            $this->flashFail("err", "Ошибка", "Находится в левом меню могут максимум 10 групп", NULL, true);
+            $this->flashFail("err", tr("error"), tr("error_max_pinned_clubs"), NULL, true);
 
         if($club->getOwner()->getId() === $this->user->identity->getId()) {
             $club->setOwner_Club_Pinned(!$isClubPinned);
@@ -237,7 +237,7 @@ final class UserPresenter extends OpenVKPresenter
             } elseif($_GET['act'] === "status") {
                 if(mb_strlen($this->postParam("status")) > 255) {
                     $statusLength = (string) mb_strlen($this->postParam("status"));
-                    $this->flashFail("err", "Ошибка", "Статус слишком длинный ($statusLength символов вместо 255 символов)", NULL, true);
+                    $this->flashFail("err", tr("error"), tr("error_status_too_long", $statusLength), NULL, true);
                 }
 
                 $user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status"));
@@ -281,7 +281,7 @@ final class UserPresenter extends OpenVKPresenter
         
         if($_SERVER["REQUEST_METHOD"] === "POST") {
             if(!$user->verifyNumber($this->postParam("code") ?? 0))
-                $this->flashFail("err", "Ошибка", "Не удалось подтвердить номер телефона: неверный код.");
+                $this->flashFail("err", tr("error"), tr("invalid_code"));
         
             $this->flash("succ", tr("changes_saved"), tr("changes_saved_comment"));
         }
diff --git a/Web/Presenters/VideosPresenter.php b/Web/Presenters/VideosPresenter.php
index 4e4d484a..9d2fddc6 100644
--- a/Web/Presenters/VideosPresenter.php
+++ b/Web/Presenters/VideosPresenter.php
@@ -58,7 +58,7 @@ final class VideosPresenter extends OpenVKPresenter
         $this->willExecuteWriteAction();
 
         if(OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading'])
-            $this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator.");
+            $this->flashFail("err", tr("error"), tr("video_uploads_disabled"));
         
         if($_SERVER["REQUEST_METHOD"] === "POST") {
             if(!empty($this->postParam("name"))) {
@@ -74,18 +74,18 @@ final class VideosPresenter extends OpenVKPresenter
                     else if(!empty($this->postParam("link")))
                         $video->setLink($this->postParam("link"));
                     else
-                        $this->flashFail("err", "Нету видеозаписи", "Выберите файл или укажите ссылку.");
+                        $this->flashFail("err", tr("no_video"), tr("no_video_desc"));
                 } catch(\DomainException $ex) {
-                    $this->flashFail("err", "Произошла ошибка", "Файл повреждён или не содержит видео." );
+                    $this->flashFail("err", tr("error_occured"), tr("error_video_damaged_file"));
                 } catch(ISE $ex) {
-                    $this->flashFail("err", "Произошла ошибка", "Возможно, ссылка некорректна.");
+                    $this->flashFail("err", tr("error_occured"), tr("error_video_incorrect_link"));
                 }
                 
                 $video->save();
                 
                 $this->redirect("/video" . $video->getPrettyId());
             } else {
-                $this->flashFail("err", "Произошла ошибка", "Видео не может быть опубликовано без названия.");
+                $this->flashFail("err", tr("error_occured"), tr("error_video_no_title"));
             }
         }
     }
@@ -99,14 +99,14 @@ final class VideosPresenter extends OpenVKPresenter
         if(!$video)
             $this->notFound();
         if(is_null($this->user) || $this->user->id !== $owner)
-            $this->flashFail("err", "Ошибка доступа", "Вы не имеете права редактировать этот ресурс.");
+            $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
         
         if($_SERVER["REQUEST_METHOD"] === "POST") {
             $video->setName(empty($this->postParam("name")) ? NULL : $this->postParam("name"));
             $video->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc"));
             $video->save();
             
-            $this->flash("succ", "Изменения сохранены", "Обновлённое описание появится на странице с видосиком.");
+            $this->flash("succ", tr("changes_saved"), tr("new_data_video"));
             $this->redirect("/video" . $video->getPrettyId());
         } 
         
@@ -128,7 +128,7 @@ final class VideosPresenter extends OpenVKPresenter
                 $video->deleteVideo($owner, $vid);
             }
         } else {
-            $this->flashFail("err", "Не удалось удалить пост", "Вы не вошли в аккаунт.");
+            $this->flashFail("err", tr("error_deleting_video"), tr("login_please"));
         }
         
         $this->redirect("/videos" . $owner);
diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php
index 96b61b48..32ac421e 100644
--- a/Web/Presenters/WallPresenter.php
+++ b/Web/Presenters/WallPresenter.php
@@ -233,7 +233,7 @@ final class WallPresenter extends OpenVKPresenter
             $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment"));
 
         if($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading'])
-            $this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator.");
+            $this->flashFail("err", tr("error"), tr("video_uploads_disabled"));
 
         $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"];
         if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) {
diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml
index f37532a7..f8a975e0 100644
--- a/Web/Presenters/templates/@layout.xml
+++ b/Web/Presenters/templates/@layout.xml
@@ -38,9 +38,8 @@
     <body>
         <div id="sudo-banner" n:if="isset($thisUser) && $userTainted">
             <p>
-                Вы вошли как <b>{$thisUser->getCanonicalName()}</b>. Пожалуйста, уважайте
-                право на тайну переписки других людей и не злоупотребляйте подменой пользователя.
-                Нажмите <a href="/setSID/unset?hash={rawurlencode($csrfToken)}">здесь</a>, чтобы выйти.
+                {_you_entered_as} <b>{$thisUser->getCanonicalName()}</b>. {_please_rights}
+                {_click_on} <a href="/setSID/unset?hash={rawurlencode($csrfToken)}">{_there}</a>, {_to_leave}.
             </p>
         </div>
 
diff --git a/Web/Presenters/templates/About/BB.xml b/Web/Presenters/templates/About/BB.xml
index 17ab3b0f..9aa745b6 100644
--- a/Web/Presenters/templates/About/BB.xml
+++ b/Web/Presenters/templates/About/BB.xml
@@ -1,12 +1,10 @@
 {extends "../@layout.xml"}
-{block title}Ваш браузер устарел{/block}
+{block title}{_deprecated_browser}{/block}
 
 {block header}
-    Устаревший браузер
+    {_deprecated_browser}
 {/block}
 
 {block content}
-    Для просмотра этого контента вам понадобится Firefox ESR 52+ или
-    эквивалентный по функционалу навигатор по всемирной сети интернет.<br/>
-    Сожалеем об этом.
+    {_deprecated_browser_description}
 {/block}
diff --git a/Web/Presenters/templates/About/Help.xml b/Web/Presenters/templates/About/Help.xml
index 060f1381..64be74a0 100644
--- a/Web/Presenters/templates/About/Help.xml
+++ b/Web/Presenters/templates/About/Help.xml
@@ -9,5 +9,5 @@
     <div id="faqhead">Для кого этот сайт?</div>
     <div id="faqcontent">Сайт предназначен для поиска друзей и знакомых, а также просмотр данных пользователя. Это как справочник города, с помощью которого люди могут быстро найти актуальную информацию о человеке. Также этот сайт подойдёт для ностальгираторов и тех, кто решил слезть с трубы "ВКонтакте", которого клон и является.<br></div>
     Я попозже допишу ок ~~ veselcraft - 12.01.2020 - 22:05 GMT+3
-
+    Давай
 {/block}
diff --git a/Web/Presenters/templates/About/Sandbox.xml b/Web/Presenters/templates/About/Sandbox.xml
index b84b515a..1b548a48 100644
--- a/Web/Presenters/templates/About/Sandbox.xml
+++ b/Web/Presenters/templates/About/Sandbox.xml
@@ -2,7 +2,7 @@
 {block title}Sandbox{/block}
 
 {block header}
-    Sandbox для разработчиков
+    {_sandbox_for_developers}
 {/block}
 
 {block content}
diff --git a/Web/Presenters/templates/Admin/BansHistory.xml b/Web/Presenters/templates/Admin/BansHistory.xml
index c0dc1b64..2144c949 100644
--- a/Web/Presenters/templates/Admin/BansHistory.xml
+++ b/Web/Presenters/templates/Admin/BansHistory.xml
@@ -1,7 +1,7 @@
 {extends "./@layout.xml"}
 
 {block title}
-    История блокировок
+    {_bans_history}
 {/block}
 
 {block heading}
@@ -13,13 +13,13 @@
     <thead>
         <tr>
             <th>ID</th>
-            <th>Забаненный</th>
-            <th>Инициатор</th>
-            <th>Начало</th>
-            <th>Конец</th>
-            <th>Время</th>
-            <th>Причина</th>
-            <th>Снята</th>
+            <th>{_bans_history_blocked}</th>
+            <th>{_bans_history_initiator}</th>
+            <th>{_bans_history_start}</th>
+            <th>{_bans_history_end}</th>
+            <th>{_bans_history_time}</th>
+            <th>{_bans_history_reason}</th>
+            <th>{_bans_history_removed}</th>
         </tr>
     </thead>
     <tbody>
@@ -77,7 +77,7 @@
                         {_admin_banned}
                     </span>
                 {else}
-                    <b style="color: red;">Активная блокировка</b>
+                    <b style="color: red;">{_bans_history_active}</b>
                 {/if}
             </td>
         </tr>
diff --git a/Web/Presenters/templates/Admin/Logs.xml b/Web/Presenters/templates/Admin/Logs.xml
index d953a378..ab5e62f5 100644
--- a/Web/Presenters/templates/Admin/Logs.xml
+++ b/Web/Presenters/templates/Admin/Logs.xml
@@ -1,11 +1,11 @@
 {extends "@layout.xml"}
 
 {block title}
-    Логи
+    {_logs}
 {/block}
 
 {block heading}
-    Логи
+    {_logs}
 {/block}
 
 {block content}
@@ -18,23 +18,23 @@
     </style>
     <form class="aui">
     <div>
-        <select class="select medium-field" type="number" id="type" name="type" placeholder="Тип изменения">
-            <option value="any" n:attr="selected => !$type">Любое</option>
-            <option value="0" n:attr="selected => $type === 0">Создание</option>
-            <option value="1" n:attr="selected => $type === 1">Редактирование</option>
-            <option value="2" n:attr="selected => $type === 2">Удаление</option>
-            <option value="3" n:attr="selected => $type === 3">Восстановление</option>
+        <select class="select medium-field" type="number" id="type" name="type" placeholder="{_logs_change_type}">
+            <option value="any" n:attr="selected => !$type">{_logs_anything}</option>
+            <option value="0" n:attr="selected => $type === 0">{_logs_adding}</option>
+            <option value="1" n:attr="selected => $type === 1">{_logs_editing}</option>
+            <option value="2" n:attr="selected => $type === 2">{_logs_removing}</option>
+            <option value="3" n:attr="selected => $type === 3">{_logs_restoring}</option>
         </select>
-        <input class="text medium-field" type="number" id="id" name="id" placeholder="ID записи" n:attr="value => $id"/>
-        <input class="text medium-field" type="text" id="uid" name="uid" placeholder="UUID пользователя" n:attr="value => $user"/>
+        <input class="text medium-field" type="number" id="id" name="id" placeholder="{_logs_id_post}" n:attr="value => $id"/>
+        <input class="text medium-field" type="text" id="uid" name="uid" placeholder="{_logs_uuid_user}" n:attr="value => $user"/>
     </div>
     <div style="margin: 8px 0;" />
     <div>
-        <select class="select medium-field" id="obj_type" name="obj_type" placeholder="Тип объекта">
-            <option value="any" n:attr="selected => !$obj_type">Любой</option>
+        <select class="select medium-field" id="obj_type" name="obj_type" placeholder="{_logs_change_object}">
+            <option value="any" n:attr="selected => !$obj_type">{_logs_anything}</option>
             <option n:foreach="$object_types as $type" n:attr="selected => $obj_type === $type">{$type}</option>
         </select>
-        <input class="text medium-field" type="number" id="obj_id" name="obj_id" placeholder="ID объекта" n:attr="value => $obj_id"/>
+        <input class="text medium-field" type="number" id="obj_id" name="obj_id" placeholder="{_logs_id_object}" n:attr="value => $obj_id"/>
         <input type="submit" class="aui-button aui-button-primary medium-field" value="Поиск" style="width: 165px;"/>
     </div>
     </form>
@@ -42,11 +42,11 @@
         <thead>
             <tr>
                 <th>ID</th>
-                <th>Пользователь</th>
-                <th>Объект</th>
-                <th>Тип</th>
-                <th>Изменения</th>
-                <th>Время</th>
+                <th>{_logs_user}</th>
+                <th>{_logs_object}</th>
+                <th>{_logs_type}</th>
+                <th>{_logs_changes}</th>
+                <th>{_logs_time}</th>
             </tr>
         </thead>
         <tbody>
diff --git a/Web/Presenters/templates/Apps/Play.xml b/Web/Presenters/templates/Apps/Play.xml
index facaa273..93637a2d 100644
--- a/Web/Presenters/templates/Apps/Play.xml
+++ b/Web/Presenters/templates/Apps/Play.xml
@@ -7,7 +7,7 @@
 
 {block header}
     {$name}
-    <a style="float: right;" onClick="reportApp()" n:if="$canReport ?? false">Пожаловаться</a>
+    <a style="float: right;" onClick="reportApp()" n:if="$canReport ?? false">{_report}</a>
 {/block}
 
 {block content}
@@ -37,20 +37,20 @@
 
     <script n:if="$canReport ?? false">
         function reportApp() {
-            uReportMsgTxt  = "Вы собираетесь пожаловаться на данное приложение.";
-            uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
-            uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
+            uReportMsgTxt  = {_going_to_report_app};
+            uReportMsgTxt += "<br/>"+tr("report_question_text");
+            uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />"
 
-            MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
+            MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [
                 (function() {
                     res = document.querySelector("#uReportMsgInput").value;
                     xhr = new XMLHttpRequest();
                     xhr.open("GET", "/report/" + {$id} + "?reason=" + res + "&type=app", true);
                     xhr.onload = (function() {
                     if(xhr.responseText.indexOf("reason") === -1)
-                        MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
+                        MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]);
                     else
-                        MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
+                        MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]);
                     });
                     xhr.send(null);
                 }),
diff --git a/Web/Presenters/templates/Away/View.xml b/Web/Presenters/templates/Away/View.xml
index f6d05a2c..870f541f 100644
--- a/Web/Presenters/templates/Away/View.xml
+++ b/Web/Presenters/templates/Away/View.xml
@@ -1,9 +1,9 @@
 {extends "../@layout.xml"}
 
-{block title}Переход по ссылке заблокирован{/block}
+{block title}{_transition_is_blocked}{/block}
 
 {block header}
-Предупреждение
+{_caution}
 {/block}
 
 {block content}
diff --git a/Web/Presenters/templates/Group/Followers.xml b/Web/Presenters/templates/Group/Followers.xml
index 45e40bbb..482d156a 100644
--- a/Web/Presenters/templates/Group/Followers.xml
+++ b/Web/Presenters/templates/Group/Followers.xml
@@ -55,7 +55,7 @@
         <tbody>
             <tr>
                 <td width="120" valign="top"><span class="nobold">{_gender}: </span></td>
-                <td>{$user->isFemale() ? "женский" : "мужской"}</td>
+                <td>{$user->isFemale() ? tr("female"): tr("male")}</td>
             </tr>
             <tr>
                 <td width="120" valign="top"><span class="nobold">{_registration_date}: </span></td>
@@ -82,8 +82,6 @@
     </table>
 
     <script n:if="$club->getOwner()->getId() != $user->getId() && $manager && $thisUser->getId() == $club->getOwner()->getId()">
-        console.log("gayshit");
-        console.log("сам такой");
         function changeOwner(club, newOwner) {
             const action = "/groups/" + club + "/setNewOwner/" + newOwner;
 
diff --git a/Web/Presenters/templates/Group/Statistics.xml b/Web/Presenters/templates/Group/Statistics.xml
index f1b53cc1..4654557a 100644
--- a/Web/Presenters/templates/Group/Statistics.xml
+++ b/Web/Presenters/templates/Group/Statistics.xml
@@ -7,12 +7,12 @@
 
 {block content}
     <div>
-        <h4>Охват</h4>
-        <p>Этот график отображает охват за последние 7 дней.</p>
+        <h4>{_coverage}</h4>
+        <p>{_coverage_this_week}</p>
         <div id="reachChart" style="width: 100%; height: 280px;"></div>
         
-        <h4>Просмотры</h4>
-        <p>Этот график отображает просмотры постов сообщества за последние 7 дней.</p>
+        <h4>{_views}</h4>
+        <p>{_views_this_week}</p>
         <div id="viewsChart" style="width: 100%; height: 280px;"></div>
         
         <style>
diff --git a/Web/Presenters/templates/Group/View.xml b/Web/Presenters/templates/Group/View.xml
index 42cdbde3..0242bbb8 100644
--- a/Web/Presenters/templates/Group/View.xml
+++ b/Web/Presenters/templates/Group/View.xml
@@ -9,7 +9,7 @@
     <img n:if="$club->isVerified()"
          class="name-checkmark"
          src="/assets/packages/static/openvk/img/checkmark.png"
-         alt="Подтверждённая страница"
+         alt="{_verified_page}"
          />
 {/block}
 
@@ -143,24 +143,24 @@
         {/if}
         {var $canReport = $thisUser->getId() != $club->getOwner()->getId()}
         {if $canReport}
-        <a class="profile_link" style="display:block;width:96%;" href="javascript:reportVideo()">{_report}</a>
+        <a class="profile_link" style="display:block;" href="javascript:reportVideo()">{_report}</a>
 
         <script>
             function reportVideo() {
-            uReportMsgTxt  = "Вы собираетесь пожаловаться на данное сообщество.";
-            uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
-            uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
+            uReportMsgTxt  = tr("going_to_report_club");
+            uReportMsgTxt += "<br/>"+tr("report_question_text");
+            uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />"
 
-            MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
+            MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [
                 (function() {
                     res = document.querySelector("#uReportMsgInput").value;
                     xhr = new XMLHttpRequest();
                     xhr.open("GET", "/report/" + {$club->getId()} + "?reason=" + res + "&type=group", true);
                     xhr.onload = (function() {
                     if(xhr.responseText.indexOf("reason") === -1)
-                        MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
+                        MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]);
                     else
-                        MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
+                        MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]);
                     });
                     xhr.send(null);
                     }),
diff --git a/Web/Presenters/templates/NoSpam/Index.xml b/Web/Presenters/templates/NoSpam/Index.xml
index 89305c5b..2bf9d1df 100644
--- a/Web/Presenters/templates/NoSpam/Index.xml
+++ b/Web/Presenters/templates/NoSpam/Index.xml
@@ -27,13 +27,13 @@
             <tbody id="models-list">
                 <tr id="0-model">
                     <td width="83px">
-                        <span class="nobold">Раздел:</span>
+                        <span class="nobold">{_section}:</span>
                     </td>
                     <td>
                         <div style="display: flex; gap: 8px; justify-content: space-between;">
                             <div id="add-model" class="noSpamIcon noSpamIcon-Add" style="display: none;" />
                             <select name="model" id="model" class="model initialModel" style="margin-left: -2px;">
-                                <option selected value="none">Не выбрано</option>
+                                <option selected value="none">{_relationship_0}</option>
                                 <option n:foreach="$models as $model" value="{$model}">{$model}</option>
                             </select>
                         </div>
@@ -47,7 +47,7 @@
                 <tbody>
                     <tr style="width: 129px; border-top: 1px solid #ECECEC;">
                         <td>
-                            <span class="nobold">Подстрока:</span>
+                            <span class="nobold">{_substring}:</span>
                         </td>
                         <td>
                             <input type="text" name="regex" placeholder="Regex" id="regex">
@@ -55,10 +55,10 @@
                     </tr>
                     <tr style="width: 129px; border-top: 1px solid #ECECEC;">
                         <td>
-                            <span class="nobold">Пользователь:</span>
+                            <span class="nobold">{_n_user}:</span>
                         </td>
                         <td>
-                            <input type="text" name="user" placeholder="Ссылка на страницу" id="user">
+                            <input type="text" name="user" placeholder="{_link_to_page}" id="user">
                         </td>
                     </tr>
                     <tr style="width: 129px">
@@ -66,12 +66,12 @@
                             <span class="nobold">IP:</span>
                         </td>
                         <td>
-                            <input type="text" name="ip" id="ip" placeholder="или подсеть">
+                            <input type="text" name="ip" id="ip" placeholder="{_or_subnet}">
                         </td>
                     </tr>
                     <tr style="width: 129px">
                         <td>
-                            <span class="nobold">Юзер-агент:</span>
+                            <span class="nobold">User-Agent:</span>
                         </td>
                         <td>
                             <input type="text" name="useragent" id="useragent" placeholder="Mozila 1.0 Blablabla/test">
@@ -79,7 +79,7 @@
                     </tr>
                     <tr style="width: 129px">
                         <td>
-                            <span class="nobold">Время раньше, чем:</span>
+                            <span class="nobold">{_time_before}:</span>
                         </td>
                         <td>
                             <input type="datetime-local" name="ts" id="ts">
@@ -87,7 +87,7 @@
                     </tr>
                     <tr style="width: 129px">
                         <td>
-                            <span class="nobold">Время позже, чем:</span>
+                            <span class="nobold">{_time_after}:</span>
                         </td>
                         <td>
                             <input type="datetime-local" name="te" id="te">
@@ -97,19 +97,19 @@
             </table>
             <textarea style="resize: vertical; width: calc(100% - 6px)" placeholder='city = "Воскресенск" && id = 1'
                       name="where" id="where"/>
-            <span style="color: grey; font-size: 8px;">WHERE для поиска по разделу</span>
+            <span style="color: grey; font-size: 8px;">{_where_for_search}</span>
             <div style="border-top: 1px solid #ECECEC; margin: 8px 0;"/>
             <table cellspacing="7" cellpadding="0" width="100%" border="0">
                 <tbody>
                     <tr style="width: 129px; border-top: 1px solid #ECECEC;">
                         <td>
-                            <span class="nobold">Параметры блокировки:</span>
+                            <span class="nobold">{_block_params}:</span>
                         </td>
                         <td>
-                            <select name="ban_type" id="noSpam-ban-type" style="width: 140px;">
-                                <option value="1">Только откат</option>
-                                <option value="2">Только блокировка</option>
-                                <option value="3">Откат и блокировка</option>
+                            <select name="ban_type" id="noSpam-ban-type" style="width: 140px;"
+                                <option value="1">{_only_rollback}</option>
+                                <option value="2">{_only_block}</option>
+                                <option value="3">{_rollback_and_block}</option>
                             </select>
                         </td>
                     </tr>
@@ -136,8 +136,8 @@
             <div style="border-top: 1px solid #ECECEC; margin: 8px 0;"/>
             <center>
                 <div id="noSpam-buttons">
-                    <input id="search" type="submit" value="Поиск" class="button"/>
-                    <input id="apply" type="submit" value="Применить" class="button" style="display: none;"/>
+                    <input id="search" type="submit" value="{_header_search}" class="button"/>
+                    <input id="apply" type="submit" value="{_subm}" class="button" style="display: none;"/>
                 </div>
                 <div id="noSpam-loader" style="display: none;">
                     <img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px;">
@@ -145,7 +145,7 @@
             </center>
         </div>
         <div id="noSpam-model-not-selected">
-            <center id="noSpam-model-not-selected-text" style="padding: 71px 25px;">Выберите раздел для начала работы</center>
+            <center id="noSpam-model-not-selected-text" style="padding: 71px 25px;">{_select_section_for_start}</center>
             <center id="noSpam-model-not-selected-loader" style="display: none;">
                 <img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px; margin: 125px 0;">
             </center>
@@ -155,11 +155,11 @@
         <center id="noSpam-results-loader" style="display: none;">
             <img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px; margin: 125px 0;">
         </center>
-        <center id="noSpam-results-text" style="margin: 125px 25px;">Здесь будут отображаться результаты поиска</center>
+        <center id="noSpam-results-text" style="margin: 125px 25px;">{_results_will_be_there}</center>
         <div id="noSpam-results-block" style="display: none;">
-            <h4 style="padding: 8px;">Результаты поиска
+            <h4 style="padding: 8px;">{_search_results}
                 <span style="color: #a2a2a2; font-weight: inherit">
-                    (<span id="noSpam-results-count" style="color: #a2a2a2; font-weight: inherit;"></span> шт.)
+                    (<span id="noSpam-results-count" style="color: #a2a2a2; font-weight: inherit;"></span> {_cnt}.)
                 </span>
             </h4>
             <ul style="padding-inline-start:18px;" id="noSpam-results-list"></ul>
@@ -242,17 +242,17 @@
                             $("#noSpam-results-block").show();
                             $("#apply").show();
                         } else {
-                            $("#noSpam-results-text").text(ban ? "Операция завершена успешно" : "Ничего не найдено :(");
+                            $("#noSpam-results-text").text(ban ? tr("operation_successfully") : tr("no_found"));
                             $("#noSpam-results-text").show();
                         }
                     } else {
-                        $("#noSpam-results-text").text(response?.error ?? "Неизвестная ошибка");
+                        $("#noSpam-results-text").text(response?.error ?? tr("unknown_error"));
                         $("#noSpam-results-text").show();
                     }
                 },
                 error: (error) => {
                     console.error("Error while searching noSpam:", error);
-                    $("#noSpam-results-text").text("Ошибка при выполнении запроса");
+                    $("#noSpam-results-text").text(tr("error_when_searching"));
                     $("#noSpam-results-text").show();
                 }
             });
@@ -323,7 +323,7 @@
                         <div style="display: flex; gap: 8px; justify-content: space-between;">
                             <div class="noSpamIcon noSpamIcon-Delete" onClick="deleteModelSelect(${ $('.model').length});"></div>
                             <select name="model" class="model" style="margin-left: -2px;" onChange="selectChange($(this).val())">
-                                <option selected value="none">Не выбрано</option>
+                                <option selected value="none">{_relationship_0}</option>
                                 {foreach $models as $model}
                                     <option value={$model}>{$model|noescape}</option>
                                 {/foreach}
diff --git a/Web/Presenters/templates/NoSpam/Tabs.xml b/Web/Presenters/templates/NoSpam/Tabs.xml
index e80db91e..69943531 100644
--- a/Web/Presenters/templates/NoSpam/Tabs.xml
+++ b/Web/Presenters/templates/NoSpam/Tabs.xml
@@ -1,9 +1,9 @@
 <div n:attr="id => ($mode === 'form' ? 'activetabs' : 'ki')" class="tab">
-    <a n:attr="id => ($mode === 'form' ? 'act_tab_a' : 'ki')" href="/noSpam">Бан по шаблону</a>
+    <a n:attr="id => ($mode === 'form' ? 'act_tab_a' : 'ki')" href="/noSpam">{_template_ban}</a>
 </div>
 <div n:attr="id => ($mode === 'templates' ? 'activetabs' : 'ki')" class="tab">
-    <a n:attr="id => ($mode === 'templates' ? 'act_tab_a' : 'ki')" href="/noSpam?act=templates">Действующие шаблоны</a>
+    <a n:attr="id => ($mode === 'templates' ? 'act_tab_a' : 'ki')" href="/noSpam?act=templates">{_active_templates}</a>
 </div>
 <div n:attr="id => ($mode === 'reports' ? 'activetabs' : 'ki')" class="tab">
-    <a n:attr="id => ($mode === 'reports' ? 'act_tab_a' : 'ki')" href="/scumfeed">Жалобы пользователей</a>
+    <a n:attr="id => ($mode === 'reports' ? 'act_tab_a' : 'ki')" href="/scumfeed">{_users_reports}</a>
 </div>
diff --git a/Web/Presenters/templates/NoSpam/Templates.xml b/Web/Presenters/templates/NoSpam/Templates.xml
index 83fd77c7..a86df534 100644
--- a/Web/Presenters/templates/NoSpam/Templates.xml
+++ b/Web/Presenters/templates/NoSpam/Templates.xml
@@ -1,6 +1,6 @@
 {extends "../@layout.xml"}
 
-{block title}Шаблоны{/block}
+{block title}{_templates}{/block}
 {block header}{include title}{/block}
 
 {block content}
@@ -44,14 +44,14 @@
         <table n:if="count($templates) > 0" cellspacing="0" cellpadding="7" width="100%">
             <tr>
                 <th style="text-align: center;">ID</th>
-                <th>Пользователь</th>
-                <th style="text-align: center;">Раздел</th>
-                <th>Подстрока</th>
+                <th>{_n_user}</th>
+                <th style="text-align: center;">{_section}</th>
+                <th>{_substring}</th>
                 <th>Where</th>
-                <th style="text-align: center;">Тип</th>
-                <th style="text-align: center;">Количество</th>
-                <th>Время</th>
-                <th style="text-align: center;">Действия</th>
+                <th style="text-align: center;">{_type}</th>
+                <th style="text-align: center;">{_count}</th>
+                <th>{_time}</th>
+                <th style="text-align: center;">{_actions}</th>
             </tr>
             <tr n:foreach="$templates as $template">
                 <td id="id-{$template->getId()}" onClick="openTableField('id', {$template->getId()})" style="text-align: center;"><b>{$template->getId()}</b></td>
@@ -75,8 +75,8 @@
                         <div id="noSpam-rollback-loader-{$template->getId()}" style="display: none;">
                             <img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px;">
                         </div>
-                        <a n:if="!$template->isRollbacked()" id="noSpam-rollback-template-link-{$template->getId()}" onClick="rollbackTemplate({$template->getId()})">откатить</a>
-                        <span n:attr="style => $template->isRollbacked() ? '' : 'display: none;'" id="noSpam-rollback-template-rollbacked-{$template->getId()}">откачен</span>
+                        <a n:if="!$template->isRollbacked()" id="noSpam-rollback-template-link-{$template->getId()}" onClick="rollbackTemplate({$template->getId()})">{_roll_back}</a>
+                        <span n:attr="style => $template->isRollbacked() ? '' : 'display: none;'" id="noSpam-rollback-template-rollbacked-{$template->getId()}">{roll_backed}</span>
                     </div>
                 </td>
             </tr>
diff --git a/Web/Presenters/templates/Notes/Create.xml b/Web/Presenters/templates/Notes/Create.xml
index cd653924..e8e2d20e 100644
--- a/Web/Presenters/templates/Notes/Create.xml
+++ b/Web/Presenters/templates/Notes/Create.xml
@@ -13,7 +13,7 @@
         <textarea name="html" style="display:none;"></textarea>
         <div id="editor" style="width:600px;height:300px;border:1px solid grey"></div>
         
-        <p><i><a href="/kb/notes">Кое-что</a> из (X)HTML поддерживается.</i></p>
+        <p><i><a href="/kb/notes">{_something}</a> {_supports_xhtml}</i></p>
         
         <input type="hidden" name="hash" value="{$csrfToken}" />
         <button class="button">{_save}</button>
diff --git a/Web/Presenters/templates/Notes/Edit.xml b/Web/Presenters/templates/Notes/Edit.xml
index b010c6bc..b904c72e 100644
--- a/Web/Presenters/templates/Notes/Edit.xml
+++ b/Web/Presenters/templates/Notes/Edit.xml
@@ -18,7 +18,7 @@
         <textarea name="html" style="display:none;"></textarea>
         <div id="editor" style="width:600px;height:300px;border:1px solid grey"></div>
         
-        <p><i><a href="/kb/notes">Кое-что</a> из (X)HTML поддерживается.</i></p>
+        <p><i><a href="/kb/notes">{_something}</a> {_supports_xhtml}</i></p>
         
         <input type="hidden" name="hash" value="{$csrfToken}" />
         <button class="button">{_save}</button>
diff --git a/Web/Presenters/templates/Photos/Album.xml b/Web/Presenters/templates/Photos/Album.xml
index 6f22c490..1bd49a6f 100644
--- a/Web/Presenters/templates/Photos/Album.xml
+++ b/Web/Presenters/templates/Photos/Album.xml
@@ -1,6 +1,6 @@
 {extends "../@layout.xml"}
 
-{block title}Альбом {$album->getName()}{/block}
+{block title}{_album} {$album->getName()}{/block}
 
 {block header}
     {var $isClub = ($album->getOwner() instanceof openvk\Web\Models\Entities\Club)}
@@ -18,7 +18,8 @@
 
 {block content}
     <a href="/album{$album->getPrettyId()}">
-        <b>{$album->getPhotosCount()} фотографий</b>
+        {* TODO: Добавить склонения *}
+        <b>{$album->getPhotosCount()} {_photos}</b>
     </a>
     
     {if !is_null($thisUser) && $album->canBeModifiedBy($thisUser) && !$album->isCreatedBySystem()}
diff --git a/Web/Presenters/templates/Photos/AlbumList.xml b/Web/Presenters/templates/Photos/AlbumList.xml
index f6742463..e59b6a58 100644
--- a/Web/Presenters/templates/Photos/AlbumList.xml
+++ b/Web/Presenters/templates/Photos/AlbumList.xml
@@ -58,7 +58,7 @@
 
 {block description}
     <span>{$x->getDescription() ?? $x->getName()}</span><br />
-    <span style="color: grey;">{$x->getPhotosCount()} фотографий</span><br />
+    <span style="color: grey;">{$x->getPhotosCount()} {_photos}</span><br />
     <span style="color: grey;">{tr("updated_at", $x->getEditTime() ?? $x->getCreationTime())}</span><br />
     <span style="color: grey;">{_created} {$x->getCreationTime()}</span><br />
 {/block}
diff --git a/Web/Presenters/templates/Photos/EditAlbum.xml b/Web/Presenters/templates/Photos/EditAlbum.xml
index a10b5c7f..5a247e27 100644
--- a/Web/Presenters/templates/Photos/EditAlbum.xml
+++ b/Web/Presenters/templates/Photos/EditAlbum.xml
@@ -1,5 +1,5 @@
 {extends "../@layout.xml"}
-{block title}Изменить альбом{/block}
+{block title}{_edit_album}{/block}
 
 {block header}
     <a href="{$album->getOwner()->getURL()}">{$album->getOwner()->getCanonicalName()}</a>
diff --git a/Web/Presenters/templates/Photos/EditPhoto.xml b/Web/Presenters/templates/Photos/EditPhoto.xml
index 9c269491..159916ea 100644
--- a/Web/Presenters/templates/Photos/EditPhoto.xml
+++ b/Web/Presenters/templates/Photos/EditPhoto.xml
@@ -1,5 +1,5 @@
 {extends "../@layout.xml"}
-{block title}Изменить фотографию{/block}
+{block title}{_edit_photo}{/block}
 
 {block header}
     <a href="{$thisUser->getURL()}">{$thisUser->getCanonicalName()}</a>
diff --git a/Web/Presenters/templates/Photos/Photo.xml b/Web/Presenters/templates/Photos/Photo.xml
index 047f382a..bb6f171f 100644
--- a/Web/Presenters/templates/Photos/Photo.xml
+++ b/Web/Presenters/templates/Photos/Photo.xml
@@ -53,20 +53,20 @@
             <a n:if="$canReport ?? false" class="profile_link" style="display:block;width:96%;" href="javascript:reportPhoto()">{_report}</a>
             <script n:if="$canReport ?? false">
                 function reportPhoto() {
-                uReportMsgTxt  = "Вы собираетесь пожаловаться на данную фотографию.";
-                uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
-                uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
+                uReportMsgTxt  = tr("going_to_report_photo");
+                uReportMsgTxt += "<br/>"+tr("report_question_text");
+                uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />"
 
-                MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
+                MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [
                     (function() {
                         res = document.querySelector("#uReportMsgInput").value;
                         xhr = new XMLHttpRequest();
                         xhr.open("GET", "/report/" + {$photo->getId()} + "?reason=" + res + "&type=photo", true);
                         xhr.onload = (function() {
                         if(xhr.responseText.indexOf("reason") === -1)
-                            MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
+                            MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]);
                         else
-                            MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
+                            MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]);
                         });
                         xhr.send(null);
                         }),
@@ -74,7 +74,6 @@
                 ]);
                 }
             </script>
-            <a href="{$photo->getURL()}" class="profile_link" target="_blank" style="display:block;width:96%;">{_open_original}</a>
         </div>
     </div>
 {/block}
diff --git a/Web/Presenters/templates/Photos/UnlinkPhoto.xml b/Web/Presenters/templates/Photos/UnlinkPhoto.xml
index 54498e06..80a4ad2e 100644
--- a/Web/Presenters/templates/Photos/UnlinkPhoto.xml
+++ b/Web/Presenters/templates/Photos/UnlinkPhoto.xml
@@ -1,20 +1,20 @@
 {extends "../@layout.xml"}
 
-{block title}Удалить фотографию?{/block}
+{block title}{_delete_photo}{/block}
 
 {block header}
-    Удаление фотографии
+    {_delete_photo}
 {/block}
 
 {block content}
-    Вы уверены что хотите удалить эту фотографию?
+    {_sure_deleting_photo}
     <br/>
     <br/>
     <form method="POST">
         <input type="hidden" value="{$csrfToken}" name="hash" />
         
-        <a href="{$_SERVER['HTTP_REFERER']}" class="button">Нет</a>
+        <a href="{$_SERVER['HTTP_REFERER']}" class="button">{_no}</a>
         &nbsp;
-        <button class="button">Да</button>
+        <button class="button">{_yes}</button>
     </form>
 {/block}
diff --git a/Web/Presenters/templates/User/Edit.xml b/Web/Presenters/templates/User/Edit.xml
index 6df070ea..b8e8398c 100644
--- a/Web/Presenters/templates/User/Edit.xml
+++ b/Web/Presenters/templates/User/Edit.xml
@@ -344,9 +344,9 @@
         <form method="POST" enctype="multipart/form-data">
             <div id="backdropEditor">
                 <div id="backdropFilePicker">
-                    <label class="button" style="">Обзор<input type="file" accept="image/*" name="backdrop1" style="display: none;"></label>
+                    <label class="button" style="">{_browse}<input type="file" accept="image/*" name="backdrop1" style="display: none;"></label>
                     <div id="spacer" style="width: 366px;"></div>
-                    <label class="button" style="">Обзор<input type="file" accept="image/*" name="backdrop2" style="display: none;"></label>
+                    <label class="button" style="">{_browse}<input type="file" accept="image/*" name="backdrop2" style="display: none;"></label>
                     <div id="spacer" style="width: 366px;"></div>
                 </div>
             </div>
diff --git a/Web/Presenters/templates/User/Settings.xml b/Web/Presenters/templates/User/Settings.xml
index 9d9a2b25..c8f0f61b 100644
--- a/Web/Presenters/templates/User/Settings.xml
+++ b/Web/Presenters/templates/User/Settings.xml
@@ -190,13 +190,13 @@
 
                 <script>
                     function viewBackupCodes() {
-                        MessageBox("Просмотр резервных кодов", `
+                        MessageBox(tr("viewing_backup_codes"), `
                             <form id="back-codes-view-form" method="post" action="/settings/2fa">
                                 <label for="password">Пароль</label>
                                 <input type="password" id="password" name="password" required />
                                 <input type="hidden" name="hash" value={$csrfToken} />
                             </form>
-                            `, ["Просмотреть", "Отменить"], [
+                            `, [tr("viewing"), tr("cancel")], [
                             () => {
                                 document.querySelector("#back-codes-view-form").submit();
                             }, Function.noop
@@ -204,13 +204,13 @@
                     }
 
                     function disableTwoFactorAuth() {
-                        MessageBox("Отключить 2FA", `
+                        MessageBox(tr("disable_2fa"), `
                             <form id="two-factor-auth-disable-form" method="post" action="/settings/2fa/disable">
                                 <label for="password">Пароль</label>
                                 <input type="password" id="password" name="password" required />
                                 <input type="hidden" name="hash" value={$csrfToken} />
                             </form>
-                            `, ["Отключить", "Отменить"], [
+                            `, [tr("disable"), tr("cancel")], [
                             () => {
                                 document.querySelector("#two-factor-auth-disable-form").submit();
                             }, Function.noop
diff --git a/Web/Presenters/templates/User/VerifyPhone.xml b/Web/Presenters/templates/User/VerifyPhone.xml
index d65b6bb4..a16101cb 100644
--- a/Web/Presenters/templates/User/VerifyPhone.xml
+++ b/Web/Presenters/templates/User/VerifyPhone.xml
@@ -1,13 +1,13 @@
 {extends "../@layout.xml"}
-{block title}Подтвердить номер телефона{/block}
+{block title}{_verify_phone_number}{/block}
 
 {block header}
-    Подтвердить номер телефона
+    {_verify_phone_number}
 {/block}
 
 {block content}
     <center>
-        <p>Мы отправили SMS с кодом на номер <b>{substr_replace($change->number, "*****", 5, 5)}</b>, введите его сюда:</p>
+        <p>{_we_sended_first} <b>{substr_replace($change->number, "*****", 5, 5)}</b>, {_we_sended_end}:</p>
         
         <form method="POST">
             <input type="text" name="code" placeholder="34156, например" required />
diff --git a/Web/Presenters/templates/User/View.xml b/Web/Presenters/templates/User/View.xml
index b3cd3257..9014750d 100644
--- a/Web/Presenters/templates/User/View.xml
+++ b/Web/Presenters/templates/User/View.xml
@@ -119,10 +119,10 @@
                         {_warn_user_action}
                     </a>
                     <a href="/admin/user{$user->getId()}/bans" class="profile_link">
-                        Блокировки
+                        {_blocks}
                     </a>
                     <a href="/admin/logs?uid={$user->getId()}" class="profile_link" style="width: 194px;">
-                        Последние действия
+                        {_last_actions}
                     </a>
                 {/if}
 
@@ -173,20 +173,20 @@
                 <a class="profile_link" style="display:block;width:96%;" href="javascript:reportUser()">{_report}</a>
                 <script>
                     function reportUser() {
-                        uReportMsgTxt  = "Вы собираетесь пожаловаться на данного пользователя.";
-                        uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
-                        uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
+                        uReportMsgTxt  = tr("going_to_report_user");
+                        uReportMsgTxt += "<br/>"+tr("report_question_text");
+                        uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />"
 
-                        MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
+                        MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [
                             (function() {
                                 res = document.querySelector("#uReportMsgInput").value;
                                 xhr = new XMLHttpRequest();
                                 xhr.open("GET", "/report/" + {$user->getId()} + "?reason=" + res + "&type=user", true);
                                 xhr.onload = (function() {
                                     if(xhr.responseText.indexOf("reason") === -1)
-                                        MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
+                                        MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]);
                                     else
-                                        MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
+                                        MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]);
                                 });
                                 xhr.send(null);
                             }),
diff --git a/Web/Presenters/templates/User/banned.xml b/Web/Presenters/templates/User/banned.xml
index 8abe4d89..8afcf642 100644
--- a/Web/Presenters/templates/User/banned.xml
+++ b/Web/Presenters/templates/User/banned.xml
@@ -3,9 +3,9 @@
     <p>
         {tr("user_banned", htmlentities($user->getFirstName()))|noescape}<br/>
         {_user_banned_comment} <b>{$user->getBanReason()}</b>.<br/>
-        Пользователь заблокирован
-        <span n:if="$user->getUnbanTime() !== NULL">до: <b>{$user->getUnbanTime()}</b></span>
-        <span n:if="$user->getUnbanTime() === NULL"><b>навсегда</b></span>
+        {_user_is_blocked}
+        <span n:if="$user->getUnbanTime() !== NULL">{_before}: <b>{$user->getUnbanTime()}</b></span>
+        <span n:if="$user->getUnbanTime() === NULL"><b>{_forever}</b></span>
     </p>
     {if isset($thisUser)}
         <p n:if="$thisUser->getChandlerUser()->can('access')->model('admin')->whichBelongsTo(NULL) || $thisUser->getChandlerUser()->can('write')->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)">
diff --git a/Web/Presenters/templates/Videos/Edit.xml b/Web/Presenters/templates/Videos/Edit.xml
index 4a97b319..c9ca9a9d 100644
--- a/Web/Presenters/templates/Videos/Edit.xml
+++ b/Web/Presenters/templates/Videos/Edit.xml
@@ -1,5 +1,5 @@
 {extends "../@layout.xml"}
-{block title}Изменить видеозапись{/block}
+{block title}{_change_video}{/block}
 
 {block header}
     <a href="{$thisUser->getURL()}">{$thisUser->getCanonicalName()}</a>
@@ -8,12 +8,12 @@
     »
     <a href="/video{$video->getPrettyId()}">{_video}</a>
     »
-    Изменить видеозапись
+    {_change_video}
 {/block}
 
 {block content}
 <div class="container_gray">
-    <h4>Изменить видеозапись</h4>
+    <h4>{_change_video}</h4>
     <form method="post" enctype="multipart/form-data">
       <table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
         <tbody>
diff --git a/Web/Presenters/templates/Videos/View.xml b/Web/Presenters/templates/Videos/View.xml
index 7cc41fe4..38967b5e 100644
--- a/Web/Presenters/templates/Videos/View.xml
+++ b/Web/Presenters/templates/Videos/View.xml
@@ -19,7 +19,7 @@
         {else}
             {var $driver = $video->getVideoDriver()}
             {if !$driver}
-                Эта видеозапись не поддерживается в вашей версии OpenVK.
+                {_unknown_video}
             {else}
                 {$driver->getEmbed()|noescape}
             {/if}
@@ -70,20 +70,20 @@
 
             <script n:if="$canReport ?? false">
                 function reportVideo() {
-                uReportMsgTxt  = "Вы собираетесь пожаловаться на данную видеозапись.";
-                uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
-                uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
+                uReportMsgTxt  = tr("going_to_report_video");
+                uReportMsgTxt += "<br/>"+tr("report_question_text");
+                uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />"
 
-                MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
+                MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [
                     (function() {
                         res = document.querySelector("#uReportMsgInput").value;
                         xhr = new XMLHttpRequest();
                         xhr.open("GET", "/report/" + {$video->getId()} + "?reason=" + res + "&type=video", true);
                         xhr.onload = (function() {
                         if(xhr.responseText.indexOf("reason") === -1)
-                            MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
+                            MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]);
                         else
-                            MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
+                           MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]);
                         });
                         xhr.send(null);
                     }),
diff --git a/Web/Presenters/templates/Wall/Post.xml b/Web/Presenters/templates/Wall/Post.xml
index 8ac11bb7..bd40b6c5 100644
--- a/Web/Presenters/templates/Wall/Post.xml
+++ b/Web/Presenters/templates/Wall/Post.xml
@@ -46,20 +46,20 @@
     </div>
     <script n:if="$canReport ?? false">
         function reportPost() {
-            uReportMsgTxt  = "Вы собираетесь пожаловаться на данную запись.";
-            uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
-            uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
+            uReportMsgTxt  = tr("going_to_report_post");
+            uReportMsgTxt += "<br/>"+tr("report_question_text");
+            uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />"
 
-            MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
+            MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [
                 (function() {
                     res = document.querySelector("#uReportMsgInput").value;
                     xhr = new XMLHttpRequest();
                     xhr.open("GET", "/report/" + {$post->getId()} + "?reason=" + res + "&type=post", true);
                     xhr.onload = (function() {
                         if(xhr.responseText.indexOf("reason") === -1)
-                            MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
+                            MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]);
                         else
-                            MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
+                            MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]);
                     });
                     xhr.send(null);
                 }),
diff --git a/Web/Presenters/templates/components/comment.xml b/Web/Presenters/templates/components/comment.xml
index 69238417..9be7d838 100644
--- a/Web/Presenters/templates/components/comment.xml
+++ b/Web/Presenters/templates/components/comment.xml
@@ -43,7 +43,7 @@
                             <a class="comment-reply">{_reply}</a>
                             {if $thisUser->getId() != $comment->getOwner()->getId()}
                                 {var $canReport = true}
-                                | <a href="javascript:reportComment()">Пожаловаться</a>
+                                | <a href="javascript:reportComment()">{_report}</a>
                             {/if}
                             <div style="float: right; font-size: .7rem;">
                                 <a class="post-like-button" href="/comment{$comment->getId()}/like?hash={rawurlencode($csrfToken)}">
@@ -87,20 +87,20 @@
 </table>
 <script n:if="$canReport ?? false">
     function reportComment() {
-        uReportMsgTxt  = "Вы собираетесь пожаловаться на данный комментарий.";
-        uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
-        uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
+        uReportMsgTxt  = tr("going_to_report_comment");
+        uReportMsgTxt += "<br/>"+tr("report_question_text");
+        uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />"
 
-        MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
+        MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [
             (function() {
                 res = document.querySelector("#uReportMsgInput").value;
                 xhr = new XMLHttpRequest();
                 xhr.open("GET", "/report/" + {$comment->getId()} + "?reason=" + res + "&type=comment", true);
                 xhr.onload = (function() {
                     if(xhr.responseText.indexOf("reason") === -1)
-                        MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
+                        MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]);
                     else
-                        MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
+                        MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]);
                     });
                 xhr.send(null);
             }),
diff --git a/Web/Presenters/templates/components/notifications/9601/_20_18_.xml b/Web/Presenters/templates/components/notifications/9601/_20_18_.xml
index 08a80a54..87eff8a3 100644
--- a/Web/Presenters/templates/components/notifications/9601/_20_18_.xml
+++ b/Web/Presenters/templates/components/notifications/9601/_20_18_.xml
@@ -1,7 +1,7 @@
 {var $gift   = $notification->getModel(0)}
 {var $sender = $notification->getModel(1)}
 
-<a href="{$sender->getURL()}"><b>{$sender->getCanonicalName()}</b></a> отправил вам подарок.
+<a href="{$sender->getURL()}"><b>{$sender->getCanonicalName()}</b></a> {_nt_sent_gift}.
 <div class="nobold">
     {$notification->getDateTime()}
 </div>
\ No newline at end of file
diff --git a/Web/Presenters/templates/components/post/microblogpost.xml b/Web/Presenters/templates/components/post/microblogpost.xml
index 6b0444c7..bc499818 100644
--- a/Web/Presenters/templates/components/post/microblogpost.xml
+++ b/Web/Presenters/templates/components/post/microblogpost.xml
@@ -82,7 +82,7 @@
                     </div>
                     <div n:if="$post->isAd()" style="color:grey;">
                         <br/>
-                        &nbsp;! Этот пост был размещён за взятку.
+                        &nbsp;! {_post_is_ad}
                     </div>
                     <div n:if="$post->isSigned()" class="post-signature">
                         {var $actualAuthor = $post->getOwner(false)}
diff --git a/Web/Presenters/templates/components/post/oldpost.xml b/Web/Presenters/templates/components/post/oldpost.xml
index 6e0b4c53..9dadeaab 100644
--- a/Web/Presenters/templates/components/post/oldpost.xml
+++ b/Web/Presenters/templates/components/post/oldpost.xml
@@ -73,7 +73,7 @@
                     </div>
                     <div n:if="$post->isAd()" style="color:grey;">
                         <br/>
-                        &nbsp;! Этот пост был размещён за взятку.
+                        &nbsp;! {_post_is_ad}
                     </div>
                     <div n:if="$post->isSigned()" class="post-signature">
                         {var $actualAuthor = $post->getOwner(false)}
diff --git a/locales/en.strings b/locales/en.strings
index f6e325d5..f09ee65e 100644
--- a/locales/en.strings
+++ b/locales/en.strings
@@ -149,6 +149,10 @@
 
 "user_banned" = "Unfortunately, we had to block the <b>$1</b> user page.";
 "user_banned_comment" = "Moderator's comment:";
+"verified_page" = "Verified page";
+"user_is_blocked" = "User is blocked";
+"before" = "before";
+"forever" = "forever";
 
 /* Wall */
 
@@ -213,6 +217,7 @@
 "graffiti" = "Graffiti";
 
 "reply" = "Reply";
+"post_is_ad" = "This post is sponsored.";
 
 "edited_short" = "edited";
 
@@ -339,9 +344,14 @@
 
 "create" = "Create";
 "albums" = "Albums";
+"album" = "Album";
+"photos" = "photos";
 "create_album" = "Create album";
 "edit_album" = "Edit album";
+"edit_photo" = "Edit photo";
 "creating_album" = "Creating album";
+"delete_photo" = "Delete photo";
+"sure_deleting_photo" = "Do you really want to delete this picture?";
 "upload_photo" = "Upload photo";
 "photo" = "Photo";
 "upload_button" = "Upload";
@@ -436,6 +446,8 @@
 
 "notes_closed" = "You can't attach note to post, because only you can see them.<br> You can change it in <a href=\"/settings?act=privacy\">settings</a>.";
 "do_not_attach_note" = "Do not attach note";
+"something" = "Something";
+"supports_xhtml" = "from (X)HTML supported.";
 
 /* Notes: Article Viewer */
 "aw_legacy_ui" = "Legacy interface";
@@ -651,6 +663,9 @@
 "two_factor_authentication_backup_codes_1" = "Backup codes allow you to validate your login when you don't have access to your phone, for example, while traveling.";
 "two_factor_authentication_backup_codes_2" = "You have <b>10 more codes</b>, each code can only be used once. Print them out, put them away in a safe place and use them when you need codes to validate your login.";
 "two_factor_authentication_backup_codes_3" = "You can get new codes if they run out. Only the last created backup codes are valid.";
+"viewing_backup_codes" = "View backup codes";
+"disable_2fa" = "Disable 2FA";
+"viewing" = "View";
 
 /* Sorting */
 
@@ -677,6 +692,8 @@
 "videos_other" = "$1 videos";
 
 "view_video" = "View";
+"change_video" = "Change video";
+"unknown_video" = "This video is not supported in your version of OpenVK.";
 
 /* Notifications */
 
@@ -716,6 +733,7 @@
 "nt_mention_in_video" = "in discussion of this video";
 "nt_mention_in_note" = "in discussion of this note";
 "nt_mention_in_topic" = "in the discussion";
+"nt_sent_gift" = "sent you a gift";
 
 /* Time */
 
@@ -930,6 +948,20 @@
 "text_of_the_post" = "Text of the post";
 "today" = "today";
 
+"will_be_watched" = "It will be reviewed by the moderators soon";
+
+"report_question" = "Report?";
+"report_question_text" = "What exactly do you find unacceptable about this material?";
+"report_reason" = "Report reason";
+"reason" = "Reason";
+"going_to_report_app" = "You are about to report this application.";
+"going_to_report_club" = "You are about to report this club.";
+"going_to_report_photo" = "You are about to report this photo.";
+"going_to_report_user" = "You are about to report this user.";
+"going_to_report_video" = "You are about to report this video.";
+"going_to_report_post" = "You are about to report this post.";
+"going_to_report_comment" = "You are about to report this comment.";
+
 "comment" = "Comment";
 "sender" = "Sender";
 
@@ -1139,6 +1171,96 @@
 "media_file_corrupted_or_too_large" = "The media content file is corrupted or too large.";
 "post_is_empty_or_too_big" = "The post is empty or too big.";
 "post_is_too_big" = "The post is too big.";
+
+"error_sending_report" = "Failed to make a report...";
+
+"error_when_saving_gift" = "Error when saving gift";
+"error_when_saving_gift_bad_image" = "Gift image is crooked.";
+"error_when_saving_gift_no_image" = "Please, upload gift's image.";
+"video_uploads_disabled" = "Video uploads are disabled by the system administrator.";
+
+"error_when_publishing_comment" = "Error when publishing comment";
+"error_when_publishing_comment_description" = "Image is corrupted, too big or one side is many times larger than the other.";
+"error_comment_empty" = "Comment is empty or too big.";
+"error_comment_too_big" = "Comment is too big.";
+"error_comment_file_too_big" = "Media file is corrupted or too big.";
+
+"comment_is_added" = "Comment has been added";
+"comment_is_added_desc" = "Your comment will appear on page.";
+
+"error_access_denied_short" = "Access denied";
+"error_access_denied" = "You don't have rights to edit this resource";
+"success" = "Success";
+"comment_will_not_appear" = "This comment will no longer appear.";
+
+"error_when_gifting" = "Failed to gift";
+"error_user_not_exists" = "User or pack does not exist.";
+"error_no_rights_gifts" = "Failed to check rights on gift.";
+"error_no_more_gifts" = "You no longer have such gifts.";
+"error_no_money" = "Shout out to a beggar.";
+
+"gift_sent" = "Gift sent";
+"gift_sent_desc" = "You sent a gift to $1 for $2 votes";
+
+"error_on_server_side" = "An error occurred on the server side. Contact your system administrator.";
+"error_no_group_name" = "You did not enter a group name.";
+
+"success_action" = "Action successful";
+"connection_error" = "Connection error";
+"connection_error_desc" = "Failed to connect to telemetry service.";
+
+"error_when_uploading_photo" = "Failed to save photo";
+
+"new_changes_desc" = "New data will appear in your group.";
+"comment_is_changed" = "Admin comment changed";
+"comment_is_deleted" = "Admin comment deleted";
+"comment_is_too_long" = "Comment is too long ($1 symbols instead 36)";
+"x_no_more_admin" = "$1 no longer an administrator.";
+"x_is_admin" = "$1 appointed as an administrator.";
+
+"x_is_now_hidden" = "Now $1 will be shown as a normal subscriber to everyone except other admins";
+"x_is_now_showed" = "Now everyone will know that $1 is an administrator.";
+
+"note_is_deleted" = "Note was deleted";
+"note_x_is_now_deleted" = "Note \"$1\" was successfully deleted.";
+"new_data_accepted" = "New data accepted.";
+
+"album_is_deleted" = "Album was deleted";
+"album_x_is_deleted" = "Album $1 was successfully deleted.";
+
+"error_adding_to_deleted" = "Failed to save photo to <b>DELETED</b>.";
+"error_adding_to_x" = "Failed to save photo to <b>$1</b>.";
+"no_photo" = "No photo";
+
+"select_file" = "Select file";
+"new_description_will_appear" = "The updated description will appear on the photo page..";
+"photo_is_deleted" = "Photo was deleted";
+"photo_is_deleted_desc" = "This photo has been successfully deleted.";
+
+"no_video" = "No video";
+"no_video_desc" = "Select a file or provide a link.";
+"error_occured" = "Error occured";
+"error_video_damaged_file" = "The file is corrupted or does not contain video.";
+"error_video_incorrect_link" = "Perhaps the link is incorrect.";
+"error_video_no_title" = "Video can't be published without title.";
+
+"new_data_video" = "The updated description will appear on the video page.";
+"error_deleting_video" = "Failed to delete video";
+"login_please" = "You are not signed in.";
+"invalid_code" = "Failed to verify phone number: Invalid code.";
+
+"error_max_pinned_clubs" = "Maximum count of the pinned groups is 10.";
+"error_viewing_subs" = "You cannot view the full list of subscriptions $1.";
+"error_status_too_long" = "Status is too long ($1 instead 255)";
+"death" = "Death...";
+"nehay" = "Live long!";
+"user_successfully_banned" = "User was successfully banned.";
+
+"content_is_deleted" = "The content has been removed and the user has received a warning.";
+"report_is_ignored" = "Report was ignored.";
+"group_owner_is_banned" = "Group's owner was successfully banned";
+"group_is_banned" = "Group was successfully banned";
+
 "description_too_long" = "Description is too long.";
 
 /* Admin actions */
@@ -1147,6 +1269,8 @@
 "manage_user_action" = "Manage user";
 "manage_group_action" = "Manage group";
 "ban_user_action" = "Ban user";
+"blocks" = "Blocks";
+"last_actions" = "Last actions";
 "unban_user_action" = "Unban user";
 "warn_user_action" = "Warn user";
 "ban_in_support_user_action" = "Ban in support";
@@ -1157,6 +1281,7 @@
 
 "admin" = "Admin panel";
 
+"sandbox_for_developers" = "Sandbox for developers";
 "admin_ownerid" = "Owner ID";
 "admin_author" = "Author";
 "admin_name" = "Name";
@@ -1242,6 +1367,11 @@
 "admin_banned_link_not_specified" = "The link is not specified";
 "admin_banned_link_not_found" = "Link not found";
 
+"admin_gift_moved_successfully" = "Gift moved successfully";
+"admin_gift_moved_to_recycle" = "This gift will now be in <b>Recycle Bin</b>.";
+
+"logs" = "Logs";
+"logs_anything" = "Anything";
 "logs_adding" = "Creation";
 "logs_editing" = "Editing";
 "logs_removing" = "Deletion";
@@ -1250,6 +1380,28 @@
 "logs_edited" = "edited";
 "logs_removed" = "removed";
 "logs_restored" = "restored";
+"logs_id_post" = "ID записи";
+"logs_id_object" = "ID объекта";
+"logs_uuid_user" = "UUID пользователя";
+"logs_change_type" = "Тип изменения";
+"logs_change_object" = "Тип объекта";
+
+"logs_user" = "User";
+"logs_object" = "Object";
+"logs_type" = "Type";
+"logs_changes" = "Changes";
+"logs_time" = "Time";
+
+"bans_history" = "Blocks history";
+"bans_history_blocked" = "Blocked";
+"bans_history_initiator" = "Initiator";
+"bans_history_start" = "Start";
+"bans_history_end" = "End";
+"bans_history_time" = "Time";
+"bans_history_reason" = "Reason";
+"bans_history_start" = "Start";
+"bans_history_removed" = "Removed";
+"bans_history_active" = "Active block";
 
 /* Paginator (deprecated) */
 
@@ -1297,6 +1449,8 @@
 
 "warning" = "Warning";
 "question_confirm" = "This action can't be undone. Do you really wanna do it?";
+"confirm_m" = "Confirm";
+"action_successfully" = "Success";
 
 /* User alerts */
 
@@ -1310,6 +1464,8 @@
 
 /* Away */
 
+"transition_is_blocked" = "Transition is blocked";
+"caution" = "Caution";
 "url_is_banned" = "Link is not allowed";
 "url_is_banned_comment" = "The <b>$1</b> administration recommends not to follow this link.";
 "url_is_banned_comment_r" = "The <b>$1</b> administration recommends not to follow this link.<br><br>The reason is: <b>$2</b>";
@@ -1574,6 +1730,41 @@
 
 "no_results" = "No results";
 
+/* BadBrowser */
+
+"deprecated_browser" = "Deprecated browser";
+"deprecated_browser_description" = "To view this content, you will need Firefox ESR 52+ or an equivalent World Wide Web navigator. Sorry about that.";
+
+/* Statistics */
+
+"coverage" = "Coverage";
+"coverage_this_week" = "This graph shows the coverage over the last 7 days.";
+"views" = "Views";
+"views_this_week" = "This graph shows the views of community posts over the last 7 days.";
+
+"full_coverage" = "Full coverage";
+"all_views" = "All views";
+
+"subs_coverage" = "Subscribers coverage";
+"subs_views" = "Subscribers views";
+
+"viral_coverage" = "Viral coverage";
+"viral_views" = "Viral views";
+
+/* Sudo */
+
+"you_entered_as" = "You logged as";
+"please_rights" = "please respect the right to privacy of other people's correspondence and do not abuse user swapping.";
+"click_on" = "Click";
+"there" = "there";
+"to_leave" = "to logout";
+
+/* Phone number */
+
+"verify_phone_number" = "Confirm phone number";
+"we_sended_first" = "We sended SMS with code on number";
+"we_sended_end" = "enter it here";
+
 /* Mobile */
 "mobile_friends" = "Friends";
 "mobile_photos" = "Photos";
@@ -1589,3 +1780,40 @@
 "mobile_like" = "Like";
 "mobile_user_info_hide" = "Hide";
 "mobile_user_info_show_details" = "Show details";
+
+/* Moderation */
+
+"section" = "Section";
+"template_ban" = "Ban by template";
+"active_templates" = "Active templates";
+"users_reports" = "Users reports";
+"substring" = "Substring";
+"n_user" = "User";
+"time_before" = "Time earlier than";
+"time_after" = "Time later than";
+"where_for_search" = "WHERE for search by section";
+"block_params" = "Block params";
+"only_rollback" = "Only rollback";
+"only_block" = "Only blocking";
+"rollback_and_block" = "Rollback and blocking";
+"subm" = "Apply";
+
+"select_section_for_start" = "Choose a section to get started";
+"results_will_be_there" = "Search results will be displayed here";
+"search_results" = "Search results";
+"cnt" = "pcs";
+
+"link_to_page" = "Link on page";
+"or_subnet" = "or subnet";
+"error_when_searching" = "Error while executing request";
+"no_found" = "No found";
+"operation_successfully" = "Operation completed successfully";
+
+"unknown_error" = "Unknown error";
+"templates" = "Template";
+"type" = "Type";
+"count" = "Count";
+"time" = "Time";
+
+"roll_back" = "rollback";
+"roll_backed" = "rollbacked";
diff --git a/locales/ru.strings b/locales/ru.strings
index b66a5404..787f1648 100644
--- a/locales/ru.strings
+++ b/locales/ru.strings
@@ -132,6 +132,10 @@
 "updated_at" = "Обновлено $1";
 "user_banned" = "К сожалению, нам пришлось заблокировать страницу пользователя <b>$1</b>.";
 "user_banned_comment" = "Комментарий модератора:";
+"verified_page" = "Подтверждённая страница";
+"user_is_blocked" = "Пользователь заблокирован";
+"before" = "до";
+"forever" = "навсегда";
 
 /* Wall */
 
@@ -191,6 +195,7 @@
 "version_incompatibility" = "Не удалось отобразить это вложение. Возможно, база данных несовместима с текущей версией OpenVK.";
 "graffiti" = "Граффити";
 "reply" = "Ответить";
+"post_is_ad" = "Этот пост был размещён за взятку.";
 "edited_short" = "ред.";
 
 /* Friends */
@@ -320,10 +325,15 @@
 /* Albums */
 
 "create" = "Создать";
+"album" = "Альбом";
 "albums" = "Альбомы";
+"photos" = "фотографий";
 "create_album" = "Создать альбом";
 "edit_album" = "Редактировать альбом";
+"edit_photo" = "Изменить фотографию";
 "creating_album" = "Создание альбома";
+"delete_photo" = "Удалить фотографию";
+"sure_deleting_photo" = "Вы уверены, что хотите удалить эту фотографию?";
 "upload_photo" = "Загрузить фотографию";
 "photo" = "Фотография";
 "upload_button" = "Загрузить";
@@ -419,6 +429,8 @@
 
 "notes_closed" = "Вы не можете прикрепить заметку к записи, так как ваши заметки видны только вам.<br><br> Вы можете поменять это в <a href=\"/settings?act=privacy\">настройках</a>.";
 "do_not_attach_note" = "Не прикреплять заметку";
+"something" = "Кое-что";
+"supports_xhtml" = "из (X)HTML поддерживается.";
 
 /* Notes: Article Viewer */
 "aw_legacy_ui" = "Старый интерфейс";
@@ -609,6 +621,9 @@
 "two_factor_authentication_backup_codes_1" = "Резервные коды позволяют подтверждать вход, когда у вас нет доступа к телефону, например, в путешествии.";
 "two_factor_authentication_backup_codes_2" = "У вас есть ещё <b>10 кодов</b>, каждым кодом можно воспользоваться только один раз. Распечатайте их, уберите в надежное место и используйте, когда потребуются коды для подтверждения входа.";
 "two_factor_authentication_backup_codes_3" = "Вы можете получить новые коды, если они заканчиваются. Действительны только последние созданные резервные коды.";
+"viewing_backup_codes" = "Просмотр резервных кодов";
+"disable_2fa" = "Отключить 2FA";
+"viewing" = "Просмотреть";
 
 /* Sorting */
 
@@ -634,6 +649,8 @@
 "videos_many" = "$1 видеозаписей";
 "videos_other" = "$1 видеозаписей";
 "view_video" = "Просмотр";
+"change_video" = "Изменить видеозапись";
+"unknown_video" = "Эта видеозапись не поддерживается в вашей версии OpenVK.";
 
 /* Notifications */
 
@@ -670,6 +687,7 @@
 "nt_mention_in_video" = "в обсуждении видеозаписи";
 "nt_mention_in_note" = "в обсуждении заметки";
 "nt_mention_in_topic" = "в обсуждении";
+"nt_sent_gift" = "отправил вам подарок";
 
 /* Time */
 
@@ -863,6 +881,20 @@
 "text_of_the_post" = "Текст записи";
 "today" = "сегодня";
 
+"will_be_watched" = "Скоро её рассмотрят модераторы";
+
+"report_question" = "Пожаловаться?";
+"report_question_text" = "Что именно вам кажется недопустимым в этом материале?";
+"report_reason" = "Причина жалобы";
+"reason" = "Причина";
+"going_to_report_app" = "Вы собираетесь пожаловаться на данное приложение.";
+"going_to_report_club" = "Вы собираетесь пожаловаться на данное сообщество.";
+"going_to_report_photo" = "Вы собираетесь пожаловаться на данную фотографию.";
+"going_to_report_user" = "Вы собираетесь пожаловаться на данного пользователя.";
+"going_to_report_video" = "Вы собираетесь пожаловаться на данную видеозапись.";
+"going_to_report_post" = "Вы собираетесь пожаловаться на данную запись.";
+"going_to_report_comment" = "Вы собираетесь пожаловаться на данный комментарий.";
+
 "comment" = "Комментарий";
 "sender" = "Отправитель";
 "author" = "Автор";
@@ -1038,6 +1070,93 @@
 "media_file_corrupted_or_too_large" = "Файл медиаконтента повреждён или слишком велик.";
 "post_is_empty_or_too_big" = "Пост пустой или слишком большой.";
 "post_is_too_big" = "Пост слишком большой.";
+"error_sending_report" = "Не удалось подать жалобу...";
+"error_when_saving_gift" = "Не удалось сохранить подарок";
+"error_when_saving_gift_bad_image" = "Изображение подарка кривое.";
+"error_when_saving_gift_no_image" = "Пожалуйста, загрузите изображение подарка.";
+"video_uploads_disabled" = "Загрузки видео отключены администратором.";
+
+"error_when_publishing_comment" = "Не удалось опубликовать комментарий";
+"error_when_publishing_comment_description" = "Файл изображения повреждён, слишком велик или одна сторона изображения в разы больше другой.";
+"error_comment_empty" = "Комментарий пустой или слишком большой.";
+"error_comment_too_big" = "Комментарий слишком большой.";
+"error_comment_file_too_big" = "Файл медиаконтента повреждён или слишком велик.";
+
+"comment_is_added" = "Комментарий добавлен";
+"comment_is_added_desc" = "Ваш комментарий появится на странице.";
+
+"error_access_denied_short" = "Ошибка доступа";
+"error_access_denied" = "У вас недостаточно прав, чтобы редактировать этот ресурс";
+"success" = "Успешно";
+"comment_will_not_appear" = "Этот комментарий больше не будет показыватся.";
+
+"error_when_gifting" = "Не удалось подарить";
+"error_user_not_exists" = "Пользователь или набор не существуют.";
+"error_no_rights_gifts" = "Не удалось подтвердить права на подарок.";
+"error_no_more_gifts" = "У вас больше не осталось таких подарков.";
+"error_no_money" = "Ору нищ не пук.";
+
+"gift_sent" = "Подарок отправлен";
+"gift_sent_desc" = "Вы отправили подарок <b>$1</b> за $2 голосов";
+
+"error_on_server_side" = "Произошла ошибка на стороне сервера. Обратитесь к системному администратору.";
+"error_no_group_name" = "Вы не ввели название группы.";
+
+"success_action" = "Операция успешна";
+"connection_error" = "Ошибка подключения";
+"connection_error_desc" = "Не удалось подключится к службе телеметрии.";
+
+"error_when_uploading_photo" = "Не удалось сохранить фотографию.";
+
+"new_changes_desc" = "Новые данные появятся в вашей группе.";
+"comment_is_changed" = "Комментарий к администратору изменён";
+"comment_is_deleted" = "Комментарий к администратору удален";
+"comment_is_too_long" = "Комментарий слишком длинный ($1 символов вместо 36 символов)";
+"x_no_more_admin" = "$1 больше не администратор.";
+"x_is_admin" = "$1 назначен(а) администратором.";
+
+"x_is_now_hidden" = "Теперь $1 будет показываться как обычный подписчик всем, кроме других администраторов";
+"x_is_now_showed" = "Теперь все будут знать, что $1 — администратор.";
+
+"note_is_deleted" = "Заметка удалена";
+"note_x_is_now_deleted" = "Заметка \"$1\" была успешно удалена.";
+"new_data_accepted" = "Новые данные приняты.";
+
+"album_is_deleted" = "Альбом удалён";
+"album_x_is_deleted" = "Альбом $1 был успешно удалён.";
+
+"error_adding_to_deleted" = "Не удалось сохранить фотографию в <b>DELETED</b>.";
+"error_adding_to_x" = "Не удалось сохранить фотографию в <b>$1</b>.";
+"no_photo" = "Нету фотографии";
+
+"select_file" = "Выберите файл";
+"new_description_will_appear" = "Обновлённое описание появится на странице с фоткой.";
+"photo_is_deleted" = "Фотография удалена";
+"photo_is_deleted_desc" = "Эта фотография была успешно удалена.";
+
+"no_video" = "Нет видеозаписи";
+"no_video_desc" = "Выберите файл или укажите ссылку.";
+"error_occured" = "Произошла ошибка";
+"error_video_damaged_file" = "Файл повреждён или не содержит видео.";
+"error_video_incorrect_link" = "Возможно, ссылка некорректна.";
+"error_video_no_title" = "Видео не может быть опубликовано без названия.";
+
+"new_data_video" = "Обновлённое описание появится на странице с видео.";
+"error_deleting_video" = "Не удалось удалить видео";
+"login_please" = "Вы не вошли в аккаунт.";
+"invalid_code" = "Не удалось подтвердить номер телефона: неверный код.";
+
+"error_max_pinned_clubs" = "Находится в левом меню могут максимум 10 групп";
+"error_viewing_subs" = "Вы не можете просматривать полный список подписок $1.";
+"error_status_too_long" = "Статус слишком длинный ($1 символов вместо 255 символов)";
+"death" = "Смэрть...";
+"nehay" = "Нехай живе!";
+"user_successfully_banned" = "Пользователь успешно забанен.";
+
+"content_is_deleted" = "Контент удалён, а пользователю прилетело предупреждение.";
+"report_is_ignored" = "Жалоба проигнорирована.";
+"group_owner_is_banned" = "Создатель сообщества успешно забанен.";
+"group_is_banned" = "Сообщество успешно забанено";
 "description_too_long" = "Описание слишком длинное.";
 
 /* Admin actions */
@@ -1046,6 +1165,8 @@
 "manage_user_action" = "Управление пользователем";
 "manage_group_action" = "Управление группой";
 "ban_user_action" = "Заблокировать пользователя";
+"blocks" = "Блокировки";
+"last_actions" = "Последние действия";
 "unban_user_action" = "Разблокировать пользователя";
 "warn_user_action" = "Предупредить пользователя";
 "ban_in_support_user_action" = "Заблокировать в поддержке";
@@ -1055,6 +1176,7 @@
 /* Admin panel */
 
 "admin" = "Админ-панель";
+"sandbox_for_developers" = "Sandbox для разработчиков";
 "admin_ownerid" = "ID владельца";
 "admin_author" = "Автор";
 "admin_name" = "Имя";
@@ -1129,6 +1251,12 @@
 "admin_banned_link_initiator" = "Инициатор";
 "admin_banned_link_not_specified" = "Ссылка не указана";
 "admin_banned_link_not_found" = "Ссылка не найдена";
+
+"admin_gift_moved_successfully" = "Подарок успешно перемещён";
+"admin_gift_moved_to_recycle" = "Теперь подарок находится в <b>корзине</b>.";
+
+"logs" = "Логи";
+"logs_anything" = "Любое";
 "logs_adding" = "Создание";
 "logs_editing" = "Редактирование";
 "logs_removing" = "Удаление";
@@ -1137,6 +1265,28 @@
 "logs_edited" = "отредактировал";
 "logs_removed" = "удалил";
 "logs_restored" = "восстановил";
+"logs_id_post" = "ID записи";
+"logs_id_object" = "ID объекта";
+"logs_uuid_user" = "UUID пользователя";
+"logs_change_type" = "Тип изменения";
+"logs_change_object" = "Тип объекта";
+
+"logs_user" = "Пользователь";
+"logs_object" = "Объект";
+"logs_type" = "Тип";
+"logs_changes" = "Изменения";
+"logs_time" = "Время";
+
+"bans_history" = "История блокировок";
+"bans_history_blocked" = "Забаненный";
+"bans_history_initiator" = "Инициатор";
+"bans_history_start" = "Начало";
+"bans_history_end" = "Конец";
+"bans_history_time" = "Время";
+"bans_history_reason" = "Причина";
+"bans_history_start" = "Начало";
+"bans_history_removed" = "Снята";
+"bans_history_active" = "Активная блокировка";
 
 /* Paginator (deprecated) */
 
@@ -1186,6 +1336,8 @@
 "close" = "Закрыть";
 "warning" = "Внимание";
 "question_confirm" = "Это действие нельзя отменить. Вы действительно уверены в том что хотите сделать?";
+"confirm_m" = "Подтвердить";
+"action_successfully" = "Операция успешна";
 
 /* User alerts */
 
@@ -1199,6 +1351,8 @@
 
 /* Away */
 
+"transition_is_blocked" = "Переход по ссылке заблокирован";
+"caution" = "Предупреждение";
 "url_is_banned" = "Переход невозможен";
 "url_is_banned_comment" = "Администрация <b>$1</b> не рекомендует переходить по этой ссылке.";
 "url_is_banned_comment_r" = "Администрация <b>$1</b> не рекомендует переходить по этой ссылке.<br><br>Причина: <b>$2</b>";
@@ -1465,6 +1619,41 @@
 
 "no_results" = "Результатов нет";
 
+/* BadBrowser */
+
+"deprecated_browser" = "Устаревший браузер";
+"deprecated_browser_description" = "Для просмотра этого контента вам понадобится Firefox ESR 52+ или эквивалентный по функционалу навигатор по всемирной сети интернет. Сожалеем об этом.";
+
+/* Statistics */
+
+"coverage" = "Охват";
+"coverage_this_week" = "Этот график отображает охват за последние 7 дней.";
+"views" = "Просмотры";
+"views_this_week" = "Этот график отображает просмотры постов сообщества за последние 7 дней.";
+
+"full_coverage" = "Полный охват";
+"all_views" = "Все просмотры";
+
+"subs_coverage" = "Охват подписчиков";
+"subs_views" = "Просмотры подписчиков";
+
+"viral_coverage" = "Виральный охват";
+"viral_views" = "Виральные просмотры";
+
+/* Sudo */
+
+"you_entered_as" = "Вы вошли как";
+"please_rights" = "пожалуйста, уважайте право на тайну переписки других людей и не злоупотребляйте подменой пользователя.";
+"click_on" = "Нажмите";
+"there" = "здесь";
+"to_leave" = "чтобы выйти";
+
+/* Phone number */
+
+"verify_phone_number" = "Подтвердить номер телефона";
+"we_sended_first" = "Мы отправили SMS с кодом на номер";
+"we_sended_end" = "введите его сюда";
+
 /* Mobile */
 "mobile_friends" = "Друзья";
 "mobile_photos" = "Фотографии";
@@ -1480,3 +1669,40 @@
 "mobile_like" = "Нравится";
 "mobile_user_info_hide" = "Скрыть";
 "mobile_user_info_show_details" = "Показать подробнее";
+
+/* Moderation */
+
+"section" = "Раздел";
+"template_ban" = "Бан по шаблону";
+"active_templates" = "Действующие шаблоны";
+"users_reports" = "Жалобы пользователей";
+"substring" = "Подстрока";
+"n_user" = "Пользователь";
+"time_before" = "Время раньше, чем";
+"time_after" = "Время позже, чем";
+"where_for_search" = "WHERE для поиска по разделу";
+"block_params" = "Параметры блокировки";
+"only_rollback" = "Только откат";
+"only_block" = "Только блокировка";
+"rollback_and_block" = "Откат и блокировка";
+"subm" = "Применить";
+
+"select_section_for_start" = "Выберите раздел для начала работы";
+"results_will_be_there" = "Здесь будут отображаться результаты поиска";
+"search_results" = "Результаты поиска";
+"cnt" = "шт";
+
+"link_to_page" = "Ссылка на страницу";
+"or_subnet" = "или подсеть";
+"error_when_searching" = "Ошибка при выполнении запроса";
+"no_found" = "Ничего не найдено";
+"operation_successfully" = "Операция завершена успешно";
+
+"unknown_error" = "Неизвестная ошибка";
+"templates" = "Шаблоны";
+"type" = "Тип";
+"count" = "Количество";
+"time" = "Время";
+
+"roll_back" = "откатить";
+"roll_backed" = "откачено";

From 43de40a0dc62c4cc29cdaad6192dac31ffa8f2e9 Mon Sep 17 00:00:00 2001
From: lalka2018 <99399973+lalka2016@users.noreply.github.com>
Date: Sun, 17 Sep 2023 19:19:25 +0300
Subject: [PATCH 17/26] Add Video Picker (#981)

---
 ServiceAPI/Wall.php                           |  44 ++++-
 VKAPI/Handlers/Wall.php                       |  27 ++-
 Web/Presenters/CommentPresenter.php           |  31 +++-
 Web/Presenters/WallPresenter.php              |  37 +++--
 .../templates/components/textArea.xml         |   8 +-
 Web/static/css/main.css                       |  34 ++++
 Web/static/img/video.png                      | Bin 0 -> 510 bytes
 Web/static/js/al_wall.js                      | 154 ++++++++++++++++++
 Web/static/js/messagebox.js                   |   2 +
 locales/en.strings                            |   8 +
 locales/ru.strings                            |   7 +
 11 files changed, 316 insertions(+), 36 deletions(-)
 create mode 100644 Web/static/img/video.png

diff --git a/ServiceAPI/Wall.php b/ServiceAPI/Wall.php
index 5677f7ba..787a998e 100644
--- a/ServiceAPI/Wall.php
+++ b/ServiceAPI/Wall.php
@@ -2,7 +2,7 @@
 namespace openvk\ServiceAPI;
 use openvk\Web\Models\Entities\Post;
 use openvk\Web\Models\Entities\User;
-use openvk\Web\Models\Repositories\{Posts, Notes};
+use openvk\Web\Models\Repositories\{Posts, Notes, Videos};
 
 class Wall implements Handler
 {
@@ -15,6 +15,7 @@ class Wall implements Handler
         $this->user  = $user;
         $this->posts = new Posts;
         $this->notes = new Notes;
+        $this->videos = new Videos;
     }
     
     function getPost(int $id, callable $resolve, callable $reject): void
@@ -95,4 +96,45 @@ class Wall implements Handler
 
         $resolve($arr);
     }
+
+    function getVideos(int $page = 1, callable $resolve, callable $reject)
+    {
+        $videos = $this->videos->getByUser($this->user, $page, 8);
+        $count  = $this->videos->getUserVideosCount($this->user);
+
+        $arr = [
+            "count"  => $count,
+            "items"  => [],
+        ];
+
+        foreach($videos as $video) {
+            $res = json_decode(json_encode($video->toVkApiStruct()), true);
+            $res["video"]["author_name"] = $video->getOwner()->getCanonicalName();
+
+            $arr["items"][] = $res;
+        }
+
+        $resolve($arr);
+    }
+
+    function searchVideos(int $page = 1, string $query, callable $resolve, callable $reject)
+    {
+        $dbc    = $this->videos->find($query);
+        $videos = $dbc->page($page, 8);
+        $count  = $dbc->size();
+
+        $arr = [
+            "count"  => $count,
+            "items"  => [],
+        ];
+
+        foreach($videos as $video) {
+            $res = json_decode(json_encode($video->toVkApiStruct()), true);
+            $res["video"]["author_name"] = $video->getOwner()->getCanonicalName();
+            
+            $arr["items"][] = $res;
+        }
+
+        $resolve($arr);
+    }
 }
diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php
index d52dfce1..6b78a0b0 100644
--- a/VKAPI/Handlers/Wall.php
+++ b/VKAPI/Handlers/Wall.php
@@ -463,28 +463,25 @@ final class Wall extends VKAPIRequestHandler
                 if($attachmentType == "photo") {
                     $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
                     if(!$attacc || $attacc->isDeleted())
-                        $this->fail(100, "Photo does not exists");
-                    if($attacc->getOwner()->getId() != $this->getUser()->getId())
-                        $this->fail(43, "You do not have access to this photo");
+                        $this->fail(100, "Invalid photo");
+                    if(!$attacc->getOwner()->getPrivacyPermission('photos.read', $this->getUser()))
+                        $this->fail(43, "Access to photo denied");
                     
                     $post->attach($attacc);
                 } elseif($attachmentType == "video") {
                     $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
                     if(!$attacc || $attacc->isDeleted())
                         $this->fail(100, "Video does not exists");
-                    if($attacc->getOwner()->getId() != $this->getUser()->getId())
-                        $this->fail(43, "You do not have access to this video");
+                    if(!$attacc->getOwner()->getPrivacyPermission('videos.read', $this->getUser()))
+                        $this->fail(43, "Access to video denied");
 
                     $post->attach($attacc);
                 } elseif($attachmentType == "note") {
                     $attacc = (new NotesRepo)->getNoteById($attachmentOwner, $attachmentId);
                     if(!$attacc || $attacc->isDeleted())
                         $this->fail(100, "Note does not exist");
-                    if($attacc->getOwner()->getId() != $this->getUser()->getId())
-                        $this->fail(43, "You do not have access to this note");
-                    
-                    if($attacc->getOwner()->getPrivacySetting("notes.read") < 1)
-                        $this->fail(11, "You can't attach note to post, because your notes list is closed. Change it in privacy settings in web-version.");
+                    if(!$attacc->getOwner()->getPrivacyPermission('notes.read', $this->getUser()))
+                        $this->fail(11, "Access to note denied");
 
                     $post->attach($attacc);
                 }
@@ -678,7 +675,7 @@ final class Wall extends VKAPIRequestHandler
         return $response;
     }
 
-    function createComment(int $owner_id, int $post_id, string $message, int $from_group = 0, string $attachments = "") {
+    function createComment(int $owner_id, int $post_id, string $message = "", int $from_group = 0, string $attachments = "") {
         $this->requireUser();
         $this->willExecuteWriteAction();
 
@@ -736,16 +733,16 @@ final class Wall extends VKAPIRequestHandler
                     $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
                     if(!$attacc || $attacc->isDeleted())
                         $this->fail(100, "Photo does not exists");
-                    if($attacc->getOwner()->getId() != $this->getUser()->getId())
-                        $this->fail(43, "You do not have access to this photo");
+                    if(!$attacc->getOwner()->getPrivacyPermission('photos.read', $this->getUser()))
+                        $this->fail(11, "Access to photo denied");
                     
                     $comment->attach($attacc);
                 } elseif($attachmentType == "video") {
                     $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
                     if(!$attacc || $attacc->isDeleted())
                         $this->fail(100, "Video does not exists");
-                    if($attacc->getOwner()->getId() != $this->getUser()->getId())
-                        $this->fail(43, "You do not have access to this video");
+                    if(!$attacc->getOwner()->getPrivacyPermission('videos.read', $this->getUser()))
+                        $this->fail(11, "Access to video denied");
 
                     $comment->attach($attacc);
                 }
diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php
index 29c54c78..b68c7d11 100644
--- a/Web/Presenters/CommentPresenter.php
+++ b/Web/Presenters/CommentPresenter.php
@@ -2,7 +2,7 @@
 namespace openvk\Web\Presenters;
 use openvk\Web\Models\Entities\{Comment, Notifications\MentionNotification, Photo, Video, User, Topic, Post};
 use openvk\Web\Models\Entities\Notifications\CommentNotification;
-use openvk\Web\Models\Repositories\{Comments, Clubs};
+use openvk\Web\Models\Repositories\{Comments, Clubs, Videos};
 
 final class CommentPresenter extends OpenVKPresenter
 {
@@ -73,7 +73,6 @@ final class CommentPresenter extends OpenVKPresenter
         # 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)
@@ -81,13 +80,28 @@ final class CommentPresenter extends OpenVKPresenter
                 
                 $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, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]);
-            }
         } catch(ISE $ex) {
             $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_file_too_big"));
         }
+
+        $videos = [];
+
+        if(!empty($this->postParam("videos"))) {
+            $un  = rtrim($this->postParam("videos"), ",");
+            $arr = explode(",", $un);
+            
+            if(sizeof($arr) < 11) {
+                foreach($arr as $dat) {
+                    $ids = explode("_", $dat);
+                    $video = (new Videos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]);
+    
+                    if(!$video || $video->isDeleted())
+                        continue;
+    
+                    $videos[] = $video;
+                }
+            }
+        }
         
         if(empty($this->postParam("text")) && !$photo && !$video)
             $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty"));
@@ -108,8 +122,9 @@ final class CommentPresenter extends OpenVKPresenter
         if(!is_null($photo))
             $comment->attach($photo);
         
-        if(!is_null($video))
-            $comment->attach($video);
+        if(sizeof($videos) > 0)
+            foreach($videos as $vid)
+                $comment->attach($vid);
         
         if($entity->getOwner()->getId() !== $this->user->identity->getId())
             if(($owner = $entity->getOwner()) instanceof User)
diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php
index 32ac421e..ef9e4689 100644
--- a/Web/Presenters/WallPresenter.php
+++ b/Web/Presenters/WallPresenter.php
@@ -3,7 +3,7 @@ namespace openvk\Web\Presenters;
 use openvk\Web\Models\Exceptions\TooMuchOptionsException;
 use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User};
 use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification};
-use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Comments};
+use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments};
 use Chandler\Database\DatabaseConnection;
 use Nette\InvalidStateException as ISE;
 use Bhaktaraz\RSSGenerator\Item;
@@ -231,10 +231,7 @@ final class WallPresenter extends OpenVKPresenter
 	
         if(!$canPost)
             $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment"));
-
-        if($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading'])
-            $this->flashFail("err", tr("error"), tr("video_uploads_disabled"));
-
+        
         $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"];
         if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) {
             $manager = $wallOwner->getManager($this->user->identity);
@@ -263,8 +260,8 @@ final class WallPresenter extends OpenVKPresenter
                 $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon);
             }
             
-            if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK)
-                $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"], $anon);
+            /*if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK)
+                $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"], $anon);*/
         } catch(\DomainException $ex) {
             $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted"));
         } catch(ISE $ex) {
@@ -295,8 +292,27 @@ final class WallPresenter extends OpenVKPresenter
                 $this->flashFail("err", " ");
             }
         }
+
+        $videos = [];
+
+        if(!empty($this->postParam("videos"))) {
+            $un  = rtrim($this->postParam("videos"), ",");
+            $arr = explode(",", $un);
+
+            if(sizeof($arr) < 11) {
+                foreach($arr as $dat) {
+                    $ids = explode("_", $dat);
+                    $video = (new Videos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]);
+    
+                    if(!$video || $video->isDeleted())
+                        continue;
+    
+                    $videos[] = $video;
+                }
+            }
+        }
         
-        if(empty($this->postParam("text")) && !$photo && !$video && !$poll && !$note)
+        if(empty($this->postParam("text")) && !$photo && sizeof($videos) < 1 && !$poll && !$note)
             $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
         
         try {
@@ -316,8 +332,9 @@ final class WallPresenter extends OpenVKPresenter
         if(!is_null($photo))
             $post->attach($photo);
         
-        if(!is_null($video))
-            $post->attach($video);
+        if(sizeof($videos) > 0)
+            foreach($videos as $vid)
+                $post->attach($vid);
         
         if(!is_null($poll))
             $post->attach($poll);
diff --git a/Web/Presenters/templates/components/textArea.xml b/Web/Presenters/templates/components/textArea.xml
index f76649d6..939e5ad5 100644
--- a/Web/Presenters/templates/components/textArea.xml
+++ b/Web/Presenters/templates/components/textArea.xml
@@ -17,6 +17,8 @@
             <div class="post-has-note">
                 
             </div>
+            <div class="post-has-videos"></div>
+
             <div n:if="$postOpts ?? true" class="post-opts">
                 {var $anonEnabled = OPENVK_ROOT_CONF['openvk']['preferences']['wall']['anonymousPosting']['enable']}
                 {if !is_null($thisUser) && !is_null($club ?? NULL) && $owner < 0}
@@ -55,7 +57,7 @@
                 </label>
             </div>
             <input type="file" class="postFileSel" id="postFilePic" name="_pic_attachment" accept="image/*" style="display:none;" />
-            <input n:if="!OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']" type="file" class="postFileSel" id="postFileVid" name="_vid_attachment" accept="video/*" style="display:none;" />
+            <input type="hidden" name="videos" value="" />
             <input type="hidden" name="poll" value="none" />
             <input type="hidden" id="note" name="note" value="none" />
             <input type="hidden" name="type" value="1" />
@@ -75,7 +77,7 @@
                         <img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-x-egon.png" />
                         {_photo}
                     </a>
-                    <a n:if="!OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']" href="javascript:void(document.querySelector('#post-buttons{$textAreaId} input[name=_vid_attachment]').click());">
+                    <a id="videoAttachment">
                         <img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-vnd.rn-realmedia.png" />
                         {_video}
                     </a>
@@ -105,6 +107,8 @@
 
         setupWallPostInputHandlers({$textAreaId});
     });
+
+    u("#post-buttons{$textAreaId} input[name='videos']")["nodes"].at(0).value = ""
 </script>
 
 {if $graffiti}
diff --git a/Web/static/css/main.css b/Web/static/css/main.css
index 46ad74b0..caa49283 100644
--- a/Web/static/css/main.css
+++ b/Web/static/css/main.css
@@ -1466,6 +1466,12 @@ body.scrolled .toTop:hover {
     display: none;
 }
 
+.post-has-videos {
+    margin-top: 11px;
+    margin-left: 3px;
+    color: #3c3c3c;
+}
+
 .post-upload::before, .post-has-poll::before, .post-has-note::before {
     content: " ";
     width: 8px;
@@ -1477,6 +1483,28 @@ body.scrolled .toTop:hover {
     margin-left: 2px;
 }
 
+.post-has-video {
+    padding-bottom: 4px;
+    cursor: pointer;
+}
+
+.post-has-video:hover span {
+    text-decoration: underline;
+}
+
+.post-has-video::before {
+    content: " ";
+    width: 14px;
+    height: 15px;
+    display: inline-block;
+    vertical-align: bottom;
+    background-image: url("/assets/packages/static/openvk/img/video.png");
+    background-repeat: no-repeat;
+    margin: 3px;
+    margin-left: 2px;
+    margin-bottom: -1px;
+}
+
 .post-opts {
     margin-top: 10px;
 }
@@ -2702,6 +2730,12 @@ body.article .floating_sidebar, body.article .page_content {
     font-size: 12px;
 }
 
+.topGrayBlock {
+    background: #F0F0F0;
+    height: 37px;
+    border-bottom: 1px solid #C7C7C7;
+}
+
 .edited {
     color: #9b9b9b;
 }
diff --git a/Web/static/img/video.png b/Web/static/img/video.png
new file mode 100644
index 0000000000000000000000000000000000000000..5c115f1c231030eafacd5f18a415232bbd0a6d12
GIT binary patch
literal 510
zcmV<a0RjGrP)<h;3K|Lk000e1NJLTq000gE000jN1^@s6)D-@700001b5ch_0Itp)
z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf6951U69E94oEQKA0h38YK~y+Tjgw7J
z!axv)-`PTo2n79q#OQ%&VxkZYiJ$nHATdGIC?tM`qX#|kmyF>cD72*o>e#70TJ^b1
zHt+7dGqbyNb9>K;=>#T|2{cUu!-KBHf;B!FL<Bt^2Q#DB{Xlkh2A<~uk5A7GpPPsK
zApS<X-46CN8Vy$cejgXtS1fLS@$&MDX0yr3WD?4;Y%DJ>k!hNt9F6jm=ks||N<o1=
z+&$d#lo1c*Xr#|ptHqs82XYwma|_h#y@hf#L)%0ym!oVp3pE;zaCv#onM@{>qlp_j
z>h(Iit_!MEDqJd+s8T&)v3JK~q*5u^whfOzgOZpDu~-ZmDXbS(saC5oom5X*?n<Uw
zEUZwN{70&1XY5)g61o8;nby~eeviFSj>fVqY;A0kL^T);P~JQEUyuF^D(~;HuIo^S
zVW7ObO=G{ezv#bzpuDq7G8`qg;@ItWLpd6m#m3g=x8ra)3>?$xbSOt713W%FB598b
z#Wj}NrDQ2reuX5lT_@Nq*CD?_q6UH@hT}N+0?diKEB)@BE&u=k07*qoM6N<$f@P%F
A4FCWD

literal 0
HcmV?d00001

diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js
index 4c8bb933..ef3d5dba 100644
--- a/Web/static/js/al_wall.js
+++ b/Web/static/js/al_wall.js
@@ -264,6 +264,160 @@ async function showArticle(note_id) {
     u("body").addClass("article");
 }
 
+$(document).on("click", "#videoAttachment", async (e) => {
+    e.preventDefault()
+    
+    let body = `
+        <div class="topGrayBlock">
+            <div style="padding-top: 11px;padding-left: 12px;">
+                <a href="/videos/upload">${tr("upload_new_video")}</a>
+                <input type="text" id="vquery" maxlength="20" placeholder="${tr("header_search")}" style="float: right;width: 160px;margin-right: 17px;margin-top: -2px;">
+            </div>
+        </div>
+
+        <div class="videosInsert" style="padding: 5px;height: 287px;overflow-y: scroll;"></div>
+    `
+
+    let form = e.currentTarget.closest("form")
+
+    MessageBox(tr("selecting_video"), body, [tr("close")], [Function.noop]);
+
+    // styles for messageboxx
+    document.querySelector(".ovk-diag-body").style.padding = "0"
+    document.querySelector(".ovk-diag-cont").style.width = "580px"
+    document.querySelector(".ovk-diag-body").style.height = "335px"
+
+    async function insertVideos(page, query = "") {
+        document.querySelector(".videosInsert").insertAdjacentHTML("beforeend", `<img id="loader" src="/assets/packages/static/openvk/img/loading_mini.gif">`)
+
+        let vidoses
+        let noVideosText = tr("no_videos")
+        if(query == "") {
+            vidoses = await API.Wall.getVideos(page)
+        } else {
+            vidoses = await API.Wall.searchVideos(page, query)
+            noVideosText = tr("no_videos_results")
+        }
+        
+        if(vidoses.count < 1) {
+            document.querySelector(".videosInsert").innerHTML = `<span>${noVideosText}</span>`
+        }
+
+        let pagesCount = Math.ceil(Number(vidoses.count) / 8)
+        u("#loader").remove()
+        let insert = document.querySelector(".videosInsert")
+
+        for(const vid of vidoses.items) {
+            let isAttached = (form.querySelector("input[name='videos']").value.includes(`${vid.video.owner_id}_${vid.video.id},`))
+
+            insert.insertAdjacentHTML("beforeend", `
+            <div class="content" style="padding: unset;">
+                <table>
+                    <tbody>
+                        <tr>
+                            <td valign="top">
+                                <a href="/video${vid.video.owner_id}_${vid.video.id}">
+                                    <div class="video-preview" style="height: 75px;width: 133px;overflow: hidden;">
+                                        <img src="${vid.video.image[0].url}" alt="${escapeHtml(vid.video.title)}" style="max-width: 133px; height: 75px; margin: auto;">
+                                    </div>
+                                </a>
+                            </td>
+                            <td valign="top" style="width: 100%">
+                                <a href="/video${vid.video.owner_id}_${vid.video.id}">
+                                    <b>
+                                        ${ovk_proc_strtr(escapeHtml(vid.video.title), 30)}
+                                    </b>
+                                </a>
+                                <br>
+                                <p>
+                                    <span>${ovk_proc_strtr(escapeHtml(vid.video.description ?? ""), 140)}</span>
+                                </p>
+                                <span><a href="/id${vid.video.owner_id}" target="_blank">${escapeHtml(vid.video.author_name ?? "")}</a></span>
+                            </td>
+                            <td valign="top" class="action_links" style="width: 150px;">
+                                <a class="profile_link" id="attachvid" data-name="${escapeHtml(vid.video.title)}" data-attachmentData="${vid.video.owner_id}_${vid.video.id}">${!isAttached ? tr("attach") : tr("detach")}</a>
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+            `)
+        }
+
+        if(page < pagesCount) {
+            document.querySelector(".videosInsert").insertAdjacentHTML("beforeend", `
+            <div id="showMoreVideos" data-pagesCount="${pagesCount}" data-page="${page + 1}" style="width: 100%;text-align: center;background: #d5d5d5;height: 22px;padding-top: 9px;cursor:pointer;">
+                <span>more...</span>
+            </div>`)
+        }
+    }
+
+    $(".videosInsert").on("click", "#showMoreVideos", (e) => {
+        u(e.currentTarget).remove()
+        insertVideos(Number(e.currentTarget.dataset.page), document.querySelector(".topGrayBlock #vquery").value)
+    })
+
+    $(".topGrayBlock #vquery").on("change", async (e) => {
+        await new Promise(r => setTimeout(r, 1000));
+
+        if(e.currentTarget.value === document.querySelector(".topGrayBlock #vquery").value) {
+            document.querySelector(".videosInsert").innerHTML = ""
+            insertVideos(1, e.currentTarget.value)
+            return;
+        } else {
+            console.info("skipping")
+        }
+    })
+
+    insertVideos(1)
+
+    function insertAttachment(id) {
+        let videos = form.querySelector("input[name='videos']") 
+
+        if(!videos.value.includes(id + ",")) {
+            if(videos.value.split(",").length > 10) {
+                NewNotification(tr("error"), tr("max_attached_videos"))
+                return false
+            }
+
+            form.querySelector("input[name='videos']").value += (id + ",")
+
+            console.info(id + " attached")
+            return true
+        } else {
+            form.querySelector("input[name='videos']").value = form.querySelector("input[name='videos']").value.replace(id + ",", "")
+
+            console.info(id + " detached")
+            return false
+        }
+    }
+
+    $(".videosInsert").on("click", "#attachvid", (ev) => {
+        // откреплено от псто
+        if(!insertAttachment(ev.currentTarget.dataset.attachmentdata)) {
+            u(`.post-has-videos .post-has-video[data-id='${ev.currentTarget.dataset.attachmentdata}']`).remove()
+            ev.currentTarget.innerHTML = tr("attach")
+        } else {
+            ev.currentTarget.innerHTML = tr("detach")
+
+            form.querySelector(".post-has-videos").insertAdjacentHTML("beforeend", `
+                <div class="post-has-video" id="unattachVideo" data-id="${ev.currentTarget.dataset.attachmentdata}">
+                    <span>${tr("video")} <b>"${ovk_proc_strtr(escapeHtml(ev.currentTarget.dataset.name), 20)}"</b></span>
+                </div>
+            `)
+
+            u(`#unattachVideo[data-id='${ev.currentTarget.dataset.attachmentdata}']`).on("click", (e) => {
+                let id = ev.currentTarget.dataset.attachmentdata
+                form.querySelector("input[name='videos']").value = form.querySelector("input[name='videos']").value.replace(id + ",", "")
+                
+                console.info(id + " detached")
+               
+                u(e.currentTarget).remove()
+            })
+        }
+    })
+})
+
 $(document).on("click", "#editPost", (e) => {
     let post = e.currentTarget.closest("table")
     let content = post.querySelector(".text")
diff --git a/Web/static/js/messagebox.js b/Web/static/js/messagebox.js
index 45791fd3..368311dd 100644
--- a/Web/static/js/messagebox.js
+++ b/Web/static/js/messagebox.js
@@ -3,6 +3,7 @@ Function.noop = () => {};
 function MessageBox(title, body, buttons, callbacks) {
     if(u(".ovk-diag-cont").length > 0) return false;
     
+    document.querySelector("html").style.overflowY = "hidden"
     let dialog = u(
     `<div class="ovk-diag-cont">
         <div class="ovk-diag">
@@ -21,6 +22,7 @@ function MessageBox(title, body, buttons, callbacks) {
             let __closeDialog = () => {
                 u("body").removeClass("dimmed");
                 u(".ovk-diag-cont").remove();
+                document.querySelector("html").style.overflowY = "scroll"
             };
             
             Reflect.apply(callbacks[callback], {
diff --git a/locales/en.strings b/locales/en.strings
index f09ee65e..22cee453 100644
--- a/locales/en.strings
+++ b/locales/en.strings
@@ -206,6 +206,7 @@
 "nsfw_warning" = "This post may have NSFW-content";
 "report" = "Report";
 "attach" = "Attach";
+"detach" = "Detach";
 "attach_photo" = "Attach photo";
 "attach_video" = "Attach video";
 "draw_graffiti" = "Draw graffiti";
@@ -692,6 +693,13 @@
 "videos_other" = "$1 videos";
 
 "view_video" = "View";
+
+"selecting_video" = "Selecting videos";
+"upload_new_video" = "Upload new video";
+"max_attached_videos" = "Max is 10 videos";
+"no_videos" = "You don't have uploaded videos.";
+"no_videos_results" = "No results.";
+
 "change_video" = "Change video";
 "unknown_video" = "This video is not supported in your version of OpenVK.";
 
diff --git a/locales/ru.strings b/locales/ru.strings
index 787f1648..2dfadb8f 100644
--- a/locales/ru.strings
+++ b/locales/ru.strings
@@ -186,6 +186,7 @@
 "nsfw_warning" = "Данный пост может содержать 18+ контент";
 "report" = "Пожаловаться";
 "attach" = "Прикрепить";
+"detach" = "Открепить";
 "attach_photo" = "Прикрепить фото";
 "attach_video" = "Прикрепить видео";
 "draw_graffiti" = "Нарисовать граффити";
@@ -652,6 +653,12 @@
 "change_video" = "Изменить видеозапись";
 "unknown_video" = "Эта видеозапись не поддерживается в вашей версии OpenVK.";
 
+"selecting_video" = "Выбор видеозаписей";
+"upload_new_video" = "Загрузить новое видео";
+"max_attached_videos" = "Максимум 10 видеозаписей";
+"no_videos" = "У вас нет видео.";
+"no_videos_results" = "Нет результатов.";
+
 /* Notifications */
 
 "feedback" = "Ответы";

From cc5a56917b57b6b92a54471ed7d47e452e042956 Mon Sep 17 00:00:00 2001
From: lalka2018 <99399973+lalka2016@users.noreply.github.com>
Date: Mon, 18 Sep 2023 18:09:25 +0300
Subject: [PATCH 18/26] fix gifts pagination (#984)

---
 Web/Models/Repositories/Gifts.php | 6 ++++++
 Web/Presenters/GiftsPresenter.php | 1 +
 2 files changed, 7 insertions(+)

diff --git a/Web/Models/Repositories/Gifts.php b/Web/Models/Repositories/Gifts.php
index f36b82a5..3baa2397 100644
--- a/Web/Models/Repositories/Gifts.php
+++ b/Web/Models/Repositories/Gifts.php
@@ -42,4 +42,10 @@ class Gifts
         foreach($cats as $cat)
             yield new GiftCategory($cat);
     }
+
+    function getCategoriesCount(): int
+    {
+        $cats  = $this->cats->where("deleted", false);
+        return $cats->count();
+    }
 }
diff --git a/Web/Presenters/GiftsPresenter.php b/Web/Presenters/GiftsPresenter.php
index 71480540..39359add 100644
--- a/Web/Presenters/GiftsPresenter.php
+++ b/Web/Presenters/GiftsPresenter.php
@@ -41,6 +41,7 @@ final class GiftsPresenter extends OpenVKPresenter
         
         $this->template->user      = $user;
         $this->template->iterator  = $cats;
+        $this->template->count     = $this->gifts->getCategoriesCount();
         $this->template->_template = "Gifts/Menu.xml";
     }
     

From edf10c424856ae2651ccd4940a8379eb6bbf2726 Mon Sep 17 00:00:00 2001
From: Alexander Minkin <weryskok@gmail.com>
Date: Fri, 22 Sep 2023 23:56:18 +0300
Subject: [PATCH 19/26] Docker: add imagick to dependencies I'm not sure if
 that's all we need to make it work, but I will solve this issue step by step

---
 install/automated/docker/base-php-apache.Dockerfile | 1 +
 1 file changed, 1 insertion(+)

diff --git a/install/automated/docker/base-php-apache.Dockerfile b/install/automated/docker/base-php-apache.Dockerfile
index de24d34f..3ead71f0 100644
--- a/install/automated/docker/base-php-apache.Dockerfile
+++ b/install/automated/docker/base-php-apache.Dockerfile
@@ -18,5 +18,6 @@ RUN apt update; \
         yaml \
         pdo_mysql \
         rdkafka \
+        imagick \
     && \
     rm -rf /var/lib/apt/lists/*
\ No newline at end of file

From 8483a2d343ad30dad61a908acd98a4605ee4d50a Mon Sep 17 00:00:00 2001
From: Alexander Minkin <weryskok@gmail.com>
Date: Sat, 23 Sep 2023 01:10:03 +0300
Subject: [PATCH 20/26] SQL: fix all `support_names`-related migrations This
 solves some problems in Docker instance

---
 install/sqls/00002-support-aliases.sql       |  8 +++++++-
 install/sqls/00032-agent-card.sql            | 10 ++--------
 install/sqls/00037-agent-card-profilefix.sql |  2 --
 3 files changed, 9 insertions(+), 11 deletions(-)
 delete mode 100644 install/sqls/00037-agent-card-profilefix.sql

diff --git a/install/sqls/00002-support-aliases.sql b/install/sqls/00002-support-aliases.sql
index b586a1dd..e3d17ca0 100644
--- a/install/sqls/00002-support-aliases.sql
+++ b/install/sqls/00002-support-aliases.sql
@@ -1 +1,7 @@
-CREATE TABLE `support_names` ( `agent` BIGINT UNSIGNED NOT NULL , `name` VARCHAR(512) NOT NULL , `icon` VARCHAR(1024) NULL DEFAULT NULL , `numerate` BOOLEAN NOT NULL DEFAULT FALSE , PRIMARY KEY (`agent`)) ENGINE = InnoDB;
\ No newline at end of file
+CREATE TABLE `support_names` ( 
+    `agent` BIGINT UNSIGNED NOT NULL,
+    `name` VARCHAR(512) NOT NULL,
+    `icon` VARCHAR(1024) NULL DEFAULT NULL, 
+    `numerate` BOOLEAN NOT NULL DEFAULT FALSE, 
+    PRIMARY KEY (`agent`)
+) ENGINE = InnoDB;
\ No newline at end of file
diff --git a/install/sqls/00032-agent-card.sql b/install/sqls/00032-agent-card.sql
index a8354c80..0f82460e 100644
--- a/install/sqls/00032-agent-card.sql
+++ b/install/sqls/00032-agent-card.sql
@@ -1,14 +1,8 @@
 
-CREATE TABLE `support_names` (
-    `id` bigint(20) UNSIGNED NOT NULL,
-    `agent` bigint(20) UNSIGNED NOT NULL,
-    `name` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL,
-    `icon` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
-    `numerate` tinyint(1) NOT NULL DEFAULT 0
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+ALTER TABLE `support_names` ADD `id` bigint(20) UNSIGNED NOT NULL FIRST;
 
 ALTER TABLE `support_names`
-    ADD PRIMARY KEY (`id`);
+    ADD UNIQUE KEY `id` (`id`);
 
 ALTER TABLE `support_names`
     MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;
diff --git a/install/sqls/00037-agent-card-profilefix.sql b/install/sqls/00037-agent-card-profilefix.sql
deleted file mode 100644
index e7ebc230..00000000
--- a/install/sqls/00037-agent-card-profilefix.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE `support_names`
-    ADD COLUMN `id` bigint(20) NOT NULL AUTO_INCREMENT UNIQUE FIRST;

From 75ce995df5200dcda4c57813a709b1d4c4f63c3d Mon Sep 17 00:00:00 2001
From: Alexander Minkin <weryskok@gmail.com>
Date: Sat, 23 Sep 2023 01:11:00 +0300
Subject: [PATCH 21/26] Docker: add KAFKA_CFG_NODE_ID to docker-compose

---
 install/automated/docker/docker-compose.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/install/automated/docker/docker-compose.yml b/install/automated/docker/docker-compose.yml
index cbd697a4..61ee3bac 100644
--- a/install/automated/docker/docker-compose.yml
+++ b/install/automated/docker/docker-compose.yml
@@ -76,6 +76,7 @@ services:
       - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092
       - KAFKA_BROKER_ID=1
       - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093
+      - KAFKA_CFG_NODE_ID=1
 
   phpmyadmin:
     image: docker.io/phpmyadmin:5

From 5710d131fd49b4b60dbfd72bb9af41d0b818d942 Mon Sep 17 00:00:00 2001
From: Alexander Minkin <weryskok@gmail.com>
Date: Sat, 23 Sep 2023 01:18:33 +0300
Subject: [PATCH 22/26] SQL: Reorder migration files The issue was that numbers
 were duplicating, so I decided to fix them

---
 install/sqls/{00018-reports.sql => 00019-reports.sql}             | 0
 .../{00019-block-in-support.sql => 00020-block-in-support.sql}    | 0
 install/sqls/{00020-image-sizes.sql => 00021-image-sizes.sql}     | 0
 .../{00021-video-processing.sql => 00022-video-processing.sql}    | 0
 install/sqls/{00022-group-alerts.sql => 00023-group-alerts.sql}   | 0
 install/sqls/{00023-email-change.sql => 00024-email-change.sql}   | 0
 .../{00024-main-page-setting.sql => 00025-main-page-setting.sql}  | 0
 .../{00025-toncoin-fetching.sql => 00026-toncoin-fetching.sql}    | 0
 .../{00026-better-birthdays.sql => 00027-better-birthdays.sql}    | 0
 install/sqls/{00027-rating.sql => 00028-rating.sql}               | 0
 install/sqls/{00028-deactivation.sql => 00029-deactivation.sql}   | 0
 .../sqls/{00029-hashtag-search.sql => 00030-hashtag-search.sql}   | 0
 install/sqls/{00030-apps.sql => 00031-apps.sql}                   | 0
 .../sqls/{00031-ban-page-until.sql => 00032-ban-page-until.sql}   | 0
 install/sqls/{00032-agent-card.sql => 00033-agent-card.sql}       | 0
 install/sqls/{00032-banned-urls.sql => 00034-banned-urls.sql}     | 0
 .../sqls/{00032-better-reports.sql => 00035-better-reports.sql}   | 0
 .../{00033-shortcode-aliases.sql => 00036-shortcode-aliases.sql}  | 0
 install/sqls/{00034-polls.sql => 00037-polls.sql}                 | 0
 install/sqls/{00035-backdrops.sql => 00038-backdrops.sql}         | 0
 install/sqls/{00036-platforms.sql => 00039-platforms.sql}         | 0
 .../{00038-noSpam-templates.sql => 00040-noSpam-templates.sql}    | 0
 22 files changed, 0 insertions(+), 0 deletions(-)
 rename install/sqls/{00018-reports.sql => 00019-reports.sql} (100%)
 rename install/sqls/{00019-block-in-support.sql => 00020-block-in-support.sql} (100%)
 rename install/sqls/{00020-image-sizes.sql => 00021-image-sizes.sql} (100%)
 rename install/sqls/{00021-video-processing.sql => 00022-video-processing.sql} (100%)
 rename install/sqls/{00022-group-alerts.sql => 00023-group-alerts.sql} (100%)
 rename install/sqls/{00023-email-change.sql => 00024-email-change.sql} (100%)
 rename install/sqls/{00024-main-page-setting.sql => 00025-main-page-setting.sql} (100%)
 rename install/sqls/{00025-toncoin-fetching.sql => 00026-toncoin-fetching.sql} (100%)
 rename install/sqls/{00026-better-birthdays.sql => 00027-better-birthdays.sql} (100%)
 rename install/sqls/{00027-rating.sql => 00028-rating.sql} (100%)
 rename install/sqls/{00028-deactivation.sql => 00029-deactivation.sql} (100%)
 rename install/sqls/{00029-hashtag-search.sql => 00030-hashtag-search.sql} (100%)
 rename install/sqls/{00030-apps.sql => 00031-apps.sql} (100%)
 rename install/sqls/{00031-ban-page-until.sql => 00032-ban-page-until.sql} (100%)
 rename install/sqls/{00032-agent-card.sql => 00033-agent-card.sql} (100%)
 rename install/sqls/{00032-banned-urls.sql => 00034-banned-urls.sql} (100%)
 rename install/sqls/{00032-better-reports.sql => 00035-better-reports.sql} (100%)
 rename install/sqls/{00033-shortcode-aliases.sql => 00036-shortcode-aliases.sql} (100%)
 rename install/sqls/{00034-polls.sql => 00037-polls.sql} (100%)
 rename install/sqls/{00035-backdrops.sql => 00038-backdrops.sql} (100%)
 rename install/sqls/{00036-platforms.sql => 00039-platforms.sql} (100%)
 rename install/sqls/{00038-noSpam-templates.sql => 00040-noSpam-templates.sql} (100%)

diff --git a/install/sqls/00018-reports.sql b/install/sqls/00019-reports.sql
similarity index 100%
rename from install/sqls/00018-reports.sql
rename to install/sqls/00019-reports.sql
diff --git a/install/sqls/00019-block-in-support.sql b/install/sqls/00020-block-in-support.sql
similarity index 100%
rename from install/sqls/00019-block-in-support.sql
rename to install/sqls/00020-block-in-support.sql
diff --git a/install/sqls/00020-image-sizes.sql b/install/sqls/00021-image-sizes.sql
similarity index 100%
rename from install/sqls/00020-image-sizes.sql
rename to install/sqls/00021-image-sizes.sql
diff --git a/install/sqls/00021-video-processing.sql b/install/sqls/00022-video-processing.sql
similarity index 100%
rename from install/sqls/00021-video-processing.sql
rename to install/sqls/00022-video-processing.sql
diff --git a/install/sqls/00022-group-alerts.sql b/install/sqls/00023-group-alerts.sql
similarity index 100%
rename from install/sqls/00022-group-alerts.sql
rename to install/sqls/00023-group-alerts.sql
diff --git a/install/sqls/00023-email-change.sql b/install/sqls/00024-email-change.sql
similarity index 100%
rename from install/sqls/00023-email-change.sql
rename to install/sqls/00024-email-change.sql
diff --git a/install/sqls/00024-main-page-setting.sql b/install/sqls/00025-main-page-setting.sql
similarity index 100%
rename from install/sqls/00024-main-page-setting.sql
rename to install/sqls/00025-main-page-setting.sql
diff --git a/install/sqls/00025-toncoin-fetching.sql b/install/sqls/00026-toncoin-fetching.sql
similarity index 100%
rename from install/sqls/00025-toncoin-fetching.sql
rename to install/sqls/00026-toncoin-fetching.sql
diff --git a/install/sqls/00026-better-birthdays.sql b/install/sqls/00027-better-birthdays.sql
similarity index 100%
rename from install/sqls/00026-better-birthdays.sql
rename to install/sqls/00027-better-birthdays.sql
diff --git a/install/sqls/00027-rating.sql b/install/sqls/00028-rating.sql
similarity index 100%
rename from install/sqls/00027-rating.sql
rename to install/sqls/00028-rating.sql
diff --git a/install/sqls/00028-deactivation.sql b/install/sqls/00029-deactivation.sql
similarity index 100%
rename from install/sqls/00028-deactivation.sql
rename to install/sqls/00029-deactivation.sql
diff --git a/install/sqls/00029-hashtag-search.sql b/install/sqls/00030-hashtag-search.sql
similarity index 100%
rename from install/sqls/00029-hashtag-search.sql
rename to install/sqls/00030-hashtag-search.sql
diff --git a/install/sqls/00030-apps.sql b/install/sqls/00031-apps.sql
similarity index 100%
rename from install/sqls/00030-apps.sql
rename to install/sqls/00031-apps.sql
diff --git a/install/sqls/00031-ban-page-until.sql b/install/sqls/00032-ban-page-until.sql
similarity index 100%
rename from install/sqls/00031-ban-page-until.sql
rename to install/sqls/00032-ban-page-until.sql
diff --git a/install/sqls/00032-agent-card.sql b/install/sqls/00033-agent-card.sql
similarity index 100%
rename from install/sqls/00032-agent-card.sql
rename to install/sqls/00033-agent-card.sql
diff --git a/install/sqls/00032-banned-urls.sql b/install/sqls/00034-banned-urls.sql
similarity index 100%
rename from install/sqls/00032-banned-urls.sql
rename to install/sqls/00034-banned-urls.sql
diff --git a/install/sqls/00032-better-reports.sql b/install/sqls/00035-better-reports.sql
similarity index 100%
rename from install/sqls/00032-better-reports.sql
rename to install/sqls/00035-better-reports.sql
diff --git a/install/sqls/00033-shortcode-aliases.sql b/install/sqls/00036-shortcode-aliases.sql
similarity index 100%
rename from install/sqls/00033-shortcode-aliases.sql
rename to install/sqls/00036-shortcode-aliases.sql
diff --git a/install/sqls/00034-polls.sql b/install/sqls/00037-polls.sql
similarity index 100%
rename from install/sqls/00034-polls.sql
rename to install/sqls/00037-polls.sql
diff --git a/install/sqls/00035-backdrops.sql b/install/sqls/00038-backdrops.sql
similarity index 100%
rename from install/sqls/00035-backdrops.sql
rename to install/sqls/00038-backdrops.sql
diff --git a/install/sqls/00036-platforms.sql b/install/sqls/00039-platforms.sql
similarity index 100%
rename from install/sqls/00036-platforms.sql
rename to install/sqls/00039-platforms.sql
diff --git a/install/sqls/00038-noSpam-templates.sql b/install/sqls/00040-noSpam-templates.sql
similarity index 100%
rename from install/sqls/00038-noSpam-templates.sql
rename to install/sqls/00040-noSpam-templates.sql

From 569a8e8bee506d4e422fece04d7e1ff5d87d317c Mon Sep 17 00:00:00 2001
From: Vladimir Barinov <veselcraft@icloud.com>
Date: Fri, 29 Sep 2023 18:47:53 +0300
Subject: [PATCH 23/26] repositories/logs does not exist

---
 Web/Presenters/AuthPresenter.php | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/Web/Presenters/AuthPresenter.php b/Web/Presenters/AuthPresenter.php
index 23b55dc9..c6a7f143 100644
--- a/Web/Presenters/AuthPresenter.php
+++ b/Web/Presenters/AuthPresenter.php
@@ -1,7 +1,7 @@
 <?php declare(strict_types=1);
 namespace openvk\Web\Presenters;
 use openvk\Web\Models\Entities\{IP, User, PasswordReset, EmailVerification};
-use openvk\Web\Models\Repositories\{Bans, IPs, Users, Restores, Verifications, Logs};
+use openvk\Web\Models\Repositories\{Bans, IPs, Users, Restores, Verifications};
 use openvk\Web\Models\Exceptions\InvalidUserNameException;
 use openvk\Web\Util\Validator;
 use Chandler\Session\Session;
@@ -130,7 +130,6 @@ final class AuthPresenter extends OpenVKPresenter
             }
             
             $this->authenticator->authenticate($chUser->getId());
-            (new Logs)->create($user->getId(), "profiles", "openvk\\Web\\Models\\Entities\\User", 0, $user, $user, $_SERVER["REMOTE_ADDR"], $_SERVER["HTTP_USER_AGENT"]);
             $this->redirect("/id" . $user->getId());
             $user->save();
         }

From db8e9d183f46d9a4fc0b37d37a60e90e89365169 Mon Sep 17 00:00:00 2001
From: lalka2016 <99399973+lalka2016@users.noreply.github.com>
Date: Mon, 2 Oct 2023 17:24:01 +0300
Subject: [PATCH 24/26] Fix typos in NoSpam

---
 Web/Presenters/templates/NoSpam/Index.xml     | 2 +-
 Web/Presenters/templates/NoSpam/Templates.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Web/Presenters/templates/NoSpam/Index.xml b/Web/Presenters/templates/NoSpam/Index.xml
index 2bf9d1df..0c465af8 100644
--- a/Web/Presenters/templates/NoSpam/Index.xml
+++ b/Web/Presenters/templates/NoSpam/Index.xml
@@ -106,7 +106,7 @@
                             <span class="nobold">{_block_params}:</span>
                         </td>
                         <td>
-                            <select name="ban_type" id="noSpam-ban-type" style="width: 140px;"
+                            <select name="ban_type" id="noSpam-ban-type" style="width: 140px;">
                                 <option value="1">{_only_rollback}</option>
                                 <option value="2">{_only_block}</option>
                                 <option value="3">{_rollback_and_block}</option>
diff --git a/Web/Presenters/templates/NoSpam/Templates.xml b/Web/Presenters/templates/NoSpam/Templates.xml
index a86df534..e2936b15 100644
--- a/Web/Presenters/templates/NoSpam/Templates.xml
+++ b/Web/Presenters/templates/NoSpam/Templates.xml
@@ -76,7 +76,7 @@
                             <img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px;">
                         </div>
                         <a n:if="!$template->isRollbacked()" id="noSpam-rollback-template-link-{$template->getId()}" onClick="rollbackTemplate({$template->getId()})">{_roll_back}</a>
-                        <span n:attr="style => $template->isRollbacked() ? '' : 'display: none;'" id="noSpam-rollback-template-rollbacked-{$template->getId()}">{roll_backed}</span>
+                        <span n:attr="style => $template->isRollbacked() ? '' : 'display: none;'" id="noSpam-rollback-template-rollbacked-{$template->getId()}">{_roll_backed}</span>
                     </div>
                 </td>
             </tr>

From 6632d070f5a255a2fc9633b4e917401d1b12bd5f Mon Sep 17 00:00:00 2001
From: Alexander Minkin <weryskok@gmail.com>
Date: Tue, 3 Oct 2023 01:49:39 +0300
Subject: [PATCH 25/26] fix(containers): :loud_sound: Set log output to stdout

Let the docker handle logs. https://12factor.net/logs motivated me to do this
---
 install/automated/common/10-openvk.conf                    | 3 +--
 install/automated/docker/acl_handler.sh                    | 1 -
 install/automated/docker/docker-compose.yml                | 2 --
 install/automated/docker/openvk.Dockerfile                 | 1 -
 install/automated/kubernetes/manifests/003-deployment.yaml | 5 -----
 5 files changed, 1 insertion(+), 11 deletions(-)

diff --git a/install/automated/common/10-openvk.conf b/install/automated/common/10-openvk.conf
index d842eefd..b272f5ac 100644
--- a/install/automated/common/10-openvk.conf
+++ b/install/automated/common/10-openvk.conf
@@ -8,6 +8,5 @@
         Require all granted
     </Directory>
 
-    ErrorLog /var/log/openvk/error.log
-    CustomLog /var/log/openvk/access.log combinedio
+    LogFormat combinedio
 </VirtualHost>
diff --git a/install/automated/docker/acl_handler.sh b/install/automated/docker/acl_handler.sh
index 8ef23239..e23176a3 100755
--- a/install/automated/docker/acl_handler.sh
+++ b/install/automated/docker/acl_handler.sh
@@ -8,7 +8,6 @@ do
     chown -R 33:33 /opt/chandler/extensions/available/openvk/tmp/api-storage/audios
     chown -R 33:33 /opt/chandler/extensions/available/openvk/tmp/api-storage/photos
     chown -R 33:33 /opt/chandler/extensions/available/openvk/tmp/api-storage/videos
-    chown -R 33:33 /var/log/openvk
 
     sleep 600
 done
\ No newline at end of file
diff --git a/install/automated/docker/docker-compose.yml b/install/automated/docker/docker-compose.yml
index 61ee3bac..685ab713 100644
--- a/install/automated/docker/docker-compose.yml
+++ b/install/automated/docker/docker-compose.yml
@@ -12,7 +12,6 @@ services:
       - openvk-audios:/opt/chandler/extensions/available/openvk/tmp/api-storage/audios
       - openvk-photos:/opt/chandler/extensions/available/openvk/tmp/api-storage/photos
       - openvk-videos:/opt/chandler/extensions/available/openvk/tmp/api-storage/videos
-      - openvk-logs:/var/log/openvk
       - ./openvk.yml:/opt/chandler/extensions/available/openvk/openvk.yml:ro
       - ./chandler.yml:/opt/chandler/chandler.yml:ro
     depends_on:
@@ -32,7 +31,6 @@ services:
       - openvk-audios:/opt/chandler/extensions/available/openvk/tmp/api-storage/audios
       - openvk-photos:/opt/chandler/extensions/available/openvk/tmp/api-storage/photos
       - openvk-videos:/opt/chandler/extensions/available/openvk/tmp/api-storage/videos
-      - openvk-logs:/var/log/openvk
       - ./acl_handler.sh:/bin/acl_handler.sh:ro
 
   mariadb-primary:
diff --git a/install/automated/docker/openvk.Dockerfile b/install/automated/docker/openvk.Dockerfile
index 47f63b77..389f7443 100644
--- a/install/automated/docker/openvk.Dockerfile
+++ b/install/automated/docker/openvk.Dockerfile
@@ -48,7 +48,6 @@ RUN ln -s /opt/chandler/extensions/available/commitcaptcha/ /opt/chandler/extens
     ln -s /opt/chandler/extensions/available/openvk/install/automated/common/10-openvk.conf /etc/apache2/sites-enabled/10-openvk.conf && \
     a2enmod rewrite
 
-VOLUME [ "/var/log/openvk" ]
 VOLUME [ "/opt/chandler/extensions/available/openvk/storage" ]
 VOLUME [ "/opt/chandler/extensions/available/openvk/tmp/api-storage/audios" ]
 VOLUME [ "/opt/chandler/extensions/available/openvk/tmp/api-storage/photos" ]
diff --git a/install/automated/kubernetes/manifests/003-deployment.yaml b/install/automated/kubernetes/manifests/003-deployment.yaml
index e15d3ccd..edbc40f9 100644
--- a/install/automated/kubernetes/manifests/003-deployment.yaml
+++ b/install/automated/kubernetes/manifests/003-deployment.yaml
@@ -38,13 +38,10 @@ items:
                 chown -R 33:33 /opt/chandler/extensions/available/openvk/tmp/api-storage/audios
                 chown -R 33:33 /opt/chandler/extensions/available/openvk/tmp/api-storage/photos
                 chown -R 33:33 /opt/chandler/extensions/available/openvk/tmp/api-storage/videos
-                chown -R 33:33 /var/log/openvk
 
                 sleep 600
             done
           volumeMounts:
-          - mountPath: /var/log/openvk
-            name: openvk-logs
           - mountPath: /opt/chandler/extensions/available/openvk/storage
             name: openvk-storage
           - mountPath: /opt/chandler/extensions/available/openvk/tmp/api-storage/audios
@@ -66,8 +63,6 @@ items:
               cpu: 100m
               memory: 512Mi
           volumeMounts:
-          - mountPath: /var/log/openvk
-            name: openvk-logs
           - mountPath: /opt/chandler/extensions/available/openvk/openvk.yml
             name: openvk-config
             subPath: openvk.yml

From a859fa13a59d542b40b83bdd8f502c1590ed364a Mon Sep 17 00:00:00 2001
From: Vladimir Barinov <veselcraft@icloud.com>
Date: Tue, 3 Oct 2023 19:40:13 +0300
Subject: [PATCH 26/26] [WIP] Textarea: Upload multiple pictures (#800)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* VKAPI: Fix bug when DELETED user appear if there is no user_ids

* Textarea: Make multiple attachments

* постмодернистское искусство

* Use only attachPic for grabbing pic attachments

TODO throw flashFail on bruh moment with pic attachments

* draft masonry picture layout in posts xddd

где мои опиаты???

* fix funny typos in computeMasonryLayout

* Fix video bruh moment in textarea

* Posts: add multiple kakahi for microblog

* Photo: Add minimal implementation of миниатюра открывашка

Co-authored-by: Daniel <60743585+myslivets@users.noreply.github.com>

* Photo: Add ability to slide trough photos in one post

This also gives ability to easily implement comments and actions

* Photo: The Fxck Is This implementation of comments under photo in viewer

* FloatingPhotoViewer: Better CSS

- Fix that details background issue
- Make slide buttons slightly shorter by height

* FloatingPhotoViewer: Refactor, and make it better

- Now you can actually check the comments under EVERY photo
- Fix for textarea. Now you can publish comments

* Fix funny typos xddd

* Kinda fix poll display in non-microblog posts

* Posts: Fix poll display in microblog posts

* Add photos picker (#986)

* early implementation of photos pickir

Добавлен пикер фоточек и быстрая загрузка фото. Так же пофикшен просмотрщик фото в группах. Но, правда, я сломал копипейст, но это ладн.

* Fiks fotos viver four coments.

* Add picking photos from clubs albums

Копипейст и граффити так и не пофикшены

* Fix graffiti and copypaste

Какого-то хуя копипаста у постов срабатывает два раза.

* some fixesx

* dragon drop

* Fix PHP 8 compatibility

* 5 (#988)

---------

Co-authored-by: celestora <kitsuruko@gmail.com>
Co-authored-by: Daniel <60743585+myslivets@users.noreply.github.com>
Co-authored-by: lalka2016 <99399973+lalka2016@users.noreply.github.com>
Co-authored-by: Alexander Minkin <weryskok@gmail.com>
---
 ServiceAPI/Photos.php                         |  92 ++++
 Web/Models/Entities/Comment.php               |   2 +-
 Web/Models/Entities/IP.php                    |   2 +-
 Web/Models/Entities/Media.php                 |   4 +-
 Web/Models/Entities/Postable.php              |   4 +-
 .../Entities/Traits/TAttachmentHost.php       |  43 +-
 Web/Models/Repositories/Photos.php            |  20 +-
 Web/Presenters/CommentPresenter.php           |  37 +-
 Web/Presenters/InternalAPIPresenter.php       |  38 ++
 Web/Presenters/PhotosPresenter.php            |  22 +-
 Web/Presenters/WallPresenter.php              |  40 +-
 Web/Presenters/templates/Photos/Album.xml     |   2 +-
 Web/Presenters/templates/Photos/Photo.xml     |  10 +-
 Web/Presenters/templates/Wall/Feed.xml        |   2 +
 Web/Presenters/templates/Wall/Post.xml        |   2 +-
 .../templates/components/attachment.xml       |   5 +-
 .../templates/components/comment.xml          |  13 +-
 .../templates/components/comments.xml         |   2 +-
 .../components/post/microblogpost.xml         |  17 +-
 .../templates/components/post/oldpost.xml     |  15 +-
 .../templates/components/textArea.xml         |  15 +-
 Web/Util/Makima/Makima.php                    | 305 +++++++++++
 Web/Util/Makima/MasonryLayout.php             |  10 +
 Web/Util/Makima/ThumbTile.php                 |  14 +
 Web/routes.yml                                |   2 +
 Web/static/css/main.css                       | 130 ++++-
 Web/static/js/al_wall.js                      | 492 +++++++++++++++++-
 Web/static/js/messagebox.js                   |   7 +-
 Web/static/js/openvk.cls.js                   |   2 +-
 locales/en.strings                            |  16 +
 locales/ru.strings                            |  16 +
 31 files changed, 1268 insertions(+), 113 deletions(-)
 create mode 100644 ServiceAPI/Photos.php
 create mode 100644 Web/Util/Makima/Makima.php
 create mode 100644 Web/Util/Makima/MasonryLayout.php
 create mode 100644 Web/Util/Makima/ThumbTile.php

diff --git a/ServiceAPI/Photos.php b/ServiceAPI/Photos.php
new file mode 100644
index 00000000..16d602f2
--- /dev/null
+++ b/ServiceAPI/Photos.php
@@ -0,0 +1,92 @@
+<?php declare(strict_types=1);
+namespace openvk\ServiceAPI;
+use openvk\Web\Models\Entities\User;
+use openvk\Web\Models\Repositories\{Photos as PhotosRepo, Albums, Clubs};
+
+class Photos implements Handler
+{
+    protected $user;
+    protected $photos;
+    
+    function __construct(?User $user)
+    {
+        $this->user  = $user;
+        $this->photos = new PhotosRepo;
+    }
+
+    function getPhotos(int $page = 1, int $album = 0, callable $resolve, callable $reject)
+    {
+        if($album == 0) {
+            $photos = $this->photos->getEveryUserPhoto($this->user, $page, 24);
+            $count  = $this->photos->getUserPhotosCount($this->user);
+        } else {
+            $album = (new Albums)->get($album);
+
+            if(!$album || $album->isDeleted())
+                $reject(55, "Invalid .");
+
+            if($album->getOwner() instanceof User) {
+                if($album->getOwner()->getId() != $this->user->getId())
+                    $reject(555, "Access to album denied");
+            } else {
+                if(!$album->getOwner()->canBeModifiedBy($this->user))
+                    $reject(555, "Access to album denied");
+            }
+
+            $photos = $album->getPhotos($page, 24);
+            $count  = $album->size();
+        }
+
+        $arr = [
+            "count"  => $count,
+            "items"  => [],
+        ];
+
+        foreach($photos as $photo) {
+            $res = json_decode(json_encode($photo->toVkApiStruct()), true);
+            
+            $arr["items"][] = $res;
+        }
+
+        $resolve($arr);
+    }
+
+    function getAlbums(int $club, callable $resolve, callable $reject)
+    {
+        $albumsRepo = (new Albums);
+        
+        $count  = $albumsRepo->getUserAlbumsCount($this->user);
+        $albums = $albumsRepo->getUserAlbums($this->user, 1, $count);
+
+        $arr = [
+            "count"  => $count,
+            "items"  => [],
+        ];
+
+        foreach($albums as $album) {
+            $res = ["id" => $album->getId(), "name" => $album->getName()];
+                
+            $arr["items"][] = $res;
+        }
+
+        if($club > 0) {
+            $cluber = (new Clubs)->get($club);
+
+            if(!$cluber || !$cluber->canBeModifiedBy($this->user))
+                $reject(1337, "Invalid (club), or you can't modify him");
+
+            $clubCount  = (new Albums)->getClubAlbumsCount($cluber);
+            $clubAlbums = (new Albums)->getClubAlbums($cluber, 1, $clubCount);
+
+            foreach($clubAlbums as $albumr) {
+                $res = ["id" => $albumr->getId(), "name" => $albumr->getName()];
+                    
+                $arr["items"][] = $res;
+            }
+
+            $arr["count"] = $arr["count"] + $clubCount;
+        }
+
+        $resolve($arr);
+    }
+}
diff --git a/Web/Models/Entities/Comment.php b/Web/Models/Entities/Comment.php
index d64a2763..37b06dda 100644
--- a/Web/Models/Entities/Comment.php
+++ b/Web/Models/Entities/Comment.php
@@ -11,7 +11,7 @@ class Comment extends Post
     
     function getPrettyId(): string
     {
-        return $this->getRecord()->id;
+        return (string)$this->getRecord()->id;
     }
     
     function getVirtualId(): int
diff --git a/Web/Models/Entities/IP.php b/Web/Models/Entities/IP.php
index df2c9787..0d9b8fd0 100644
--- a/Web/Models/Entities/IP.php
+++ b/Web/Models/Entities/IP.php
@@ -105,7 +105,7 @@ class IP extends RowModel
         $this->stateChanges("ip", $ip);
     }
     
-    function save($log): void
+    function save(?bool $log = false): void
     {
         if(is_null($this->getRecord()))
             $this->stateChanges("first_seen", time());
diff --git a/Web/Models/Entities/Media.php b/Web/Models/Entities/Media.php
index 9377f3e8..648d3564 100644
--- a/Web/Models/Entities/Media.php
+++ b/Web/Models/Entities/Media.php
@@ -121,14 +121,14 @@ abstract class Media extends Postable
         $this->stateChanges("hash", $hash);
     }
 
-    function save(): void
+    function save(?bool $log = false): void
     {
         if(!is_null($this->processingPlaceholder) && is_null($this->getRecord())) {
             $this->stateChanges("processed", 0);
             $this->stateChanges("last_checked", time());
         }
 
-        parent::save();
+        parent::save($log);
     }
 
     function delete(bool $softly = true): void
diff --git a/Web/Models/Entities/Postable.php b/Web/Models/Entities/Postable.php
index ffbf480c..8f783238 100644
--- a/Web/Models/Entities/Postable.php
+++ b/Web/Models/Entities/Postable.php
@@ -152,7 +152,7 @@ abstract class Postable extends Attachable
         throw new ISE("Setting virtual id manually is forbidden");
     }
     
-    function save(): void
+    function save(?bool $log = false): void
     {
         $vref = $this->upperNodeReferenceColumnName;
         
@@ -171,7 +171,7 @@ abstract class Postable extends Attachable
             $this->stateChanges("edited", time());
         }*/
         
-        parent::save();
+        parent::save($log);
     }
     
     use Traits\TAttachmentHost;
diff --git a/Web/Models/Entities/Traits/TAttachmentHost.php b/Web/Models/Entities/Traits/TAttachmentHost.php
index cbe7cad2..db814cce 100644
--- a/Web/Models/Entities/Traits/TAttachmentHost.php
+++ b/Web/Models/Entities/Traits/TAttachmentHost.php
@@ -1,6 +1,7 @@
 <?php declare(strict_types=1);
 namespace openvk\Web\Models\Entities\Traits;
-use openvk\Web\Models\Entities\Attachable;
+use openvk\Web\Models\Entities\{Attachable, Photo};
+use openvk\Web\Util\Makima\Makima;
 use Chandler\Database\DatabaseConnection;
 
 trait TAttachmentHost
@@ -29,6 +30,46 @@ trait TAttachmentHost
             yield $repo->get($rel->attachable_id);
         }
     }
+
+    function getChildrenWithLayout(int $w, int $h = -1): object
+    {
+        if($h < 0)
+            $h = $w;
+
+        $children = $this->getChildren();
+        $skipped  = $photos = $result = [];
+        foreach($children as $child) {
+            if($child instanceof Photo) {
+                $photos[] = $child;
+                continue;
+            }
+
+            $skipped[] = $child;
+        }
+
+        $height = "unset";
+        $width  = $w;
+        if(sizeof($photos) < 2) {
+            if(isset($photos[0]))
+                $result[] = ["100%", "unset", $photos[0], "unset"];
+        } else {
+            $mak    = new Makima($photos);
+            $layout = $mak->computeMasonryLayout($w, $h);
+            $height = $layout->height;
+            $width  = $layout->width;
+            for($i = 0; $i < sizeof($photos); $i++) {
+                $tile = $layout->tiles[$i];
+                $result[] = [$tile->width . "px", $tile->height . "px", $photos[$i], "left"];
+            }
+        }
+
+        return (object) [
+            "width"  => $width . "px",
+            "height" => $height . "px",
+            "tiles"  => $result,
+            "extras" => $skipped,
+        ];
+    }
     
     function attach(Attachable $attachment): void
     {
diff --git a/Web/Models/Repositories/Photos.php b/Web/Models/Repositories/Photos.php
index 88c7e804..0698c914 100644
--- a/Web/Models/Repositories/Photos.php
+++ b/Web/Models/Repositories/Photos.php
@@ -33,14 +33,26 @@ class Photos
         return new Photo($photo);
     }
 
-    function getEveryUserPhoto(User $user): \Traversable
+    function getEveryUserPhoto(User $user, int $page = 1, ?int $perPage = NULL): \Traversable
     {
+        $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
         $photos = $this->photos->where([
-            "owner" => $user->getId()
-        ]);
+            "owner"   => $user->getId(),
+            "deleted" => 0
+        ])->order("id DESC");
 
-        foreach($photos as $photo) {
+        foreach($photos->page($page, $perPage) as $photo) {
             yield new Photo($photo);
         }
     }
+
+    function getUserPhotosCount(User $user) 
+    {
+        $photos = $this->photos->where([
+            "owner"   => $user->getId(),
+            "deleted" => 0
+        ]);
+
+        return sizeof($photos);
+    }
 }
diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php
index b68c7d11..e005af86 100644
--- a/Web/Presenters/CommentPresenter.php
+++ b/Web/Presenters/CommentPresenter.php
@@ -2,7 +2,7 @@
 namespace openvk\Web\Presenters;
 use openvk\Web\Models\Entities\{Comment, Notifications\MentionNotification, Photo, Video, User, Topic, Post};
 use openvk\Web\Models\Entities\Notifications\CommentNotification;
-use openvk\Web\Models\Repositories\{Comments, Clubs, Videos};
+use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos};
 
 final class CommentPresenter extends OpenVKPresenter
 {
@@ -54,9 +54,6 @@ final class CommentPresenter extends OpenVKPresenter
         if ($entity instanceof Post && $entity->getWallOwner()->isBanned())
             $this->flashFail("err", tr("error"), tr("forbidden"));
 
-        if($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading'])
-            $this->flashFail("err", tr("error"), tr("video_uploads_disabled"));
-        
         $flags = 0;
         if($this->postParam("as_group") === "on" && !is_null($club) && $club->canBeModifiedBy($this->user->identity))
             $flags |= 0b10000000;
@@ -70,18 +67,22 @@ final class CommentPresenter extends OpenVKPresenter
             }
         }
         
-        # TODO move to trait
-        try {
-            $photo = 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);
+        $photos = [];
+        if(!empty($this->postParam("photos"))) {
+            $un  = rtrim($this->postParam("photos"), ",");
+            $arr = explode(",", $un);
+
+            if(sizeof($arr) < 11) {
+                foreach($arr as $dat) {
+                    $ids = explode("_", $dat);
+                    $photo = (new Photos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]);
+    
+                    if(!$photo || $photo->isDeleted())
+                        continue;
+    
+                    $photos[] = $photo;
+                }
             }
-        } catch(ISE $ex) {
-            $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_file_too_big"));
         }
 
         $videos = [];
@@ -103,7 +104,7 @@ final class CommentPresenter extends OpenVKPresenter
             }
         }
         
-        if(empty($this->postParam("text")) && !$photo && !$video)
+        if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1)
             $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty"));
         
         try {
@@ -119,8 +120,8 @@ final class CommentPresenter extends OpenVKPresenter
             $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_too_big"));
         }
         
-        if(!is_null($photo))
-            $comment->attach($photo);
+        foreach($photos as $photo)
+        	$comment->attach($photo);
         
         if(sizeof($videos) > 0)
             foreach($videos as $vid)
diff --git a/Web/Presenters/InternalAPIPresenter.php b/Web/Presenters/InternalAPIPresenter.php
index 1a107659..e2e6b50e 100644
--- a/Web/Presenters/InternalAPIPresenter.php
+++ b/Web/Presenters/InternalAPIPresenter.php
@@ -1,5 +1,6 @@
 <?php declare(strict_types=1);
 namespace openvk\Web\Presenters;
+use openvk\Web\Models\Repositories\{Posts, Comments};
 use MessagePack\MessagePack;
 use Chandler\Session\Session;
 
@@ -95,4 +96,41 @@ final class InternalAPIPresenter extends OpenVKPresenter
             ]);
         }
     }
+
+    function renderGetPhotosFromPost(int $owner_id, int $post_id) {
+        if($_SERVER["REQUEST_METHOD"] !== "POST") {
+            header("HTTP/1.1 405 Method Not Allowed");
+            exit("иди нахуй заебал");
+        }
+
+        if($this->postParam("parentType", false) == "post") {
+            $post = (new Posts)->getPostById($owner_id, $post_id);
+        } else {
+            $post = (new Comments)->get($post_id);
+        }
+    
+
+        if(is_null($post)) {
+            $this->returnJson([
+                "success" => 0
+            ]);
+        } else {
+            $response = [];
+            $attachments = $post->getChildren();
+            foreach($attachments as $attachment) 
+            {
+                if($attachment instanceof \openvk\Web\Models\Entities\Photo)
+                {
+                    $response[] = [
+                        "url" => $attachment->getURLBySizeId('normal'),
+                        "id"  => $attachment->getPrettyId()
+                    ];
+                }
+            }
+            $this->returnJson([
+                "success" => 1,
+                "body" => $response
+            ]);
+        }
+    }
 }
diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php
index bef984b7..0a8b87e4 100644
--- a/Web/Presenters/PhotosPresenter.php
+++ b/Web/Presenters/PhotosPresenter.php
@@ -222,15 +222,20 @@ final class PhotosPresenter extends OpenVKPresenter
     {
         $this->assertUserLoggedIn();
         $this->willExecuteWriteAction(true);
-        
-        if(is_null($this->queryParam("album")))
-            $this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true);
-        
-        [$owner, $id] = explode("_", $this->queryParam("album"));
-        $album = $this->albums->get((int) $id);
+
+        if(is_null($this->queryParam("album"))) {
+            $album = $this->albums->getUserWallAlbum($this->user->identity);
+        } else {
+            [$owner, $id] = explode("_", $this->queryParam("album"));
+            $album = $this->albums->get((int) $id);
+        }
+
         if(!$album)
             $this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true);
-        if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity))
+
+        # Для быстрой загрузки фоток из пикера фотографий нужен альбом, но юзер не может загружать фото
+        # в системные альбомы, так что так.
+        if(is_null($this->user) || !is_null($this->queryParam("album")) && !$album->canBeModifiedBy($this->user->identity))
             $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), 500, true);
         
         if($_SERVER["REQUEST_METHOD"] === "POST") {
@@ -261,6 +266,9 @@ final class PhotosPresenter extends OpenVKPresenter
                 $this->flashFail("err", tr("no_photo"), tr("select_file"), 500, true);
             
             $photos = [];
+            if((int)$this->postParam("count") > 10)
+                $this->flashFail("err", tr("no_photo"), "ты еблан", 500, true);
+
             for($i = 0; $i < $this->postParam("count"); $i++) {
                 try {
                     $photo = new Photo;
diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php
index ef9e4689..2f9d611d 100644
--- a/Web/Presenters/WallPresenter.php
+++ b/Web/Presenters/WallPresenter.php
@@ -3,7 +3,7 @@ namespace openvk\Web\Presenters;
 use openvk\Web\Models\Exceptions\TooMuchOptionsException;
 use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User};
 use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification};
-use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments};
+use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos};
 use Chandler\Database\DatabaseConnection;
 use Nette\InvalidStateException as ISE;
 use Bhaktaraz\RSSGenerator\Item;
@@ -249,23 +249,23 @@ final class WallPresenter extends OpenVKPresenter
         if($this->postParam("force_sign") === "on")
             $flags |= 0b01000000;
         
-        try {
-            $photo = NULL;
-            $video = NULL;
-            if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) {
-                $album = NULL;
-                if(!$anon && $wall > 0 && $wall === $this->user->id)
-                    $album = (new Albums)->getUserWallAlbum($wallOwner);
-                
-                $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon);
+        $photos = [];
+
+        if(!empty($this->postParam("photos"))) {
+            $un  = rtrim($this->postParam("photos"), ",");
+            $arr = explode(",", $un);
+
+            if(sizeof($arr) < 11) {
+                foreach($arr as $dat) {
+                    $ids = explode("_", $dat);
+                    $photo = (new Photos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]);
+    
+                    if(!$photo || $photo->isDeleted())
+                        continue;
+    
+                    $photos[] = $photo;
+                }
             }
-            
-            /*if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK)
-                $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"], $anon);*/
-        } catch(\DomainException $ex) {
-            $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted"));
-        } catch(ISE $ex) {
-            $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large"));
         }
         
         try {
@@ -312,7 +312,7 @@ final class WallPresenter extends OpenVKPresenter
             }
         }
         
-        if(empty($this->postParam("text")) && !$photo && sizeof($videos) < 1 && !$poll && !$note)
+        if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && !$poll && !$note)
             $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
         
         try {
@@ -329,8 +329,8 @@ final class WallPresenter extends OpenVKPresenter
             $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big"));
         }
         
-        if(!is_null($photo))
-            $post->attach($photo);
+        foreach($photos as $photo)
+        	$post->attach($photo);
         
         if(sizeof($videos) > 0)
             foreach($videos as $vid)
diff --git a/Web/Presenters/templates/Photos/Album.xml b/Web/Presenters/templates/Photos/Album.xml
index 1bd49a6f..0c919e90 100644
--- a/Web/Presenters/templates/Photos/Album.xml
+++ b/Web/Presenters/templates/Photos/Album.xml
@@ -41,7 +41,7 @@
                     </a>
                     
                     <a href="/photo{$photo->getPrettyId()}?from=album{$album->getId()}">
-                        <img class="album-photo--image" src="{$photo->getURL()}" alt="{$photo->getDescription()}" />
+                        <img class="album-photo--image" src="{$photo->getURLBySizeId('tinier')}" alt="{$photo->getDescription()}" />
                     </a>
                 </div>
             {/foreach}
diff --git a/Web/Presenters/templates/Photos/Photo.xml b/Web/Presenters/templates/Photos/Photo.xml
index bb6f171f..e3ecaf9c 100644
--- a/Web/Presenters/templates/Photos/Photo.xml
+++ b/Web/Presenters/templates/Photos/Photo.xml
@@ -26,11 +26,11 @@
     
     <hr/>
     
-    <div style="width: 100%; min-height: 100px;">
-        <div style="float: left; min-height: 100px; width: 70%;">
-            {include "../components/comments.xml", comments => $comments, count => $cCount, page => $cPage, model => "photos", parent => $photo}
+    <div style="width: 100%; min-height: 100px;" class="ovk-photo-details">
+        <div style="float: left; min-height: 100px; width: 68%;margin-left: 3px;">
+            {include "../components/comments.xml", comments => $comments, count => $cCount, page => $cPage, model => "photos", parent => $photo, custom_id => 999}
         </div>
-        <div style="float: left; min-height: 100px; width: 30%;">
+        <div style="float:right;min-height: 100px;width: 30%;margin-left: 1px;">
             <div>
                 <h4>{_information}</h4>
                 <span style="color: grey;">{_info_description}:</span>
@@ -42,7 +42,7 @@
             </div>
             <br/>
             <h4>{_actions}</h4>
-            {if $thisUser->getId() != $photo->getOwner()->getId()}
+            {if isset($thisUser) && $thisUser->getId() != $photo->getOwner()->getId()}
                 {var canReport = true}
             {/if}
             <div n:if="isset($thisUser) && $thisUser->getId() === $photo->getOwner()->getId()">
diff --git a/Web/Presenters/templates/Wall/Feed.xml b/Web/Presenters/templates/Wall/Feed.xml
index 5ed1e2fb..44c8cd5b 100644
--- a/Web/Presenters/templates/Wall/Feed.xml
+++ b/Web/Presenters/templates/Wall/Feed.xml
@@ -6,6 +6,8 @@
 {/block}
 
 {block content}
+    {php $GLOBALS["_bigWall"] = 1}
+
     <div class="tabs">
         <div n:attr="id => (isset($globalFeed) ? 'ki' : 'activetabs')" class="tab">
             <a n:attr="id => (isset($globalFeed) ? 'ki' : 'act_tab_a')" href="/feed">{_my_news}</a>
diff --git a/Web/Presenters/templates/Wall/Post.xml b/Web/Presenters/templates/Wall/Post.xml
index bd40b6c5..6ce9edd9 100644
--- a/Web/Presenters/templates/Wall/Post.xml
+++ b/Web/Presenters/templates/Wall/Post.xml
@@ -35,7 +35,7 @@
         
         <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()"
+            n:if="isset($thisUser) && $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()}"
diff --git a/Web/Presenters/templates/components/attachment.xml b/Web/Presenters/templates/components/attachment.xml
index c73e792d..a46be837 100644
--- a/Web/Presenters/templates/components/attachment.xml
+++ b/Web/Presenters/templates/components/attachment.xml
@@ -1,7 +1,8 @@
 {if $attachment instanceof \openvk\Web\Models\Entities\Photo}
     {if !$attachment->isDeleted()}
-        <a href="{$attachment->getPageUrl()}">
-            <img class="media" src="{$attachment->getURLBySizeId('normal')}" alt="{$attachment->getDescription()}" />
+        {var $link = "/photo" . ($attachment->isAnonymous() ? ("s/" . base_convert((string) $attachment->getId(), 10, 32)) : $attachment->getPrettyId())}
+        <a href="{$link}" onclick="OpenMiniature(event, {$attachment->getURLBySizeId('normal')}, {$parent->getPrettyId()}, {$attachment->getPrettyId()}, {$parentType})">
+            <img class="media media_makima" src="{$attachment->getURLBySizeId('normal')}" alt="{$attachment->getDescription()}" />
         </a>
     {else}
         <a href="javascript:alert('{_attach_no_longer_available}');">
diff --git a/Web/Presenters/templates/components/comment.xml b/Web/Presenters/templates/components/comment.xml
index 9be7d838..461b8307 100644
--- a/Web/Presenters/templates/components/comment.xml
+++ b/Web/Presenters/templates/components/comment.xml
@@ -22,9 +22,16 @@
                     <div class="text" id="text{$comment->getId()}">
                         <span data-text="{$comment->getText(false)}" class="really_text">{$comment->getText()|noescape}</span>
                         
-                        <div n:ifcontent class="attachments_b">
-                            <div class="attachment" n:foreach="$comment->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
-                                {include "attachment.xml", attachment => $attachment}
+                        {var $attachmentsLayout = $comment->getChildrenWithLayout(288)}
+                        <div n:ifcontent class="attachments attachments_b" style="height: {$attachmentsLayout->height|noescape}; width: {$attachmentsLayout->width|noescape};">
+                            <div class="attachment" n:foreach="$attachmentsLayout->tiles as $attachment" style="float: {$attachment[3]|noescape}; width: {$attachment[0]|noescape}; height: {$attachment[1]|noescape};" data-localized-nsfw-text="{_nsfw_warning}">
+                                {include "attachment.xml", attachment => $attachment[2], parent => $comment, parentType => "comment"}
+                            </div>
+                        </div>
+
+                        <div n:ifcontent class="attachments attachments_m">
+                            <div class="attachment" n:foreach="$attachmentsLayout->extras as $attachment">
+                                {include "attachment.xml", attachment => $attachment, post => $comment}
                             </div>
                         </div>
                     </div>
diff --git a/Web/Presenters/templates/components/comments.xml b/Web/Presenters/templates/components/comments.xml
index 3290e35c..0df0c91f 100644
--- a/Web/Presenters/templates/components/comments.xml
+++ b/Web/Presenters/templates/components/comments.xml
@@ -4,7 +4,7 @@
     {var $commentsURL = "/al_comments/create/$model/" . $parent->getId()}
     {var $club = $parent instanceof \openvk\Web\Models\Entities\Post && $parent->getTargetWall() < 0 ? (new openvk\Web\Models\Repositories\Clubs)->get(abs($parent->getTargetWall())) : $club}
     {if !$readOnly}
-        {include "textArea.xml", route => $commentsURL, postOpts => false, graffiti => (bool) ovkGetQuirk("comments.allow-graffiti"), club => $club}
+        {include "textArea.xml", route => $commentsURL, postOpts => false, graffiti => (bool) ovkGetQuirk("comments.allow-graffiti"), club => $club, custom_id => $custom_id}
     {/if}
 </div>
 
diff --git a/Web/Presenters/templates/components/post/microblogpost.xml b/Web/Presenters/templates/components/post/microblogpost.xml
index bc499818..8dba32f4 100644
--- a/Web/Presenters/templates/components/post/microblogpost.xml
+++ b/Web/Presenters/templates/components/post/microblogpost.xml
@@ -73,10 +73,21 @@
                 <div class="post-content" id="{$post->getPrettyId()}">
                     <div class="text">
                         <span data-text="{$post->getText(false)}" class="really_text">{$post->getText()|noescape}</span>
+                        
+                        {var $width = ($GLOBALS["_bigWall"] ?? false) ? 550 : 320}
+                        {if isset($GLOBALS["_nesAttGloCou"])}
+                            {var $width = $width - 70 * $GLOBALS["_nesAttGloCou"]}
+                        {/if}
+                        {var $attachmentsLayout = $post->getChildrenWithLayout($width)}
+                        <div n:ifcontent class="attachments attachments_b" style="height: {$attachmentsLayout->height|noescape}; width: {$attachmentsLayout->width|noescape};">
+                            <div class="attachment" n:foreach="$attachmentsLayout->tiles as $attachment" style="float: {$attachment[3]|noescape}; width: {$attachment[0]|noescape}; height: {$attachment[1]|noescape};" data-localized-nsfw-text="{_nsfw_warning}">
+                                {include "../attachment.xml", attachment => $attachment[2], parent => $post, parentType => "post"}
+                            </div>
+                        </div>
 
-                        <div n:ifcontent class="attachments_b">
-                            <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
-                                {include "../attachment.xml", attachment => $attachment}
+                        <div n:ifcontent class="attachments attachments_m">
+                            <div class="attachment" n:foreach="$attachmentsLayout->extras as $attachment">
+                                {include "../attachment.xml", attachment => $attachment, post => $post}
                             </div>
                         </div>
                     </div>
diff --git a/Web/Presenters/templates/components/post/oldpost.xml b/Web/Presenters/templates/components/post/oldpost.xml
index 9dadeaab..3223e6b7 100644
--- a/Web/Presenters/templates/components/post/oldpost.xml
+++ b/Web/Presenters/templates/components/post/oldpost.xml
@@ -65,8 +65,19 @@
 
                         <span data-text="{$post->getText(false)}" class="really_text">{$post->getText()|noescape}</span>
 
-                        <div n:ifcontent class="attachments_b">
-                            <div class="attachment" n:foreach="$post->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
+                        {var $width = ($GLOBALS["_bigWall"] ?? false) ? 550 : 320}
+                        {if isset($GLOBALS["_nesAttGloCou"])}
+                            {var $width = $width - 70 * $GLOBALS["_nesAttGloCou"]}
+                        {/if}
+                        {var $attachmentsLayout = $post->getChildrenWithLayout($width)}
+                        <div n:ifcontent class="attachments attachments_b" style="height: {$attachmentsLayout->height|noescape}; width: {$attachmentsLayout->width|noescape};">
+                            <div class="attachment" n:foreach="$attachmentsLayout->tiles as $attachment" style="float: {$attachment[3]|noescape}; width: {$attachment[0]|noescape}; height: {$attachment[1]|noescape};" data-localized-nsfw-text="{_nsfw_warning}">
+                                {include "../attachment.xml", attachment => $attachment[2], parent => $post, parentType => "post"}
+                            </div>
+                        </div>
+
+                        <div n:ifcontent class="attachments attachments_m">
+                            <div class="attachment" n:foreach="$attachmentsLayout->extras as $attachment">
                                 {include "../attachment.xml", attachment => $attachment}
                             </div>
                         </div>
diff --git a/Web/Presenters/templates/components/textArea.xml b/Web/Presenters/templates/components/textArea.xml
index 939e5ad5..8727addf 100644
--- a/Web/Presenters/templates/components/textArea.xml
+++ b/Web/Presenters/templates/components/textArea.xml
@@ -1,5 +1,6 @@
 {php if(!isset($GLOBALS["textAreaCtr"])) $GLOBALS["textAreaCtr"] = 10;}
 {var $textAreaId = ($post ?? NULL) === NULL ? (++$GLOBALS["textAreaCtr"]) : $post->getId()}
+{var $textAreaId = ($custom_id ?? NULL) === NULL ? $textAreaId : $custom_id}
 
 <div id="write" style="padding: 5px 0;" onfocusin="expand_wall_textarea({$textAreaId});">
     <form action="{$route}" method="post" enctype="multipart/form-data" style="margin:0;">
@@ -8,8 +9,11 @@
             <!-- padding to fix <br/> bug -->
         </div>
         <div id="post-buttons{$textAreaId}" style="display: none;">
+            <div class="upload">
+
+            </div>
             <div class="post-upload">
-                {_attachment}: <span>(unknown)</span>
+                <span style="color: inherit;"></span>
             </div>
             <div class="post-has-poll">
                 {_poll}
@@ -56,7 +60,7 @@
                     <input type="checkbox" name="as_group" /> {_comment_as_group}
                 </label>
             </div>
-            <input type="file" class="postFileSel" id="postFilePic" name="_pic_attachment" accept="image/*" style="display:none;" />
+            <input type="hidden" name="photos" value="" />
             <input type="hidden" name="videos" value="" />
             <input type="hidden" name="poll" value="none" />
             <input type="hidden" id="note" name="note" value="none" />
@@ -73,7 +77,7 @@
                     <a class="header" href="javascript:toggleMenu({$textAreaId});">
                         {_attach}
                     </a>
-                    <a href="javascript:void(document.querySelector('#post-buttons{$textAreaId} input[name=_pic_attachment]').click());">
+                    <a id="photosAttachments" {if !is_null($club ?? NULL) && $club->canBeModifiedBy($thisUser)}data-club="{$club->getId()}"{/if}>
                         <img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-x-egon.png" />
                         {_photo}
                     </a>
@@ -101,14 +105,11 @@
 
 <script>
     $(document).ready(() => {
-        u("#post-buttons{$textAreaId} .postFileSel").on("change", function() {
-            handleUpload.bind(this, {$textAreaId})();
-        });
-
         setupWallPostInputHandlers({$textAreaId});
     });
 
     u("#post-buttons{$textAreaId} input[name='videos']")["nodes"].at(0).value = ""
+    u("#post-buttons{$textAreaId} input[name='photos']")["nodes"].at(0).value = ""
 </script>
 
 {if $graffiti}
diff --git a/Web/Util/Makima/Makima.php b/Web/Util/Makima/Makima.php
new file mode 100644
index 00000000..e31bfa42
--- /dev/null
+++ b/Web/Util/Makima/Makima.php
@@ -0,0 +1,305 @@
+<?php declare(strict_types=1);
+namespace openvk\Web\Util\Makima;
+use openvk\Web\Models\Entities\Photo;
+
+class Makima
+{
+    private $photos;
+
+    const ORIENT_WIDE    = 0;
+    const ORIENT_REGULAR = 1;
+    const ORIENT_SLIM    = 2;
+
+    function __construct(array $photos)
+    {
+        if(sizeof($photos) < 2)
+            throw new \LogicException("Minimum attachment count for tiled layout is 2");
+
+        $this->photos = $photos;
+    }
+
+    private function getOrientation(Photo $photo, &$ratio): int
+    {
+        [$width, $height] = $photo->getDimensions();
+        $ratio = $width / $height;
+        if($ratio >= 1.2)
+            return Makima::ORIENT_WIDE;
+        else if($ratio >= 0.8)
+            return Makima::ORIENT_REGULAR;
+        else
+            return Makima::ORIENT_SLIM;
+    }
+
+    private function calculateMultiThumbsHeight(array $ratios, float $w, float $m): float
+    {
+        return ($w - (sizeof($ratios) - 1) * $m) / array_sum($ratios);
+    }
+
+    private function extractSubArr(array $arr, int $from, int $to): array
+    {
+        return array_slice($arr, $from, sizeof($arr) - $from - (sizeof($arr) - $to));
+    }
+
+    function computeMasonryLayout(float $maxWidth, float $maxHeight): MasonryLayout
+    {
+        $orients = [];
+        $ratios  = [];
+        $count   = sizeof($this->photos);
+        $result  = new MasonryLayout;
+
+        foreach($this->photos as $photo) {
+            $orients[] = $this->getOrientation($photo, $ratio);
+            $ratios[]  = $ratio;
+        }
+
+        $avgRatio = array_sum($ratios) / sizeof($ratios);
+        if($maxWidth < 0)
+            $maxWidth = $maxHeight = 510;
+
+        $maxRatio    = $maxWidth / $maxHeight;
+        $marginWidth = $marginHeight = 2;
+
+        switch($count) {
+            case 2:
+                if(
+                    $orients == [Makima::ORIENT_WIDE, Makima::ORIENT_WIDE] # two wide pics
+                    && $avgRatio > (1.4 * $maxRatio) && abs($ratios[0] - $ratios[1]) < 0.2 # that can be positioned on top of each other
+                ) {
+                    $computedHeight = ceil( min( $maxWidth / $ratios[0], min( $maxWidth / $ratios[1], ($maxHeight - $marginHeight) / 2 ) ) );
+
+                    $result->colSizes = [1];
+                    $result->rowSizes = [1, 1];
+                    $result->width    = ceil($maxWidth);
+                    $result->height   = $computedHeight;
+                    $result->tiles    = [new ThumbTile(1, 1, $maxWidth, $computedHeight), new ThumbTile(1, 1, $maxWidth, $computedHeight)];
+                } else if(
+                    $orients == [Makima::ORIENT_WIDE, Makima::ORIENT_WIDE]
+                    || $orients == [Makima::ORIENT_REGULAR, Makima::ORIENT_REGULAR] # two normal pics of same ratio
+                ) {
+                    $computedWidth = ($maxWidth - $marginWidth) / 2;
+                    $height        = min( $computedWidth / $ratios[0], min( $computedWidth / $ratios[1], $maxHeight ) );
+
+                    $result->colSizes = [1, 1];
+                    $result->rowSizes = [1];
+                    $result->width    = ceil($maxWidth);
+                    $result->height   = ceil($height);
+                    $result->tiles    = [new ThumbTile(1, 1, $computedWidth, $height), new ThumbTile(1, 1, $computedWidth, $height)];
+                } else /* next to each other, different ratios */ {
+                    $w0 = (
+                        ($maxWidth - $marginWidth) / $ratios[1] / ( (1 / $ratios[0]) + (1 / $ratios[1]) )
+                    );
+                    $w1 = $maxWidth - $w0 - $marginWidth;
+                    $h  = min($maxHeight, min($w0 / $ratios[0], $w1 / $ratios[1]));
+
+                    $result->colSizes = [ceil($w0), ceil($w1)];
+                    $result->rowSizes = [1];
+                    $result->width    = ceil($w0 + $w1 + $marginWidth);
+                    $result->height   = ceil($h);
+                    $result->tiles    = [new ThumbTile(1, 1, $w0, $h), new ThumbTile(1, 1, $w1, $h)];
+                }
+            break;
+            case 3:
+                # Three wide photos, we will put two of them below and one on top
+                if($orients == [Makima::ORIENT_WIDE, Makima::ORIENT_WIDE, Makima::ORIENT_WIDE]) {
+                    $hCover = min($maxWidth / $ratios[0], ($maxHeight - $marginHeight) * (2 / 3));
+                    $w2     = ($maxWidth - $marginWidth) / 2;
+                    $h      = min($maxHeight - $hCover - $marginHeight, min($w2 / $ratios[1], $w2 / $ratios[2]));
+
+                    $result->colSizes = [1, 1];
+                    $result->rowSizes = [ceil($hCover), ceil($h)];
+                    $result->width    = ceil($maxWidth);
+                    $result->height   = ceil($marginHeight + $hCover + $h);
+                    $result->tiles    = [
+                        new ThumbTile(2, 1, $maxWidth, $hCover),
+                        new ThumbTile(1, 1, $w2, $h), new ThumbTile(1, 1, $w2, $h),
+                    ];
+                } else /* Photos have different sizes or are not wide, so we will put one to left and two to the right */ {
+                    $wCover = min($maxHeight * $ratios[0], ($maxWidth - $marginWidth) * (3 / 4));
+                    $h1     = ($ratios[1] * ($maxHeight - $marginHeight) / ($ratios[2] + $ratios[1]));
+                    $h0     = $maxHeight - $marginHeight - $h1;
+                    $w      = min($maxWidth - $marginWidth - $wCover, min($h1 * $ratios[2], $h0 * $ratios[1]));
+
+                    $result->colSizes = [ceil($wCover), ceil($w)];
+                    $result->rowSizes = [ceil($h0), ceil($h1)];
+                    $result->width    = ceil($w + $wCover + $marginWidth);
+                    $result->height   = ceil($maxHeight);
+                    $result->tiles    = [
+                        new ThumbTile(1, 2, $wCover, $maxHeight), new ThumbTile(1, 1, $w, $h0),
+                                                                  new ThumbTile(1, 1, $w, $h1),
+                    ];
+                }
+            break;
+            case 4:
+                # Four wide photos, we will put one to the top and rest below
+                if($orients == [Makima::ORIENT_WIDE, Makima::ORIENT_WIDE, Makima::ORIENT_WIDE, Makima::ORIENT_WIDE]) {
+                    $hCover = min($maxWidth / $ratios[0], ($maxHeight - $marginHeight) / (2 / 3));
+                    $h      = ($maxWidth - 2 * $marginWidth) / (array_sum($ratios) - $ratios[0]);
+                    $w0     = $h * $ratios[1];
+                    $w1     = $h * $ratios[2];
+                    $w2     = $h * $ratios[3];
+                    $h      = min($maxHeight - $marginHeight - $hCover, $h);
+
+                    $result->colSizes = [ceil($w0), ceil($w1), ceil($w2)];
+                    $result->rowSizes = [ceil($hCover), ceil($h)];
+                    $result->width    = ceil($maxWidth);
+                    $result->height   = ceil($hCover + $marginHeight + $h);
+                    $result->tiles    = [
+                                                new ThumbTile(3, 1, $maxWidth, $hCover),
+                        new ThumbTile(1, 1, $w0, $h), new ThumbTile(1, 1, $w1, $h), new ThumbTile(1, 1, $w2, $h),
+                    ];
+                } else /* Four photos, we will put one to the left and rest to the right */ {
+                    $wCover = min($maxHeight * $ratios[0], ($maxWidth - $marginWidth) * (2 / 3));
+                    $w      = ($maxHeight - 2 * $marginHeight) / (1 / $ratios[1] + 1 / $ratios[2] + 1 / $ratios[3]);
+                    $h0     = $w / $ratios[1];
+                    $h1     = $w / $ratios[2];
+                    $h2     = $w / $ratios[3] + $marginHeight;
+                    $w      = min($w, $maxWidth - $marginWidth - $wCover);
+
+                    $result->colSizes = [ceil($wCover), ceil($w)];
+                    $result->rowSizes = [ceil($h0), ceil($h1), ceil($h2)];
+                    $result->width    = ceil($wCover + $marginWidth + $w);
+                    $result->height   = ceil($maxHeight);
+                    $result->tiles    = [
+                        new ThumbTile(1, 3, $wCover, $maxHeight), new ThumbTile(1, 1, $w, $h0),
+                                                                  new ThumbTile(1, 1, $w, $h1),
+                                                                  new ThumbTile(1, 1, $w, $h1),
+                    ];
+                }
+            break;
+            default:
+                // как лопать пузырики
+                $ratiosCropped = [];
+                if($avgRatio > 1.1) {
+                    foreach($ratios as $ratio)
+                        $ratiosCropped[] = max($ratio, 1.0);
+                } else {
+                    foreach($ratios as $ratio)
+                        $ratiosCropped[] = min($ratio, 1.0);
+                }
+
+                $tries = [];
+
+                $firstLine;
+                $secondLine;
+                $thirdLine;
+
+                # Try one line:
+                $tries[$firstLine = $count] = [$this->calculateMultiThumbsHeight($ratiosCropped, $maxWidth, $marginWidth)];
+
+                # Try two lines:
+                for($firstLine = 1; $firstLine < ($count - 1); $firstLine++) {
+                    $secondLine  = $count - $firstLine;
+                    $key         = "$firstLine&$secondLine";
+                    $tries[$key] = [
+                        $this->calculateMultiThumbsHeight(array_slice($ratiosCropped, 0, $firstLine), $maxWidth, $marginWidth),
+                        $this->calculateMultiThumbsHeight(array_slice($ratiosCropped, $firstLine), $maxWidth, $marginWidth),
+                    ];
+                }
+
+                # Try three lines:
+                for($firstLine = 1; $firstLine < ($count - 2); $firstLine++) {
+                    for($secondLine  = 1; $secondLine < ($count - $firstLine - 1); $secondLine++) {
+                        $thirdLine   = $count - $firstLine - $secondLine;
+                        $key         = "$firstLine&$secondLine&$thirdLine";
+                        $tries[$key] = [
+                            $this->calculateMultiThumbsHeight(array_slice($ratiosCropped, 0, $firstLine), $maxWidth, $marginWidth),
+                            $this->calculateMultiThumbsHeight($this->extractSubArr($ratiosCropped, $firstLine, $firstLine + $secondLine), $maxWidth, $marginWidth),
+                            $this->calculateMultiThumbsHeight($this->extractSubArr($ratiosCropped, $firstLine + $secondLine, sizeof($ratiosCropped)), $maxWidth, $marginWidth),
+                        ];
+                    }
+                }
+
+                # Now let's find the most optimal configuration:
+                $optimalConfiguration = $optimalDifference = NULL;
+                foreach($tries as $config => $heights) {
+                    $config = explode('&', (string) $config); # да да стринговые ключи пхп даже со стриктайпами автокастует к инту (см. 187)
+                    $confH  = $marginHeight * (sizeof($heights) - 1);
+                    foreach($heights as $h)
+                        $confH += $h;
+
+                    $confDiff = abs($confH - $maxHeight);
+                    if(sizeof($config) > 1)
+                        if($config[0] > $config[1] || sizeof($config) >= 2 && $config[1] > $config[2])
+                            $confDiff *= 1.1;
+
+                    if(!$optimalConfiguration || $confDigff < $optimalDifference) {
+                        $optimalConfiguration = $config;
+                        $optimalDifference    = $confDiff;
+                    }
+                }
+
+                $thumbsRemain = $this->photos;
+                $ratiosRemain = $ratiosCropped;
+                $optHeights   = $tries[implode('&', $optimalConfiguration)];
+                $k            = 0;
+
+                $result->width    = ceil($maxWidth);
+                $result->rowSizes = [sizeof($optHeights)];
+                $result->tiles    = [];
+
+                $totalHeight     = 0.0;
+                $gridLineOffsets = [];
+                $rowTiles        = []; // vector<vector<ThumbTile>>
+
+                for($i = 0; $i < sizeof($optimalConfiguration); $i++) {
+                    $lineChunksNum = $optimalConfiguration[$i];
+                    $lineThumbs    = [];
+                    for($j = 0; $j < $lineChunksNum; $j++)
+                        $lineThumbs[] = array_shift($thumbsRemain);
+
+                    $lineHeight   = $optHeights[$i];
+                    $totalHeight += $lineHeight;
+
+                    $result->rowSizes[$i] = ceil($lineHeight);
+
+                    $totalWidth = 0;
+                    $row        = [];
+                    for($j = 0; $j < sizeof($lineThumbs); $j++) {
+                        $thumbRatio = array_shift($ratiosRemain);
+                        if($j == sizeof($lineThumbs) - 1)
+                            $w = $maxWidth - $totalWidth;
+                        else
+                            $w = $thumbRatio * $lineHeight;
+
+                        $totalWidth += ceil($w);
+                        if($j < (sizeof($lineThumbs) - 1) && !in_array($totalWidth, $gridLineOffsets))
+                            $gridLineOffsets[] = $totalWidth;
+
+                        $tile = new ThumbTile(1, 1, $w, $lineHeight);
+                        $result->tiles[$k++] = $row[] = $tile;
+                    }
+
+                    $rowTiles[] = $row;
+                }
+
+                sort($gridLineOffsets, SORT_NUMERIC);
+                $gridLineOffsets[] = $maxWidth;
+
+                $result->colSizes = [$gridLineOffsets[0]];
+                for($i = sizeof($gridLineOffsets) - 1; $i > 0; $i--)
+                    $result->colSizes[$i] = $gridLineOffsets[$i] - $gridLineOffsets[$i - 1];
+
+                foreach($rowTiles as $row) {
+                    $columnOffset = 0;
+                    foreach($row as $tile) {
+                        $startColumn   = $columnOffset;
+                        $width         = 0;
+                        $tile->colSpan = 0;
+                        for($i = $startColumn; $i < sizeof($result->colSizes); $i++) {
+                            $width += $result->colSizes[$i];
+                            $tile->colSpan++;
+                            if($width == $tile->width)
+                                break;
+                        }
+
+                        $columnOffset += $tile->colSpan;
+                    }
+                }
+
+                $result->height = ceil($totalHeight + $marginHeight * (sizeof($optHeights) - 1));
+            break;
+        }
+
+        return $result;
+    }
+}
diff --git a/Web/Util/Makima/MasonryLayout.php b/Web/Util/Makima/MasonryLayout.php
new file mode 100644
index 00000000..b23aa483
--- /dev/null
+++ b/Web/Util/Makima/MasonryLayout.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+namespace openvk\Web\Util\Makima;
+
+class MasonryLayout {
+    public $colSizes;
+    public $rowSizes;
+    public $tiles;
+    public $width;
+    public $height;
+}
diff --git a/Web/Util/Makima/ThumbTile.php b/Web/Util/Makima/ThumbTile.php
new file mode 100644
index 00000000..360f280c
--- /dev/null
+++ b/Web/Util/Makima/ThumbTile.php
@@ -0,0 +1,14 @@
+<?php declare(strict_types=1);
+namespace openvk\Web\Util\Makima;
+
+class ThumbTile {
+    public $width;
+    public $height;
+    public $rowSpan;
+    public $colSpan;
+
+    function __construct(int $rs, int $cs, float $w, float $h)
+    {
+        [$this->width, $this->height, $this->rowSpan, $this->colSpan] = [ceil($w), ceil($h), $rs, $cs];
+    }
+}
diff --git a/Web/routes.yml b/Web/routes.yml
index bc95f44a..dea8ddd5 100644
--- a/Web/routes.yml
+++ b/Web/routes.yml
@@ -371,6 +371,8 @@ routes:
       handler: "About->humansTxt"
     - url: "/dev"
       handler: "About->dev"
+    - url: "/iapi/getPhotosFromPost/{num}_{num}"
+      handler: "InternalAPI->getPhotosFromPost"
     - url: "/tour"
       handler: "About->tour"
     - url: "/{?shortCode}"
diff --git a/Web/static/css/main.css b/Web/static/css/main.css
index caa49283..47d67317 100644
--- a/Web/static/css/main.css
+++ b/Web/static/css/main.css
@@ -744,10 +744,14 @@ h4 {
     line-height: 130%;
 }
 
-.post-content .attachments_b {
+.post-content .attachments:first-of-type {
     margin-top: 8px;
 }
 
+.post-content .attachments_m .attachment {
+    width: 98%;
+}
+
 .attachment .post {
     width: 102%;
 }
@@ -757,6 +761,12 @@ h4 {
     image-rendering: -webkit-optimize-contrast;
 }
 
+.post-content .media_makima {
+    width: calc(100% - 4px);
+    height: calc(100% - 4px);
+    object-fit: cover;
+}
+
 .post-signature {
     margin: 4px;
     margin-bottom: 2px;
@@ -2279,6 +2289,124 @@ a.poll-retract-vote {
     border-radius: 1px;
 }
 
+.progress {
+    border: 1px solid #eee;
+    height: 15px;
+    background: linear-gradient(to bottom, #fefefe, #fafafa);
+}
+
+.progress .progress-bar {
+    background: url('progress.png');
+    background-repeat: repeat-x;
+    height: 15px;
+    animation-name: progress;
+    animation-duration: 1s;
+    animation-iteration-count: infinite;
+    animation-timing-function: linear;
+}
+
+@keyframes progress {
+    from {
+        background-position: 0;
+    }
+
+    to {
+        background-position: 20px;
+    }
+}
+
+.upload {
+    margin-top: 8px;
+}
+
+.upload .upload-item {
+    width: 75px;
+    height: 60px;
+    overflow: hidden;
+    display: inline-block;
+    margin-right: 3px;
+}
+
+.upload-item .upload-delete {
+    position: absolute;
+    background: rgba(0,0,0,0.5);
+    padding: 2px 5px;
+    text-decoration: none;
+    color: #fff;
+    font-size: 11px;
+    margin-left: 57px; /* мне лень переделывать :DDDD */
+    opacity: 0;
+    transition: 0.25s;
+}
+
+.upload-item:hover > .upload-delete {
+    opacity: 1;
+}
+
+.upload-item img {
+    width: 100%;
+    max-height: 60px;
+    object-fit: cover;
+    border-radius: 3px;
+}
+
+/* https://imgur.com/a/ihB3JZ4 */
+
+.ovk-photo-view-dimmer {
+    position: fixed;
+    left: 0px;
+    top: 0px;
+    right: 0px;
+    bottom: 0px;
+    overflow: auto;
+    padding-bottom: 20px;
+    z-index: 300;
+}
+
+.ovk-photo-view {
+    position: relative;
+    z-index: 999;
+    background: #fff;
+    width: 610px;
+    padding: 20px;
+    padding-top: 15px;
+    padding-bottom: 10px;
+    box-shadow: 0px 0px 3px 1px #222;
+    margin: 15px auto 0 auto;
+}
+
+.ovk-photo-details {
+    overflow: auto;
+}
+
+.photo_com_title {
+	font-weight: bold;
+	padding-bottom: 20px;
+}
+
+.photo_com_title div {
+	float: right;
+	font-weight: normal;
+}
+
+.ovk-photo-slide-left {
+    left: 0;
+    width: 35%;
+    height: 100%;
+    max-height: 60vh;
+    position: absolute;
+    cursor: pointer;
+}
+
+.ovk-photo-slide-right {
+    right: 0;
+    width: 35%;
+    height: 100%;
+    max-height: 60vh;
+    position: absolute;
+    cursor: pointer;
+}
+
 .client_app > img {
 	top: 3px;
 	position: relative;
diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js
index ef3d5dba..39ee2199 100644
--- a/Web/static/js/al_wall.js
+++ b/Web/static/js/al_wall.js
@@ -22,37 +22,14 @@ function trim(string) {
     return newStr;
 }
 
-function handleUpload(id) {
-    console.warn("блять...");
-    
-    u("#post-buttons" + id + " .postFileSel").not("#" + this.id).each(input => input.value = null);
-    
-    var indicator = u("#post-buttons" + id + " .post-upload");
-    var file      = this.files[0];
-    if(typeof file === "undefined") {
-        indicator.attr("style", "display: none;");
-    } else {
-        u("span", indicator.nodes[0]).text(trim(file.name) + " (" + humanFileSize(file.size, false) + ")");
-        indicator.attr("style", "display: block;");
-    }
-
-    document.querySelector("#post-buttons" + id + " #wallAttachmentMenu").classList.add("hidden");
-}
-
 function initGraffiti(id) {
     let canvas = null;
     let msgbox = MessageBox(tr("draw_graffiti"), "<div id='ovkDraw'></div>", [tr("save"), tr("cancel")], [function() {
         canvas.getImage({includeWatermark: false}).toBlob(blob => {
             let fName = "Graffiti-" + Math.ceil(performance.now()).toString() + ".jpeg";
             let image = new File([blob], fName, {type: "image/jpeg", lastModified: new Date().getTime()});
-            let trans = new DataTransfer();
-            trans.items.add(image);
             
-            let fileSelect = document.querySelector("#post-buttons" + id + " input[name='_pic_attachment']");
-            fileSelect.files = trans.files;
-            
-            u(fileSelect).trigger("change");
-            u("#post-buttons" + id + " #write textarea").trigger("focusin");
+            fastUploadImage(id, image)
         }, "image/jpeg", 0.92);
         
         canvas.teardown();
@@ -75,6 +52,79 @@ function initGraffiti(id) {
     });
 }
 
+function fastUploadImage(textareaId, file) {
+    // uploading images
+
+    if(!file.type.startsWith('image/')) {
+        MessageBox(tr("error"), tr("only_images_accepted", escapeHtml(file.name)), [tr("ok")], [() => {Function.noop}])
+        return;
+    }
+
+    // 🤓🤓🤓
+    if(file.size > 5 * 1024 * 1024) {
+        MessageBox(tr("error"), tr("max_filesize", 5), [tr("ok")], [() => {Function.noop}])
+        return;
+    }
+
+    let imagesCount = document.querySelector("#post-buttons" + textareaId + " input[name='photos']").value.split(",").length
+
+    if(imagesCount > 10) {
+        MessageBox(tr("error"), tr("too_many_photos"), [tr("ok")], [() => {Function.noop}])
+        return
+    }
+
+    let xhr = new XMLHttpRequest
+    let data = new FormData
+
+    data.append("photo_0", file)
+    data.append("count", 1)
+    data.append("hash", u("meta[name=csrf]").attr("value"))
+
+    xhr.open("POST", "/photos/upload")
+
+    xhr.onloadstart = () => {
+        document.querySelector("#post-buttons"+textareaId+" .upload").insertAdjacentHTML("beforeend", `<img id="loader" src="/assets/packages/static/openvk/img/loading_mini.gif">`)
+    }
+
+    xhr.onload = () => {
+        let response = JSON.parse(xhr.responseText)
+
+        appendImage(response, textareaId)
+    }
+
+    xhr.send(data)
+}
+
+// append image after uploading via /photos/upload
+function appendImage(response, textareaId) {
+    if(!response.success) {
+        MessageBox(tr("error"), (tr("error_uploading_photo") + response.flash.message), [tr("ok")], [() => {Function.noop}])
+    } else {
+        let form        = document.querySelector("#post-buttons"+textareaId)
+        let photosInput = form.querySelector("input[name='photos']")
+        let photosIndicator = form.querySelector(".upload")
+        
+        for(const phot of response.photos) {
+            let id = phot.owner + "_" + phot.vid
+
+            photosInput.value += (id + ",")
+
+            u(photosIndicator).append(u(`
+                <div class="upload-item" id="aP" data-id="${id}">
+                    <a class="upload-delete">×</a>
+                    <img src="${phot.url}">
+                </div>
+            `))
+
+            u(photosIndicator.querySelector(`.upload #aP[data-id='${id}'] .upload-delete`)).on("click", () => {
+                photosInput.value = photosInput.value.replace(id + ",", "")
+                u(form.querySelector(`.upload #aP[data-id='${id}']`)).remove()
+            })
+        }
+    }
+    u(`#post-buttons${textareaId} .upload #loader`).remove()
+}
+
 u(".post-like-button").on("click", function(e) {
     e.preventDefault();
     
@@ -97,11 +147,12 @@ u(".post-like-button").on("click", function(e) {
 
 function setupWallPostInputHandlers(id) {
     u("#wall-post-input" + id).on("paste", function(e) {
+        // Если вы находитесь на странице с постом с id 11, то копирование произойдёт джва раза.
+        // Оч ржачный баг, но вот как его исправить, я, если честно, не знаю.
+
         if(e.clipboardData.files.length === 1) {
-            var input = u("#post-buttons" + id + " input[name=_pic_attachment]").nodes[0];
-            input.files = e.clipboardData.files;
-            
-            u(input).trigger("change");
+            fastUploadImage(id, e.clipboardData.files[0])
+            return;
         }
     });
     
@@ -116,6 +167,183 @@ function setupWallPostInputHandlers(id) {
         // revert to original size if it is larger (possibly changed by user)
         // textArea.style.height = (newHeight > originalHeight ? (newHeight + boost) : originalHeight) + "px";
     });
+
+    u("#wall-post-input" + id).on("dragover", function(e) {
+        e.preventDefault()
+
+        // todo add animation
+        return;
+    });
+
+    $("#wall-post-input" + id).on("drop", function(e) {
+        e.originalEvent.dataTransfer.dropEffect = 'move';
+        fastUploadImage(id, e.originalEvent.dataTransfer.files[0])
+        return;
+    });
+}
+
+function OpenMiniature(e, photo, post, photo_id, type = "post") {
+    /*
+    костыли но смешные однако
+    */
+    e.preventDefault();
+
+    if(u(".ovk-photo-view").length > 0) u(".ovk-photo-view-dimmer").remove();
+
+    // Значения для переключения фоток
+
+    let json;
+
+    let imagesCount = 0;
+    let imagesIndex = 0;
+
+    let tempDetailsSection = [];
+    
+    let dialog = u(
+    `<div class="ovk-photo-view-dimmer">
+        <div class="ovk-photo-view">
+            <div class="photo_com_title">
+                <text id="photo_com_title_photos">
+                    <img src="/assets/packages/static/openvk/img/loading_mini.gif">
+                </text>
+                <div>
+                    <a id="ovk-photo-close">${tr("close")}</a>
+                </div>
+            </div>
+            <center style="margin-bottom: 8pt;">
+                <div class="ovk-photo-slide-left"></div>
+                <div class="ovk-photo-slide-right"></div>
+                <img src="${photo}" style="max-width: 100%; max-height: 60vh; user-select:none;" id="ovk-photo-img">
+            </center>
+            <div class="ovk-photo-details">
+                <img src="/assets/packages/static/openvk/img/loading_mini.gif">
+            </div>
+        </div>
+    </div>`);
+    u("body").addClass("dimmed").append(dialog);
+    document.querySelector("html").style.overflowY = "hidden"
+    
+    let button = u("#ovk-photo-close");
+
+    button.on("click", function(e) {
+        let __closeDialog = () => {
+            u("body").removeClass("dimmed");
+            u(".ovk-photo-view-dimmer").remove();
+            document.querySelector("html").style.overflowY = "scroll"
+        };
+        
+        __closeDialog();
+    });
+
+    function __reloadTitleBar() {
+        u("#photo_com_title_photos").last().innerHTML = imagesCount > 1 ? tr("photo_x_from_y", imagesIndex, imagesCount) : tr("photo");
+    }
+
+    function __loadDetails(photo_id, index) {
+        if(tempDetailsSection[index] == null) {
+            u(".ovk-photo-details").last().innerHTML = '<img src="/assets/packages/static/openvk/img/loading_mini.gif">';
+            ky("/photo" + photo_id, {
+                hooks: {
+                    afterResponse: [
+                        async (_request, _options, response) => {
+                            let parser = new DOMParser();
+                            let body = parser.parseFromString(await response.text(), "text/html");
+
+                            let element = u(body.getElementsByClassName("ovk-photo-details")).last();
+
+                            tempDetailsSection[index] = element.innerHTML;
+
+                            if(index == imagesIndex) {
+                                u(".ovk-photo-details").last().innerHTML = element.innerHTML;
+                            }
+
+                            document.querySelectorAll(".ovk-photo-details .bsdn").forEach(bsdnInitElement)
+                            document.querySelectorAll(".ovk-photo-details script").forEach(scr => {
+                                // stolen from #953
+                                let newScr = document.createElement('script')
+
+                                if(scr.src) {
+                                    newScr.src = scr.src
+                                } else {
+                                    newScr.textContent = scr.textContent
+                                }
+
+                                document.querySelector(".ovk-photo-details").appendChild(newScr);
+                            })
+                        }
+                    ]
+                }
+            });
+        } else {
+            u(".ovk-photo-details").last().innerHTML = tempDetailsSection[index];
+        }
+    }
+
+    function __slidePhoto(direction) {
+        /* direction = 1 - right
+           direction = 0 - left */
+        if(json == undefined) {
+            console.log("Да подожди ты. Куда торопишься?");
+        } else {
+            if(imagesIndex >= imagesCount && direction == 1) {
+                imagesIndex = 1;
+            } else if(imagesIndex <= 1 && direction == 0) {
+                imagesIndex = imagesCount;
+            } else if(direction == 1) {
+                imagesIndex++;
+            } else if(direction == 0) {
+                imagesIndex--;
+            }
+
+            let photoURL = json.body[imagesIndex - 1].url;
+
+            u("#ovk-photo-img").last().src = photoURL;
+            __reloadTitleBar();
+            __loadDetails(json.body[imagesIndex - 1].id, imagesIndex);
+        }
+    }
+
+    let slideLeft = u(".ovk-photo-slide-left");
+
+    slideLeft.on("click", (e) => {
+        __slidePhoto(0);
+    });
+
+    let slideRight = u(".ovk-photo-slide-right");
+
+    slideRight.on("click", (e) => {
+        __slidePhoto(1);
+    });
+
+    let data = new FormData()
+    data.append('parentType', type);
+    ky.post("/iapi/getPhotosFromPost/" + (type == "post" ? post : "1_"+post), {
+        hooks: {
+            afterResponse: [
+                async (_request, _options, response) => {
+                    json = await response.json();
+
+                    imagesCount = json.body.length;
+                    imagesIndex = 0;
+                    // Это всё придётся правда на 1 прибавлять
+                    
+                    json.body.every(element => {
+                        imagesIndex++;
+                        if(element.id == photo_id) {
+                            return false;
+                        } else {
+                            return true;
+                        }
+                    });
+
+                    __reloadTitleBar();
+                    __loadDetails(json.body[imagesIndex - 1].id, imagesIndex);                }
+            ]
+        },
+        body: data
+    });
+
+    return u(".ovk-photo-view-dimmer");
 }
 
 u("#write > form").on("keydown", function(event) {
@@ -210,6 +438,7 @@ function addNote(textareaId, nid)
 
     u("body").removeClass("dimmed");
     u(".ovk-diag-cont").remove();
+    document.querySelector("html").style.overflowY = "scroll"
 }
 
 async function attachNote(id)
@@ -525,3 +754,210 @@ $(document).on("click", "#editPost", (e) => {
         text.style.display = "block"
     }
 })
+
+// copypaste from videos picker
+$(document).on("click", "#photosAttachments", async (e) => {
+    let body = `
+        <div class="topGrayBlock">
+            <div style="padding-top: 7px;padding-left: 12px;">
+                ${tr("upload_new_photo")}:
+                <input type="file" multiple accept="image/*" id="fastFotosUplod" style="display:none">
+                <input type="button" class="button" value="${tr("upload_button")}" onclick="fastFotosUplod.click()">
+                <select id="albumSelect" style="width: 154px;float: right;margin-right: 17px;">
+                    <option value="0">${tr("all_photos")}</option>
+                </select>
+            </div>
+        </div>
+
+        <div class="photosInsert" style="padding: 5px;height: 287px;overflow-y: scroll;">
+            <div style="position: fixed;z-index: 1007;width: 92%;background: white;margin-top: -5px;padding-top: 6px;"><h4>${tr("is_x_photos", 0)}</h4></div>
+            <div class="photosList album-flex" style="margin-top: 20px;"></div>
+        </div>
+    `
+
+    let form = e.currentTarget.closest("form")
+
+    MessageBox(tr("select_photo"), body, [tr("close")], [Function.noop]);
+
+    document.querySelector(".ovk-diag-body").style.padding = "0"
+    document.querySelector(".ovk-diag-cont").style.width = "630px"
+    document.querySelector(".ovk-diag-body").style.height = "335px"
+
+    async function insertPhotos(page, album = 0) {
+        u("#loader").remove()
+
+        let insertPlace = document.querySelector(".photosInsert .photosList")
+        document.querySelector(".photosInsert").insertAdjacentHTML("beforeend", `<img id="loader" style="max-height: 8px;max-width: 36px;" src="/assets/packages/static/openvk/img/loading_mini.gif">`)
+        
+        let photos;
+
+        try {
+            photos = await API.Photos.getPhotos(page, Number(album))
+        } catch(e) {
+            document.querySelector(".photosInsert h4").innerHTML = tr("is_x_photos", -1)
+            insertPlace.innerHTML = "Invalid album"
+            console.error(e)
+            u("#loader").remove()
+            return;
+        }
+
+        document.querySelector(".photosInsert h4").innerHTML = tr("is_x_photos", photos.count)
+
+        let pagesCount = Math.ceil(Number(photos.count) / 24)
+        u("#loader").remove()
+
+        for(const photo of photos.items) {
+            let isAttached = (form.querySelector("input[name='photos']").value.includes(`${photo.owner_id}_${photo.id},`))
+
+            insertPlace.insertAdjacentHTML("beforeend", `
+            <div style="width: 14%;margin-bottom: 7px;margin-left: 13px;" class="album-photo" data-attachmentdata="${photo.owner_id}_${photo.id}" data-preview="${photo.photo_130}">          
+                <a href="/photo${photo.owner_id}_${photo.id}">
+                    <img class="album-photo--image" src="${photo.photo_130}" alt="..." style="${isAttached ? "background-color: #646464" : ""}">
+                </a>
+            </div>
+            `)
+        }
+
+        if(page < pagesCount) {
+            insertPlace.insertAdjacentHTML("beforeend", `
+            <div id="showMorePhotos" data-pagesCount="${pagesCount}" data-page="${page + 1}" style="width: 100%;text-align: center;background: #f0f0f0;height: 22px;padding-top: 9px;cursor:pointer;">
+                <span>more...</span>
+            </div>`)
+        }
+    }
+
+    insertPhotos(1)
+
+    let albums = await API.Photos.getAlbums(Number(e.currentTarget.dataset.club ?? 0))
+    
+    for(const alb of albums.items) {
+        let sel = document.querySelector(".ovk-diag-body #albumSelect")
+
+        sel.insertAdjacentHTML("beforeend", `<option value="${alb.id}">${ovk_proc_strtr(escapeHtml(alb.name), 20)}</option>`)
+    }
+
+    $(".photosInsert").on("click", "#showMorePhotos", (e) => {
+        u(e.currentTarget).remove()
+        insertPhotos(Number(e.currentTarget.dataset.page), document.querySelector(".topGrayBlock #albumSelect").value)
+    })
+
+    $(".topGrayBlock #albumSelect").on("change", (evv) => {
+        document.querySelector(".photosInsert .photosList").innerHTML = ""
+
+        insertPhotos(1, evv.currentTarget.value)
+    })
+
+    function insertAttachment(id) {
+        let photos = form.querySelector("input[name='photos']") 
+
+        if(!photos.value.includes(id + ",")) {
+            if(photos.value.split(",").length > 10) {
+                NewNotification(tr("error"), tr("max_attached_photos"))
+                return false
+            }
+
+            form.querySelector("input[name='photos']").value += (id + ",")
+
+            console.info(id + " attached")
+            return true
+        } else {
+            form.querySelector("input[name='photos']").value = form.querySelector("input[name='photos']").value.replace(id + ",", "")
+
+            console.info(id + " detached")
+            return false
+        }
+    }
+
+    $(".photosList").on("click", ".album-photo", (ev) => {
+        ev.preventDefault()
+
+        if(!insertAttachment(ev.currentTarget.dataset.attachmentdata)) {
+            u(form.querySelector(`.upload #aP[data-id='${ev.currentTarget.dataset.attachmentdata}']`)).remove()
+            ev.currentTarget.querySelector("img").style.backgroundColor = "white"
+        } else {
+            ev.currentTarget.querySelector("img").style.backgroundColor = "#646464"
+            let id = ev.currentTarget.dataset.attachmentdata
+
+            u(form.querySelector(`.upload`)).append(u(`
+                <div class="upload-item" id="aP" data-id="${ev.currentTarget.dataset.attachmentdata}">
+                    <a class="upload-delete">×</a>
+                    <img src="${ev.currentTarget.dataset.preview}">
+                </div>
+            `));
+
+            u(`.upload #aP[data-id='${ev.currentTarget.dataset.attachmentdata}'] .upload-delete`).on("click", () => {
+                form.querySelector("input[name='photos']").value = form.querySelector("input[name='photos']").value.replace(id + ",", "")
+                u(form.querySelector(`.upload #aP[data-id='${ev.currentTarget.dataset.attachmentdata}']`)).remove()
+            })
+        }
+    })
+
+    u("#fastFotosUplod").on("change", (evn) => {
+        let xhr = new XMLHttpRequest()
+        xhr.open("POST", "/photos/upload")
+
+        let formdata = new FormData()
+        let iterator = 0
+
+        for(const fille of evn.currentTarget.files) {
+            if(!fille.type.startsWith('image/')) {
+                continue;
+            }
+
+            if(fille.size > 5 * 1024 * 1024) {
+                continue;
+            }
+
+            if(evn.currentTarget.files.length >= 10) {
+                NewNotification(tr("error"), tr("max_attached_photos"))
+                return;
+            }
+
+            formdata.append("photo_"+iterator, fille)
+            iterator += 1
+        }
+        
+        xhr.onloadstart = () => {
+            evn.currentTarget.parentNode.insertAdjacentHTML("beforeend", `<img id="loader" style="max-height: 8px;max-width: 36px;" src="/assets/packages/static/openvk/img/loading_mini.gif">`)
+        }
+
+        xhr.onload = () => {
+            let result = JSON.parse(xhr.responseText)
+
+            u("#loader").remove()
+            if(result.success) {
+                for(const pht of result.photos) {
+                    let id = pht.owner + "_" + pht.vid
+
+                    if(!insertAttachment(id)) {
+                        return
+                    }
+                    
+                    u(form.querySelector(`.upload`)).append(u(`
+                        <div class="upload-item" id="aP" data-id="${pht.owner + "_" + pht.vid}">
+                            <a class="upload-delete">×</a>
+                            <img src="${pht.url}">
+                        </div>
+                    `));
+
+                    u(`.upload #aP[data-id='${pht.owner + "_" + pht.vid}'] .upload-delete`).on("click", () => {
+                        form.querySelector("input[name='photos']").value = form.querySelector("input[name='photos']").value.replace(id + ",", "")
+                        u(form.querySelector(`.upload #aP[data-id='${id}']`)).remove()
+                    })
+                }
+
+                u("body").removeClass("dimmed");
+                u(".ovk-diag-cont").remove();
+                document.querySelector("html").style.overflowY = "scroll"
+            } else {
+                // todo: https://vk.com/wall-32295218_78593
+                alert(result.flash.message)
+            }
+        }
+
+        formdata.append("hash", u("meta[name=csrf]").attr("value"))
+        formdata.append("count", iterator)
+        
+        xhr.send(formdata)
+    })
+})
diff --git a/Web/static/js/messagebox.js b/Web/static/js/messagebox.js
index 368311dd..e56c720f 100644
--- a/Web/static/js/messagebox.js
+++ b/Web/static/js/messagebox.js
@@ -20,9 +20,12 @@ function MessageBox(title, body, buttons, callbacks) {
         
         button.on("click", function(e) {
             let __closeDialog = () => {
-                u("body").removeClass("dimmed");
+                if(document.querySelector(".ovk-photo-view-dimmer") == null) {
+                    u("body").removeClass("dimmed");
+                    document.querySelector("html").style.overflowY = "scroll"
+                }
+
                 u(".ovk-diag-cont").remove();
-                document.querySelector("html").style.overflowY = "scroll"
             };
             
             Reflect.apply(callbacks[callback], {
diff --git a/Web/static/js/openvk.cls.js b/Web/static/js/openvk.cls.js
index 22144cfc..b131bfa0 100644
--- a/Web/static/js/openvk.cls.js
+++ b/Web/static/js/openvk.cls.js
@@ -68,7 +68,7 @@ function toggleMenu(id) {
 }
 document.addEventListener("DOMContentLoaded", function() { //BEGIN
 
-    u("#_photoDelete").on("click", function(e) {
+    $(document).on("click", "#_photoDelete", function(e) {
         var formHtml = "<form id='tmpPhDelF' action='" + u(this).attr("href") + "' >";
         formHtml    += "<input type='hidden' name='hash' value='" + u("meta[name=csrf]").attr("value") + "' />";
         formHtml    += "</form>";
diff --git a/locales/en.strings b/locales/en.strings
index 22cee453..b1d211d1 100644
--- a/locales/en.strings
+++ b/locales/en.strings
@@ -347,6 +347,7 @@
 "albums" = "Albums";
 "album" = "Album";
 "photos" = "photos";
+"photo" = "Photo";
 "create_album" = "Create album";
 "edit_album" = "Edit album";
 "edit_photo" = "Edit photo";
@@ -412,6 +413,20 @@
 "tip" = "Tip";
 "tip_ctrl" = "to select multiple photos at once, hold down the Ctrl key when selecting files in Windows or the CMD key in Mac OS.";
 "album_poster" = "Album poster";
+"select_photo" = "Select photos";
+"upload_new_photo" = "Upload new photo";
+
+"is_x_photos_zero" = "Just zero photos.";
+"is_x_photos_one" = "Just one photo.";
+"is_x_photos_few" = "Just $1 photos.";
+"is_x_photos_many" = "Just $1 photos.";
+"is_x_photos_other" = "Just $1 photos.";
+
+"all_photos" = "All photos";
+"error_uploading_photo" = "Error when uploading photo. Error text: ";
+"too_many_photos" = "Too many photos.";
+
+"photo_x_from_y" = "Photo $1 from $2";
 
 /* Notes */
 
@@ -697,6 +712,7 @@
 "selecting_video" = "Selecting videos";
 "upload_new_video" = "Upload new video";
 "max_attached_videos" = "Max is 10 videos";
+"max_attached_photos" = "Max is 10 photos";
 "no_videos" = "You don't have uploaded videos.";
 "no_videos_results" = "No results.";
 
diff --git a/locales/ru.strings b/locales/ru.strings
index 2dfadb8f..49871399 100644
--- a/locales/ru.strings
+++ b/locales/ru.strings
@@ -329,6 +329,7 @@
 "album" = "Альбом";
 "albums" = "Альбомы";
 "photos" = "фотографий";
+"photo" = "Фотография";
 "create_album" = "Создать альбом";
 "edit_album" = "Редактировать альбом";
 "edit_photo" = "Изменить фотографию";
@@ -394,6 +395,20 @@
 "tip" = "Подсказка";
 "tip_ctrl" = "для того, чтобы выбрать сразу несколько фотографий, удерживайте клавишу Ctrl при выборе файлов в ОС Windows или клавишу CMD в Mac OS.";
 "album_poster" = "Обложка альбома";
+"select_photo" = "Выберите фотографию";
+"upload_new_photo" = "Загрузить новую фотографию";
+
+"is_x_photos_zero" = "Всего ноль фотографий.";
+"is_x_photos_one" = "Всего одна фотография.";
+"is_x_photos_few" = "Всего $1 фотографии.";
+"is_x_photos_many" = "Всего $1 фотографий.";
+"is_x_photos_other" = "Всего $1 фотографий.";
+
+"all_photos" = "Все фотографии";
+"error_uploading_photo" = "Не удалось загрузить фотографию. Текст ошибки: ";
+"too_many_photos" = "Слишком много фотографий.";
+
+"photo_x_from_y" = "Фотография $1 из $2";
 
 /* Notes */
 
@@ -656,6 +671,7 @@
 "selecting_video" = "Выбор видеозаписей";
 "upload_new_video" = "Загрузить новое видео";
 "max_attached_videos" = "Максимум 10 видеозаписей";
+"max_attached_photos" = "Максимум 10 фотографий";
 "no_videos" = "У вас нет видео.";
 "no_videos_results" = "Нет результатов.";