Merge branch 'master' into other-gender-options

This commit is contained in:
n1rwana 2022-10-13 12:08:08 +03:00 committed by GitHub
commit 95f8f4a3cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 2193 additions and 2713 deletions

1
.gitignore vendored
View file

@ -10,5 +10,6 @@ tmp/*
themepacks/*
!themepacks/.gitkeep
!themepacks/openvk_modern
!themepacks/midnight
storage/*
!storage/.gitkeep

50
ServiceAPI/Mentions.php Normal file
View file

@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\{Users, Clubs};
class Mentions implements Handler
{
protected $user;
function __construct(?User $user)
{
$this->user = $user;
}
function resolve(int $id, callable $resolve, callable $reject): void
{
if($id > 0) {
$user = (new Users)->get($id);
if(!$user) {
$reject("Not found");
return;
}
$resolve([
"url" => $user->getURL(),
"name" => $user->getFullName(),
"ava" => $user->getAvatarURL("miniscule"),
"about" => $user->getStatus() ?? "",
"online" => ($user->isFemale() ? tr("was_online_f") : tr("was_online_m")) . " " . $user->getOnline(),
"verif" => $user->isVerified(),
]);
return;
}
$club = (new Clubs)->get(abs($id));
if(!$club) {
$reject("Not found");
return;
}
$resolve([
"url" => $club->getURL(),
"name" => $club->getName(),
"ava" => $club->getAvatarURL("miniscule"),
"about" => $club->getDescription() ?? "",
"online" => tr("participants", $club->getFollowersCount()),
"verif" => $club->isVerified(),
]);
}
}

70
ServiceAPI/Polls.php Normal file
View file

@ -0,0 +1,70 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use Chandler\MVC\Routing\Router;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Exceptions\{AlreadyVotedException, InvalidOptionException, PollLockedException};
use openvk\Web\Models\Repositories\Polls as PollRepo;
use UnexpectedValueException;
class Polls implements Handler
{
protected $user;
protected $polls;
function __construct(?User $user)
{
$this->user = $user;
$this->polls = new PollRepo;
}
private function getPollHtml(int $poll): string
{
return Router::i()->execute("/poll$poll", "SAPI");
}
function vote(int $pollId, string $options, callable $resolve, callable $reject): void
{
$poll = $this->polls->get($pollId);
if(!$poll) {
$reject("Poll not found");
return;
}
try {
$options = explode(",", $options);
$poll->vote($this->user, $options);
} catch(AlreadyVotedException $ex) {
$reject("Poll state changed: user has already voted.");
return;
} catch(PollLockedException $ex) {
$reject("Poll state changed: poll has ended.");
return;
} catch(InvalidOptionException $ex) {
$reject("Foreign options passed.");
return;
} catch(UnexpectedValueException $ex) {
$reject("Too much options passed.");
return;
}
$resolve(["html" => $this->getPollHtml($pollId)]);
}
function unvote(int $pollId, callable $resolve, callable $reject): void
{
$poll = $this->polls->get($pollId);
if(!$poll) {
$reject("Poll not found");
return;
}
try {
$poll->revokeVote($this->user);
} catch(PollLockedException $ex) {
$reject("Votes can't be revoked from this poll.");
return;
}
$resolve(["html" => $this->getPollHtml($pollId)]);
}
}

View file

@ -13,7 +13,7 @@ final class Account extends VKAPIRequestHandler
"last_name" => $this->getUser()->getLastName(),
"home_town" => $this->getUser()->getHometown(),
"status" => $this->getUser()->getStatus(),
"bdate" => $this->getUser()->getBirthday()->format('%e.%m.%Y'),
"bdate" => is_null($this->getUser()->getBirthday()) ? '01.01.1970' : $this->getUser()->getBirthday()->format('%e.%m.%Y'),
"bdate_visibility" => $this->getUser()->getBirthdayPrivacy(),
"phone" => "+420 ** *** 228", # TODO
"relation" => $this->getUser()->getMaritalStatus(),

View file

@ -133,15 +133,18 @@ final class Friends extends VKAPIRequestHandler
return $response;
}
function getRequests(string $fields = "", int $offset = 0, int $count = 100): object
function getRequests(string $fields = "", int $offset = 0, int $count = 100, int $extended = 0): object
{
if ($count >= 1000)
$this->fail(100, "One of the required parameters was not passed or is invalid.");
$this->requireUser();
$i = 0;
$offset++;
$followers = [];
foreach($this->getUser()->getFollowers() as $follower) {
foreach($this->getUser()->getFollowers($offset, $count) as $follower) {
$followers[$i] = $follower->getId();
$i++;
}
@ -149,8 +152,10 @@ final class Friends extends VKAPIRequestHandler
$response = $followers;
$usersApi = new Users($this->getUser());
if(!is_null($fields))
$response = $usersApi->get(implode(',', $followers), $fields, 0, $count); # FIXME
if($extended == 1)
$response = $usersApi->get(implode(',', $followers), $fields, 0, $count);
else
$response = $usersApi->get(implode(',', $followers), "", 0, $count);
foreach($response as $user)
$user->user_id = $user->id;

View file

@ -10,7 +10,7 @@ final class Groups extends VKAPIRequestHandler
$this->requireUser();
if($user_id == 0) {
foreach($this->getUser()->getClubs($offset+1) as $club)
foreach($this->getUser()->getClubs($offset, false, $count, true) as $club)
$clbs[] = $club;
$clbsCount = $this->getUser()->getClubCount();
} else {
@ -20,7 +20,7 @@ final class Groups extends VKAPIRequestHandler
if(is_null($user))
$this->fail(15, "Access denied");
foreach($user->getClubs($offset+1) as $club)
foreach($user->getClubs($offset, false, $count, true) as $club)
$clbs[] = $club;
$clbsCount = $user->getClubCount();
@ -33,17 +33,9 @@ final class Groups extends VKAPIRequestHandler
$ic = $count;
if(!empty($clbs)) {
$clbs = array_slice($clbs, $offset * $count);
for($i=0; $i < $ic; $i++) {
$usr = $clbs[$i];
if(is_null($usr)) {
$rClubs[$i] = (object)[
"id" => $clbs[$i],
"name" => "DELETED",
"deactivated" => "deleted"
];
} else if($clbs[$i] == NULL) {
if(is_null($usr)) {
} else {
$rClubs[$i] = (object) [
@ -102,23 +94,32 @@ final class Groups extends VKAPIRequestHandler
];
}
function getById(string $group_ids = "", string $group_id = "", string $fields = ""): ?array
function getById(string $group_ids = "", string $group_id = "", string $fields = "", int $offset = 0, int $count = 500): ?array
{
/* Both offset and count SHOULD be used only in OpenVK code,
not in your app or script, since it's not oficially documented by VK */
$clubs = new ClubsRepo;
if($group_ids == NULL && $group_id != NULL)
if(empty($group_ids) && !empty($group_id))
$group_ids = $group_id;
if($group_ids == NULL && $group_id == NULL)
if(empty($group_ids) && empty($group_id))
$this->fail(100, "One of the parameters specified was missing or invalid: group_ids is undefined");
$clbs = explode(',', $group_ids);
$response;
$response = array();
$ic = sizeof($clbs);
if(sizeof($clbs) > $count)
$ic = $count;
$clbs = array_slice($clbs, $offset * $count);
for($i=0; $i < $ic; $i++) {
if($i > 500)
if($i > 500 || $clbs[$i] == 0)
break;
if($clbs[$i] < 0)
@ -142,6 +143,7 @@ final class Groups extends VKAPIRequestHandler
"screen_name" => $clb->getShortCode() ?? "club".$clb->getId(),
"is_closed" => false,
"type" => "group",
"is_member" => !is_null($this->getUser()) ? (int) $clb->getSubscriptionStatus($this->getUser()) : 0,
"can_access_closed" => true,
];
@ -204,10 +206,6 @@ final class Groups extends VKAPIRequestHandler
else
$response[$i]->can_post = $clb->canPost();
break;
case "is_member":
if(!is_null($this->getUser()))
$response[$i]->is_member = (int) $clb->getSubscriptionStatus($this->getUser());
break;
}
}
}
@ -215,4 +213,24 @@ final class Groups extends VKAPIRequestHandler
return $response;
}
function search(string $q, int $offset = 0, int $count = 100)
{
$clubs = new ClubsRepo;
$array = [];
$find = $clubs->find($q);
foreach ($find as $group)
$array[] = $group->getId();
return (object) [
"count" => $find->size(),
"items" => $this->getById(implode(',', $array), "", "is_admin,is_member,is_advertiser,photo_50,photo_100,photo_200", $offset, $count)
/*
* As there is no thing as "fields" by the original documentation
* i'll just bake this param by the example shown here: https://dev.vk.com/method/groups.search
*/
];
}
}

View file

@ -21,10 +21,17 @@ final class Wall extends VKAPIRequestHandler
$groups = [];
$cnt = $posts->getPostCountOnUserWall($owner_id);
$wallOnwer = (new UsersRepo)->get($owner_id);
if ($owner_id > 0)
$wallOnwer = (new UsersRepo)->get($owner_id);
else
$wallOnwer = (new ClubsRepo)->get($owner_id * -1);
if(!$wallOnwer || $wallOnwer->isDeleted() || $wallOnwer->isDeleted())
$this->fail(18, "User was deleted or banned");
if ($owner_id > 0)
if(!$wallOnwer || $wallOnwer->isDeleted())
$this->fail(18, "User was deleted or banned");
else
if(!$wallOnwer)
$this->fail(15, "Access denied: wall is disabled"); // Don't search for logic here pls
foreach($posts->getPostsFromUsersWall($owner_id, 1, $count, $offset) as $post) {
$from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId();
@ -37,12 +44,14 @@ final class Wall extends VKAPIRequestHandler
continue;
$attachments[] = $this->getApiPhoto($attachment);
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = [];
foreach($attachment->getChildren() as $repostAttachment) {
if($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) {
if($attachment->isDeleted())
if($repostAttachment->isDeleted())
continue;
$repostAttachments[] = $this->getApiPhoto($repostAttachment);
@ -178,6 +187,8 @@ final class Wall extends VKAPIRequestHandler
foreach($post->getChildren() as $attachment) {
if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
$attachments[] = $this->getApiPhoto($attachment);
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $user);
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = [];
@ -561,7 +572,7 @@ final class Wall extends VKAPIRequestHandler
return 1;
}
private function getApiPhoto($attachment) {
return [
"type" => "photo",
@ -576,4 +587,44 @@ final class Wall extends VKAPIRequestHandler
]
];
}
private function getApiPoll($attachment, $user) {
$answers = array();
foreach($attachment->getResults()->options as $answer) {
$answers[] = (object)[
"id" => $answer->id,
"rate" => $answer->pct,
"text" => $answer->name,
"votes" => $answer->votes
];
}
$userVote = array();
foreach($attachment->getUserVote($user) as $vote)
$userVote[] = $vote[0];
return [
"type" => "poll",
"poll" => [
"multiple" => $attachment->isMultipleChoice(),
"end_date" => $attachment->endsAt() == NULL ? 0 : $attachment->endsAt()->timestamp(),
"closed" => $attachment->hasEnded(),
"is_board" => false,
"can_edit" => false,
"can_vote" => $attachment->canVote($user),
"can_report" => false,
"can_share" => true,
"created" => 0,
"id" => $attachment->getId(),
"owner_id" => $attachment->getOwner()->getId(),
"question" => $attachment->getTitle(),
"votes" => $attachment->getVoterCount(),
"disable_unvote" => $attachment->isRevotable(),
"anonymous" => $attachment->isAnonymous(),
"answer_ids" => $userVote,
"answers" => $answers,
"author_id" => $attachment->getOwner()->getId(),
]
];
}
}

View file

@ -27,14 +27,23 @@ class NewMessageEvent implements ILPEmitable
if($peer === $userId)
$peer = $msg->getRecipient()->getId();
/*
* Source:
* https://github.com/danyadev/longpoll-doc
*/
return [
4, # event type
$msg->getId(), # messageId
256, # checked for spam flag
$peer, # TODO calculate peer correctly
$msg->getSendTime()->timestamp(), # creation time in unix
$msg->getText(), # text (formatted)
[], # empty additional info
[], # empty attachments
$msg->getId() << 2, # id as random_id
$peer, # conversation id
0 # not edited yet
];
}
}

View file

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{User, Club};
use openvk\Web\Models\Repositories\{Users, Clubs};
class Alias extends RowModel
{
protected $tableName = "aliases";
function getOwnerId(): int
{
return $this->getRecord()->owner_id;
}
function getType(): string
{
if ($this->getOwnerId() < 0)
return "club";
return "user";
}
function getUser(): ?User
{
return (new Users)->get($this->getOwnerId());
}
function getClub(): ?Club
{
return (new Clubs)->get($this->getOwnerId() * -1);
}
}

View file

@ -0,0 +1,295 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Util\DateTime;
use \UnexpectedValueException;
use Nette\InvalidStateException;
use openvk\Web\Models\Repositories\Users;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Exceptions\PollLockedException;
use openvk\Web\Models\Exceptions\AlreadyVotedException;
use openvk\Web\Models\Exceptions\InvalidOptionException;
class Poll extends Attachable
{
protected $tableName = "polls";
private $choicesToPersist = [];
function getTitle(): string
{
return $this->getRecord()->title;
}
function getMetaDescription(): string
{
$props = [];
$props[] = tr($this->isAnonymous() ? "poll_anon" : "poll_public");
if($this->isMultipleChoice()) $props[] = tr("poll_multi");
if(!$this->isRevotable()) $props[] = tr("poll_lock");
if(!is_null($this->endsAt())) $props[] = tr("poll_until", $this->endsAt());
return implode("", $props);
}
function getOwner(): User
{
return (new Users)->get($this->getRecord()->owner);
}
function getOptions(): array
{
$options = $this->getRecord()->related("poll_options.poll");
$res = [];
foreach($options as $opt)
$res[$opt->id] = $opt->name;
return $res;
}
function getUserVote(User $user): ?array
{
$ctx = DatabaseConnection::i()->getContext();
$votedOpts = $ctx->table("poll_votes")
->where(["user" => $user->getId(), "poll" => $this->getId()]);
if($votedOpts->count() == 0)
return NULL;
$res = [];
foreach($votedOpts as $votedOpt) {
$option = $ctx->table("poll_options")->get($votedOpt->option);
$res[] = [$option->id, $option->name];
}
return $res;
}
function getVoters(int $optionId, int $page = 1, ?int $perPage = NULL): array
{
$res = [];
$ctx = DatabaseConnection::i()->getContext();
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
$voters = $ctx->table("poll_votes")->where(["poll" => $this->getId(), "option" => $optionId]);
foreach($voters->page($page, $perPage) as $vote)
$res[] = (new Users)->get($vote->user);
return $res;
}
function getVoterCount(?int $optionId = NULL): int
{
$votes = DatabaseConnection::i()->getContext()->table("poll_votes");
if(!$optionId)
return $votes->select("COUNT(DISTINCT user) AS c")->where("poll", $this->getId())->fetch()->c;
return $votes->where(["poll" => $this->getId(), "option" => $optionId])->count();
}
function getResults(?User $user = NULL): object
{
$ctx = DatabaseConnection::i()->getContext();
$voted = NULL;
if(!is_null($user))
$voted = $this->getUserVote($user);
$result = (object) [];
$result->totalVotes = $this->getVoterCount();
$unsOptions = [];
foreach($this->getOptions() as $id => $title) {
$option = (object) [];
$option->id = $id;
$option->name = $title;
$option->votes = $this->getVoterCount($id);
$option->pct = $result->totalVotes == 0 ? 0 : min(100, floor(($option->votes / $result->totalVotes) * 100));
$option->voters = $this->getVoters($id, 1, 10);
if(!$user || !$voted)
$option->voted = NULL;
else
$option->voted = in_array([$id, $title], $voted);
$unsOptions[$id] = $option;
}
$optionsC = sizeof($unsOptions);
$sOptions = $unsOptions;
usort($sOptions, function($a, $b) { return $a->votes <=> $b->votes; });
for($i = 0; $i < $optionsC; $i++)
$unsOptions[$id]->rate = $optionsC - $i - 1;
$result->options = array_values($unsOptions);
return $result;
}
function isAnonymous(): bool
{
return (bool) $this->getRecord()->is_anonymous;
}
function isMultipleChoice(): bool
{
return (bool) $this->getRecord()->allows_multiple;
}
function isRevotable(): bool
{
return (bool) $this->getRecord()->can_revote;
}
function endsAt(): ?DateTime
{
if(!$this->getRecord()->until)
return NULL;
return new DateTime($this->getRecord()->until);
}
function hasEnded(): bool
{
if($this->getRecord()->ended)
return true;
if(!is_null($this->getRecord()->until))
return time() >= $this->getRecord()->until;
return false;
}
function hasVoted(User $user): bool
{
return !is_null($this->getUserVote($user));
}
function canVote(User $user): bool
{
return !$this->hasEnded() && !$this->hasVoted($user);
}
function vote(User $user, array $optionIds): void
{
if($this->hasEnded())
throw new PollLockedException;
if($this->hasVoted($user))
throw new AlreadyVotedException;
$optionIds = array_map(function($x) { return (int) $x; }, array_unique($optionIds));
$validOpts = array_keys($this->getOptions());
if(empty($optionIds) || (sizeof($optionIds) > 1 && !$this->isMultipleChoice()))
throw new UnexpectedValueException;
if(sizeof(array_diff($optionIds, $validOpts)) > 0)
throw new InvalidOptionException;
foreach($optionIds as $opt) {
DatabaseConnection::i()->getContext()->table("poll_votes")->insert([
"user" => $user->getId(),
"poll" => $this->getId(),
"option" => $opt,
]);
}
}
function revokeVote(User $user): void
{
if(!$this->isRevotable())
throw new PollLockedException;
$this->getRecord()->related("poll_votes.poll")
->where("user", $user->getId())->delete();
}
function setOwner(User $owner): void
{
$this->stateChanges("owner", $owner->getId());
}
function setEndDate(int $timestamp): void
{
if(!is_null($this->getRecord()))
throw new PollLockedException;
$this->stateChanges("until", $timestamp);
}
function setEnded(): void
{
$this->stateChanges("ended", 1);
}
function setOptions(array $options): void
{
if(!is_null($this->getRecord()))
throw new PollLockedException;
if(sizeof($options) > ovkGetQuirk("polls.max-opts"))
throw new TooMuchOptionsException;
$this->choicesToPersist = $options;
}
function setRevotability(bool $canReVote): void
{
if(!is_null($this->getRecord()))
throw new PollLockedException;
$this->stateChanges("can_revote", $canReVote);
}
function setAnonymity(bool $anonymous): void
{
$this->stateChanges("is_anonymous", $anonymous);
}
function setMultipleChoice(bool $mc): void
{
$this->stateChanges("allows_multiple", $mc);
}
function importXML(User $owner, string $xml): void
{
$xml = simplexml_load_string($xml);
$this->setOwner($owner);
$this->setTitle($xml["title"] ?? "Untitled");
$this->setMultipleChoice(($xml["multiple"] ?? "no") == "yes");
$this->setAnonymity(($xml["anonymous"] ?? "no") == "yes");
$this->setRevotability(($xml["locked"] ?? "no") == "no");
if(ctype_digit((string) ($xml["duration"] ?? "")))
$this->setEndDate(time() + ((86400 * (int) $xml["duration"])));
$options = [];
foreach($xml->options->option as $opt)
$options[] = (string) $opt;
if(empty($options))
throw new UnexpectedValueException;
$this->setOptions($options);
}
static function import(User $owner, string $xml): Poll
{
$poll = new Poll;
$poll->importXML($owner, $xml);
$poll->save();
return $poll;
}
function save(): void
{
if(empty($this->choicesToPersist))
throw new InvalidStateException;
parent::save();
foreach($this->choicesToPersist as $option) {
DatabaseConnection::i()->getContext()->table("poll_options")->insert([
"poll" => $this->getId(),
"name" => $option,
]);
}
}
}

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Repositories\{Clubs, Users};
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\Notifications\LikeNotification;
@ -55,6 +55,15 @@ class Post extends Postable
{
return $this->getRecord()->wall;
}
function getWallOwner()
{
$w = $this->getRecord()->wall;
if($w < 0)
return (new Clubs)->get(abs($w));
return (new Users)->get($w);
}
function getRepostCount(): int
{

View file

@ -535,12 +535,15 @@ class User extends RowModel
return sizeof(DatabaseConnection::i()->getContext()->table("messages")->where(["recipient_id" => $this->getId(), "unread" => 1]));
}
function getClubs(int $page = 1, bool $admin = false): \Traversable
function getClubs(int $page = 1, bool $admin = false, int $count = OPENVK_DEFAULT_PER_PAGE, bool $offset = false): \Traversable
{
if(!$offset)
$page = ($page - 1) * $count;
if($admin) {
$id = $this->getId();
$query = "SELECT `id` FROM `groups` WHERE `owner` = ? UNION SELECT `club` as `id` FROM `group_coadmins` WHERE `user` = ?";
$query .= " LIMIT " . OPENVK_DEFAULT_PER_PAGE . " OFFSET " . ($page - 1) * OPENVK_DEFAULT_PER_PAGE;
$query .= " LIMIT " . $count . " OFFSET " . $page;
$sel = DatabaseConnection::i()->getConnection()->query($query, $id, $id);
foreach($sel as $target) {
@ -550,7 +553,7 @@ class User extends RowModel
yield $target;
}
} else {
$sel = $this->getRecord()->related("subscriptions.follower")->page($page, OPENVK_DEFAULT_PER_PAGE);
$sel = $this->getRecord()->related("subscriptions.follower")->limit($count, $page);
foreach($sel->where("model", "openvk\\Web\\Models\\Entities\\Club") as $target) {
$target = (new Clubs)->get($target->target);
if(!$target) continue;
@ -926,6 +929,10 @@ class User extends RowModel
$pClub = DatabaseConnection::i()->getContext()->table("groups")->where("shortcode", $code)->fetch();
if(!is_null($pClub))
return false;
$pAlias = DatabaseConnection::i()->getContext()->table("aliases")->where("shortcode", $code)->fetch();
if(!is_null($pAlias))
return false;
}
$this->stateChanges("shortcode", $code);

View file

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
final class AlreadyVotedException extends \RuntimeException
{
}

View file

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
final class InvalidOptionException extends \UnexpectedValueException
{
}

View file

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
use Nette\InvalidStateException;
final class PollLockedException extends InvalidStateException
{
}

View file

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
final class TooMuchOptionsException extends \UnexpectedValueException
{
}

View file

@ -0,0 +1,35 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Alias;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Entities\{Club, User};
use openvk\Web\Models\Repositories\{Clubs, Users};
class Aliases
{
private $context;
private $aliases;
function __construct()
{
$this->context = DB::i()->getContext();
$this->aliases = $this->context->table("aliases");
}
private function toAlias(?ActiveRow $ar): ?Alias
{
return is_null($ar) ? NULL : new Alias($ar);
}
function get(int $id): ?Alias
{
return $this->toAlias($this->aliases->get($id));
}
function getByShortcode(string $shortcode): ?Alias
{
return $this->toAlias($this->aliases->where("shortcode", $shortcode)->fetch());
}
}

View file

@ -1,6 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Repositories\Aliases;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection;
@ -22,7 +23,17 @@ class Clubs
function getByShortURL(string $url): ?Club
{
return $this->toClub($this->clubs->where("shortcode", $url)->fetch());
$shortcode = $this->toClub($this->clubs->where("shortcode", $url)->fetch());
if ($shortcode)
return $shortcode;
$alias = (new Aliases)->getByShortcode($url);
if (!$alias) return NULL;
if ($alias->getType() !== "club") return NULL;
return $alias->getClub();
}
function get(int $id): ?Club
@ -45,6 +56,9 @@ class Clubs
function getPopularClubs(): \Traversable
{
// TODO rewrite
/*
$query = "SELECT ROW_NUMBER() OVER (ORDER BY `subscriptions` DESC) as `place`, `target` as `id`, COUNT(`follower`) as `subscriptions` FROM `subscriptions` WHERE `model` = \"openvk\\\Web\\\Models\\\Entities\\\Club\" GROUP BY `target` ORDER BY `subscriptions` DESC, `id` LIMIT 30;";
$entries = DatabaseConnection::i()->getConnection()->query($query);
@ -54,6 +68,7 @@ class Clubs
"club" => $this->get($entry["id"]),
"subscriptions" => $entry["subscriptions"],
];
*/
}
use \Nette\SmartObject;

View file

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\Poll;
class Polls
{
private $polls;
function __construct()
{
$this->polls = DatabaseConnection::i()->getContext()->table("polls");
}
function get(int $id): ?Poll
{
$poll = $this->polls->get($id);
if(!$poll)
return NULL;
return new Poll($poll);
}
}

View file

@ -1,6 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Aliases;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection;
use Chandler\Security\User as ChandlerUser;
@ -9,11 +10,13 @@ class Users
{
private $context;
private $users;
private $aliases;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->users = $this->context->table("profiles");
$this->aliases = $this->context->table("aliases");
}
private function toUser(?ActiveRow $ar): ?User
@ -28,7 +31,17 @@ class Users
function getByShortURL(string $url): ?User
{
return $this->toUser($this->users->where("shortcode", $url)->fetch());
$shortcode = $this->toUser($this->users->where("shortcode", $url)->fetch());
if ($shortcode)
return $shortcode;
$alias = (new Aliases)->getByShortcode($url);
if (!$alias) return NULL;
if ($alias->getType() !== "user") return NULL;
return $alias->getUser();
}
function getByChandlerUser(ChandlerUser $user): ?User

View file

@ -64,7 +64,7 @@ final class AboutPresenter extends OpenVKPresenter
$this->template->usersStats = (new Users)->getStatistics();
$this->template->clubsCount = (new Clubs)->getCount();
$this->template->postsCount = (new Posts)->getCount();
$this->template->popularClubs = iterator_to_array((new Clubs)->getPopularClubs());
$this->template->popularClubs = [];
$this->template->admins = iterator_to_array((new Users)->getInstanceAdmins());
}

View file

@ -6,7 +6,7 @@ use openvk\Web\Models\Repositories\Applications;
final class AppsPresenter extends OpenVKPresenter
{
private $apps;
protected $presenterName = "apps";
function __construct(Applications $apps)
{
$this->apps = $apps;

View file

@ -6,6 +6,7 @@ use openvk\Web\Models\Repositories\{Comments, Clubs};
final class CommentPresenter extends OpenVKPresenter
{
protected $presenterName = "comment";
private $models = [
"posts" => "openvk\\Web\\Models\\Repositories\\Posts",
"photos" => "openvk\\Web\\Models\\Repositories\\Photos",

View file

@ -7,6 +7,7 @@ final class GiftsPresenter extends OpenVKPresenter
{
private $gifts;
private $users;
protected $presenterName = "gifts";
function __construct(Gifts $gifts, Users $users)
{

View file

@ -8,7 +8,8 @@ use Chandler\Security\Authenticator;
final class GroupPresenter extends OpenVKPresenter
{
private $clubs;
protected $presenterName = "group";
function __construct(Clubs $clubs)
{
$this->clubs = $clubs;

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace openvk\Web\Presenters;
final class MaintenancePresenter extends OpenVKPresenter
{
protected $presenterName = "maintenance";
function renderSection(string $name): void
{
if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"][$name])
$this->flashFail("err", tr("error"), tr("forbidden"));
$this->template->name = [
"photos" => tr("my_photos"),
"videos" => tr("my_videos"),
"messenger" => tr("my_messages"),
"user" => tr("users"),
"group" => tr("my_groups"),
"comment" => tr("comments"),
"gifts" => tr("gifts"),
"apps" => tr("apps"),
"notes" => tr("my_notes"),
"notification" => tr("my_feedback"),
"support" => tr("menu_support"),
"topics" => tr("topics")
][$name] ?? $name;
}
function renderAll(): void
{
}
}

View file

@ -9,11 +9,13 @@ final class MessengerPresenter extends OpenVKPresenter
{
private $messages;
private $signaler;
protected $presenterName = "messenger";
function __construct(Messages $messages)
{
$this->messages = $messages;
$this->signaler = SignalManager::i();
parent::__construct();
}
@ -30,7 +32,7 @@ final class MessengerPresenter extends OpenVKPresenter
function renderIndex(): void
{
$this->assertUserLoggedIn();
if(isset($_GET["sel"]))
$this->pass("openvk!Messenger->app", $_GET["sel"]);
@ -93,6 +95,13 @@ final class MessengerPresenter extends OpenVKPresenter
}
$legacy = $this->queryParam("version") < 3;
$time = intval($this->queryParam("wait"));
if($time > 60)
$time = 60;
elseif($time == 0)
$time = 25; // default
$this->signaler->listen(function($event, $eId) use ($id) {
exit(json_encode([
@ -101,7 +110,7 @@ final class MessengerPresenter extends OpenVKPresenter
$event->getVKAPISummary($id),
],
]));
}, $id);
}, $id, $time);
}
function renderApiGetMessages(int $sel, int $lastMsg): void

View file

@ -6,7 +6,8 @@ use openvk\Web\Models\Entities\Note;
final class NotesPresenter extends OpenVKPresenter
{
private $notes;
protected $presenterName = "notes";
function __construct(Notes $notes)
{
$this->notes = $notes;

View file

@ -3,6 +3,8 @@ namespace openvk\Web\Presenters;
final class NotificationPresenter extends OpenVKPresenter
{
protected $presenterName = "notification";
function renderFeed(): void
{
$this->assertUserLoggedIn();

View file

@ -17,7 +17,8 @@ abstract class OpenVKPresenter extends SimplePresenter
protected $deactivationTolerant = false;
protected $errorTemplate = "@error";
protected $user = NULL;
protected $presenterName;
private function calculateQueryString(array $data): string
{
$rawUrl = "tcp+stratum://fakeurl.net$_SERVER[REQUEST_URI]"; #HTTP_HOST can be tainted
@ -196,12 +197,13 @@ abstract class OpenVKPresenter extends SimplePresenter
function onStartup(): void
{
$user = Authenticator::i()->getUser();
$this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false;
$this->template->isTimezoned = Session::i()->get("_timezoneOffset");
$userValidated = 0;
$cacheTime = OPENVK_ROOT_CONF["openvk"]["preferences"]["nginxCacheTime"] ?? 0;
if(!is_null($user)) {
$this->user = (object) [];
$this->user->raw = $user;
@ -226,7 +228,7 @@ abstract class OpenVKPresenter extends SimplePresenter
}
exit;
}
if($this->user->identity->isBanned() && !$this->banTolerant) {
header("HTTP/1.1 403 Forbidden");
$this->getTemplatingEngine()->render(__DIR__ . "/templates/@banned.xml", [
@ -247,23 +249,33 @@ abstract class OpenVKPresenter extends SimplePresenter
]);
exit;
}
$userValidated = 1;
$cacheTime = 0; # Force no cache
if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) {
$this->user->identity->setOnline(time());
$this->user->identity->save();
}
$this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByUserId($this->user->id, 1);
if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0))
$this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0);
}
header("X-OpenVK-User-Validated: $userValidated");
header("X-Accel-Expires: $cacheTime");
setlocale(LC_TIME, ...(explode(";", tr("__locale"))));
if (!OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"]["all"]) {
if (OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"][$this->presenterName]) {
$this->pass("openvk!Maintenance->section", $this->presenterName);
}
} else {
if ($this->presenterName != "maintenance") {
$this->redirect("/maintenances/");
}
}
parent::onStartup();
}
@ -272,10 +284,14 @@ abstract class OpenVKPresenter extends SimplePresenter
parent::onBeforeRender();
$whichbrowser = new WhichBrowser\Parser(getallheaders());
$featurephonetheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultFeaturePhoneTheme"];
$mobiletheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultMobileTheme"];
if($mobiletheme && $whichbrowser->isType('mobile') && Session::i()->get("_tempTheme") == NULL)
if($featurephonetheme && $this->isOldThing($whichbrowser) && Session::i()->get("_tempTheme") == NULL) {
$this->setSessionTheme($featurephonetheme);
} elseif($mobiletheme && $whichbrowser->isType('mobile') && Session::i()->get("_tempTheme") == NULL)
$this->setSessionTheme($mobiletheme);
$theme = NULL;
if(Session::i()->get("_tempTheme")) {
$theme = Themepacks::i()[Session::i()->get("_tempTheme", "ovk")];
@ -306,4 +322,33 @@ abstract class OpenVKPresenter extends SimplePresenter
header("Content-Length: $size");
exit($payload);
}
protected function isOldThing($whichbrowser) {
if($whichbrowser->isOs('Series60') ||
$whichbrowser->isOs('Series40') ||
$whichbrowser->isOs('Series80') ||
$whichbrowser->isOs('Windows CE') ||
$whichbrowser->isOs('Windows Mobile') ||
$whichbrowser->isOs('Nokia Asha Platform') ||
$whichbrowser->isOs('UIQ') ||
$whichbrowser->isEngine('NetFront') || // PSP and other japanese portable systems
$whichbrowser->isOs('Android') ||
$whichbrowser->isOs('iOS') ||
$whichbrowser->isBrowser('Internet Explorer', '<=', '8')) {
// yeah, it's old, but ios and android are?
if($whichbrowser->isOs('iOS') && $whichbrowser->isOs('iOS', '<=', '9'))
return true;
elseif($whichbrowser->isOs('iOS') && $whichbrowser->isOs('iOS', '>', '9'))
return false;
if($whichbrowser->isOs('Android') && $whichbrowser->isOs('Android', '<=', '5'))
return true;
elseif($whichbrowser->isOs('Android') && $whichbrowser->isOs('Android', '>', '5'))
return false;
return true;
} else {
return false;
}
}
}

View file

@ -9,7 +9,8 @@ final class PhotosPresenter extends OpenVKPresenter
private $users;
private $photos;
private $albums;
protected $presenterName = "photos";
function __construct(Photos $photos, Albums $albums, Users $users)
{
$this->users = $users;

View file

@ -0,0 +1,75 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\Poll;
use openvk\Web\Models\Repositories\Polls;
final class PollPresenter extends OpenVKPresenter
{
private $polls;
function __construct(Polls $polls)
{
$this->polls = $polls;
parent::__construct();
}
function renderView(int $id): void
{
$poll = $this->polls->get($id);
if(!$poll)
$this->notFound();
$this->template->id = $poll->getId();
$this->template->title = $poll->getTitle();
$this->template->isAnon = $poll->isAnonymous();
$this->template->multiple = $poll->isMultipleChoice();
$this->template->unlocked = $poll->isRevotable();
$this->template->until = $poll->endsAt();
$this->template->votes = $poll->getVoterCount();
$this->template->meta = $poll->getMetaDescription();
$this->template->ended = $ended = $poll->hasEnded();
if((is_null($this->user) || $poll->canVote($this->user->identity)) && !$ended) {
$this->template->options = $poll->getOptions();
$this->template->_template = "Poll/Poll.xml";
return;
}
if(is_null($this->user)) {
$this->template->voted = false;
$this->template->results = $poll->getResults();
} else {
$this->template->voted = $poll->hasVoted($this->user->identity);
$this->template->results = $poll->getResults($this->user->identity);
}
$this->template->_template = "Poll/PollResults.xml";
}
function renderVoters(int $pollId): void
{
$poll = $this->polls->get($pollId);
if(!$poll)
$this->notFound();
if($poll->isAnonymous())
$this->flashFail("err", tr("forbidden"), tr("poll_err_anonymous"));
$options = $poll->getOptions();
$option = (int) base_convert($this->queryParam("option"), 32, 10);
if(!in_array($option, array_keys($options)))
$this->notFound();
$page = (int) ($this->queryParam("p") ?? 1);
$voters = $poll->getVoters($option, $page);
$this->template->pollId = $pollId;
$this->template->options = $options;
$this->template->option = [$option, $options[$option]];
$this->template->tabs = $options;
$this->template->iterator = $voters;
$this->template->count = $poll->getVoterCount($option);
$this->template->page = $page;
}
}

View file

@ -11,6 +11,7 @@ final class SupportPresenter extends OpenVKPresenter
{
protected $banTolerant = true;
protected $deactivationTolerant = true;
protected $presenterName = "support";
private $tickets;
private $comments;
@ -155,11 +156,12 @@ final class SupportPresenter extends OpenVKPresenter
$this->notFound();
} else {
if($ticket->getUserId() !== $this->user->id && $this->hasPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0))
$this->redirect("/support/tickets");
$_redirect = "/support/tickets";
else
$this->redirect("/support");
$_redirect = "/support?act=list";
$ticket->delete();
$this->redirect($_redirect);
}
}
}

View file

@ -7,7 +7,8 @@ final class TopicsPresenter extends OpenVKPresenter
{
private $topics;
private $clubs;
protected $presenterName = "topics";
function __construct(Topics $topics, Clubs $clubs)
{
$this->topics = $topics;

View file

@ -15,12 +15,13 @@ use Nette\Database\UniqueConstraintViolationException;
final class UserPresenter extends OpenVKPresenter
{
private $users;
public $deactivationTolerant = false;
protected $presenterName = "user";
function __construct(Users $users)
{
$this->users = $users;
parent::__construct();
}

View file

@ -8,7 +8,8 @@ final class VideosPresenter extends OpenVKPresenter
{
private $videos;
private $users;
protected $presenterName = "videos";
function __construct(Videos $videos, Users $users)
{
$this->videos = $videos;

View file

@ -1,6 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Post, Photo, Video, Club, User};
use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User};
use openvk\Web\Models\Entities\Notifications\{RepostNotification, WallPostNotification};
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums};
use Chandler\Database\DatabaseConnection;
@ -44,9 +45,6 @@ final class WallPresenter extends OpenVKPresenter
function renderWall(int $user, bool $embedded = false): void
{
if(false)
exit(tr("forbidden") . ": " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) {
$canPost = false;
@ -65,7 +63,10 @@ final class WallPresenter extends OpenVKPresenter
}
if ($embedded == true) $this->template->_template = "components/wall.xml";
$this->template->oObj = $owner;
$this->template->oObj = $owner;
if($user < 0)
$this->template->club = $owner;
$this->template->owner = $user;
$this->template->canPost = $canPost;
$this->template->count = $this->posts->getPostCountOnUserWall($user);
@ -88,9 +89,6 @@ final class WallPresenter extends OpenVKPresenter
function renderRSS(int $user): void
{
if(false)
exit(tr("forbidden") . ": " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) {
$canPost = false;
@ -259,16 +257,26 @@ 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) {
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK)
$video = Video::fastMake($this->user->id, $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"));
}
if(empty($this->postParam("text")) && !$photo && !$video)
try {
$poll = NULL;
$xml = $this->postParam("poll");
if (!is_null($xml) && $xml != "none")
$poll = Poll::import($this->user->identity, $xml);
} catch(TooMuchOptionsException $e) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("poll_err_to_much_options"));
} catch(\UnexpectedValueException $e) {
$this->flashFail("err", tr("failed_to_publish_post"), "Poll format invalid");
}
if(empty($this->postParam("text")) && !$photo && !$video && !$poll)
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
try {
@ -291,6 +299,9 @@ final class WallPresenter extends OpenVKPresenter
if(!is_null($video))
$post->attach($video);
if(!is_null($poll))
$post->attach($poll);
if($wall > 0 && $wall !== $this->user->identity->getId())
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();

View file

@ -17,62 +17,19 @@
{script "js/l10n.js"}
{script "js/openvk.cls.js"}
{css "js/node_modules/tippy.js/dist/backdrop.css"}
{css "js/node_modules/tippy.js/dist/border.css"}
{css "js/node_modules/tippy.js/dist/svg-arrow.css"}
{css "js/node_modules/tippy.js/themes/light.css"}
{script "js/node_modules/@popperjs/core/dist/umd/popper.min.js"}
{script "js/node_modules/tippy.js/dist/tippy-bundle.umd.min.js"}
{script "js/node_modules/handlebars/dist/handlebars.min.js"}
{if $isTimezoned == NULL}
{script "js/timezone.js"}
{/if}
{ifset $thisUser}
{if $thisUser->getNsfwTolerance() < 2}
{css "css/nsfw-posts.css"}
{/if}
{if $theme !== NULL}
{if $theme->inheritDefault()}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/if}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/stylesheet/styles.css" />
{if $isXmas}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/resource/xmas.css" />
{/if}
{else}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/if}
{if $thisUser->getStyleAvatar() == 1}
{css "css/avatar.1.css"}
{/if}
{if $thisUser->getStyleAvatar() == 2}
{css "css/avatar.2.css"}
{/if}
{if $thisUser->hasMicroblogEnabled() == 1}
{css "css/microblog.css"}
{/if}
{else}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/nsfw-posts.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/ifset}
{include "_includeCSS.xml"}
{ifset headIncludes}
{include headIncludes}
@ -325,6 +282,8 @@
{script "js/scroll.js"}
{script "js/al_wall.js"}
{script "js/al_api.js"}
{script "js/al_mentions.js"}
{script "js/al_polls.js"}
{ifset $thisUser}
{script "js/al_notifs.js"}

View file

@ -0,0 +1,20 @@
{extends "../@layout.xml"}
{block title}
{_global_maintenance}
{/block}
{block header}
{_global_maintenance}
{/block}
{block content}
<div class="container_gray">
<center style="background: white;border: #DEDEDE solid 1px;">
<img src="/assets/packages/static/openvk/img/oof.apng" style="width: 20%;" />
<span style="color: #707070;margin: 10px 0;display: block;">
{_undergoing_global_maintenance}
</span>
</center>
</div>
{/block}

View file

@ -0,0 +1,20 @@
{extends "../@layout.xml"}
{block title}
{_section_maintenance}
{/block}
{block header}
{_section_maintenance}
{/block}
{block content}
<div class="container_gray">
<center style="background: white;border: #DEDEDE solid 1px;">
<img src="/assets/packages/static/openvk/img/oof.apng" style="width: 20%;" />
<span style="color: #707070;margin: 10px 0;display: block;">
{tr("undergoing_section_maintenance", $name)|noescape}
</span>
</center>
</div>
{/block}

View file

@ -0,0 +1,44 @@
{if !isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'}
<link rel="shortcut icon" href="/assets/packages/static/openvk/img/icon.ico" />
<meta n:ifset="$csrfToken" name="csrf" value="{$csrfToken}" />
<script src="/language/{getLanguage()}.js" crossorigin="anonymous"></script>
{script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/umbrellajs/umbrella.min.js"}
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
{script "js/messagebox.js"}
{script "js/l10n.js"}
{script "js/al_api.js"}
{script "js/al_polls.js"}
{include "../_includeCSS.xml"}
<style>body { margin: 8px; }</style>
{/if}
<div class="poll">
<h4>{$title}</h4>
<form onsubmit="pollFormSubmit(event, this)" data-multi="{$multiple ? '1' : '0'}" data-pid="{$id}">
<div class="poll-options">
<div n:foreach="$options as $oid => $option" class="poll-option">
<label>
{if $multiple}
<input n:attr="disabled => is_null($thisUser)" type="checkbox" name="option{$oid}" onclick="pollCheckBoxPressed(this)" />
{else}
<input n:attr="disabled => is_null($thisUser)" type="radio" value="{$oid}" name="vote" onclick="pollRadioPressed(this)" />
{/if}
{$option}
</label>
</div>
</div>
{if $multiple}
<br/>
<input type="submit" class="button" value="{_cast_vote}" disabled="disabled" />
{/if}
</form>
<div class="poll-meta">
{tr("poll_voter_count", $votes)|noescape}<br/>
<span class="nobold">{$meta}</span>
</div>
</div>

View file

@ -0,0 +1,56 @@
{if !isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'}
<link rel="shortcut icon" href="/assets/packages/static/openvk/img/icon.ico" />
<meta n:ifset="$csrfToken" name="csrf" value="{$csrfToken}" />
<script src="/language/{getLanguage()}.js" crossorigin="anonymous"></script>
{script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/umbrellajs/umbrella.min.js"}
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
{script "js/messagebox.js"}
{script "js/l10n.js"}
{script "js/al_api.js"}
{script "js/al_polls.js"}
{include "../_includeCSS.xml"}
<style>body { margin: 8px; } .poll { border: 1px solid #e3e3e3; }</style>
{/if}
<div class="poll" data-id="{$id}">
<a n:if="$unlocked && $voted" href="javascript:pollRetractVote({$id})" class="poll-retract-vote">{_retract_vote}</a>
<h4>{$title}</h4>
<div class="poll-results">
<div n:foreach="$results->options as $option" class="poll-result">
{if $isAnon}
<a href="javascript:false">
{if $option->voted}
<b>{$option->name}</b>
{else}
{$option->name}
{/if}
</a>
{else}
<a href="/poll{$id}/voters?option={base_convert($option->id, 10, 32)}">
{if $option->voted}
<b>{$option->name}</b>
{else}
{$option->name}
{/if}
</a>
{/if}
<div class="poll-result-barspace">
<div class="poll-result-bar">
<span class="poll-result-count">{$option->votes}</span>
<div class="poll-result-bar-sub" style="width: {$option->pct}%">&nbsp;</div>
</div>
<div class="poll-result-pct">
<strong>{$option->pct}%</strong>
</div>
</div>
</div>
</div>
<div class="poll-meta">
{tr("poll_voter_count", $votes)|noescape}<br/>
<span class="nobold">{$meta}</span>
</div>
</div>

View file

@ -0,0 +1,40 @@
{extends "../@listView.xml"}
{block title}
{_poll_voters_list}
{/block}
{block header}
{_poll_voters_list} »
{$option[1]}
{/block}
{block tabs}
<div n:foreach="$options as $optionId => $optionName" class="tab" id="{$optionId == $option[0] ? 'activetabs' : 'ki'}">
<a id="{$optionId == $option[0] ? 'act_tab_a' : ''}" href="/poll{$pollId}/voters?option={base_convert($optionId, 10, 32)}">{$optionName}</a>
</div>
{/block}
{* BEGIN ELEMENTS DESCRIPTION *}
{block link|strip|stripHtml}
{$x->getURL()}
{/block}
{block preview}
<img src="{$x->getAvatarUrl('miniscule')}" width="75" alt="Фотография пользователя" />
{/block}
{block name}
{$x->getCanonicalName()}
<img n:if="$x->isVerified()"
class="name-checkmark"
src="/assets/packages/static/openvk/img/checkmark.png"
/>
{/block}
{block description}
{/block}
{block actions}
{/block}

View file

@ -53,7 +53,7 @@
<div n:attr="id => ($act === 'online' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($act === 'online' ? 'act_tab_a' : 'ki')" href="?act=online">{_online}</a>
</div>
<div n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'activetabs' : 'ki')" class="tab">
<div n:if="!is_null($thisUser) && $user->getId() === $thisUser->getId()" n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'act_tab_a' : 'ki')" href="?act=incoming">{_req}</a>
</div>
{/block}

View file

@ -79,9 +79,9 @@
{/block}
{block actions}
<a href="{$x->getURL()}" class="profile_link">{_check_community}</a>
{if $x->canBeModifiedBy($thisUser ?? NULL)}
{var $clubPinned = $thisUser->isClubPinned($x)}
<a href="{$x->getURL()}" class="profile_link">{_check_community}</a>
<a href="/groups_pin?club={$x->getId()}&hash={rawurlencode($csrfToken)}" class="profile_link" n:if="$clubPinned || $thisUser->getPinnedClubCount() <= 10" id="_pinGroup" data-group-name="{$x->getName()}" data-group-url="{$x->getUrl()}">
{if $clubPinned}
{_remove_from_left_menu}
@ -89,21 +89,21 @@
{_add_to_left_menu}
{/if}
</a>
{if $x->getSubscriptionStatus($thisUser) == false}
<form class="profile_link_form" action="/setSub/club" method="post">
<input type="hidden" name="act" value="add" />
<input type="hidden" name="id" value="{$x->getId()}" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" class="profile_link" value="{_join_community}" />
</form>
{else}
<form class="profile_link_form" action="/setSub/club" method="post">
<input type="hidden" name="act" value="rem" />
<input type="hidden" name="id" value="{$x->getId()}" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" class="profile_link" value="{_leave_community}" />
</form>
{/if}
{/if}
{if $x->getSubscriptionStatus($thisUser) == false}
<form class="profile_link_form" action="/setSub/club" method="post">
<input type="hidden" name="act" value="add" />
<input type="hidden" name="id" value="{$x->getId()}" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" class="profile_link" value="{_join_community}" />
</form>
{else}
<form class="profile_link_form" action="/setSub/club" method="post">
<input type="hidden" name="act" value="rem" />
<input type="hidden" name="id" value="{$x->getId()}" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" class="profile_link" value="{_leave_community}" />
</form>
{/if}
{/block}

View file

@ -16,7 +16,7 @@
</div>
<div n:class="postFeedWrapper, $thisUser->hasMicroblogEnabled() ? postFeedWrapperMicroblog">
{include "../components/textArea.xml", route => "/wall" . $thisUser->getId() . "/makePost"}
{include "../components/textArea.xml", route => "/wall" . $thisUser->getId() . "/makePost", graffiti => true, polls => true}
</div>
{foreach $posts as $post}

View file

@ -0,0 +1,52 @@
{ifset $thisUser}
{if $thisUser->getNsfwTolerance() < 2}
{css "css/nsfw-posts.css"}
{/if}
{if $theme !== NULL}
{if $theme->inheritDefault()}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/if}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/stylesheet/styles.css" />
{if $isXmas}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/resource/xmas.css" />
{/if}
{else}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/if}
{if $thisUser->getStyleAvatar() == 1}
{css "css/avatar.1.css"}
{/if}
{if $thisUser->getStyleAvatar() == 2}
{css "css/avatar.2.css"}
{/if}
{if $thisUser->hasMicroblogEnabled() == 1}
{css "css/microblog.css"}
{/if}
{else}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/nsfw-posts.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/ifset}

View file

@ -11,6 +11,8 @@
{/if}
{elseif $attachment instanceof \openvk\Web\Models\Entities\Video}
<video class="media" src="{$attachment->getURL()}" controls="controls"></video>
{elseif $attachment instanceof \openvk\Web\Models\Entities\Poll}
{presenter "openvk!Poll->view", $attachment->getId()}
{elseif $attachment instanceof \openvk\Web\Models\Entities\Post}
{php $GLOBALS["_nesAttGloCou"] = (isset($GLOBALS["_nesAttGloCou"]) ? $GLOBALS["_nesAttGloCou"] : 0) + 1}
{if $GLOBALS["_nesAttGloCou"] > 2}

View file

@ -24,17 +24,15 @@
<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"))}
{if ($onWallOf ?? false) &&!$post->isPostedOnBehalfOfGroup() && $post->getOwnerPost() !== $post->getTargetWall()}
{var $wallId = $post->getTargetWall()}
{var $wallURL = $wallId > -1 ? "/id$wallId" : "/club" . abs($wallId)}
на
<a href="{$wallURL}">
{var $wallOwner = $post->getWallOwner()}
<a href="{$wallOwner->getURL()}" class="mention" data-mention-ref="{$post->getTargetWall()}">
<b>
{if isset($thisUser) && $thisUser->getId() === $wallId}
вашей
{/if}
стене
{if $wallId < 0}
группы
{if isset($thisUser) && $thisUser->getId() === $post->getTargetWall()}
{_post_on_your_wall}
{elseif $wallOwner instanceof \openvk\Web\Models\Entities\Club}
{tr("post_on_group_wall", ovk_proc_strtr($wallOwner->getName(), 52))}
{else}
{tr("post_on_user_wall", $wallOwner->getMorphedName("genitive", false))}
{/if}
</b>
</a>
@ -77,7 +75,7 @@
{var $actualAuthor = $post->getOwner(false)}
<span>
{_author}:
<a href="{$actualAuthor->getURL()}">
<a href="{$actualAuthor->getURL()}" class="mention" data-mention-ref="{$actualAuthor->getId()}">
{$actualAuthor->getCanonicalName()}
</a>
</span>

View file

@ -20,17 +20,15 @@
<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->isPostedOnBehalfOfGroup() ? tr("post_writes_g") : ($author->isFemale() ? tr("post_writes_f") : tr("post_writes_m")))}
{if ($onWallOf ?? false) &&!$post->isPostedOnBehalfOfGroup() && $post->getOwnerPost() !== $post->getTargetWall()}
{var $wallId = $post->getTargetWall()}
{var $wallURL = $wallId > -1 ? "/id$wallId" : "/club" . abs($wallId)}
на
<a href="{$wallURL}">
{var $wallOwner = $post->getWallOwner()}
<a href="{$wallOwner->getURL()}" class="mention" data-mention-ref="{$post->getTargetWall()}">
<b>
{if isset($thisUser) && $thisUser->getId() === $wallId}
вашей
{/if}
стене
{if $wallId < 0}
группы
{if isset($thisUser) && $thisUser->getId() === $post->getTargetWall()}
{_post_on_your_wall}
{elseif $wallOwner instanceof \openvk\Web\Models\Entities\Club}
{tr("post_on_group_wall", ovk_proc_strtr($wallOwner->getName(), 52))}
{else}
{tr("post_on_user_wall", $wallOwner->getMorphedName("genitive", false))}
{/if}
</b>
</a>
@ -58,7 +56,7 @@
{var $actualAuthor = $post->getOwner(false)}
<span>
{_author}:
<a href="{$actualAuthor->getURL()}">
<a href="{$actualAuthor->getURL()}" class="mention" data-mention-ref="{$actualAuthor->getId()}">
{$actualAuthor->getCanonicalName()}
</a>
</span>

View file

@ -11,9 +11,11 @@
<div class="post-upload">
{_attachment}: <span>(unknown)</span>
</div>
<div class="post-has-poll">
{_poll}
</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}
{if $club->canBeModifiedBy($thisUser)}
<script>
@ -43,6 +45,7 @@
<input type="checkbox" name="nsfw" /> {_contains_nsfw}
</label>
</div>
<div n:if="!($postOpts ?? true) && !is_null($thisUser) && !is_null($club ?? NULL) && $club->canBeModifiedBy($thisUser)" class="post-opts">
<label>
<input type="checkbox" name="as_group" /> {_comment_as_group}
@ -50,6 +53,7 @@
</div>
<input type="file" class="postFileSel" id="postFilePic" name="_pic_attachment" accept="image/*" style="display:none;" />
<input type="file" class="postFileSel" id="postFileVid" name="_vid_attachment" accept="video/*" style="display:none;" />
<input type="hidden" name="poll" value="none" />
<input type="hidden" name="type" value="1" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<br/>
@ -75,6 +79,10 @@
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/draw-brush.png" />
{_graffiti}
</a>
<a n:if="$polls ?? false" href="javascript:initPoll({$textAreaId})">
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/office-chart-bar-stacked.png" />
{_poll}
</a>
</div>
</div>
</div>

View file

@ -8,7 +8,7 @@
</div>
<div>
<div n:if="$canPost" class="content_subtitle">
{include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true}
{include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true, polls => true}
</div>
<div class="content">

View file

@ -21,7 +21,8 @@ class DateTime
$then = date_create("@" . $this->timestamp);
$now = date_create();
$diff = date_diff($now, $then);
if($diff->invert === 0) return __OPENVK_ERROR_CLOCK_IN_FUTURE;
if($diff->invert === 0)
return ovk_strftime_safe("%e %B %Y ", $this->timestamp) . tr("time_at_sp") . ovk_strftime_safe(" %R %p", $this->timestamp);
if($this->timestamp >= strtotime("midnight")) { # Today
if($diff->h >= 1)
@ -52,13 +53,10 @@ class DateTime
switch($type) {
case static::RELATIVE_FORMAT_NORMAL:
return mb_convert_case($this->zmdate(), MB_CASE_TITLE_SIMPLE);
break;
case static::RELATIVE_FORMAT_LOWER:
return $this->zmdate();
break;
case static::RELATIVE_FORMAT_SHORT:
return "";
break;
}
}

View file

@ -23,9 +23,11 @@ services:
- openvk\Web\Presenters\AppsPresenter
- openvk\Web\Presenters\ThemepacksPresenter
- openvk\Web\Presenters\VKAPIPresenter
- openvk\Web\Presenters\PollPresenter
- openvk\Web\Presenters\BannedLinkPresenter
- openvk\Web\Models\Repositories\Users
- openvk\Web\Models\Repositories\Posts
- openvk\Web\Models\Repositories\Polls
- openvk\Web\Models\Repositories\Photos
- openvk\Web\Models\Repositories\Albums
- openvk\Web\Models\Repositories\Clubs
@ -43,4 +45,6 @@ services:
- openvk\Web\Models\Repositories\Topics
- openvk\Web\Models\Repositories\Applications
- openvk\Web\Models\Repositories\ContentSearchRepository
- openvk\Web\Models\Repositories\Aliases
- openvk\Web\Models\Repositories\BannedLinks
- openvk\Web\Presenters\MaintenancePresenter

View file

@ -273,6 +273,10 @@ routes:
handler: "Apps->edit"
- url: "/apps/uninstall"
handler: "Apps->unInstall"
- url: "/poll{num}"
handler: "Poll->view"
- url: "/poll{num}/voters"
handler: "Poll->voters"
- url: "/admin"
handler: "Admin->index"
- url: "/admin/users"
@ -331,3 +335,7 @@ routes:
handler: "UnknownTextRouteStrategy->delegate"
placeholders:
shortCode: "[a-z][a-z0-9\\@\\.\\_]{0,30}[a-z0-9]"
- url: "/maintenance/{text}"
handler: "Maintenance->section"
- url: "/maintenances/"
handler: "Maintenance->all"

View file

@ -448,6 +448,15 @@ table {
color: #e8e8e8;
}
.button[disabled] {
filter: opacity(0.5);
cursor: not-allowed;
}
.button[disabled]:hover {
color: #fff;
}
.button-loading {
display: inline-block;
background-image: url('/assets/packages/static/openvk/img/loading_mini.gif');
@ -801,6 +810,8 @@ table.User {
padding: 0 10px;
margin-left: -10px;
width: 607px;
overflow-x: auto;
white-space: nowrap;
}
.tabs.stupid-fix {
@ -1338,14 +1349,14 @@ body.scrolled .toTop:hover {
font-weight: bold;
}
.post-upload {
.post-upload, .post-has-poll {
margin-top: 11px;
margin-left: 3px;
color: #3c3c3c;
display: none;
}
.post-upload::before {
.post-upload::before, .post-has-poll::before {
content: " ";
width: 8px;
height: 8px;
@ -2051,6 +2062,88 @@ table td[width="120"] {
max-height: 250px;
}
.poll {
padding: 8px;
transition: .1s filter ease-in;
border: 1px solid #e3e3e3;
}
.poll.loading {
filter: opacity(0.5);
cursor: progress;
user-select: none;
}
.poll.loading * {
pointer-events: none;
}
.poll-embed {
float: right;
}
.poll h4 {
padding-bottom: 4px;
margin-bottom: 8px;
}
.poll-meta .nobold {
font-style: oblique;
}
.poll-result-barspace {
display: flex;
}
.poll-result {
margin-bottom: 5px;
}
.poll-result a {
color: unset;
}
.poll-result-bar {
position: relative;
margin: 5px 0;
background-color: #f7f7f7;
height: 13pt;
flex: 14;
}
span.poll-result-count {
position: absolute;
top: 50%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
color: #7d96af;
}
.poll-result-bar-sub {
height: 100%;
background-color: #d9e1ea;
}
.poll-result-pct {
flex: 2;
display: flex;
justify-content: flex-end;
align-items: center;
padding-left: 5px;
}
a.poll-retract-vote {
float: right;
}
.tippy-box[data-theme~="vk"] {
user-select: none;
background-color: #fff;
border: 1px solid #DCE1E6;
border-radius: 1px;
}
@keyframes appearing {
from {
opacity: 0;

View file

@ -0,0 +1,42 @@
var tooltipTemplate = Handlebars.compile(`
<table>
<tr>
<td width="54" valign="top">
<img src="{{ava}}" width="54" />
</td>
<td width="1"></td>
<td width="150" valign="top">
<span>
<a href="{{url}}"><b>{{name}}</b></a>
{{#if verif}}
<img class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png" />
{{/if}}
</span><br/>
<span style="color: #444;">{{online}}</span><br/>
<span style="color: #000;">{{about}}</span>
</td>
</tr>
</table>
`);
tippy(".mention", {
theme: "light vk",
content: "⌛",
allowHTML: true,
interactive: true,
interactiveDebounce: 500,
onCreate: async function(that) {
that._resolvedMention = null;
},
onShow: async function(that) {
if(!that._resolvedMention) {
let id = Number(that.reference.dataset.mentionRef);
that._resolvedMention = await API.Mentions.resolve(id);
}
let res = that._resolvedMention;
that.setContent(tooltipTemplate(res));
}
});

154
Web/static/js/al_polls.js Normal file
View file

@ -0,0 +1,154 @@
function escapeXML(text) {
return $("<span/>").text(text).html();
}
async function pollRetractVote(id) {
let poll = $(`.poll[data-id=${id}]`);
poll.addClass("loading");
try {
let html = (await API.Polls.unvote(poll.data("id"))).html;
poll.prop("outerHTML", html);
} catch(e) {
MessageBox(tr("error"), "Sorry: " + e.message, ["OK"], [Function.noop]);
} finally {
poll.removeClass("loading");
}
}
async function pollFormSubmit(e, form) {
e.preventDefault();
form = $(form);
let options;
let isMultiple = form.data("multi");
let pollId = form.data("pid");
let formData = form.serializeArray();
if(!isMultiple) {
options = [Number(formData[0].value)];
} else {
options = [];
formData.forEach(function(record) {
if(!record.name.startsWith("option") || record.value !== "on")
return;
options.push(Number(record.name.substr(6)));
});
}
let poll = form.parent();
poll.addClass("loading");
try {
let html = (await API.Polls.vote(pollId, options.join(","))).html;
poll.prop("outerHTML", html);
} catch(e) {
MessageBox(tr("error"), "Sorry: " + e.message, ["OK"], [Function.noop]);
} finally {
poll.removeClass("loading");
}
}
function pollCheckBoxPressed(cb) {
cb = $(cb);
let form = cb.parent().parent().parent().parent();
let checked = $("input:checked", form);
if(checked.length >= 1)
$("input[type=submit]", form).removeAttr("disabled");
else
$("input[type=submit]", form).attr("disabled", "disabled");
}
function pollRadioPressed(radio) {
let form = $(radio).parent().parent().parent().parent();
form.submit();
}
function initPoll(id) {
let form = $(`#wall-post-input${id}`).parent();
let mBody = `
<div id="poll_editor${id}">
<input type="text" name="title" placeholder="${tr("poll_title")}" />
<div class="poll-options" style="margin-top: 10px;"></div>
<input type="text" name="newOption" placeholder="${tr("poll_add_option")}" style="margin: 5px 0;" />
<hr/>
<label><input type="checkbox" name="anon" /> ${tr("poll_anonymous")}</label><br/>
<label><input type="checkbox" name="multi" /> ${tr("poll_multiple")}</label><br/>
<label><input type="checkbox" name="locked" /> ${tr("poll_locked")}</label><br/>
<label>
<input type="checkbox" name="expires" />
${tr("poll_edit_expires")}
<select name="expires_in" style="width: unset;">
${[...Array(32).keys()].reduce((p, c) => (!p ? '' : p) + ("<option value='" + c + "'>" + c + " " + tr("poll_edit_expires_days") + "</option>\n"))}
</select>
</label>
<div class="nobold" style="margin: 10px 5px 0">${tr("poll_editor_tips")}</div>
</div>
`;
MessageBox(tr("create_poll"), mBody, [tr("attach"), tr("cancel")], [
function() {
let dialog = $(this.$dialog().nodes[0]);
$("input", dialog).unbind();
let title = $("input[name=title]", dialog).val();
let anon = $("input[name=anon]", dialog).prop("checked") ? "yes" : "no";
let multi = $("input[name=multi]", dialog).prop("checked") ? "yes" : "no";
let lock = $("input[name=locked]", dialog).prop("checked") ? "yes" : "no";
let expires = "infinite";
if($("input[name=expires]", dialog).prop("checked"))
expires = $("select[name=expires_in]", dialog).val();
let options = "";
$(".poll-option", dialog).each(function() {
if($(this).val().length === 0)
return;
options += `<option>${escapeXML($(this).val())}</option>`;
});
let xml = `
<Poll title="${title}" anonymous="${anon}" multiple="${multi}" locked="${lock}" duration="${expires}">
<options>${options}</options>
</Poll>
`;
$("input[name=poll]", form).val(xml);
$(".post-has-poll", form).show();
},
function() {
$("input", $(this.$dialog().nodes[0])).unbind();
}
]);
let editor = $(`#poll_editor${id}`);
$("input[name=newOption]", editor).bind("focus", function() {
let newOption = $('<input type="text" class="poll-option" style="margin: 5px 0;" />');
newOption.appendTo($(".poll-options", editor));
newOption.focus();
newOption.bind("keydown", function(e) {
if(e.key === "Enter" && $(this).next().length === 0) {
$("input[name=newOption]", editor).focus();
return;
}
if($(this).val().length > 0)
return;
if(e.key !== "Backspace")
return;
if($(this).siblings().length === 0)
return;
if($(this).prev().length === 0)
$(this).next().focus();
else
$(this).prev().focus();
e.preventDefault();
$(this).unbind("keydown");
$(this).remove();
});
});
}

View file

@ -2,6 +2,7 @@
"dependencies": {
"@atlassian/aui": "^8.5.1",
"create-react-class": "^15.7.0",
"handlebars": "^4.7.7",
"jquery": "^2.1.0",
"knockout": "^3.5.1",
"ky": "^0.19.0",
@ -15,6 +16,7 @@
"requirejs": "^2.3.6",
"soundjs": "^1.0.1",
"textfit": "^2.4.0",
"tippy.js": "^6.3.7",
"umbrellajs": "^3.1.0"
}
}

View file

@ -29,6 +29,11 @@
resolved "https://registry.yarnpkg.com/@atlassian/tipsy/-/tipsy-1.3.2.tgz#ab759d461670d712425b2dac7573b79575a10502"
integrity sha512-H7qWMs66bztELt2QpOCLYDU9ZM3VZfE0knbRHHLBukH7v9dMkIS5ZwqcGREjWnVt0KNETaBeXxj0FD88TEOGVw==
"@popperjs/core@^2.9.0":
version "2.11.6"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
asap@~2.0.3:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
@ -89,6 +94,18 @@ fbjs@^0.8.0:
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
handlebars@^4.7.7:
version "4.7.7"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
dependencies:
minimist "^1.2.5"
neo-async "^2.6.0"
source-map "^0.6.1"
wordwrap "^1.0.0"
optionalDependencies:
uglify-js "^3.1.4"
iconv-lite@^0.6.2:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
@ -170,6 +187,11 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
minimist@^1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
monaco-editor@^0.20.0:
version "0.20.0"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea"
@ -185,6 +207,11 @@ msgpack-lite@^0.1.26:
int64-buffer "^0.1.9"
isarray "^1.0.0"
neo-async@^2.6.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@ -271,11 +298,23 @@ soundjs@^1.0.1:
resolved "https://registry.yarnpkg.com/soundjs/-/soundjs-1.0.1.tgz#99970542d28d0df2a1ebd061ae75c961a98c8180"
integrity sha512-MgFPvmKYfpcNiE3X5XybNvScie3DMQlZgmNzUn4puBcpw64f4LqjH/fhM8Sb/eTJ8hK57Crr7mWy0bfJOqPj6Q==
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
textfit@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/textfit/-/textfit-2.4.0.tgz#80cba8006bfb9c3d9d552739257957bdda95c79c"
integrity sha512-/x4aoY5+/tJmu+iwpBH1yw75TFp86M6X15SvaaY/Eep7YySQYtqdOifEtfvVyMwzl7SZ+G4RQw00FD9g5R6i1Q==
tippy.js@^6.3.7:
version "6.3.7"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==
dependencies:
"@popperjs/core" "^2.9.0"
trim-extra-html-whitespace@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/trim-extra-html-whitespace/-/trim-extra-html-whitespace-1.3.0.tgz#b47efb0d1a5f2a56a85cc45cea525651e93404cf"
@ -286,6 +325,11 @@ ua-parser-js@^0.7.18:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
uglify-js@^3.1.4:
version "3.17.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.3.tgz#f0feedf019c4510f164099e8d7e72ff2d7304377"
integrity sha512-JmMFDME3iufZnBpyKL+uS78LRiC+mK55zWfM5f/pWBJfpOttXAqYfdDGRukYhJuyRinvPVAtUhvy7rlDybNtFg==
umbrellajs@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/umbrellajs/-/umbrellajs-3.1.0.tgz#a4e6f0f6381f9d93110b5eee962e0e0864b10bd0"
@ -300,3 +344,8 @@ whatwg-fetch@>=0.10.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
wordwrap@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==

2504
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,36 +2,36 @@ OpenVK-KB-Heading: Правила
Адміністрація сайту дозволяє Вам користуватися сайтом на умовах, вказаних у цих правилах.
Робити можна усе те, що не відноситься до заборонених дій, а до заборонених дій відносяться:
Робити можна усе те, що не належать до заборонених дій, а до заборонених дій відносяться:
1. Відмова у підпорядкуванні правилам чи їх ігнорування,
2. Відмова від своєчасного виконання запитів агентів тех. підтримки чи адміністраціі,
2. Відмова від своєчасного виконання запитів агентів технічної підтримки чи адміністрації,
3. Використання чужих сторінок без дозволу власника,
4. Видавання себе за інших людей для отримання вигоди. Винятки:
1. Людина дозволила використання своєї особи,
2. Людина є оригінальним персонажем, та належить Вам
5. Створення массових розсилок будь-яким способом,
6. Створення ситуацій, будь-яким чином заважаючих роботі OpenVK,
5. Створення масових розсилок будь-яким способом,
6. Створення ситуацій, що заважає роботі OpenVK,
7. Публікація та зберігання на ресурсі вмісту, який:
1. Є незаконним на територіі Франціі,
1. Є незаконним на території Франції,
2. Містить порнографічні сцени за участю осіб молодше 18 років,
3. Містить рекламу заборонених у Франціі препаратів чи інструкціі по їх виготовленню,
4. Містить інформацію заборонену на територіі Франціі,
3. Містить рекламу заборонених у Франції препаратів чи інструкції по їх виготовленню,
4. Містить інформацію заборонену на території Франції,
5. Містить сцени нелюдського поводження з людьми або тваринами,
6. Порушує авторські та суміжні права,
7. Порушує права людини,
8. Заважає користувачам виконувати правила чи нормально користуватися послугами, наданими проектом OpenVK.
8. Заважає користувачам виконувати правила чи нормально користуватися послугами, наданими проєктом OpenVK.
Адміністрация є вищим органом влади, який має повне право приймати рішення на рахунок спірних ситуацій, не описаних у правилах.
Адміністрація є вищим органом влади, який має повне право приймати рішення на рахунок спірних ситуацій, не описаних у правилах.
Адміністрація може видати покарання, якщо користувачі:
1. Публікують заборонений контент на сторінках інших користувачів,
2. Обдурюють адміністрацію чи агентів тех. підтримки,
2. Обдурюють адміністрацію чи агентів технічної підтримки,
3. Вводять в оману користувачів сайту,
4. Публічно необґрунтовано критикують OpenVK, чи адміністрацію з ціллю принизити чи образити учасників проекту, чи його керівників,
5. Забанені у офіційному чаті OpenVK у Telegram,
6. Не поважають адміністрацію проекту чи агентів тех. підтримки.
4. Публічно необґрунтовано критикують OpenVK чи адміністрацію з ціллю принизити, образити учасників проєкт, його керівників,
5. Заблоковані в офіційному чаті OpenVK у Telegram,
6. Не поважають адміністрацію проєкт чи агентів технічної підтримки.
При порушенні правил, адміністрація може:

View file

@ -1,14 +1,13 @@
CREATE TABLE `links_banned` (
`id` bigint UNSIGNED NOT NULL,
`domain` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`regexp_rule` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
`domain` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL,
`regexp_rule` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL,
`reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci,
`initiator` bigint UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
ALTER TABLE `links_banned`
ADD PRIMARY KEY (`id`);
ALTER TABLE `links_banned`
MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT;
COMMIT;

View file

@ -0,0 +1,11 @@
CREATE TABLE `aliases` (
`id` bigint UNSIGNED NOT NULL,
`owner_id` bigint NOT NULL,
`shortcode` varchar(36) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
ALTER TABLE `aliases`
ADD PRIMARY KEY (`id`);
ALTER TABLE `aliases`
MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT;

View file

@ -0,0 +1,32 @@
CREATE TABLE IF NOT EXISTS `polls` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`owner` bigint unsigned NOT NULL,
`title` text NOT NULL,
`allows_multiple` bit(1) NOT NULL DEFAULT b'0',
`is_anonymous` bit(1) NOT NULL DEFAULT b'0',
`can_revote` bit(1) NOT NULL DEFAULT b'0',
`until` bigint unsigned DEFAULT NULL,
`ended` bit(1) NOT NULL DEFAULT b'0',
`deleted` bit(1) NOT NULL DEFAULT b'0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `poll_options` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`poll` bigint unsigned NOT NULL,
`name` varchar(512) NOT NULL,
PRIMARY KEY (`id`),
KEY `poll` (`poll`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `poll_votes` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user` bigint unsigned NOT NULL,
`poll` bigint unsigned NOT NULL,
`option` bigint unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_option` (`user`,`option`),
KEY `option` (`option`),
KEY `poll` (`poll`),
KEY `user_poll` (`user`,`poll`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;

View file

@ -17,6 +17,8 @@
"password" = "Գաղտնաբառ";
"registration" = "Գրանցում";
"forgot_password" = "Մոռացե՞լ եք գաղտնաբառը";
"checkbox_in_registration" = "Ես համաձայն եմ <a href='/privacy'>կոնֆիդենցիալության քաղաքականությանն</a> ու <a href='/about'>կայքի կանոնադրությանը</a>։";
"checkbox_in_registration_unchecked" = "Դուք պետք է համաձայնվեք պայմանների հետ նախքան գրանցվելը։";
"login_failed" = "Չհաջողվեց մուտք գործել";
"invalid_username_or_password" = "Սխալ օգտատիրոջ անուն կամ գաղտնաբառ։ Դուք կարող եք <a href='/restore'>վերականգնել ձեր գաղտնաբառը</a>։";
@ -93,6 +95,9 @@
"years_many" = "$1 տարեկան";
"years_other" = "$1 տարեկան";
"show_my_birthday" = "Ցույց տալ ծննդյան օրը";
"show_only_month_and_day" = "Ցուցադրել միայն ամիսն ու օրը";
"relationship" = "Ընտանեկան դրություն";
"relationship_0" = "Ընտրված չէ";
@ -144,8 +149,8 @@
"updated_at" = "Թարմացված է $1";
"user_banned" = "Ցավո՛ք, մենք ստիպված <b>$1</b>-ի էջը կասեցրել ենք։";
"user_banned_comment" = "Մոդերատորի մեկնաբանությունը․ ";
"user_banned" = "Ցավո՛ք, մենք ստիպված կասեցրել ենք <b>$1</b>-ի էջը։";
"user_banned_comment" = "Մոդերատորի մեկնաբանությունը․";
/* Wall */
@ -154,6 +159,10 @@
"post_writes_m" = "գրել է";
"post_writes_f" = "գրել է";
"post_writes_g" = "հրապարակել են";
"post_deact_m" = "ջնջել է էջը հետևյալ բառերով.";
"post_deact_f" = "ջնջել է էջը հետևյալ բառերով.";
"post_deact_silent_m" = "սուս ու փուս ջնջել է էջը։";
"post_deact_silent_f" = "սուս ու փուս ջնջել է էջը։";
"wall" = "Պատ";
"post" = "Գրություն";
"write" = "Գրել";
@ -167,7 +176,7 @@
"comments_tip" = "Եղե՛ք առաջինը ով կթողնի իր կարծիքը։";
"your_comment" = "Ձեր մեկնաբանությունը";
"shown" = "Ցուցադրված է";
"x_out_of" = "$1 –ը ՝";
"x_out_of" = "$1ը";
"wall_zero" = "գրություն չկա";
"wall_one" = "մեկ գրություն";
"wall_few" = "$1 գրություն";
@ -215,6 +224,8 @@
"incoming_req" = "Բաժանորդներ";
"outcoming_req" = "Հայցեր";
"req" = "Հայցեր";
"friends_online" = "Ընկերները ցանցում";
"all_friends" = "Բոլոր ընկերները";
"req_zero" = "Ոչ մի հայտ չի գտնվել...";
"req_one" = "Գտնվեց մեկ հայտ";
@ -246,6 +257,12 @@
"subscriptions_many" = "$1 բաժանորդագրություն";
"subscriptions_other" = "$1 բաժանորդագրություն";
"friends_list_online_zero" = "Դուք դեռ չունեք ցանցի մեջ գտնվող ընկերներ";
"friends_list_online_one" = "Ձեր $1 ընկերը ցանցի մեջ է";
"friends_list_online_few" = "Ձեր $1 ընկերները ցանցի մեջ են";
"friends_list_online_many" = "Ձեր $1 ընկերները ցանցի մեջ են";
"friends_list_online_other" = "Ձեր $1 ընկերները ցանցի մեջ են";
/* Group */
"name_group" = "Անվանում";
@ -364,6 +381,10 @@
"edit_note" = "Խմբագրել նշումը";
"actions" = "Գործողություններ";
"notes_start_screen" = "Նշումների շնորհիվ Դուք կարող եք կիսվել ընկերների հետ տարբեր իրադարձություններով, և իմանալ թե ինչ է կատարվում իրենց մոտ։";
"note_preview" = "Նախադիտում";
"note_preview_warn" = "Սա ընդամենը նախադիտում է";
"note_preview_warn_details" = "Պահպանելուց հետո նշումները կարող են այլ տեսք ունենալ։ Ու մեկ էլ, այդքան հաճախ նախադիտում մի արեք։";
"note_preview_empty_err" = "Ինչու՞ նախադիտել նշումը առանց վերնագրի կամ բովանդակության։";
"edited" = "Խմբագրված է";
@ -428,6 +449,7 @@
"avatar" = "Ավատար";
"privacy" = "Գաղտնիություն";
"interface" = "Արտաքին տեսք";
"security" = "Անվտանգություն";
"profile_picture" = "Էջի նկար";
@ -445,7 +467,7 @@
"arbitrary_avatars" = "Կամայական";
"cut" = "Կտրվածք";
"round_avatars" = "Կլոր ավատար";
"round_avatars" = "Շրջանաձև";
"apply_style_for_this_device" = "Հաստատել տեսքը միայն այս սարքի համար";
@ -508,6 +530,33 @@
"email_change_confirm_message" = "Որպեսզի Ձեր փոփոխությունը կատարվի, հաստատե՛ք Ձեր նոր էլեկտրոնային հասցեն։ Այնտեղ մենք ուղարկել ենք հրահանգները։";
"profile_deactivate" = "Էջի հեռացում";
"profile_deactivate_button" = "Ջնջել էջը";
"profile_deactivate_header" = "Մենք ցավում ենք որ Դուք ցանկանում եք ջնջել ձեր էջը։ Դրա համար Դուք կարող եք նշել հեռացման պատճառը և Ձեր կարծիքը այդ առումով։ Մեզ համար կարևոր է Ձեր կարծիքը, որպեսզի դարձնենք կայքը ավելի լավը։";
"profile_deactivate_reason_header" = "Խնդրում ենք, նշել Ձեր էջի ջնջման պատճառները";
"profile_deactivate_reason_1" = "Ես ունեմ այլ էջ այս կայնքում";
"profile_deactivate_reason_1_text" = "Ես նոր էջ եմ սկսել ու ցանկանում եմ սկսել ամեն ինչ նորից";
"profile_deactivate_reason_2" = "Կայքն ինձնից շատ ժամանակ է խլում";
"profile_deactivate_reason_2_text" = "Թեկուզ այս կայքը լավն է, բայց ցավոք ինձնից ահագին ժամանակ է խլում։";
"profile_deactivate_reason_3" = "Կայքը բազմաթիվ անցանկանալի մատերիալներ է պարունակում";
"profile_deactivate_reason_3_text" = "Ես բազմաթիվ պոռնոգրաֆիա ու անօրինական կինոներ եմ գրել բոլ ա։ Հիմա հավես չկա։";
"profile_deactivate_reason_4" = "Ինձ անհանգստացնում է իմ տվյալների անվտանգությունը";
"profile_deactivate_reason_4_text" = "Ինձ հետևում են ու շատ վախենալու է ինձ այստեղ գտնվելը";
"profile_deactivate_reason_5" = "Իմ էջը չեն մեկնաբանում";
"profile_deactivate_reason_5_text" = "Ինձ այստեղ շան տեղ դնող չկա ու ես տխրում եմ։ Դուք կզղջաք որ ես հեռացա...";
"profile_deactivate_reason_6" = "Այլ պատճառ";
"profile_deactivated_msg" = "Ձեր էջը <b>ջնջված է</b>։<br/><br/>Եթե Դուք ուզենաք նորից օգտվել Ձեր էջով, կարող եք <a href='/settings/reactivate'>ապաակտիվացնել այն</a> մինչև $1:";
"profile_deactivated_status" = "Էջը ջնջված է";
"profile_deactivated_info" = "Օգտատիրոջ էջը հեռացվել է։<br/>Մանրամասն տեղեկատվությունը բացակայում է։";
"share_with_friends" = "Պատմել ընկերներին";
"end_all_sessions" = "Դուրս գալ բոլոր սեսսիաներից";
"end_all_sessions_description" = "Եթե ցանկանում եք դուրս գալ $1ից ամեն դեվայսից, սեղմե՛ք ներքևի կոճակը";
"end_all_sessions_done" = "Բոլոր սեսսիաները նետված են, ներառյալ բջջային հավելվածները";
/* Two-factor authentication */
"two_factor_authentication" = "Երկքայլ աուտենտիֆիկացիա";
@ -708,6 +757,75 @@
"users_gifts" = "Նվերներ";
/* Apps */
"app" = "Հավելված";
"apps" = "Հավելվածներ";
"my_apps" = "Իմ հավելվածները";
"all_apps" = "Բոլոր հավելվածները";
"installed_apps" = "Տեղադրված հավելվածները";
"own_apps" = "Կառավարում";
"own_apps_alternate" = "Իմ այլ հավելվածները";
"app_play" = "միացնել";
"app_uninstall" = "անջատել";
"app_edit" = "խմբագրել";
"app_dev" = "Մշակող";
"create_app" = "Ստեղծել հավելված";
"edit_app" = "Խմբագրել հավելվածը";
"new_app" = "Նոր հավելված";
"app_news" = "Նորություններով նշում";
"app_state" = "Կարգավիճակ";
"app_enabled" = "Միացված է";
"app_creation_hint_url" = "URLում նշեք կոնկրետ հասցեն իր սխեմայով (https), պորտով (80) և անհրաժեշտ միացման կարգավորումներով։";
"app_creation_hint_iframe" = "Ձեր հավելվածը բացված է iframeով։";
"app_balance" = "Հավելվածի հաշվին կա <b>$1</b> ձայն։";
"app_users" = "Ձեր հավելվածով օգտվում է <b>$1</b> հոգի։";
"app_withdrawal_q" = "դուրս բերե՞լ";
"app_withdrawal" = "Միջոցների դուրս բերում";
"app_withdrawal_empty" = "Կներեք, դատարկությունը չհաջողվեց դուրս բերել։";
"app_withdrawal_created" = "$1 ձայնի դուրս բերման հայտը գրանցված է։ Սպասեք հաշվառմանը։";
"appjs_payment" = "Գնման վճարում";
"appjs_payment_intro" = "Դուք պատրաստվում եք հավելվածի գնումը վճարել";
"appjs_order_items" = "Գնման ցուցակ";
"appjs_payment_total" = "Վճարման ընդհանուր գին";
"appjs_payment_confirm" = "Վճարել";
"appjs_err_funds" = "Չհաջողվե՛ց վճարել գնումը անբավարար միջոցների համար։";
"appjs_wall_post" = "Հրապարակել գրությունտը";
"appjs_wall_post_desc" = "ցանկանում է Ձեր պատին գրություն թողնել";
"appjs_sperm_friends" = "ձեր ընկերներին";
"appjs_sperm_friends_desc" = "ավելացնել օգտատերերին որպես ընկերներ և կարդալ Ձեր գրությունները";
"appjs_sperm_wall" = "ձեր պատին";
"appjs_sperm_wall_desc" = "դիտել Ձեր լուրերը, կարդալ պատն ու թողել գրություններ";
"appjs_sperm_messages" = "ձեր նամակներին";
"appjs_sperm_messages_desc" = "կարդալ և գրել նամակներ Ձեր անունից";
"appjs_sperm_groups" = "ձեր հանրություններին";
"appjs_sperm_groups_desc" = "դիտել Ձեր խմբերի ցուցակն ու բաժանորդագրել դեպի այլ խմբեր";
"appjs_sperm_likes" = "լայքելու ֆունկցիոնալին";
"appjs_sperm_likes_desc" = "տեղադրել և հանել \"Դուր գալու\" ռեակցիաները ձայնագրություններից";
"appjs_sperm_request" = "Հասանելիության հարցում";
"appjs_sperm_requests" = "հասանելիություն է խնդրում";
"appjs_sperm_can" = "Հավելվածը կարող է";
"appjs_sperm_allow" = "Թույլատրել";
"appjs_sperm_disallow" = "Չթույլատրել";
"app_uninstalled" = "Հավելվածն անջատված է";
"app_uninstalled_desc" = "Այն Ձեր անունից էլ չի կարող կատարել գործողություններ։";
"app_err_not_found" = "Հավելվածը չի գտնվել";
"app_err_not_found_desc" = "Սխալ կամ անջատված իդենտիֆիկատոր։";
"app_err_forbidden_desc" = "Այս հավելվածը Ձերը չէ։";
"app_err_url" = "Սխալ հասցե";
"app_err_url_desc" = "Հավելվածի հասցեն չանցավ ստուգումը. համոզվե՛ք որ այն ճիշտ է գրված:";
"app_err_ava" = "Չհաջողվե՛ց վերբեռնել ավատարը:";
"app_err_ava_desc" = "Ավատարը չափազանց մեծ և ծուռ է. ընդհանուր բնույթի սխալ №$res.";
"app_err_note" = "Չհաջողվե՛ց ամրացնել նորությունների նիշքը";
"app_err_note_desc" = "Համոզվե՛ք որ հղումը ճիշտ է և պատկանում է Ձեզ։";
/* Support */
"support_opened" = "Բաց";
@ -767,7 +885,12 @@
"banned_alt" = "Օգտատերը արգելափակված է";
"banned_1" = "Կներե՛ք, <b>$1</b>, բայց Դուք կասեցված եք։";
"banned_2" = "Պատճառը հետևյալն է․ <b>$1</b>. Ափսոս, բայց մենք ստիպված Ձեզ հավերժ ենք կասեցրել;";
"banned_perm" = "Ցավոք, մենք ստիպված արգելափակել ենք Ձեզ ընդմիշտ։";
"banned_until_time" = "Այս անգամ մենք ստիպված արգելափակել ենք Ձեզ մինչև <b>$1</b>";
"banned_3" = "Դուք դեռ կարող եք <a href=\"/support?act=new\">գրել նամակ աջակցության ծառայությանը</a>, եթե համարում եք որ դա սխալմունք է, կամ էլ կարող եք <a href=\"/logout?hash=$1\">դուրս գալ</a>։";
"banned_unban_myself" = "Ապասառեցնել էջը";
"banned_unban_title" = "Ձեր հաշիվը ապասառեցված է։";
"banned_unban_description" = "Աշխատե՛ք այլևս չխախտել կանոնները։";
/* Registration confirm */
@ -996,6 +1119,17 @@
"admin_commerce_disabled" = "Կոմմերցիան անջատված է համակարգային ադմինիստրատորի կողմից";
"admin_commerce_disabled_desc" = "Վաուչերների և նվերների կարգավորումները կպահպանվեն, բայց ոչ մի ազդեցություն չեն ունենա։";
"admin_banned_links" = "Արգելափակված հղումներ";
"admin_banned_link" = "Հղում";
"admin_banned_domain" = "Դոմեն";
"admin_banned_link_description" = "Պրոտոկոլով (https://example.com/)";
"admin_banned_link_regexp" = "Ռեգուլյար արտահայտություն";
"admin_banned_link_regexp_description" = "Տեղադրվում է վերոնշյալ դոմենից հետո։ Մի լրացրե՛ք, եթե ցանկանում եք արգելափակել ամբողջ դոմենը";
"admin_banned_link_reason" = "Պատճառ";
"admin_banned_link_initiator" = "Նախաձեռնող";
"admin_banned_link_not_specified" = "Հղումը նշված չէ";
"admin_banned_link_not_found" = "Հղումը չի գտնվել";
/* Paginator (deprecated) */
"paginator_back" = "Հետ";
@ -1059,3 +1193,21 @@
"cookies_popup_content" = "Cookie բառը անգլերենից նշանակում է թխվածքաբլիթ, իսկ թխվածքաբլիթը համեղ է։ Մեր կայքը չի ուտում թխվածք, բայց օգտագործում է այն ուղղակի սեսսիան կողմնորոշելու համար։ Ավելի մանրամասն կարող եք ծանոթանալ մեր <a href='/privacy'>գաղտնիության քաղաքականությանը</a> հավելյալ ինֆորմացիայի համար։";
"cookies_popup_agree" = "Համաձայն եմ";
/* Away */
"url_is_banned" = "Անցումն անհնար է";
"url_is_banned_comment" = "Ադմինիստրացիան <b>$1</b> խորհուրդ չի տալից անցնել այս հղումով։";
"url_is_banned_comment_r" = "Ադմինիստրացիան <b>$1</b> խորհուրդ չի տալից անցնել այս հղումով։<br><br>Պատճառը: <b>$2</b>";
"url_is_banned_default_reason" = "Հղումը դեպի կայք կարող է ստեղծված լինել շորթողներից ՝ օգտատերերին խաբելու և խարդախության նպատակներով, շահույթ ստանալու համար։";
"url_is_banned_title" = "Հղում դեպի կասկածելի կայք";
"url_is_banned_proceed" = "Անցնել հղումով";
/* Maintenance */
"global_maintenance" = "Տեխնիկական աշխատանքներ";
"section_maintenance" = "Բաժինը անհասանելի է";
"undergoing_global_maintenance" = "Ցավոք սրտի, հիմա հոսքը փակված է տեխնիկական աշխատանքներ անցկացնելու համար։ Մենք արդեն աշխատում ենք խնդիրները շտկելու ուղղությամբ։ Խնդրում ենք այցելել մի քիչ ուշ։";
"undergoing_section_maintenance" = "Ցավոք սրտի, <b>$1</b> բաժինը ժամանակավորապես անհասանելի է։ Մենք արդեն աշխատում ենք խնդիրները շտկելու ուղղությամբ։ Խնդրում ենք այցելել մի քիչ ուշ։";
"topics" = "Թեմաներ";

View file

@ -16,6 +16,9 @@
"registration" = "Registration";
"forgot_password" = "Forgot your password?";
"checkbox_in_registration" = "I agree to the <a href='/privacy'>privacy policy</a> and <a href='/about'>site policies</a>";
"checkbox_in_registration_unchecked" = "You must agree to the privacy policy and rules in order to register.";
"login_failed" = "Login failed";
"invalid_username_or_password" = "The username or password you entered is incorrect. <a href='/restore'>Forgot your password?</a>";
@ -158,6 +161,9 @@
"post_deact_f" = "deleted her profile saying:";
"post_deact_silent_m" = "silently deleted his profile.";
"post_deact_silent_f" = "silently deleted her profile.";
"post_on_your_wall" = "on your wall";
"post_on_group_wall" = "in $1";
"post_on_user_wall" = "on $1's wall";
"wall" = "Wall";
"post" = "Post";
"write" = "Write";
@ -874,6 +880,38 @@
"messages_error_1" = "Message not delivered";
"messages_error_1_description" = "There was a general error in sending this message...";
/* Polls */
"poll" = "Poll";
"create_poll" = "Create poll";
"poll_title" = "Ask a question";
"poll_add_option" = "Add an option...";
"poll_anonymous" = "Anonymous votes";
"poll_multiple" = "Multiple answers";
"poll_locked" = "Quiz mode (no retraction)";
"poll_edit_expires" = "Expires in: ";
"poll_edit_expires_days" = "days";
"poll_editor_tips" = "Pressing backspace in empty option will remove it. Use Tab/Enter (in last option) to create new options faster.";
"poll_embed" = "Embed code";
"poll_voter_count_zero" = "Be <b>the first one</b> to vote!";
"poll_voter_count_one" = "<b>Only one</b> user voted.";
"poll_voter_count_few" = "<b>$1</b> users voted.";
"poll_voter_count_many" = "<b>$1</b> users voted.";
"poll_voter_count_other" = "<b>$1</b> users voted.";
"poll_voters_list" = "Voters";
"poll_anon" = "Anonymous";
"poll_public" = "Public";
"poll_multi" = "multiple choice";
"poll_lock" = "can't revoke";
"poll_until" = "until $1";
"poll_err_to_much_options" = "Too much options supplied.";
"poll_err_anonymous" = "Can't access voters list: poll is anonymous.";
"cast_vote" = "Vote!";
"retract_vote" = "Cancel my vote";
/* Discussions */
"discussions" = "Discussions";

View file

@ -13,7 +13,7 @@ list:
flag: "ua"
name: "Ukrainian"
native_name: "Україньска"
author: "Andrej Lenťaj, Maxim Hrabovi (dechioyo) and Kirill (mbsoft)"
author: "Yaroslav Bjelograd, Andrej Lenťaj, Maxim Hrabovi (dechioyo) and Kirill (mbsoft)"
- code: "by"
flag: "by"
name: "Belarussian"

1
locales/qqx.strings Normal file
View file

@ -0,0 +1 @@
/* Used for viewing message keys */

View file

@ -166,6 +166,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" = "Написать";
@ -921,6 +924,38 @@
"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" = "Обсуждения";
@ -996,7 +1031,7 @@
"changes_saved_comment" = "Новые данные появятся на вашей странице";
"photo_saved" = "Фотография сохранена";
"photo_saved_comment" = "Новое изображние профиля появится у вас на странице";
"photo_saved_comment" = "Новое изображение профиля появится у вас на странице";
"shared_succ" = "Запись появится на вашей стене. Нажмите на уведомление, чтобы перейти к своей стене.";

View file

@ -15,6 +15,8 @@
"password" = "Пароль";
"registration" = "Реєстрація";
"forgot_password" = "Забули пароль?";
"checkbox_in_registration" = "Я згоден з <a href='/privacy'>політикою конфіденційності</a> і <a href='/about'>правилами сайту</a>";
"checkbox_in_registration_unchecked" = "Ви повинні погодитися з політикою конфіденційності та правилами, щоб зареєструватися.";
"login_failed" = "Не вдалося увійти";
"invalid_username_or_password" = "Неправильне ім'я користувача або пароль. <a href='/restore'>Забули пароль?</a>";
@ -160,6 +162,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" = "Написати";
@ -210,6 +215,7 @@
/* Friends */
"friends" = "Друзі";
"followers" = "Підписники";
"follower" = "Підписник";
"friends_add" = "Додати в друзі";
@ -218,10 +224,11 @@
"friends_accept" = "Прийняти заявку";
"send_message" = "Відправити повідомлення";
"incoming_req" = "Підписники";
"outcoming_req" = "Вихідні";
"req" = "Заявки";
"outcoming_req" = "Заявки";
"friends_online" = "Друзі онлайн";
"all_friends" = "Усі друзі";
"req" = "Заявки";
"req_zero" = "Не знайдено жодної заявки...";
"req_one" = "Знайдена $1 заявка";
@ -235,18 +242,18 @@
"friends_many" = "$1 друзів";
"friends_other" = "$1 друзів";
"friends_list_zero" = "У вас поки немає друзів";
"friends_list_one" = "У Вас $1 друг";
"friends_list_few" = "У Вас $1 друг";
"friends_many" = "$1 друзів";
"friends_other" = "$1 друзів";
"friends_online_zero" = "Жодного друга онлайн";
"friends_online_one" = "$1 друг онлайн";
"friends_online_few" = "$1 друга онлайн";
"friends_online_many" = "$1 друзів онлайн";
"friends_online_other" = "$1 друзів онлайн";
"friends_list_zero" = "У вас поки немає друзів";
"friends_list_one" = "У Вас $1 друг";
"friends_list_few" = "У Вас $1 друг";
"friends_many" = "$1 друзів";
"friends_other" = "$1 друзів";
"followers_zero" = "Жодного підписника";
"followers_one" = "$1 підписник";
"followers_few" = "$1 підписника";
@ -1122,6 +1129,17 @@
"admin_commerce_disabled" = "Комерція відключена системним адміністратором";
"admin_commerce_disabled_desc" = "Налаштування ваучерів та подарунків будуть збережені, але не матимуть впливу.";
"admin_banned_links" = "Заблоковані посилання";
"admin_banned_link" = "Посилання";
"admin_banned_domain" = "Домен";
"admin_banned_link_description" = "З протоколом (https://example.org/)";
"admin_banned_link_regexp" = "Регулярний вираз";
"admin_banned_link_regexp_description" = "Підставляється після домену, зазначеного вище. Не заповнюйте, якщо хочете заблокувати весь домен";
"admin_banned_link_reason" = "Причина";
"admin_banned_link_initiator" = "Ініціатор";
"admin_banned_link_not_specified" = "Посилання не зазначено";
"admin_banned_link_not_found" = "Посилання не знайдено";
/* Paginator (deprecated) */
"paginator_back" = "Назад";
@ -1185,3 +1203,53 @@
"cookies_popup_content" = "Цей веб-сайт використовує cookies для того, щоб ідентифікувати вашу сесію і нічого більше. Ознайомтеся з нашою <a href='/privacy'>політикою конфіденційності</a> для отримання додаткової інформації.";
"cookies_popup_agree" = "Згоден";
/* Away */
"url_is_banned" = "Перехід неможливий";
"url_is_banned_comment" = "Адміністрація <b>$1</b> не рекомендує переходити за цим посиланням.";
"url_is_banned_comment_r" = "Адміністрація <b>$1</b> не рекомендує переходити за цим посиланням.<br><br>Підстава: <b>$2</b>";
"url_is_banned_default_reason" = "Посилання, за яким Ви спробували перейти, може вести на сайт, що був створений з метою обману користувачів і отримання шляхом цього неправомірного прибутку.";
"url_is_banned_title" = "Посилання на підозрілий сайт";
"url_is_banned_proceed" = "Перейти за посиланням";
/* Maintenance */
"global_maintenance" = "Технічні роботи";
"section_maintenance" = "Розділ недоступний";
"undergoing_global_maintenance" = "На жаль, зараз інстанція закрита на технічні роботи. Ми вже працюємо над усуненням неполадок. Будь ласка, спробуйте зайти пізніше.";
"undergoing_section_maintenance" = "На жаль, розділ <b>$1</b> тимчасово недоступний. Ми вже працюємо над усуненням неполадок. Будь ласка, спробуйте зайти пізніше.";
"topics" = "Теми";
/* Polls */
"poll" = "Опитування";
"create_poll" = "Нове опитування";
"poll_title" = "Тема опитування";
"poll_add_option" = "Додати новий варіант відповіді";
"poll_anonymous" = "Анонiмне опитування";
"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" = "Відмінити голос";

View file

@ -58,6 +58,20 @@ openvk:
susLinks:
warnings: true
showReason: true
maintenanceMode:
all: false
photos: false
videos: false
messenger: false
user: false
group: false
comment: false
gifts: false
apps: false
notes: false
notification: false
support: false
topics: false
ton:
enabled: false
address: "🅿"

View file

@ -44,3 +44,5 @@ comments.allow-graffiti: 0
# + Set this option to -1 if you want to disable the limit
# + Set this option to any non-negative number to be this limit
wall.repost-liking-recursion-limit: 10
polls.max-opts: 10

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -0,0 +1,7 @@
.page_header {
background-image: url('/themepack/midnight/0.0.1.0/resource/xheader.png');
}
.page_custom_header {
background-image: url('/themepack/midnight/0.0.1.0/resource/xheader_custom.png');
}

View file

@ -0,0 +1,214 @@
* {
scrollbar-color: #3f365b #1d192b;
}
html {
color-scheme: dark !important;
}
body {
background-color: #0e0b1a;
color: #c6d2e8;
}
span, .post-author .date, .crp-entry--message---text, .messenger-app--messages---message .time, .navigation-lang .link_new {
color: #8b9ab5 !important;
}
.nobold, nobold {
color: #6f82a8;
}
.page_status {
color: #c6d2e8;
}
.profileName h2 {
color: #8eb2d0;
}
.wrap1, .wrap2, .page-wrap, #wrapH, #wrapHI {
border-color: #1c202f;
}
.accountInfo, .left_small_block, #profile_link, .profile_link, .navigation .link, .navigation .link:hover, .navigation_footer .link, .navigation_footer .link:hover, .completeness-gauge, input[type="text"], input[type="password"], input[type~="text"], input[type~="password"], input[type="email"], input[type="phone"], input[type~="email"], input[type~="phone"], input[type="search"], input[type~="search"], input[type~="date"], select, .content_title_expanded, .content_title_unexpanded, .content_subtitle, textarea, .post-content, .post-author, hr, h4, .postFeedWrapper, .tabs, #wallAttachmentMenu, .ovk-diag, .ovk-diag-head, #ovkDraw, #ovkDraw .literally .lc-picker, .literally .lc-options.horz-toolbar, .page_wrap, .container_gray .content, .summaryBar, .groups_options, form[action="/search"] > input, .header_search_input, .header_search_inputbt, .accent-box, .page_status_popup, .messenger-app--input, .messenger-app, .crp-entry:first-of-type, .crp-list, .crp-entry, .note_footer, .page_content > div, #editor, .note_header, center[style="background: white;border: #DEDEDE solid 1px;"], .album-photo img, .mb_tabs, .mb_tab#active div, .navigation-lang .link_new, #faqhead, #faqcontent, .post-divider, .comment, .commentsTextFieldWrap, tr, td, th, #votesBalance, .paginator a.active, .paginator a:hover, .topic-list-item, #userContent blockquote, .tippy-box[data-theme~="vk"], .poll {
border-color: #2c2640 !important;
}
.tippy-box[data-theme~="vk"][data-placement^='top'] > .tippy-arrow::before, .tippy-box[data-theme~="vk"][data-placement^='bottom'] > .tippy-arrow::before {
border-top-color: #1e1a2b;
border-bottom-color: #1e1a2b;
}
hr {
background-color: #2c2640 !important;
}
.cookies-popup {
background: linear-gradient(#1e1a2b, #231e33);
box-shadow: unset;
}
.button, #activetabs, .messagebox-content-header, .accent-box, .button_search {
background-color: #383052;
}
.tab:hover {
background-color: #40375e;
}
.menu_divider, .ovk-diag-action, .minilink .counter {
background-color: #2c2640;
}
#ovkDraw .literally .lc-picker, .literally .lc-options.horz-toolbar, .mb_tab#active {
background-color: #453e5e;
}
.ovk-diag-cont {
background-color: #272e4894;
}
a, .page_footer .link, #profile_link, .profile_link {
color: #8fb9d8;
}
.page_footer .link:hover, .navigation .link:hover, .navigation .edit-button:hover, #profile_link:hover, .profile_link:hover, #wallAttachmentMenu > a:hover, .crp-entry:hover, .navigation-lang .link_new:hover, .paginator a:hover, .post-share-button:hover, .post-like-button:hover {
background-color: #272138 !important;
}
.header_navigation .link a {
color: #bcc3d0;
}
.header_navigation .link a:hover, .home_button_custom {
color: #c7cdd9;
}
.navigation .link {
color: #d9e0ee;
}
.navigation .edit-button {
background-color: #0e0b1a !important;
color: #7b94c4 !important;
}
#test-label, .msg.msg_err {
background-color: #30161d;
}
.msg.msg_succ {
background-color: #163f13;
}
h4, .content_title_expanded, .summaryBar .summary, .content_title_unexpanded {
color: #7c94c5;
}
.notes_titles small, .post-upload, .post-has-poll {
color: #6f82a8;
}
.content_title_expanded, .content_title_unexpanded, .ovk-diag, .settings_delete, center[style="background: white;border: #DEDEDE solid 1px;"], .album-photo img, #faqhead, td.e, tr.e {
background-color: #231e33 !important;
}
.content_subtitle, .postFeedWrapper, .ovk-diag-head, .container_gray, .page_status_popup, .messenger-app--input, .note_header, #faqcontent, .commentsTextFieldWrap, td.v, tr.v, #votesBalance, .expand_button, #userContent blockquote, .tippy-box[data-theme~="vk"] {
background-color: #1e1a2b !important;
}
.post-author {
background-color: #1e1a2b;
/* this is fix to correct the unexpected behavior of the microblog style lol */
}
.content_title_expanded {
background-image: url("/themepack/midnight/0.0.1.4/resource/flex_arrow_open.png");
}
.content_title_unexpanded {
background-image: url("/themepack/midnight/0.0.1.4/resource/flex_arrow_shut.gif");
}
.ovk-video > .preview, .video-preview {
box-shadow: inset 0 0 0 1px #231e33, inset 0 0 0 2px #1e1a2b;
}
#wallAttachmentMenu, .container_gray .content, .mb_tabs {
background-color: #120e1f;
}
#wallAttachmentMenu > .header, .messenger-app--messages---message.unread, tr.h {
background-color: #1d192b;
}
.toTop {
background-color: #272138;
color: #c6d2e8;
}
.page_yellowheader {
color: #c6d2e8;
background-image: url("/themepack/midnight/0.0.1.4/resource/header_purple.png");
background-color: #231f34;
border-color: #231f34;
}
.page_header {
background-image: url("/themepack/midnight/0.0.1.4/resource/header.png");
}
.page_custom_header {
background-image: url("/themepack/midnight/0.0.1.4/resource/header_custom.png");
}
.page_yellowheader span, .page_yellowheader a {
color: #a48aff !important;
}
.completeness-gauge, .poll-result-bar {
background-color: #231e33;
}
.completeness-gauge > div, .poll-result-bar-sub {
background-color: #2c2640;
}
form[action="/search"] > input, .header_search_input, textarea, input[type="text"] {
background-color: #181826 !important;
}
input[type="checkbox"] {
background-image: url("/themepack/midnight/0.0.1.4/resource/checkbox.png");
}
input[type="radio"] {
background-image: url("/themepack/midnight/0.0.1.4/resource/radio.png");
}
.header_navigation .link {
background: unset;
}
.heart {
background-image: url("/themepack/midnight/0.0.1.4/resource/like.gif") !important;
}
.pinned-mark, .post-author .pin {
background-image: url("/themepack/midnight/0.0.1.4/resource/pin.png") !important;
}
.repost-icon {
background-image: url("/themepack/midnight/0.0.1.4/resource/published.gif") !important;
}
.post-author .delete {
background-image: url("/themepack/midnight/0.0.1.4/resource/input_clear.gif") !important;
}
.user-alert {
background-color: #41311a;
color: #d5b88c;
border-color: #514534;
}

View file

@ -0,0 +1,12 @@
id: midnight
version: "0.0.1.4"
openvk_version: 0
enabled: 1
metadata:
name:
_: "OpenVK Midnight"
en: "OpenVK Midnight"
ru: "OpenVK Midnight"
uk: "OpenVK Midnight"
author: "Ilya Prokopenko"
description: "A dark theme for OpenVK"

View file

@ -7,5 +7,6 @@ metadata:
_: "OpenVK Modern"
en: "OpenVK Modern"
ru: "OpenVK Modern"
uk: "OpenVK Modern"
author: "Mikita Wiśniewski (rudzik8)"
description: "OpenVK theme in modern style"

View file

@ -1,10 +1,10 @@
# OpenVK Themepacks
This folder contains all themes that can be used by any user on instance.
This folder contains all themes that can be used by any user on an instance.
## How do i create the theme?
## How do I create a theme?
Create a directory, the name of which should contain only Latin letters and numbers, and create a file there `theme.yml`, and fill it with the following content:
Create a directory, the name of which should contain only Latin letters and numbers, then create a file in this directory called `theme.yml`, and fill it with the following content:
```yaml
id: vk2007
@ -24,13 +24,13 @@ metadata:
`id` is the name of the folder
`version` - version of the theme
`version` is the version of the theme
`openvk_version` - version OpenVK *(it is necessary to leave the value 0)*
`openvk_version` is the version of OpenVK *(it is necessary to leave the value to 0)*
`metadata`:
* `name` - the name of the theme for the end user. Inside it you can leave names for different languages. `_` (underscore) - for all languages.
* `name` - the name of the theme for the end user. Inside it you can leave names for different languages. `_` (underscore) is for all languages.
Next, in `stylesheet.css` you can insert any CSS code, with which you can change the elements of the site. If you need additional pictures or resources, just create a `res` folder, and access the resources via the `/themepack/{directory name}/{theme version}/resource/{resource}` path.

View file

@ -34,7 +34,7 @@ metadata:
Далее, в `stylesheet.css` вставляем любой CSS код, с помощью которого вы можете изменить элементы сайта. Если вам нужны дополнительные картинки или ресурсы, то для этого просто создайте папку `res`, и в CSS коде обращайтесь к ресурсам через путь `/themepack/{название директории}/{версия темы}/resource/{ресурс}`.
Для поддержки новогоднего насторения, которое включается автоматически с 1 декабря по 15 января, создайте файл `xmas.css` в папку `res`, и внесите вам нужные изменения.
Для поддержки новогоднего настроения, которое включается автоматически с 1 декабря по 15 января, создайте файл `xmas.css` в папку `res`, и внесите вам нужные изменения.
**В конце концов, иерархия директории с темой должна выглядеть вот так:**