diff --git a/frontend/src/components/MarkupEditor/MarkupCodeModal.jsx b/frontend/src/components/MarkupEditor/MarkupCodeModal.jsx index b2c1f03acf..5cc0a28713 100644 --- a/frontend/src/components/MarkupEditor/MarkupCodeModal.jsx +++ b/frontend/src/components/MarkupEditor/MarkupCodeModal.jsx @@ -100,7 +100,7 @@ class MarkupCodeModal extends React.Component { data-dismiss="modal" type="button" > - {gettext("Cancel")} + {pgettext("markup editor", "Cancel")} )} ) diff --git a/frontend/src/components/MarkupEditor/MarkupEditorToolbar.jsx b/frontend/src/components/MarkupEditor/MarkupEditorToolbar.jsx index d0bb34751c..0a160f756a 100644 --- a/frontend/src/components/MarkupEditor/MarkupEditorToolbar.jsx +++ b/frontend/src/components/MarkupEditor/MarkupEditorToolbar.jsx @@ -178,7 +178,7 @@ const MarkupEditorToolbar = ({ { modal.show() diff --git a/frontend/src/components/MarkupEditor/MarkupImageModal.jsx b/frontend/src/components/MarkupEditor/MarkupImageModal.jsx index 71f2f1afa6..cdd0e016d2 100644 --- a/frontend/src/components/MarkupEditor/MarkupImageModal.jsx +++ b/frontend/src/components/MarkupEditor/MarkupImageModal.jsx @@ -61,7 +61,7 @@ class MarkupImageModal extends React.Component {
this.setState({ url: event.target.value }) } @@ -100,7 +101,7 @@ class MarkupImageModal extends React.Component { data-dismiss="modal" type="button" > - {gettext("Cancel")} + {pgettext("markup editor", "Cancel")}
-

- {pgettext( - "delete your account form", - "You are going to delete your account. This action is nonreversible, and will result in following data being deleted:" - )} -

-

- -{" "} {pgettext( "delete your account form", - "Stored IP addresses associated with content that you have posted will be deleted." + "This form lets you delete your account. This action is not reversible." )}

- -{" "} {pgettext( "delete your account form", - "Your username will become available for other user to rename to or for new user to register their account with." + "Your account will be deleted together with its profile details, IP addresses and notifications." )}

- -{" "} {pgettext( "delete your account form", - "Your e-mail will become available for use in new account registration." + "Other content will NOT be deleted, but username displayed next to it will be changed to one shared by all deleted accounts." )}

- -
-

{pgettext( "delete your account form", - "All your posted content will NOT be deleted, but username associated with it will be changed to one shared by all deleted accounts." + "Your username and e-maill address will become available again for use during registration or for other accounts to change to." )}

@@ -117,9 +99,10 @@ export default class extends React.Component { type="password" placeholder={pgettext( "delete your account form field", - "Enter your password to confirm account deletion." + "Enter your password to confirm" )} value={this.state.password} + required onChange={this.onPasswordChange} /> diff --git a/frontend/src/components/options/edit-details.js b/frontend/src/components/options/edit-details.js index 1c0f1317e5..7ed2bf2bc7 100644 --- a/frontend/src/components/options/edit-details.js +++ b/frontend/src/components/options/edit-details.js @@ -13,7 +13,7 @@ export default class extends React.Component { onSuccess = () => { snackbar.info( - pgettext("profile details form", "Your details have been updated.") + pgettext("profile details form", "Your details have been changed.") ) } diff --git a/frontend/src/components/options/forum-options.js b/frontend/src/components/options/forum-options.js index bc47d643b1..949af8de71 100644 --- a/frontend/src/components/options/forum-options.js +++ b/frontend/src/components/options/forum-options.js @@ -144,7 +144,7 @@ export default class ForumOptionsForm extends Form { }) ) snackbar.success( - pgettext("forum options form", "Your forum options have been updated.") + pgettext("forum options form", "Your forum options have been changed.") ) } @@ -184,7 +184,7 @@ export default class ForumOptionsForm extends Form { label={pgettext("forum options form", "Hide my presence")} helpText={pgettext( "forum options form", - "If you hide your presence, only members with permission to see hidden users will see when you are online." + "If you hide your presence, only members with permission to see hidden presence will see when you are online." )} for="id_is_hiding_presence" > diff --git a/frontend/src/components/options/sign-in-credentials/UnusablePasswordMessage.js b/frontend/src/components/options/sign-in-credentials/UnusablePasswordMessage.js index 2845c83b60..9e330bc2d0 100644 --- a/frontend/src/components/options/sign-in-credentials/UnusablePasswordMessage.js +++ b/frontend/src/components/options/sign-in-credentials/UnusablePasswordMessage.js @@ -8,7 +8,7 @@ const UnusablePasswordMessage = () => {

{pgettext( "change sign in credentials title", - "Change email or password" + "Change e-mail or password" )}

@@ -20,7 +20,7 @@ const UnusablePasswordMessage = () => {

{pgettext( "change sign in credentials", - "You need to set a password for your account to be able to change your username or email." + "You need to set a password for your account to be able to change your e-mail or password." )}

diff --git a/frontend/src/components/options/sign-in-credentials/change-password.js b/frontend/src/components/options/sign-in-credentials/change-password.js index cbba7e1b02..4a17c8c7b4 100644 --- a/frontend/src/components/options/sign-in-credentials/change-password.js +++ b/frontend/src/components/options/sign-in-credentials/change-password.js @@ -33,7 +33,7 @@ export default class extends Form { ] if (lengths.indexOf(0) !== -1) { - snackbar.error(gettext("Fill out all fields.")) + snackbar.error(pgettext("change password form", "Fill out all fields.")) return false } diff --git a/frontend/src/components/options/sign-in-credentials/root.js b/frontend/src/components/options/sign-in-credentials/root.js index 76c8fe9ce9..087eb814cc 100644 --- a/frontend/src/components/options/sign-in-credentials/root.js +++ b/frontend/src/components/options/sign-in-credentials/root.js @@ -10,7 +10,7 @@ export default class extends React.Component { title.set({ title: pgettext( "change sign in credentials title", - "Change email or password" + "Change e-mail or password" ), parent: pgettext("forum options", "Change your options"), }) diff --git a/frontend/src/components/poll/voting/index.js b/frontend/src/components/poll/voting/index.js index 7ffe9e04f1..7be346de55 100644 --- a/frontend/src/components/poll/voting/index.js +++ b/frontend/src/components/poll/voting/index.js @@ -70,7 +70,7 @@ export default class extends Form { clean() { if (this.state.choicesLeft === this.props.poll.allowed_choices) { snackbar.error( - pgettext("thread poll vote", "You need to select at least one choice") + pgettext("thread poll vote", "You need to select at least one choice.") ) return false } diff --git a/frontend/src/components/post-changelog/toolbar.js b/frontend/src/components/post-changelog/toolbar.js index a0126656f9..acd48cd809 100644 --- a/frontend/src/components/post-changelog/toolbar.js +++ b/frontend/src/components/post-changelog/toolbar.js @@ -115,11 +115,11 @@ export function RevertBtn(props) { disabled={props.disabled} onClick={props.onClick} title={pgettext( - "post history modal btn", + "post revert btn", "Revert post to state from before this edit." )} > - {pgettext("post history modal btn", "Revert")} + {pgettext("post revert btn", "Revert")} ) diff --git a/frontend/src/components/post-feed/post/body.js b/frontend/src/components/post-feed/post/body.js index 5550789655..66ea69e645 100644 --- a/frontend/src/components/post-feed/post/body.js +++ b/frontend/src/components/post-feed/post/body.js @@ -22,13 +22,13 @@ export function Invalid(props) {

{pgettext( - "posts feed item body", + "post body invalid", "This post's contents cannot be displayed." )}

{pgettext( - "posts feed item body", + "post body invalid", "This error is caused by invalid post content manipulation." )}

diff --git a/frontend/src/components/posting/PostingThreadOptions.jsx b/frontend/src/components/posting/PostingThreadOptions.jsx index 8a6c42a3e0..710a7f4821 100644 --- a/frontend/src/components/posting/PostingThreadOptions.jsx +++ b/frontend/src/components/posting/PostingThreadOptions.jsx @@ -62,7 +62,7 @@ export default function PostingThreadOptions({ disabled={disabled} > bookmark_outline - {pgettext("post thread", "Pinned locally")} + {pgettext("post thread", "Pinned in category")} )} diff --git a/frontend/src/components/posting/start.js b/frontend/src/components/posting/start.js index 4877f687cb..50d6ec0aa4 100644 --- a/frontend/src/components/posting/start.js +++ b/frontend/src/components/posting/start.js @@ -294,7 +294,7 @@ export default class extends Form { {}} onChange={() => {}} diff --git a/frontend/src/components/posting/utils/options.js b/frontend/src/components/posting/utils/options.js index 6c7fd1603a..70b15e93b9 100644 --- a/frontend/src/components/posting/utils/options.js +++ b/frontend/src/components/posting/utils/options.js @@ -134,7 +134,7 @@ export function PinOptions(props) { case 1: icon = "bookmark_outline" onClick = props.onPinGlobally - label = pgettext("posting form", "Pinned locally") + label = pgettext("posting form", "Pinned in category") if (props.show == 2) { onClick = props.onPinGlobally diff --git a/frontend/src/components/posts-list/event/controls.js b/frontend/src/components/posts-list/event/controls.js index 0cc3afa076..f1fb073451 100644 --- a/frontend/src/components/posts-list/event/controls.js +++ b/frontend/src/components/posts-list/event/controls.js @@ -117,7 +117,7 @@ export class Unhide extends React.Component { export class Delete extends React.Component { onClick = () => { const decision = window.confirm( - gettext( + pgettext( "event delete", "Are you sure you wish to delete this event? This action is not reversible!" ) diff --git a/frontend/src/components/posts-list/event/message.js b/frontend/src/components/posts-list/event/message.js index 1c8e137d9e..c97e976b2f 100644 --- a/frontend/src/components/posts-list/event/message.js +++ b/frontend/src/components/posts-list/event/message.js @@ -6,7 +6,10 @@ const MESSAGE = { "event message", "Thread has been pinned globally." ), - pinned_locally: pgettext("event message", "Thread has been pinned locally."), + pinned_locally: pgettext( + "event message", + "Thread has been pinned in category." + ), unpinned: pgettext("event message", "Thread has been unpinned."), approved: pgettext("event message", "Thread has been approved."), diff --git a/frontend/src/components/posts-list/post/controls/split.js b/frontend/src/components/posts-list/post/controls/split.js index e7343fd5b4..b2f68ef498 100644 --- a/frontend/src/components/posts-list/post/controls/split.js +++ b/frontend/src/components/posts-list/post/controls/split.js @@ -209,7 +209,7 @@ export class ModerationForm extends Form { { value: 1, icon: "bookmark_border", - label: pgettext("thread weight choice", "Pinned locally"), + label: pgettext("thread weight choice", "Pinned in category"), }, ] diff --git a/frontend/src/components/profile/details/index.js b/frontend/src/components/profile/details/index.js index caedc6d483..f722f72c32 100644 --- a/frontend/src/components/profile/details/index.js +++ b/frontend/src/components/profile/details/index.js @@ -38,13 +38,13 @@ export default class extends React.Component { if (isAuthenticated) { message = pgettext( "profile details form", - "Your details have been updated." + "Your details have been changed." ) } else { message = interpolate( pgettext( "profile details form", - "%(username)s's details have been updated." + "%(username)s's details have been changed." ), { username: profile.username, diff --git a/frontend/src/components/profile/feed/index.js b/frontend/src/components/profile/feed/index.js index 20fec5c608..f012271b20 100644 --- a/frontend/src/components/profile/feed/index.js +++ b/frontend/src/components/profile/feed/index.js @@ -4,10 +4,13 @@ import Route from "./route" export function Threads(props) { let emptyMessage = null if (props.user.id === props.profile.id) { - emptyMessage = pgettext("profile threads", "You have no started threads.") + emptyMessage = pgettext( + "profile threads", + "You haven't started any threads." + ) } else { emptyMessage = interpolate( - pgettext("profile threads", "%(username)s started no threads."), + pgettext("profile threads", "%(username)s hasn't started any threads"), { username: props.profile.username, }, diff --git a/frontend/src/components/profile/username-history.js b/frontend/src/components/profile/username-history.js index b5638183e6..e60045ab70 100644 --- a/frontend/src/components/profile/username-history.js +++ b/frontend/src/components/profile/username-history.js @@ -183,8 +183,8 @@ export default class extends React.Component { ) } else if (this.props.user.id === this.props.profile.id) { return pgettext( - "profile username history", - "No name changes have been recorded for your account." + "username history empty", + "Your account has no history of name changes." ) } else { return interpolate( diff --git a/frontend/src/components/register-button.js b/frontend/src/components/register-button.js deleted file mode 100644 index 853d8ab2e3..0000000000 --- a/frontend/src/components/register-button.js +++ /dev/null @@ -1,78 +0,0 @@ -import classnames from "classnames" -import React from "react" -import Loader from "misago/components/loader" -import RegisterForm from "misago/components/register.js" -import ajax from "misago/services/ajax" -import captcha from "misago/services/captcha" -import modal from "misago/services/modal" -import snackbar from "misago/services/snackbar" - -export default class extends React.Component { - constructor(props) { - super(props) - - this.state = { - isLoading: false, - isLoaded: false, - - criteria: null, - } - } - - showRegisterForm = () => { - if (misago.get("SETTINGS").account_activation === "closed") { - snackbar.info( - pgettext( - "registration btn error", - "New registrations are currently disabled." - ) - ) - } else if (this.state.isLoaded) { - modal.show() - } else { - this.setState({ isLoading: true }) - - Promise.all([ - captcha.load(), - ajax.get(misago.get("AUTH_CRITERIA_API")), - ]).then( - (result) => { - this.setState({ - isLoading: false, - isLoaded: true, - criteria: result[1], - }) - - modal.show() - }, - () => { - this.setState({ isLoading: false }) - - snackbar.error( - pgettext( - "registration btn error", - "Registration is currently unavailable due to an error." - ) - ) - } - ) - } - } - - render() { - return ( - - ) - } -} diff --git a/frontend/src/components/register.js b/frontend/src/components/register.js index 33af22f412..6682714f63 100644 --- a/frontend/src/components/register.js +++ b/frontend/src/components/register.js @@ -254,13 +254,13 @@ export class RegisterComplete extends React.Component { getLead() { if (this.props.activation === "user") { return pgettext( - "register modal", + "account activation required", "%(username)s, your account has been created but you need to activate it before you will be able to sign in." ) } else if (this.props.activation === "admin") { return pgettext( - "register modal", - "%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in." + "account activation required", + "%(username)s, your account has been created but the site administrator will have to activate it before you will be able to sign in." ) } } @@ -268,12 +268,12 @@ export class RegisterComplete extends React.Component { getSubscript() { if (this.props.activation === "user") { return pgettext( - "register modal", + "account activation required", "We have sent an e-mail to %(email)s with link that you have to click to activate your account." ) } else if (this.props.activation === "admin") { return pgettext( - "register modal", + "account activation required", "We will send an e-mail to %(email)s when this takes place." ) } diff --git a/frontend/src/components/request-activation-link.js b/frontend/src/components/request-activation-link.js index 3607fa321c..8684b573c9 100644 --- a/frontend/src/components/request-activation-link.js +++ b/frontend/src/components/request-activation-link.js @@ -27,7 +27,10 @@ export class RequestLinkForm extends Form { return true } else { snackbar.error( - pgettext("request activation link form", "Enter a valid email address.") + pgettext( + "request activation link form", + "Enter a valid e-mail address." + ) ) return false } diff --git a/frontend/src/components/request-password-reset.js b/frontend/src/components/request-password-reset.js index 4680f1d1dd..445040bc53 100644 --- a/frontend/src/components/request-password-reset.js +++ b/frontend/src/components/request-password-reset.js @@ -28,7 +28,7 @@ export class RequestResetForm extends Form { return true } else { snackbar.error( - pgettext("request password reset form", "Enter a valid email address.") + pgettext("request password reset form", "Enter a valid e-mail address.") ) return false } diff --git a/frontend/src/components/reset-password-form.js b/frontend/src/components/reset-password-form.js index fd8dcab07b..a9f7760625 100644 --- a/frontend/src/components/reset-password-form.js +++ b/frontend/src/components/reset-password-form.js @@ -85,7 +85,7 @@ export class PasswordChangedPage extends React.Component { return interpolate( pgettext( "password reset form", - "%(username)s, your password has been changed successfully." + "%(username)s, your password has been changed." ), { username: this.props.user.username, @@ -112,7 +112,7 @@ export class PasswordChangedPage extends React.Component {

{pgettext( "password reset form", - "You will have to sign in using new password before continuing." + "Sign in using new password to continue." )}

diff --git a/frontend/src/components/search-route/page.js b/frontend/src/components/search-route/page.js index c56c063d92..cc94bbdf56 100644 --- a/frontend/src/components/search-route/page.js +++ b/frontend/src/components/search-route/page.js @@ -32,7 +32,7 @@ export function SearchTime(props) { if (time === null) return null - const copy = pgettext("search time", "Search took %(time)s s to complete") + const copy = pgettext("search time", "Search took %(time)s s") return (

diff --git a/frontend/src/components/search-route/threads/post.js b/frontend/src/components/search-route/threads/post.js index 315bd4241e..bd3a95283c 100644 --- a/frontend/src/components/search-route/threads/post.js +++ b/frontend/src/components/search-route/threads/post.js @@ -34,13 +34,13 @@ export function PostBody(props) {

{pgettext( - "search threads result body invalid", + "post body invalid", "This post's contents cannot be displayed." )}

{pgettext( - "search threads result body invalid", + "post body invalid", "This error is caused by invalid post content manipulation." )}

diff --git a/frontend/src/components/social-auth/complete.js b/frontend/src/components/social-auth/complete.js index bbd34e6f2f..bebf8fccfa 100644 --- a/frontend/src/components/social-auth/complete.js +++ b/frontend/src/components/social-auth/complete.js @@ -8,13 +8,13 @@ const Complete = ({ activation, backend_name, username }) => { let message = "" if (activation === "user") { message = pgettext( - "social auth complete", + "account activation required", "%(username)s, your account has been created but you need to activate it before you will be able to sign in." ) } else if (activation === "admin") { message = pgettext( - "social auth complete", - "%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in." + "account activation required", + "%(username)s, your account has been created but the site administrator will have to activate it before you will be able to sign in." ) } else { message = pgettext( diff --git a/frontend/src/components/social-auth/register.js b/frontend/src/components/social-auth/register.js index 92ff5193eb..c35e139553 100644 --- a/frontend/src/components/social-auth/register.js +++ b/frontend/src/components/social-auth/register.js @@ -154,7 +154,7 @@ export default class Register extends Form {

{pgettext( "social auth form title", - "Complete your details" + "Complete your account" )}

diff --git a/frontend/src/components/thread/moderation/posts/split.js b/frontend/src/components/thread/moderation/posts/split.js index 923cfa6f02..0368abf242 100644 --- a/frontend/src/components/thread/moderation/posts/split.js +++ b/frontend/src/components/thread/moderation/posts/split.js @@ -217,7 +217,7 @@ export class ModerationForm extends Form { { value: 1, icon: "bookmark_border", - label: pgettext("thread weight choice", "Pinned locally"), + label: pgettext("thread weight choice", "Pinned in category"), }, ] diff --git a/frontend/src/components/thread/moderation/thread/controls.js b/frontend/src/components/thread/moderation/thread/controls.js index a57756e351..b73741ff3f 100644 --- a/frontend/src/components/thread/moderation/thread/controls.js +++ b/frontend/src/components/thread/moderation/thread/controls.js @@ -58,7 +58,7 @@ export default class extends React.Component { value: 1, }, ], - pgettext("thread moderation", "Thread has been pinned locally.") + pgettext("thread moderation", "Thread has been pinned in category.") ) } @@ -215,7 +215,7 @@ export default class extends React.Component { type="button" > bookmark_border - {pgettext("thread moderation btn", "Pin locally")} + {pgettext("thread moderation btn", "Pin in category")} )} diff --git a/frontend/src/components/threads/moderation/controls.js b/frontend/src/components/threads/moderation/controls.js index ed33a1a826..2af7952379 100644 --- a/frontend/src/components/threads/moderation/controls.js +++ b/frontend/src/components/threads/moderation/controls.js @@ -96,7 +96,10 @@ export default class extends React.Component { value: 1, }, ], - pgettext("threads moderation", "Selected threads were pinned locally.") + pgettext( + "threads moderation", + "Selected threads were pinned in category." + ) ) } @@ -324,7 +327,7 @@ export default class extends React.Component { onClick={this.pinLocally} > bookmark_border - {pgettext("threads moderation btn", "Pin threads locally")} + {pgettext("threads moderation btn", "Pin threads in categories")} )} diff --git a/frontend/src/components/threads/moderation/merge.js b/frontend/src/components/threads/moderation/merge.js index baab02aa93..dc13256d62 100644 --- a/frontend/src/components/threads/moderation/merge.js +++ b/frontend/src/components/threads/moderation/merge.js @@ -201,7 +201,7 @@ export default class extends Form { { value: 1, icon: "bookmark_border", - label: pgettext("thread weight choice", "Pinned locally"), + label: pgettext("thread weight choice", "Pinned in category"), }, ] @@ -340,7 +340,7 @@ export default class extends Form {

{pgettext( "threads moderation merge", - "You can't move threads because there are no categories you are allowed to move them to." + "You can't merge threads because there are no categories you are allowed to move them to." )}

diff --git a/frontend/src/components/username-history/list-empty.js b/frontend/src/components/username-history/list-empty.js index 0e80459e07..4a6f5e5387 100644 --- a/frontend/src/components/username-history/list-empty.js +++ b/frontend/src/components/username-history/list-empty.js @@ -7,7 +7,7 @@ export default class extends React.Component { } else { return pgettext( "username history empty", - "No name changes have been recorded for your account." + "Your account has no history of name changes." ) } } diff --git a/frontend/src/components/users/active-posters/list-empty.js b/frontend/src/components/users/active-posters/list-empty.js index 8ee4e11eea..a1f6cb7f48 100644 --- a/frontend/src/components/users/active-posters/list-empty.js +++ b/frontend/src/components/users/active-posters/list-empty.js @@ -5,9 +5,11 @@ import UsersNav from "../UsersNav" export default class extends React.Component { getEmptyMessage() { return interpolate( - pgettext( + npgettext( "top posters empty", - "No users have posted any new messages during last %(days)s days." + "No users have posted any new messages during last %(days)s day.", + "No users have posted any new messages during last %(days)s days.", + this.props.trackedPeriod ), { days: this.props.trackedPeriod }, true diff --git a/frontend/src/components/users/rank/RankUsersLeft.jsx b/frontend/src/components/users/rank/RankUsersLeft.jsx index f21bc4e5a0..acd07cb40d 100644 --- a/frontend/src/components/users/rank/RankUsersLeft.jsx +++ b/frontend/src/components/users/rank/RankUsersLeft.jsx @@ -7,8 +7,8 @@ const RankUsersLeft = ({ users }) => { {interpolate( npgettext( "rank users list", - "There is %(more)s more member with this role.", - "There are %(more)s more members with this role.", + "There is %(more)s more user with this rank.", + "There are %(more)s more users with this rank.", users.more ), { more: users.more }, @@ -22,7 +22,7 @@ const RankUsersLeft = ({ users }) => {

{pgettext( "rank users list empty", - "There are no more members with this role." + "There are no more users with this rank." )}

) diff --git a/frontend/src/services/ajax.js b/frontend/src/services/ajax.js index cbe792ce4a..c70dbec533 100644 --- a/frontend/src/services/ajax.js +++ b/frontend/src/services/ajax.js @@ -45,7 +45,7 @@ export class Ajax { if (rejection.status === 0) { rejection.detail = pgettext( "ajax client error", - "Could not connect to server." + "Could not connect to the site." ) } @@ -61,7 +61,7 @@ export class Ajax { if (rejection.status === 500 && !rejection.detail) { rejection.detail = pgettext( "ajax client error", - "Unknown error has occured." + "Unknown error has occurred." ) } @@ -227,22 +227,22 @@ export class Ajax { if (rejection.status === 0) { rejection.detail = pgettext( - "ajax client error", - "Could not connect to server." + "api error", + "Could not connect to the site." ) } if (rejection.status === 413 && !rejection.detail) { rejection.detail = pgettext( - "ajax client error", - "Upload was rejected by server as too large." + "api error", + "Upload was rejected by the site as too large." ) } if (rejection.status === 404) { if (!rejection.detail || rejection.detail === "NOT FOUND") { rejection.detail = pgettext( - "ajax client error", + "api error", "Action link is invalid." ) } @@ -250,7 +250,7 @@ export class Ajax { if (rejection.status === 500 && !rejection.detail) { rejection.detail = pgettext( - "ajax client error", + "api error", "Unknown error has occurred." ) } diff --git a/frontend/src/services/captcha.js b/frontend/src/services/captcha.js index c93e172de9..a9efe0fb49 100644 --- a/frontend/src/services/captcha.js +++ b/frontend/src/services/captcha.js @@ -128,7 +128,7 @@ export class ReCaptcha extends BaseCaptcha { validation={kwargs.form.state.errors.captcha} helpText={pgettext( "captcha field", - "This test helps us prevent automated spam registrations on our site." + "This test helps us prevent automated spam registrations on the site." )} > 1: title += " (%s)" % ( - pgettext("page number in title", "page: %(page)s") + pgettext("page title pagination", "page: %(page)s") % {"page": kwargs["page"]} ) diff --git a/misago/locale/en/LC_MESSAGES/django.po b/misago/locale/en/LC_MESSAGES/django.po index cb1bc9d5d8..ff0e661472 100644 --- a/misago/locale/en/LC_MESSAGES/django.po +++ b/misago/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-24 12:16+0000\n" +"POT-Creation-Date: 2023-11-06 19:19+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1643,8 +1643,10 @@ msgid "This action is not available to guests." msgstr "" #: core/templatetags/misago_pagetitle.py:11 +#: templates/misago/thread/private_thread.html:7 +#: templates/misago/thread/thread.html:7 #, python-format -msgctxt "page number in title" +msgctxt "page title pagination" msgid "page: %(page)s" msgstr "" @@ -2458,8 +2460,8 @@ msgstr "" #: templates/misago/activation/request.html:38 msgctxt "account activation page" msgid "" -"To receive this link, enter your account's e-mail address in the form and " -"press the \"Send link\" button." +"To receive this link, enter your account's e-mail address and press the " +"\"Send link\" button." msgstr "" #: templates/misago/activation/request.html:56 @@ -4252,36 +4254,43 @@ msgid "Create" msgstr "" #: templates/misago/admin/themes/assets/css.html:31 +msgctxt "admin theme assets" msgid "Name" msgstr "" #: templates/misago/admin/themes/assets/css.html:32 +msgctxt "admin theme assets" msgid "Modified" msgstr "" #: templates/misago/admin/themes/assets/css.html:33 +msgctxt "admin theme assets" msgid "Size" msgstr "" #: templates/misago/admin/themes/assets/css.html:68 +msgctxt "admin theme assets" msgid "Move down" msgstr "" #: templates/misago/admin/themes/assets/css.html:73 +msgctxt "admin theme assets" msgid "Move up" msgstr "" #: templates/misago/admin/themes/assets/css.html:80 #: templates/misago/admin/themes/assets/css.html:84 -#: users/admin/djangoadmin.py:72 +msgctxt "admin theme assets" msgid "Edit" msgstr "" #: templates/misago/admin/themes/assets/css.html:94 +msgctxt "admin theme assets" msgid "This theme has no CSS files." msgstr "" #: templates/misago/admin/themes/assets/css.html:103 +msgctxt "admin theme assets" msgid "Delete selected" msgstr "" @@ -4828,16 +4837,6 @@ msgid "Categories" msgstr "" #: templates/misago/categories/base.html:18 -#, python-format -msgid "" -"There is %(categories)s main category currenty available on the " -"%(forum_name)s." -msgid_plural "" -"There are %(categories)s main categories currenty available on the " -"%(forum_name)s." -msgstr[0] "" -msgstr[1] "" - #: templates/misago/categories/base.html:49 #: templates/misago/categories/base.html:62 #, python-format @@ -4878,17 +4877,17 @@ msgstr "" #: templates/misago/categories/last_thread.html:54 msgctxt "category last reply" -msgid "This category is empty. No threads were posted within it so far." +msgid "Empty category" msgstr "" #: templates/misago/categories/last_thread.html:68 msgctxt "category last reply" -msgid "This category is private. You can see only your own threads within it." +msgid "Private category" msgstr "" #: templates/misago/categories/last_thread.html:82 msgctxt "category last reply" -msgid "This category is protected. You can't browse it's contents." +msgid "Protected category" msgstr "" #: templates/misago/categories/stats.html:5 @@ -4911,7 +4910,7 @@ msgstr[1] "" #: templates/misago/emails/activation/by_admin.txt:6 #, python-format msgctxt "account activated email" -msgid "%(user)s, your account has been activated by forum administrator." +msgid "%(user)s, your account has been activated by site administrator." msgstr "" #: templates/misago/emails/activation/by_admin.html:12 @@ -4939,13 +4938,13 @@ msgstr "" #: templates/misago/emails/activation/by_user.html:11 msgctxt "account activation email cta link" -msgid "Activate my account!" +msgid "Activate my account" msgstr "" -#: templates/misago/emails/base.txt:12 +#: templates/misago/emails/base.html:73 templates/misago/emails/base.txt:12 #, python-format msgctxt "email footer" -msgid "Sent from %(settings.forum_address)s" +msgid "Sent from %(forum_host)s" msgstr "" #: templates/misago/emails/change_email.html:6 @@ -4965,7 +4964,7 @@ msgstr "" #: templates/misago/emails/change_email.html:16 msgctxt "confirm email change email cta" -msgid "Save changes" +msgid "Confirm change" msgstr "" #: templates/misago/emails/change_password.html:6 @@ -4985,7 +4984,7 @@ msgstr "" #: templates/misago/emails/change_password.html:16 msgctxt "confirm password change email cta" -msgid "Save changes" +msgid "Confirm change" msgstr "" #: templates/misago/emails/change_password_form_link.html:6 @@ -4994,13 +4993,13 @@ msgstr "" msgctxt "change forgotten password email" msgid "" "%(user)s, you are receiving this message because you want to change " -"forgotten password for your forum account." +"forgotten password to your account." msgstr "" #: templates/misago/emails/change_password_form_link.html:11 #: templates/misago/emails/change_password_form_link.txt:10 msgctxt "change forgotten password email" -msgid "To change your account password click the link below:" +msgid "To set new password to your account click the link below:" msgstr "" #: templates/misago/emails/change_password_form_link.html:16 @@ -5074,7 +5073,7 @@ msgstr "" #: templates/misago/emails/register/complete.txt:10 msgctxt "welcome email" msgid "" -"You may now join discussion on our forums. Why not spend a minute or two to " +"You may now join discussion on our site. Why not spend a minute or two to " "have a look around and share your opinions and knowledge with rest of " "community?" msgstr "" @@ -5099,16 +5098,14 @@ msgstr "" #: templates/misago/emails/register/inactive.txt:10 msgctxt "welcome email" msgid "" -"Before you will be able to join discussion on our forums, one of our " +"Before you will be able to join discussion on our site, one of " "administrators will have to activate your account." msgstr "" #: templates/misago/emails/register/inactive.html:12 #: templates/misago/emails/register/inactive.txt:15 msgctxt "welcome email" -msgid "" -"This may take a while, but you will receive e-mail with notification once it " -"happens." +msgid "You will receive an e-mail with notification once this happens." msgstr "" #: templates/misago/emails/register/inactive.html:17 @@ -5121,13 +5118,13 @@ msgstr "" #: templates/misago/emails/register/inactive.txt:25 msgctxt "welcome email" msgid "" -"Before you will be able to join discussion on our forums, you have to " -"activate your account. To do so, simply click the link below:" +"Before you will be able to join discussion on our site, you have to activate " +"your account. To do so, simply click the link below:" msgstr "" #: templates/misago/emails/register/inactive.html:27 msgctxt "welcome email activate link" -msgid "Activate my account!" +msgid "Activate my account" msgstr "" #: templates/misago/emails/register/inactive.html:31 @@ -5279,7 +5276,7 @@ msgstr "" #: templates/misago/errorpages/csrf_failure.html:9 msgctxt "error csrf page" msgid "" -"This is usually caused by your browser not accepting or using outdated " +"This error is usually caused by your browser not accepting or using outdated " "cookies. Check your browser configuration and try again." msgstr "" @@ -5289,10 +5286,11 @@ msgid "Suspicious request blocked." msgstr "" #: templates/misago/errorpages/csrf_failure.html:24 -msgctxt "error csrf page" +#: templates/misago/errorpages/csrf_failure_authenticated.html:19 +msgctxt "error csrf authenticated page" msgid "" -"This is usually caused by your browser not accepting or using outdated " -"cookies." +"This error is usually caused by the used web browser not accepting or using " +"outdated cookies." msgstr "" #: templates/misago/errorpages/csrf_failure.html:25 @@ -5310,13 +5308,6 @@ msgctxt "error csrf authenticated page" msgid "Suspicious request blocked." msgstr "" -#: templates/misago/errorpages/csrf_failure_authenticated.html:19 -msgctxt "error csrf authenticated page" -msgid "" -"This is usually caused by your browser not accepting or using outdated " -"cookies." -msgstr "" - #: templates/misago/errorpages/csrf_failure_authenticated.html:20 msgctxt "error csrf authenticated page" msgid "Check your browser configuration and try again." @@ -5468,8 +5459,8 @@ msgstr "" #: templates/misago/forgottenpassword/request.html:50 msgctxt "forgotten password page" msgid "" -"To receive this link, enter your account's e-mail addres in form and press " -"the \"Send link\" button." +"To receive this link, enter your account's e-mail address and press the " +"\"Send link\" button." msgstr "" #: templates/misago/forgottenpassword/request.html:68 @@ -5531,7 +5522,7 @@ msgstr "" #: templates/misago/notifications_disabled.html:5 #: templates/misago/notifications_disabled.html:18 msgctxt "notifications disabled page" -msgid "E-mail notifications disabled page" +msgid "E-mail notifications disabled" msgstr "" #: templates/misago/notifications_disabled.html:19 @@ -5544,8 +5535,8 @@ msgstr "" #: templates/misago/notifications_disabled.html:20 msgctxt "notifications disabled page" msgid "" -"If you change your mind and want to re-enable e-mail notifications, visit " -"the thread's page and adjust your notification settings." +"If you change your mind and want to re-enable e-mail notifications, you can " +"do so by adjusting your notification settings on thread's page." msgstr "" #: templates/misago/notifications_disabled.html:23 @@ -5555,7 +5546,7 @@ msgstr "" #: templates/misago/notifications_disabled.html:26 msgctxt "notifications disabled page" -msgid "Go to forum home" +msgid "Go to home page" msgstr "" #: templates/misago/options/credentials_changed.html:5 @@ -5583,7 +5574,7 @@ msgstr "" #: templates/misago/options/credentials_error.html:22 msgctxt "credentials changed page cta" -msgid "Request another activation link." +msgid "Request another confirmation link." msgstr "" #: templates/misago/options/noscript.html:5 @@ -5631,12 +5622,12 @@ msgstr[1] "" #: templates/misago/poll/info.html:15 #, python-format msgctxt "thread poll" -msgid "Voting ends on %(ends_on)s." +msgid "Voting ends %(ends_on)s." msgstr "" #: templates/misago/poll/info.html:22 msgctxt "thread poll" -msgid "Votes are public." +msgid "Voting is public." msgstr "" #: templates/misago/poll/info.html:36 @@ -5793,12 +5784,14 @@ msgid "See post" msgstr "" #: templates/misago/profile/feed.html:62 -msgctxt "user profile page feed item" +#: templates/misago/thread/posts/post/body.html:10 +msgctxt "post body invalid" msgid "This post's contents cannot be displayed." msgstr "" #: templates/misago/profile/feed.html:63 -msgctxt "user profile page feed item" +#: templates/misago/thread/posts/post/body.html:11 +msgctxt "post body invalid" msgid "This error is caused by invalid post content manipulation." msgstr "" @@ -5954,13 +5947,13 @@ msgstr[1] "" #: templates/misago/profile/threads.html:42 msgctxt "user profile page threads" -msgid "You have no started threads." +msgid "You haven't started any threads." msgstr "" #: templates/misago/profile/threads.html:44 #, python-format msgctxt "user profile page threads" -msgid "%(username)s started no threads." +msgid "%(username)s hasn't started any threads" msgstr "" #: templates/misago/profile/username_history.html:5 @@ -5987,8 +5980,8 @@ msgstr[0] "" msgstr[1] "" #: templates/misago/profile/username_history.html:83 -msgctxt "user profile page username history" -msgid "Your username was never changed." +msgctxt "username history empty" +msgid "Your account has no history of name changes." msgstr "" #: templates/misago/profile/username_history.html:85 @@ -6000,7 +5993,7 @@ msgstr "" #: templates/misago/required_agreement.html:9 #, python-format msgctxt "agreement overlay" -msgid "Please review the updated %(agreement)s:" +msgid "%(agreement)s has been updated:" msgstr "" #: templates/misago/required_agreement.html:19 @@ -6057,7 +6050,7 @@ msgstr "" #: templates/misago/thread/header.html:38 #, python-format msgctxt "thread page header" -msgid "Started on %(date)s" +msgid "Started on: %(timestamp)s" msgstr "" #: templates/misago/thread/notifications.html:4 @@ -6119,7 +6112,7 @@ msgstr "" #: templates/misago/thread/posts/event/index.html:58 msgctxt "thread event" -msgid "Thread has been pinned locally." +msgid "Thread has been pinned in category." msgstr "" #: templates/misago/thread/posts/event/index.html:60 @@ -6217,7 +6210,7 @@ msgstr "" #: templates/misago/thread/posts/post/body-hidden.html:4 msgctxt "post body hidden" -msgid "This post is hidden. You cannot not see its contents." +msgid "This post is hidden. You cannot see its contents." msgstr "" #: templates/misago/thread/posts/post/body-hidden.html:14 @@ -6226,16 +6219,6 @@ msgctxt "post body hidden" msgid "Hidden by %(hidden_by)s on %(hidden_on)s." msgstr "" -#: templates/misago/thread/posts/post/body.html:10 -msgctxt "post body invalid" -msgid "This post's contents cannot be displayed." -msgstr "" - -#: templates/misago/thread/posts/post/body.html:11 -msgctxt "post body invalid" -msgid "This error is caused by invalid post content manipulation." -msgstr "" - #: templates/misago/thread/posts/post/flags.html:7 #, python-format msgctxt "post best answer flag" @@ -6320,27 +6303,17 @@ msgctxt "post removed poster username" msgid "Removed user" msgstr "" -#: templates/misago/thread/private_thread.html:7 -#, python-format -msgctxt "thread page title" -msgid "page: %(page)s" -msgstr "" - -#: templates/misago/thread/thread.html:7 -#, python-format -msgctxt "private thread page title" -msgid "page: %(page)s" -msgstr "" - #: templates/misago/thread/thread.html:19 #: templates/misago/thread/thread.html:30 #, python-format +msgctxt "thread page meta" msgid "Started by %(starter)s on %(started_on)s in the %(category)s category." msgstr "" #: templates/misago/thread/thread.html:21 #: templates/misago/thread/thread.html:32 #, python-format +msgctxt "thread page meta" msgid "%(replies)s reply, last one from %(last_post_on)s." msgid_plural "%(replies)s replies, last one from %(last_post_on)s." msgstr[0] "" @@ -6348,11 +6321,13 @@ msgstr[1] "" #: templates/misago/thread/thread.html:25 #: templates/misago/thread/thread.html:36 +msgctxt "thread page meta" msgid "Answered." msgstr "" #: templates/misago/thread/thread.html:25 #: templates/misago/thread/thread.html:36 +msgctxt "thread page meta" msgid "Closed." msgstr "" @@ -6368,6 +6343,8 @@ msgid "Go to top" msgstr "" #: templates/misago/thread/toolbar_top.html:6 +#: templates/misago/thread/toolbar_top.html:32 +msgctxt "thread page options" msgid "Shortcuts" msgstr "" @@ -6377,11 +6354,6 @@ msgctxt "thread page options" msgid "Add poll" msgstr "" -#: templates/misago/thread/toolbar_top.html:32 -msgctxt "thread page options" -msgid "Shortcuts" -msgstr "" - #: templates/misago/thread_flags.html:7 msgctxt "thread flag" msgid "Pinned globally" @@ -6421,26 +6393,21 @@ msgstr[0] "" msgstr[1] "" #: templates/misago/threadslist/base.html:53 -msgctxt "threads list" +msgctxt "threads list empty" msgid "There are no threads in this category." msgstr "" #: templates/misago/threadslist/base.html:55 -msgctxt "threads list" -msgid "There are no threads on this forum... yet!" -msgstr "" - -#: templates/misago/threadslist/base.html:59 -msgctxt "threads list" -msgid "Why not start one yourself?" +msgctxt "threads list empty" +msgid "There are no threads on this site yet." msgstr "" -#: templates/misago/threadslist/base.html:63 -msgctxt "threads list" +#: templates/misago/threadslist/base.html:60 +msgctxt "threads list empty" msgid "No threads matching specified criteria were found." msgstr "" -#: templates/misago/threadslist/base.html:78 +#: templates/misago/threadslist/base.html:75 msgctxt "threads list paginator" msgid "Next page" msgstr "" @@ -6459,11 +6426,6 @@ msgstr "" #: templates/misago/threadslist/private_threads.html:62 msgctxt "private threads page" -msgid "Why not start one yourself?" -msgstr "" - -#: templates/misago/threadslist/private_threads.html:65 -msgctxt "private threads page" msgid "No threads matching specified criteria were found." msgstr "" @@ -6543,13 +6505,15 @@ msgid "Unread threads" msgstr "" #: templates/misago/threadslist/toolbar.html:63 +#: threads/viewmodels/threads.py:26 msgctxt "threads list" -msgid "Subscribed threads" +msgid "Watched threads" msgstr "" #: templates/misago/threadslist/toolbar.html:65 +#: threads/viewmodels/threads.py:27 msgctxt "threads list" -msgid "Unapproved threads" +msgid "Unapproved content" msgstr "" #: templates/misago/threadslist/toolbar.html:77 @@ -6571,21 +6535,24 @@ msgstr[0] "" msgstr[1] "" #: templates/misago/userslists/active_posters.html:16 -#: templates/misago/userslists/active_posters.html:35 -#: templates/misago/userslists/active_posters.html:50 +#: templates/misago/userslists/active_posters.html:37 +#: templates/misago/userslists/active_posters.html:54 +#: templates/misago/userslists/active_posters.html:151 #, python-format msgctxt "active posters page meta" -msgid "No users have posted any new messages during last %(days)s days." -msgstr "" +msgid "No users have posted any new messages during last %(days)s day." +msgid_plural "No users have posted any new messages during last %(days)s days." +msgstr[0] "" +msgstr[1] "" -#: templates/misago/userslists/active_posters.html:23 -#: templates/misago/userslists/active_posters.html:24 +#: templates/misago/userslists/active_posters.html:25 +#: templates/misago/userslists/active_posters.html:26 msgctxt "active posters page meta" msgid "Top posters" msgstr "" -#: templates/misago/userslists/active_posters.html:29 -#: templates/misago/userslists/active_posters.html:44 +#: templates/misago/userslists/active_posters.html:31 +#: templates/misago/userslists/active_posters.html:48 #, python-format msgctxt "active posters page meta" msgid "%(posters)s top poster from last %(days)s days." @@ -6593,7 +6560,7 @@ msgid_plural "%(posters)s top posters from last %(days)s days." msgstr[0] "" msgstr[1] "" -#: templates/misago/userslists/active_posters.html:69 +#: templates/misago/userslists/active_posters.html:75 #, python-format msgctxt "active posters page" msgid "%(posters)s top poster from last %(days)s days." @@ -6601,34 +6568,28 @@ msgid_plural "%(posters)s top posters from last %(days)s days." msgstr[0] "" msgstr[1] "" -#: templates/misago/userslists/active_posters.html:83 +#: templates/misago/userslists/active_posters.html:89 msgctxt "active posters list item" msgid "Avatar" msgstr "" -#: templates/misago/userslists/active_posters.html:112 -#: templates/misago/userslists/active_posters.html:123 +#: templates/misago/userslists/active_posters.html:118 +#: templates/misago/userslists/active_posters.html:129 msgctxt "active posters list item" msgid "Rank" msgstr "" -#: templates/misago/userslists/active_posters.html:116 -#: templates/misago/userslists/active_posters.html:128 +#: templates/misago/userslists/active_posters.html:122 +#: templates/misago/userslists/active_posters.html:134 msgctxt "active posters list item" msgid "Ranked posts" msgstr "" -#: templates/misago/userslists/active_posters.html:133 +#: templates/misago/userslists/active_posters.html:139 msgctxt "active posters list item" msgid "Total posts" msgstr "" -#: templates/misago/userslists/active_posters.html:145 -#, python-format -msgctxt "active posters page" -msgid "No users have posted any new messages during last %(days)s days." -msgstr "" - #: templates/misago/userslists/base.html:5 #: templates/misago/userslists/base.html:18 msgctxt "users page" @@ -6648,14 +6609,14 @@ msgstr[1] "" #: templates/misago/userslists/rank.html:102 #, python-format msgctxt "rank users page" -msgid "There is %(more)s more members with this role." -msgid_plural "There are %(more)s more members with this role." +msgid "There is %(more)s more users with this rank." +msgid_plural "There are %(more)s more users with this rank." msgstr[0] "" msgstr[1] "" #: templates/misago/userslists/rank.html:108 msgctxt "rank users page" -msgid "There are no more members with this role." +msgid "There are no more users with this rank." msgstr "" #: templates/misago/userslists/rank.html:117 @@ -7276,7 +7237,7 @@ msgstr "" #: threads/api/attachments.py:56 msgctxt "attachments api" -msgid "Uploaded image was corrupted or invalid." +msgid "Uploaded image is unsupported or invalid." msgstr "" #: threads/api/attachments.py:97 @@ -7293,7 +7254,7 @@ msgstr "" #: threads/api/postendpoints/edits.py:88 msgctxt "posts api" -msgid "Edits record is unavailable for this post." +msgid "This post has no changes history." msgstr "" #: threads/api/postendpoints/merge.py:12 @@ -7303,7 +7264,7 @@ msgstr "" #: threads/api/postendpoints/move.py:11 msgctxt "posts api" -msgid "You can't move posts in this thread." +msgid "You can't move posts from this thread." msgstr "" #: threads/api/postendpoints/patch_post.py:37 @@ -7534,7 +7495,7 @@ msgstr "" #: threads/api/threadposts.py:220 msgctxt "posts api" -msgid "You can't reply to events." +msgid "Events can't be replied to." msgstr "" #: threads/api/threadposts.py:223 @@ -7928,7 +7889,7 @@ msgstr "" #: threads/permissions/polls.py:61 msgctxt "polls permission" -msgid "Time limit for own polls edits, in minutes" +msgid "Time limit for editing own polls, in minutes" msgstr "" #: threads/permissions/polls.py:64 @@ -7943,7 +7904,7 @@ msgstr "" #: threads/permissions/polls.py:73 msgctxt "polls permission" -msgid "Allows users to see who voted in poll even if poll votes are secret." +msgid "Allows users to see who voted in poll even if voting was not public." msgstr "" #: threads/permissions/polls.py:129 @@ -8072,9 +8033,9 @@ msgctxt "polls permission" msgid "This thread is closed. You can't vote in it." msgstr "" -#: threads/permissions/polls.py:327 +#: threads/permissions/polls.py:328 msgctxt "polls permission" -msgid "You dont have permission to this poll's voters." +msgid "You dont have permission to see this poll's voters." msgstr "" #: threads/permissions/privatethreads.py:33 @@ -8148,12 +8109,12 @@ msgstr "" #: threads/permissions/privatethreads.py:239 msgctxt "private threads permission" -msgid "Only thread owner and moderators can change threads owners." +msgid "Only thread owner and moderators can appoint a new thread owner." msgstr "" #: threads/permissions/privatethreads.py:247 msgctxt "private threads permission" -msgid "Only moderators can change closed threads owners." +msgid "Only moderators can appoint a new thread owner in a closed thread." msgstr "" #: threads/permissions/privatethreads.py:263 @@ -8196,7 +8157,7 @@ msgstr "" #: threads/permissions/privatethreads.py:342 #, python-format msgctxt "private threads permission" -msgid "%(user)s is not allowing invitations to private threads." +msgid "%(user)s has disabled invitations to private threads." msgstr "" #: threads/permissions/privatethreads.py:352 @@ -8321,7 +8282,7 @@ msgstr "" #: threads/permissions/threads.py:143 msgctxt "threads permission" -msgid "Time limit for own threads edits, in minutes" +msgid "Time limit for editing own threads, in minutes" msgstr "" #: threads/permissions/threads.py:146 @@ -8361,7 +8322,7 @@ msgstr "" #: threads/permissions/threads.py:168 msgctxt "threads pin permission choice" -msgid "Locally" +msgid "In category" msgstr "" #: threads/permissions/threads.py:169 @@ -8431,7 +8392,7 @@ msgstr "" #: threads/permissions/threads.py:216 msgctxt "threads permission" -msgid "Time limit for own post edits, in minutes" +msgid "Time limit for editing own post, in minutes" msgstr "" #: threads/permissions/threads.py:219 @@ -8885,42 +8846,43 @@ msgid_plural "You can't reveal posts that are older than %(minutes)s minutes." msgstr[0] "" msgstr[1] "" -#: threads/permissions/threads.py:1226 +#: threads/permissions/threads.py:1228 msgctxt "threads permission" -msgid "You can't reveal thread's first post." +msgid "" +"Thread's first post can only be revealed using the reveal thread option." msgstr "" -#: threads/permissions/threads.py:1234 +#: threads/permissions/threads.py:1237 msgctxt "threads permission" msgid "This category is closed. You can't reveal posts in it." msgstr "" -#: threads/permissions/threads.py:1241 +#: threads/permissions/threads.py:1244 msgctxt "threads permission" msgid "This thread is closed. You can't reveal posts in it." msgstr "" -#: threads/permissions/threads.py:1252 +#: threads/permissions/threads.py:1255 msgctxt "threads permission" msgid "You have to sign in to hide posts." msgstr "" -#: threads/permissions/threads.py:1263 +#: threads/permissions/threads.py:1266 msgctxt "threads permission" msgid "You can't hide posts in this category." msgstr "" -#: threads/permissions/threads.py:1271 +#: threads/permissions/threads.py:1274 msgctxt "threads permission" msgid "You can't hide other users posts in this category." msgstr "" -#: threads/permissions/threads.py:1278 +#: threads/permissions/threads.py:1281 msgctxt "threads permission" msgid "This post is protected. You can't hide it." msgstr "" -#: threads/permissions/threads.py:1285 +#: threads/permissions/threads.py:1288 #, python-format msgctxt "threads permission" msgid "You can't hide posts that are older than %(minutes)s minute." @@ -8928,42 +8890,42 @@ msgid_plural "You can't hide posts that are older than %(minutes)s minutes." msgstr[0] "" msgstr[1] "" -#: threads/permissions/threads.py:1295 +#: threads/permissions/threads.py:1300 msgctxt "threads permission" -msgid "You can't hide thread's first post." +msgid "Thread's first post can only be hidden using the hide thread option." msgstr "" -#: threads/permissions/threads.py:1303 +#: threads/permissions/threads.py:1309 msgctxt "threads permission" msgid "This category is closed. You can't hide posts in it." msgstr "" -#: threads/permissions/threads.py:1310 +#: threads/permissions/threads.py:1316 msgctxt "threads permission" msgid "This thread is closed. You can't hide posts in it." msgstr "" -#: threads/permissions/threads.py:1321 +#: threads/permissions/threads.py:1327 msgctxt "threads permission" msgid "You have to sign in to delete posts." msgstr "" -#: threads/permissions/threads.py:1332 +#: threads/permissions/threads.py:1338 msgctxt "threads permission" msgid "You can't delete posts in this category." msgstr "" -#: threads/permissions/threads.py:1340 +#: threads/permissions/threads.py:1346 msgctxt "threads permission" msgid "You can't delete other users posts in this category." msgstr "" -#: threads/permissions/threads.py:1347 +#: threads/permissions/threads.py:1353 msgctxt "threads permission" msgid "This post is protected. You can't delete it." msgstr "" -#: threads/permissions/threads.py:1354 +#: threads/permissions/threads.py:1360 #, python-format msgctxt "threads permission" msgid "You can't delete posts that are older than %(minutes)s minute." @@ -8971,222 +8933,222 @@ msgid_plural "You can't delete posts that are older than %(minutes)s minutes." msgstr[0] "" msgstr[1] "" -#: threads/permissions/threads.py:1364 +#: threads/permissions/threads.py:1372 msgctxt "threads permission" -msgid "You can't delete thread's first post." +msgid "Thread's first post can only be deleted together with thread." msgstr "" -#: threads/permissions/threads.py:1372 +#: threads/permissions/threads.py:1381 msgctxt "threads permission" msgid "This category is closed. You can't delete posts in it." msgstr "" -#: threads/permissions/threads.py:1379 +#: threads/permissions/threads.py:1388 msgctxt "threads permission" msgid "This thread is closed. You can't delete posts in it." msgstr "" -#: threads/permissions/threads.py:1390 +#: threads/permissions/threads.py:1399 msgctxt "threads permission" msgid "You have to sign in to protect posts." msgstr "" -#: threads/permissions/threads.py:1400 +#: threads/permissions/threads.py:1409 msgctxt "threads permission" msgid "You can't protect posts in this category." msgstr "" -#: threads/permissions/threads.py:1406 +#: threads/permissions/threads.py:1415 msgctxt "threads permission" msgid "You can't protect posts you can't edit." msgstr "" -#: threads/permissions/threads.py:1417 +#: threads/permissions/threads.py:1426 msgctxt "threads permission" msgid "You have to sign in to approve posts." msgstr "" -#: threads/permissions/threads.py:1427 +#: threads/permissions/threads.py:1436 msgctxt "threads permission" msgid "You can't approve posts in this category." msgstr "" -#: threads/permissions/threads.py:1433 +#: threads/permissions/threads.py:1443 msgctxt "threads permission" -msgid "You can't approve thread's first post." +msgid "Thread's first post can only be approved together with thread." msgstr "" -#: threads/permissions/threads.py:1444 +#: threads/permissions/threads.py:1454 msgctxt "threads permission" msgid "You can't approve posts the content you can't see." msgstr "" -#: threads/permissions/threads.py:1453 +#: threads/permissions/threads.py:1463 msgctxt "threads permission" msgid "This category is closed. You can't approve posts in it." msgstr "" -#: threads/permissions/threads.py:1460 +#: threads/permissions/threads.py:1470 msgctxt "threads permission" msgid "This thread is closed. You can't approve posts in it." msgstr "" -#: threads/permissions/threads.py:1471 +#: threads/permissions/threads.py:1481 msgctxt "threads permission" msgid "You have to sign in to move posts." msgstr "" -#: threads/permissions/threads.py:1481 +#: threads/permissions/threads.py:1491 msgctxt "threads permission" msgid "You can't move posts in this category." msgstr "" -#: threads/permissions/threads.py:1486 +#: threads/permissions/threads.py:1496 msgctxt "threads permission" msgid "Events can't be moved." msgstr "" -#: threads/permissions/threads.py:1490 +#: threads/permissions/threads.py:1502 msgctxt "threads permission" -msgid "You can't move thread's first post." +msgid "Thread's first post can only be moved together with thread." msgstr "" -#: threads/permissions/threads.py:1495 +#: threads/permissions/threads.py:1508 msgctxt "threads permission" msgid "You can't move posts the content you can't see." msgstr "" -#: threads/permissions/threads.py:1504 +#: threads/permissions/threads.py:1517 msgctxt "threads permission" msgid "This category is closed. You can't move posts in it." msgstr "" -#: threads/permissions/threads.py:1511 +#: threads/permissions/threads.py:1524 msgctxt "threads permission" msgid "This thread is closed. You can't move posts in it." msgstr "" -#: threads/permissions/threads.py:1522 +#: threads/permissions/threads.py:1535 msgctxt "threads permission" msgid "You have to sign in to merge posts." msgstr "" -#: threads/permissions/threads.py:1532 +#: threads/permissions/threads.py:1545 msgctxt "threads permission" msgid "You can't merge posts in this category." msgstr "" -#: threads/permissions/threads.py:1537 +#: threads/permissions/threads.py:1550 msgctxt "threads permission" msgid "Events can't be merged." msgstr "" -#: threads/permissions/threads.py:1546 +#: threads/permissions/threads.py:1559 msgctxt "threads permission" msgid "You can't merge posts the content you can't see." msgstr "" -#: threads/permissions/threads.py:1555 +#: threads/permissions/threads.py:1568 msgctxt "threads permission" msgid "This category is closed. You can't merge posts in it." msgstr "" -#: threads/permissions/threads.py:1562 +#: threads/permissions/threads.py:1575 msgctxt "threads permission" msgid "This thread is closed. You can't merge posts in it." msgstr "" -#: threads/permissions/threads.py:1573 +#: threads/permissions/threads.py:1586 msgctxt "threads permission" msgid "You have to sign in to split posts." msgstr "" -#: threads/permissions/threads.py:1583 +#: threads/permissions/threads.py:1596 msgctxt "threads permission" msgid "You can't split posts in this category." msgstr "" -#: threads/permissions/threads.py:1588 +#: threads/permissions/threads.py:1601 msgctxt "threads permission" msgid "Events can't be split." msgstr "" -#: threads/permissions/threads.py:1592 +#: threads/permissions/threads.py:1605 msgctxt "threads permission" -msgid "You can't split thread's first post." +msgid "Thread's first post can't be split." msgstr "" -#: threads/permissions/threads.py:1597 +#: threads/permissions/threads.py:1610 msgctxt "threads permission" msgid "You can't split posts the content you can't see." msgstr "" -#: threads/permissions/threads.py:1606 +#: threads/permissions/threads.py:1619 msgctxt "threads permission" msgid "This category is closed. You can't split posts in it." msgstr "" -#: threads/permissions/threads.py:1613 +#: threads/permissions/threads.py:1626 msgctxt "threads permission" msgid "This thread is closed. You can't split posts in it." msgstr "" -#: threads/permissions/threads.py:1624 +#: threads/permissions/threads.py:1637 msgctxt "threads permission" msgid "You have to sign in to reveal events." msgstr "" -#: threads/permissions/threads.py:1634 +#: threads/permissions/threads.py:1647 msgctxt "threads permission" msgid "You can't reveal events in this category." msgstr "" -#: threads/permissions/threads.py:1643 +#: threads/permissions/threads.py:1656 msgctxt "threads permission" msgid "This category is closed. You can't reveal events in it." msgstr "" -#: threads/permissions/threads.py:1650 +#: threads/permissions/threads.py:1663 msgctxt "threads permission" msgid "This thread is closed. You can't reveal events in it." msgstr "" -#: threads/permissions/threads.py:1661 +#: threads/permissions/threads.py:1674 msgctxt "threads permission" msgid "You have to sign in to hide events." msgstr "" -#: threads/permissions/threads.py:1671 +#: threads/permissions/threads.py:1684 msgctxt "threads permission" msgid "You can't hide events in this category." msgstr "" -#: threads/permissions/threads.py:1680 +#: threads/permissions/threads.py:1693 msgctxt "threads permission" msgid "This category is closed. You can't hide events in it." msgstr "" -#: threads/permissions/threads.py:1687 +#: threads/permissions/threads.py:1700 msgctxt "threads permission" msgid "This thread is closed. You can't hide events in it." msgstr "" -#: threads/permissions/threads.py:1698 +#: threads/permissions/threads.py:1711 msgctxt "threads permission" msgid "You have to sign in to delete events." msgstr "" -#: threads/permissions/threads.py:1708 +#: threads/permissions/threads.py:1721 msgctxt "threads permission" msgid "You can't delete events in this category." msgstr "" -#: threads/permissions/threads.py:1717 +#: threads/permissions/threads.py:1730 msgctxt "threads permission" msgid "This category is closed. You can't delete events in it." msgstr "" -#: threads/permissions/threads.py:1724 +#: threads/permissions/threads.py:1737 msgctxt "threads permission" msgid "This thread is closed. You can't delete events in it." msgstr "" @@ -9201,8 +9163,9 @@ msgctxt "delete posts serializer" msgid "You have to specify at least one post to delete." msgstr "" -#: threads/serializers/moderation.py:57 -msgctxt "delete posts serializer" +#: threads/serializers/moderation.py:57 threads/serializers/moderation.py:121 +#: threads/serializers/moderation.py:232 threads/serializers/moderation.py:417 +msgctxt "invalid posts ids" msgid "One or more post ids received were invalid." msgstr "" @@ -9224,11 +9187,6 @@ msgctxt "merge posts serializer" msgid "You have to select at least two posts to merge." msgstr "" -#: threads/serializers/moderation.py:121 -msgctxt "merge posts serializer" -msgid "One or more post ids received were invalid." -msgstr "" - #: threads/serializers/moderation.py:140 #, python-format msgctxt "merge posts serializer" @@ -9267,13 +9225,8 @@ msgctxt "move posts serializer" msgid "Enter link to new thread." msgstr "" -#: threads/serializers/moderation.py:232 -msgctxt "move posts serializer" -msgid "One or more post ids received were invalid." -msgstr "" - -#: threads/serializers/moderation.py:251 -msgctxt "move posts serializer" +#: threads/serializers/moderation.py:251 threads/serializers/moderation.py:568 +msgctxt "invalid thread url" msgid "This is not a valid thread link." msgstr "" @@ -9337,11 +9290,6 @@ msgctxt "split posts serializer" msgid "You have to specify at least one post to split." msgstr "" -#: threads/serializers/moderation.py:417 -msgctxt "split posts serializer" -msgid "One or more post ids received were invalid." -msgstr "" - #: threads/serializers/moderation.py:433 #, python-format msgctxt "split posts serializer" @@ -9360,8 +9308,8 @@ msgctxt "delete threads serializer" msgid "You have to specify at least one thread to delete." msgstr "" -#: threads/serializers/moderation.py:482 -msgctxt "delete threads serializer" +#: threads/serializers/moderation.py:482 threads/serializers/moderation.py:624 +msgctxt "invalid threads ids" msgid "One or more thread ids received were invalid." msgstr "" @@ -9384,15 +9332,11 @@ msgid "Enter link to new thread." msgstr "" #: threads/serializers/moderation.py:550 threads/serializers/moderation.py:556 -msgctxt "merge thread serializer" +#: threads/serializers/moderation.py:638 threads/serializers/moderation.py:644 +msgctxt "merge threads serializer" msgid "Invalid choice." msgstr "" -#: threads/serializers/moderation.py:568 -msgctxt "merge thread serializer" -msgid "This is not a valid thread link." -msgstr "" - #: threads/serializers/moderation.py:573 msgctxt "merge thread serializer" msgid "You can't merge thread with itself." @@ -9415,16 +9359,6 @@ msgctxt "merge threads serializer" msgid "You have to select at least two threads to merge." msgstr "" -#: threads/serializers/moderation.py:624 -msgctxt "merge threads serializer" -msgid "One or more thread ids received were invalid." -msgstr "" - -#: threads/serializers/moderation.py:638 threads/serializers/moderation.py:644 -msgctxt "merge threads serializer" -msgid "Invalid choice." -msgstr "" - #: threads/serializers/moderation.py:653 #, python-format msgctxt "merge threads serializer" @@ -9622,16 +9556,6 @@ msgctxt "threads list" msgid "Your threads" msgstr "" -#: threads/viewmodels/threads.py:26 -msgctxt "threads list" -msgid "Watched threads" -msgstr "" - -#: threads/viewmodels/threads.py:27 -msgctxt "threads list" -msgid "Unapproved content" -msgstr "" - #: threads/viewmodels/threads.py:33 msgctxt "threads list" msgid "You have to sign in to see list of threads that you have started." @@ -9692,6 +9616,10 @@ msgstr "" msgid "Edit permissions and groups" msgstr "" +#: users/admin/djangoadmin.py:72 +msgid "Edit" +msgstr "" + #: users/admin/djangoadmin.py:76 msgid "Edit the user from Misago admin panel" msgstr "" @@ -9888,9 +9816,9 @@ msgctxt "admin user form" msgid "Notify about new private thread invitations from other users" msgstr "" -#: users/admin/forms.py:287 +#: users/admin/forms.py:287 users/serializers/moderation.py:37 #, python-format -msgctxt "admin user form" +msgctxt "signature length validator" msgid "Signature can't be longer than %(limit)s character." msgid_plural "Signature can't be longer than %(limit)s characters." msgstr[0] "" @@ -10041,87 +9969,87 @@ msgid "" "others through dedicated page on forum users list." msgstr "" -#: users/admin/forms.py:543 +#: users/admin/forms.py:544 msgctxt "admin rank form" msgid "There's already an other rank with this name." msgstr "" -#: users/admin/forms.py:552 +#: users/admin/forms.py:553 msgctxt "admin ban users form" msgid "Values to ban" msgstr "" -#: users/admin/forms.py:557 +#: users/admin/forms.py:558 msgctxt "admin ban users form" msgid "User message" msgstr "" -#: users/admin/forms.py:562 +#: users/admin/forms.py:563 msgctxt "admin ban users form" msgid "Optional message displayed to users instead of default one." msgstr "" -#: users/admin/forms.py:567 users/admin/forms.py:582 +#: users/admin/forms.py:568 users/admin/forms.py:583 msgctxt "admin ban users form" msgid "Message can't be longer than 1000 characters." msgstr "" -#: users/admin/forms.py:572 +#: users/admin/forms.py:573 msgctxt "admin ban users form" msgid "Team message" msgstr "" -#: users/admin/forms.py:577 +#: users/admin/forms.py:578 msgctxt "admin ban users form" msgid "Optional ban message for moderators and administrators." msgstr "" -#: users/admin/forms.py:587 +#: users/admin/forms.py:588 msgctxt "admin ban users form" msgid "Expiration date" msgstr "" -#: users/admin/forms.py:596 +#: users/admin/forms.py:597 msgctxt "admin ban users form" msgid "Usernames" msgstr "" -#: users/admin/forms.py:597 +#: users/admin/forms.py:598 msgctxt "admin ban users form" msgid "E-mails" msgstr "" -#: users/admin/forms.py:598 +#: users/admin/forms.py:599 msgctxt "admin ban users form" msgid "E-mail domains" msgstr "" -#: users/admin/forms.py:604 +#: users/admin/forms.py:605 msgctxt "admin ban users form" msgid "IP addresses" msgstr "" -#: users/admin/forms.py:608 +#: users/admin/forms.py:609 msgctxt "admin ban users form" msgid "First segment of IP addresses" msgstr "" -#: users/admin/forms.py:614 +#: users/admin/forms.py:615 msgctxt "admin ban users form" msgid "First two segments of IP addresses" msgstr "" -#: users/admin/forms.py:622 +#: users/admin/forms.py:623 msgctxt "admin ban form" msgid "Check type" msgstr "" -#: users/admin/forms.py:627 +#: users/admin/forms.py:628 msgctxt "admin ban form" msgid "Restrict this ban to registrations" msgstr "" -#: users/admin/forms.py:630 +#: users/admin/forms.py:631 msgctxt "admin ban form" msgid "" "Changing this to yes will make this ban check be only performed on " @@ -10130,12 +10058,12 @@ msgid "" "existing users." msgstr "" -#: users/admin/forms.py:634 +#: users/admin/forms.py:635 msgctxt "admin ban form" msgid "Banned value" msgstr "" -#: users/admin/forms.py:638 +#: users/admin/forms.py:639 msgctxt "admin ban form" msgid "" "This value is case-insensitive and accepts asterisk (*) for partial matches. " @@ -10143,122 +10071,122 @@ msgid "" "beginning with \"83.\"." msgstr "" -#: users/admin/forms.py:642 +#: users/admin/forms.py:643 msgctxt "admin ban form" msgid "Banned value can't be longer than 250 characters." msgstr "" -#: users/admin/forms.py:647 +#: users/admin/forms.py:648 msgctxt "admin ban form" msgid "User message" msgstr "" -#: users/admin/forms.py:652 +#: users/admin/forms.py:653 msgctxt "admin ban form" msgid "Optional message displayed to user instead of default one." msgstr "" -#: users/admin/forms.py:657 users/admin/forms.py:671 +#: users/admin/forms.py:658 users/admin/forms.py:672 msgctxt "admin ban form" msgid "Message can't be longer than 1000 characters." msgstr "" -#: users/admin/forms.py:662 +#: users/admin/forms.py:663 msgctxt "admin ban form" msgid "Team message" msgstr "" -#: users/admin/forms.py:666 +#: users/admin/forms.py:667 msgctxt "admin ban form" msgid "Optional ban message for moderators and administrators." msgstr "" -#: users/admin/forms.py:676 +#: users/admin/forms.py:677 msgctxt "admin ban form" msgid "Expiration date" msgstr "" -#: users/admin/forms.py:698 +#: users/admin/forms.py:699 msgctxt "admin ban form" msgid "Banned value is too vague." msgstr "" -#: users/admin/forms.py:706 +#: users/admin/forms.py:707 msgctxt "admin bans filter form" msgid "Type" msgstr "" -#: users/admin/forms.py:709 +#: users/admin/forms.py:710 msgctxt "admin bans type filter choice" msgid "All bans" msgstr "" -#: users/admin/forms.py:710 +#: users/admin/forms.py:711 msgctxt "admin bans type filter choice" msgid "Usernames" msgstr "" -#: users/admin/forms.py:711 +#: users/admin/forms.py:712 msgctxt "admin bans filter form" msgid "E-mails" msgstr "" -#: users/admin/forms.py:712 +#: users/admin/forms.py:713 msgctxt "admin bans type filter choice" msgid "IPs" msgstr "" -#: users/admin/forms.py:716 +#: users/admin/forms.py:717 msgctxt "admin bans filter form" msgid "Banned value begins with" msgstr "" -#: users/admin/forms.py:720 +#: users/admin/forms.py:721 msgctxt "admin bans filter form" msgid "Registration only" msgstr "" -#: users/admin/forms.py:723 +#: users/admin/forms.py:724 msgctxt "admin bans registration filter choice" msgid "Any" msgstr "" -#: users/admin/forms.py:724 +#: users/admin/forms.py:725 msgctxt "admin bans registration filter choice" msgid "Yes" msgstr "" -#: users/admin/forms.py:725 +#: users/admin/forms.py:726 msgctxt "admin bans registration filter choice" msgid "No" msgstr "" -#: users/admin/forms.py:729 +#: users/admin/forms.py:730 msgctxt "admin bans filter form" msgid "State" msgstr "" -#: users/admin/forms.py:732 +#: users/admin/forms.py:733 msgctxt "admin bans state filter choice" msgid "Any" msgstr "" -#: users/admin/forms.py:733 +#: users/admin/forms.py:734 msgctxt "admin bans state filter choice" msgid "Active" msgstr "" -#: users/admin/forms.py:734 +#: users/admin/forms.py:735 msgctxt "admin bans state filter choice" msgid "Expired" msgstr "" -#: users/admin/forms.py:770 +#: users/admin/forms.py:771 msgctxt "admin data download request form" msgid "Usernames or emails" msgstr "" -#: users/admin/forms.py:773 +#: users/admin/forms.py:774 msgctxt "admin data download request form" msgid "" "Enter every item in new line. Duplicates will be ignored. This field is case " @@ -10267,7 +10195,7 @@ msgid "" "notification will be sent to every user once their download is ready." msgstr "" -#: users/admin/forms.py:787 +#: users/admin/forms.py:788 #, python-format msgctxt "admin data download request form" msgid "" @@ -10275,22 +10203,22 @@ msgid "" "%(show_value)s)." msgstr "" -#: users/admin/forms.py:807 +#: users/admin/forms.py:808 msgctxt "admin data download request form" msgid "One or more specified users could not be found." msgstr "" -#: users/admin/forms.py:816 +#: users/admin/forms.py:817 msgctxt "admin data download requests filter form" msgid "Status" msgstr "" -#: users/admin/forms.py:821 +#: users/admin/forms.py:822 msgctxt "admin data download requests filter form" msgid "User" msgstr "" -#: users/admin/forms.py:825 +#: users/admin/forms.py:826 msgctxt "admin data download requests filter form" msgid "Requested by" msgstr "" @@ -10641,7 +10569,7 @@ msgstr "" #: users/api/auth.py:195 msgctxt "change forgotten password api" -msgid "Form link is invalid. Please try again." +msgid "Form link is invalid. Please request new one." msgstr "" #: users/api/auth.py:199 @@ -10659,57 +10587,57 @@ msgctxt "avatar api" msgid "Your avatar is locked. You can't change it." msgstr "" -#: users/api/userendpoints/avatar.py:111 +#: users/api/userendpoints/avatar.py:113 msgctxt "avatar api" -msgid "This avatar type is not allowed." +msgid "This avatar type is not available." msgstr "" -#: users/api/userendpoints/avatar.py:118 +#: users/api/userendpoints/avatar.py:122 msgctxt "avatar api" msgid "Unknown avatar type." msgstr "" -#: users/api/userendpoints/avatar.py:140 +#: users/api/userendpoints/avatar.py:144 msgctxt "avatar api" msgid "New avatar based on your account was set." msgstr "" -#: users/api/userendpoints/avatar.py:146 +#: users/api/userendpoints/avatar.py:150 msgctxt "avatar api" msgid "Gravatar was downloaded and set as new avatar." msgstr "" -#: users/api/userendpoints/avatar.py:150 +#: users/api/userendpoints/avatar.py:154 msgctxt "avatar api" msgid "No Gravatar is associated with your e-mail address." msgstr "" -#: users/api/userendpoints/avatar.py:155 +#: users/api/userendpoints/avatar.py:158 msgctxt "avatar api" -msgid "Failed to connect to Gravatar servers." +msgid "Failed to connect to Gravatar." msgstr "" -#: users/api/userendpoints/avatar.py:166 +#: users/api/userendpoints/avatar.py:168 msgctxt "avatar api" msgid "Avatar from gallery was set." msgstr "" -#: users/api/userendpoints/avatar.py:168 +#: users/api/userendpoints/avatar.py:170 msgctxt "avatar api" msgid "Incorrect image." msgstr "" -#: users/api/userendpoints/avatar.py:174 +#: users/api/userendpoints/avatar.py:176 msgctxt "avatar api" msgid "No file was sent." msgstr "" -#: users/api/userendpoints/avatar.py:187 +#: users/api/userendpoints/avatar.py:189 msgctxt "avatar api" msgid "Avatar was re-cropped." msgstr "" -#: users/api/userendpoints/avatar.py:192 +#: users/api/userendpoints/avatar.py:194 msgctxt "avatar api" msgid "Uploaded avatar was set." msgstr "" @@ -10886,7 +10814,7 @@ msgstr "" #: users/apps.py:45 msgctxt "user options page" -msgid "Change email or password" +msgid "Change e-mail or password" msgstr "" #: users/apps.py:56 @@ -10944,14 +10872,9 @@ msgctxt "avatar upload size validator" msgid "Uploaded file is too big." msgstr "" -#: users/avatars/uploaded.py:51 -msgctxt "avatar upload extension validator" -msgid "Uploaded file type is not allowed." -msgstr "" - -#: users/avatars/uploaded.py:61 -msgctxt "avatar upload mimetype validator" -msgid "Uploaded file type is not allowed." +#: users/avatars/uploaded.py:51 users/avatars/uploaded.py:61 +msgctxt "avatar upload validator" +msgid "Uploaded file type is not supported." msgstr "" #: users/avatars/uploaded.py:73 @@ -11095,7 +11018,7 @@ msgstr "" #: users/forms/register.py:69 msgctxt "register form" -msgid "New registrations from this IP address are not allowed." +msgid "New registrations from your current IP address are not allowed." msgstr "" #: users/management/commands/prepareuserdatadownloads.py:39 @@ -11324,7 +11247,7 @@ msgstr "" #: users/permissions/delete.py:91 msgctxt "users delete permission" -msgid "You can't delete administrators." +msgid "Administrators can't be deleted." msgstr "" #: users/permissions/delete.py:98 @@ -11461,7 +11384,7 @@ msgstr "" #: users/permissions/moderation.py:219 msgctxt "users moderation permission" -msgid "You can't ban administrators." +msgid "Administrators can't be banned." msgstr "" #: users/permissions/moderation.py:230 @@ -11557,7 +11480,7 @@ msgstr "" #: users/permissions/profiles.py:167 msgctxt "users profiles permission" -msgid "You can't block administrators." +msgid "Administrators can't be blocked." msgstr "" #: users/permissions/profiles.py:172 @@ -11623,9 +11546,9 @@ msgstr "" #: users/profilefields/default.py:42 msgctxt "website profile field" msgid "" -"If you own website in the internet you wish to share on your profile you may " -"enter its address here. Remember to for it to be valid http address starting " -"with either \"http://\" or \"https://\"." +"If you own a website you wish to share on your profile, you may enter its " +"address here. Remember that, for it to be valid, it should start with either " +"\"http://\" or \"https://\"." msgstr "" #: users/profilefields/default.py:48 @@ -11633,33 +11556,26 @@ msgctxt "skype id profile field" msgid "Skype ID" msgstr "" -#: users/profilefields/default.py:51 -msgctxt "skype id profile field" -msgid "" -"Entering your Skype ID in this field may invite other users to contact you " -"over the Skype instead of via private threads." -msgstr "" - -#: users/profilefields/default.py:57 +#: users/profilefields/default.py:53 msgctxt "twitter handle profile field" -msgid "Twitter handle" +msgid "X (formerly Twitter) handle" msgstr "" -#: users/profilefields/default.py:62 +#: users/profilefields/default.py:58 #, python-format msgctxt "twitter handle profile field" msgid "" -"If you own Twitter account, here you may enter your Twitter handle for other " -"users to find you. Starting your handle with \"@\" sign is optional. Either " +"If you own an X account, here you may enter your X handle for other users to " +"find you. Starting your handle with \"@\" sign is optional. Either " "\"@%(slug)s\" or \"%(slug)s\" are valid values." msgstr "" -#: users/profilefields/default.py:74 +#: users/profilefields/default.py:70 msgctxt "twitter handle profile field" -msgid "This is not a valid twitter handle." +msgid "This is not a valid X handle." msgstr "" -#: users/profilefields/default.py:82 +#: users/profilefields/default.py:78 msgctxt "join ip profile field" msgid "Join IP" msgstr "" @@ -11690,14 +11606,6 @@ msgctxt "ban message" msgid "You are banned." msgstr "" -#: users/serializers/moderation.py:37 -#, python-format -msgctxt "signature length validator" -msgid "Signature can't be longer than %(limit)s character." -msgid_plural "Signature can't be longer than %(limit)s characters." -msgstr[0] "" -msgstr[1] "" - #: users/serializers/options.py:55 msgctxt "edit signature serializer" msgid "Signature is too long." @@ -11755,7 +11663,7 @@ msgstr "" #: users/signals.py:30 msgctxt "archived user details" -msgid "Joined from ip" +msgid "Joined from IP" msgstr "" #: users/signals.py:73 @@ -11768,39 +11676,44 @@ msgctxt "archived username history" msgid "Old username" msgstr "" -#: users/validators.py:42 +#: users/validators.py:28 +msgctxt "email validator" +msgid "Enter a valid e-mail address." +msgstr "" + +#: users/validators.py:45 msgctxt "user email validator" msgid "This e-mail address is not available." msgstr "" -#: users/validators.py:58 +#: users/validators.py:61 msgctxt "user email validator" msgid "This e-mail address is not allowed." msgstr "" -#: users/validators.py:77 +#: users/validators.py:80 msgctxt "username validator" msgid "This username is not available." msgstr "" -#: users/validators.py:91 +#: users/validators.py:94 msgctxt "username validator" msgid "This username is not allowed." msgstr "" -#: users/validators.py:100 +#: users/validators.py:103 msgctxt "username validator" msgid "" "Username can only contain Latin alphabet letters, digits, and an underscore " "sign." msgstr "" -#: users/validators.py:108 +#: users/validators.py:111 msgctxt "username validator" msgid "Username must contain Latin alphabet letters or digits." msgstr "" -#: users/validators.py:117 +#: users/validators.py:120 #, python-format msgctxt "username length validator" msgid "Username must be at least %(limit_value)s character long." @@ -11808,7 +11721,7 @@ msgid_plural "Username must be at least %(limit_value)s characters long." msgstr[0] "" msgstr[1] "" -#: users/validators.py:126 +#: users/validators.py:129 #, python-format msgctxt "username length validator" msgid "Username cannot be longer than %(limit_value)s characters." @@ -11816,14 +11729,14 @@ msgid_plural "Username cannot be longer than %(limit_value)s characters." msgstr[0] "" msgstr[1] "" -#: users/validators.py:165 +#: users/validators.py:168 msgctxt "stop forum spam validator" msgid "Data entered was found in spammers database." msgstr "" -#: users/validators.py:180 +#: users/validators.py:183 msgctxt "gmail email validator" -msgid "This email is not allowed." +msgid "This e-mail is not allowed." msgstr "" #: users/views/activation.py:23 diff --git a/misago/locale/en/LC_MESSAGES/djangojs.po b/misago/locale/en/LC_MESSAGES/djangojs.po index b0fcb4cc36..4b11d924e5 100644 --- a/misago/locale/en/LC_MESSAGES/djangojs.po +++ b/misago/locale/en/LC_MESSAGES/djangojs.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-24 12:16+0000\n" +"POT-Creation-Date: 2023-11-06 19:19+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -132,28 +132,28 @@ msgid "Register account" msgstr "" #: static/misago/js/misago.js:1 -msgctxt "register modal" +msgctxt "account activation required" msgid "" "%(username)s, your account has been created but you need to activate it " "before you will be able to sign in." msgstr "" #: static/misago/js/misago.js:1 -msgctxt "register modal" +msgctxt "account activation required" msgid "" -"%(username)s, your account has been created but board administrator will " +"%(username)s, your account has been created but the site administrator will " "have to activate it before you will be able to sign in." msgstr "" #: static/misago/js/misago.js:1 -msgctxt "register modal" +msgctxt "account activation required" msgid "" "We have sent an e-mail to %(email)s with link that you have to click to " "activate your account." msgstr "" #: static/misago/js/misago.js:1 -msgctxt "register modal" +msgctxt "account activation required" msgid "We will send an e-mail to %(email)s when this takes place." msgstr "" @@ -169,12 +169,12 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "register form" -msgid "New registrations are currently disabled." +msgid "Registration form is currently disabled by the site administrator." msgstr "" #: static/misago/js/misago.js:1 msgctxt "register form" -msgid "Registration is currently unavailable due to an error." +msgid "Registration form is currently unavailable due to an error." msgstr "" #: static/misago/js/misago.js:1 @@ -549,7 +549,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "merge threads conflict best answer" msgid "" -"Please select the best answer for your newly merged thread. No posts will be " +"Select the best answer for your newly merged thread. No posts will be " "deleted during the merge." msgstr "" @@ -561,7 +561,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "merge threads conflict poll" msgid "" -"Please select the poll for your newly merged thread. Rejected polls will be " +"Select the poll for your newly merged thread. Rejected polls will be " "permanently deleted and cannot be recovered." msgstr "" @@ -591,12 +591,12 @@ msgid "Ok" msgstr "" #: static/misago/js/misago.js:1 -msgctxt "posts feed item body" +msgctxt "post body invalid" msgid "This post's contents cannot be displayed." msgstr "" #: static/misago/js/misago.js:1 -msgctxt "posts feed item body" +msgctxt "post body invalid" msgid "This error is caused by invalid post content manipulation." msgstr "" @@ -686,6 +686,7 @@ msgid "Preview" msgstr "" #: static/misago/js/misago.js:1 +msgctxt "markup editor" msgid "Post" msgstr "" @@ -714,6 +715,7 @@ msgid "Code to insert" msgstr "" #: static/misago/js/misago.js:1 +msgctxt "markup editor" msgid "Cancel" msgstr "" @@ -1054,7 +1056,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "markup editor" -msgid "Image address" +msgid "Image URL" msgstr "" #: static/misago/js/misago.js:1 @@ -1169,7 +1171,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "markup editor" -msgid "Formatting help" +msgid "Open formatting help" msgstr "" #: static/misago/js/misago.js:1 @@ -1209,7 +1211,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "post thread" -msgid "Pinned locally" +msgid "Pinned in category" msgstr "" #: static/misago/js/misago.js:1 @@ -1259,7 +1261,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "post thread submit" -msgid "Post thread" +msgid "Start thread" msgstr "" #: static/misago/js/misago.js:1 @@ -1267,11 +1269,6 @@ msgctxt "post thread" msgid "Thread title" msgstr "" -#: static/misago/js/misago.js:1 -msgctxt "post thread submit" -msgid "Start thread" -msgstr "" - #: static/misago/js/misago.js:1 msgctxt "post thread" msgid "Start new thread" @@ -1493,7 +1490,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "username history empty" -msgid "No name changes have been recorded for your account." +msgid "Your account has no history of name changes." msgstr "" #: static/misago/js/misago.js:1 @@ -1584,22 +1581,38 @@ msgid "No categories exist or you don't have permission to see them." msgstr "" #: static/misago/js/misago.js:1 -msgid "category status" +msgctxt "category status" +msgid "This category has no new posts. (closed)" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "category status" +msgid "This category has new posts. (closed)" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "category status" +msgid "This category has no new posts." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "category status" +msgid "This category has new posts." msgstr "" #: static/misago/js/misago.js:1 msgctxt "category last thread" -msgid "This category is empty. No threads were posted within it so far." +msgid "Empty category" msgstr "" #: static/misago/js/misago.js:1 msgctxt "category last thread" -msgid "This category is private. You can see only your own threads within it." +msgid "Private category" msgstr "" #: static/misago/js/misago.js:1 msgctxt "category last thread" -msgid "This category is protected. You can't browse its contents." +msgid "Protected category" msgstr "" #: static/misago/js/misago.js:1 @@ -1728,7 +1741,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "delete your account form" -msgid "Enter your password to confirm account deletion." +msgid "Complete the form." msgstr "" #: static/misago/js/misago.js:1 @@ -1743,40 +1756,33 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "delete your account form" -msgid "" -"You are going to delete your account. This action is nonreversible, and will " -"result in following data being deleted:" +msgid "This form lets you delete your account. This action is not reversible." msgstr "" #: static/misago/js/misago.js:1 msgctxt "delete your account form" msgid "" -"Stored IP addresses associated with content that you have posted will be " -"deleted." +"Your account will be deleted together with its profile details, IP addresses " +"and notifications." msgstr "" #: static/misago/js/misago.js:1 msgctxt "delete your account form" msgid "" -"Your username will become available for other user to rename to or for new " -"user to register their account with." -msgstr "" - -#: static/misago/js/misago.js:1 -msgctxt "delete your account form" -msgid "Your e-mail will become available for use in new account registration." +"Other content will NOT be deleted, but username displayed next to it will be " +"changed to one shared by all deleted accounts." msgstr "" #: static/misago/js/misago.js:1 msgctxt "delete your account form" msgid "" -"All your posted content will NOT be deleted, but username associated with it " -"will be changed to one shared by all deleted accounts." +"Your username and e-maill address will become available again for use during " +"registration or for other accounts to change to." msgstr "" #: static/misago/js/misago.js:1 msgctxt "delete your account form field" -msgid "Enter your password to confirm account deletion." +msgid "Enter your password to confirm" msgstr "" #: static/misago/js/misago.js:1 @@ -1786,7 +1792,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "profile details form" -msgid "Your details have been updated." +msgid "Your details have been changed." msgstr "" #: static/misago/js/misago.js:1 @@ -1903,7 +1909,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "forum options form" -msgid "Your forum options have been updated." +msgid "Your forum options have been changed." msgstr "" #: static/misago/js/misago.js:1 @@ -1934,8 +1940,8 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "forum options form" msgid "" -"If you hide your presence, only members with permission to see hidden users " -"will see when you are online." +"If you hide your presence, only members with permission to see hidden " +"presence will see when you are online." msgstr "" #: static/misago/js/misago.js:1 @@ -2009,7 +2015,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "change username" -msgid "You have used up available name changes." +msgid "You have changed your name allowed number of times." msgstr "" #: static/misago/js/misago.js:1 @@ -2034,7 +2040,7 @@ msgstr[1] "" #: static/misago/js/misago.js:1 msgctxt "change username form" -msgid "Your new username is same as current one." +msgid "New username is same as current one." msgstr "" #: static/misago/js/misago.js:1 @@ -2049,7 +2055,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "change username" -msgid "Your username has been changed successfully." +msgid "Your username has been changed." msgstr "" #: static/misago/js/misago.js:1 @@ -2078,6 +2084,7 @@ msgid "Change e-mail" msgstr "" #: static/misago/js/misago.js:1 +msgctxt "change password form" msgid "Fill out all fields." msgstr "" @@ -2113,14 +2120,14 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "change sign in credentials title" -msgid "Change email or password" +msgid "Change e-mail or password" msgstr "" #: static/misago/js/misago.js:1 msgctxt "change sign in credentials" msgid "" -"You need to set a password for your account to be able to change your " -"username or email." +"You need to set a password for your account to be able to change your e-mail " +"or password." msgstr "" #: static/misago/js/misago.js:1 @@ -2205,7 +2212,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "profile details form" -msgid "%(username)s's details have been updated." +msgid "%(username)s's details have been changed." msgstr "" #: static/misago/js/misago.js:1 @@ -2305,11 +2312,6 @@ msgctxt "profile username history" msgid "Search returned no username changes matching specified criteria." msgstr "" -#: static/misago/js/misago.js:1 -msgctxt "profile username history" -msgid "No name changes have been recorded for your account." -msgstr "" - #: static/misago/js/misago.js:1 msgctxt "profile username history" msgid "%(username)s's username was never changed." @@ -2375,8 +2377,8 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "profile avatar moderation field" msgid "" -"Optional message for user explaining why he/she is prohibited form changing " -"avatar." +"Optional message for user explaining why they are prohibited from changing " +"their avatar." msgstr "" #: static/misago/js/misago.js:1 @@ -2387,8 +2389,8 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "profile avatar moderation field" msgid "" -"Optional message for forum team members explaining why user is prohibited " -"form changing avatar." +"Optional message for forum team members explaining why the user is " +"prohibited form changing their avatar." msgstr "" #: static/misago/js/misago.js:1 @@ -2559,12 +2561,12 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "profile threads" -msgid "You have no started threads." +msgid "You haven't started any threads." msgstr "" #: static/misago/js/misago.js:1 msgctxt "profile threads" -msgid "%(username)s started no threads." +msgid "%(username)s hasn't started any threads" msgstr "" #: static/misago/js/misago.js:1 @@ -2639,7 +2641,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "request activation link form" -msgid "Enter a valid email address." +msgid "Enter a valid e-mail address." msgstr "" #: static/misago/js/misago.js:1 @@ -2664,7 +2666,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "request password reset form" -msgid "Enter a valid email address." +msgid "Enter a valid e-mail address." msgstr "" #: static/misago/js/misago.js:1 @@ -2714,12 +2716,12 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "password reset form" -msgid "%(username)s, your password has been changed successfully." +msgid "%(username)s, your password has been changed." msgstr "" #: static/misago/js/misago.js:1 msgctxt "password reset form" -msgid "You will have to sign in using new password before continuing." +msgid "Sign in using new password to continue." msgstr "" #: static/misago/js/misago.js:1 @@ -2749,7 +2751,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "search time" -msgid "Search took %(time)s s to complete" +msgid "Search took %(time)s s" msgstr "" #: static/misago/js/misago.js:1 @@ -2804,7 +2806,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "social auth form title" -msgid "Complete your details" +msgid "Complete your account" msgstr "" #: static/misago/js/misago.js:1 @@ -2822,20 +2824,6 @@ msgctxt "social auth form btn" msgid "Sign in" msgstr "" -#: static/misago/js/misago.js:1 -msgctxt "social auth complete" -msgid "" -"%(username)s, your account has been created but you need to activate it " -"before you will be able to sign in." -msgstr "" - -#: static/misago/js/misago.js:1 -msgctxt "social auth complete" -msgid "" -"%(username)s, your account has been created but board administrator will " -"have to activate it before you will be able to sign in." -msgstr "" - #: static/misago/js/misago.js:1 msgctxt "social auth complete" msgid "" @@ -3056,7 +3044,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "thread poll vote" -msgid "You need to select at least one choice" +msgid "You need to select at least one choice." msgstr "" #: static/misago/js/misago.js:1 @@ -3226,7 +3214,9 @@ msgid "Unhide" msgstr "" #: static/misago/js/misago.js:1 -msgid "event delete" +msgctxt "event delete" +msgid "" +"Are you sure you wish to delete this event? This action is not reversible!" msgstr "" #: static/misago/js/misago.js:1 @@ -3256,7 +3246,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "event message" -msgid "Thread has been pinned locally." +msgid "Thread has been pinned in category." msgstr "" #: static/misago/js/misago.js:1 @@ -3354,16 +3344,6 @@ msgctxt "post body hidden" msgid "This post is hidden. You cannot see its contents." msgstr "" -#: static/misago/js/misago.js:1 -msgctxt "post body invalid" -msgid "This post's contents cannot be displayed." -msgstr "" - -#: static/misago/js/misago.js:1 -msgctxt "post body invalid" -msgid "This error is caused by invalid post content manipulation." -msgstr "" - #: static/misago/js/misago.js:1 msgctxt "post best answer flag" msgid "Marked as best answer by you %(marked_on)s." @@ -3505,16 +3485,6 @@ msgctxt "post history modal btn" msgid "See next change" msgstr "" -#: static/misago/js/misago.js:1 -msgctxt "post history modal btn" -msgid "Revert post to state from before this edit." -msgstr "" - -#: static/misago/js/misago.js:1 -msgctxt "post history modal btn" -msgid "Revert" -msgstr "" - #: static/misago/js/misago.js:1 msgctxt "post history modal" msgid "By %(edited_by)s %(edited_on)s." @@ -3568,7 +3538,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "thread weight choice" -msgid "Pinned locally" +msgid "Pinned in category" msgstr "" #: static/misago/js/misago.js:1 @@ -3871,7 +3841,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "thread moderation" -msgid "Thread has been pinned locally." +msgid "Thread has been pinned in category." msgstr "" #: static/misago/js/misago.js:1 @@ -3925,7 +3895,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "thread moderation btn" -msgid "Pin locally" +msgid "Pin in category" msgstr "" #: static/misago/js/misago.js:1 @@ -4309,7 +4279,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "threads moderation merge" msgid "" -"You can't move threads because there are no categories you are allowed to " +"You can't merge threads because there are no categories you are allowed to " "move them to." msgstr "" @@ -4381,7 +4351,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "threads moderation" -msgid "Selected threads were pinned locally." +msgid "Selected threads were pinned in category." msgstr "" #: static/misago/js/misago.js:1 @@ -4451,7 +4421,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "threads moderation btn" -msgid "Pin threads locally" +msgid "Pin threads in categories" msgstr "" #: static/misago/js/misago.js:1 @@ -4521,12 +4491,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "threads list empty" -msgid "Why not start one yourself?" -msgstr "" - -#: static/misago/js/misago.js:1 -msgctxt "threads list empty" -msgid "There are no threads on this forum... yet!" +msgid "There are no threads on this site yet." msgstr "" #: static/misago/js/misago.js:1 @@ -4679,8 +4644,10 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "top posters empty" -msgid "No users have posted any new messages during last %(days)s days." -msgstr "" +msgid "No users have posted any new messages during last %(days)s day." +msgid_plural "No users have posted any new messages during last %(days)s days." +msgstr[0] "" +msgstr[1] "" #: static/misago/js/misago.js:1 msgctxt "top posters list item" @@ -4711,14 +4678,14 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "rank users list" -msgid "There is %(more)s more member with this role." -msgid_plural "There are %(more)s more members with this role." +msgid "There is %(more)s more user with this rank." +msgid_plural "There are %(more)s more users with this rank." msgstr[0] "" msgstr[1] "" #: static/misago/js/misago.js:1 msgctxt "rank users list empty" -msgid "There are no more members with this role." +msgid "There are no more users with this rank." msgstr "" #: static/misago/js/misago.js:1 @@ -4748,7 +4715,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "ajax client error" -msgid "Could not connect to server." +msgid "Could not connect to the site." msgstr "" #: static/misago/js/misago.js:1 @@ -4758,16 +4725,26 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "ajax client error" -msgid "Unknown error has occured." +msgid "Unknown error has occurred." msgstr "" #: static/misago/js/misago.js:1 -msgctxt "ajax client error" -msgid "Upload was rejected by server as too large." +msgctxt "api error" +msgid "Could not connect to the site." msgstr "" #: static/misago/js/misago.js:1 -msgctxt "ajax client error" +msgctxt "api error" +msgid "Upload was rejected by the site as too large." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "api error" +msgid "Action link is invalid." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "api error" msgid "Unknown error has occurred." msgstr "" @@ -4783,7 +4760,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "captcha field" -msgid "This test helps us prevent automated spam registrations on our site." +msgid "This test helps us prevent automated spam registrations on the site." msgstr "" #: static/misago/js/misago.js:1 @@ -4795,21 +4772,6 @@ msgstr "" msgid "You are already working on other message. Do you want to discard it?" msgstr "" -#: static/misago/js/misago.js:1 -msgctxt "api error" -msgid "Could not connect to server." -msgstr "" - -#: static/misago/js/misago.js:1 -msgctxt "api error" -msgid "Action link is invalid." -msgstr "" - -#: static/misago/js/misago.js:1 -msgctxt "api error" -msgid "Unknown error has occurrsed." -msgstr "" - #: static/misago/js/misago.js:1 msgctxt "api error" msgid "You don't have permission to perform this action." @@ -4852,7 +4814,7 @@ msgstr "" #: static/misago/js/misago.js:1 msgctxt "email validator" -msgid "Enter a valid email address." +msgid "Enter a valid e-mail address." msgstr "" #: static/misago/js/misago.js:1 @@ -4893,7 +4855,7 @@ msgstr[1] "" #: static/misago/js/misago.js:1 msgctxt "username validator" -msgid "Username can must contain Latin alphabet letters or digits." +msgid "Username must contain Latin alphabet letters or digits." msgstr "" #: static/misago/js/misago.js:1 diff --git a/misago/locale/es/LC_MESSAGES/djangojs.mo b/misago/locale/es/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000..8d3a38b351 Binary files /dev/null and b/misago/locale/es/LC_MESSAGES/djangojs.mo differ diff --git a/misago/locale/es/LC_MESSAGES/djangojs.po b/misago/locale/es/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000000..acf5c2c912 --- /dev/null +++ b/misago/locale/es/LC_MESSAGES/djangojs.po @@ -0,0 +1,5068 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +# Translators: +# Daniel Gerardo Rondón García, 2023 +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-11-06 19:09+0000\n" +"PO-Revision-Date: 2023-07-01 08:52+0000\n" +"Last-Translator: Daniel Gerardo Rondón García, 2023\n" +"Language-Team: Spanish (https://app.transifex.com/misago/teams/65369/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" + +#: static/misago/js/misago.js:1 +msgctxt "notifications list" +msgid "You don't have any read notifications." +msgstr "No tienes ninguna notificación leída." + +#: static/misago/js/misago.js:1 +msgctxt "notifications list" +msgid "You don't have any unread notifications." +msgstr "No tienes ninguna notificación sin leer." + +#: static/misago/js/misago.js:1 +msgctxt "notifications list" +msgid "You don't have any notifications." +msgstr "No tienes ninguna notificación." + +#: static/misago/js/misago.js:1 +msgctxt "notification status" +msgid "Read notification" +msgstr "Notificación leída" + +#: static/misago/js/misago.js:1 +msgctxt "notification status" +msgid "Unread notification" +msgstr "Notificación sin leer" + +#: static/misago/js/misago.js:1 +msgid "Check your internet connection and try refreshing the site." +msgstr "Verifica tu conexión a internet e intenta refrescar el sitio." + +#: static/misago/js/misago.js:1 +msgctxt "notifications list" +msgid "Notifications could not be loaded." +msgstr "Las notificaciones no pudieron ser cargadas." + +#: static/misago/js/misago.js:1 +msgctxt "notifications list" +msgid "Loading notifications..." +msgstr "Cargando notificaciones..." + +#: static/misago/js/misago.js:1 +msgctxt "modal" +msgid "Close" +msgstr "Cerrar" + +#: static/misago/js/misago.js:1 +msgctxt "password strength indicator" +msgid "Entered password is very weak." +msgstr "La contraseña ingresada es muy débil." + +#: static/misago/js/misago.js:1 +msgctxt "password strength indicator" +msgid "Entered password is weak." +msgstr "La contraseña ingresada es débil." + +#: static/misago/js/misago.js:1 +msgctxt "password strength indicator" +msgid "Entered password is average." +msgstr "La contraseña ingresada está más o menos bien." + +#: static/misago/js/misago.js:1 +msgctxt "password strength indicator" +msgid "Entered password is strong." +msgstr "La contraseña ingresada es fuerte." + +#: static/misago/js/misago.js:1 +msgctxt "password strength indicator" +msgid "Entered password is very strong." +msgstr "La contraseña ingresada es muy fuerte." + +#: static/misago/js/misago.js:1 +msgid "Form contains errors." +msgstr "El formulario contiene errores." + +#: static/misago/js/misago.js:1 +msgctxt "register modal title" +msgid "Register" +msgstr "Registrarse" + +#: static/misago/js/misago.js:1 +msgctxt "register modal field" +msgid "Join with %(site)s" +msgstr "Únete con %(site)s" + +#: static/misago/js/misago.js:1 +msgctxt "register modal field" +msgid "Or create forum account:" +msgstr "O crea una cuenta de foro:" + +#: static/misago/js/misago.js:1 +msgctxt "register modal field" +msgid "Username" +msgstr "Nombre de usuario" + +#: static/misago/js/misago.js:1 +msgctxt "register modal field" +msgid "E-mail" +msgstr "Correo electrónico" + +#: static/misago/js/misago.js:1 +msgctxt "register modal field" +msgid "Password" +msgstr "Contraseña" + +#: static/misago/js/misago.js:1 +msgctxt "register modal btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "register modal btn" +msgid "Register account" +msgstr "Registrar cuenta" + +#: static/misago/js/misago.js:1 +msgctxt "account activation required" +msgid "" +"%(username)s, your account has been created but you need to activate it " +"before you will be able to sign in." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "account activation required" +msgid "" +"%(username)s, your account has been created but the site administrator will " +"have to activate it before you will be able to sign in." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "account activation required" +msgid "" +"We have sent an e-mail to %(email)s with link that you have to click to " +"activate your account." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "account activation required" +msgid "We will send an e-mail to %(email)s when this takes place." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "register modal title" +msgid "Registration complete" +msgstr "Registro completado" + +#: static/misago/js/misago.js:1 +msgctxt "register modal dismiss" +msgid "Ok" +msgstr "Ok" + +#: static/misago/js/misago.js:1 +msgctxt "register form" +msgid "Registration form is currently disabled by the site administrator." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "register form" +msgid "Registration form is currently unavailable due to an error." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "cta" +msgid "Register" +msgstr "Registrarse" + +#: static/misago/js/misago.js:1 +msgctxt "register form agreement prompt" +msgid "I have read and accept %(agreement)s." +msgstr "He leído y acepto %(agreement)s." + +#: static/misago/js/misago.js:1 +msgctxt "register form agreement prompt" +msgid "the terms of service" +msgstr "los términos de servicio" + +#: static/misago/js/misago.js:1 +msgctxt "register form agreement prompt" +msgid "the privacy policy" +msgstr "la política de privacidad" + +#: static/misago/js/misago.js:1 +msgctxt "search cta" +msgid "Enter search query (at least 3 characters)." +msgstr "Ingresa una consulta de búsqueda (al menos 3 caracteres)." + +#: static/misago/js/misago.js:1 +msgctxt "search results list" +msgid "See all %(count)s result." +msgid_plural "See all %(count)s results." +msgstr[0] "Ver el %(count)s resultado." +msgstr[1] "Ver todos los %(count)s resultados." +msgstr[2] "Ver todos los %(count)s resultados." + +#: static/misago/js/misago.js:1 +msgctxt "search results" +msgid "The search returned no results." +msgstr "La búsqueda no devolvió resultados." + +#: static/misago/js/misago.js:1 +msgctxt "search results" +msgid "The search could not be completed." +msgstr "La búsqueda no pudo ser completada." + +#: static/misago/js/misago.js:1 +msgctxt "search results" +msgid "Searching..." +msgstr "Buscando..." + +#: static/misago/js/misago.js:1 +msgctxt "cta" +msgid "Search" +msgstr "Buscar" + +#: static/misago/js/misago.js:1 +msgctxt "cta" +msgid "Sign in" +msgstr "Iniciar sesión" + +#: static/misago/js/misago.js:1 +msgctxt "site nav" +msgid "Threads" +msgstr "Temas" + +#: static/misago/js/misago.js:1 +msgctxt "site nav" +msgid "Categories" +msgstr "Categorías" + +#: static/misago/js/misago.js:1 +msgctxt "site nav" +msgid "Search" +msgstr "Buscar" + +#: static/misago/js/misago.js:1 +msgctxt "cta" +msgid "You are not signed in" +msgstr "No has iniciado sesión" + +#: static/misago/js/misago.js:1 +msgctxt "site nav section" +msgid "Users" +msgstr "Usuarios" + +#: static/misago/js/misago.js:1 +msgctxt "site nav section" +msgid "Categories" +msgstr "Categorías" + +#: static/misago/js/misago.js:1 +msgctxt "site nav section" +msgid "Footer" +msgstr "Pie de página" + +#: static/misago/js/misago.js:1 +msgctxt "site nav title" +msgid "Menu" +msgstr "Menú" + +#: static/misago/js/misago.js:1 +msgctxt "thread flag" +msgid "Pinned globally" +msgstr "Fijado globalmente" + +#: static/misago/js/misago.js:1 +msgctxt "thread flag" +msgid "Pinned in category" +msgstr "Fijado en categoría" + +#: static/misago/js/misago.js:1 +msgctxt "thread flag" +msgid "Answered" +msgstr "Respondido" + +#: static/misago/js/misago.js:1 +msgctxt "thread flag" +msgid "Poll" +msgstr "Encuesta" + +#: static/misago/js/misago.js:1 +msgctxt "thread flag" +msgid "Awaiting approval" +msgstr "Esperando aprobación" + +#: static/misago/js/misago.js:1 +msgctxt "thread flag" +msgid "Has unapproved posts" +msgstr "Tiene mensajes no aprobados" + +#: static/misago/js/misago.js:1 +msgctxt "thread flag" +msgid "Closed" +msgstr "Cerrado" + +#: static/misago/js/misago.js:1 +msgctxt "thread flag" +msgid "Hidden" +msgstr "Oculto" + +#: static/misago/js/misago.js:1 +msgctxt "thread replies stat" +msgid "%(replies)s reply" +msgid_plural "%(replies)s replies" +msgstr[0] "%(replies)s respuesta" +msgstr[1] "%(replies)s respuestas" +msgstr[2] "%(replies)s respuestas" + +#: static/misago/js/misago.js:1 +msgctxt "time ago" +msgid "moment ago" +msgstr "hace un momento" + +#: static/misago/js/misago.js:1 +msgctxt "time ago" +msgid "now" +msgstr "ahora" + +#: static/misago/js/misago.js:1 +msgctxt "day at time" +msgid "%(day)s at %(time)s" +msgstr "%(day)s a las %(time)s" + +#: static/misago/js/misago.js:1 +msgctxt "day at time" +msgid "Tomorrow at %(time)s" +msgstr "Mañana a las %(time)s" + +#: static/misago/js/misago.js:1 +msgctxt "day at time" +msgid "Yesterday at %(time)s" +msgstr "Ayer a las %(time)s" + +#: static/misago/js/misago.js:1 +msgctxt "short minutes" +msgid "%(time)sm" +msgstr "%(time)s min" + +#: static/misago/js/misago.js:1 +msgctxt "short hours" +msgid "%(time)sh" +msgstr "%(time)s h" + +#: static/misago/js/misago.js:1 +msgctxt "short days" +msgid "%(time)sd" +msgstr "1 %(time)s d" + +#: static/misago/js/misago.js:1 +msgctxt "avatar modal btn" +msgid "Download my Gravatar" +msgstr "Descargar mi Gravatar" + +#: static/misago/js/misago.js:1 +msgctxt "avatar modal btn" +msgid "Re-crop uploaded image" +msgstr "Recortar imagen subida" + +#: static/misago/js/misago.js:1 +msgctxt "avatar modal btn" +msgid "Upload new image" +msgstr "Subir nueva imagen" + +#: static/misago/js/misago.js:1 +msgctxt "avatar modal btn" +msgid "Pick avatar from gallery" +msgstr "Elegir avatar de la galería" + +#: static/misago/js/misago.js:1 +msgctxt "avatar modal btn" +msgid "Generate my individual avatar" +msgstr "Generar mi avatar individual" + +#: static/misago/js/misago.js:1 +msgctxt "avatar crop modal btn" +msgid "Set avatar" +msgstr "Establecer avatar" + +#: static/misago/js/misago.js:1 +msgctxt "avatar crop modal btn" +msgid "Crop image" +msgstr "Recortar imagen" + +#: static/misago/js/misago.js:1 +msgctxt "avatar crop modal btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "avatar upload modal" +msgid "Your image has been uploaded and you may now crop it." +msgstr "Tu imagen ha sido subida y ahora puedes recortarla." + +#: static/misago/js/misago.js:1 +msgctxt "avatar upload modal" +msgid "Selected file is too big. (%(filesize)s)" +msgstr "El archivo seleccionado es demasiado grande. (%(filesize)s)" + +#: static/misago/js/misago.js:1 +msgctxt "avatar upload modal" +msgid "Selected file type is not supported." +msgstr "El tipo de archivo seleccionado no es soportado." + +#: static/misago/js/misago.js:1 +msgctxt "avatar upload modal" +msgid "%(files)s files smaller than %(limit)s" +msgstr "%(files)s archivos más pequeños que %(limit)s" + +#: static/misago/js/misago.js:1 +msgctxt "avatar upload modal field" +msgid "Select file" +msgstr "Seleccionar archivo" + +#: static/misago/js/misago.js:1 +msgctxt "avatar upload modal field" +msgid "%(progress)s % complete" +msgstr "%(progress)s % completado" + +#: static/misago/js/misago.js:1 +msgctxt "avatar upload modal btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "avatar gallery modal btn" +msgid "Save choice" +msgstr "Guardar elección" + +#: static/misago/js/misago.js:1 +msgctxt "avatar gallery modal btn" +msgid "Select avatar" +msgstr "Seleccionar avatar" + +#: static/misago/js/misago.js:1 +msgctxt "avatar gallery modal btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "avatar modal dismiss" +msgid "Ok" +msgstr "Ok" + +#: static/misago/js/misago.js:1 +msgctxt "avatar modal title" +msgid "Change your avatar" +msgstr "Cambiar tu avatar" + +#: static/misago/js/misago.js:1 +msgctxt "user nav" +msgid "Go to your profile" +msgstr "Ir a tu perfil" + +#: static/misago/js/misago.js:1 +msgctxt "user nav" +msgid "Notifications" +msgstr "Notificaciones" + +#: static/misago/js/misago.js:1 +msgctxt "user nav" +msgid "Private threads" +msgstr "Temas privados" + +#: static/misago/js/misago.js:1 +msgctxt "user nav" +msgid "Admin control panel" +msgstr "Panel de control de administrador" + +#: static/misago/js/misago.js:1 +msgctxt "user nav section" +msgid "Change options" +msgstr "Cambiar opciones" + +#: static/misago/js/misago.js:1 +msgctxt "user nav" +msgid "Change avatar" +msgstr "Cambiar avatar" + +#: static/misago/js/misago.js:1 +msgctxt "user nav" +msgid "See more" +msgstr "Ver más" + +#: static/misago/js/misago.js:1 +msgctxt "user nav" +msgid "Log out" +msgstr "Cerrar sesión" + +#: static/misago/js/misago.js:1 +msgctxt "user nav title" +msgid "Your options" +msgstr "Tus opciones" + +#: static/misago/js/misago.js:1 +msgctxt "user profile details" +msgid "No profile details are editable at this time." +msgstr "No hay detalles de perfil editables en este momento." + +#: static/misago/js/misago.js:1 +msgctxt "user profile details" +msgid "This option is currently unavailable." +msgstr "Esta opción no está disponible actualmente." + +#: static/misago/js/misago.js:1 +msgctxt "user profile details form btn" +msgid "Save changes" +msgstr "Guardar cambios" + +#: static/misago/js/misago.js:1 +msgctxt "user profile details form btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "user profile details form title" +msgid "Edit details" +msgstr "Editar detalles" + +#: static/misago/js/misago.js:1 +msgctxt "field validation status" +msgid "(error)" +msgstr "(error)" + +#: static/misago/js/misago.js:1 +msgctxt "field validation status" +msgid "(success)" +msgstr "(éxito)" + +#: static/misago/js/misago.js:1 +msgctxt "merge threads conflict best answer" +msgid "Best answer" +msgstr "Mejor respuesta" + +#: static/misago/js/misago.js:1 +msgctxt "merge threads conflict best answer" +msgid "" +"Select the best answer for your newly merged thread. No posts will be " +"deleted during the merge." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "merge threads conflict poll" +msgid "Poll" +msgstr "Encuesta" + +#: static/misago/js/misago.js:1 +msgctxt "merge threads conflict poll" +msgid "" +"Select the poll for your newly merged thread. Rejected polls will be " +"permanently deleted and cannot be recovered." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "merge threads conflict form" +msgid "Are you sure you want to delete all polls?" +msgstr "¿Estás seguro de que quieres eliminar todas las encuestas?" + +#: static/misago/js/misago.js:1 +msgctxt "merge threads conflict modal title" +msgid "Merge threads" +msgstr "Fusionar temas" + +#: static/misago/js/misago.js:1 +msgctxt "merge threads conflict btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "merge threads conflict btn" +msgid "Merge threads" +msgstr "Fusionar temas" + +#: static/misago/js/misago.js:1 +msgctxt "modal message dismiss btn" +msgid "Ok" +msgstr "Ok" + +#: static/misago/js/misago.js:1 +msgctxt "post body invalid" +msgid "This post's contents cannot be displayed." +msgstr "El contenido de este mensaje no se puede mostrar." + +#: static/misago/js/misago.js:1 +msgctxt "post body invalid" +msgid "This error is caused by invalid post content manipulation." +msgstr "" +"Este error es causado por una manipulación de contenido de mensaje no " +"válida." + +#: static/misago/js/misago.js:1 +msgctxt "posts feed item header" +msgid "posted %(posted_on)s" +msgstr "publicado %(posted_on)s" + +#: static/misago/js/misago.js:1 +msgctxt "go to post link" +msgid "See post" +msgstr "Ver post" + +#: static/misago/js/misago.js:1 +msgctxt "post removed poster username" +msgid "Removed user" +msgstr "Usuario eliminado" + +#: static/misago/js/misago.js:1 +msgctxt "post reply" +msgid "Quote" +msgstr "Citar" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Attachment details" +msgstr "Detalles del adjunto" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Type and size" +msgstr "Tipo y tamaño" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Uploaded at" +msgstr "Subido el" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Uploader" +msgstr "Subido por" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Insert into message" +msgstr "Insertar en el mensaje" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Remove attachment" +msgstr "Eliminar adjunto" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Remove this attachment?" +msgstr "¿Eliminar este adjunto?" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "See error" +msgstr "Ver error" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "%(filename)s: %(error)s" +msgstr "%(filename)s: %(error)s" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Protected" +msgstr "Protegido" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Protect" +msgstr "Proteger" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Edit" +msgstr "Editar" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Preview" +msgstr "Vista previa" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Post" +msgstr "" + +#: static/misago/js/misago.js:1 +msgid "This field is required." +msgstr "Este campo es requerido." + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Code" +msgstr "Código" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Syntax highlighting" +msgstr "Resaltar sintaxis" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "No syntax highlighting" +msgstr "Sin resaltado de sintaxis" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Code to insert" +msgstr "Código a insertar" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Cancel" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Insert code" +msgstr "Insertar código" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Formatting help" +msgstr "Ayuda de formato" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Emphasis text" +msgstr "Texto de énfasis" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "_This text will have emphasis_" +msgstr "_Este texto tendrá énfasis_" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "This text will have emphasis" +msgstr "Este texto tendrá énfasis" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Bold text" +msgstr "Texto en negrilla" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "**This text will be bold**" +msgstr "**Este texto estará en negrilla**" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "This text will be bold" +msgstr "Este texto estará en negrilla" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Removed text" +msgstr "Texto eliminado" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "~~This text will be removed~~" +msgstr "~~Este texto será eliminado~~" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "This text will be removed" +msgstr "Este texto será eliminado" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Bold text (BBCode)" +msgstr "Texto en negrilla (BBCode)" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "[b]This text will be bold[/b]" +msgstr "[b]Este texto estará en negrilla[/b]" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Underlined text (BBCode)" +msgstr "Texto subrayado (BBCode)" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "[u]This text will be underlined[/u]" +msgstr "[u]Este texto será subrayado[/u]" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "This text will be underlined" +msgstr "Este texto será subrayado" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Italics text (BBCode)" +msgstr "Texto en cursiva (BBCode)" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "[i]This text will be in italics[/i]" +msgstr "[i]Este texto estará en cursiva[/i]" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "This text will be in italics" +msgstr "Este texto estará en cursiva" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Link" +msgstr "Enlace" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Link with text" +msgstr "Enlace con texto" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Link text" +msgstr "Texto del enlace" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Link (BBCode)" +msgstr "Enlace (BBCode)" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Link with text (BBCode)" +msgstr "Enlace con texto (BBCode)" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Image" +msgstr "Imagen" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Image with alternate text" +msgstr "Imagen con texto alternativo" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Image text" +msgstr "Texto de la imagen" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Image (BBCode)" +msgstr "Imagen (BBCode)" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Mention user by their name" +msgstr "Mencionar usuario por su nombre" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Heading 1" +msgstr "Encabezado 1" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "# First level heading" +msgstr "# Encabezado de primer nivel" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "First level heading" +msgstr "Encabezado de primer nivel" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Heading 2" +msgstr "Encabezado 2" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "## Second level heading" +msgstr "## Encabezado de segundo nivel" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Second level heading" +msgstr "Encabezado de segundo nivel" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Heading 3" +msgstr "Encabezado 3" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "### Third level heading" +msgstr "### Encabezado de tercer nivel" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Third level heading" +msgstr "Encabezado de tercer nivel" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Heading 4" +msgstr "Encabezado 4" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "#### Fourth level heading" +msgstr "#### Encabezado de cuarto nivel" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Fourth level heading" +msgstr "Encabezado de cuarto nivel" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Heading 5" +msgstr "Encabezado 5" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "##### Fifth level heading" +msgstr "##### Encabezado de quinto nivel" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Fifth level heading" +msgstr "Encabezado de quinto nivel" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Unordered list" +msgstr "Lista desordenada" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Ordered list" +msgstr "Lista ordenada" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Quote text" +msgstr "Texto citado" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Quoted text" +msgstr "Texto citado" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Quote text (BBCode)" +msgstr "Texto citado (BBCode)" + +#: static/misago/js/misago.js:1 +msgid "Quoted message:" +msgstr "Mensaje citado:" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Quote text with author (BBCode)" +msgstr "Texto citado con autor (BBCode)" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Quote author" +msgstr "Autor de la cita" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Quote author has written:" +msgstr "El autor de la cita ha escrito:" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Spoiler" +msgstr "Spoiler" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Secret text" +msgstr "Texto secreto" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Inline code" +msgstr "Código en línea" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "`Inline code`" +msgstr "`Código en línea`" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Code block" +msgstr "Bloque de código" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Code block with syntax highlighting" +msgstr "Bloque de código con resaltado de sintaxis" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Code block (BBCode)" +msgstr "Bloque de código (BBCode)" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Code block with syntax highlighting (BBCode)" +msgstr "Bloque de código con resaltado de sintaxis (BBCode)" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Horizontal rule" +msgstr "Regla horizontal" + +#: static/misago/js/misago.js:1 +msgctxt "markup help" +msgid "Horizontal rule (BBCode)" +msgstr "Regla horizontal (BBCode)" + +#: static/misago/js/misago.js:1 +msgid "Reveal spoiler" +msgstr "Mostrar spoiler" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Image" +msgstr "Imagen" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Image description" +msgstr "Descripción de la imagen" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "" +"Optional but recommended . Will be displayed instead of image when it fails " +"to load." +msgstr "" +"Opcional pero recomendado. Se mostrará en lugar de la imagen cuando no se " +"pueda cargar." + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Image URL" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Insert image" +msgstr "Insertar imagen" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Link" +msgstr "Enlace" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Link text" +msgstr "Texto del enlace" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Optional. Will be displayed instead of link's address." +msgstr "Opcional. Se mostrará en lugar de la dirección del enlace." + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Link address" +msgstr "Dirección del enlace" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Insert link" +msgstr "Insertar enlace" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Quote" +msgstr "Cita" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Quote's author or source" +msgstr "Autor o fuente de la cita" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Optional. If it's username, put \"@\" before it (\"@JohnDoe\")." +msgstr "Opcional. Si es un nombre de usuario, pon \"@\" antes de él (\"@JohnDoe\")." + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Quoted text" +msgstr "Texto citado" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Insert quote" +msgstr "Insertar cita" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "File %(filename)s is bigger than %(limit)s." +msgstr "El archivo %(filename)s es más grande que %(limit)s." + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Strong" +msgstr "Negrilla" + +#: static/misago/js/misago.js:1 +msgctxt "example markup" +msgid "Strong text" +msgstr "Texto en negrilla" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Emphasis" +msgstr "Énfasis" + +#: static/misago/js/misago.js:1 +msgctxt "example markup" +msgid "Text with emphasis" +msgstr "Texto con énfasis" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Strikethrough" +msgstr "Tachado" + +#: static/misago/js/misago.js:1 +msgctxt "example markup" +msgid "Text with strikethrough" +msgstr "Texto tachado" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Horizontal ruler" +msgstr "Regla horizontal" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Spoiler" +msgstr "Spoiler" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Spoiler text" +msgstr "Texto del spoiler" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Upload file" +msgstr "Subir archivo" + +#: static/misago/js/misago.js:1 +msgctxt "markup editor" +msgid "Open formatting help" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "dialog" +msgid "Open" +msgstr "Abrir" + +#: static/misago/js/misago.js:1 +msgctxt "dialog" +msgid "Minimize" +msgstr "Minimizar" + +#: static/misago/js/misago.js:1 +msgctxt "dialog" +msgid "Exit the fullscreen mode" +msgstr "Salir del modo de pantalla completa" + +#: static/misago/js/misago.js:1 +msgctxt "dialog" +msgid "Enter the fullscreen mode" +msgstr "Entrar al modo de pantalla completa" + +#: static/misago/js/misago.js:1 +msgctxt "dialog" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Options" +msgstr "Opciones" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Pinned globally" +msgstr "Fijado globalmente" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Pinned in category" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Not pinned" +msgstr "No fijado" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Open" +msgstr "Abierto" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Closed" +msgstr "Cerrado" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Visible" +msgstr "Visible" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Hidden" +msgstr "Oculto" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Are you sure you want to discard thread?" +msgstr "¿Estás seguro de que quieres descartar el tema?" + +#: static/misago/js/misago.js:1 +msgctxt "posting form" +msgid "You have to enter thread title." +msgstr "Debes ingresar un título para el tema." + +#: static/misago/js/misago.js:1 +msgctxt "posting form" +msgid "You have to enter a message." +msgstr "Debes ingresar un mensaje." + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Your thread has been posted." +msgstr "Tu tema ha sido publicado." + +#: static/misago/js/misago.js:1 +msgctxt "post thread submit" +msgid "Start thread" +msgstr "Iniciar tema" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Thread title" +msgstr "Título del tema" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Start new thread" +msgstr "Iniciar nuevo tema" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Are you sure you want to discard private thread?" +msgstr "¿Estás seguro de que quieres descartar el tema privado?" + +#: static/misago/js/misago.js:1 +msgctxt "posting form" +msgid "You have to enter at least one recipient." +msgstr "Debes ingresar al menos un destinatario." + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Recipients, eg.: Danny, Lisa, Alice" +msgstr "Destinatarios, ej.: Danny, Lisa, Alice" + +#: static/misago/js/misago.js:1 +msgctxt "post thread" +msgid "Start private thread" +msgstr "Iniciar tema privado" + +#: static/misago/js/misago.js:1 +msgctxt "post reply" +msgid "Are you sure you want to discard your reply?" +msgstr "¿Estás seguro de que quieres descartar tu respuesta?" + +#: static/misago/js/misago.js:1 +msgctxt "post reply" +msgid "Your reply has been posted." +msgstr "Tu respuesta ha sido posteada." + +#: static/misago/js/misago.js:1 +msgctxt "post reply submit" +msgid "Post reply" +msgstr "Postear respuesta" + +#: static/misago/js/misago.js:1 +msgctxt "post reply" +msgid "Reply to: %(thread)s" +msgstr "Responder a: %(thread)s" + +#: static/misago/js/misago.js:1 +msgctxt "edit reply" +msgid "Are you sure you want to discard changes?" +msgstr "¿Estás seguro de que quieres descartar los cambios?" + +#: static/misago/js/misago.js:1 +msgctxt "edit reply" +msgid "Reply has been edited." +msgstr "La respuesta ha sido editada." + +#: static/misago/js/misago.js:1 +msgctxt "edit reply submit" +msgid "Edit reply" +msgstr "Editar respuesta" + +#: static/misago/js/misago.js:1 +msgctxt "edit reply" +msgid "Edit reply by %(poster)s from %(date)s" +msgstr "Editar respuesta de %(poster)s desde %(date)s" + +#: static/misago/js/misago.js:1 +msgctxt "thread title length validator" +msgid "" +"Thread title should be at least %(limit_value)s character long (it has " +"%(show_value)s)." +msgid_plural "" +"Thread title should be at least %(limit_value)s characters long (it has " +"%(show_value)s)." +msgstr[0] "" +"El título del tema debe tener al menos %(limit_value)s carácter (tiene " +"%(show_value)s)." +msgstr[1] "" +"El título del tema debe tener al menos %(limit_value)s caracteres (tiene " +"%(show_value)s)." +msgstr[2] "" +"El título del tema debe tener al menos %(limit_value)s caracteres (tiene " +"%(show_value)s)." + +#: static/misago/js/misago.js:1 +msgctxt "thread title length validator" +msgid "" +"Thread title cannot be longer than %(limit_value)s character (it has " +"%(show_value)s)." +msgid_plural "" +"Thread title cannot be longer than %(limit_value)s characters (it has " +"%(show_value)s)." +msgstr[0] "" +"El título del tema no puede tener más de %(limit_value)s carácter (tiene " +"%(show_value)s)." +msgstr[1] "" +"El título del tema no puede tener más de %(limit_value)s caracteres (tiene " +"%(show_value)s)." +msgstr[2] "" +"El título del tema no puede tener más de %(limit_value)s caracteres (tiene " +"%(show_value)s)." + +#: static/misago/js/misago.js:1 +msgctxt "post length validator" +msgid "" +"Posted message cannot be longer than %(limit_value)s character (it has " +"%(show_value)s)." +msgid_plural "" +"Posted message cannot be longer than %(limit_value)s characters (it has " +"%(show_value)s)." +msgstr[0] "" +"El mensaje publicado no puede tener más de %(limit_value)s carácter (tiene " +"%(show_value)s)." +msgstr[1] "" +"El mensaje publicado no puede tener más de %(limit_value)s caracteres (tiene" +" %(show_value)s)." +msgstr[2] "" +"El mensaje publicado no puede tener más de %(limit_value)s caracteres (tiene" +" %(show_value)s)." + +#: static/misago/js/misago.js:1 +msgctxt "post length validator" +msgid "" +"Posted message should be at least %(limit_value)s character long (it has " +"%(show_value)s)." +msgid_plural "" +"Posted message should be at least %(limit_value)s characters long (it has " +"%(show_value)s)." +msgstr[0] "" +"El mensaje publicado debe tener al menos %(limit_value)s carácter (tiene " +"%(show_value)s)." +msgstr[1] "" +"El mensaje publicado debe tener al menos %(limit_value)s caracteres (tiene " +"%(show_value)s)." +msgstr[2] "" +"El mensaje publicado debe tener al menos %(limit_value)s caracteres (tiene " +"%(show_value)s)." + +#: static/misago/js/misago.js:1 +msgctxt "sign in modal" +msgid "Fill out both fields." +msgstr "Rellena ambos campos." + +#: static/misago/js/misago.js:1 +msgctxt "sign in modal btn" +msgid "Activate account" +msgstr "Activar cuenta" + +#: static/misago/js/misago.js:1 +msgctxt "sign in modal title" +msgid "Sign in" +msgstr "Iniciar sesión" + +#: static/misago/js/misago.js:1 +msgctxt "sign in modal" +msgid "Sign in with %(site)s" +msgstr "Iniciar sesión con %(site)s" + +#: static/misago/js/misago.js:1 +msgctxt "sign in modal" +msgid "Or use your forum account:" +msgstr "O usa tu cuenta del foro:" + +#: static/misago/js/misago.js:1 +msgctxt "sign in modal field" +msgid "Username or e-mail" +msgstr "Nombre de usuario o correo electrónico" + +#: static/misago/js/misago.js:1 +msgctxt "sign in modal field" +msgid "Password" +msgstr "Contraseña" + +#: static/misago/js/misago.js:1 +msgctxt "sign in modal btn" +msgid "Sign in" +msgstr "Iniciar sesión" + +#: static/misago/js/misago.js:1 +msgctxt "sign in modal btn" +msgid "Forgot password?" +msgstr "¿Olvidaste tu contraseña?" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "%(username)s is banned until %(ban_expires)s" +msgstr "%(username)s está baneado hasta %(ban_expires)s" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "%(username)s is banned" +msgstr "%(username)s está baneado" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "%(username)s is hiding presence" +msgstr "%(username)s está ocultando su presencia" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "%(username)s is online (hidden)" +msgstr "%(username)s está en línea (oculto)" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "%(username)s was last seen %(last_click)s (hidden)" +msgstr "%(username)s fue visto por última vez %(last_click)s (oculto)" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "%(username)s is online" +msgstr "%(username)s está en línea" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "%(username)s was last seen %(last_click)s" +msgstr "%(username)s fue visto por última vez %(last_click)s" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "Banned" +msgstr "Baneado" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "Hidden" +msgstr "Oculto" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "Online (hidden)" +msgstr "En línea (oculto)" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "Offline (hidden)" +msgstr "Desconectado (oculto)" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "Online" +msgstr "En línea" + +#: static/misago/js/misago.js:1 +msgctxt "user status" +msgid "Offline" +msgstr "Desconectado" + +#: static/misago/js/misago.js:1 +msgctxt "username history empty" +msgid "Your account has no history of name changes." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "users list item" +msgid "Joined on %(joined_on)s" +msgstr "Se unió el %(joined_on)s" + +#: static/misago/js/misago.js:1 +msgctxt "users list item" +msgid "Joined %(joined_on)s" +msgstr "Se unió %(joined_on)s" + +#: static/misago/js/misago.js:1 +msgctxt "users list item" +msgid "%(posts)s post" +msgid_plural "%(posts)s posts" +msgstr[0] "%(posts)s post" +msgstr[1] "%(posts)s posts" +msgstr[2] "%(posts)s posts" + +#: static/misago/js/misago.js:1 +msgctxt "users list item" +msgid "%(threads)s thread" +msgid_plural "%(threads)s threads" +msgstr[0] "%(threads)s tema" +msgstr[1] "%(threads)s temas" +msgstr[2] "%(threads)s temas" + +#: static/misago/js/misago.js:1 +msgctxt "users list item" +msgid "%(followers)s follower" +msgid_plural "%(followers)s followers" +msgstr[0] "%(followers)s seguidor" +msgstr[1] "%(followers)s seguidores" +msgstr[2] "%(followers)s seguidores" + +#: static/misago/js/misago.js:1 +msgctxt "yesno switch choice" +msgid "yes" +msgstr "sí" + +#: static/misago/js/misago.js:1 +msgctxt "yesno switch choice" +msgid "no" +msgstr "no" + +#: static/misago/js/misago.js:1 +msgctxt "accept agreement prompt" +msgid "" +"Declining will result in immediate deactivation and deletion of your " +"account. This action is not reversible." +msgstr "" +"Rechazar resultará en la desactivación inmediata y eliminación de tu cuenta." +" Esta acción no es reversible." + +#: static/misago/js/misago.js:1 +msgctxt "accept agreement choice" +msgid "Decline" +msgstr "Rechazar" + +#: static/misago/js/misago.js:1 +msgctxt "accept agreement choice" +msgid "Accept and continue" +msgstr "Aceptar y continuar" + +#: static/misago/js/misago.js:1 +msgctxt "auth message" +msgid "" +"You have signed in as %(username)s. Please refresh the page before " +"continuing." +msgstr "" +"Has iniciado sesión como %(username)s. Por favor, actualiza la página antes " +"de continuar." + +#: static/misago/js/misago.js:1 +msgctxt "auth message" +msgid "" +"%(username)s, you have been signed out. Please refresh the page before " +"continuing." +msgstr "" +"%(username)s, has cerrado sesión. Por favor, actualiza la página antes de " +"continuar." + +#: static/misago/js/misago.js:1 +msgctxt "auth message" +msgid "Reload page" +msgstr "Recargar página" + +#: static/misago/js/misago.js:1 +msgctxt "auth message" +msgid "or press F5 key." +msgstr "o presiona la tecla F5." + +#: static/misago/js/misago.js:1 +msgctxt "categories list" +msgid "No categories exist or you don't have permission to see them." +msgstr "No existen categorías o no tienes permiso para verlas." + +#: static/misago/js/misago.js:1 +msgctxt "category status" +msgid "This category has no new posts. (closed)" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "category status" +msgid "This category has new posts. (closed)" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "category status" +msgid "This category has no new posts." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "category status" +msgid "This category has new posts." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "category last thread" +msgid "Empty category" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "category last thread" +msgid "Private category" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "category last thread" +msgid "Protected category" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "category stats" +msgid "%(threads)s thread" +msgid_plural "%(threads)s threads" +msgstr[0] "%(threads)s tema" +msgstr[1] "%(threads)s temas" +msgstr[2] "%(threads)s temas" + +#: static/misago/js/misago.js:1 +msgctxt "category stats" +msgid "%(posts)s post" +msgid_plural "%(posts)s posts" +msgstr[0] "%(posts)s post" +msgstr[1] "%(posts)s posts" +msgstr[2] "%(posts)s posts" + +#: static/misago/js/misago.js:1 +msgctxt "notifications title" +msgid "Notifications" +msgstr "Notificaciones" + +#: static/misago/js/misago.js:1 +msgctxt "notifications dropdown" +msgid "All" +msgstr "Todo" + +#: static/misago/js/misago.js:1 +msgctxt "notifications dropdown" +msgid "Unread" +msgstr "No leído" + +#: static/misago/js/misago.js:1 +msgctxt "notifications" +msgid "See all notifications" +msgstr "Ver todas las notificaciones" + +#: static/misago/js/misago.js:1 +msgctxt "navbar" +msgid "You have unread notifications!" +msgstr "¡Tienes notificaciones sin leer!" + +#: static/misago/js/misago.js:1 +msgctxt "navbar" +msgid "Open notifications" +msgstr "Abrir notificaciones" + +#: static/misago/js/misago.js:1 +msgctxt "navbar" +msgid "You have unread private threads!" +msgstr "¡Tienes temas privados sin leer!" + +#: static/misago/js/misago.js:1 +msgctxt "navbar" +msgid "Open private threads" +msgstr "Abrir temas privados" + +#: static/misago/js/misago.js:1 +msgctxt "navbar" +msgid "Open search" +msgstr "Abrir búsqueda" + +#: static/misago/js/misago.js:1 +msgctxt "navbar" +msgid "Open menu" +msgstr "Abrir menú" + +#: static/misago/js/misago.js:1 +msgctxt "navbar" +msgid "Open your options" +msgstr "Abrir tus opciones" + +#: static/misago/js/misago.js:1 +msgctxt "notifications nav" +msgid "All" +msgstr "Todo" + +#: static/misago/js/misago.js:1 +msgctxt "notifications nav" +msgid "Unread" +msgstr "No leído" + +#: static/misago/js/misago.js:1 +msgctxt "notifications nav" +msgid "Read" +msgstr "Leído" + +#: static/misago/js/misago.js:1 +msgctxt "notifications pagination" +msgid "Latest" +msgstr "Último" + +#: static/misago/js/misago.js:1 +msgctxt "notifications pagination" +msgid "Newer" +msgstr "Más nuevo" + +#: static/misago/js/misago.js:1 +msgctxt "notifications pagination" +msgid "Older" +msgstr "Más viejo" + +#: static/misago/js/misago.js:1 +msgctxt "notifications" +msgid "Mark all as read" +msgstr "Marcar todo como leído" + +#: static/misago/js/misago.js:1 +msgctxt "notifications title" +msgid "Unread notifications" +msgstr "Notificaciones no leídas" + +#: static/misago/js/misago.js:1 +msgctxt "notifications title" +msgid "Read notifications" +msgstr "Notificaciones leídas" + +#: static/misago/js/misago.js:1 +msgctxt "notifications" +msgid "Mark all notifications as read?" +msgstr "¿Marcar todas las notificaciones como leídas?" + +#: static/misago/js/misago.js:1 +msgctxt "notifications" +msgid "All notifications have been marked as read." +msgstr "Todas las notificaciones han sido marcadas como leídas." + +#: static/misago/js/misago.js:1 +msgctxt "delete your account form" +msgid "Complete the form." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "delete your account title" +msgid "Delete account" +msgstr "Eliminar cuenta" + +#: static/misago/js/misago.js:1 +msgctxt "forum options" +msgid "Change your options" +msgstr "Cambiar tus opciones" + +#: static/misago/js/misago.js:1 +msgctxt "delete your account form" +msgid "This form lets you delete your account. This action is not reversible." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "delete your account form" +msgid "" +"Your account will be deleted together with its profile details, IP addresses" +" and notifications." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "delete your account form" +msgid "" +"Other content will NOT be deleted, but username displayed next to it will be" +" changed to one shared by all deleted accounts." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "delete your account form" +msgid "" +"Your username and e-maill address will become available again for use during" +" registration or for other accounts to change to." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "delete your account form field" +msgid "Enter your password to confirm" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "delete your account form btn" +msgid "Delete my account" +msgstr "Eliminar mi cuenta" + +#: static/misago/js/misago.js:1 +msgctxt "profile details form" +msgid "Your details have been changed." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "edit details" +msgid "Edit details" +msgstr "Editar detalles" + +#: static/misago/js/misago.js:1 +msgctxt "download your data" +msgid "Your request for data download has been registered." +msgstr "Tu solicitud de descarga de datos ha sido registrada." + +#: static/misago/js/misago.js:1 +msgctxt "download your data title" +msgid "Download your data" +msgstr "Descargar tus datos" + +#: static/misago/js/misago.js:1 +msgctxt "download your data" +msgid "" +"To download your data from the site, click the \"Request data download\" " +"button. Depending on amount of data to be archived and number of users " +"wanting to download their data at same time it may take up to few days for " +"your download to be prepared. An e-mail with notification will be sent to " +"you when your data is ready to be downloaded." +msgstr "" +"Para descargar tus datos del sitio, haz clic en el botón \"Solicitar " +"descarga de datos\". Dependiendo de la cantidad de datos que se archiven y " +"del número de usuarios que quieran descargar sus datos al mismo tiempo, " +"puede tardar hasta unos días en prepararse la descarga. Se te enviará un " +"correo electrónico con una notificación cuando tus datos estén listos para " +"ser descargados." + +#: static/misago/js/misago.js:1 +msgctxt "download your data" +msgid "" +"The download will only be available for limited amount of time, after which " +"it will be deleted from the site and marked as expired." +msgstr "" +"La descarga solo estará disponible por un tiempo limitado, después del cual " +"será eliminada del sitio y marcada como expirada." + +#: static/misago/js/misago.js:1 +msgctxt "download your data table" +msgid "Requested on" +msgstr "Solicitado el" + +#: static/misago/js/misago.js:1 +msgctxt "download your data table" +msgid "Download" +msgstr "Descargar" + +#: static/misago/js/misago.js:1 +msgctxt "download your data table" +msgid "You have no data downloads." +msgstr "No tienes descargas de datos." + +#: static/misago/js/misago.js:1 +msgctxt "download your data btn" +msgid "Request data download" +msgstr "Solicitar descarga de datos" + +#: static/misago/js/misago.js:1 +msgctxt "download your data table btn" +msgid "Download is being prepared" +msgstr "Descarga en preparación" + +#: static/misago/js/misago.js:1 +msgctxt "download your data table btn" +msgid "Download your data" +msgstr "Descargar tus datos" + +#: static/misago/js/misago.js:1 +msgctxt "download your data table btn" +msgid "Download is expired" +msgstr "Descarga expirada" + +#: static/misago/js/misago.js:1 +msgctxt "watch thread choice" +msgid "No" +msgstr "No" + +#: static/misago/js/misago.js:1 +msgctxt "watch thread choice" +msgid "Yes, with on site notifications" +msgstr "Sí, con notificaciones en el sitio" + +#: static/misago/js/misago.js:1 +msgctxt "watch thread choice" +msgid "Yes, with on site and e-mail notifications" +msgstr "Sí, con notificaciones en el sitio y por correo electrónico" + +#: static/misago/js/misago.js:1 +msgctxt "notification preference" +msgid "Don't notify" +msgstr "No notificar" + +#: static/misago/js/misago.js:1 +msgctxt "notification preference" +msgid "Notify on site" +msgstr "Notificar en el sitio" + +#: static/misago/js/misago.js:1 +msgctxt "notification preference" +msgid "Notify on site and with e-mail" +msgstr "Notificar en el sitio y por correo electrónico" + +#: static/misago/js/misago.js:1 +msgctxt "private threads preference" +msgid "Anybody can invite me to their private threads" +msgstr "Cualquiera puede invitarme a sus temas privados" + +#: static/misago/js/misago.js:1 +msgctxt "private threads preference" +msgid "Only those I follow can invite me to their private threads" +msgstr "Solo aquellos a quienes sigo pueden invitarme a sus temas privados" + +#: static/misago/js/misago.js:1 +msgctxt "private threads preference" +msgid "Nobody can invite me to their private threads" +msgstr "Nadie puede invitarme a sus temas privados" + +#: static/misago/js/misago.js:1 +msgctxt "forum options form" +msgid "Your forum options have been changed." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "forum options form" +msgid "Please reload the page and try again." +msgstr "Por favor, recarga la página e inténtalo de nuevo." + +#: static/misago/js/misago.js:1 +msgctxt "forum options title" +msgid "Forum options" +msgstr "Opciones del foro" + +#: static/misago/js/misago.js:1 +msgctxt "forum options form title" +msgid "Change forum options" +msgstr "Cambiar opciones del foro" + +#: static/misago/js/misago.js:1 +msgctxt "forum options form" +msgid "Privacy settings" +msgstr "Configuración de privacidad" + +#: static/misago/js/misago.js:1 +msgctxt "forum options form" +msgid "Hide my presence" +msgstr "Ocultar mi presencia" + +#: static/misago/js/misago.js:1 +msgctxt "forum options form" +msgid "" +"If you hide your presence, only members with permission to see hidden " +"presence will see when you are online." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "forum options form" +msgid "Hide my presence from other users" +msgstr "Ocultar mi presencia a otros usuarios" + +#: static/misago/js/misago.js:1 +msgctxt "forum options form" +msgid "Show my presence to other users" +msgstr "Mostrar mi presencia a otros usuarios" + +#: static/misago/js/misago.js:1 +msgctxt "forum options form" +msgid "Limit private thread invitations from other users" +msgstr "Limitar las invitaciones a temas privados de otros usuarios" + +#: static/misago/js/misago.js:1 +msgctxt "notifications options" +msgid "Notifications preferences" +msgstr "Preferencias de notificaciones" + +#: static/misago/js/misago.js:1 +msgctxt "notifications options" +msgid "Automatically watch threads I start" +msgstr "Ver automáticamente los temas que inicio" + +#: static/misago/js/misago.js:1 +msgctxt "notifications options" +msgid "Automatically watch threads I reply to" +msgstr "Ver automáticamente los temas a los que respondo" + +#: static/misago/js/misago.js:1 +msgctxt "notifications options" +msgid "" +"Automatically watch new private threads I'm invited to by the members I am " +"following" +msgstr "" +"Ver automáticamente los nuevos temas privados a los que me invitan los " +"miembros a los que sigo" + +#: static/misago/js/misago.js:1 +msgctxt "notifications options" +msgid "" +"Automatically watch new private threads I'm invited to by other members" +msgstr "" +"Ver automáticamente los nuevos temas privados a los que me invitan otros " +"miembros" + +#: static/misago/js/misago.js:1 +msgctxt "notifications options" +msgid "" +"Notify me about new private thread invitations from the members I am " +"following" +msgstr "" +"Notificarme sobre las nuevas invitaciones a temas privados de los miembros a" +" los que sigo" + +#: static/misago/js/misago.js:1 +msgctxt "notifications options" +msgid "Notify me about new private thread invitations from other members" +msgstr "" +"Notificarme sobre las nuevas invitaciones a temas privados de otros miembros" + +#: static/misago/js/misago.js:1 +msgctxt "forum options form btn" +msgid "Save changes" +msgstr "Guardar cambios" + +#: static/misago/js/misago.js:1 +msgctxt "change username title" +msgid "Change username" +msgstr "Cambiar nombre de usuario" + +#: static/misago/js/misago.js:1 +msgctxt "change username" +msgid "You will be able to change your username %(next_change)s." +msgstr "Podrás cambiar tu nombre de usuario %(next_change)s." + +#: static/misago/js/misago.js:1 +msgctxt "change username" +msgid "You have changed your name allowed number of times." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "change username" +msgid "You can't change your username at the moment." +msgstr "No puedes cambiar tu nombre de usuario en este momento." + +#: static/misago/js/misago.js:1 +msgctxt "change username form" +msgid "You can change your username %(changes_left)s more time." +msgid_plural "You can change your username %(changes_left)s more times." +msgstr[0] "Puedes cambiar tu nombre de usuario %(changes_left)s vez más." +msgstr[1] "Puedes cambiar tu nombre de usuario %(changes_left)s veces más." +msgstr[2] "Puedes cambiar tu nombre de usuario %(changes_left)s veces más." + +#: static/misago/js/misago.js:1 +msgctxt "change username form" +msgid "Used changes become available again after %(name_changes_expire)s day." +msgid_plural "" +"Used changes become available again after %(name_changes_expire)s days." +msgstr[0] "" +"Los cambios usados vuelven a estar disponibles después de " +"%(name_changes_expire)s día." +msgstr[1] "" +"Los cambios usados vuelven a estar disponibles después de " +"%(name_changes_expire)s días." +msgstr[2] "" +"Los cambios usados vuelven a estar disponibles después de " +"%(name_changes_expire)s días." + +#: static/misago/js/misago.js:1 +msgctxt "change username form" +msgid "New username is same as current one." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "change username form field" +msgid "New username" +msgstr "Nuevo nombre de usuario" + +#: static/misago/js/misago.js:1 +msgctxt "change username form btn" +msgid "Change username" +msgstr "Cambiar nombre de usuario" + +#: static/misago/js/misago.js:1 +msgctxt "change username" +msgid "Your username has been changed." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "change email form" +msgid "Fill out all fields." +msgstr "Rellena todos los campos." + +#: static/misago/js/misago.js:1 +msgctxt "change email title" +msgid "Change e-mail address" +msgstr "Cambiar dirección de correo electrónico" + +#: static/misago/js/misago.js:1 +msgctxt "change email form field" +msgid "New e-mail" +msgstr "Nuevo correo electrónico" + +#: static/misago/js/misago.js:1 +msgctxt "change email form field" +msgid "Your current password" +msgstr "Tu contraseña actual" + +#: static/misago/js/misago.js:1 +msgctxt "change email form btn" +msgid "Change e-mail" +msgstr "Cambiar correo electrónico" + +#: static/misago/js/misago.js:1 +msgctxt "change password form" +msgid "Fill out all fields." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "change password form" +msgid "New passwords are different." +msgstr "Las nuevas contraseñas son diferentes." + +#: static/misago/js/misago.js:1 +msgctxt "change password title" +msgid "Change password" +msgstr "Cambiar contraseña" + +#: static/misago/js/misago.js:1 +msgctxt "change password form field" +msgid "New password" +msgstr "Nueva contraseña" + +#: static/misago/js/misago.js:1 +msgctxt "change password form field" +msgid "Repeat password" +msgstr "Repetir contraseña" + +#: static/misago/js/misago.js:1 +msgctxt "change password form field" +msgid "Your current password" +msgstr "Tu contraseña actual" + +#: static/misago/js/misago.js:1 +msgctxt "change password form btn" +msgid "Change password" +msgstr "Cambiar contraseña" + +#: static/misago/js/misago.js:1 +msgctxt "change sign in credentials title" +msgid "Change e-mail or password" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "change sign in credentials" +msgid "" +"You need to set a password for your account to be able to change your e-mail" +" or password." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "change sign in credentials link" +msgid "Set password" +msgstr "Establecer contraseña" + +#: static/misago/js/misago.js:1 +msgctxt "change sign in credentials link" +msgid "Change forgotten password" +msgstr "Cambiar contraseña olvidada" + +#: static/misago/js/misago.js:1 +msgctxt "forum options nav btn" +msgid "Menu" +msgstr "Menú" + +#: static/misago/js/misago.js:1 +msgctxt "profile ban details title" +msgid "Ban details" +msgstr "Detalles del baneo" + +#: static/misago/js/misago.js:1 +msgctxt "profile ban details" +msgid "User-shown ban message" +msgstr "Mensaje de baneo mostrado al usuario" + +#: static/misago/js/misago.js:1 +msgctxt "profile ban details" +msgid "Team-shown ban message" +msgstr "Mensaje de baneo mostrado al equipo" + +#: static/misago/js/misago.js:1 +msgctxt "profile ban details" +msgid "This ban expires on %(expires_on)s." +msgstr "Este baneo expira el %(expires_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "profile ban details" +msgid "This ban expires %(expires_on)s." +msgstr "Este baneo expira %(expires_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "profile ban details" +msgid "This ban has expired." +msgstr "Este baneo ha expirado." + +#: static/misago/js/misago.js:1 +msgctxt "profile ban details" +msgid "%(username)s's ban is permanent." +msgstr "El baneo de %(username)s es permanente." + +#: static/misago/js/misago.js:1 +msgctxt "profile ban details" +msgid "Ban expiration" +msgstr "Expiración del baneo" + +#: static/misago/js/misago.js:1 +msgctxt "profile ban details" +msgid "No ban is active at the moment." +msgstr "No hay ningún baneo activo en este momento." + +#: static/misago/js/misago.js:1 +msgctxt "profile details empty" +msgid "You are not sharing any details with others." +msgstr "No estás compartiendo ningún detalle con los demás." + +#: static/misago/js/misago.js:1 +msgctxt "profile details empty" +msgid "%(username)s is not sharing any details with others." +msgstr "%(username)s no está compartiendo ningún detalle con los demás." + +#: static/misago/js/misago.js:1 +msgctxt "profile details title" +msgid "Details" +msgstr "Detalles" + +#: static/misago/js/misago.js:1 +msgctxt "profile details edit btn" +msgid "Edit" +msgstr "Editar" + +#: static/misago/js/misago.js:1 +msgctxt "profile details form" +msgid "%(username)s's details have been changed." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "profile load more btn" +msgid "Show older activity" +msgstr "Mostrar actividad más antigua" + +#: static/misago/js/misago.js:1 +msgctxt "quick search placeholder" +msgid "Search..." +msgstr "Buscar..." + +#: static/misago/js/misago.js:1 +msgctxt "profile followers title" +msgid "Followers" +msgstr "Seguidores" + +#: static/misago/js/misago.js:1 +msgctxt "profile followers" +msgid "Found %(users)s user." +msgid_plural "Found %(users)s users." +msgstr[0] "Encontró %(users)s usuario." +msgstr[1] "Encontró %(users)s usuarios." +msgstr[2] "Encontró %(users)s usuarios." + +#: static/misago/js/misago.js:1 +msgctxt "profile followers" +msgid "You have %(users)s follower." +msgid_plural "You have %(users)s followers." +msgstr[0] "Tienes %(users)s seguidor." +msgstr[1] "Tienes %(users)s seguidores." +msgstr[2] "Tienes %(users)s seguidores." + +#: static/misago/js/misago.js:1 +msgctxt "profile followers" +msgid "%(username)s has %(users)s follower." +msgid_plural "%(username)s has %(users)s followers." +msgstr[0] "%(username)s tiene %(users)s seguidor." +msgstr[1] "%(username)s tiene %(users)s seguidores." +msgstr[2] "%(username)s tiene %(users)s seguidores." + +#: static/misago/js/misago.js:1 +msgctxt "profile followers" +msgid "Search returned no users matching specified criteria." +msgstr "" +"La búsqueda no ha devuelto ningún usuario que coincida con los criterios " +"especificados." + +#: static/misago/js/misago.js:1 +msgctxt "profile followers" +msgid "You have no followers." +msgstr "No tienes seguidores." + +#: static/misago/js/misago.js:1 +msgctxt "profile followers" +msgid "%(username)s has no followers." +msgstr "%(username)s no tiene seguidores." + +#: static/misago/js/misago.js:1 +msgctxt "profile followers" +msgid "Show more (%(more)s)" +msgstr "Mostrar más (%(more)s)" + +#: static/misago/js/misago.js:1 +msgctxt "profile followers search" +msgid "Search users..." +msgstr "Buscar usuarios..." + +#: static/misago/js/misago.js:1 +msgctxt "profile username history title" +msgid "Username history" +msgstr "Historial de nombres de usuario" + +#: static/misago/js/misago.js:1 +msgctxt "profile username history" +msgid "Found %(changes)s username change." +msgid_plural "Found %(changes)s username changes." +msgstr[0] "Encontró %(changes)s cambio de nombre de usuario." +msgstr[1] "Encontró %(changes)s cambios de nombre de usuario." +msgstr[2] "Encontró %(changes)s cambios de nombre de usuario." + +#: static/misago/js/misago.js:1 +msgctxt "profile username history" +msgid "Your username was changed %(changes)s time." +msgid_plural "Your username was changed %(changes)s times." +msgstr[0] "Tu nombre de usuario fue cambiado %(changes)s vez." +msgstr[1] "Tu nombre de usuario fue cambiado %(changes)s veces." +msgstr[2] "Tu nombre de usuario fue cambiado %(changes)s veces." + +#: static/misago/js/misago.js:1 +msgctxt "profile username history" +msgid "%(username)s's username was changed %(changes)s time." +msgid_plural "%(username)s's username was changed %(changes)s times." +msgstr[0] "El nombre de usuario de %(username)s fue cambiado %(changes)s vez." +msgstr[1] "" +"El nombre de usuario de %(username)s fue cambiado %(changes)s veces." +msgstr[2] "" +"El nombre de usuario de %(username)s fue cambiado %(changes)s veces." + +#: static/misago/js/misago.js:1 +msgctxt "profile username history" +msgid "Loading..." +msgstr "Cargando..." + +#: static/misago/js/misago.js:1 +msgctxt "profile username history" +msgid "Search returned no username changes matching specified criteria." +msgstr "" +"La búsqueda no ha devuelto ningún cambio de nombre de usuario que coincida " +"con los criterios especificados." + +#: static/misago/js/misago.js:1 +msgctxt "profile username history" +msgid "%(username)s's username was never changed." +msgstr "El nombre de usuario de %(username)s nunca ha sido cambiado." + +#: static/misago/js/misago.js:1 +msgctxt "profile username history" +msgid "Show older (%(more)s)" +msgstr "Mostrar más antiguos (%(more)s)" + +#: static/misago/js/misago.js:1 +msgctxt "profile username history search input" +msgid "Search history..." +msgstr "Buscar historial..." + +#: static/misago/js/misago.js:1 +msgctxt "user profile follow btn" +msgid "Following" +msgstr "Siguiendo" + +#: static/misago/js/misago.js:1 +msgctxt "user profile follow btn" +msgid "Follow" +msgstr "Seguir" + +#: static/misago/js/misago.js:1 +msgctxt "profile message btn" +msgid "Message" +msgstr "Mensaje" + +#: static/misago/js/misago.js:1 +msgctxt "profile avatar moderation" +msgid "Avatar controls have been changed." +msgstr "Los controles del avatar han sido cambiados." + +#: static/misago/js/misago.js:1 +msgctxt "profile avatar moderation field" +msgid "Lock avatar" +msgstr "Bloquear avatar" + +#: static/misago/js/misago.js:1 +msgctxt "profile avatar moderation field" +msgid "" +"Locking user avatar will prohibit user from changing his avatar and will " +"reset his/her avatar to default one." +msgstr "" +"El bloqueo del avatar del usuario prohibirá al usuario cambiar su avatar y " +"restablecerá su avatar al predeterminado." + +#: static/misago/js/misago.js:1 +msgctxt "profile avatar moderation field" +msgid "Disallow user from changing avatar" +msgstr "Prohibir al usuario cambiar el avatar" + +#: static/misago/js/misago.js:1 +msgctxt "profile avatar moderation field" +msgid "Allow user to change avatar" +msgstr "Permitir al usuario cambiar el avatar" + +#: static/misago/js/misago.js:1 +msgctxt "profile avatar moderation field" +msgid "User message" +msgstr "Mensaje del usuario" + +#: static/misago/js/misago.js:1 +msgctxt "profile avatar moderation field" +msgid "" +"Optional message for user explaining why they are prohibited from changing " +"their avatar." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "profile avatar moderation field" +msgid "Staff message" +msgstr "Mensaje del equipo" + +#: static/misago/js/misago.js:1 +msgctxt "profile avatar moderation field" +msgid "" +"Optional message for forum team members explaining why the user is " +"prohibited form changing their avatar." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "profile avatar moderation btn" +msgid "Close" +msgstr "Cerrar" + +#: static/misago/js/misago.js:1 +msgctxt "profile avatar moderation btn" +msgid "Save changes" +msgstr "Guardar cambios" + +#: static/misago/js/misago.js:1 +msgctxt "profile avatar moderation title" +msgid "Avatar controls" +msgstr "Controles del avatar" + +#: static/misago/js/misago.js:1 +msgctxt "profile username moderation" +msgid "Username has been changed." +msgstr "El nombre de usuario ha sido cambiado." + +#: static/misago/js/misago.js:1 +msgctxt "profile username moderation field" +msgid "New username" +msgstr "Nuevo nombre de usuario" + +#: static/misago/js/misago.js:1 +msgctxt "profile username moderation btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "profile username moderation btn" +msgid "Change username" +msgstr "Cambiar nombre de usuario" + +#: static/misago/js/misago.js:1 +msgctxt "profile username moderation title" +msgid "Change username" +msgstr "Cambiar nombre de usuario" + +#: static/misago/js/misago.js:1 +msgctxt "profile delete" +msgid "" +"%(username)s's account, threads, posts and other content has been deleted." +msgstr "" +"La cuenta de %(username)s, los temas, los mensajes y otro contenido han sido" +" eliminados." + +#: static/misago/js/misago.js:1 +msgctxt "profile delete" +msgid "" +"%(username)s's account has been deleted and other content has been hidden." +msgstr "" +"La cuenta de %(username)s ha sido eliminada y otro contenido ha sido " +"ocultado." + +#: static/misago/js/misago.js:1 +msgctxt "profile delete btn" +msgid "Delete %(username)s" +msgstr "Eliminar %(username)s" + +#: static/misago/js/misago.js:1 +msgctxt "profile delete btn" +msgid "Please wait... (%(countdown)ss)" +msgstr "Por favor, espera... (%(countdown)ss)" + +#: static/misago/js/misago.js:1 +msgctxt "profile delete" +msgid "User content" +msgstr "Contenido del usuario" + +#: static/misago/js/misago.js:1 +msgctxt "profile delete content" +msgid "Delete together with user's account" +msgstr "Eliminar junto con la cuenta del usuario" + +#: static/misago/js/misago.js:1 +msgctxt "profile delete content" +msgid "Hide after deleting user's account" +msgstr "Ocultar después de eliminar la cuenta del usuario" + +#: static/misago/js/misago.js:1 +msgctxt "profile delete btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "profile delete link" +msgid "Return to users list" +msgstr "Volver a la lista de usuarios" + +#: static/misago/js/misago.js:1 +msgctxt "profile delete title" +msgid "Delete user account" +msgstr "Eliminar cuenta de usuario" + +#: static/misago/js/misago.js:1 +msgctxt "profile moderation menu" +msgid "Avatar controls" +msgstr "Controles del avatar" + +#: static/misago/js/misago.js:1 +msgctxt "profile moderation menu" +msgid "Change username" +msgstr "Cambiar nombre de usuario" + +#: static/misago/js/misago.js:1 +msgctxt "profile moderation menu" +msgid "Delete account" +msgstr "Eliminar cuenta" + +#: static/misago/js/misago.js:1 +msgctxt "profile data list" +msgid "This user's account has been disabled by administrator." +msgstr "La cuenta de este usuario ha sido deshabilitada por el administrador." + +#: static/misago/js/misago.js:1 +msgctxt "profile data list" +msgid "Account disabled" +msgstr "Cuenta deshabilitada" + +#: static/misago/js/misago.js:1 +msgctxt "profile data list" +msgid "Joined on %(joined_on)s" +msgstr "Unido el %(joined_on)s" + +#: static/misago/js/misago.js:1 +msgctxt "profile data list" +msgid "Joined %(joined_on)s" +msgstr "Unido %(joined_on)s" + +#: static/misago/js/misago.js:1 +msgctxt "profile options btn" +msgid "Options" +msgstr "Opciones" + +#: static/misago/js/misago.js:1 +msgctxt "profile posts" +msgid "You have posted no messages." +msgstr "No has publicado ningún mensaje." + +#: static/misago/js/misago.js:1 +msgctxt "profile posts" +msgid "%(username)s posted no messages." +msgstr "%(username)s no ha publicado ningún mensaje." + +#: static/misago/js/misago.js:1 +msgctxt "profile posts" +msgid "You have posted %(posts)s message." +msgid_plural "You have posted %(posts)s messages." +msgstr[0] "Tú has posteado %(posts)s mensaje." +msgstr[1] "Tú has posteado %(posts)s mensajes." +msgstr[2] "Tú has posteado %(posts)s mensajes." + +#: static/misago/js/misago.js:1 +msgctxt "profile posts" +msgid "%(username)s has posted %(posts)s message." +msgid_plural "%(username)s has posted %(posts)s messages." +msgstr[0] "%(username)s ha posteado %(posts)s mensaje." +msgstr[1] "%(username)s ha posteado %(posts)s mensajes." +msgstr[2] "%(username)s ha posteado %(posts)s mensajes." + +#: static/misago/js/misago.js:1 +msgctxt "profile posts" +msgid "Loading..." +msgstr "Cargando..." + +#: static/misago/js/misago.js:1 +msgctxt "profile posts title" +msgid "Posts" +msgstr "Posts" + +#: static/misago/js/misago.js:1 +msgctxt "profile threads" +msgid "You haven't started any threads." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "profile threads" +msgid "%(username)s hasn't started any threads" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "profile threads" +msgid "You have started %(threads)s thread." +msgid_plural "You have started %(threads)s threads." +msgstr[0] "Tú has iniciado %(threads)s tema." +msgstr[1] "Tú has iniciado %(threads)s temas." +msgstr[2] "Tú has iniciado %(threads)s temas." + +#: static/misago/js/misago.js:1 +msgctxt "profile threads" +msgid "%(username)s has started %(threads)s thread." +msgid_plural "%(username)s has started %(threads)s threads." +msgstr[0] "%(username)s ha iniciado %(threads)s tema." +msgstr[1] "%(username)s ha iniciado %(threads)s temas." +msgstr[2] "%(username)s ha iniciado %(threads)s temas." + +#: static/misago/js/misago.js:1 +msgctxt "profile threads" +msgid "Loading..." +msgstr "Cargando..." + +#: static/misago/js/misago.js:1 +msgctxt "profile threads title" +msgid "Threads" +msgstr "Temas" + +#: static/misago/js/misago.js:1 +msgctxt "profile follows title" +msgid "Follows" +msgstr "Seguidos" + +#: static/misago/js/misago.js:1 +msgctxt "profile follows" +msgid "Found %(users)s user." +msgid_plural "Found %(users)s users." +msgstr[0] "Encontró %(users)s usuario." +msgstr[1] "Encontró %(users)s usuarios." +msgstr[2] "Encontró %(users)s usuarios." + +#: static/misago/js/misago.js:1 +msgctxt "profile follows" +msgid "You are following %(users)s user." +msgid_plural "You are following %(users)s users." +msgstr[0] "Estás siguiendo %(users)s usuario." +msgstr[1] "Estás siguiendo %(users)s usuarios." +msgstr[2] "Estás siguiendo %(users)s usuarios." + +#: static/misago/js/misago.js:1 +msgctxt "profile follows" +msgid "%(username)s is following %(users)s user." +msgid_plural "%(username)s is following %(users)s users." +msgstr[0] "%(username)s está siguiendo %(users)s usuario." +msgstr[1] "%(username)s está siguiendo %(users)s usuarios." +msgstr[2] "%(username)s está siguiendo %(users)s usuarios." + +#: static/misago/js/misago.js:1 +msgctxt "profile follows" +msgid "Loading..." +msgstr "Cargando..." + +#: static/misago/js/misago.js:1 +msgctxt "profile follows" +msgid "Search returned no users matching specified criteria." +msgstr "" +"La búsqueda no ha devuelto ningún usuario que coincida con los criterios " +"especificados." + +#: static/misago/js/misago.js:1 +msgctxt "profile follows" +msgid "You are not following any users." +msgstr "No estás siguiendo a ningún usuario." + +#: static/misago/js/misago.js:1 +msgctxt "profile follows" +msgid "%(username)s is not following any users." +msgstr "%(username)s no está siguiendo a ningún usuario." + +#: static/misago/js/misago.js:1 +msgctxt "request activation link form" +msgid "Enter a valid e-mail address." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "request activation link form field" +msgid "Your e-mail address" +msgstr "Tu dirección de correo electrónico" + +#: static/misago/js/misago.js:1 +msgctxt "request activation link form btn" +msgid "Send link" +msgstr "Enviar enlace" + +#: static/misago/js/misago.js:1 +msgctxt "request activation link form" +msgid "Activation link was sent to %(email)s" +msgstr "El enlace de activación ha sido enviado a %(email)s" + +#: static/misago/js/misago.js:1 +msgctxt "request activation link form btn" +msgid "Request another link" +msgstr "Solicitar otro enlace" + +#: static/misago/js/misago.js:1 +msgctxt "request password reset form" +msgid "Enter a valid e-mail address." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "request password reset form field" +msgid "Your e-mail address" +msgstr "Tu dirección de correo electrónico" + +#: static/misago/js/misago.js:1 +msgctxt "request password reset form btn" +msgid "Send link" +msgstr "Enviar enlace" + +#: static/misago/js/misago.js:1 +msgctxt "request password reset form" +msgid "Reset password link was sent to %(email)s" +msgstr "El enlace para restablecer la contraseña ha sido enviado a %(email)s" + +#: static/misago/js/misago.js:1 +msgctxt "request password reset form btn" +msgid "Request another link" +msgstr "Solicitar otro enlace" + +#: static/misago/js/misago.js:1 +msgctxt "request password reset form error" +msgid "Activate your account." +msgstr "Activa tu cuenta." + +#: static/misago/js/misago.js:1 +msgctxt "request password reset form error" +msgid "Your account is inactive." +msgstr "Tu cuenta está inactiva." + +#: static/misago/js/misago.js:1 +msgctxt "password reset form" +msgid "Enter new password." +msgstr "Introduce una nueva contraseña." + +#: static/misago/js/misago.js:1 +msgctxt "password reset form field" +msgid "Enter new password" +msgstr "Introduce una nueva contraseña" + +#: static/misago/js/misago.js:1 +msgctxt "password reset form btn" +msgid "Change password" +msgstr "Cambiar contraseña" + +#: static/misago/js/misago.js:1 +msgctxt "password reset form" +msgid "%(username)s, your password has been changed." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "password reset form" +msgid "Sign in using new password to continue." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "password reset form btn" +msgid "Sign in" +msgstr "Iniciar sesión" + +#: static/misago/js/misago.js:1 +msgctxt "search form" +msgid "You have to enter search query." +msgstr "Debes introducir una consulta de búsqueda." + +#: static/misago/js/misago.js:1 +msgctxt "search form title" +msgid "Search" +msgstr "Buscar" + +#: static/misago/js/misago.js:1 +msgctxt "search form input" +msgid "Search" +msgstr "Buscar" + +#: static/misago/js/misago.js:1 +msgctxt "search form btn" +msgid "Search" +msgstr "Buscar" + +#: static/misago/js/misago.js:1 +msgctxt "search time" +msgid "Search took %(time)s s" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "search threads btn" +msgid "Show more" +msgstr "Mostrar más" + +#: static/misago/js/misago.js:1 +msgctxt "search threads" +msgid "Loading results..." +msgstr "Cargando resultados..." + +#: static/misago/js/misago.js:1 +msgctxt "search threads" +msgid "No threads matching search query have been found." +msgstr "No se han encontrado temas que coincidan con la consulta de búsqueda." + +#: static/misago/js/misago.js:1 +msgctxt "search threads" +msgid "Enter at least two characters to search threads." +msgstr "Introduce al menos dos caracteres para buscar temas." + +#: static/misago/js/misago.js:1 +msgctxt "search users" +msgid "Loading results..." +msgstr "Cargando resultados..." + +#: static/misago/js/misago.js:1 +msgctxt "search users" +msgid "No users matching search query have been found." +msgstr "" +"No se han encontrado usuarios que coincidan con la consulta de búsqueda." + +#: static/misago/js/misago.js:1 +msgctxt "search users" +msgid "Enter at least two characters to search users." +msgstr "Introduce al menos dos caracteres para buscar usuarios." + +#: static/misago/js/misago.js:1 +msgctxt "social auth title" +msgid "Sign in with %(backend)s" +msgstr "Iniciar sesión con %(backend)s" + +#: static/misago/js/misago.js:1 +msgctxt "social auth form" +msgid "Fill out all fields." +msgstr "Rellena todos los campos." + +#: static/misago/js/misago.js:1 +msgctxt "social auth form" +msgid "Your e-mail address has been verified by %(backend)s." +msgstr "" +"Tu dirección de correo electrónico ha sido verificada por %(backend)s." + +#: static/misago/js/misago.js:1 +msgctxt "social auth form title" +msgid "Complete your account" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "social auth form field" +msgid "Username" +msgstr "Nombre de usuario" + +#: static/misago/js/misago.js:1 +msgctxt "social auth form field" +msgid "E-mail address" +msgstr "Dirección de correo electrónico" + +#: static/misago/js/misago.js:1 +msgctxt "social auth form btn" +msgid "Sign in" +msgstr "Iniciar sesión" + +#: static/misago/js/misago.js:1 +msgctxt "social auth complete" +msgid "" +"%(username)s, your account has been created and you have been signed in to " +"it." +msgstr "%(username)s, tu cuenta ha sido creada y has iniciado sesión en ella." + +#: static/misago/js/misago.js:1 +msgctxt "social auth complete title" +msgid "Registration completed!" +msgstr "¡Registro completado!" + +#: static/misago/js/misago.js:1 +msgctxt "social auth complete link" +msgid "Return to forum index" +msgstr "Volver al índice del foro" + +#: static/misago/js/misago.js:1 +msgctxt "add private thread participant" +msgid "You have to enter user name." +msgstr "Debes introducir el nombre de usuario." + +#: static/misago/js/misago.js:1 +msgctxt "add private thread participant" +msgid "New participant has been added to thread." +msgstr "Se ha añadido un nuevo participante al tema." + +#: static/misago/js/misago.js:1 +msgctxt "add private thread participant field" +msgid "User to add" +msgstr "Usuario a añadir" + +#: static/misago/js/misago.js:1 +msgctxt "add private thread participant btn" +msgid "Add participant" +msgstr "Añadir participante" + +#: static/misago/js/misago.js:1 +msgctxt "add private thread participant btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "add private thread participant modal title" +msgid "Add participant" +msgstr "Añadir participante" + +#: static/misago/js/misago.js:1 +msgctxt "add participant btn" +msgid "Add participant" +msgstr "Añadir participante" + +#: static/misago/js/misago.js:1 +msgctxt "private thread owner change" +msgid "Are you sure you want to take over this thread?" +msgstr "¿Estás seguro de que quieres hacerte cargo de este tema?" + +#: static/misago/js/misago.js:1 +msgctxt "private thread owner change" +msgid "Are you sure you want to change thread owner to %(user)s?" +msgstr "" +"¿Estás seguro de que quieres cambiar el propietario del tema a %(user)s?" + +#: static/misago/js/misago.js:1 +msgctxt "thread participants actions" +msgid "%(user)s has been made new thread owner." +msgstr "%(user)s ha sido nombrado nuevo propietario del tema." + +#: static/misago/js/misago.js:1 +msgctxt "private thread owner change btn" +msgid "Make owner" +msgstr "Hacer propietario" + +#: static/misago/js/misago.js:1 +msgctxt "private thread leave" +msgid "Are you sure you want to leave this thread?" +msgstr "¿Estás seguro de que quieres abandonar este tema?" + +#: static/misago/js/misago.js:1 +msgctxt "private thread leave" +msgid "Are you sure you want to remove %(user)s from this thread?" +msgstr "¿Estás seguro de que quieres eliminar a %(user)s de este tema?" + +#: static/misago/js/misago.js:1 +msgctxt "thread participants actions" +msgid "You have left this thread." +msgstr "Has abandonado este tema." + +#: static/misago/js/misago.js:1 +msgctxt "thread participants actions" +msgid "%(user)s has been removed from this thread." +msgstr "%(user)s ha sido eliminado de este tema." + +#: static/misago/js/misago.js:1 +msgctxt "private thread leave btn" +msgid "Leave thread" +msgstr "Abandonar tema" + +#: static/misago/js/misago.js:1 +msgctxt "private thread leave btn" +msgid "Remove" +msgstr "Eliminar" + +#: static/misago/js/misago.js:1 +msgctxt "thread participants profile link" +msgid "See profile" +msgstr "Ver perfil" + +#: static/misago/js/misago.js:1 +msgctxt "thread participants owner status" +msgid "Thread owner" +msgstr "Propietario del tema" + +#: static/misago/js/misago.js:1 +msgctxt "thread participants stat" +msgid "This thread has %(users)s participant." +msgid_plural "This thread has %(users)s participants." +msgstr[0] "Este tema tiene %(users)s participante." +msgstr[1] "Este tema tiene %(users)s participantes." +msgstr[2] "Este tema tiene %(users)s participantes." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "%(votes)s vote, %(proc)s% of total." +msgid_plural "%(votes)s votes, %(proc)s% of total." +msgstr[0] "%(votes)s voto, %(proc)s% del total." +msgstr[1] "%(votes)s votos, %(proc)s% del total." +msgstr[2] "%(votes)s votos, %(proc)s% del total." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "You've voted on this choice." +msgstr "Has votado en esta opción." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Poll votes" +msgstr "Votos de la encuesta" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "%(votes)s user has voted for this choice." +msgid_plural "%(votes)s users have voted for this choice." +msgstr[0] "%(votes)s usuario ha votado por esta opción." +msgstr[1] "%(votes)s usuarios han votado por esta opción." +msgstr[2] "%(votes)s usuarios han votado por esta opción." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Vote" +msgstr "Votar" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "See votes" +msgstr "Ver votos" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Edit" +msgstr "Editar" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "" +"Are you sure you want to delete this poll? This action is not reversible." +msgstr "" +"¿Estás seguro de que quieres eliminar esta encuesta? Esta acción no se puede" +" deshacer." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Poll has been deleted" +msgstr "La encuesta ha sido eliminada" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Delete" +msgstr "Eliminar" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Started by %(poster)s %(posted_on)s." +msgstr "Iniciado por %(poster)s %(posted_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Voting ends %(ends_on)s." +msgstr "La votación termina %(ends_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "%(votes)s vote." +msgid_plural "%(votes)s votes." +msgstr[0] "%(votes)s voto." +msgstr[1] "%(votes)s votos." +msgstr[2] "%(votes)s votos." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Voting is public." +msgstr "La votación es pública." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "You can't select any more choices." +msgstr "No puedes seleccionar más opciones." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "You can select %(choices)s more choice." +msgid_plural "You can select %(choices)s more choices." +msgstr[0] "Puedes seleccionar %(choices)s opción más." +msgstr[1] "Puedes seleccionar %(choices)s opciones más." +msgstr[2] "Puedes seleccionar %(choices)s opciones más." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "You can change your vote later." +msgstr "Puedes cambiar tu voto más tarde." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Votes are final." +msgstr "Los votos son definitivos." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll vote" +msgid "You need to select at least one choice." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll vote" +msgid "Your vote has been saved." +msgstr "Tu voto ha sido guardado." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll vote btn" +msgid "Save your vote" +msgstr "Guardar tu voto" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll vote btn" +msgid "See results" +msgstr "Ver resultados" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Add choice" +msgstr "Añadir opción" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Are you sure you want to remove this choice?" +msgstr "¿Estás seguro de que quieres eliminar esta opción?" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Remove this choice" +msgstr "Eliminar esta opción" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Poll choice" +msgstr "Opción de la encuesta" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Are you sure you want to discard changes?" +msgstr "¿Estás seguro de que quieres descartar los cambios?" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Are you sure you want to discard new poll?" +msgstr "¿Estás seguro de que quieres descartar la nueva encuesta?" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Poll has been edited." +msgstr "La encuesta ha sido editada." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Poll has been posted." +msgstr "La encuesta ha sido publicada." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Edit poll" +msgstr "Editar encuesta" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Add poll" +msgstr "Añadir encuesta" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Question and choices" +msgstr "Pregunta y opciones" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Poll question" +msgstr "Pregunta de la encuesta" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Available choices" +msgstr "Opciones disponibles" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Voting" +msgstr "Votación" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Poll length" +msgstr "Duración de la encuesta" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "" +"Enter number of days for which voting in this poll should be possible or " +"zero to run this poll indefinitely." +msgstr "" +"Introduce el número de días durante los cuales se podrá votar en esta " +"encuesta o cero para ejecutar esta encuesta indefinidamente." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Allowed choices" +msgstr "Opciones permitidas" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Allow vote changes" +msgstr "Permitir cambios de voto" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Allow participants to change their vote" +msgstr "Permitir a los participantes cambiar su voto" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Don't allow participants to change their vote" +msgstr "No permitir a los participantes cambiar su voto" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Save changes" +msgstr "Guardar cambios" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Post poll" +msgstr "Postear encuesta" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Make voting public" +msgstr "Hacer pública la votación" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "" +"Making voting public will allow everyone to access detailed list of votes, " +"showing which users voted for which choices and at which times. This option " +"can't be changed after poll's creation. Moderators may see voting details " +"for all polls." +msgstr "" +"Hacer pública la votación permitirá a todo el mundo acceder a una lista " +"detallada de votos, mostrando qué usuarios votaron por qué opciones y en qué" +" momentos. Esta opción no se puede cambiar después de la creación de la " +"encuesta. Los moderadores pueden ver los detalles de la votación de todas " +"las encuestas." + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Votes are public" +msgstr "Los votos son públicos" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll" +msgid "Votes are hidden" +msgstr "Los votos están ocultos" + +#: static/misago/js/misago.js:1 +msgctxt "event hide btn" +msgid "Hide" +msgstr "Ocultar" + +#: static/misago/js/misago.js:1 +msgctxt "event reveal btn" +msgid "Unhide" +msgstr "Mostrar" + +#: static/misago/js/misago.js:1 +msgctxt "event delete" +msgid "" +"Are you sure you wish to delete this event? This action is not reversible!" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "event delete" +msgid "Event has been deleted." +msgstr "El evento ha sido eliminado." + +#: static/misago/js/misago.js:1 +msgctxt "event delete btn" +msgid "Delete" +msgstr "Eliminar" + +#: static/misago/js/misago.js:1 +msgctxt "event info" +msgid "Hidden by %(event_by)s %(event_on)s." +msgstr "Ocultado por %(event_by)s %(event_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "event info" +msgid "By %(event_by)s %(event_on)s." +msgstr "Por %(event_by)s %(event_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Thread has been pinned globally." +msgstr "El tema ha sido fijado globalmente." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Thread has been pinned in category." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Thread has been unpinned." +msgstr "El tema ha sido desanclado." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Thread has been approved." +msgstr "El tema ha sido aprobado." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Thread has been opened." +msgstr "El tema ha sido abierto." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Thread has been closed." +msgstr "El tema ha sido cerrado." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Thread has been revealed." +msgstr "El tema ha sido mostrado." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Thread has been made hidden." +msgstr "El tema ha sido ocultado." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Took thread over." +msgstr "Se hizo cargo del tema." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Owner has left thread. This thread is now closed." +msgstr "El propietario ha abandonado el tema. Este tema está ahora cerrado." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Participant has left thread." +msgstr "El participante ha abandonado el tema." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Thread title has been changed from %(old_title)s." +msgstr "El título del tema ha sido cambiado desde %(old_title)s." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Thread has been moved from %(from_category)s." +msgstr "El tema ha sido movido desde %(from_category)s." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "The %(merged_thread)s thread has been merged into this thread." +msgstr "El tema %(merged_thread)s ha sido fusionado en este tema." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Changed thread owner to %(user)s." +msgstr "Cambiado el propietario del tema a %(user)s." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Added %(user)s to thread." +msgstr "Añadido %(user)s al tema." + +#: static/misago/js/misago.js:1 +msgctxt "event message" +msgid "Removed %(user)s from thread." +msgstr "Eliminado %(user)s del tema." + +#: static/misago/js/misago.js:1 +msgctxt "event unread label" +msgid "New event" +msgstr "Nuevo evento" + +#: static/misago/js/misago.js:1 +msgctxt "post attachment" +msgid "%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s." +msgstr "%(filetype)s, %(size)s, subido por %(uploader)s %(uploaded_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "post body hidden" +msgid "Hidden by %(hidden_by)s %(hidden_on)s." +msgstr "Ocultado por %(hidden_by)s %(hidden_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "post body hidden" +msgid "This post is hidden. You cannot see its contents." +msgstr "Este mensaje está oculto. No puedes ver su contenido." + +#: static/misago/js/misago.js:1 +msgctxt "post best answer flag" +msgid "Marked as best answer by you %(marked_on)s." +msgstr "Marcado como mejor respuesta por ti %(marked_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "post best answer flag" +msgid "Marked as best answer by %(marked_by)s %(marked_on)s." +msgstr "Marcado como mejor respuesta por %(marked_by)s %(marked_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "post hidden flag" +msgid "This post is hidden. Only users with permission may see its contents." +msgstr "" +"Este mensaje está oculto. Solo los usuarios con permiso pueden ver su " +"contenido." + +#: static/misago/js/misago.js:1 +msgctxt "post unapproved flag" +msgid "" +"This post is unapproved. Only users with permission to approve posts and its" +" author may see its contents." +msgstr "" +"Este mensaje no está aprobado. Solo los usuarios con permiso para aprobar " +"mensajes y su autor pueden ver su contenido." + +#: static/misago/js/misago.js:1 +msgctxt "post protected flag" +msgid "This post is protected. Only moderators may change it." +msgstr "Este mensaje está protegido. Solo los moderadores pueden cambiarlo." + +#: static/misago/js/misago.js:1 +msgctxt "post likes modal" +msgid "No users have liked this post." +msgstr "Ningún usuario ha dado me gusta a este mensaje." + +#: static/misago/js/misago.js:1 +msgctxt "post likes modal title" +msgid "Post Likes" +msgstr "Me gusta del mensaje" + +#: static/misago/js/misago.js:1 +msgctxt "post likes modal" +msgid "%(likes)s like" +msgid_plural "%(likes)s likes" +msgstr[0] "%(likes)s me gusta" +msgstr[1] "%(likes)s me gusta" +msgstr[2] "%(likes)s me gusta" + +#: static/misago/js/misago.js:1 +msgctxt "post footer btn" +msgid "Best answer" +msgstr "Mejor respuesta" + +#: static/misago/js/misago.js:1 +msgctxt "post footer btn" +msgid "Liked" +msgstr "Me gusta" + +#: static/misago/js/misago.js:1 +msgctxt "post footer btn" +msgid "Like" +msgstr "Me gusta" + +#: static/misago/js/misago.js:1 +msgctxt "post likes" +msgid "%(user)s likes this." +msgstr "%(user)s le gusta esto." + +#: static/misago/js/misago.js:1 +msgctxt "post likes" +msgid "%(users)s and %(last_user)s" +msgstr "%(users)s y %(last_user)s" + +#: static/misago/js/misago.js:1 +msgctxt "post likes" +msgid "%(users)s like this." +msgstr "%(users)s les gusta esto." + +#: static/misago/js/misago.js:1 +msgctxt "post likes" +msgid "%(users)s and %(likes)s other user like this." +msgid_plural "%(users)s and %(likes)s other users like this." +msgstr[0] "%(users)s y %(likes)s otro usuario les gusta esto." +msgstr[1] "%(users)s y %(likes)s otros usuarios les gusta esto." +msgstr[2] "%(users)s y %(likes)s otros usuarios les gusta esto." + +#: static/misago/js/misago.js:1 +msgctxt "post footer btn" +msgid "Reply" +msgstr "Responder" + +#: static/misago/js/misago.js:1 +msgctxt "post footer btn" +msgid "Quote" +msgstr "Citar" + +#: static/misago/js/misago.js:1 +msgctxt "post footer btn" +msgid "Edit" +msgstr "Editar" + +#: static/misago/js/misago.js:1 +msgctxt "post move modal" +msgid "You have to enter link to the other thread." +msgstr "Debes introducir el enlace al otro tema." + +#: static/misago/js/misago.js:1 +msgctxt "post move modal" +msgid "Selected post was moved to the other thread." +msgstr "El mensaje seleccionado fue movido al otro tema." + +#: static/misago/js/misago.js:1 +msgctxt "post move modal field" +msgid "Link to thread you want to move post to" +msgstr "Enlace al tema al que quieres mover el mensaje" + +#: static/misago/js/misago.js:1 +msgctxt "post move modal btn" +msgid "Move post" +msgstr "Mover mensaje" + +#: static/misago/js/misago.js:1 +msgctxt "post move modal title" +msgid "Move post" +msgstr "Mover mensaje" + +#: static/misago/js/misago.js:1 +msgctxt "post revert btn" +msgid "Revert post to state from before this edit." +msgstr "Revertir el mensaje al estado anterior a esta edición." + +#: static/misago/js/misago.js:1 +msgctxt "post revert btn" +msgid "Revert" +msgstr "Revertir" + +#: static/misago/js/misago.js:1 +msgctxt "post history modal btn" +msgid "See previous change" +msgstr "Ver cambio anterior" + +#: static/misago/js/misago.js:1 +msgctxt "post history modal btn" +msgid "See next change" +msgstr "Ver cambio siguiente" + +#: static/misago/js/misago.js:1 +msgctxt "post history modal" +msgid "By %(edited_by)s %(edited_on)s." +msgstr "Por %(edited_by)s %(edited_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "post revert" +msgid "" +"Are you sure you with to revert this post to the state from before this " +"edit?" +msgstr "" +"¿Estás seguro de que quieres revertir este mensaje al estado anterior a esta" +" edición?" + +#: static/misago/js/misago.js:1 +msgctxt "post revert" +msgid "Post has been reverted to previous state." +msgstr "El mensaje ha sido revertido al estado anterior." + +#: static/misago/js/misago.js:1 +msgctxt "post history modal title" +msgid "Post edits history" +msgstr "Postear historial de ediciones" + +#: static/misago/js/misago.js:1 +msgctxt "thread hidden switch choice" +msgid "No" +msgstr "No" + +#: static/misago/js/misago.js:1 +msgctxt "thread hidden switch choice" +msgid "Yes" +msgstr "Sí" + +#: static/misago/js/misago.js:1 +msgctxt "thread closed switch choice" +msgid "No" +msgstr "No" + +#: static/misago/js/misago.js:1 +msgctxt "thread closed switch choice" +msgid "Yes" +msgstr "Sí" + +#: static/misago/js/misago.js:1 +msgctxt "post split modal" +msgid "Selected post was split into new thread." +msgstr "El mensaje seleccionado fue dividido en un nuevo tema." + +#: static/misago/js/misago.js:1 +msgctxt "thread weight choice" +msgid "Not pinned" +msgstr "No fijado" + +#: static/misago/js/misago.js:1 +msgctxt "thread weight choice" +msgid "Pinned in category" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "thread weight choice" +msgid "Pinned globally" +msgstr "Fijado globalmente" + +#: static/misago/js/misago.js:1 +msgctxt "posts split modal field" +msgid "Thread weight" +msgstr "Peso del tema" + +#: static/misago/js/misago.js:1 +msgctxt "posts split modal field" +msgid "Hide thread" +msgstr "Ocultar tema" + +#: static/misago/js/misago.js:1 +msgctxt "posts split modal field" +msgid "Close thread" +msgstr "Cerrar tema" + +#: static/misago/js/misago.js:1 +msgctxt "posts split modal field" +msgid "Thread title" +msgstr "Título del tema" + +#: static/misago/js/misago.js:1 +msgctxt "posts split modal field" +msgid "Category" +msgstr "Categoría" + +#: static/misago/js/misago.js:1 +msgctxt "posts split modal btn" +msgid "Split post" +msgstr "Dividir mensaje" + +#: static/misago/js/misago.js:1 +msgctxt "post split modal" +msgid "You can't move this post at the moment." +msgstr "No puedes mover este mensaje en este momento." + +#: static/misago/js/misago.js:1 +msgctxt "posts split modal title" +msgid "Split post into new thread" +msgstr "Dividir mensaje en un nuevo tema" + +#: static/misago/js/misago.js:1 +msgctxt "post permalink" +msgid "Permament link to this post:" +msgstr "Enlace permanente a este mensaje:" + +#: static/misago/js/misago.js:1 +msgctxt "post options permalink btn" +msgid "Permament link" +msgstr "Enlace permanente" + +#: static/misago/js/misago.js:1 +msgctxt "post options edit btn" +msgid "Edit" +msgstr "Editar" + +#: static/misago/js/misago.js:1 +msgctxt "post options best answer btn" +msgid "Mark as best answer" +msgstr "Marcar como mejor respuesta" + +#: static/misago/js/misago.js:1 +msgctxt "post options best answer btn" +msgid "Unmark best answer" +msgstr "Desmarcar mejor respuesta" + +#: static/misago/js/misago.js:1 +msgctxt "post edits" +msgid "This post was edited %(edits)s time." +msgid_plural "This post was edited %(edits)s times." +msgstr[0] "Este mensaje fue editado %(edits)s vez." +msgstr[1] "Este mensaje fue editado %(edits)s veces." +msgstr[2] "Este mensaje fue editado %(edits)s veces." + +#: static/misago/js/misago.js:1 +msgctxt "post options edit btn" +msgid "Changes history" +msgstr "Historial de cambios" + +#: static/misago/js/misago.js:1 +msgctxt "post options approve btn" +msgid "Approve" +msgstr "Aprobar" + +#: static/misago/js/misago.js:1 +msgctxt "post options move btn" +msgid "Move" +msgstr "Mover" + +#: static/misago/js/misago.js:1 +msgctxt "post options split btn" +msgid "Split" +msgstr "Dividir" + +#: static/misago/js/misago.js:1 +msgctxt "post options protect btn" +msgid "Protect" +msgstr "Proteger" + +#: static/misago/js/misago.js:1 +msgctxt "post options protect btn" +msgid "Remove protection" +msgstr "Eliminar protección" + +#: static/misago/js/misago.js:1 +msgctxt "post options hide btn" +msgid "Hide" +msgstr "Ocultar" + +#: static/misago/js/misago.js:1 +msgctxt "post options hide btn" +msgid "Unhide" +msgstr "Mostrar" + +#: static/misago/js/misago.js:1 +msgctxt "post delete" +msgid "" +"Are you sure you want to delete this post? This action is not reversible!" +msgstr "" +"¿Estás seguro de que quieres eliminar este mensaje? ¡Esta acción no se puede" +" deshacer!" + +#: static/misago/js/misago.js:1 +msgctxt "post delete" +msgid "Post has been deleted." +msgstr "El post ha sido eliminado." + +#: static/misago/js/misago.js:1 +msgctxt "post options delete btn" +msgid "Delete" +msgstr "Eliminar" + +#: static/misago/js/misago.js:1 +msgctxt "post unread label" +msgid "New post" +msgstr "Nuevo mensaje" + +#: static/misago/js/misago.js:1 +msgctxt "post unread label" +msgid "New" +msgstr "Nuevo" + +#: static/misago/js/misago.js:1 +msgctxt "post timestamp" +msgid "posted %(posted_on)s" +msgstr "posteado %(posted_on)s" + +#: static/misago/js/misago.js:1 +msgctxt "post edits stat" +msgid "This post was edited %(edits)s time." +msgid_plural "This post was edited %(edits)s times." +msgstr[0] "Este mensaje fue editado %(edits)s vez." +msgstr[1] "Este mensaje fue editado %(edits)s veces." +msgstr[2] "Este mensaje fue editado %(edits)s veces." + +#: static/misago/js/misago.js:1 +msgctxt "post edits stat" +msgid "edited %(edits)s time" +msgid_plural "edited %(edits)s times" +msgstr[0] "editado %(edits)s vez" +msgstr[1] "editado %(edits)s veces" +msgstr[2] "editado %(edits)s veces" + +#: static/misago/js/misago.js:1 +msgctxt "post edits stat" +msgid "%(edits)s edit" +msgid_plural "%(edits)s edits" +msgstr[0] "%(edits)s edición" +msgstr[1] "%(edits)s ediciones" +msgstr[2] "%(edits)s ediciones" + +#: static/misago/js/misago.js:1 +msgctxt "post protected label" +msgid "This post is protected and may not be edited." +msgstr "Este mensaje está protegido y no se puede editar." + +#: static/misago/js/misago.js:1 +msgctxt "post protected label" +msgid "protected" +msgstr "protegido" + +#: static/misago/js/misago.js:1 +msgctxt "poster stats" +msgid "%(posts)s post" +msgid_plural "%(posts)s posts" +msgstr[0] "%(posts)s mensaje" +msgstr[1] "%(posts)s mensajes" +msgstr[2] "%(posts)s mensajes" + +#: static/misago/js/misago.js:1 +msgctxt "thread starter info" +msgid "Thread author" +msgstr "Autor del tema" + +#: static/misago/js/misago.js:1 +msgctxt "thread starter info" +msgid "Started on: %(timestamp)s" +msgstr "Comenzado en: %(timestamp)s" + +#: static/misago/js/misago.js:1 +msgctxt "thread title form" +msgid "You have to enter thread title." +msgstr "Debes introducir el título del tema." + +#: static/misago/js/misago.js:1 +msgctxt "thread title form field" +msgid "Thread title" +msgstr "Título del tema" + +#: static/misago/js/misago.js:1 +msgctxt "thread title form btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "thread title form btn" +msgid "Change title" +msgstr "Cambiar título" + +#: static/misago/js/misago.js:1 +msgctxt "thread title form title" +msgid "Change title" +msgstr "Cambiar título" + +#: static/misago/js/misago.js:1 +msgctxt "thread merge form" +msgid "Thread has been merged with other one." +msgstr "El tema ha sido fusionado con otro." + +#: static/misago/js/misago.js:1 +msgctxt "thread merge form" +msgid "You have to enter link to the other thread." +msgstr "Debes introducir el enlace al otro tema." + +#: static/misago/js/misago.js:1 +msgctxt "thread merge form field" +msgid "Link to thread you want to merge with" +msgstr "Enlace al tema con el que quieres fusionar" + +#: static/misago/js/misago.js:1 +msgctxt "thread merge form field" +msgid "" +"Merge will delete current thread and move its contents to the thread " +"specified here." +msgstr "" +"La fusión eliminará el tema actual y moverá su contenido al tema " +"especificado aquí." + +#: static/misago/js/misago.js:1 +msgctxt "thread merge form btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "thread merge form btn" +msgid "Merge thread" +msgstr "Fusionar tema" + +#: static/misago/js/misago.js:1 +msgctxt "thread merge form title" +msgid "Merge thread" +msgstr "Fusionar tema" + +#: static/misago/js/misago.js:1 +msgctxt "thread move form" +msgid "Thread has been moved." +msgstr "El tema ha sido movido." + +#: static/misago/js/misago.js:1 +msgctxt "thread move form field" +msgid "New category" +msgstr "Nueva categoría" + +#: static/misago/js/misago.js:1 +msgctxt "thread move form btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "thread move form btn" +msgid "Move thread" +msgstr "Mover tema" + +#: static/misago/js/misago.js:1 +msgctxt "thread move form title" +msgid "Move thread" +msgstr "Mover tema" + +#: static/misago/js/misago.js:1 +msgctxt "thread move form" +msgid "You can't move this thread at the moment." +msgstr "No puedes mover este tema en este momento." + +#: static/misago/js/misago.js:1 +msgctxt "thread move form dismiss btn" +msgid "Ok" +msgstr "Ok" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation" +msgid "Thread has been pinned globally." +msgstr "El tema ha sido fijado globalmente." + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation" +msgid "Thread has been pinned in category." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation" +msgid "Thread has been unpinned." +msgstr "El tema ha sido desanclado." + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation" +msgid "Thread has been approved." +msgstr "El tema ha sido aprobado." + +#: static/misago/js/misago.js:1 +msgid "Thread has been opened." +msgstr "El tema ha sido abierto." + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation" +msgid "Thread has been closed." +msgstr "El tema ha sido cerrado." + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation" +msgid "Thread has been made visible." +msgstr "El tema ha sido mostrado." + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation" +msgid "Thread has been made hidden." +msgstr "El tema ha sido ocultado." + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation" +msgid "Are you sure you want to delete this thread?" +msgstr "¿Estás seguro de que quieres eliminar este tema?" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation" +msgid "Thread has been deleted." +msgstr "El tema ha sido eliminado." + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation btn" +msgid "Change title" +msgstr "Cambiar título" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation btn" +msgid "Pin globally" +msgstr "Fijar globalmente" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation btn" +msgid "Pin in category" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation btn" +msgid "Unpin" +msgstr "Desanclar" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation btn" +msgid "Move" +msgstr "Mover" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation btn" +msgid "Merge" +msgstr "Fusionar" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation btn" +msgid "Approve" +msgstr "Aprobar" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation btn" +msgid "Open" +msgstr "Abrir" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation btn" +msgid "Close" +msgstr "Cerrar" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation btn" +msgid "Unhide" +msgstr "Mostrar" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation btn" +msgid "Hide" +msgstr "Ocultar" + +#: static/misago/js/misago.js:1 +msgctxt "thread moderation btn" +msgid "Delete" +msgstr "Eliminar" + +#: static/misago/js/misago.js:1 +msgctxt "thread options btn" +msgid "Thread options" +msgstr "Opciones del tema" + +#: static/misago/js/misago.js:1 +msgctxt "watch thread" +msgid "Watching" +msgstr "Observando" + +#: static/misago/js/misago.js:1 +msgctxt "watch thread" +msgid "Watch" +msgstr "Observar" + +#: static/misago/js/misago.js:1 +msgctxt "watch thread" +msgid "Notify about new replies" +msgstr "Notificar sobre nuevas respuestas" + +#: static/misago/js/misago.js:1 +msgctxt "watch thread" +msgid "On site and with e-mail" +msgstr "En el sitio y con correo electrónico" + +#: static/misago/js/misago.js:1 +msgctxt "watch thread" +msgid "On site only" +msgstr "Solo en el sitio" + +#: static/misago/js/misago.js:1 +msgctxt "watch thread" +msgid "Don't notify" +msgstr "No notificar" + +#: static/misago/js/misago.js:1 +msgctxt "breadcrumb" +msgid "Threads" +msgstr "Temas" + +#: static/misago/js/misago.js:1 +msgctxt "breadcrumb" +msgid "Private threads" +msgstr "Temas privados" + +#: static/misago/js/misago.js:1 +msgctxt "paginator" +msgid "Go to first page" +msgstr "Ir a la primera página" + +#: static/misago/js/misago.js:1 +msgctxt "paginator" +msgid "Go to previous page" +msgstr "Ir a la página anterior" + +#: static/misago/js/misago.js:1 +msgctxt "paginator" +msgid "Page %(page)s of %(pages)s" +msgstr "Página %(page)s de %(pages)s" + +#: static/misago/js/misago.js:1 +msgctxt "paginator input" +msgid "Page" +msgstr "Página" + +#: static/misago/js/misago.js:1 +msgctxt "paginator" +msgid "Go" +msgstr "Ir" + +#: static/misago/js/misago.js:1 +msgctxt "paginator" +msgid "Go to next page" +msgstr "Ir a la página siguiente" + +#: static/misago/js/misago.js:1 +msgctxt "paginator" +msgid "Go to last page" +msgstr "Ir a la última página" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation modal title" +msgid "Moderation" +msgstr "Moderación" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation modal" +msgid "One or more posts could not be changed:" +msgstr "Uno o más mensajes no se pudieron cambiar:" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation modal" +msgid "%(username)s on %(posted_on)s" +msgstr "%(username)s en %(posted_on)s" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation move" +msgid "You have to enter link to the other thread." +msgstr "Debes introducir el enlace al otro tema." + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation move" +msgid "Selected posts were moved to the other thread." +msgstr "Los mensajes seleccionados fueron movidos al otro tema." + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation move" +msgid "Link to thread you want to move posts to" +msgstr "Enlace al tema al que quieres mover los mensajes" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation move btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation move btn" +msgid "Move posts" +msgstr "Mover mensajes" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation move title" +msgid "Move posts" +msgstr "Mover mensajes" + +#: static/misago/js/misago.js:1 +msgctxt "posts moderation split" +msgid "Selected posts were split into new thread." +msgstr "Los mensajes seleccionados fueron divididos en un nuevo tema." + +#: static/misago/js/misago.js:1 +msgctxt "posts moderation split field" +msgid "Thread weight" +msgstr "Peso del tema" + +#: static/misago/js/misago.js:1 +msgctxt "posts moderation split field" +msgid "Hide thread" +msgstr "Ocultar tema" + +#: static/misago/js/misago.js:1 +msgctxt "posts moderation split field" +msgid "Close thread" +msgstr "Cerrar tema" + +#: static/misago/js/misago.js:1 +msgctxt "posts moderation split field" +msgid "Thread title" +msgstr "Título del tema" + +#: static/misago/js/misago.js:1 +msgctxt "posts moderation split field" +msgid "Category" +msgstr "Categoría" + +#: static/misago/js/misago.js:1 +msgctxt "posts moderation split btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "posts moderation split btn" +msgid "Split posts" +msgstr "Dividir mensajes" + +#: static/misago/js/misago.js:1 +msgctxt "posts moderation split" +msgid "You can't move selected posts at the moment." +msgstr "No puedes mover los mensajes seleccionados en este momento." + +#: static/misago/js/misago.js:1 +msgctxt "posts moderation split dismiss btn" +msgid "Ok" +msgstr "Ok" + +#: static/misago/js/misago.js:1 +msgctxt "posts moderation split title" +msgid "Split posts into new thread" +msgstr "Dividir mensajes en un nuevo tema" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation" +msgid "Approve" +msgstr "Aprobar" + +#: static/misago/js/misago.js:1 +msgctxt "merge posts" +msgid "" +"Are you sure you want to merge selected posts? This action is not " +"reversible!" +msgstr "" +"¿Estás seguro de que quieres fusionar los mensajes seleccionados? ¡Esta " +"acción no se puede deshacer!" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation" +msgid "Merge" +msgstr "Fusionar" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation" +msgid "Move" +msgstr "Mover" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation" +msgid "Split" +msgstr "Dividir" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation" +msgid "Protect" +msgstr "Proteger" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation" +msgid "Unprotect" +msgstr "Desproteger" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation" +msgid "Hide" +msgstr "Ocultar" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation" +msgid "Unhide" +msgstr "Mostrar" + +#: static/misago/js/misago.js:1 +msgctxt "delete posts" +msgid "" +"Are you sure you want to delete selected posts? This action is not " +"reversible!" +msgstr "" +"¿Estás seguro de que quieres eliminar los mensajes seleccionados? ¡Esta " +"acción no se puede deshacer!" + +#: static/misago/js/misago.js:1 +msgctxt "thread posts moderation" +msgid "Delete" +msgstr "Eliminar" + +#: static/misago/js/misago.js:1 +msgctxt "post options btn" +msgid "Posts options" +msgstr "Opciones de mensajes" + +#: static/misago/js/misago.js:1 +msgctxt "thread reply btn" +msgid "Reply" +msgstr "Responder" + +#: static/misago/js/misago.js:1 +msgctxt "go up" +msgid "Go to top" +msgstr "Ir arriba" + +#: static/misago/js/misago.js:1 +msgctxt "thread poll btn" +msgid "Add poll" +msgstr "Añadir encuesta" + +#: static/misago/js/misago.js:1 +msgctxt "thread shortcuts btn" +msgid "Shortcuts" +msgstr "Atajos" + +#: static/misago/js/misago.js:1 +msgctxt "thread shortcut btn" +msgid "Go to first post" +msgstr "Ir al primer mensaje" + +#: static/misago/js/misago.js:1 +msgctxt "thread shortcut btn" +msgid "Go to new post" +msgstr "Ir al nuevo mensaje" + +#: static/misago/js/misago.js:1 +msgctxt "thread shortcut btn" +msgid "Go to best answer" +msgstr "Ir a la mejor respuesta" + +#: static/misago/js/misago.js:1 +msgctxt "thread shortcut btn" +msgid "Go to unapproved post" +msgstr "Ir al mensaje no aprobado" + +#: static/misago/js/misago.js:1 +msgctxt "thread shortcut btn" +msgid "Go to last post" +msgstr "Ir al último mensaje" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation title" +msgid "Threads moderation" +msgstr "Moderación de temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "One or more threads could not be deleted:" +msgstr "Uno o más temas no se pudieron eliminar:" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation merge field" +msgid "Thread weight" +msgstr "Peso del tema" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation merge field" +msgid "Hide thread" +msgstr "Ocultar tema" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation merge field" +msgid "Close thread" +msgstr "Cerrar tema" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation merge field" +msgid "Thread title" +msgstr "Título del tema" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation merge field" +msgid "Category" +msgstr "Categoría" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation merge btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation merge btn" +msgid "Merge threads" +msgstr "Fusionar temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation merge" +msgid "" +"You can't merge threads because there are no categories you are allowed to " +"move them to." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation merge" +msgid "" +"You need permission to start threads in category to be able to merge threads" +" to it." +msgstr "" +"Necesitas permiso para iniciar temas en la categoría para poder fusionar " +"temas en ella." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation merge dismiss btn" +msgid "Ok" +msgstr "Ok" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation merge title" +msgid "Merge threads" +msgstr "Fusionar temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation move" +msgid "Selected threads were moved." +msgstr "Los temas seleccionados fueron movidos." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation move field" +msgid "New category" +msgstr "Nueva categoría" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation move btn" +msgid "Cancel" +msgstr "Cancelar" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation move btn" +msgid "Move threads" +msgstr "Mover temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation move" +msgid "" +"You can't move threads because there are no categories you are allowed to " +"move them to." +msgstr "" +"No puedes mover temas porque no hay categorías a las que se te permita " +"moverlos." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation move" +msgid "" +"You need permission to start threads in category to be able to move threads " +"to it." +msgstr "" +"Necesitas permiso para iniciar temas en la categoría para poder mover temas " +"a ella." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation move dismiss btn" +msgid "Ok" +msgstr "Ok" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation move title" +msgid "Move threads" +msgstr "Mover temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "Selected threads were pinned globally." +msgstr "Los temas seleccionados fueron fijados globalmente." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "Selected threads were pinned in category." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "Selected threads were unpinned." +msgstr "Los temas seleccionados fueron desanclados." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "Selected threads were approved." +msgstr "Los temas seleccionados fueron aprobados." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "Selected threads were opened." +msgstr "Los temas seleccionados fueron abiertos." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "Selected threads were closed." +msgstr "Los temas seleccionados fueron cerrados." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "Selected threads were unhidden." +msgstr "Los temas seleccionados fueron mostrados." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "Selected threads were hidden." +msgstr "Los temas seleccionados fueron ocultados." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "You don't have permission to merge this thread with others." +msgstr "No tienes permiso para fusionar este tema con otros." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "You have to select at least two threads to merge." +msgstr "Debes seleccionar al menos dos temas para fusionar." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "Are you sure you want to delete selected threads?" +msgstr "¿Estás seguro de que quieres eliminar los temas seleccionados?" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation" +msgid "Selected threads were deleted." +msgstr "Los temas seleccionados fueron eliminados." + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Select all" +msgstr "Seleccionar todo" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Select none" +msgstr "Seleccionar ninguno" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Pin threads globally" +msgstr "Fijar temas globalmente" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Pin threads in categories" +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Unpin threads" +msgstr "Desanclar temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Move threads" +msgstr "Mover temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Merge threads" +msgstr "Fusionar temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Approve threads" +msgstr "Aprobar temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Open threads" +msgstr "Abrir temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Close threads" +msgstr "Cerrar temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Unhide threads" +msgstr "Mostrar temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Hide threads" +msgstr "Ocultar temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads moderation btn" +msgid "Delete threads" +msgstr "Eliminar temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads list nav" +msgid "Moderation" +msgstr "Moderación" + +#: static/misago/js/misago.js:1 +msgctxt "threads list nav" +msgid "All categories" +msgstr "Todas las categorías" + +#: static/misago/js/misago.js:1 +msgctxt "threads list nav" +msgid "All subcategories" +msgstr "Todas las subcategorías" + +#: static/misago/js/misago.js:1 +msgctxt "threads list nav" +msgid "Start thread" +msgstr "Iniciar tema" + +#: static/misago/js/misago.js:1 +msgctxt "threads list empty" +msgid "There are no threads on the site yet." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "threads list empty" +msgid "This category is empty." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "threads list empty" +msgid "No threads matching specified criteria were found." +msgstr "" +"No se encontraron temas que coincidan con los criterios especificados." + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "%(timestamp)s - latest activity" +msgstr "%(timestamp)s - última actividad" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "%(poster)s - latest poster" +msgstr "%(poster)s - último mensaje" + +#: static/misago/js/misago.js:1 +msgctxt "watch thread" +msgid "Send e-mail notifications" +msgstr "Enviar notificaciones por correo electrónico" + +#: static/misago/js/misago.js:1 +msgctxt "watch thread" +msgid "Without e-mail notifications" +msgstr "Sin notificaciones por correo electrónico" + +#: static/misago/js/misago.js:1 +msgid "Not watching" +msgstr "No observando" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "Contains unread posts" +msgstr "Contiene mensajes no leídos" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "No unread posts" +msgstr "No hay mensajes no leídos" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "%(starter)s - original poster" +msgstr "%(starter)s - autor original" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "Go to page: %(page)s" +msgstr "Ir a la página: %(page)s" + +#: static/misago/js/misago.js:1 +msgctxt "threads list update prompt" +msgid "There is %(threads)s new or updated thread. Click here to show it." +msgid_plural "" +"There are %(threads)s new or updated threads. Click here to show them." +msgstr[0] "" +"Hay %(threads)s tema nuevo o actualizado. Haz clic aquí para mostrarlo." +msgstr[1] "" +"Hay %(threads)s temas nuevos o actualizados. Haz clic aquí para mostrarlos." +msgstr[2] "" +"Hay %(threads)s temas nuevos o actualizados. Haz clic aquí para mostrarlos." + +#: static/misago/js/misago.js:1 +msgctxt "threads list title" +msgid "Threads" +msgstr "Temas" + +#: static/misago/js/misago.js:1 +msgctxt "threas list more btn" +msgid "Show more" +msgstr "Mostrar más" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "All" +msgstr "Todos" + +#: static/misago/js/misago.js:1 +msgid "All threads" +msgstr "Todos los temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "My" +msgstr "Mis" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "My threads" +msgstr "Mis temas" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "New" +msgstr "Nuevo" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "New threads" +msgstr "Temas nuevos" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "Unread" +msgstr "No leído" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "Unread threads" +msgstr "Temas no leídos" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "Watched" +msgstr "Observado" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "Watched threads" +msgstr "Temas observados" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "Unapproved" +msgstr "No aprobado" + +#: static/misago/js/misago.js:1 +msgctxt "threads list" +msgid "Unapproved content" +msgstr "Contenido no aprobado" + +#: static/misago/js/misago.js:1 +msgctxt "private threads title" +msgid "Private threads" +msgstr "Temas privados" + +#: static/misago/js/misago.js:1 +msgctxt "private threads list" +msgid "" +"Private threads are threads which only those that started them and those " +"they have invited may see and participate in." +msgstr "" +"Los temas privados son temas que solo aquellos que los iniciaron y aquellos " +"a los que invitaron pueden ver y participar en ellos." + +#: static/misago/js/misago.js:1 +msgctxt "private threads list empty" +msgid "You aren't participating in any private threads." +msgstr "No estás participando en ningún tema privado." + +#: static/misago/js/misago.js:1 +msgctxt "top posters empty" +msgid "No users have posted any new messages during last %(days)s day." +msgid_plural "" +"No users have posted any new messages during last %(days)s days." +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: static/misago/js/misago.js:1 +msgctxt "top posters list item" +msgid "Rank" +msgstr "Rango" + +#: static/misago/js/misago.js:1 +msgctxt "top posters list item" +msgid "Ranked posts" +msgstr "Mensajes clasificados" + +#: static/misago/js/misago.js:1 +msgctxt "top posters list item" +msgid "Total posts" +msgstr "Mensajes totales" + +#: static/misago/js/misago.js:1 +msgctxt "top posters list" +msgid "%(posters)s top poster from last %(days)s days." +msgid_plural "%(posters)s top posters from last %(days)s days." +msgstr[0] "%(posters)s mejor poster de los últimos %(days)s días." +msgstr[1] "%(posters)s mejores posters de los últimos %(days)s días." +msgstr[2] "%(posters)s mejores posters de los últimos %(days)s días." + +#: static/misago/js/misago.js:1 +msgctxt "users page title" +msgid "Users" +msgstr "Usuarios" + +#: static/misago/js/misago.js:1 +msgctxt "rank users list" +msgid "There is %(more)s more user with this rank." +msgid_plural "There are %(more)s more users with this rank." +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: static/misago/js/misago.js:1 +msgctxt "rank users list empty" +msgid "There are no more users with this rank." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "rank users list paginator" +msgid "Go to first page" +msgstr "Ir a la primera página" + +#: static/misago/js/misago.js:1 +msgctxt "rank users list paginator" +msgid "Go to previous page" +msgstr "Ir a la página anterior" + +#: static/misago/js/misago.js:1 +msgctxt "rank users list paginator" +msgid "Go to next page" +msgstr "Ir a la página siguiente" + +#: static/misago/js/misago.js:1 +msgctxt "rank users list paginator" +msgid "Go to last page" +msgstr "Ir a la última página" + +#: static/misago/js/misago.js:1 +msgctxt "rank users list" +msgid "There are no users with this rank at the moment." +msgstr "No hay usuarios con este rango en este momento." + +#: static/misago/js/misago.js:1 +msgctxt "ajax client error" +msgid "Could not connect to the site." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "ajax client error" +msgid "Action link is invalid." +msgstr "El enlace de acción no es válido." + +#: static/misago/js/misago.js:1 +msgctxt "ajax client error" +msgid "Unknown error has occurred." +msgstr "Ha ocurrido un error desconocido." + +#: static/misago/js/misago.js:1 +msgctxt "api error" +msgid "Could not connect to the site." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "api error" +msgid "Upload was rejected by the site as too large." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "api error" +msgid "Action link is invalid." +msgstr "El enlace de acción no es válido." + +#: static/misago/js/misago.js:1 +msgctxt "api error" +msgid "Unknown error has occurred." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "captcha field" +msgid "Failed to load CAPTCHA." +msgstr "Error al cargar CAPTCHA." + +#: static/misago/js/misago.js:1 +msgctxt "captcha field" +msgid "Please solve the quick test" +msgstr "Por favor, resuelve la prueba rápida" + +#: static/misago/js/misago.js:1 +msgctxt "captcha field" +msgid "This test helps us prevent automated spam registrations on the site." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "page title pagination" +msgid "page: %(page)s" +msgstr "página: %(page)s" + +#: static/misago/js/misago.js:1 +msgid "You are already working on other message. Do you want to discard it?" +msgstr "Ya estás trabajando en otro mensaje. ¿Quieres descartarlo?" + +#: static/misago/js/misago.js:1 +msgctxt "api error" +msgid "You don't have permission to perform this action." +msgstr "No tienes permiso para realizar esta acción." + +#: static/misago/js/misago.js:1 +msgctxt "banned page" +msgid "This ban expires on %(expires_on)s." +msgstr "Este baneo expira el %(expires_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "banned page" +msgid "This ban expires %(expires_on)s." +msgstr "Este baneo expira %(expires_on)s." + +#: static/misago/js/misago.js:1 +msgctxt "banned page" +msgid "This ban has expired." +msgstr "Este baneo ha expirado." + +#: static/misago/js/misago.js:1 +msgctxt "banned page" +msgid "This ban is permanent." +msgstr "Este baneo es permanente." + +#: static/misago/js/misago.js:1 +msgctxt "banned error title" +msgid "You are banned" +msgstr "Estás baneado" + +#: static/misago/js/misago.js:1 +msgctxt "agreement validator" +msgid "You have to accept the terms of service." +msgstr "Debes aceptar los términos del servicio." + +#: static/misago/js/misago.js:1 +msgctxt "agreement validator" +msgid "You have to accept the privacy policy." +msgstr "Debes aceptar la política de privacidad." + +#: static/misago/js/misago.js:1 +msgctxt "email validator" +msgid "Enter a valid e-mail address." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "value length validator" +msgid "" +"Ensure this value has at least %(limit_value)s character (it has " +"%(show_value)s)." +msgid_plural "" +"Ensure this value has at least %(limit_value)s characters (it has " +"%(show_value)s)." +msgstr[0] "" +"Asegúrate de que este valor tiene al menos %(limit_value)s carácter (tiene " +"%(show_value)s)." +msgstr[1] "" +"Asegúrate de que este valor tiene al menos %(limit_value)s caracteres (tiene" +" %(show_value)s)." +msgstr[2] "" +"Asegúrate de que este valor tiene al menos %(limit_value)s caracteres (tiene" +" %(show_value)s)." + +#: static/misago/js/misago.js:1 +msgctxt "value length validator" +msgid "" +"Ensure this value has at most %(limit_value)s character (it has " +"%(show_value)s)." +msgid_plural "" +"Ensure this value has at most %(limit_value)s characters (it has " +"%(show_value)s)." +msgstr[0] "" +"Asegúrate de que este valor tiene como máximo %(limit_value)s carácter " +"(tiene %(show_value)s)." +msgstr[1] "" +"Asegúrate de que este valor tiene como máximo %(limit_value)s caracteres " +"(tiene %(show_value)s)." +msgstr[2] "" +"Asegúrate de que este valor tiene como máximo %(limit_value)s caracteres " +"(tiene %(show_value)s)." + +#: static/misago/js/misago.js:1 +msgctxt "username length validator" +msgid "Username must be at least %(limit_value)s character long." +msgid_plural "Username must be at least %(limit_value)s characters long." +msgstr[0] "El nombre de usuario debe tener al menos %(limit_value)s carácter." +msgstr[1] "" +"El nombre de usuario debe tener al menos %(limit_value)s caracteres." +msgstr[2] "" +"El nombre de usuario debe tener al menos %(limit_value)s caracteres." + +#: static/misago/js/misago.js:1 +msgctxt "username length validator" +msgid "Username cannot be longer than %(limit_value)s character." +msgid_plural "Username cannot be longer than %(limit_value)s characters." +msgstr[0] "" +"El nombre de usuario no puede tener más de %(limit_value)s carácter." +msgstr[1] "" +"El nombre de usuario no puede tener más de %(limit_value)s caracteres." +msgstr[2] "" +"El nombre de usuario no puede tener más de %(limit_value)s caracteres." + +#: static/misago/js/misago.js:1 +msgctxt "username validator" +msgid "Username must contain Latin alphabet letters or digits." +msgstr "" + +#: static/misago/js/misago.js:1 +msgctxt "username validator" +msgid "" +"Username can only contain Latin alphabet letters, digits, and an underscore " +"sign." +msgstr "" +"El nombre de usuario solo puede contener letras del alfabeto latino, dígitos" +" y un signo de subrayado." + +#: static/misago/js/misago.js:1 +msgctxt "password length validator" +msgid "Valid password must be at least %(limit_value)s character long." +msgid_plural "" +"Valid password must be at least %(limit_value)s characters long." +msgstr[0] "La contraseña válida debe tener al menos %(limit_value)s carácter." +msgstr[1] "" +"La contraseña válida debe tener al menos %(limit_value)s caracteres." +msgstr[2] "" +"La contraseña válida debe tener al menos %(limit_value)s caracteres." + +#: static/misago/js/vendor.js:2 +msgid "');l(e).replaceWith(a),a.wrap('
')}};function p(e){const t=function(e){let t=e;return"https://"===e.substr(0,8)?t=t.substr(8):"http://"===e.substr(0,7)&&(t=t.substr(7)),"www."===t.substr(0,4)&&(t=t.substr(4)),t}(e),s=function(e){if(-1===e.indexOf("youtu"))return null;const t=e.match(d);return t?t[1]:null}(t);if(!s)return null;let a=0;if(t.indexOf("?")>0){const e=t.substr(t.indexOf("?")+1).split("&").filter((e=>"t="===e.substr(0,2)))[0];if(e){const t=e.substr(2).split("m");"s"===t[0].substr(-1)?a+=parseInt(t[0].substr(0,t[0].length-1)):(a+=60*parseInt(t[0]),t[1]&&"s"===t[1].substr(-1)&&(a+=parseInt(t[1].substr(0,t[1].length-1))))}}return{start:a,video:s}}var u=s(19755),h=class extends n().Component{componentDidMount(){c.render(this.documentNode),u(this.documentNode).find(".spoiler-reveal").click(m)}componentDidUpdate(e,t){c.render(this.documentNode),u(this.documentNode).find(".spoiler-reveal").click(m)}shouldComponentUpdate(e,t){return e.markup!==this.props.markup}render(){return n().createElement("article",{className:i()("misago-markup",this.props.className),dangerouslySetInnerHTML:{__html:this.props.markup},"data-author":this.props.author||void 0,ref:e=>{this.documentNode=e}})}};function m(e){var t=e.target;u(t).parent().parent().addClass("revealed")}},3784:function(e,t,s){"use strict";var a,i=s(22928),o=s(57588),n=s.n(o),r=s(37848);t.Z=class extends n().Component{render(){return a||(a=(0,i.Z)("div",{className:"modal-body modal-loader"},void 0,(0,i.Z)(r.Z,{})))}}},30337:function(e,t,s){"use strict";var a=s(22928),i=(s(57588),s(33556));t.Z=class extends i.Z{getHelpText(){return this.props.helpText?(0,a.Z)("p",{className:"help-block"},void 0,this.props.helpText):null}render(){return(0,a.Z)("div",{className:"modal-body"},void 0,(0,a.Z)("div",{className:"message-icon"},void 0,(0,a.Z)("span",{className:"material-icon"},void 0,this.props.icon||"info_outline")),(0,a.Z)("div",{className:"message-body"},void 0,(0,a.Z)("p",{className:"lead"},void 0,this.props.message),this.getHelpText(),(0,a.Z)("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},void 0,pgettext("modal message dismiss btn","Ok"))))}}},95187:function(e,t,s){"use strict";var a,i=s(22928),o=s(57588),n=s.n(o),r=s(37848);t.Z=class extends n().Component{render(){return a||(a=(0,i.Z)("div",{className:"panel-body panel-body-loading"},void 0,(0,i.Z)(r.Z,{className:"loader loader-spaced"})))}}},33556:function(e,t,s){"use strict";var a=s(22928),i=s(57588),o=s.n(i);t.Z=class extends o().Component{getHelpText(){return this.props.helpText?(0,a.Z)("p",{className:"help-block"},void 0,this.props.helpText):null}render(){return(0,a.Z)("div",{className:"panel-body panel-message-body"},void 0,(0,a.Z)("div",{className:"message-icon"},void 0,(0,a.Z)("span",{className:"material-icon"},void 0,this.props.icon||"info_outline")),(0,a.Z)("div",{className:"message-body"},void 0,(0,a.Z)("p",{className:"lead"},void 0,this.props.message),this.getHelpText()))}}},11005:function(e,t,s){"use strict";s.d(t,{Z:function(){return w}});var a=s(22928),i=s(57588),o=s.n(i),n=s(69092);function r(e){return e.post.content?o().createElement(l,e):o().createElement(d,e)}function l(e){return(0,a.Z)("div",{className:"post-body"},void 0,(0,a.Z)(n.Z,{markup:e.post.content}))}function d(e){return(0,a.Z)("div",{className:"post-body post-body-invalid"},void 0,(0,a.Z)("p",{className:"lead"},void 0,pgettext("posts feed item body","This post's contents cannot be displayed.")),(0,a.Z)("p",{className:"text-muted"},void 0,pgettext("posts feed item body","This error is caused by invalid post content manipulation.")))}function c(e){let{post:t}=e;const{category:s,thread:i}=t,o=interpolate(pgettext("posts feed item header","posted %(posted_on)s"),{posted_on:t.posted_on.format("LL, LT")},!0);return(0,a.Z)("div",{className:"post-heading"},void 0,(0,a.Z)("a",{className:"btn btn-link item-title",href:i.url},void 0,i.title),(0,a.Z)("a",{className:"btn btn-link post-category",href:s.url.index},void 0,s.name),(0,a.Z)("a",{href:t.url.index,className:"btn btn-link posted-on",title:o},void 0,t.posted_on.fromNow()))}var p,u,h=s(19605);function m(e){let{post:t}=e;return(0,a.Z)("a",{className:"btn btn-default btn-icon pull-right",href:t.url.index},void 0,(0,a.Z)("span",{className:"btn-text-left hidden-xs"},void 0,pgettext("go to post link","See post")),p||(p=(0,a.Z)("span",{className:"material-icon"},void 0,"chevron_right")))}function v(e){let{post:t}=e;return(0,a.Z)("div",{className:"post-side post-side-anonymous"},void 0,(0,a.Z)(m,{post:t}),(0,a.Z)("div",{className:"media"},void 0,u||(u=(0,a.Z)("div",{className:"media-left"},void 0,(0,a.Z)("span",{},void 0,(0,a.Z)(h.ZP,{className:"poster-avatar",size:50})))),(0,a.Z)("div",{className:"media-body"},void 0,(0,a.Z)("div",{className:"media-heading"},void 0,(0,a.Z)("span",{className:"item-title"},void 0,t.poster_name)),(0,a.Z)("span",{className:"user-title user-title-anonymous"},void 0,pgettext("post removed poster username","Removed user")))))}function g(e){let{rank:t,title:s}=e,i=s||t.title||t.name,o="user-title";return t.css_class&&(o+=" user-title-"+t.css_class),t.is_tab?(0,a.Z)("a",{className:o,href:t.url},void 0,i):(0,a.Z)("span",{className:o},void 0,i)}function Z(e){let{post:t,poster:s}=e;return(0,a.Z)("div",{className:"post-side post-side-registered"},void 0,(0,a.Z)(m,{post:t}),(0,a.Z)("div",{className:"media"},void 0,(0,a.Z)("div",{className:"media-left"},void 0,(0,a.Z)("a",{href:s.url},void 0,(0,a.Z)(h.ZP,{className:"poster-avatar",size:50,user:s}))),(0,a.Z)("div",{className:"media-body"},void 0,(0,a.Z)("div",{className:"media-heading"},void 0,(0,a.Z)("a",{className:"item-title",href:s.url},void 0,s.username)),(0,a.Z)(g,{title:s.title,rank:s.rank}))))}function f(e){let{post:t,poster:s}=e;return s&&s.id?(0,a.Z)(Z,{post:t,poster:s}):(0,a.Z)(v,{post:t})}function b(e){let{post:t,poster:s}=e;const i=s||t.poster;let o="post";return i&&i.rank.css_class&&(o+=" post-"+i.rank.css_class),(0,a.Z)("li",{className:o,id:"post-"+t.id},void 0,(0,a.Z)("div",{className:"panel panel-default panel-post"},void 0,(0,a.Z)("div",{className:"panel-body"},void 0,(0,a.Z)("div",{className:"panel-content"},void 0,(0,a.Z)(f,{post:t,poster:i}),(0,a.Z)(c,{post:t}),(0,a.Z)(r,{post:t})))))}var _,N,x=s(44039);function y(){return(0,a.Z)("ul",{className:"posts-list post-feed ui-preview"},void 0,(0,a.Z)("li",{className:"post"},void 0,(0,a.Z)("div",{className:"panel panel-default panel-post"},void 0,(0,a.Z)("div",{className:"panel-body"},void 0,(0,a.Z)("div",{className:"panel-content"},void 0,(0,a.Z)("div",{className:"post-side post-side-anonymous"},void 0,(0,a.Z)("div",{className:"media"},void 0,_||(_=(0,a.Z)("div",{className:"media-left"},void 0,(0,a.Z)("span",{},void 0,(0,a.Z)(h.ZP,{className:"poster-avatar",size:50})))),(0,a.Z)("div",{className:"media-body"},void 0,(0,a.Z)("div",{className:"media-heading"},void 0,(0,a.Z)("span",{className:"item-title"},void 0,(0,a.Z)("span",{className:"ui-preview-text",style:{width:x.e(30,200)+"px"}},void 0," "))),(0,a.Z)("span",{className:"user-title user-title-anonymous"},void 0,(0,a.Z)("span",{className:"ui-preview-text",style:{width:x.e(30,200)+"px"}},void 0," "))))),(0,a.Z)("div",{className:"post-heading"},void 0,(0,a.Z)("span",{className:"ui-preview-text",style:{width:x.e(30,200)+"px"}},void 0," ")),(0,a.Z)("div",{className:"post-body"},void 0,(0,a.Z)("article",{className:"misago-markup"},void 0,(0,a.Z)("p",{},void 0,(0,a.Z)("span",{className:"ui-preview-text",style:{width:x.e(30,200)+"px"}},void 0," ")," ",(0,a.Z)("span",{className:"ui-preview-text",style:{width:x.e(30,200)+"px"}},void 0," ")," ",(0,a.Z)("span",{className:"ui-preview-text",style:{width:x.e(30,200)+"px"}},void 0," ")))))))))}function w(e){let{isReady:t,posts:s,poster:i}=e;return t?(0,a.Z)("ul",{className:"posts-list post-feed ui-ready"},void 0,s.map((e=>(0,a.Z)(b,{post:e,poster:i},e.id)))):N||(N=(0,a.Z)(y,{}))}},9771:function(e,t,s){"use strict";s.d(t,{mv:function(){return d},ZP:function(){return os},MO:function(){return C},Fi:function(){return v}});var a,i=s(57588),o=s.n(i),n=s(22928),r=s(4942),l=s(64646);class d extends o().Component{constructor(e){super(e),(0,r.Z)(this,"selected",(()=>{if(this.element){const e=p(this.element)||null,t=e?e.getBoundingClientRect():null;this.setState({range:e,rect:t})}})),(0,r.Z)(this,"reply",(()=>{if(l.Z.isOpen()){const e=C();e&&!e.disabled&&(e.quote(v(this.state.range)),this.setState({range:null,rect:null}),c())}else{const e=v(this.state.range);l.Z.open(Object.assign({},this.props.posting,{default:e})),this.setState({range:null,rect:null}),window.setTimeout(c,1e3)}})),(0,r.Z)(this,"render",(()=>(0,n.Z)("div",{},void 0,o().createElement("div",{ref:e=>{e&&(this.element=e)},onMouseUp:this.selected,onTouchEnd:this.selected},this.props.children),!!this.state.rect&&(0,n.Z)("div",{className:"quote-control",style:{position:"absolute",left:this.state.rect.left+window.scrollX,top:this.state.rect.bottom+window.scrollY}},void 0,a||(a=(0,n.Z)("div",{className:"quote-control-arrow"})),(0,n.Z)("div",{className:"quote-control-inner"},void 0,(0,n.Z)("button",{className:"btn quote-control-btn",type:"button",onClick:this.reply},void 0,pgettext("post reply","Quote"))))))),this.state={range:null,rect:null},this.element=null}}function c(){const e=document.querySelector("#posting-mount textarea");e.focus(),e.selectionStart=e.selectionEnd=e.value.length}const p=e=>{if(void 0===window.getSelection)return;const t=window.getSelection();if(!t)return;if("Range"!==t.type)return;if(1!==t.rangeCount)return;const s=t.getRangeAt(0);return u(s,e)&&h(s)&&m(s.cloneContents())?s:void 0},u=(e,t)=>{const s=e.commonAncestorContainer;if(s===t)return!0;let a=s.parentNode;for(;a;){if(a===t)return!0;a=a.parentNode}return!1},h=e=>{const t=e.commonAncestorContainer;if("ARTICLE"===t.nodeName)return!0;if(t.dataset&&"1"===t.dataset.noquote)return!1;let s=t.parentNode;for(;s;){if(s.dataset&&"1"===s.dataset.noquote)return!1;if("ARTICLE"===s.nodeName)return!0;s=s.parentNode}return!1},m=e=>{for(let t=0;t0)return!0;if("IMG"===s.nodeName)return!0;if(m(s))return!0}return!1};var v=e=>{const t=g(e);let s=y(e.cloneContents().childNodes,[]),a=t?`[quote="${t}"]\n`:"[quote]\n",i="\n[/quote]\n\n";const o=b(e);return o?(a+=o.syntax?`[code=${o.syntax}]\n`:"[code]\n",i="\n[/code]"+i):N(e)?(s=s.trim(),a+="`",i="`"+i):s=s.trim(),a+s+i};const g=e=>{const t=e.commonAncestorContainer;if(Z(t))return f(t);let s=t.parentNode;for(;s;){if(Z(s))return f(s);s=s.parentNode}return""},Z=e=>e.nodeType===Node.ELEMENT_NODE&&("ARTICLE"===e.nodeName||"BLOCKQUOTE"===e.nodeName&&e.dataset&&"quote"===e.dataset.block),f=e=>e.dataset&&e.dataset.author||null,b=e=>{const t=e.commonAncestorContainer;if(_(t))return x(t);let s=t.parentNode;for(;s;){if(_(s))return x(s);s=s.parentNode}return null},_=e=>"PRE"===e.nodeName,N=e=>{const t=e.commonAncestorContainer;if("CODE"===t.nodeName)return!0;let s=t.parentNode;for(;s;){if(Z(s))return!1;if("CODE"===s.nodeName)return!0;s=s.parentNode}return!1},x=e=>e.dataset?{syntax:e.dataset.syntax||null}:{syntax:null},y=(e,t)=>{let s="";for(let a=0;a{const s=e.dataset||{};if(e.nodeType===Node.TEXT_NODE)return e.textContent||"";if(e.nodeType===Node.ELEMENT_NODE){if(s.quote)return s.quote||"";if("1"===s.noquote)return""}if(e.nodeType===Node.ELEMENT_NODE&&s.quote&&s.quote.trim())return"";if("HR"===e.nodeName)return"\n\n- - -";if(w[e.nodeName]){const[s,a]=w[e.nodeName];return s+y(e.childNodes,[...t,e.nodeName])+a}if("A"===e.nodeName){const s=e.href,a=y(e.childNodes,[...t,e.nodeName]);return a?`[${a}](${s})`:`!(${s})`}if("IMG"===e.nodeName){const t=e.src,s=e.alt;return s?`![${s}](${t})`:`!(${t})`}if("DIV"===e.nodeName||"ASIDE"===e.nodeName){const a=s.block&&s.block.toUpperCase();if(a&&w[a]){const[s,i]=w[a];return s+y(e.childNodes,[...t,a])+i}return y(e.childNodes,t)}if("BLOCKQUOTE"===e.nodeName){if("spoiler"===s.block){const s=y(e.childNodes,[...t,"SPOILER"]).trim();if(!s)return"";let a="\n[spoiler]\n";return a+=s,a+="\n[/spoiler]",a}const a=y(e.childNodes,[...t,"QUOTE"]).trim();if(!a)return"";const i=f(e);let o=i?`\n[quote=${i}]\n`:"\n\n[quote]\n";return o+=a,o+="\n[/quote]",o}if("PRE"===e.nodeName){const t=s.syntax||null,a=e.querySelector("code"),i=a&&a.innerText||"";return i.trim()?"\n[code"+(t?"="+t:"")+"]"+i+"[/code]":""}if("CODE"===e.nodeName)return"`"+e.innerText+"`";if("P"===e.nodeName)return"\n"+y(e.childNodes,[...t,e.nodeName]);if("UL"===e.nodeName||"OL"===e.nodeName)return(0===t.filter((e=>"OL"===e||"UL"===e)).length?"\n":"")+y(e.childNodes,[...t,e.nodeName]);if("LI"===e.nodeName){let a="";const i=t.filter((e=>"OL"===e||"UL"===e)).length;for(let e=1;ee.id&&!e.isRemoved)).map((e=>e.id))}var A,R=s(12891),D=s(78657),j=s(53904),U=s(94184),z=s.n(U),M=s(32233),B=s(69092),q=s(59801),F=s(48772);function H(e){let{attachment:t}=e;return(0,n.Z)("div",{className:"modal-dialog modal-lg",role:"document"},void 0,(0,n.Z)("div",{className:"modal-content"},void 0,(0,n.Z)("div",{className:"modal-header"},void 0,(0,n.Z)("button",{"aria-label":pgettext("modal","Close"),className:"close","data-dismiss":"modal",type:"button"},void 0,A||(A=(0,n.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,n.Z)("h4",{className:"modal-title"},void 0,pgettext("markup editor","Attachment details"))),(0,n.Z)("div",{className:"modal-body"},void 0,!!t.is_image&&(0,n.Z)("div",{className:"markup-editor-attachment-modal-preview"},void 0,(0,n.Z)("a",{href:t.url.index+"?shva=1",target:"_blank"},void 0,(0,n.Z)("img",{src:t.url.index+"?shva=1",alt:""}))),(0,n.Z)("div",{className:"markup-editor-attachment-modal-filename"},void 0,t.filename),(0,n.Z)("div",{className:"row markup-editor-attachment-modal-details"},void 0,(0,n.Z)("div",{className:"col-xs-12 col-md-3"},void 0,(0,n.Z)("strong",{},void 0,t.filetype+", "+(0,F.Z)(t.size)),(0,n.Z)("div",{className:"text-muted"},void 0,(0,n.Z)("small",{},void 0,pgettext("markup editor","Type and size")))),(0,n.Z)("div",{className:"col-xs-12 col-md-4"},void 0,(0,n.Z)("strong",{},void 0,(0,n.Z)("abbr",{title:t.uploaded_on.format("LLL")},void 0,t.uploaded_on.fromNow())),(0,n.Z)("div",{className:"text-muted"},void 0,(0,n.Z)("small",{},void 0,pgettext("markup editor","Uploaded at")))),(0,n.Z)("div",{className:"col-xs-12 col-md-3"},void 0,t.url.uploader?(0,n.Z)("a",{href:t.url.uploader,target:"_blank",className:"item-title"},void 0,t.uploader_name):(0,n.Z)("span",{className:"item-title"},void 0,t.uploader_name),(0,n.Z)("div",{className:"text-muted"},void 0,(0,n.Z)("small",{},void 0,pgettext("markup editor","Uploader")))))),(0,n.Z)("div",{className:"modal-footer"},void 0,(0,n.Z)("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},void 0,pgettext("modal","Close")))))}const Y=(e,t,s,a,i)=>{const o=e.text||i||"";let n=e.prefix;n+=s+o+a,n+=e.suffix,t(n),window.setTimeout((()=>{W(e.textarea);const t=e.start+s.length;e.textarea.setSelectionRange(t,t+o.length)}),250)},V=(e,t,s)=>{let a=e.prefix;a+=s,a+=e.suffix,t(a),window.setTimeout((()=>{W(e.textarea);const t=e.end+s.length;e.textarea.setSelectionRange(t,t)}),250)},G=e=>{if(document.selection){e.focus();const t=document.selection.createRange(),s=t.text.length;return t.moveStart("character",-e.value.length),$(e,t.text.length-s,t.text.length)}if(e.selectionStart||"0"==e.selectionStart)return $(e,e.selectionStart,e.selectionEnd)},$=(e,t,s)=>({textarea:e,start:t,end:s,text:e.value.substring(t,s),prefix:e.value.substring(0,t),suffix:e.value.substring(s)});function W(e){const t=e.scrollTop;e.focus(),e.scrollTop=t}var Q,X,K,J,ee,te,se=e=>{var t;let{attachment:s,disabled:a,element:i,setState:o,update:r}=e;return(0,n.Z)("div",{className:"markup-editor-attachments-item"},void 0,(0,n.Z)("div",{className:"markup-editor-attachment"},void 0,(0,n.Z)("div",{className:"markup-editor-attachment-details"},void 0,s.id?(0,n.Z)("a",{className:"item-title",href:s.url.index+"?shva=1",target:"_blank",onClick:e=>{e.preventDefault(),q.Z.show(t||(t=(0,n.Z)(H,{attachment:s})))}},void 0,s.filename):(0,n.Z)("strong",{className:"item-title"},void 0,s.filename),(0,n.Z)("div",{className:"text-muted"},void 0,(0,n.Z)("ul",{className:"list-unstyled list-inline"},void 0,!s.id&&(0,n.Z)("li",{},void 0,s.progress+"%"),!!s.filetype&&(0,n.Z)("li",{},void 0,s.filetype),s.size>0&&(0,n.Z)("li",{},void 0,(0,F.Z)(s.size))))),!!s.id&&(0,n.Z)("div",{className:"markup-editor-attachment-buttons"},void 0,(0,n.Z)("button",{className:"btn btn-markup-editor-attachment btn-icon",title:pgettext("markup editor","Insert into message"),type:"button",disabled:a,onClick:()=>{const e=function(e){let t="[";return e.is_image?(t+="!["+e.filename+"]",t+="("+(e.url.thumb||e.url.index)+"?shva=1)"):t+=e.filename,t+="]("+e.url.index+"?shva=1)",t}(s),t=G(i);V(t,r,e)}},void 0,Q||(Q=(0,n.Z)("span",{className:"material-icon"},void 0,"flip_to_front"))),(0,n.Z)("button",{className:"btn btn-markup-editor-attachment btn-icon",title:pgettext("markup editor","Remove attachment"),type:"button",disabled:a,onClick:()=>{o((e=>{let{attachments:t}=e;if(window.confirm(pgettext("markup editor","Remove this attachment?")))return{attachments:t.filter((e=>{let{id:t}=e;return t!==s.id}))}}))}},void 0,X||(X=(0,n.Z)("span",{className:"material-icon"},void 0,"close")))),!s.id&&!!s.key&&(0,n.Z)("div",{className:"markup-editor-attachment-buttons"},void 0,s.error&&(0,n.Z)("button",{className:"btn btn-markup-editor-attachment btn-icon",title:pgettext("markup editor","See error"),type:"button",onClick:()=>{j.Z.error(interpolate(pgettext("markup editor","%(filename)s: %(error)s"),{filename:s.filename,error:s.error},!0))}},void 0,K||(K=(0,n.Z)("span",{className:"material-icon"},void 0,"warning"))),(0,n.Z)("button",{className:"btn btn-markup-editor-attachment btn-icon",title:pgettext("markup editor","Remove attachment"),type:"button",disabled:a,onClick:()=>{o((e=>{let{attachments:t}=e;return{attachments:t.filter((e=>{let{key:t}=e;return t!==s.key}))}}))}},void 0,J||(J=(0,n.Z)("span",{className:"material-icon"},void 0,"close"))))))},ae=e=>{let{attachments:t,disabled:s,element:a,setState:i,update:o}=e;return(0,n.Z)("div",{className:"markup-editor-attachments"},void 0,(0,n.Z)("div",{className:"markup-editor-attachments-container"},void 0,t.map((e=>(0,n.Z)(se,{attachment:e,disabled:s,element:a,setState:i,update:o},e.key||e.id)))))},ie=s(82211),oe=e=>{let{canProtect:t,disabled:s,empty:a,preview:i,isProtected:o,submitText:r,showPreview:l,closePreview:d,enableProtection:c,disableProtection:p}=e;return(0,n.Z)("div",{className:"markup-editor-footer"},void 0,!!t&&(0,n.Z)(ie.Z,{className:"btn-default btn-icon hidden-sm hidden-md hidden-lg",title:o?pgettext("markup editor","Protected"):pgettext("markup editor","Protect"),type:"button",disabled:s,onClick:()=>{o?p():c()}},void 0,(0,n.Z)("span",{className:"material-icon"},void 0,o?"lock":"lock_open")),!!t&&(0,n.Z)("div",{},void 0,(0,n.Z)(ie.Z,{className:"btn-default hidden-xs",type:"button",disabled:s,onClick:()=>{o?p():c()}},void 0,(0,n.Z)("span",{className:"material-icon"},void 0,o?"lock":"lock_open"),o?pgettext("markup editor","Protected"):pgettext("markup editor","Protect"))),ee||(ee=(0,n.Z)("div",{className:"markup-editor-spacer"})),i?(0,n.Z)(ie.Z,{className:"btn-default btn-auto",type:"button",onClick:d},void 0,pgettext("markup editor","Edit")):(0,n.Z)(ie.Z,{className:"btn-default btn-auto",disabled:s||a,type:"button",onClick:l},void 0,pgettext("markup editor","Preview")),(0,n.Z)(ie.Z,{className:"btn-primary btn-auto",disabled:s||a},void 0,r||gettext("Post")))},ne=s(96359);class re extends o().Component{constructor(e){super(e),(0,r.Z)(this,"handleSubmit",(e=>{e.preventDefault();const{selection:t,update:s}=this.props,a=this.state.syntax.trim(),i=this.state.text.trim();if(0===i.length)return this.setState({error:gettext("This field is required.")}),!1;const o=t.prefix.trim().length?"\n\n":"";return V(Object.assign({},t,{text:i}),s,o+"```"+a+"\n"+i+"\n```\n\n"),q.Z.hide(),!1})),this.state={error:null,syntax:"",text:e.selection.text}}render(){return(0,n.Z)("div",{className:"modal-dialog modal-lg",role:"document"},void 0,(0,n.Z)("div",{className:"modal-content"},void 0,(0,n.Z)("div",{className:"modal-header"},void 0,(0,n.Z)("button",{"aria-label":pgettext("modal","Close"),className:"close","data-dismiss":"modal",type:"button"},void 0,te||(te=(0,n.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,n.Z)("h4",{className:"modal-title"},void 0,pgettext("markup editor","Code"))),(0,n.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,n.Z)("div",{className:"modal-body"},void 0,(0,n.Z)(ne.Z,{for:"markup_code_syntax",label:pgettext("markup editor","Syntax highlighting")},void 0,(0,n.Z)("select",{id:"markup_code_syntax",className:"form-control",value:this.state.syntax,onChange:e=>this.setState({syntax:e.target.value})},void 0,(0,n.Z)("option",{value:""},void 0,pgettext("markup editor","No syntax highlighting")),le.map((e=>{let{value:t,name:s}=e;return(0,n.Z)("option",{value:t},t,s)})))),(0,n.Z)(ne.Z,{for:"markup_code_text",label:pgettext("markup editor","Code to insert"),validation:this.state.error?[this.state.error]:void 0},void 0,(0,n.Z)("textarea",{id:"markup_code_text",className:"form-control",rows:"8",value:this.state.text,onChange:e=>this.setState({text:e.target.value})}))),(0,n.Z)("div",{className:"modal-footer"},void 0,(0,n.Z)("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},void 0,gettext("Cancel")),(0,n.Z)("button",{className:"btn btn-primary"},void 0,pgettext("markup editor","Insert code"))))))}}const le=[{value:"bash",name:"Bash"},{value:"c",name:"C"},{value:"c#",name:"C#"},{value:"c++",name:"C++"},{value:"css",name:"CSS"},{value:"diff",name:"Diff"},{value:"go",name:"Go"},{value:"graphql",name:"GraphQL"},{value:"html,",name:"HTML"},{value:"xml",name:"XML"},{value:"json",name:"JSON"},{value:"java",name:"Java"},{value:"javascript",name:"JavaScript"},{value:"kotlin",name:"Kotlin"},{value:"less",name:"Less"},{value:"lua",name:"Lua"},{value:"makefile",name:"Makefile"},{value:"markdown",name:"Markdown"},{value:"objective-C",name:"Objective-C"},{value:"php",name:"PHP"},{value:"perl",name:"Perl"},{value:"plain",name:"Plain"},{value:"text",name:"text"},{value:"python",name:"Python"},{value:"repl",name:"REPL"},{value:"r",name:"R"},{value:"ruby",name:"Ruby"},{value:"rust",name:"Rust"},{value:"scss",name:"SCSS"},{value:"sql",name:"SQL"},{value:"shell",name:"Shell Session"},{value:"swift",name:"Swift"},{value:"toml",name:"TOML"},{value:"ini",name:"INI"},{value:"typescript",name:"TypeScript"},{value:"visualbasic",name:"Visual Basic .NET"},{value:"webassembly",name:"WebAssembly"},{value:"yaml",name:"YAML"}];var de,ce,pe,ue,he,me,ve,ge,Ze,fe,be,_e,Ne,xe,ye,we,ke,Ce,Se,Ee,Te,Le,Pe,Oe,Ie,Ae,Re,De,je,Ue,ze,Me,Be,qe,Fe,He,Ye,Ve,Ge,$e,We,Qe,Xe,Ke,Je,et=re;function tt(){return(0,n.Z)("div",{className:"modal-dialog modal-lg",role:"document"},void 0,(0,n.Z)("div",{className:"modal-content"},void 0,(0,n.Z)("div",{className:"modal-header"},void 0,(0,n.Z)("button",{"aria-label":pgettext("modal","Close"),className:"close","data-dismiss":"modal",type:"button"},void 0,de||(de=(0,n.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,n.Z)("h4",{className:"modal-title"},void 0,pgettext("markup help","Formatting help"))),(0,n.Z)("div",{className:"modal-body formatting-help"},void 0,(0,n.Z)("h4",{},void 0,pgettext("markup help","Emphasis text")),(0,n.Z)(st,{markup:pgettext("markup help","_This text will have emphasis_"),result:(0,n.Z)("p",{},void 0,(0,n.Z)("em",{},void 0,pgettext("markup help","This text will have emphasis")))}),ce||(ce=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Bold text")),(0,n.Z)(st,{markup:pgettext("markup help","**This text will be bold**"),result:(0,n.Z)("p",{},void 0,(0,n.Z)("strong",{},void 0,pgettext("markup help","This text will be bold")))}),pe||(pe=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Removed text")),(0,n.Z)(st,{markup:pgettext("markup help","~~This text will be removed~~"),result:(0,n.Z)("p",{},void 0,(0,n.Z)("del",{},void 0,pgettext("markup help","This text will be removed")))}),ue||(ue=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Bold text (BBCode)")),(0,n.Z)(st,{markup:pgettext("markup help","[b]This text will be bold[/b]"),result:(0,n.Z)("p",{},void 0,(0,n.Z)("b",{},void 0,pgettext("markup help","This text will be bold")))}),he||(he=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Underlined text (BBCode)")),(0,n.Z)(st,{markup:pgettext("markup help","[u]This text will be underlined[/u]"),result:(0,n.Z)("p",{},void 0,(0,n.Z)("u",{},void 0,pgettext("markup help","This text will be underlined")))}),me||(me=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Italics text (BBCode)")),(0,n.Z)(st,{markup:pgettext("markup help","[i]This text will be in italics[/i]"),result:(0,n.Z)("p",{},void 0,(0,n.Z)("i",{},void 0,pgettext("markup help","This text will be in italics")))}),ve||(ve=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Link")),ge||(ge=(0,n.Z)(st,{markup:"",result:(0,n.Z)("p",{},void 0,(0,n.Z)("a",{href:"#"},void 0,"example.com"))})),Ze||(Ze=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Link with text")),(0,n.Z)(st,{markup:"["+pgettext("markup help","Link text")+"](http://example.com)",result:(0,n.Z)("p",{},void 0,(0,n.Z)("a",{href:"#"},void 0,pgettext("markup help","Link text")))}),fe||(fe=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Link (BBCode)")),be||(be=(0,n.Z)(st,{markup:"[url]http://example.com[/url]",result:(0,n.Z)("p",{},void 0,(0,n.Z)("a",{href:"#"},void 0,"example.com"))})),_e||(_e=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Link with text (BBCode)")),(0,n.Z)(st,{markup:"[url=http://example.com]"+pgettext("markup help","Link text")+"[/url]",result:(0,n.Z)("p",{},void 0,(0,n.Z)("a",{href:"#"},void 0,pgettext("markup help","Link text")))}),Ne||(Ne=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Image")),xe||(xe=(0,n.Z)(st,{markup:"!(http://placekitten.com/38/38)",result:(0,n.Z)("p",{},void 0,(0,n.Z)("img",{alt:"",src:"http://placekitten.com/38/38"}))})),ye||(ye=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Image with alternate text")),(0,n.Z)(st,{markup:"!["+pgettext("markup help","Image text")+"](http://placekitten.com/38/38)",result:(0,n.Z)("p",{},void 0,(0,n.Z)("img",{alt:pgettext("markup help","Image text"),src:"http://placekitten.com/38/38"}))}),we||(we=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Image (BBCode)")),ke||(ke=(0,n.Z)(st,{markup:"[img]http://placekitten.com/38/38[/img]",result:(0,n.Z)("p",{},void 0,(0,n.Z)("img",{alt:"",src:"http://placekitten.com/38/38"}))})),Ce||(Ce=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Mention user by their name")),Se||(Se=(0,n.Z)(st,{markup:"@username",result:(0,n.Z)("p",{},void 0,(0,n.Z)("a",{href:"#"},void 0,"@username"))})),Ee||(Ee=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Heading 1")),(0,n.Z)(st,{markup:pgettext("markup help","# First level heading"),result:(0,n.Z)("h1",{},void 0,pgettext("markup help","First level heading"))}),Te||(Te=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Heading 2")),(0,n.Z)(st,{markup:pgettext("markup help","## Second level heading"),result:(0,n.Z)("h2",{},void 0,pgettext("markup help","Second level heading"))}),Le||(Le=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Heading 3")),(0,n.Z)(st,{markup:pgettext("markup help","### Third level heading"),result:(0,n.Z)("h3",{},void 0,pgettext("markup help","Third level heading"))}),Pe||(Pe=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Heading 4")),(0,n.Z)(st,{markup:pgettext("markup help","#### Fourth level heading"),result:(0,n.Z)("h4",{},void 0,pgettext("markup help","Fourth level heading"))}),Oe||(Oe=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Heading 5")),(0,n.Z)(st,{markup:pgettext("markup help","##### Fifth level heading"),result:(0,n.Z)("h5",{},void 0,pgettext("markup help","Fifth level heading"))}),Ie||(Ie=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Unordered list")),Ae||(Ae=(0,n.Z)(st,{markup:"- Lorem ipsum\n- Dolor met\n- Vulputate lectus",result:(0,n.Z)("ul",{},void 0,(0,n.Z)("li",{},void 0,"Lorem ipsum"),(0,n.Z)("li",{},void 0,"Dolor met"),(0,n.Z)("li",{},void 0,"Vulputate lectus"))})),Re||(Re=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Ordered list")),De||(De=(0,n.Z)(st,{markup:"1. Lorem ipsum\n2. Dolor met\n3. Vulputate lectus",result:(0,n.Z)("ol",{},void 0,(0,n.Z)("li",{},void 0,"Lorem ipsum"),(0,n.Z)("li",{},void 0,"Dolor met"),(0,n.Z)("li",{},void 0,"Vulputate lectus"))})),je||(je=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Quote text")),(0,n.Z)(st,{markup:"> "+pgettext("markup help","Quoted text"),result:(0,n.Z)("blockquote",{},void 0,(0,n.Z)("p",{},void 0,pgettext("markup help","Quoted text")))}),Ue||(Ue=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Quote text (BBCode)")),(0,n.Z)(st,{markup:"[quote]\n"+pgettext("markup help","Quoted text")+"\n[/quote]",result:(0,n.Z)("aside",{className:"quote-block"},void 0,(0,n.Z)("div",{className:"quote-heading"},void 0,gettext("Quoted message:")),(0,n.Z)("blockquote",{className:"quote-body"},void 0,(0,n.Z)("p",{},void 0,pgettext("markup help","Quoted text"))))}),ze||(ze=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Quote text with author (BBCode)")),(0,n.Z)(st,{markup:'[quote="'+pgettext("markup help","Quote author")+'"]\n'+pgettext("markup help","Quoted text")+"\n[/quote]",result:(0,n.Z)("aside",{className:"quote-block"},void 0,(0,n.Z)("div",{className:"quote-heading"},void 0,pgettext("markup help","Quote author has written:")),(0,n.Z)("blockquote",{className:"quote-body"},void 0,(0,n.Z)("p",{},void 0,pgettext("markup help","Quoted text"))))}),Me||(Me=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Spoiler")),(0,n.Z)(st,{markup:"[spoiler]\n"+pgettext("markup help","Secret text")+"\n[/spoiler]",result:(0,n.Z)(at,{},void 0,pgettext("markup help","Secret text"))}),Be||(Be=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Inline code")),(0,n.Z)(st,{markup:pgettext("markup help","`Inline code`"),result:(0,n.Z)("p",{},void 0,(0,n.Z)("code",{},void 0,pgettext("markup help","Inline code")))}),qe||(qe=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Code block")),Fe||(Fe=(0,n.Z)(st,{markup:'```\nalert("Hello world!");\n```',result:(0,n.Z)("pre",{},void 0,(0,n.Z)("code",{className:"hljs"},void 0,'alert("Hello world!");'))})),He||(He=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Code block with syntax highlighting")),Ye||(Ye=(0,n.Z)(st,{markup:'```python\nprint("Hello world!");\n```',result:(0,n.Z)("pre",{},void 0,(0,n.Z)("code",{className:"hljs language-python"},void 0,(0,n.Z)("span",{className:"hljs-built_in"},void 0,"print"),'("Hello world!");'))})),Ve||(Ve=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Code block (BBCode)")),Ge||(Ge=(0,n.Z)(st,{markup:'[code]\nalert("Hello world!");\n[/code]',result:(0,n.Z)("pre",{},void 0,(0,n.Z)("code",{className:"hljs"},void 0,'alert("Hello world!");'))})),$e||($e=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Code block with syntax highlighting (BBCode)")),We||(We=(0,n.Z)(st,{markup:'[code="python"]\nprint("Hello world!");\n[/code]',result:(0,n.Z)("pre",{},void 0,(0,n.Z)("code",{className:"hljs language-python"},void 0,(0,n.Z)("span",{className:"hljs-built_in"},void 0,"print"),'("Hello world!");'))})),Qe||(Qe=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Horizontal rule")),Xe||(Xe=(0,n.Z)(st,{markup:"Lorem ipsum\n- - -\nDolor met",result:(0,n.Z)("div",{},void 0,(0,n.Z)("p",{},void 0,"Lorem ipsum"),(0,n.Z)("hr",{}),(0,n.Z)("p",{},void 0,"Dolor met"))})),Ke||(Ke=(0,n.Z)("hr",{})),(0,n.Z)("h4",{},void 0,pgettext("markup help","Horizontal rule (BBCode)")),Je||(Je=(0,n.Z)(st,{markup:"Lorem ipsum\n[hr]\nDolor met",result:(0,n.Z)("div",{},void 0,(0,n.Z)("p",{},void 0,"Lorem ipsum"),(0,n.Z)("hr",{}),(0,n.Z)("p",{},void 0,"Dolor met"))}))),(0,n.Z)("div",{className:"modal-footer"},void 0,(0,n.Z)("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},void 0,pgettext("modal","Close")))))}function st(e){let{markup:t,result:s}=e;return(0,n.Z)("div",{className:"formatting-help-item"},void 0,(0,n.Z)("div",{className:"formatting-help-item-markup"},void 0,(0,n.Z)("pre",{},void 0,(0,n.Z)("code",{},void 0,t))),(0,n.Z)("div",{className:"formatting-help-item-preview"},void 0,(0,n.Z)("article",{className:"misago-markup"},void 0,s)))}class at extends o().Component{constructor(e){super(e),this.state={reveal:!1}}render(){return(0,n.Z)("aside",{className:this.state.reveal?"spoiler-block revealed":"spoiler-block"},void 0,(0,n.Z)("blockquote",{className:"spoiler-body"},void 0,(0,n.Z)("p",{},void 0,this.props.children)),!this.state.reveal&&(0,n.Z)("div",{className:"spoiler-overlay"},void 0,(0,n.Z)("button",{className:"spoiler-reveal",type:"button",onClick:()=>{this.setState({reveal:!0})}},void 0,gettext("Reveal spoiler"))))}}const it=new RegExp("^(((ftps?)|(https?))://)","i");function ot(e){return it.test(e.trim())}var nt;class rt extends o().Component{constructor(e){super(e),(0,r.Z)(this,"handleSubmit",(e=>{e.preventDefault();const{selection:t,update:s}=this.props,a=this.state.text.trim(),i=this.state.url.trim();return 0===i.length?(this.setState({error:gettext("This field is required.")}),!1):(a.length>0?V(t,s,"!["+a+"]("+i+")"):V(t,s,"!("+i+")"),q.Z.hide(),!1)}));const t=e.selection.text.trim(),s=ot(t);this.state={error:null,text:s?"":t,url:s?t:""}}render(){return(0,n.Z)("div",{className:"modal-dialog",role:"document"},void 0,(0,n.Z)("div",{className:"modal-content"},void 0,(0,n.Z)("div",{className:"modal-header"},void 0,(0,n.Z)("button",{"aria-label":pgettext("modal","Close"),className:"close","data-dismiss":"modal",type:"button"},void 0,nt||(nt=(0,n.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,n.Z)("h4",{className:"modal-title"},void 0,pgettext("markup editor","Image"))),(0,n.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,n.Z)("div",{className:"modal-body"},void 0,(0,n.Z)(ne.Z,{for:"markup_link_url",label:pgettext("markup editor","Image description"),helpText:pgettext("markup editor","Optional but recommended . Will be displayed instead of image when it fails to load.")},void 0,(0,n.Z)("input",{id:"markup_link_text",className:"form-control",type:"text",value:this.state.text,onChange:e=>this.setState({text:e.target.value})})),(0,n.Z)(ne.Z,{for:"markup_link_url",label:pgettext("markup editor","Image address"),validation:this.state.error?[this.state.error]:void 0},void 0,(0,n.Z)("input",{id:"markup_link_url",className:"form-control",type:"text",value:this.state.url,onChange:e=>this.setState({url:e.target.value})}))),(0,n.Z)("div",{className:"modal-footer"},void 0,(0,n.Z)("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},void 0,gettext("Cancel")),(0,n.Z)("button",{className:"btn btn-primary"},void 0,pgettext("markup editor","Insert image"))))))}}var lt,dt=rt;class ct extends o().Component{constructor(e){super(e),(0,r.Z)(this,"handleSubmit",(e=>{e.preventDefault();const{selection:t,update:s}=this.props,a=this.state.text.trim(),i=this.state.url.trim();return 0===i.length?(this.setState({error:gettext("This field is required.")}),!1):(a.length>0?V(t,s,"["+a+"]("+i+")"):V(t,s,"<"+i+">"),q.Z.hide(),!1)}));const t=e.selection.text.trim(),s=ot(t);this.state={error:null,text:s?"":t,url:s?t:""}}render(){return(0,n.Z)("div",{className:"modal-dialog",role:"document"},void 0,(0,n.Z)("div",{className:"modal-content"},void 0,(0,n.Z)("div",{className:"modal-header"},void 0,(0,n.Z)("button",{"aria-label":pgettext("modal","Close"),className:"close","data-dismiss":"modal",type:"button"},void 0,lt||(lt=(0,n.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,n.Z)("h4",{className:"modal-title"},void 0,pgettext("markup editor","Link"))),(0,n.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,n.Z)("div",{className:"modal-body"},void 0,(0,n.Z)(ne.Z,{for:"markup_link_url",label:pgettext("markup editor","Link text"),helpText:pgettext("markup editor","Optional. Will be displayed instead of link's address.")},void 0,(0,n.Z)("input",{id:"markup_link_text",className:"form-control",type:"text",value:this.state.text,onChange:e=>this.setState({text:e.target.value})})),(0,n.Z)(ne.Z,{for:"markup_link_url",label:pgettext("markup editor","Link address"),validation:this.state.error?[this.state.error]:void 0},void 0,(0,n.Z)("input",{id:"markup_link_url",className:"form-control",type:"text",value:this.state.url,onChange:e=>this.setState({url:e.target.value})}))),(0,n.Z)("div",{className:"modal-footer"},void 0,(0,n.Z)("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},void 0,gettext("Cancel")),(0,n.Z)("button",{className:"btn btn-primary"},void 0,pgettext("markup editor","Insert link"))))))}}var pt,ut=ct;class ht extends o().Component{constructor(e){super(e),(0,r.Z)(this,"handleSubmit",(e=>{e.preventDefault();const{selection:t,update:s}=this.props,a=this.state.author.trim(),i=this.state.text.trim();if(0===i.length)return this.setState({error:gettext("This field is required.")}),!1;const o=t.prefix.trim().length?"\n\n":"";return V(t,s,a?o+'[quote="'+a+'"]\n'+i+"\n[/quote]\n\n":o+"[quote]\n"+i+"\n[/quote]\n\n"),q.Z.hide(),!1})),this.state={error:null,author:"",text:e.selection.text}}render(){return(0,n.Z)("div",{className:"modal-dialog modal-lg",role:"document"},void 0,(0,n.Z)("div",{className:"modal-content"},void 0,(0,n.Z)("div",{className:"modal-header"},void 0,(0,n.Z)("button",{"aria-label":pgettext("modal","Close"),className:"close","data-dismiss":"modal",type:"button"},void 0,pt||(pt=(0,n.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,n.Z)("h4",{className:"modal-title"},void 0,pgettext("markup editor","Quote"))),(0,n.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,n.Z)("div",{className:"modal-body"},void 0,(0,n.Z)(ne.Z,{for:"markup_quote_author",label:pgettext("markup editor","Quote's author or source"),helpText:pgettext("markup editor",'Optional. If it\'s username, put "@" before it ("@JohnDoe").')},void 0,(0,n.Z)("input",{id:"markup_quote_author",className:"form-control",type:"text",value:this.state.author,onChange:e=>this.setState({author:e.target.value})})),(0,n.Z)(ne.Z,{for:"markup_quote_text",label:pgettext("markup editor","Quoted text"),validation:this.state.error?[this.state.error]:void 0},void 0,(0,n.Z)("textarea",{id:"markup_quote_text",className:"form-control",rows:"8",value:this.state.text,onChange:e=>this.setState({text:e.target.value})}))),(0,n.Z)("div",{className:"modal-footer"},void 0,(0,n.Z)("button",{className:"btn btn-default","data-dismiss":"modal",type:"button"},void 0,gettext("Cancel")),(0,n.Z)("button",{className:"btn btn-primary"},void 0,pgettext("markup editor","Insert quote"))))))}}var mt,vt,gt=ht,Zt=e=>{let{disabled:t,icon:s,title:a,onClick:i}=e;return(0,n.Z)("button",{className:"btn btn-markup-editor",title:a,type:"button",disabled:t,onClick:i},void 0,(0,n.Z)("span",{className:"material-icon"},void 0,s))},ft=s(54031),bt=(e,t)=>{const s=1024*M.Z.get("user").acl.max_attachment_size;if(e.size>s)return void j.Z.error(interpolate(pgettext("markup editor","File %(filename)s is bigger than %(limit)s."),{filename:e.name,limit:(0,F.Z)(s)},!0));let a={id:null,key:(0,ft.ZP)(32),error:null,uploaded_on:null,progress:0,filename:e.name,filetype:null,is_image:!1,size:e.size,url:null,uploader_name:null};t((e=>{let{attachments:t}=e;return{attachments:[a].concat(t)}}));const i=()=>{t((e=>{let{attachments:t}=e;return{attachments:t.concat()}}))},o=new FormData;o.append("upload",e),D.Z.upload(M.Z.get("ATTACHMENTS_API"),o,(e=>{a.progress=e,i()})).then((e=>{Object.assign(a,e,{uploaded_on:O()(e.uploaded_on)}),i()}),(e=>{400===e.status||413===e.status?(a.error=e.detail,j.Z.error(e.detail),i()):j.Z.apiError(e)}))};var _t=e=>{let{disabled:t,element:s,update:a,updateAttachments:i}=e;const o=[{name:pgettext("markup editor","Strong"),icon:"format_bold",onClick:()=>{Y(G(s),a,"**","**",pgettext("example markup","Strong text"))}},{name:pgettext("markup editor","Emphasis"),icon:"format_italic",onClick:()=>{Y(G(s),a,"*","*",pgettext("example markup","Text with emphasis"))}},{name:pgettext("markup editor","Strikethrough"),icon:"format_strikethrough",onClick:()=>{Y(G(s),a,"~~","~~",pgettext("example markup","Text with strikethrough"))}},{name:pgettext("markup editor","Horizontal ruler"),icon:"remove",onClick:()=>{V(G(s),a,"\n\n- - -\n\n")}},{name:pgettext("markup editor","Link"),icon:"insert_link",onClick:()=>{const e=G(s);q.Z.show((0,n.Z)(ut,{selection:e,element:s,update:a}))}},{name:pgettext("markup editor","Image"),icon:"insert_photo",onClick:()=>{const e=G(s);q.Z.show((0,n.Z)(dt,{selection:e,element:s,update:a}))}},{name:pgettext("markup editor","Quote"),icon:"format_quote",onClick:()=>{const e=G(s);q.Z.show((0,n.Z)(gt,{selection:e,element:s,update:a}))}},{name:pgettext("markup editor","Spoiler"),icon:"visibility_off",onClick:()=>{((e,t)=>{const s=G(e),a=s.prefix.trim().length?"\n\n":"";Y(s,t,a+"[spoiler]\n","\n[/spoiler]\n\n",pgettext("markup editor","Spoiler text"))})(s,a)}},{name:pgettext("markup editor","Code"),icon:"code",onClick:()=>{const e=G(s);q.Z.show((0,n.Z)(et,{selection:e,element:s,update:a}))}}];return M.Z.get("user").acl.max_attachment_size&&o.push({name:pgettext("markup editor","Upload file"),icon:"file_upload",onClick:()=>(e=>{const t=document.createElement("input");t.type="file",t.multiple="multiple",t.addEventListener("change",(function(){for(let s=0;s{let{name:a,icon:i,onClick:o}=e;return(0,n.Z)(Zt,{title:a,icon:i,disabled:t||!s,onClick:o},i)}))),(0,n.Z)("div",{className:"markup-editor-toolbar-right"},void 0,(0,n.Z)("div",{className:"markup-editor-controls-dropdown"},void 0,(0,n.Z)("button",{type:"button",className:"btn btn-markup-editor dropdown-toggle","data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false",disabled:t||!s},void 0,mt||(mt=(0,n.Z)("span",{className:"material-icon"},void 0,"more_vert"))),(0,n.Z)("ul",{className:"dropdown-menu dropdown-menu-right stick-to-bottom"},void 0,o.map((e=>{let{name:a,icon:i,onClick:o}=e;return(0,n.Z)("li",{},i,(0,n.Z)("button",{type:"button",className:"btn-link",disabled:t||!s,onClick:o},void 0,(0,n.Z)("span",{className:"material-icon"},void 0,i),a))})))),(0,n.Z)(Zt,{title:pgettext("markup editor","Formatting help"),icon:"help_outline",onClick:()=>{q.Z.show(vt||(vt=(0,n.Z)(tt,{})))}})))},Nt=s(19755);class xt extends o().Component{constructor(e){super(e),(0,r.Z)(this,"showPreview",(()=>{this.state.loading||(this.setState({loading:!0,preview:!0,element:null}),D.Z.post(M.Z.get("PARSE_MARKUP_API"),{post:this.props.value}).then((e=>{this.setState({loading:!1,parsed:e.parsed})}),(e=>{400===e.status?j.Z.error(e.detail):j.Z.apiError(e),this.setState({loading:!1,preview:!1})})))})),(0,r.Z)(this,"closePreview",(()=>{this.setState({loading:!1,preview:!1})})),(0,r.Z)(this,"onDrop",(e=>{if(e.preventDefault(),e.stopPropagation(),!e.dataTransfer.files)return;const{onAttachmentsChange:t}=this.props;if(M.Z.get("user").acl.max_attachment_size)for(let s=0;s{const{onAttachmentsChange:t}=this.props,s=[];for(let t=0;t(0,n.Z)("div",{className:z()("markup-editor",{"markup-editor-focused":this.state.focused&&!this.state.preview})},void 0,(0,n.Z)(_t,{disabled:this.props.disabled||this.state.preview,element:this.state.element,update:e=>this.props.onChange({target:{value:e}}),updateAttachments:this.props.onAttachmentsChange}),this.state.preview?(0,n.Z)("div",{className:"markup-editor-preview"},void 0,this.state.loading?(0,n.Z)("div",{className:"markup-editor-preview-loading"},void 0,(0,n.Z)("div",{className:"ui-preview"},void 0,(0,n.Z)("span",{className:"ui-preview-text",style:{width:"240px"}}))):(0,n.Z)(B.Z,{className:"markup-editor-preview-contents",markup:this.state.parsed})):o().createElement("textarea",{className:"markup-editor-textarea form-control",placeholder:this.props.placeholder,value:this.props.value,disabled:this.props.disabled||this.state.loading,rows:6,ref:e=>{e&&this.state.element!==e&&(this.setState({element:e}),function(e,t){Nt(t).atwho({at:"@",displayTpl:'
  • ${username}
  • ',insertTpl:"@${username}",searchKey:"username",callbacks:{remoteFilter:function(e,t){Nt.getJSON(M.Z.get("MENTION_API"),{q:e},t)}}}),Nt(t).on("inserted.atwho",((t,s,a,i)=>{const{query:o}=i,n=a.target.innerText.trim(),r=t.target.value.substr(0,o.headPos),l=t.target.value.substr(o.endPos);t.target.value=r+n+l,e.onChange(t);const d=o.headPos+n.length;t.target.setSelectionRange(d,d),t.target.focus()}))}(this.props,e))},onChange:this.props.onChange,onDrop:this.onDrop,onFocus:()=>this.setState({focused:!0}),onPaste:this.onPaste,onBlur:()=>this.setState({focused:!1})}),this.props.attachments.length>0&&(0,n.Z)(ae,{attachments:this.props.attachments,disabled:this.props.disabled||this.state.preview,element:this.state.element,setState:this.props.onAttachmentsChange,update:e=>this.props.onChange({target:{value:e}})}),(0,n.Z)(oe,{preview:this.state.preview,canProtect:this.props.canProtect,isProtected:this.props.isProtected,disabled:this.props.disabled,empty:this.props.value.trim().length{let{children:t}=e;return(0,n.Z)("div",{className:"posting-dialog-body"},void 0,t)},Gt=e=>{let{close:t,message:s}=e;return(0,n.Z)("div",{className:"posting-dialog-error"},void 0,Lt||(Lt=(0,n.Z)("div",{className:"posting-dialog-error-icon"},void 0,(0,n.Z)("span",{className:"material-icon"},void 0,"error_outlined"))),(0,n.Z)("div",{className:"posting-dialog-error-detail"},void 0,(0,n.Z)("p",{},void 0,s),(0,n.Z)("button",{type:"button",className:"btn btn-default",onClick:t},void 0,pgettext("modal","Close"))))},$t=e=>{let{children:t,close:s,fullscreen:a,minimize:i,minimized:o,fullscreenEnter:r,fullscreenExit:l,open:d}=e;return(0,n.Z)("div",{className:"posting-dialog-header"},void 0,(0,n.Z)("div",{className:"posting-dialog-caption"},void 0,t),o?(0,n.Z)("button",{className:"btn btn-posting-dialog",title:pgettext("dialog","Open"),type:"button",onClick:d},void 0,Pt||(Pt=(0,n.Z)("span",{className:"material-icon"},void 0,"expand_less"))):(0,n.Z)("button",{className:"btn btn-posting-dialog",title:pgettext("dialog","Minimize"),type:"button",onClick:i},void 0,Ot||(Ot=(0,n.Z)("span",{className:"material-icon"},void 0,"expand_more"))),a?(0,n.Z)("button",{className:"btn btn-posting-dialog hidden-xs",title:pgettext("dialog","Exit the fullscreen mode"),type:"button",onClick:l},void 0,It||(It=(0,n.Z)("span",{className:"material-icon"},void 0,"fullscreen_exit"))):(0,n.Z)("button",{className:"btn btn-posting-dialog hidden-xs",title:pgettext("dialog","Enter the fullscreen mode"),type:"button",onClick:r},void 0,At||(At=(0,n.Z)("span",{className:"material-icon"},void 0,"fullscreen"))),(0,n.Z)("button",{className:"btn btn-posting-dialog",title:pgettext("dialog","Cancel"),type:"button",onClick:s},void 0,Rt||(Rt=(0,n.Z)("span",{className:"material-icon"},void 0,"close"))))};function Wt(e){let{isClosed:t,isHidden:s,isPinned:a,disabled:i,options:o,close:r,open:l,hide:d,unhide:c,pinGlobally:p,pinLocally:u,unpin:h}=e;const m=function(e,t,s){const a=[];return 2===s&&a.push("bookmark"),1===s&&a.push("bookmark_outline"),e&&a.push("lock"),t&&a.push("visibility_off"),a}(t,s,a);return(0,n.Z)("div",{className:"dropdown"},void 0,(0,n.Z)("button",{className:"btn btn-default btn-outline btn-icon",title:pgettext("post thread","Options"),"aria-expanded":"true","aria-haspopup":"true","data-toggle":"dropdown",type:"button",disabled:i},void 0,m.length>0?(0,n.Z)("span",{className:"btn-icons-family"},void 0,m.map((e=>(0,n.Z)("span",{className:"material-icon"},e,e)))):Dt||(Dt=(0,n.Z)("span",{className:"material-icon"},void 0,"more_horiz"))),(0,n.Z)("ul",{className:"dropdown-menu dropdown-menu-right stick-to-bottom"},void 0,2===o.pin&&2!==a&&(0,n.Z)("li",{},void 0,(0,n.Z)("button",{className:"btn btn-link",onClick:p,type:"button",disabled:i},void 0,jt||(jt=(0,n.Z)("span",{className:"material-icon"},void 0,"bookmark")),pgettext("post thread","Pinned globally"))),o.pin>=a&&1!==a&&(0,n.Z)("li",{},void 0,(0,n.Z)("button",{className:"btn btn-link",onClick:u,type:"button",disabled:i},void 0,Ut||(Ut=(0,n.Z)("span",{className:"material-icon"},void 0,"bookmark_outline")),pgettext("post thread","Pinned locally"))),o.pin>=a&&0!==a&&(0,n.Z)("li",{},void 0,(0,n.Z)("button",{className:"btn btn-link",onClick:h,type:"button",disabled:i},void 0,zt||(zt=(0,n.Z)("span",{className:"material-icon"},void 0,"radio_button_unchecked")),pgettext("post thread","Not pinned"))),o.close&&!!t&&(0,n.Z)("li",{},void 0,(0,n.Z)("button",{className:"btn btn-link",onClick:l,type:"button",disabled:i},void 0,Mt||(Mt=(0,n.Z)("span",{className:"material-icon"},void 0,"lock_outline")),pgettext("post thread","Open"))),o.close&&!t&&(0,n.Z)("li",{},void 0,(0,n.Z)("button",{className:"btn btn-link",onClick:r,type:"button",disabled:i},void 0,Bt||(Bt=(0,n.Z)("span",{className:"material-icon"},void 0,"lock")),pgettext("post thread","Closed"))),o.hide&&!!s&&(0,n.Z)("li",{},void 0,(0,n.Z)("button",{className:"btn btn-link",onClick:c,type:"button",disabled:i},void 0,qt||(qt=(0,n.Z)("span",{className:"material-icon"},void 0,"visibility")),pgettext("post thread","Visible"))),o.hide&&!s&&(0,n.Z)("li",{},void 0,(0,n.Z)("button",{className:"btn btn-link",onClick:d,type:"button",disabled:i},void 0,Ft||(Ft=(0,n.Z)("span",{className:"material-icon"},void 0,"visibility_off")),pgettext("post thread","Hidden")))))}var Qt=class extends L.Z{constructor(e){super(e),(0,r.Z)(this,"loadSuccess",(e=>{let t=null,s=null;const a=e.map((e=>(!1===e.post||t&&e.id!=this.state.category||(t=e.id,s=e.post),Object.assign(e,{disabled:!1===e.post,label:e.name,value:e.id}))));this.setState({isReady:!0,options:s,categories:a,category:t})})),(0,r.Z)(this,"loadError",(e=>{this.setState({error:e.detail})})),(0,r.Z)(this,"onCancel",(()=>{if(0===this.state.post.length&&0===this.state.title.length&&0===this.state.attachments.length)return this.minimize(),l.Z.close();window.confirm(pgettext("post thread","Are you sure you want to discard thread?"))&&(this.minimize(),l.Z.close())})),(0,r.Z)(this,"onTitleChange",(e=>{this.changeValue("title",e.target.value)})),(0,r.Z)(this,"onCategoryChange",(e=>{const t=this.state.categories.find((t=>e.target.value==t.value));let s=this.state.pin;t.post.pin&&t.post.pin{this.changeValue("post",e.target.value)})),(0,r.Z)(this,"onAttachmentsChange",(e=>{this.setState(e)})),(0,r.Z)(this,"onClose",(()=>{this.changeValue("close",!0)})),(0,r.Z)(this,"onOpen",(()=>{this.changeValue("close",!1)})),(0,r.Z)(this,"onPinGlobally",(()=>{this.changeValue("pin",2)})),(0,r.Z)(this,"onPinLocally",(()=>{this.changeValue("pin",1)})),(0,r.Z)(this,"onUnpin",(()=>{this.changeValue("pin",0)})),(0,r.Z)(this,"onHide",(()=>{this.changeValue("hide",!0)})),(0,r.Z)(this,"onUnhide",(()=>{this.changeValue("hide",!1)})),(0,r.Z)(this,"close",(()=>{this.minimize(),l.Z.close()})),(0,r.Z)(this,"minimize",(()=>{this.setState({fullscreen:!1,minimized:!0})})),(0,r.Z)(this,"open",(()=>{this.setState({minimized:!1}),this.state.fullscreen})),(0,r.Z)(this,"fullscreenEnter",(()=>{this.setState({fullscreen:!0,minimized:!1})})),(0,r.Z)(this,"fullscreenExit",(()=>{this.setState({fullscreen:!1,minimized:!1})})),this.state={isReady:!1,isLoading:!1,error:null,minimized:!1,fullscreen:!1,options:null,title:"",category:e.category||null,categories:[],post:"",attachments:[],close:!1,hide:!1,pin:0,validators:{title:(0,R.jn)(),post:(0,R.Jh)()},errors:{}}}componentDidMount(){D.Z.get(this.props.config).then(this.loadSuccess,this.loadError)}clean(){if(!this.state.title.trim().length)return j.Z.error(pgettext("posting form","You have to enter thread title.")),!1;if(!this.state.post.trim().length)return j.Z.error(pgettext("posting form","You have to enter a message.")),!1;const e=this.validate();return e.title?(j.Z.error(e.title[0]),!1):!e.post||(j.Z.error(e.post[0]),!1)}send(){return D.Z.post(this.props.submit,{title:this.state.title,category:this.state.category,post:this.state.post,attachments:I(this.state.attachments),close:this.state.close,hide:this.state.hide,pin:this.state.pin})}handleSuccess(e){this.setState({isLoading:!0}),this.close(),j.Z.success(pgettext("post thread","Your thread has been posted.")),window.location=e.url}handleError(e){if(400===e.status){const t=[].concat(e.non_field_errors||[],e.category||[],e.title||[],e.post||[],e.attachments||[]);j.Z.error(t[0])}else j.Z.apiError(e)}render(){const e={minimized:this.state.minimized,minimize:this.minimize,open:this.open,fullscreen:this.state.fullscreen,fullscreenEnter:this.fullscreenEnter,fullscreenExit:this.fullscreenExit,close:this.onCancel};if(this.state.error)return o().createElement(Xt,e,(0,n.Z)(Gt,{message:this.state.error,close:this.close}));if(!this.state.isReady)return o().createElement(Xt,e,(0,n.Z)("div",{className:"posting-loading ui-preview"},void 0,Ht||(Ht=(0,n.Z)(wt.o8,{className:"posting-dialog-toolbar"},void 0,(0,n.Z)(wt.Z2,{className:"posting-dialog-thread-title",auto:!0},void 0,(0,n.Z)(wt.Eg,{auto:!0},void 0,(0,n.Z)("input",{className:"form-control",disabled:!0,type:"text"}))),(0,n.Z)(wt.Z2,{className:"posting-dialog-category-select",auto:!0},void 0,(0,n.Z)(wt.Eg,{},void 0,(0,n.Z)("input",{className:"form-control",disabled:!0,type:"text"}))))),(0,n.Z)(yt,{attachments:[],value:"",submitText:pgettext("post thread submit","Post thread"),disabled:!0,onAttachmentsChange:()=>{},onChange:()=>{}})));const t=!!(this.state.options.close||this.state.options.hide||this.state.options.pin);return o().createElement(Xt,e,(0,n.Z)("form",{className:"posting-dialog-form",onSubmit:this.handleSubmit},void 0,(0,n.Z)(wt.o8,{className:"posting-dialog-toolbar"},void 0,(0,n.Z)(wt.Z2,{className:"posting-dialog-thread-title",auto:!0},void 0,(0,n.Z)(wt.Eg,{auto:!0},void 0,(0,n.Z)("input",{className:"form-control",disabled:this.state.isLoading,onChange:this.onTitleChange,placeholder:pgettext("post thread","Thread title"),type:"text",value:this.state.title}))),(0,n.Z)(wt.Z2,{className:"posting-dialog-category-select",auto:!0},void 0,(0,n.Z)(wt.Eg,{},void 0,(0,n.Z)(T.Z,{choices:this.state.categories,disabled:this.state.isLoading,onChange:this.onCategoryChange,value:this.state.category})),t&&(0,n.Z)(wt.Eg,{shrink:!0},void 0,(0,n.Z)(Wt,{isClosed:this.state.close,isHidden:this.state.hide,isPinned:this.state.pin,disabled:this.state.isLoading,options:this.state.options,close:this.onClose,open:this.onOpen,hide:this.onHide,unhide:this.onUnhide,pinGlobally:this.onPinGlobally,pinLocally:this.onPinLocally,unpin:this.onUnpin})))),(0,n.Z)(yt,{attachments:this.state.attachments,value:this.state.post,submitText:pgettext("post thread submit","Start thread"),disabled:this.state.isLoading,onAttachmentsChange:this.onAttachmentsChange,onChange:this.onPostChange})))}};const Xt=e=>{let{children:t,close:s,minimized:a,minimize:i,open:o,fullscreen:r,fullscreenEnter:l,fullscreenExit:d}=e;return(0,n.Z)(Yt,{fullscreen:r,minimized:a},void 0,(0,n.Z)($t,{fullscreen:r,fullscreenEnter:l,fullscreenExit:d,minimized:a,minimize:i,open:o,close:s},void 0,pgettext("post thread","Start new thread")),(0,n.Z)(Vt,{},void 0,t))};function Kt(e){const t=e.split(",").map((e=>e.trim().toLowerCase())).filter((e=>e.length>0));return t.filter(((e,s)=>t.indexOf(e)==s))}var Jt=class extends L.Z{constructor(e){super(e),(0,r.Z)(this,"onCancel",(()=>{if(0===this.state.post.length&&0===this.state.title.length&&0===this.state.to.length&&0===this.state.attachments.length)return this.close();window.confirm(pgettext("post thread","Are you sure you want to discard private thread?"))&&this.close()})),(0,r.Z)(this,"onToChange",(e=>{this.changeValue("to",e.target.value)})),(0,r.Z)(this,"onTitleChange",(e=>{this.changeValue("title",e.target.value)})),(0,r.Z)(this,"onPostChange",(e=>{this.changeValue("post",e.target.value)})),(0,r.Z)(this,"onAttachmentsChange",(e=>{this.setState(e)})),(0,r.Z)(this,"close",(()=>{this.minimize(),l.Z.close()})),(0,r.Z)(this,"minimize",(()=>{this.setState({fullscreen:!1,minimized:!0})})),(0,r.Z)(this,"open",(()=>{this.setState({minimized:!1}),this.state.fullscreen})),(0,r.Z)(this,"fullscreenEnter",(()=>{this.setState({fullscreen:!0,minimized:!1})})),(0,r.Z)(this,"fullscreenExit",(()=>{this.setState({fullscreen:!1,minimized:!1})}));const t=(e.to||[]).map((e=>e.username)).join(", ");this.state={isLoading:!1,error:null,minimized:!1,fullscreen:!1,to:t,title:"",post:"",attachments:[],validators:{title:(0,R.jn)(),post:(0,R.Jh)()},errors:{}}}clean(){if(!Kt(this.state.to).length)return j.Z.error(pgettext("posting form","You have to enter at least one recipient.")),!1;if(!this.state.title.trim().length)return j.Z.error(pgettext("posting form","You have to enter thread title.")),!1;if(!this.state.post.trim().length)return j.Z.error(pgettext("posting form","You have to enter a message.")),!1;const e=this.validate();return e.title?(j.Z.error(e.title[0]),!1):!e.post||(j.Z.error(e.post[0]),!1)}send(){return D.Z.post(this.props.submit,{to:Kt(this.state.to),title:this.state.title,post:this.state.post,attachments:I(this.state.attachments)})}handleSuccess(e){this.setState({isLoading:!0}),this.close(),j.Z.success(pgettext("post thread","Your thread has been posted.")),window.location=e.url}handleError(e){if(400===e.status){const t=[].concat(e.non_field_errors||[],e.to||[],e.title||[],e.post||[],e.attachments||[]);j.Z.error(t[0])}else j.Z.apiError(e)}render(){const e={minimized:this.state.minimized,minimize:this.minimize,open:this.open,fullscreen:this.state.fullscreen,fullscreenEnter:this.fullscreenEnter,fullscreenExit:this.fullscreenExit,close:this.onCancel};return o().createElement(es,e,(0,n.Z)("form",{className:"posting-dialog-form",onSubmit:this.handleSubmit},void 0,(0,n.Z)(wt.o8,{className:"posting-dialog-toolbar"},void 0,(0,n.Z)(wt.Z2,{className:"posting-dialog-thread-recipients",auto:!0},void 0,(0,n.Z)(wt.Eg,{auto:!0},void 0,(0,n.Z)("input",{className:"form-control",disabled:this.state.isLoading,onChange:this.onToChange,placeholder:pgettext("post thread","Recipients, eg.: Danny, Lisa, Alice"),type:"text",value:this.state.to}))),(0,n.Z)(wt.Z2,{className:"posting-dialog-thread-title",auto:!0},void 0,(0,n.Z)(wt.Eg,{auto:!0},void 0,(0,n.Z)("input",{className:"form-control",disabled:this.state.isLoading,onChange:this.onTitleChange,placeholder:pgettext("post thread","Thread title"),type:"text",value:this.state.title})))),(0,n.Z)(yt,{attachments:this.state.attachments,value:this.state.post,submitText:pgettext("post thread submit","Start thread"),disabled:this.state.isLoading,onAttachmentsChange:this.onAttachmentsChange,onChange:this.onPostChange})))}};const es=e=>{let{children:t,close:s,minimized:a,minimize:i,open:o,fullscreen:r,fullscreenEnter:l,fullscreenExit:d}=e;return(0,n.Z)(Yt,{fullscreen:r,minimized:a},void 0,(0,n.Z)($t,{fullscreen:r,fullscreenEnter:l,fullscreenExit:d,minimized:a,minimize:i,open:o,close:s},void 0,pgettext("post thread","Start private thread")),(0,n.Z)(Vt,{},void 0,t))};var ts=class extends L.Z{constructor(e){super(e),(0,r.Z)(this,"loadSuccess",(e=>{this.setState({isReady:!0,post:e.post?'[quote="@'+e.poster+'"]\n'+e.post+"\n[/quote]":this.state.post}),this.quoteText=e.post?'[quote="@'+e.poster+'"]\n'+e.post+"\n[/quote]":this.state.post})),(0,r.Z)(this,"loadError",(e=>{this.setState({error:e.detail})})),(0,r.Z)(this,"appendData",(e=>{const t=e.post?'[quote="@'+e.poster+'"]\n'+e.post+"\n[/quote]\n\n":"";this.setState(((e,s)=>e.post.length>0?{post:e.post.trim()+"\n\n"+t}:{post:t})),this.open()})),(0,r.Z)(this,"onCancel",(()=>{if(this.state.post===this.quoteText&&0===this.state.attachments.length)return this.close();window.confirm(pgettext("post reply","Are you sure you want to discard your reply?"))&&this.close()})),(0,r.Z)(this,"onPostChange",(e=>{this.changeValue("post",e.target.value)})),(0,r.Z)(this,"onAttachmentsChange",(e=>{this.setState(e)})),(0,r.Z)(this,"onQuote",(e=>{this.setState((t=>{let{post:s}=t;return s.length>0?{post:s.trim()+"\n\n"+e}:{post:e}})),this.open()})),(0,r.Z)(this,"close",(()=>{this.minimize(),l.Z.close()})),(0,r.Z)(this,"minimize",(()=>{this.setState({fullscreen:!1,minimized:!0})})),(0,r.Z)(this,"open",(()=>{this.setState({minimized:!1}),this.state.fullscreen})),(0,r.Z)(this,"fullscreenEnter",(()=>{this.setState({fullscreen:!0,minimized:!1})})),(0,r.Z)(this,"fullscreenExit",(()=>{this.setState({fullscreen:!1,minimized:!1})})),this.state={isReady:!1,isLoading:!1,error:null,minimized:!1,fullscreen:!1,post:this.props.default||"",attachments:[],validators:{post:(0,R.Jh)()},errors:{}},this.quoteText=""}componentDidMount(){D.Z.get(this.props.config,this.props.context||null).then(this.loadSuccess,this.loadError),S(!1,this.onQuote)}componentWillUnmount(){E()}componentWillReceiveProps(e){const t=this.props.context,s=e.context;t&&s&&!s.reply||D.Z.get(e.config,e.context||null).then(this.appendData,j.Z.apiError)}clean(){if(!this.state.post.trim().length)return j.Z.error(pgettext("posting form","You have to enter a message.")),!1;const e=this.validate();return!e.post||(j.Z.error(e.post[0]),!1)}send(){return S(!0,this.onQuote),D.Z.post(this.props.submit,{post:this.state.post,attachments:I(this.state.attachments)})}handleSuccess(e){this.setState({isLoading:!0}),this.close(),S(!1,this.onQuote),j.Z.success(pgettext("post reply","Your reply has been posted.")),window.location=e.url.index}handleError(e){if(400===e.status){const t=[].concat(e.non_field_errors||[],e.post||[],e.attachments||[]);j.Z.error(t[0])}else j.Z.apiError(e);S(!1,this.onQuote)}render(){const e={thread:this.props.thread,minimized:this.state.minimized,minimize:this.minimize,open:this.open,fullscreen:this.state.fullscreen,fullscreenEnter:this.fullscreenEnter,fullscreenExit:this.fullscreenExit,close:this.onCancel};return this.state.error?o().createElement(ss,e,(0,n.Z)(Gt,{message:this.state.error,close:this.close})):this.state.isReady?o().createElement(ss,e,(0,n.Z)("form",{className:"posting-dialog-form",method:"POST",onSubmit:this.handleSubmit},void 0,(0,n.Z)(yt,{attachments:this.state.attachments,value:this.state.post,submitText:pgettext("post reply submit","Post reply"),disabled:this.state.isLoading,onAttachmentsChange:this.onAttachmentsChange,onChange:this.onPostChange}))):o().createElement(ss,e,(0,n.Z)("div",{className:"posting-loading ui-preview"},void 0,(0,n.Z)(yt,{attachments:[],value:"",submitText:pgettext("post reply submit","Post reply"),disabled:!0,onAttachmentsChange:()=>{},onChange:()=>{}})))}};const ss=e=>{let{children:t,close:s,minimized:a,minimize:i,open:o,fullscreen:r,fullscreenEnter:l,fullscreenExit:d,thread:c}=e;return(0,n.Z)(Yt,{fullscreen:r,minimized:a},void 0,(0,n.Z)($t,{fullscreen:r,fullscreenEnter:l,fullscreenExit:d,minimized:a,minimize:i,open:o,close:s},void 0,interpolate(pgettext("post reply","Reply to: %(thread)s"),{thread:c.title},!0)),(0,n.Z)(Vt,{},void 0,t))};var as=class extends L.Z{constructor(e){super(e),(0,r.Z)(this,"loadSuccess",(e=>{var t;this.originalPost=e.post,this.setState({isReady:!0,post:e.post,attachments:(t=e.attachments,t.map((e=>Object.assign({},e,{uploaded_on:O()(e.uploaded_on)})))),protect:e.is_protected,canProtect:e.can_protect})})),(0,r.Z)(this,"loadError",(e=>{this.setState({error:e.detail})})),(0,r.Z)(this,"appendData",(e=>{const t=e.post?'[quote="@'+e.poster+'"]\n'+e.post+"\n[/quote]\n\n":"";this.setState(((e,s)=>e.post.length>0?{post:e.post.trim()+"\n\n"+t}:{post:t})),this.open()})),(0,r.Z)(this,"onCancel",(()=>{const e=this.state.originalPost===this.state.post,t=0===this.state.attachments.length;if(e&&t)return this.close();window.confirm(pgettext("edit reply","Are you sure you want to discard changes?"))&&this.close()})),(0,r.Z)(this,"onProtect",(()=>{this.setState({protect:!0})})),(0,r.Z)(this,"onUnprotect",(()=>{this.setState({protect:!1})})),(0,r.Z)(this,"onPostChange",(e=>{this.changeValue("post",e.target.value)})),(0,r.Z)(this,"onAttachmentsChange",(e=>{this.setState(e)})),(0,r.Z)(this,"onQuote",(e=>{this.setState((t=>{let{post:s}=t;return s.length>0?{post:s.trim()+"\n\n"+e}:{post:e}})),this.open()})),(0,r.Z)(this,"close",(()=>{this.minimize(),l.Z.close()})),(0,r.Z)(this,"minimize",(()=>{this.setState({fullscreen:!1,minimized:!0})})),(0,r.Z)(this,"open",(()=>{this.setState({minimized:!1}),this.state.fullscreen})),(0,r.Z)(this,"fullscreenEnter",(()=>{this.setState({fullscreen:!0,minimized:!1})})),(0,r.Z)(this,"fullscreenExit",(()=>{this.setState({fullscreen:!1,minimized:!1})})),this.state={isReady:!1,isLoading:!1,error:!1,minimized:!1,fullscreen:!1,post:"",attachments:[],protect:!1,canProtect:!1,validators:{post:(0,R.Jh)()},errors:{}},this.originalPost=""}componentDidMount(){D.Z.get(this.props.config).then(this.loadSuccess,this.loadError),S(!1,this.onQuote)}componentWillUnmount(){E()}componentWillReceiveProps(e){const t=this.props.context,s=e.context;t&&s&&t.reply===s.reply||D.Z.get(e.config,e.context||null).then(this.appendData,j.Z.apiError)}clean(){if(!this.state.post.trim().length)return j.Z.error(pgettext("posting form","You have to enter a message.")),!1;const e=this.validate();return!e.post||(j.Z.error(e.post[0]),!1)}send(){return S(!0,this.onQuote),D.Z.put(this.props.submit,{post:this.state.post,attachments:I(this.state.attachments),protect:this.state.protect})}handleSuccess(e){this.setState({isLoading:!0}),this.close(),S(!1,this.onQuote),j.Z.success(pgettext("edit reply","Reply has been edited.")),window.location=e.url.index}handleError(e){if(400===e.status){const t=[].concat(e.non_field_errors||[],e.category||[],e.title||[],e.post||[],e.attachments||[]);j.Z.error(t[0])}else j.Z.apiError(e);S(!1,this.onQuote)}render(){const e={post:this.props.post,minimized:this.state.minimized,minimize:this.minimize,open:this.open,fullscreen:this.state.fullscreen,fullscreenEnter:this.fullscreenEnter,fullscreenExit:this.fullscreenExit,close:this.onCancel};return this.state.error?o().createElement(is,e,(0,n.Z)(Gt,{message:this.state.error,close:this.close})):this.state.isReady?o().createElement(is,e,(0,n.Z)("form",{className:"posting-dialog-form",method:"POST",onSubmit:this.handleSubmit},void 0,(0,n.Z)(yt,{attachments:this.state.attachments,canProtect:this.state.canProtect,isProtected:this.state.protect,enableProtection:()=>this.setState({protect:!0}),disableProtection:()=>this.setState({protect:!1}),value:this.state.post,submitText:pgettext("edit reply submit","Edit reply"),disabled:this.state.isLoading,onAttachmentsChange:this.onAttachmentsChange,onChange:this.onPostChange}))):o().createElement(is,e,(0,n.Z)("div",{className:"posting-loading ui-preview"},void 0,(0,n.Z)(yt,{attachments:[],value:"",submitText:pgettext("edit reply submit","Edit reply"),disabled:!0,onAttachmentsChange:()=>{},onChange:()=>{}})))}};const is=e=>{let{children:t,close:s,minimized:a,minimize:i,open:o,fullscreen:r,fullscreenEnter:l,fullscreenExit:d,post:c}=e;return(0,n.Z)(Yt,{fullscreen:r,minimized:a},void 0,(0,n.Z)($t,{fullscreen:r,fullscreenEnter:l,fullscreenExit:d,minimized:a,minimize:i,open:o,close:s},void 0,interpolate(pgettext("edit reply","Edit reply by %(poster)s from %(date)s"),{poster:c.poster?c.poster.username:c.poster_name,date:c.posted_on.fromNow()},!0)),(0,n.Z)(Vt,{},void 0,t))};function os(e){switch(e.mode){case"START":return o().createElement(Qt,e);case"START_PRIVATE":return o().createElement(Jt,e);case"REPLY":return o().createElement(ts,e);case"EDIT":return o().createElement(as,e);default:return null}}},12891:function(e,t,s){"use strict";s.d(t,{Jh:function(){return n},jn:function(){return o}});var a=s(55210),i=s(32233);function o(){return[(0,a.Ei)(i.Z.get("SETTINGS").thread_title_length_min,((e,t)=>{const s=npgettext("thread title length validator","Thread title should be at least %(limit_value)s character long (it has %(show_value)s).","Thread title should be at least %(limit_value)s characters long (it has %(show_value)s).",e);return interpolate(s,{limit_value:e,show_value:t},!0)})),(0,a.BS)(i.Z.get("SETTINGS").thread_title_length_max,((e,t)=>{const s=npgettext("thread title length validator","Thread title cannot be longer than %(limit_value)s character (it has %(show_value)s).","Thread title cannot be longer than %(limit_value)s characters (it has %(show_value)s).",e);return interpolate(s,{limit_value:e,show_value:t},!0)}))]}function n(){return i.Z.get("SETTINGS").post_length_max?[r(),(0,a.BS)(i.Z.get("SETTINGS").post_length_max||1e6,((e,t)=>{const s=npgettext("post length validator","Posted message cannot be longer than %(limit_value)s character (it has %(show_value)s).","Posted message cannot be longer than %(limit_value)s characters (it has %(show_value)s).",e);return interpolate(s,{limit_value:e,show_value:t},!0)}))]:[r()]}function r(){return(0,a.Ei)(i.Z.get("SETTINGS").post_length_min,((e,t)=>{const s=npgettext("post length validator","Posted message should be at least %(limit_value)s character long (it has %(show_value)s).","Posted message should be at least %(limit_value)s characters long (it has %(show_value)s).",e);return interpolate(s,{limit_value:e,show_value:t},!0)}))}},60471:function(e,t,s){"use strict";var a=s(22928),i=s(4942),o=s(57588),n=s.n(o);function r(e){let{icon:t}=e;return t?(0,a.Z)("span",{className:"material-icon"},void 0,t):null}t.Z=class extends n().Component{constructor(){super(...arguments),(0,i.Z)(this,"change",(e=>()=>{this.props.onChange({target:{value:e}})}))}getChoice(){let e=null;return this.props.choices.map((t=>{t.value===this.props.value&&(e=t)})),e}getIcon(){return this.getChoice().icon}getLabel(){return this.getChoice().label}render(){return(0,a.Z)("div",{className:"btn-group btn-select-group"},void 0,(0,a.Z)("button",{type:"button",className:"btn btn-select dropdown-toggle",id:this.props.id||null,"data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false","aria-describedby":this.props["aria-describedby"]||null,disabled:this.props.disabled||!1},void 0,(0,a.Z)(r,{icon:this.getIcon()}),this.getLabel()),(0,a.Z)("ul",{className:"dropdown-menu"},void 0,this.props.choices.map(((e,t)=>(0,a.Z)("li",{},t,(0,a.Z)("button",{type:"button",className:"btn-link",onClick:this.change(e.value)},void 0,(0,a.Z)(r,{icon:e.icon}),e.label))))))}}},14467:function(e,t,s){"use strict";var a,i=s(22928),o=(s(57588),s(32233)),n=s(82211),r=s(43345),l=s(47235),d=s(78657),c=s(59801),p=s(53904),u=s(93051),h=s(19755);t.Z=class extends r.Z{constructor(e){super(e),this.state={isLoading:!1,showActivation:!1,username:"",password:"",validators:{username:[],password:[]}}}clean(){return!!this.isValid()||(p.Z.error(pgettext("sign in modal","Fill out both fields.")),!1)}send(){return d.Z.post(o.Z.get("AUTH_API"),{username:this.state.username,password:this.state.password})}handleSuccess(){let e=h("#hidden-login-form");e.append(''),e.append(''),e.find('input[type="hidden"]').val(d.Z.getCsrfToken()),e.find('input[name="redirect_to"]').val(window.location.pathname),e.find('input[name="username"]').val(this.state.username),e.find('input[name="password"]').val(this.state.password),e.submit(),this.setState({isLoading:!0})}handleError(e){400===e.status?"inactive_admin"===e.code?p.Z.info(e.detail):"inactive_user"===e.code?(p.Z.info(e.detail),this.setState({showActivation:!0})):"banned"===e.code?((0,u.Z)(e.detail),c.Z.hide()):p.Z.error(e.detail):403===e.status&&e.ban?((0,u.Z)(e.ban),c.Z.hide()):p.Z.apiError(e)}getActivationButton(){return this.state.showActivation?(0,i.Z)("a",{className:"btn btn-success btn-block",href:o.Z.get("REQUEST_ACTIVATION_URL")},void 0,pgettext("sign in modal btn","Activate account")):null}render(){return(0,i.Z)("div",{className:"modal-dialog modal-sm modal-sign-in",role:"document"},void 0,(0,i.Z)("div",{className:"modal-content"},void 0,(0,i.Z)("div",{className:"modal-header"},void 0,(0,i.Z)("button",{"aria-label":pgettext("modal","Close"),className:"close","data-dismiss":"modal",type:"button"},void 0,a||(a=(0,i.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,i.Z)("h4",{className:"modal-title"},void 0,pgettext("sign in modal title","Sign in"))),(0,i.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,i.Z)("div",{className:"modal-body"},void 0,(0,i.Z)(l.Z,{buttonLabel:pgettext("sign in modal","Sign in with %(site)s"),formLabel:pgettext("sign in modal","Or use your forum account:"),labelClassName:"text-center"}),(0,i.Z)("div",{className:"form-group"},void 0,(0,i.Z)("div",{className:"control-input"},void 0,(0,i.Z)("input",{className:"form-control input-lg",disabled:this.state.isLoading,id:"id_username",onChange:this.bindInput("username"),placeholder:pgettext("sign in modal field","Username or e-mail"),type:"text",value:this.state.username}))),(0,i.Z)("div",{className:"form-group"},void 0,(0,i.Z)("div",{className:"control-input"},void 0,(0,i.Z)("input",{className:"form-control input-lg",disabled:this.state.isLoading,id:"id_password",onChange:this.bindInput("password"),placeholder:pgettext("sign in modal field","Password"),type:"password",value:this.state.password})))),(0,i.Z)("div",{className:"modal-footer"},void 0,this.getActivationButton(),(0,i.Z)(n.Z,{className:"btn-primary btn-block",loading:this.state.isLoading},void 0,pgettext("sign in modal btn","Sign in")),(0,i.Z)("a",{className:"btn btn-default btn-block",href:o.Z.get("FORGOTTEN_PASSWORD_URL")},void 0,pgettext("sign in modal btn","Forgot password?"))))))}}},24678:function(e,t,s){"use strict";s.d(t,{Jj:function(){return n},pg:function(){return r}});var a=s(22928),i=s(57588),o=s.n(i);t.ZP=class extends o().Component{getClass(){return function(e){let t="";return e.is_banned?t="banned":e.is_hidden?t="offline":e.is_online_hidden?t="online":e.is_offline_hidden?t="offline":e.is_online?t="online":e.is_offline&&(t="offline"),"user-status user-"+t}(this.props.status)}render(){return(0,a.Z)("span",{className:this.getClass()},void 0,this.props.children)}};class n extends o().Component{getIcon(){return this.props.status.is_banned?"remove_circle_outline":this.props.status.is_hidden?"help_outline":this.props.status.is_online_hidden?"label":this.props.status.is_offline_hidden?"label_outline":this.props.status.is_online?"lens":this.props.status.is_offline?"panorama_fish_eye":void 0}render(){return(0,a.Z)("span",{className:"material-icon status-icon"},void 0,this.getIcon())}}class r extends o().Component{getHelp(){return e=this.props.user,(t=this.props.status).is_banned?t.banned_until?interpolate(pgettext("user status","%(username)s is banned until %(ban_expires)s"),{username:e.username,ban_expires:t.banned_until.format("LL, LT")},!0):interpolate(pgettext("user status","%(username)s is banned"),{username:e.username},!0):t.is_hidden?interpolate(pgettext("user status","%(username)s is hiding presence"),{username:e.username},!0):t.is_online_hidden?interpolate(pgettext("user status","%(username)s is online (hidden)"),{username:e.username},!0):t.is_offline_hidden?interpolate(pgettext("user status","%(username)s was last seen %(last_click)s (hidden)"),{username:e.username,last_click:t.last_click.fromNow()},!0):t.is_online?interpolate(pgettext("user status","%(username)s is online"),{username:e.username},!0):t.is_offline?interpolate(pgettext("user status","%(username)s was last seen %(last_click)s"),{username:e.username,last_click:t.last_click.fromNow()},!0):void 0;var e,t}getLabel(){return this.props.status.is_banned?pgettext("user status","Banned"):this.props.status.is_hidden?pgettext("user status","Hidden"):this.props.status.is_online_hidden?pgettext("user status","Online (hidden)"):this.props.status.is_offline_hidden?pgettext("user status","Offline (hidden)"):this.props.status.is_online?pgettext("user status","Online"):this.props.status.is_offline?pgettext("user status","Offline"):void 0}render(){return(0,a.Z)("span",{className:this.props.className||"status-label",title:this.getHelp()},void 0,this.getLabel())}}},7850:function(e,t,s){"use strict";s.d(t,{Z:function(){return f}});var a,i,o,n,r,l=s(22928),d=s(57588),c=s.n(d),p=class extends c().Component{getEmptyMessage(){return this.props.emptyMessage?this.props.emptyMessage:pgettext("username history empty","No name changes have been recorded for your account.")}render(){return(0,l.Z)("div",{className:"username-history ui-ready"},void 0,(0,l.Z)("ul",{className:"list-group"},void 0,(0,l.Z)("li",{className:"list-group-item empty-message"},void 0,this.getEmptyMessage())))}},u=s(19605),h=class extends c().Component{renderUserAvatar(){return this.props.change.changed_by?(0,l.Z)("a",{href:this.props.change.changed_by.url,className:"user-avatar-wrapper"},void 0,(0,l.Z)(u.ZP,{user:this.props.change.changed_by,size:"100"})):a||(a=(0,l.Z)("span",{className:"user-avatar-wrapper"},void 0,(0,l.Z)(u.ZP,{size:"100"})))}renderUsername(){return this.props.change.changed_by?(0,l.Z)("a",{href:this.props.change.changed_by.url,className:"item-title"},void 0,this.props.change.changed_by.username):(0,l.Z)("span",{className:"item-title"},void 0,this.props.change.changed_by_username)}render(){return(0,l.Z)("li",{className:"list-group-item"},this.props.change.id,(0,l.Z)("div",{className:"change-avatar"},void 0,this.renderUserAvatar()),(0,l.Z)("div",{className:"change-author"},void 0,this.renderUsername()),(0,l.Z)("div",{className:"change"},void 0,(0,l.Z)("span",{className:"old-username"},void 0,this.props.change.old_username),i||(i=(0,l.Z)("span",{className:"material-icon"},void 0,"arrow_forward")),(0,l.Z)("span",{className:"new-username"},void 0,this.props.change.new_username)),(0,l.Z)("div",{className:"change-date"},void 0,(0,l.Z)("abbr",{title:this.props.change.changed_on.format("LLL")},void 0,this.props.change.changed_on.fromNow())))}},m=class extends c().Component{render(){return(0,l.Z)("div",{className:"username-history ui-ready"},void 0,(0,l.Z)("ul",{className:"list-group"},void 0,this.props.changes.map((e=>(0,l.Z)(h,{change:e},e.id)))))}},v=s(44039),g=class extends c().Component{shouldComponentUpdate(){return!1}getClassName(){return this.props.hiddenOnMobile?"list-group-item hidden-xs hidden-sm":"list-group-item"}render(){return(0,l.Z)("li",{className:this.getClassName()},void 0,o||(o=(0,l.Z)("div",{className:"change-avatar"},void 0,(0,l.Z)("span",{className:"user-avatar"},void 0,(0,l.Z)(u.ZP,{size:"100"})))),(0,l.Z)("div",{className:"change-author"},void 0,(0,l.Z)("span",{className:"ui-preview-text",style:{width:v.e(30,100)+"px"}},void 0," ")),(0,l.Z)("div",{className:"change"},void 0,(0,l.Z)("span",{className:"ui-preview-text",style:{width:v.e(30,70)+"px"}},void 0," "),n||(n=(0,l.Z)("span",{className:"material-icon"},void 0,"arrow_forward")),(0,l.Z)("span",{className:"ui-preview-text",style:{width:v.e(30,70)+"px"}},void 0," ")),(0,l.Z)("div",{className:"change-date"},void 0,(0,l.Z)("span",{className:"ui-preview-text",style:{width:v.e(80,140)+"px"}},void 0," ")))}},Z=class extends c().Component{shouldComponentUpdate(){return!1}render(){return(0,l.Z)("div",{className:"username-history ui-preview"},void 0,(0,l.Z)("ul",{className:"list-group"},void 0,[0,1,2].map((e=>(0,l.Z)(g,{hiddenOnMobile:e>0},e)))))}},f=class extends c().Component{render(){return this.props.isLoaded?this.props.changes.length?(0,l.Z)(m,{changes:this.props.changes}):(0,l.Z)(p,{emptyMessage:this.props.emptyMessage}):r||(r=(0,l.Z)(Z,{}))}}},40429:function(e,t,s){"use strict";s.d(t,{Z:function(){return k}});var a,i=s(22928),o=s(57588),n=s.n(o),r=s(19605),l=s(24678);function d(e){let{showStatus:t,user:s}=e;return(0,i.Z)("ul",{className:"list-unstyled"},void 0,(0,i.Z)(c,{showStatus:t,user:s}),(0,i.Z)(p,{user:s}),a||(a=(0,i.Z)("li",{className:"user-stat-divider"})),(0,i.Z)(u,{user:s}),(0,i.Z)(h,{user:s}),(0,i.Z)(m,{user:s}))}function c(e){let{showStatus:t,user:s}=e;return t?(0,i.Z)("li",{className:"user-stat-status"},void 0,(0,i.Z)(l.ZP,{status:s.status},void 0,(0,i.Z)(l.pg,{status:s.status,user:s}))):null}function p(e){let{user:t}=e;const{joined_on:s}=t;let a=interpolate(pgettext("users list item","Joined on %(joined_on)s"),{joined_on:s.format("LL, LT")},!0),o=interpolate(pgettext("users list item","Joined %(joined_on)s"),{joined_on:s.fromNow()},!0);return(0,i.Z)("li",{className:"user-stat-join-date"},void 0,(0,i.Z)("abbr",{title:a},void 0,o))}function u(e){let{user:t}=e;const s=v("user-stat-posts",t.posts),a=npgettext("users list item","%(posts)s post","%(posts)s posts",t.posts);return(0,i.Z)("li",{className:s},void 0,interpolate(a,{posts:t.posts},!0))}function h(e){let{user:t}=e;const s=v("user-stat-threads",t.threads),a=npgettext("users list item","%(threads)s thread","%(threads)s threads",t.threads);return(0,i.Z)("li",{className:s},void 0,interpolate(a,{threads:t.threads},!0))}function m(e){let{user:t}=e;const s=v("user-stat-followers",t.followers),a=npgettext("users list item","%(followers)s follower","%(followers)s followers",t.followers);return(0,i.Z)("li",{className:s},void 0,interpolate(a,{followers:t.followers},!0))}function v(e,t){return 0===t?e+" user-stat-empty":e}function g(e){let{rank:t,title:s}=e,a=s||t.title||t.name,o="user-title";return t.css_class&&(o+=" user-title-"+t.css_class),t.is_tab?(0,i.Z)("a",{className:o,href:t.url},void 0,a):(0,i.Z)("span",{className:o},void 0,a)}function Z(e){let{showStatus:t,user:s}=e;const{rank:a}=s;let o="panel user-card";return a.css_class&&(o+=" user-card-"+a.css_class),(0,i.Z)("div",{className:o},void 0,(0,i.Z)("div",{className:"panel-body"},void 0,(0,i.Z)("div",{className:"row"},void 0,(0,i.Z)("div",{className:"col-xs-3 user-card-left"},void 0,(0,i.Z)("div",{className:"user-card-small-avatar"},void 0,(0,i.Z)("a",{href:s.url},void 0,(0,i.Z)(r.ZP,{size:"50",size2x:"80",user:s})))),(0,i.Z)("div",{className:"col-xs-9 col-sm-12 user-card-body"},void 0,(0,i.Z)("div",{className:"user-card-avatar"},void 0,(0,i.Z)("a",{href:s.url},void 0,(0,i.Z)(r.ZP,{size:"150",size2x:"200",user:s}))),(0,i.Z)("div",{className:"user-card-username"},void 0,(0,i.Z)("a",{href:s.url},void 0,s.username)),(0,i.Z)("div",{className:"user-card-title"},void 0,(0,i.Z)(g,{rank:a,title:s.title})),(0,i.Z)("div",{className:"user-card-stats"},void 0,(0,i.Z)(d,{showStatus:t,user:s}))))))}var f,b,_,N,x=s(44039),y=class extends n().Component{shouldComponentUpdate(){return!1}render(){return(0,i.Z)("div",{className:"panel user-card user-card-preview"},void 0,(0,i.Z)("div",{className:"panel-body"},void 0,(0,i.Z)("div",{className:"row"},void 0,f||(f=(0,i.Z)("div",{className:"col-xs-3 user-card-left"},void 0,(0,i.Z)("div",{className:"user-card-small-avatar"},void 0,(0,i.Z)("span",{},void 0,(0,i.Z)(r.ZP,{size:"50",size2x:"80"}))))),(0,i.Z)("div",{className:"col-xs-9 col-sm-12 user-card-body"},void 0,b||(b=(0,i.Z)("div",{className:"user-card-avatar"},void 0,(0,i.Z)("span",{},void 0,(0,i.Z)(r.ZP,{size:"150",size2x:"200"})))),(0,i.Z)("div",{className:"user-card-username"},void 0,(0,i.Z)("span",{className:"ui-preview-text",style:{width:x.e(60,150)+"px"}},void 0," ")),(0,i.Z)("div",{className:"user-card-title"},void 0,(0,i.Z)("span",{className:"ui-preview-text",style:{width:x.e(60,150)+"px"}},void 0," ")),(0,i.Z)("div",{className:"user-card-stats"},void 0,(0,i.Z)("ul",{className:"list-unstyled"},void 0,(0,i.Z)("li",{},void 0,(0,i.Z)("span",{className:"ui-preview-text",style:{width:x.e(30,70)+"px"}},void 0," ")),(0,i.Z)("li",{},void 0,(0,i.Z)("span",{className:"ui-preview-text",style:{width:x.e(30,70)+"px"}},void 0," ")),_||(_=(0,i.Z)("li",{className:"user-stat-divider"})),(0,i.Z)("li",{},void 0,(0,i.Z)("span",{className:"ui-preview-text",style:{width:x.e(30,70)+"px"}},void 0," ")),(0,i.Z)("li",{},void 0,(0,i.Z)("span",{className:"ui-preview-text",style:{width:x.e(30,70)+"px"}},void 0," "))))))))}};function w(e){let{colClassName:t,cols:s}=e;const a=Array.apply(null,{length:s}).map(Number.call,Number);return(0,i.Z)("div",{className:"users-cards-list ui-preview"},void 0,(0,i.Z)("div",{className:"row"},void 0,a.map((e=>{let s=t;return 0!==e&&(s+=" hidden-xs"),3===e&&(s+=" hidden-sm"),(0,i.Z)("div",{className:s},e,N||(N=(0,i.Z)(y,{})))}))))}function k(e){let{cols:t,isReady:s,showStatus:a,users:o}=e,n="col-xs-12 col-sm-4";return 4===t&&(n+=" col-md-3"),s?(0,i.Z)("div",{className:"users-cards-list ui-ready"},void 0,(0,i.Z)("div",{className:"row"},void 0,o.map((e=>(0,i.Z)("div",{className:n},e.id,(0,i.Z)(Z,{showStatus:a,user:e})))))):(0,i.Z)(w,{colClassName:n,cols:t})}},82125:function(e,t,s){"use strict";var a=s(4942),i=s(57588),o=s.n(i);t.Z=class extends o().Component{constructor(e){super(e),(0,a.Z)(this,"toggleNav",(()=>{this.setState({dropdown:!this.state.dropdown})})),(0,a.Z)(this,"hideNav",(()=>{this.setState({dropdown:!1})})),this.state={dropdown:!1}}getCompactNavClassName(){return this.state.dropdown?"compact-nav open":"compact-nav"}}},7227:function(e,t,s){"use strict";var a=s(22928),i=s(4942),o=s(57588),n=s.n(o);t.Z=class extends n().Component{constructor(){super(...arguments),(0,i.Z)(this,"toggle",(()=>{this.props.onChange({target:{value:!this.props.value}})}))}getClassName(){return this.props.value?"btn btn-yes-no btn-yes-no-on":"btn btn-yes-no btn-yes-no-off"}getIcon(){return this.props.value?this.props.iconOn||"check_box":this.props.iconOff||"check_box_outline_blank"}getLabel(){return this.props.value?this.props.labelOn||pgettext("yesno switch choice","yes"):this.props.labelOff||pgettext("yesno switch choice","no")}render(){return(0,a.Z)("button",{type:"button",onClick:this.toggle,className:this.getClassName(),id:this.props.id||null,"aria-describedby":this.props["aria-describedby"]||null,disabled:this.props.disabled||!1},void 0,(0,a.Z)("span",{className:"material-icon"},void 0,this.getIcon()),(0,a.Z)("span",{className:"btn-text"},void 0,this.getLabel()))}}},32233:function(e,t,s){"use strict";s.d(t,{Z:function(){return i}}),s(58294),s(95377),s(68852),s(39737),s(14316),s(43204),s(7023);var a=new class{constructor(){this._initializers=[],this._context={}}addInitializer(e){this._initializers.push({key:e.name,item:e.initializer,after:e.after,before:e.before})}init(e){this._context=e,new class{constructor(e){this.isOrdered=!1,this._items=e||[]}add(e,t,s){this._items.push({key:e,item:t,after:s&&s.after||null,before:s&&s.before||null})}get(e,t){for(var s=0;s0&&t.length!==a.length;)o-=1,e.forEach(i);return s}}(this._initializers).orderedValues().forEach((e=>{e(this)}))}has(e){return!!this._context[e]}get(e,t){return this.has(e)?this._context[e]:t||void 0}pop(e){if(this.has(e)){let t=this._context[e];return this._context[e]=null,t}}};window.misago=a;var i=a},58339:function(e,t,s){"use strict";var a=s(32233),i=s(78657);a.Z.addInitializer({name:"ajax",initializer:function(){i.Z.init(a.Z.get("CSRF_COOKIE_NAME"))}})},64109:function(e,t,s){"use strict";var a=s(32233),i=s(35486),o=s(78657),n=s(53904),r=s(90287);a.Z.addInitializer({name:"auth-sync",initializer:function(e){e.get("isAuthenticated")&&window.setInterval((function(){o.Z.get(e.get("AUTH_API")).then((function(e){r.Z.dispatch((0,i.r$)(e))}),(function(e){n.Z.apiError(e)}))}),45e3)},after:"auth"})},46226:function(e,t,s){"use strict";var a=s(32233),i=s(98274),o=s(59801),n=s(90287),r=s(62833);a.Z.addInitializer({name:"auth",initializer:function(){i.Z.init(n.Z,r.Z,o.Z)},after:"store"})},93240:function(e,t,s){"use strict";var a=s(32233),i=s(78657),o=s(93825),n=s(96142),r=s(53904);a.Z.addInitializer({name:"captcha",initializer:function(e){o.ZP.init(e,i.Z,n.Z,r.Z)}})},75147:function(e,t,s){"use strict";var a=s(22928),i=s(57588),o=s.n(i),n=s(32233),r=s(4942),l=s(78657);class d extends o().Component{constructor(e){super(e),(0,r.Z)(this,"handleDecline",(()=>{this.state.submiting||window.confirm(pgettext("accept agreement prompt","Declining will result in immediate deactivation and deletion of your account. This action is not reversible."))&&(this.setState({submiting:!0}),l.Z.post(this.props.api,{accept:!1}).then((()=>{window.location.reload(!0)})))})),(0,r.Z)(this,"handleAccept",(()=>{this.state.submiting||(this.setState({submiting:!0}),l.Z.post(this.props.api,{accept:!0}).then((()=>{window.location.reload(!0)})))})),this.state={submiting:!1}}render(){return(0,a.Z)("div",{},void 0,(0,a.Z)("button",{className:"btn btn-default",disabled:this.state.submiting,type:"buton",onClick:this.handleDecline},void 0,pgettext("accept agreement choice","Decline")),(0,a.Z)("button",{className:"btn btn-primary",disabled:this.state.submiting,type:"buton",onClick:this.handleAccept},void 0,pgettext("accept agreement choice","Accept and continue")))}}var c=s(4869);n.Z.addInitializer({name:"component:accept-agreement",initializer:function(e){document.getElementById("required-agreement-mount")&&(0,c.Z)((0,a.Z)(d,{api:e.get("REQUIRED_AGREEMENT_API")}),"required-agreement-mount",!1)},after:"store"})},4894:function(e,t,s){"use strict";var a=s(37424),i=s(32233),o=s(22928),n=s(57588),r=s.n(n),l=class extends r().Component{refresh(){window.location.reload()}getMessage(){return this.props.signedIn?interpolate(pgettext("auth message","You have signed in as %(username)s. Please refresh the page before continuing."),{username:this.props.signedIn.username},!0):this.props.signedOut?interpolate(pgettext("auth message","%(username)s, you have been signed out. Please refresh the page before continuing."),{username:this.props.user.username},!0):void 0}render(){let e="auth-message";return(this.props.signedIn||this.props.signedOut)&&(e+=" show"),(0,o.Z)("div",{className:e},void 0,(0,o.Z)("div",{className:"container"},void 0,(0,o.Z)("p",{className:"lead"},void 0,this.getMessage()),(0,o.Z)("p",{},void 0,(0,o.Z)("button",{className:"btn btn-default",type:"button",onClick:this.refresh},void 0,pgettext("auth message","Reload page")),(0,o.Z)("span",{className:"hidden-xs hidden-sm"},void 0," "+pgettext("auth message","or press F5 key.")))))}};function d(e){return{user:e.auth.user,signedIn:e.auth.signedIn,signedOut:e.auth.signedOut}}var c=s(4869);i.Z.addInitializer({name:"component:auth-message",initializer:function(){(0,c.Z)((0,a.$j)(d)(l),"auth-message-mount")},after:"store"})},29223:function(e,t,s){"use strict";var a=s(32233),i=s(93051);a.Z.addInitializer({name:"component:banmed-page",initializer:function(e){e.has("BAN_MESSAGE")&&(0,i.Z)(e.get("BAN_MESSAGE"),!1)},after:"store"})},3026:function(e,t,s){"use strict";var a=s(37424),i=s(22928),o=s(4942),n=s(30381),r=s.n(n),l=s(57588),d=s.n(l);function c(e){return(0,i.Z)("div",{className:"categories-list"},void 0,(0,i.Z)("ul",{className:"list-group"},void 0,(0,i.Z)("li",{className:"list-group-item empty-message"},void 0,(0,i.Z)("p",{className:"lead"},void 0,pgettext("categories list","No categories exist or you don't have permission to see them.")))))}function p(e){let{category:t}=e;return t.description?(0,i.Z)("div",{className:"category-description",dangerouslySetInnerHTML:{__html:t.description.html}}):null}function u(e){let{category:t}=e;return(0,i.Z)("div",{className:h(t),title:m(t)},void 0,(0,i.Z)("span",{className:"material-icon"},void 0,function(e){return e.is_closed?e.is_read?"lock_outline":"lock":e.is_read?"chat_bubble_outline":"chat_bubble"}(t)))}function h(e){return e.is_read?"read-status item-read":"read-status item-new"}function m(e){return e.is_closed?e.is_read?gettext("category status","This category has no new posts. (closed)"):gettext("category status","This category has new posts. (closed)"):e.is_read?gettext("category status","This category has no new posts."):gettext("category status","This category has new posts.")}function v(e){let{category:t}=e;return(0,i.Z)("div",{className:"col-xs-12 col-sm-6 col-md-6 category-main"},void 0,(0,i.Z)("div",{className:"media"},void 0,(0,i.Z)("div",{className:"media-left"},void 0,(0,i.Z)(u,{category:t})),(0,i.Z)("div",{className:"media-body"},void 0,(0,i.Z)("h4",{className:"media-heading"},void 0,(0,i.Z)("a",{href:t.url.index},void 0,t.name)),(0,i.Z)(p,{category:t}))))}var g,Z,f,b=s(19605);function _(e){let{category:t}=e;return(0,i.Z)("div",{className:"col-xs-12 col-sm-6 col-md-4 category-last-thread"},void 0,(0,i.Z)(N,{category:t}),(0,i.Z)(w,{category:t}),(0,i.Z)(k,{category:t}),(0,i.Z)(C,{category:t}))}function N(e){let{category:t}=e;return t.acl.can_browse&&t.acl.can_see_all_threads&&t.last_thread_title?(0,i.Z)("div",{className:"media"},void 0,(0,i.Z)("div",{className:"media-left hidden-xs"},void 0,(0,i.Z)(x,{category:t})),(0,i.Z)("div",{className:"media-body"},void 0,(0,i.Z)("div",{className:"media-heading"},void 0,(0,i.Z)("a",{className:"item-title thread-title",href:t.url.last_thread_new,title:t.last_thread_title},void 0,t.last_thread_title)),(0,i.Z)("ul",{className:"list-inline"},void 0,(0,i.Z)("li",{className:"category-last-thread-poster"},void 0,(0,i.Z)(y,{category:t})),g||(g=(0,i.Z)("li",{className:"divider"},void 0,"—")),(0,i.Z)("li",{className:"category-last-thread-date"},void 0,(0,i.Z)("a",{href:t.url.last_post},void 0,t.last_post_on.fromNow()))))):null}function x(e){let{category:t}=e;return t.last_poster?(0,i.Z)("a",{className:"last-poster-avatar",href:t.last_poster.url,title:t.last_poster_name},void 0,(0,i.Z)(b.ZP,{className:"media-object",size:40,user:t.last_poster})):(0,i.Z)("span",{className:"last-poster-avatar",title:t.last_poster_name},void 0,Z||(Z=(0,i.Z)(b.ZP,{className:"media-object",size:40})))}function y(e){let{category:t}=e;return t.last_poster?(0,i.Z)("a",{className:"item-title",href:t.last_poster.url},void 0,t.last_poster_name):(0,i.Z)("span",{className:"item-title"},void 0,t.last_poster_name)}function w(e){let{category:t}=e;return t.acl.can_browse&&t.acl.can_see_all_threads?t.last_thread_title?null:(0,i.Z)(S,{message:pgettext("category last thread","This category is empty. No threads were posted within it so far.")}):null}function k(e){let{category:t}=e;return t.acl.can_browse?t.acl.can_see_all_threads?null:(0,i.Z)(S,{message:pgettext("category last thread","This category is private. You can see only your own threads within it.")}):null}function C(e){let{category:t}=e;return t.acl.can_browse?null:(0,i.Z)(S,{message:pgettext("category last thread","This category is protected. You can't browse its contents.")})}function S(e){let{message:t}=e;return(0,i.Z)("div",{className:"media category-thread-message"},void 0,f||(f=(0,i.Z)("div",{className:"media-left"},void 0,(0,i.Z)("span",{className:"material-icon"},void 0,"info_outline"))),(0,i.Z)("div",{className:"media-body"},void 0,(0,i.Z)("p",{},void 0,t)))}function E(e){let{category:t}=e;return(0,i.Z)("div",{className:"col-md-2 hidden-xs hidden-sm"},void 0,(0,i.Z)("ul",{className:"list-unstyled category-stats"},void 0,(0,i.Z)(T,{threads:t.threads}),(0,i.Z)(L,{posts:t.posts})))}function T(e){let{threads:t}=e;const s=npgettext("category stats","%(threads)s thread","%(threads)s threads",t);return(0,i.Z)("li",{className:"category-stat-threads"},void 0,interpolate(s,{threads:t},!0))}function L(e){let{posts:t}=e;const s=npgettext("category stats","%(posts)s post","%(posts)s posts",t);return(0,i.Z)("li",{className:"category-stat-posts"},void 0,interpolate(s,{posts:t},!0))}function P(e){let{category:t}=e,s="btn btn-default btn-block btn-sm btn-subcategory";return t.is_read||(s+=" btn-subcategory-new"),(0,i.Z)("div",{className:"col-xs-12 col-sm-4 col-md-3"},void 0,(0,i.Z)("a",{className:s,href:t.url.index},void 0,(0,i.Z)("span",{className:"material-icon"},void 0,function(e){return e.is_closed?e.is_read?"lock_outline":"lock":e.is_read?"chat_bubble_outline":"chat_bubble"}(t)),(0,i.Z)("span",{className:"icon-text"},void 0,t.name)))}function O(e){let{category:t,isFirst:s}=e;return s||0===t.subcategories.length?null:(0,i.Z)("div",{className:"row subcategories-list"},void 0,t.subcategories.map((e=>(0,i.Z)(P,{category:e},e.id))))}function I(e){let{category:t,isFirst:s}=e,a="list-group-item";return t.description?a+=" list-group-category-has-description":a+=" list-group-category-no-description",s&&(a+=" list-group-item-first"),t.css_class&&(a+=" list-group-category-has-flavor",a+=" list-group-item-category-"+t.css_class),(0,i.Z)("li",{className:a},void 0,(0,i.Z)("div",{className:"row"},void 0,(0,i.Z)(v,{category:t}),(0,i.Z)(E,{category:t}),(0,i.Z)(_,{category:t})),(0,i.Z)(O,{category:t,isFirst:s}))}function A(e){let{category:t}=e,s="list-group list-group-category";return t.css_class&&(s+=" list-group-category-has-flavor",s+=" list-group-category-"+t.css_class),(0,i.Z)("ul",{className:s},void 0,(0,i.Z)(I,{category:t,isFirst:!0}),t.subcategories.map((e=>(0,i.Z)(I,{category:e,isFirst:!1},e.id))))}function R(e){let{categories:t}=e;return(0,i.Z)("div",{className:"categories-list"},void 0,t.map((e=>(0,i.Z)(A,{category:e},e.id))))}var D,j=s(32233),U=s(55547);const z=function(e){return Object.assign({},e,{last_post_on:e.last_post_on?r()(e.last_post_on):null,subcategories:e.subcategories.map(z)})};var M=class extends d().Component{constructor(e){super(e),(0,o.Z)(this,"update",(e=>{this.setState({categories:e.map(z)})})),this.state={categories:j.Z.get("CATEGORIES").map(z)},this.startPolling(j.Z.get("CATEGORIES_API"))}startPolling(e){U.Z.start({poll:"categories",url:e,frequency:18e4,update:this.update})}render(){const{categories:e}=this.state;return 0===e.length?D||(D=(0,i.Z)(c,{})):(0,i.Z)(R,{categories:e})}};function B(e){return{tick:e.tick.tick}}var q=s(4869);j.Z.addInitializer({name:"component:categories",initializer:function(){document.getElementById("categories-mount")&&(0,q.Z)((0,a.$j)(B)(M),"categories-mount")},after:"store"})},73806:function(e,t,s){"use strict";var a,i=s(22928),o=s(57588),n=s.n(o),r=s(73935),l=s.n(r),d=s(37424),c=s(993),p=s(40689),u=s(80261),h=s(59801),m=s(14467);class v extends n().Component{componentDidMount(){"?modal=login"===window.document.location.search&&window.setTimeout((()=>h.Z.show(a||(a=(0,i.Z)(m.Z,{})))),300)}render(){return null}}var g=v;function Z(e){let{logo:t,logoXs:s,text:a,url:o}=e;return t?(0,i.Z)("div",{className:"navbar-branding"},void 0,(0,i.Z)("a",{href:o,className:"navbar-branding-logo"},void 0,(0,i.Z)("img",{src:t,alt:a}))):(0,i.Z)("div",{className:"navbar-branding"},void 0,!!s&&(0,i.Z)("a",{href:o,className:"navbar-branding-logo-xs"},void 0,(0,i.Z)("img",{src:s,alt:a})),!!a&&(0,i.Z)("a",{href:o,className:"navbar-branding-text"},void 0,a))}function f(e){let{items:t}=e;return(0,i.Z)("ul",{className:"navbar-extra-menu",role:"nav"},void 0,t.map(((e,t)=>(0,i.Z)("li",{className:e.className},t,(0,i.Z)("a",{href:e.url,target:e.targetBlank?"_blank":null,rel:e.rel},void 0,e.title)))))}var b,_=s(49021),N=s(4942),x=s(63026),y=s(66462),w=s(94184),k=s.n(w);function C(e){let{children:t,showAll:s,showUnread:a,unread:o}=e;return(0,i.Z)("div",{className:"notifications-dropdown-body"},void 0,(0,i.Z)(_.Aw,{},void 0,pgettext("notifications title","Notifications")),(0,i.Z)(_.KE,{},void 0,(0,i.Z)(S,{active:!o,onClick:s},void 0,pgettext("notifications dropdown","All")),(0,i.Z)(S,{active:o,onClick:a},void 0,pgettext("notifications dropdown","Unread"))),t,(0,i.Z)(_.kE,{},void 0,(0,i.Z)("a",{className:"btn btn-default btn-block",href:misago.get("NOTIFICATIONS_URL")},void 0,pgettext("notifications","See all notifications"))))}function S(e){let{active:t,children:s,onClick:a}=e;return(0,i.Z)("button",{className:k()("btn",{"btn-primary":t,"btn-default":!t}),type:"button",onClick:a},void 0,s)}class E extends n().Component{constructor(e){super(e),(0,N.Z)(this,"render",(()=>(0,i.Z)(C,{unread:this.state.unread,showAll:()=>this.setState({unread:!1}),showUnread:()=>this.setState({unread:!0})},void 0,(0,i.Z)(x.Z,{filter:this.state.unread?"unread":"all",disabled:!this.props.active},void 0,(e=>{let{data:t,loading:s,error:a}=e;return s?b||(b=(0,i.Z)(y.Pu,{})):a?(0,i.Z)(y.lb,{error:a}):(0,i.Z)(y.uE,{filter:this.state.unread?"unread":"all",items:t?t.results:[]})}))))),this.state={unread:!1,url:""}}getApiUrl(){let e=misago.get("NOTIFICATIONS_API")+"?limit=20";return e+=this.state.unread?"&filter=unread":"",e}}var T,L=E;function P(e){let{id:t,className:s,badge:a,url:o,active:n,onClick:r}=e;const l=a?pgettext("navbar","You have unread notifications!"):pgettext("navbar","Open notifications");return(0,i.Z)("a",{id:t,className:k()("btn btn-navbar-icon",s,{active:n}),href:o,title:l,onClick:r},void 0,!!a&&(0,i.Z)("span",{className:"navbar-item-badge"},void 0,a),(0,i.Z)("span",{className:"material-icon"},void 0,a?"notifications_active":"notifications_none"))}function O(e){let{id:t,className:s,badge:a,url:o}=e;return(0,i.Z)(_.Lt,{id:t,toggle:e=>{let{isOpen:t,toggle:n}=e;return(0,i.Z)(P,{className:s,active:t,badge:a,url:o,onClick:e=>{e.preventDefault(),n()}})},menuClassName:"notifications-dropdown",menuAlignRight:!0},void 0,(e=>{let{isOpen:t}=e;return(0,i.Z)(L,{active:t})}))}function I(e){let{id:t,className:s,badge:a,url:o,active:n,onClick:r}=e;const l=a?pgettext("navbar","You have unread private threads!"):pgettext("navbar","Open private threads");return(0,i.Z)("a",{id:t,className:k()("btn btn-navbar-icon",s,{active:n}),href:o,title:l,onClick:r},void 0,!!a&&(0,i.Z)("span",{className:"navbar-item-badge"},void 0,a),T||(T=(0,i.Z)("span",{className:"material-icon"},void 0,"inbox")))}var A,R,D,j=s(62989);function U(e){let{id:t,className:s,url:a,active:o,onClick:n}=e;return(0,i.Z)("a",{id:t,className:k()("btn btn-navbar-icon",s,{active:o}),href:a,title:pgettext("navbar","Open search"),onClick:n},void 0,A||(A=(0,i.Z)("span",{className:"material-icon"},void 0,"search")))}function z(e){let{id:t,className:s,url:a}=e;return(0,i.Z)(_.Lt,{id:t,toggle:e=>{let{isOpen:t,toggle:o}=e;return(0,i.Z)(U,{className:s,active:t,url:a,onClick:e=>{e.preventDefault(),o(),window.setTimeout((()=>{document.querySelector(".search-dropdown .form-control-search").focus()}),0)}})},menuClassName:"search-dropdown",menuAlignRight:!0},void 0,(()=>R||(R=(0,i.Z)(j.E,{}))))}function M(e){let{id:t,className:s,active:a,onClick:o}=e;return(0,i.Z)("button",{id:t,className:k()("btn btn-navbar-icon",s,{active:a}),title:pgettext("navbar","Open menu"),type:"button",onClick:o},void 0,D||(D=(0,i.Z)("span",{className:"material-icon"},void 0,"menu")))}var B=s(6333);function q(e){let{id:t,className:s}=e;return(0,i.Z)(_.Lt,{id:t,toggle:e=>{let{isOpen:t,toggle:a}=e;return(0,i.Z)(M,{className:s,active:t,onClick:a})},menuClassName:"site-nav-dropdown",menuAlignRight:!0},void 0,(e=>{let{isOpen:t,close:s}=e;return(0,i.Z)(B.bS,{close:s})}))}var F=s(19605);function H(e){let{id:t,className:s,user:a,active:o,onClick:n}=e;return(0,i.Z)("a",{id:t,className:k()("btn-navbar-image",s,{active:o}),href:a.url,title:pgettext("navbar","Open your options"),onClick:n},void 0,(0,i.Z)(F.ZP,{user:a,size:34}))}var Y,V,G,$,W=s(28166);function Q(e){let{id:t,className:s,user:a}=e;return(0,i.Z)(_.Lt,{id:t,toggle:e=>{let{isOpen:t,toggle:o}=e;return(0,i.Z)(H,{className:s,active:t,user:a,onClick:e=>{e.preventDefault(),o()}})},menuClassName:"user-nav-dropdown",menuAlignRight:!0},void 0,(e=>{let{isOpen:t,close:s}=e;return(0,i.Z)(W.o4,{close:s})}))}var X,K=(0,d.$j)((function(e){const t=misago.get("SETTINGS"),s=e.auth.user;return{branding:{logo:t.logo,logoXs:t.logo_small,text:t.logo_text,url:misago.get("MISAGO_PATH")},extraMenuItems:misago.get("extraMenuItems"),user:s.id?{id:s.id,username:s.username,email:s.email,avatars:s.avatars,unreadNotifications:s.unreadNotifications,unreadPrivateThreads:s.unread_private_threads,url:s.url}:null,searchUrl:misago.get("SEARCH_URL"),notificationsUrl:misago.get("NOTIFICATIONS_URL"),privateThreadsUrl:misago.get("PRIVATE_THREADS_URL"),authDelegated:t.enable_oauth2_client,showSearch:!!s.acl.can_search,showPrivateThreads:!!s&&!!s.acl.can_use_private_threads}}))((function(e){let{dispatch:t,branding:s,extraMenuItems:a,authDelegated:o,user:r,searchUrl:l,notificationsUrl:d,privateThreadsUrl:h,showSearch:m,showPrivateThreads:v}=e;return(0,i.Z)("div",{className:"container navbar-container"},void 0,n().createElement(Z,s),(0,i.Z)("div",{className:"navbar-right"},void 0,a.length>0&&(0,i.Z)(f,{items:a}),!!m&&(0,i.Z)(z,{id:"navbar-search-dropdown",url:l}),!!m&&(0,i.Z)(U,{id:"navbar-search-overlay",url:l,onClick:e=>{t(c.UL()),e.preventDefault()}}),Y||(Y=(0,i.Z)(q,{id:"navbar-site-nav-dropdown"})),(0,i.Z)(M,{id:"navbar-site-nav-overlay",onClick:()=>{t(c.AU())}}),!!v&&(0,i.Z)(I,{id:"navbar-private-threads",badge:r.unreadPrivateThreads,url:h}),!!r&&(0,i.Z)(O,{id:"navbar-notifications-dropdown",badge:r.unreadNotifications,url:d}),!!r&&(0,i.Z)(P,{id:"navbar-notifications-overlay",badge:r.unreadNotifications,url:d,onClick:e=>{t(c.hN()),e.preventDefault()}}),!!r&&(0,i.Z)(Q,{id:"navbar-user-nav-dropdown",user:r}),!!r&&(0,i.Z)(H,{id:"navbar-user-nav-overlay",user:r,onClick:e=>{t(c.T5()),e.preventDefault()}}),!r&&(V||(V=(0,i.Z)(u.Z,{className:"btn-navbar-sign-in"}))),!r&&!o&&(G||(G=(0,i.Z)(p.Z,{className:"btn-navbar-register"}))),!r&&!o&&($||($=(0,i.Z)(g,{})))))})),J=s(90287);misago.addInitializer({name:"component:navbar",initializer:function(e){const t=document.getElementById("misago-navbar");l().render((0,i.Z)(d.zt,{store:J.Z.getStore()},void 0,X||(X=(0,i.Z)(K,{}))),t)},after:"store"})},27015:function(e,t,s){"use strict";var a,i=s(22928),o=s(57588),n=s.n(o),r=s(73935),l=s.n(r),d=s(37424),c=s(4942),p=s(63026),u=s(66462),h=s(94184),m=s.n(h),v=s(49021),g=s(64836);function Z(e){let{children:t,open:s,showAll:a,showUnread:o,unread:n}=e;return(0,i.Z)(g.a,{open:s},void 0,(0,i.Z)(g.i,{},void 0,pgettext("notifications title","Notifications")),(0,i.Z)(v.KE,{},void 0,(0,i.Z)(f,{active:!n,onClick:a},void 0,pgettext("notifications dropdown","All")),(0,i.Z)(f,{active:n,onClick:o},void 0,pgettext("notifications dropdown","Unread"))),t,(0,i.Z)(v.kE,{},void 0,(0,i.Z)("a",{className:"btn btn-default btn-block",href:misago.get("NOTIFICATIONS_URL")},void 0,pgettext("notifications","See all notifications"))))}function f(e){let{active:t,children:s,onClick:a}=e;return(0,i.Z)("button",{className:m()("btn",{"btn-primary":t,"btn-default":!t}),type:"button",onClick:a},void 0,s)}class b extends n().Component{constructor(e){super(e),(0,c.Z)(this,"render",(()=>(0,i.Z)(Z,{open:this.props.open,unread:this.state.unread,showAll:()=>this.setState({unread:!1}),showUnread:()=>this.setState({unread:!0})},void 0,(0,i.Z)(p.Z,{filter:this.state.unread?"unread":"all",disabled:!this.props.open},void 0,(e=>{let{data:t,loading:s,error:o}=e;return s?a||(a=(0,i.Z)(u.Pu,{})):o?(0,i.Z)(u.lb,{error:o}):(0,i.Z)(u.uE,{filter:this.state.unread?"unread":"all",items:t?t.results:[]})}))))),this.body=document.body,this.state={unread:!1,url:""}}getApiUrl(){let e=misago.get("NOTIFICATIONS_API")+"?limit=20";return e+=this.state.unread?"&filter=unread":"",e}componentDidUpdate(e,t){e.open!==this.props.open&&(this.props.open?this.body.classList.add("notifications-fullscreen"):this.body.classList.remove("notifications-fullscreen"))}}var _,N=(0,d.$j)((function(e){return{open:e.overlay.notifications}}))(b),x=s(90287);misago.addInitializer({name:"component:notifications-overlay",initializer:function(e){if(e.get("isAuthenticated")){const e=document.getElementById("notifications-mount");l().render((0,i.Z)(d.zt,{store:x.Z.getStore()},void 0,_||(_=(0,i.Z)(N,{}))),e)}},after:"store"})},88097:function(e,t,s){"use strict";var a=s(22928),i=s(57588),o=s.n(i),n=s(73935),r=s.n(n),l=s(37424),d=s(69987),c=s(99755);function p(){return(0,a.Z)(c.Iv,{header:pgettext("notifications title","Notifications"),styleName:"notifications"})}var u=s(87462),h=s(35486),m=s(53904),v=s(60642),g=s(63026),Z=function(e){let{title:t,subtitle:s}=e;const a=[];return s&&a.push(s),t&&a.push(t),a.push(misago.get("SETTINGS").forum_name),document.title=a.join(" | "),null},f=s(59131),b=s(66462);function _(e){let{children:t}=e;return(0,a.Z)("ul",{className:"nav nav-pills"},void 0,t)}var N=s(94184),x=s.n(N);function y(e){let{active:t,link:s,icon:i,children:o}=e;return(0,a.Z)("li",{className:x()({active:t})},void 0,(0,a.Z)(d.rU,{to:s,activeClassName:""},void 0,!!i&&(0,a.Z)("span",{className:"material-icon"},void 0,i),o))}var w=s(92490);function k(e){let{filter:t}=e;const s=misago.get("NOTIFICATIONS_URL");return(0,a.Z)(w.o8,{},void 0,(0,a.Z)(w.Z2,{auto:!0},void 0,(0,a.Z)(w.Eg,{},void 0,(0,a.Z)(_,{},void 0,(0,a.Z)(y,{active:"all"===t,link:s},void 0,pgettext("notifications nav","All")),(0,a.Z)(y,{active:"unread"===t,link:s+"unread/"},void 0,pgettext("notifications nav","Unread")),(0,a.Z)(y,{active:"read"===t,link:s+"read/"},void 0,pgettext("notifications nav","Read"))))))}var C,S,E,T=s(82211);function L(e){let{baseUrl:t,data:s,disabled:i}=e;return(0,a.Z)("div",{className:"misago-pagination"},void 0,(0,a.Z)(P,{url:t,disabled:i||!s||!s.hasPrevious},void 0,pgettext("notifications pagination","Latest")),(0,a.Z)(P,{url:t+"?before="+(s?s.firstCursor:""),disabled:i||!s||!s.hasPrevious},void 0,pgettext("notifications pagination","Newer")),(0,a.Z)(P,{url:t+"?after="+(s?s.lastCursor:""),disabled:i||!s||!s.hasNext},void 0,pgettext("notifications pagination","Older")))}function P(e){let{disabled:t,children:s,url:i}=e;return t?(0,a.Z)("button",{className:"btn btn-default",type:"disabled",disabled:!0},void 0,s):(0,a.Z)(d.rU,{to:i,className:"btn btn-default",activeClassName:""},void 0,s)}function O(e){let{baseUrl:t,data:s,disabled:i,bottom:o,markAllAsRead:n}=e;return(0,a.Z)(w.o8,{},void 0,(0,a.Z)(w.Z2,{},void 0,(0,a.Z)(w.Eg,{},void 0,(0,a.Z)(L,{baseUrl:t,data:s,disabled:i}))),C||(C=(0,a.Z)(w.tw,{})),(0,a.Z)(w.Z2,{className:x()({"hidden-xs":!o})},void 0,(0,a.Z)(w.Eg,{},void 0,(0,a.Z)(T.Z,{className:"btn-default btn-block",type:"button",disabled:i||!s||!s.unreadNotifications,onClick:n},void 0,S||(S=(0,a.Z)("span",{className:"material-icon"},void 0,"done_all")),pgettext("notifications","Mark all as read")))))}function I(e){return"unread"===e?pgettext("notifications title","Unread notifications"):"read"===e?pgettext("notifications title","Read notifications"):null}var A,R=(0,l.$j)()((function(e){let{dispatch:t,location:s,route:i}=e;const{query:n}=s,{filter:r}=i.props,l=function(e){let t=misago.get("NOTIFICATIONS_URL");return"all"!==e&&(t+=e+"/"),t}(r);return(0,a.Z)(f.Z,{},void 0,(0,a.Z)(Z,{title:pgettext("notifications title","Notifications"),subtitle:I(r)}),(0,a.Z)(k,{filter:r}),(0,a.Z)(g.Z,{filter:r,query:n},void 0,(e=>{var s;let{data:i,loading:d,error:c,refetch:p}=e;return(0,a.Z)(v.D,{url:misago.get("NOTIFICATIONS_API")+"read-all/"},void 0,((e,v)=>{let{loading:g}=v;const Z={baseUrl:l,data:i,disabled:d||g||!i||0===i.results.length,markAllAsRead:async()=>{window.confirm(pgettext("notifications","Mark all notifications as read?"))&&e({onSuccess:async()=>{p(),t((0,h.yH)({unreadNotifications:null})),m.Z.success(pgettext("notifications","All notifications have been marked as read."))},onError:m.Z.apiError})}};return d||g?(0,a.Z)("div",{},void 0,o().createElement(O,Z),E||(E=(0,a.Z)(b.Pu,{})),o().createElement(O,(0,u.Z)({},Z,{bottom:!0}))):c?(0,a.Z)("div",{},void 0,o().createElement(O,Z),s||(s=(0,a.Z)(b.lb,{error:c})),o().createElement(O,(0,u.Z)({},Z,{bottom:!0}))):i?(!i.hasPrevious&&n&&window.history.replaceState({},"",l),(0,a.Z)("div",{},void 0,o().createElement(O,Z),(0,a.Z)(b.uE,{filter:r,items:i.results,hasNext:i.hasNext,hasPrevious:i.hasPrevious}),o().createElement(O,(0,u.Z)({},Z,{bottom:!0})))):null}))})))}));s(4517);var D,j=function(){const e=misago.get("NOTIFICATIONS_URL");return(0,a.Z)("div",{className:"page page-notifications"},void 0,A||(A=(0,a.Z)(p,{})),(0,a.Z)(d.F0,{history:d.mW,routes:[{path:e,component:R,props:{filter:"all"}},{path:e+"unread/",component:R,props:{filter:"unread"}},{path:e+"read/",component:R,props:{filter:"read"}}]}))},U=s(90287);misago.addInitializer({name:"component:notifications",initializer:function(e){const t=misago.get("NOTIFICATIONS_URL");if(document.location.pathname.startsWith(t)&&!document.location.pathname.startsWith(t+"disable-email/")&&e.get("isAuthenticated")){const e=document.getElementById("page-mount");r().render((0,a.Z)(l.zt,{store:U.Z.getStore()},void 0,D||(D=(0,a.Z)(j,{}))),e)}},after:"store"})},94795:function(e,t,s){"use strict";var a=s(22928),i=s(57588),o=s.n(i),n=s(37424),r=s(69987),l=s(94417);function d(e){return(0,a.Z)("div",{className:"list-group nav-side"},void 0,e.options.map((t=>(0,a.Z)(r.rU,{to:e.baseUrl+t.component+"/",className:"list-group-item",activeClassName:"active"},t.component,(0,a.Z)("span",{className:"material-icon"},void 0,t.icon),t.name))))}function c(e){return(0,a.Z)("ul",{className:e.className||"dropdown-menu",role:"menu"},void 0,e.options.map((t=>(0,a.Z)(l.Z,{path:e.baseUrl+t.component+"/"},t.component,(0,a.Z)(r.rU,{to:e.baseUrl+t.component+"/",onClick:e.hideNav},void 0,(0,a.Z)("span",{className:"material-icon hidden-sm"},void 0,t.icon),t.name)))))}var p,u=s(4942),h=s(82211),m=s(78657),v=s(53328),g=s(53904),Z=s(32233),f=class extends o().Component{constructor(e){super(e),(0,u.Z)(this,"onPasswordChange",(e=>{this.setState({password:e.target.value})})),(0,u.Z)(this,"handleSubmit",(e=>{e.preventDefault();const{isLoading:t,password:s}=this.state,{user:a}=this.props;return 0==s.length?(g.Z.error(pgettext("delete your account form","Enter your password to confirm account deletion.")),!1):!t&&(this.setState({isLoading:!0}),void m.Z.post(a.api.delete,{password:s}).then((e=>{window.location.href=Z.Z.get("MISAGO_PATH")}),(e=>{this.setState({isLoading:!1}),e.password?g.Z.error(e.password[0]):g.Z.apiError(e)})))})),this.state={isLoading:!1,password:""}}componentDidMount(){v.Z.set({title:pgettext("delete your account title","Delete account"),parent:pgettext("forum options","Change your options")})}render(){return(0,a.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,a.Z)("div",{className:"panel panel-danger panel-form"},void 0,(0,a.Z)("div",{className:"panel-heading"},void 0,(0,a.Z)("h3",{className:"panel-title"},void 0,pgettext("delete your account title","Delete account"))),(0,a.Z)("div",{className:"panel-body"},void 0,(0,a.Z)("p",{className:"lead"},void 0,pgettext("delete your account form","You are going to delete your account. This action is nonreversible, and will result in following data being deleted:")),(0,a.Z)("p",{},void 0,"-"," ",pgettext("delete your account form","Stored IP addresses associated with content that you have posted will be deleted.")),(0,a.Z)("p",{},void 0,"-"," ",pgettext("delete your account form","Your username will become available for other user to rename to or for new user to register their account with.")),(0,a.Z)("p",{},void 0,"-"," ",pgettext("delete your account form","Your e-mail will become available for use in new account registration.")),p||(p=(0,a.Z)("hr",{})),(0,a.Z)("p",{},void 0,pgettext("delete your account form","All your posted content will NOT be deleted, but username associated with it will be changed to one shared by all deleted accounts."))),(0,a.Z)("div",{className:"panel-footer"},void 0,(0,a.Z)("div",{className:"input-group"},void 0,(0,a.Z)("input",{className:"form-control",disabled:this.state.isLoading,name:"password-confirmation",type:"password",placeholder:pgettext("delete your account form field","Enter your password to confirm account deletion."),value:this.state.password,onChange:this.onPasswordChange}),(0,a.Z)("span",{className:"input-group-btn"},void 0,(0,a.Z)(h.Z,{className:"btn-danger",loading:this.state.isLoading},void 0,pgettext("delete your account form btn","Delete my account")))))))}},b=s(21688),_=class extends o().Component{constructor(){super(...arguments),(0,u.Z)(this,"onSuccess",(()=>{g.Z.info(pgettext("profile details form","Your details have been updated."))}))}componentDidMount(){v.Z.set({title:pgettext("edit details","Edit details"),parent:pgettext("forum options","Change your options")})}render(){return(0,a.Z)(b.Z,{api:this.props.user.api.edit_details,onSuccess:this.onSuccess})}},N=s(30381),x=s.n(N);class y extends o().Component{constructor(e){super(e),(0,u.Z)(this,"handleLoadDownloads",(()=>{m.Z.get(this.props.user.api.data_downloads).then((e=>{this.setState({isLoading:!1,downloads:e})}),(e=>{g.Z.apiError(e)}))})),(0,u.Z)(this,"handleRequestDataDownload",(()=>{this.setState({isSubmiting:!0}),m.Z.post(this.props.user.api.request_data_download).then((()=>{this.handleLoadDownloads(),g.Z.success(pgettext("download your data","Your request for data download has been registered.")),this.setState({isSubmiting:!1})}),(e=>{g.Z.apiError(e),this.setState({isSubmiting:!1})}))})),this.state={isLoading:!1,isSubmiting:!1,downloads:[]}}componentDidMount(){v.Z.set({title:pgettext("download your data title","Download your data"),parent:pgettext("forum options","Change your options")}),this.handleLoadDownloads()}render(){return(0,a.Z)("div",{},void 0,(0,a.Z)("div",{className:"panel panel-default panel-form"},void 0,(0,a.Z)("div",{className:"panel-heading"},void 0,(0,a.Z)("h3",{className:"panel-title"},void 0,pgettext("download your data title","Download your data"))),(0,a.Z)("div",{className:"panel-body"},void 0,(0,a.Z)("p",{},void 0,pgettext("download your data",'To download your data from the site, click the "Request data download" button. Depending on amount of data to be archived and number of users wanting to download their data at same time it may take up to few days for your download to be prepared. An e-mail with notification will be sent to you when your data is ready to be downloaded.')),(0,a.Z)("p",{},void 0,pgettext("download your data","The download will only be available for limited amount of time, after which it will be deleted from the site and marked as expired."))),(0,a.Z)("table",{className:"table"},void 0,(0,a.Z)("thead",{},void 0,(0,a.Z)("tr",{},void 0,(0,a.Z)("th",{},void 0,pgettext("download your data table","Requested on")),(0,a.Z)("th",{className:"col-md-4"},void 0,pgettext("download your data table","Download")))),(0,a.Z)("tbody",{},void 0,this.state.downloads.map((e=>(0,a.Z)("tr",{},e.id,(0,a.Z)("td",{style:w},void 0,x()(e.requested_on).fromNow()),(0,a.Z)("td",{},void 0,(0,a.Z)(k,{exportFile:e.file,status:e.status}))))),0==this.state.downloads.length?(0,a.Z)("tr",{},void 0,(0,a.Z)("td",{colSpan:"2"},void 0,pgettext("download your data table","You have no data downloads."))):null)),(0,a.Z)("div",{className:"panel-footer text-right"},void 0,(0,a.Z)(h.Z,{className:"btn-primary",loading:this.state.isSubmiting,type:"button",onClick:this.handleRequestDataDownload},void 0,pgettext("download your data btn","Request data download")))))}}const w={verticalAlign:"middle"},k=e=>{let{exportFile:t,status:s}=e;return 0===s||1===s?(0,a.Z)(h.Z,{className:"btn-info btn-sm btn-block",disabled:!0,type:"button"},void 0,pgettext("download your data table btn","Download is being prepared")):t?(0,a.Z)("a",{className:"btn btn-success btn-sm btn-block",href:t},void 0,pgettext("download your data table btn","Download your data")):(0,a.Z)(h.Z,{className:"btn-default btn-sm btn-block",disabled:!0,type:"button"},void 0,pgettext("download your data table btn","Download is expired"))};var C=s(43345),S=s(96359),E=s(60471),T=s(7227),L=s(35486),P=s(90287);const O=[{value:0,icon:"notifications_none",label:pgettext("watch thread choice","No")},{value:1,icon:"notifications",label:pgettext("watch thread choice","Yes, with on site notifications")},{value:2,icon:"mail",label:pgettext("watch thread choice","Yes, with on site and e-mail notifications")}],I=[{value:0,icon:"notifications_none",label:pgettext("notification preference","Don't notify")},{value:1,icon:"notifications",label:pgettext("notification preference","Notify on site")},{value:2,icon:"mail",label:pgettext("notification preference","Notify on site and with e-mail")}];class A extends C.Z{constructor(e){super(e),this.state={isLoading:!1,is_hiding_presence:e.user.is_hiding_presence,limits_private_thread_invites_to:e.user.limits_private_thread_invites_to,watch_started_threads:e.user.watch_started_threads,watch_replied_threads:e.user.watch_replied_threads,watch_new_private_threads_by_followed:e.user.watch_new_private_threads_by_followed,watch_new_private_threads_by_other_users:e.user.watch_new_private_threads_by_other_users,notify_new_private_threads_by_followed:e.user.notify_new_private_threads_by_followed,notify_new_private_threads_by_other_users:e.user.notify_new_private_threads_by_other_users,errors:{}},this.privateThreadInvitesChoices=[{value:0,icon:"help_outline",label:pgettext("private threads preference","Anybody can invite me to their private threads")},{value:1,icon:"done_all",label:pgettext("private threads preference","Only those I follow can invite me to their private threads")},{value:2,icon:"highlight_off",label:pgettext("private threads preference","Nobody can invite me to their private threads")}]}send(){return m.Z.post(this.props.user.api.options,{is_hiding_presence:this.state.is_hiding_presence,limits_private_thread_invites_to:this.state.limits_private_thread_invites_to,watch_started_threads:this.state.watch_started_threads,watch_replied_threads:this.state.watch_replied_threads,watch_new_private_threads_by_followed:this.state.watch_new_private_threads_by_followed,watch_new_private_threads_by_other_users:this.state.watch_new_private_threads_by_other_users,notify_new_private_threads_by_followed:this.state.notify_new_private_threads_by_followed,notify_new_private_threads_by_other_users:this.state.notify_new_private_threads_by_other_users})}handleSuccess(){P.Z.dispatch((0,L.r$)({is_hiding_presence:this.state.is_hiding_presence,limits_private_thread_invites_to:this.state.limits_private_thread_invites_to,watch_started_threads:this.state.watch_started_threads,watch_replied_threads:this.state.watch_replied_threads,watch_new_private_threads_by_followed:this.state.watch_new_private_threads_by_followed,watch_new_private_threads_by_other_users:this.state.watch_new_private_threads_by_other_users,notify_new_private_threads_by_followed:this.state.notify_new_private_threads_by_followed,notify_new_private_threads_by_other_users:this.state.notify_new_private_threads_by_other_users})),g.Z.success(pgettext("forum options form","Your forum options have been updated."))}handleError(e){400===e.status?g.Z.error(pgettext("forum options form","Please reload the page and try again.")):g.Z.apiError(e)}componentDidMount(){v.Z.set({title:pgettext("forum options title","Forum options"),parent:pgettext("forum options","Change your options")})}render(){return(0,a.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,a.Z)("div",{className:"panel panel-default panel-form"},void 0,(0,a.Z)("div",{className:"panel-heading"},void 0,(0,a.Z)("h3",{className:"panel-title"},void 0,pgettext("forum options form title","Change forum options"))),(0,a.Z)("div",{className:"panel-body"},void 0,(0,a.Z)("fieldset",{},void 0,(0,a.Z)("legend",{},void 0,pgettext("forum options form","Privacy settings")),(0,a.Z)(S.Z,{label:pgettext("forum options form","Hide my presence"),helpText:pgettext("forum options form","If you hide your presence, only members with permission to see hidden users will see when you are online."),for:"id_is_hiding_presence"},void 0,(0,a.Z)(T.Z,{id:"id_is_hiding_presence",disabled:this.state.isLoading,iconOn:"visibility_off",iconOff:"visibility",labelOn:pgettext("forum options form","Hide my presence from other users"),labelOff:pgettext("forum options form","Show my presence to other users"),onChange:this.bindInput("is_hiding_presence"),value:this.state.is_hiding_presence})),(0,a.Z)(S.Z,{label:pgettext("forum options form","Limit private thread invitations from other users"),for:"id_limits_private_thread_invites_to"},void 0,(0,a.Z)(E.Z,{id:"id_limits_private_thread_invites_to",disabled:this.state.isLoading,onChange:this.bindInput("limits_private_thread_invites_to"),value:this.state.limits_private_thread_invites_to,choices:this.privateThreadInvitesChoices}))),(0,a.Z)("fieldset",{},void 0,(0,a.Z)("legend",{},void 0,pgettext("notifications options","Notifications preferences")),(0,a.Z)(S.Z,{label:pgettext("notifications options","Automatically watch threads I start"),for:"id_watch_started_threads"},void 0,(0,a.Z)(E.Z,{id:"id_watch_started_threads",disabled:this.state.isLoading,onChange:this.bindInput("watch_started_threads"),value:this.state.watch_started_threads,choices:O})),(0,a.Z)(S.Z,{label:pgettext("notifications options","Automatically watch threads I reply to"),for:"id_watch_replied_threads"},void 0,(0,a.Z)(E.Z,{id:"id_watch_replied_threads",disabled:this.state.isLoading,onChange:this.bindInput("watch_replied_threads"),value:this.state.watch_replied_threads,choices:O})),(0,a.Z)(S.Z,{label:pgettext("notifications options","Automatically watch new private threads I'm invited to by the members I am following"),for:"id_watch_new_private_threads_by_followed"},void 0,(0,a.Z)(E.Z,{id:"id_watch_new_private_threads_by_followed",disabled:this.state.isLoading,onChange:this.bindInput("watch_new_private_threads_by_followed"),value:this.state.watch_new_private_threads_by_followed,choices:O})),(0,a.Z)(S.Z,{label:pgettext("notifications options","Automatically watch new private threads I'm invited to by other members"),for:"id_watch_new_private_threads_by_other_users"},void 0,(0,a.Z)(E.Z,{id:"id_watch_new_private_threads_by_other_users",disabled:this.state.isLoading,onChange:this.bindInput("watch_new_private_threads_by_other_users"),value:this.state.watch_new_private_threads_by_other_users,choices:O})),(0,a.Z)(S.Z,{label:pgettext("notifications options","Notify me about new private thread invitations from the members I am following"),for:"id_notify_new_private_threads_by_followed"},void 0,(0,a.Z)(E.Z,{id:"id_notify_new_private_threads_by_followed",disabled:this.state.isLoading,onChange:this.bindInput("notify_new_private_threads_by_followed"),value:this.state.notify_new_private_threads_by_followed,choices:I})),(0,a.Z)(S.Z,{label:pgettext("notifications options","Notify me about new private thread invitations from other members"),for:"id_notify_new_private_threads_by_other_users"},void 0,(0,a.Z)(E.Z,{id:"id_notify_new_private_threads_by_other_users",disabled:this.state.isLoading,onChange:this.bindInput("notify_new_private_threads_by_other_users"),value:this.state.notify_new_private_threads_by_other_users,choices:I})))),(0,a.Z)("div",{className:"panel-footer"},void 0,(0,a.Z)(h.Z,{className:"btn-primary",loading:this.state.isLoading},void 0,pgettext("forum options form btn","Save changes")))))}}var R,D=s(95187);function j(){return(0,a.Z)("div",{className:"panel panel-default panel-form"},void 0,(0,a.Z)("div",{className:"panel-heading"},void 0,(0,a.Z)("h3",{className:"panel-title"},void 0,pgettext("change username title","Change username"))),R||(R=(0,a.Z)(D.Z,{})))}var U,z,M,B,q,F,H,Y=s(33556),V=class extends o().Component{getHelpText(){return this.props.options.next_on?interpolate(pgettext("change username","You will be able to change your username %(next_change)s."),{next_change:this.props.options.next_on.fromNow()},!0):pgettext("change username","You have used up available name changes.")}render(){return(0,a.Z)("div",{className:"panel panel-default panel-form"},void 0,(0,a.Z)("div",{className:"panel-heading"},void 0,(0,a.Z)("h3",{className:"panel-title"},void 0,pgettext("change username title","Change username"))),(0,a.Z)(Y.Z,{helpText:this.getHelpText(),message:pgettext("change username","You can't change your username at the moment.")}))}},G=s(55210),$=class extends C.Z{constructor(e){super(e),this.state={username:"",validators:{username:[G.lG(),G.HR(e.options.length_min),G.gS(e.options.length_max)]},isLoading:!1}}getHelpText(){let e=[];if(this.props.options.changes_left>0){let t=npgettext("change username form","You can change your username %(changes_left)s more time.","You can change your username %(changes_left)s more times.",this.props.options.changes_left);e.push(interpolate(t,{changes_left:this.props.options.changes_left},!0))}if(this.props.user.acl.name_changes_expire>0){let t=npgettext("change username form","Used changes become available again after %(name_changes_expire)s day.","Used changes become available again after %(name_changes_expire)s days.",this.props.user.acl.name_changes_expire);e.push(interpolate(t,{name_changes_expire:this.props.user.acl.name_changes_expire},!0))}return e.length?e.join(" "):null}clean(){let e=this.validate();return e.username?(g.Z.error(e.username[0]),!1):this.state.username.trim()!==this.props.user.username||(g.Z.info(pgettext("change username form","Your new username is same as current one.")),!1)}send(){return m.Z.post(this.props.user.api.username,{username:this.state.username})}handleSuccess(e){this.setState({username:""}),this.props.complete(e.username,e.slug,e.options)}handleError(e){g.Z.apiError(e)}render(){return(0,a.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,a.Z)("div",{className:"panel panel-default panel-form"},void 0,(0,a.Z)("div",{className:"panel-heading"},void 0,(0,a.Z)("h3",{className:"panel-title"},void 0,pgettext("change username title","Change username"))),(0,a.Z)("div",{className:"panel-body"},void 0,(0,a.Z)(S.Z,{label:pgettext("change username form field","New username"),for:"id_username",helpText:this.getHelpText()},void 0,(0,a.Z)("input",{type:"text",id:"id_username",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("username"),value:this.state.username}))),(0,a.Z)("div",{className:"panel-footer"},void 0,(0,a.Z)(h.Z,{className:"btn-primary",loading:this.state.isLoading},void 0,pgettext("change username form btn","Change username")))))}},W=s(7850),Q=s(48927),X=s(6935),K=class extends o().Component{constructor(e){super(e),(0,u.Z)(this,"onComplete",((e,t,s)=>{this.setState({options:s}),P.Z.dispatch((0,Q.KP)({username:e,slug:t},this.props.user,this.props.user)),P.Z.dispatch((0,X._S)(this.props.user,e,t)),g.Z.success(pgettext("change username","Your username has been changed successfully."))})),this.state={isLoaded:!1,options:null}}componentDidMount(){v.Z.set({title:pgettext("change username title","Change username"),parent:pgettext("forum options","Change your options")}),Promise.all([m.Z.get(this.props.user.api.username),m.Z.get(Z.Z.get("USERNAME_CHANGES_API"),{user:this.props.user.id})]).then((e=>{P.Z.dispatch((0,Q.ZB)(e[1].results)),this.setState({isLoaded:!0,options:{changes_left:e[0].changes_left,length_min:e[0].length_min,length_max:e[0].length_max,next_on:e[0].next_on?x()(e[0].next_on):null}})}))}getChangeForm(){return this.state.isLoaded?0===this.state.options.changes_left?(0,a.Z)(V,{options:this.state.options}):(0,a.Z)($,{complete:this.onComplete,options:this.state.options,user:this.props.user}):U||(U=(0,a.Z)(j,{}))}render(){return(0,a.Z)("div",{},void 0,this.getChangeForm(),(0,a.Z)(W.Z,{changes:this.props["username-history"],isLoaded:this.state.isLoaded}))}},J=class extends C.Z{constructor(e){super(e),this.state={new_email:"",password:"",validators:{new_email:[G.Do()],password:[]},isLoading:!1}}clean(){let e=this.validate();return-1!==[this.state.new_email.trim().length,this.state.password.trim().length].indexOf(0)?(g.Z.error(pgettext("change email form","Fill out all fields.")),!1):!e.new_email||(g.Z.error(e.new_email[0]),!1)}send(){return m.Z.post(this.props.user.api.change_email,{new_email:this.state.new_email,password:this.state.password})}handleSuccess(e){this.setState({new_email:"",password:""}),g.Z.success(e.detail)}handleError(e){400===e.status?e.new_email?g.Z.error(e.new_email):g.Z.error(e.password):g.Z.apiError(e)}render(){return(0,a.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,a.Z)("input",{type:"type",style:{display:"none"}}),(0,a.Z)("input",{type:"password",style:{display:"none"}}),(0,a.Z)("div",{className:"panel panel-default panel-form"},void 0,(0,a.Z)("div",{className:"panel-heading"},void 0,(0,a.Z)("h3",{className:"panel-title"},void 0,pgettext("change email title","Change e-mail address"))),(0,a.Z)("div",{className:"panel-body"},void 0,(0,a.Z)(S.Z,{label:pgettext("change email form field","New e-mail"),for:"id_new_email"},void 0,(0,a.Z)("input",{type:"text",id:"id_new_email",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("new_email"),value:this.state.new_email})),z||(z=(0,a.Z)("hr",{})),(0,a.Z)(S.Z,{label:pgettext("change email form field","Your current password"),for:"id_confirm_email"},void 0,(0,a.Z)("input",{type:"password",id:"id_confirm_email",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("password"),value:this.state.password}))),(0,a.Z)("div",{className:"panel-footer"},void 0,(0,a.Z)(h.Z,{className:"btn-primary",loading:this.state.isLoading},void 0,pgettext("change email form btn","Change e-mail")))))}},ee=class extends C.Z{constructor(e){super(e),this.state={new_password:"",repeat_password:"",password:"",validators:{new_password:[],repeat_password:[],password:[]},isLoading:!1}}clean(){let e=this.validate();return-1!==[this.state.new_password.trim().length,this.state.repeat_password.trim().length,this.state.password.trim().length].indexOf(0)?(g.Z.error(gettext("Fill out all fields.")),!1):e.new_password?(g.Z.error(e.new_password[0]),!1):this.state.new_password===this.state.repeat_password||(g.Z.error(pgettext("change password form","New passwords are different.")),!1)}send(){return m.Z.post(this.props.user.api.change_password,{new_password:this.state.new_password,password:this.state.password})}handleSuccess(e){this.setState({new_password:"",repeat_password:"",password:""}),g.Z.success(e.detail)}handleError(e){400===e.status?e.new_password?g.Z.error(e.new_password):g.Z.error(e.password):g.Z.apiError(e)}render(){return(0,a.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,a.Z)("input",{type:"type",style:{display:"none"}}),(0,a.Z)("input",{type:"password",style:{display:"none"}}),(0,a.Z)("div",{className:"panel panel-default panel-form"},void 0,(0,a.Z)("div",{className:"panel-heading"},void 0,(0,a.Z)("h3",{className:"panel-title"},void 0,pgettext("change password title","Change password"))),(0,a.Z)("div",{className:"panel-body"},void 0,(0,a.Z)(S.Z,{label:pgettext("change password form field","New password"),for:"id_new_password"},void 0,(0,a.Z)("input",{type:"password",id:"id_new_password",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("new_password"),value:this.state.new_password})),(0,a.Z)(S.Z,{label:pgettext("change password form field","Repeat password"),for:"id_repeat_password"},void 0,(0,a.Z)("input",{type:"password",id:"id_repeat_password",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("repeat_password"),value:this.state.repeat_password})),M||(M=(0,a.Z)("hr",{})),(0,a.Z)(S.Z,{label:pgettext("change password form field","Your current password"),for:"id_confirm_password"},void 0,(0,a.Z)("input",{type:"password",id:"id_confirm_password",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("password"),value:this.state.password}))),(0,a.Z)("div",{className:"panel-footer"},void 0,(0,a.Z)(h.Z,{className:"btn-primary",loading:this.state.isLoading},void 0,pgettext("change password form btn","Change password")))))}},te=()=>(0,a.Z)("div",{className:"panel panel-default panel-form"},void 0,(0,a.Z)("div",{className:"panel-heading"},void 0,(0,a.Z)("h3",{className:"panel-title"},void 0,pgettext("change sign in credentials title","Change email or password"))),(0,a.Z)("div",{className:"panel-body panel-message-body"},void 0,B||(B=(0,a.Z)("div",{className:"message-icon"},void 0,(0,a.Z)("span",{className:"material-icon"},void 0,"info_outline"))),(0,a.Z)("div",{className:"message-body"},void 0,(0,a.Z)("p",{className:"lead"},void 0,pgettext("change sign in credentials","You need to set a password for your account to be able to change your username or email.")),(0,a.Z)("p",{className:"help-block"},void 0,(0,a.Z)("a",{className:"btn btn-primary",href:Z.Z.get("FORGOTTEN_PASSWORD_URL")},void 0,pgettext("change sign in credentials link","Set password")))))),se=class extends o().Component{componentDidMount(){v.Z.set({title:pgettext("change sign in credentials title","Change email or password"),parent:pgettext("forum options","Change your options")})}render(){return this.props.user.has_usable_password?(0,a.Z)("div",{},void 0,(0,a.Z)(J,{user:this.props.user}),(0,a.Z)(ee,{user:this.props.user}),(0,a.Z)("p",{className:"message-line"},void 0,F||(F=(0,a.Z)("span",{className:"material-icon"},void 0,"warning")),(0,a.Z)("a",{href:Z.Z.get("FORGOTTEN_PASSWORD_URL")},void 0,pgettext("change sign in credentials link","Change forgotten password")))):q||(q=(0,a.Z)(te,{}))}},ae=s(82125),ie=s(98936),oe=s(59131),ne=s(99755),re=class extends ae.Z{render(){const e=Z.Z.get("USER_OPTIONS").filter((e=>{const t=Z.Z.get("USERCP_URL")+e.component+"/";return this.props.location.pathname.substr(0,t.length)===t}))[0];return(0,a.Z)("div",{className:"page page-options"},void 0,(0,a.Z)(ne.sP,{},void 0,(0,a.Z)(ne.mr,{styleName:"options"},void 0,(0,a.Z)(ne.gC,{styleName:"options"},void 0,(0,a.Z)(ie.gq,{},void 0,(0,a.Z)(ie.kw,{auto:!0},void 0,(0,a.Z)(ie.Z6,{auto:!0},void 0,(0,a.Z)("h1",{},void 0,pgettext("forum options","Change your options"))),(0,a.Z)(ie.Z6,{className:"hidden-xs hidden-md hidden-lg",shrink:!0},void 0,(0,a.Z)("div",{className:"dropdown"},void 0,(0,a.Z)("button",{type:"button",className:"btn btn-default btn-outline btn-icon dropdown-toggle",title:pgettext("forum options nav btn","Menu"),"data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false"},void 0,H||(H=(0,a.Z)("span",{className:"material-icon"},void 0,"menu"))),(0,a.Z)(c,{className:"dropdown-menu dropdown-menu-right",baseUrl:Z.Z.get("USERCP_URL"),options:Z.Z.get("USER_OPTIONS")})))),(0,a.Z)(ie.kw,{className:"hidden-sm hidden-md hidden-lg"},void 0,(0,a.Z)(ie.Z6,{},void 0,(0,a.Z)("div",{className:"dropdown"},void 0,(0,a.Z)("button",{type:"button",className:"btn btn-default btn-outline btn-block dropdown-toggle","data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false"},void 0,(0,a.Z)("span",{className:"material-icon"},void 0,e.icon),e.name),(0,a.Z)(c,{className:"dropdown-menu",baseUrl:Z.Z.get("USERCP_URL"),options:Z.Z.get("USER_OPTIONS")})))))))),(0,a.Z)(oe.Z,{},void 0,(0,a.Z)("div",{className:"row"},void 0,(0,a.Z)("div",{className:"col-md-3 hidden-xs hidden-sm"},void 0,(0,a.Z)(d,{baseUrl:Z.Z.get("USERCP_URL"),options:Z.Z.get("USER_OPTIONS")})),(0,a.Z)("div",{className:"col-md-9"},void 0,this.props.children))))}};function le(e){return{tick:e.tick.tick,user:e.auth.user,"username-history":e["username-history"]}}function de(){const e=[{path:Z.Z.get("USERCP_URL")+"forum-options/",component:(0,n.$j)(le)(A)},{path:Z.Z.get("USERCP_URL")+"edit-details/",component:(0,n.$j)(le)(_)}],t=Z.Z.get("SETTINGS").DELEGATE_AUTH;return t||(e.push({path:Z.Z.get("USERCP_URL")+"change-username/",component:(0,n.$j)(le)(K)}),e.push({path:Z.Z.get("USERCP_URL")+"sign-in-credentials/",component:(0,n.$j)(le)(se)})),Z.Z.get("ENABLE_DOWNLOAD_OWN_DATA")&&e.push({path:Z.Z.get("USERCP_URL")+"download-data/",component:(0,n.$j)(le)(y)}),!t&&Z.Z.get("ENABLE_DELETE_OWN_ACCOUNT")&&e.push({path:Z.Z.get("USERCP_URL")+"delete-account/",component:(0,n.$j)(le)(f)}),e}var ce=s(39633);Z.Z.addInitializer({name:"component:options",initializer:function(e){e.has("USER_OPTIONS")&&(0,ce.Z)({root:Z.Z.get("USERCP_URL"),component:re,paths:de()})},after:"store"})},95563:function(e,t,s){"use strict";var a,i=s(37424),o=s(22928),n=s(4942),r=s(57588),l=s.n(r),d=s(30381),c=s.n(d),p=s(95187),u=s(33556),h=s(32233),m=s(55547),v=s(53328),g=class extends l().Component{constructor(e){super(e),(0,n.Z)(this,"update",(e=>{e.expires_on&&(e.expires_on=c()(e.expires_on)),this.setState({isLoaded:!0,error:null,ban:e})})),(0,n.Z)(this,"error",(e=>{this.setState({isLoaded:!0,error:e.detail,ban:null})})),h.Z.has("PROFILE_BAN")?this.initWithPreloadedData(h.Z.pop("PROFILE_BAN")):this.initWithoutPreloadedData(),this.startPolling(e.profile.api.ban)}initWithPreloadedData(e){e.expires_on&&(e.expires_on=c()(e.expires_on)),this.state={isLoaded:!0,ban:e}}initWithoutPreloadedData(){this.state={isLoaded:!1}}startPolling(e){m.Z.start({poll:"ban-details",url:e,frequency:9e4,update:this.update,error:this.error})}componentDidMount(){v.Z.set({title:pgettext("profile ban details title","Ban details"),parent:this.props.profile.username})}componentWillUnmount(){m.Z.stop("ban-details")}getUserMessage(){return this.state.ban.user_message?(0,o.Z)("div",{className:"panel-body ban-message ban-user-message"},void 0,(0,o.Z)("h4",{},void 0,pgettext("profile ban details","User-shown ban message")),(0,o.Z)("div",{className:"lead",dangerouslySetInnerHTML:{__html:this.state.ban.user_message.html}})):null}getStaffMessage(){return this.state.ban.staff_message?(0,o.Z)("div",{className:"panel-body ban-message ban-staff-message"},void 0,(0,o.Z)("h4",{},void 0,pgettext("profile ban details","Team-shown ban message")),(0,o.Z)("div",{className:"lead",dangerouslySetInnerHTML:{__html:this.state.ban.staff_message.html}})):null}getExpirationMessage(){if(this.state.ban.expires_on){if(this.state.ban.expires_on.isAfter(c()())){let e=interpolate(pgettext("profile ban details","This ban expires on %(expires_on)s."),{expires_on:this.state.ban.expires_on.format("LL, LT")},!0),t=interpolate(pgettext("profile ban details","This ban expires %(expires_on)s."),{expires_on:this.state.ban.expires_on.fromNow()},!0);return(0,o.Z)("abbr",{title:e},void 0,t)}return pgettext("profile ban details","This ban has expired.")}return interpolate(pgettext("profile ban details","%(username)s's ban is permanent."),{username:this.props.profile.username},!0)}getPanelBody(){return this.state.ban?Object.keys(this.state.ban).length?(0,o.Z)("div",{},void 0,this.getUserMessage(),this.getStaffMessage(),(0,o.Z)("div",{className:"panel-body ban-expires"},void 0,(0,o.Z)("h4",{},void 0,pgettext("profile ban details","Ban expiration")),(0,o.Z)("p",{className:"lead"},void 0,this.getExpirationMessage()))):(0,o.Z)("div",{},void 0,(0,o.Z)(u.Z,{message:pgettext("profile ban details","No ban is active at the moment.")})):this.state.error?(0,o.Z)("div",{},void 0,(0,o.Z)(u.Z,{icon:"error_outline",message:this.state.error})):a||(a=(0,o.Z)("div",{},void 0,(0,o.Z)(p.Z,{})))}render(){return(0,o.Z)("div",{className:"profile-ban-details"},void 0,(0,o.Z)("div",{className:"panel panel-default"},void 0,(0,o.Z)("div",{className:"panel-heading"},void 0,(0,o.Z)("h3",{className:"panel-title"},void 0,pgettext("profile ban details title","Ban details"))),this.getPanelBody()))}},Z=s(21688);function f(e){let{api:t,display:s,onCancel:a,onSuccess:i}=e;return s?(0,o.Z)(Z.Z,{api:t,onCancel:a,onSuccess:i}):null}function b(e){let{isAuthenticated:t,profile:s}=e,a=null;return a=t?pgettext("profile details empty","You are not sharing any details with others."):interpolate(pgettext("profile details empty","%(username)s is not sharing any details with others."),{username:s.username},!0),(0,o.Z)("div",{className:"panel panel-default"},void 0,(0,o.Z)("div",{className:"panel-body text-center lead"},void 0,a))}function _(e){let{html:t,text:s,url:a}=e;return t?(0,o.Z)("div",{className:"form-control-static col-md-9",dangerouslySetInnerHTML:{__html:t}}):(0,o.Z)("div",{className:"form-control-static col-md-9"},void 0,(0,o.Z)(N,{text:s,url:a}))}function N(e){let{text:t,url:s}=e;return s?(0,o.Z)("p",{},void 0,(0,o.Z)("a",{href:s,target:"_blank",rel:"nofollow"},void 0,t||s)):t?(0,o.Z)("p",{},void 0,t):null}function x(e){return(0,o.Z)("div",{className:"form-group"},void 0,(0,o.Z)("strong",{className:"control-label col-md-3"},void 0,e.name,":"),l().createElement(_,e))}function y(e){let{fields:t,name:s}=e;return(0,o.Z)("div",{className:"panel panel-default panel-profile-details-group"},void 0,(0,o.Z)("div",{className:"panel-heading"},void 0,(0,o.Z)("h3",{className:"panel-title"},void 0,s)),(0,o.Z)("div",{className:"panel-body"},void 0,(0,o.Z)("div",{className:"form-horizontal"},void 0,t.map((e=>{let{fieldname:t,html:s,name:a,text:i,url:n}=e;return(0,o.Z)(x,{name:a,html:s,text:i,url:n},t)})))))}var w,k=s(37848);function C(e){let{display:t,groups:s,isAuthenticated:a,loading:i,profile:n}=e;return t?i?w||(w=(0,o.Z)(k.Z,{})):s.length?(0,o.Z)("div",{},void 0,s.map(((e,t)=>(0,o.Z)(y,{fields:e.fields,name:e.name},t)))):(0,o.Z)(b,{isAuthenticated:a,profile:n}):null}var S,E=s(92490),T=e=>{let{onEdit:t,showEditButton:s}=e;return(0,o.Z)(E.o8,{},void 0,(0,o.Z)(E.Z2,{auto:!0},void 0,(0,o.Z)(E.Eg,{auto:!0},void 0,(0,o.Z)("h3",{},void 0,pgettext("profile details title","Details")))),s&&(0,o.Z)(E.Z2,{},void 0,(0,o.Z)(E.Eg,{},void 0,(0,o.Z)("button",{className:"btn btn-default btn-outline btn-block",onClick:t,type:"button"},void 0,pgettext("profile details edit btn","Edit")))))},L=s(58598),P=s(78657),O=s(53904),I=class extends l().Component{componentDidMount(){const{data:e,dispatch:t,user:s}=this.props;e&&e.id===s.id||P.Z.get(this.props.user.api.details).then((e=>{t((0,L.zD)(e))}),(e=>{O.Z.apiError(e)}))}render(){return this.props.children}},A=class extends l().Component{constructor(e){super(e),(0,n.Z)(this,"onCancel",(()=>{this.setState({editing:!1})})),(0,n.Z)(this,"onEdit",(()=>{this.setState({editing:!0})})),(0,n.Z)(this,"onSuccess",(e=>{const{dispatch:t,isAuthenticated:s,profile:a}=this.props;let i=null;i=s?pgettext("profile details form","Your details have been updated."):interpolate(pgettext("profile details form","%(username)s's details have been updated."),{username:a.username},!0),O.Z.info(i),t((0,L.zD)(e)),this.setState({editing:!1})})),this.state={editing:!1}}componentDidMount(){v.Z.set({title:pgettext("profile details title","Details"),parent:this.props.profile.username})}render(){const{dispatch:e,isAuthenticated:t,profile:s,profileDetails:a}=this.props,i=a.id!==s.id;return(0,o.Z)(I,{data:a,dispatch:e,user:s},void 0,(0,o.Z)("div",{className:"profile-details"},void 0,(0,o.Z)(T,{onEdit:this.onEdit,showEditButton:!!a.edit&&!this.state.editing}),(0,o.Z)(C,{display:!this.state.editing,groups:a.groups,isAuthenticated:t,loading:i,profile:s}),(0,o.Z)(f,{api:s.api.edit_details,dispatch:e,display:this.state.editing,onCancel:this.onCancel,onSuccess:this.onSuccess})))}},R=s(87462),D=s(11005),j=s(82211),U=s(21981),z=s(90287),M=class extends l().Component{constructor(e){super(e),(0,n.Z)(this,"loadMore",(()=>{this.setState({isLoading:!0}),this.loadItems(this.props.posts.next)})),this.state={isLoading:!1}}loadItems(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;P.Z.get(this.props.api,{start:e||0}).then((t=>{0===e?z.Z.dispatch(U.zD(t)):z.Z.dispatch(U.R3(t)),this.setState({isLoading:!1})}),(e=>{this.setState({isLoading:!1}),O.Z.apiError(e)}))}componentDidMount(){v.Z.set({title:this.props.title,parent:this.props.profile.username}),this.loadItems()}render(){return(0,o.Z)("div",{className:"profile-feed"},void 0,(0,o.Z)(E.o8,{},void 0,(0,o.Z)(E.Z2,{auto:!0},void 0,(0,o.Z)(E.Eg,{auto:!0},void 0,(0,o.Z)("h3",{},void 0,this.props.header)))),l().createElement(B,(0,R.Z)({isLoading:this.state.isLoading,loadMore:this.loadMore},this.props)))}};function B(e){return e.posts.isLoaded&&!e.posts.results.length?(0,o.Z)("p",{className:"lead"},void 0,e.emptyMessage):(0,o.Z)("div",{},void 0,(0,o.Z)(D.Z,{isReady:e.posts.isLoaded,posts:e.posts.results,poster:e.profile}),(0,o.Z)(q,{isLoading:e.isLoading,loadMore:e.loadMore,next:e.posts.next}))}function q(e){return e.next?(0,o.Z)("div",{className:"pager-more"},void 0,(0,o.Z)(j.Z,{className:"btn btn-default btn-outline",loading:e.isLoading,onClick:e.loadMore},void 0,pgettext("profile load more btn","Show older activity"))):null}var F,H,Y,V,G,$,W,Q,X,K,J,ee=class extends l().Component{getClassName(){return this.props.className?"form-search "+this.props.className:"form-search"}render(){return(0,o.Z)("div",{className:this.getClassName()},void 0,(0,o.Z)("input",{type:"text",className:"form-control",value:this.props.value,onChange:this.props.onChange,placeholder:this.props.placeholder||pgettext("quick search placeholder","Search...")}),S||(S=(0,o.Z)("span",{className:"material-icon"},void 0,"search")))}},te=s(40429),se=s(6935),ae=class extends l().Component{constructor(e){super(e),(0,n.Z)(this,"loadMore",(()=>{this.setState({isBusy:!0}),this.loadUsers(this.state.page+1,this.state.search)})),(0,n.Z)(this,"search",(e=>{this.setState({isLoaded:!1,isBusy:!0,search:e.target.value,count:0,more:0,page:1,pages:1}),this.loadUsers(1,e.target.value)})),this.setSpecialProps(),h.Z.has(this.PRELOADED_DATA_KEY)?this.initWithPreloadedData(h.Z.pop(this.PRELOADED_DATA_KEY)):this.initWithoutPreloadedData()}setSpecialProps(){this.PRELOADED_DATA_KEY="PROFILE_FOLLOWERS",this.TITLE=pgettext("profile followers title","Followers"),this.API_FILTER="followers"}initWithPreloadedData(e){this.state={isLoaded:!0,isBusy:!1,search:"",count:e.count,more:e.more,page:e.page,pages:e.pages},z.Z.dispatch((0,se.ZB)(e.results))}initWithoutPreloadedData(){this.state={isLoaded:!1,isBusy:!1,search:"",count:0,more:0,page:1,pages:1},this.loadUsers()}loadUsers(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const s=this.props.profile.api[this.API_FILTER];P.Z.get(s,{search:t,page:e||1},"user-"+this.API_FILTER).then((t=>{1===e?z.Z.dispatch((0,se.ZB)(t.results)):z.Z.dispatch((0,se.R3)(t.results)),this.setState({isLoaded:!0,isBusy:!1,count:t.count,more:t.more,page:t.page,pages:t.pages})}),(e=>{O.Z.apiError(e)}))}componentDidMount(){v.Z.set({title:this.TITLE,parent:this.props.profile.username})}getLabel(){if(this.state.isLoaded){if(this.state.search){let e=npgettext("profile followers","Found %(users)s user.","Found %(users)s users.",this.state.count);return interpolate(e,{users:this.state.count},!0)}if(this.props.profile.id===this.props.user.id){let e=npgettext("profile followers","You have %(users)s follower.","You have %(users)s followers.",this.state.count);return interpolate(e,{users:this.state.count},!0)}{let e=npgettext("profile followers","%(username)s has %(users)s follower.","%(username)s has %(users)s followers.",this.state.count);return interpolate(e,{username:this.props.profile.username,users:this.state.count},!0)}}return pgettext("Loading...")}getEmptyMessage(){return this.state.search?pgettext("profile followers","Search returned no users matching specified criteria."):this.props.user.id===this.props.profile.id?pgettext("profile followers","You have no followers."):interpolate(pgettext("profile followers","%(username)s has no followers."),{username:this.props.profile.username},!0)}getMoreButton(){return this.state.more?(0,o.Z)("div",{className:"pager-more"},void 0,(0,o.Z)(j.Z,{className:"btn btn-default btn-outline",loading:this.state.isBusy,onClick:this.loadMore},void 0,interpolate(pgettext("profile followers","Show more (%(more)s)"),{more:this.state.more},!0))):null}getListBody(){return this.state.isLoaded&&0===this.state.count?(0,o.Z)("p",{className:"lead"},void 0,this.getEmptyMessage()):(0,o.Z)("div",{},void 0,(0,o.Z)(te.Z,{cols:3,isReady:this.state.isLoaded,users:this.props.users}),this.getMoreButton())}getClassName(){return"profile-"+this.API_FILTER}render(){return(0,o.Z)("div",{className:this.getClassName()},void 0,(0,o.Z)(E.o8,{},void 0,(0,o.Z)(E.Z2,{auto:!0},void 0,(0,o.Z)(E.Eg,{auto:!0},void 0,(0,o.Z)("h3",{},void 0,this.getLabel()))),(0,o.Z)(E.Z2,{},void 0,(0,o.Z)(E.Eg,{},void 0,(0,o.Z)(ee,{value:this.state.search,onChange:this.search,placeholder:pgettext("profile followers search","Search users...")})))),this.getListBody())}},ie=s(7850),oe=s(48927),ne=class extends l().Component{constructor(e){super(e),(0,n.Z)(this,"loadMore",(()=>{this.setState({isBusy:!0}),this.loadChanges(this.state.page+1,this.state.search)})),(0,n.Z)(this,"search",(e=>{this.setState({isLoaded:!1,isBusy:!0,search:e.target.value,count:0,more:0,page:1,pages:1}),this.loadChanges(1,e.target.value)})),h.Z.has("PROFILE_NAME_HISTORY")?this.initWithPreloadedData(h.Z.pop("PROFILE_NAME_HISTORY")):this.initWithoutPreloadedData()}initWithPreloadedData(e){this.state={isLoaded:!0,isBusy:!1,search:"",count:e.count,more:e.more,page:e.page,pages:e.pages},z.Z.dispatch((0,oe.ZB)(e.results))}initWithoutPreloadedData(){this.state={isLoaded:!1,isBusy:!1,search:"",count:0,more:0,page:1,pages:1},this.loadChanges()}loadChanges(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;P.Z.get(h.Z.get("USERNAME_CHANGES_API"),{user:this.props.profile.id,search:t,page:e||1},"search-username-history").then((t=>{1===e?z.Z.dispatch((0,oe.ZB)(t.results)):z.Z.dispatch((0,oe.R3)(t.results)),this.setState({isLoaded:!0,isBusy:!1,count:t.count,more:t.more,page:t.page,pages:t.pages})}),(e=>{O.Z.apiError(e)}))}componentDidMount(){v.Z.set({title:pgettext("profile username history title","Username history"),parent:this.props.profile.username})}getLabel(){if(this.state.isLoaded){if(this.state.search){let e=npgettext("profile username history","Found %(changes)s username change.","Found %(changes)s username changes.",this.state.count);return interpolate(e,{changes:this.state.count},!0)}if(this.props.profile.id===this.props.user.id){let e=npgettext("profile username history","Your username was changed %(changes)s time.","Your username was changed %(changes)s times.",this.state.count);return interpolate(e,{changes:this.state.count},!0)}{let e=npgettext("profile username history","%(username)s's username was changed %(changes)s time.","%(username)s's username was changed %(changes)s times.",this.state.count);return interpolate(e,{username:this.props.profile.username,changes:this.state.count},!0)}}return pgettext("profile username history","Loading...")}getEmptyMessage(){return this.state.search?pgettext("profile username history","Search returned no username changes matching specified criteria."):this.props.user.id===this.props.profile.id?pgettext("profile username history","No name changes have been recorded for your account."):interpolate(pgettext("profile username history","%(username)s's username was never changed."),{username:this.props.profile.username},!0)}getMoreButton(){return this.state.more?(0,o.Z)("div",{className:"pager-more"},void 0,(0,o.Z)(j.Z,{className:"btn btn-default btn-outline",loading:this.state.isBusy,onClick:this.loadMore},void 0,interpolate(pgettext("profile username history","Show older (%(more)s)"),{more:this.state.more},!0))):null}render(){return(0,o.Z)("div",{className:"profile-username-history"},void 0,(0,o.Z)(E.o8,{},void 0,(0,o.Z)(E.Z2,{auto:!0},void 0,(0,o.Z)(E.Eg,{auto:!0},void 0,(0,o.Z)("h3",{},void 0,this.getLabel()))),(0,o.Z)(E.Z2,{},void 0,(0,o.Z)(E.Eg,{},void 0,(0,o.Z)(ee,{value:this.state.search,onChange:this.search,placeholder:pgettext("profile username history search input","Search history...")})))),(0,o.Z)(ie.Z,{isLoaded:this.state.isLoaded,emptyMessage:this.getEmptyMessage(),changes:this.props["username-history"]}),this.getMoreButton())}},re=s(82125),le=s(27519),de=s(59131),ce=s(19605),pe=s(98936),ue=s(99755),he=class extends l().Component{constructor(e){super(e),(0,n.Z)(this,"action",(()=>{this.setState({isLoading:!0}),this.props.profile.is_followed?z.Z.dispatch((0,le.r$)({is_followed:!1,followers:this.props.profile.followers-1})):z.Z.dispatch((0,le.r$)({is_followed:!0,followers:this.props.profile.followers+1})),P.Z.post(this.props.profile.api.follow).then((e=>{this.setState({isLoading:!1}),z.Z.dispatch((0,le.r$)(e))}),(e=>{this.setState({isLoading:!1}),O.Z.apiError(e)}))})),this.state={isLoading:!1}}getClassName(){return this.props.profile.is_followed?this.props.className+" btn-default btn-following":this.props.className+" btn-default btn-follow"}getIcon(){return this.props.profile.is_followed?"favorite":"favorite_border"}getLabel(){return this.props.profile.is_followed?pgettext("user profile follow btn","Following"):pgettext("user profile follow btn","Follow")}render(){return(0,o.Z)(j.Z,{className:this.getClassName(),disabled:this.state.isLoading,onClick:this.action},void 0,(0,o.Z)("span",{className:"material-icon"},void 0,this.getIcon()),this.getLabel())}},me=s(64646),ve=class extends l().Component{constructor(){super(...arguments),(0,n.Z)(this,"onClick",(()=>{me.Z.open({mode:"START_PRIVATE",submit:h.Z.get("PRIVATE_THREADS_API"),to:[this.props.profile]})}))}render(){const e=this.props.user.acl.can_start_private_threads,t=this.props.user.id===this.props.profile.id;return!e||t?null:(0,o.Z)("button",{className:this.props.className,onClick:this.onClick,type:"button"},void 0,F||(F=(0,o.Z)("span",{className:"material-icon"},void 0,"comment")),pgettext("profile message btn","Message"))}},ge=s(43345),Ze=s(96359),fe=s(3784),be=s(7227),_e=s(30337),Ne=class extends ge.Z{constructor(e){super(e),this.state={isLoaded:!1,isLoading:!1,error:null,is_avatar_locked:"",avatar_lock_user_message:"",avatar_lock_staff_message:""}}componentDidMount(){P.Z.get(this.props.profile.api.moderate_avatar).then((e=>{this.setState({isLoaded:!0,is_avatar_locked:e.is_avatar_locked,avatar_lock_user_message:e.avatar_lock_user_message||"",avatar_lock_staff_message:e.avatar_lock_staff_message||""})}),(e=>{this.setState({isLoaded:!0,error:e.detail})}))}clean(){return!!this.isValid()||(O.Z.error(this.validate().username[0]),!1)}send(){return P.Z.post(this.props.profile.api.moderate_avatar,{is_avatar_locked:this.state.is_avatar_locked,avatar_lock_user_message:this.state.avatar_lock_user_message,avatar_lock_staff_message:this.state.avatar_lock_staff_message})}handleSuccess(e){z.Z.dispatch((0,se.n1)(this.props.profile,e.avatar_hash)),O.Z.success(pgettext("profile avatar moderation","Avatar controls have been changed."))}getFormBody(){return(0,o.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,o.Z)("div",{className:"modal-body"},void 0,(0,o.Z)(Ze.Z,{label:pgettext("profile avatar moderation field","Lock avatar"),helpText:pgettext("profile avatar moderation field","Locking user avatar will prohibit user from changing his avatar and will reset his/her avatar to default one."),for:"id_is_avatar_locked"},void 0,(0,o.Z)(be.Z,{id:"id_is_avatar_locked",disabled:this.state.isLoading,iconOn:"lock_outline",iconOff:"lock_open",labelOn:pgettext("profile avatar moderation field","Disallow user from changing avatar"),labelOff:pgettext("profile avatar moderation field","Allow user to change avatar"),onChange:this.bindInput("is_avatar_locked"),value:this.state.is_avatar_locked})),(0,o.Z)(Ze.Z,{label:pgettext("profile avatar moderation field","User message"),helpText:pgettext("profile avatar moderation field","Optional message for user explaining why he/she is prohibited form changing avatar."),for:"id_avatar_lock_user_message"},void 0,(0,o.Z)("textarea",{id:"id_avatar_lock_user_message",className:"form-control",rows:"4",disabled:this.state.isLoading,onChange:this.bindInput("avatar_lock_user_message"),value:this.state.avatar_lock_user_message})),(0,o.Z)(Ze.Z,{label:pgettext("profile avatar moderation field","Staff message"),helpText:pgettext("profile avatar moderation field","Optional message for forum team members explaining why user is prohibited form changing avatar."),for:"id_avatar_lock_staff_message"},void 0,(0,o.Z)("textarea",{id:"id_avatar_lock_staff_message",className:"form-control",rows:"4",disabled:this.state.isLoading,onChange:this.bindInput("avatar_lock_staff_message"),value:this.state.avatar_lock_staff_message}))),(0,o.Z)("div",{className:"modal-footer"},void 0,(0,o.Z)("button",{type:"button",className:"btn btn-default","data-dismiss":"modal"},void 0,pgettext("profile avatar moderation btn","Close")),(0,o.Z)(j.Z,{className:"btn-primary",loading:this.state.isLoading},void 0,pgettext("profile avatar moderation btn","Save changes"))))}getModalBody(){return this.state.error?(0,o.Z)(_e.Z,{icon:"remove_circle_outline",message:this.state.error}):this.state.isLoaded?this.getFormBody():H||(H=(0,o.Z)(fe.Z,{}))}getClassName(){return this.state.error?"modal-dialog modal-message modal-avatar-controls":"modal-dialog modal-avatar-controls"}render(){return(0,o.Z)("div",{className:this.getClassName(),role:"document"},void 0,(0,o.Z)("div",{className:"modal-content"},void 0,(0,o.Z)("div",{className:"modal-header"},void 0,(0,o.Z)("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":pgettext("modal","Close")},void 0,Y||(Y=(0,o.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,o.Z)("h4",{className:"modal-title"},void 0,pgettext("profile avatar moderation title","Avatar controls"))),this.getModalBody()))}},xe=s(55210),ye=class extends ge.Z{constructor(e){super(e),this.state={isLoaded:!1,isLoading:!1,error:null,username:"",validators:{username:[xe.lG()]}}}componentDidMount(){P.Z.get(this.props.profile.api.moderate_username).then((()=>{this.setState({isLoaded:!0})}),(e=>{this.setState({isLoaded:!0,error:e.detail})}))}clean(){return!!this.isValid()||(O.Z.error(this.validate().username[0]),!1)}send(){return P.Z.post(this.props.profile.api.moderate_username,{username:this.state.username})}handleSuccess(e){this.setState({username:""}),z.Z.dispatch((0,oe.KP)(e,this.props.profile,this.props.user)),z.Z.dispatch((0,se._S)(this.props.profile,e.username,e.slug)),O.Z.success(pgettext("profile username moderation","Username has been changed."))}getFormBody(){return(0,o.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,o.Z)("div",{className:"modal-body"},void 0,(0,o.Z)(Ze.Z,{label:pgettext("profile username moderation field","New username"),for:"id_username"},void 0,(0,o.Z)("input",{type:"text",id:"id_username",className:"form-control",disabled:this.state.isLoading,onChange:this.bindInput("username"),value:this.state.username}))),(0,o.Z)("div",{className:"modal-footer"},void 0,(0,o.Z)("button",{className:"btn btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},void 0,pgettext("profile username moderation btn","Cancel")),(0,o.Z)(j.Z,{className:"btn-primary",loading:this.state.isLoading},void 0,pgettext("profile username moderation btn","Change username"))))}getModalBody(){return this.state.error?(0,o.Z)(_e.Z,{icon:"remove_circle_outline",message:this.state.error}):this.state.isLoaded?this.getFormBody():V||(V=(0,o.Z)(fe.Z,{}))}getClassName(){return this.state.error?"modal-dialog modal-message modal-rename-user":"modal-dialog modal-rename-user"}render(){return(0,o.Z)("div",{className:this.getClassName(),role:"document"},void 0,(0,o.Z)("div",{className:"modal-content"},void 0,(0,o.Z)("div",{className:"modal-header"},void 0,(0,o.Z)("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":pgettext("modal","Close")},void 0,G||(G=(0,o.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,o.Z)("h4",{className:"modal-title"},void 0,pgettext("profile username moderation title","Change username"))),this.getModalBody()))}},we=class extends ge.Z{constructor(e){super(e),(0,n.Z)(this,"countdown",(()=>{window.setTimeout((()=>{this.state.countdown>1?(this.setState({countdown:this.state.countdown-1}),this.countdown()):this.state.confirm||this.setState({confirm:!0})}),1e3)})),this.state={isLoaded:!1,isLoading:!1,isDeleted:!1,error:null,countdown:5,confirm:!1,with_content:!1}}componentDidMount(){P.Z.get(this.props.profile.api.delete).then((()=>{this.setState({isLoaded:!0}),this.countdown()}),(e=>{this.setState({isLoaded:!0,error:e.detail})}))}send(){return P.Z.post(this.props.profile.api.delete,{with_content:this.state.with_content})}handleSuccess(){m.Z.stop("user-profile"),this.state.with_content?this.setState({isDeleted:interpolate(pgettext("profile delete","%(username)s's account, threads, posts and other content has been deleted."),{username:this.props.profile.username},!0)}):this.setState({isDeleted:interpolate(pgettext("profile delete","%(username)s's account has been deleted and other content has been hidden."),{username:this.props.profile.username},!0)})}getButtonLabel(){return this.state.confirm?interpolate(pgettext("profile delete btn","Delete %(username)s"),{username:this.props.profile.username},!0):interpolate(pgettext("profile delete btn","Please wait... (%(countdown)ss)"),{countdown:this.state.countdown},!0)}getForm(){return(0,o.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,o.Z)("div",{className:"modal-body"},void 0,(0,o.Z)(Ze.Z,{label:pgettext("profile delete","User content"),for:"id_with_content"},void 0,(0,o.Z)(be.Z,{id:"id_with_content",disabled:this.state.isLoading,labelOn:pgettext("profile delete content","Delete together with user's account"),labelOff:pgettext("profile delete content","Hide after deleting user's account"),onChange:this.bindInput("with_content"),value:this.state.with_content}))),(0,o.Z)("div",{className:"modal-footer"},void 0,(0,o.Z)("button",{type:"button",className:"btn btn-default","data-dismiss":"modal"},void 0,pgettext("profile delete btn","Cancel")),(0,o.Z)(j.Z,{className:"btn-danger",loading:this.state.isLoading,disabled:!this.state.confirm},void 0,this.getButtonLabel())))}getDeletedBody(){return(0,o.Z)("div",{className:"modal-body"},void 0,$||($=(0,o.Z)("div",{className:"message-icon"},void 0,(0,o.Z)("span",{className:"material-icon"},void 0,"info_outline"))),(0,o.Z)("div",{className:"message-body"},void 0,(0,o.Z)("p",{className:"lead"},void 0,this.state.isDeleted),(0,o.Z)("p",{},void 0,(0,o.Z)("a",{href:h.Z.get("USERS_LIST_URL")},void 0,pgettext("profile delete link","Return to users list")))))}getModalBody(){return this.state.error?(0,o.Z)(_e.Z,{icon:"remove_circle_outline",message:this.state.error}):this.state.isLoaded?this.state.isDeleted?this.getDeletedBody():this.getForm():W||(W=(0,o.Z)(fe.Z,{}))}getClassName(){return this.state.error||this.state.isDeleted?"modal-dialog modal-message modal-delete-account":"modal-dialog modal-delete-account"}render(){return(0,o.Z)("div",{className:this.getClassName(),role:"document"},void 0,(0,o.Z)("div",{className:"modal-content"},void 0,(0,o.Z)("div",{className:"modal-header"},void 0,(0,o.Z)("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":pgettext("modal","Close")},void 0,Q||(Q=(0,o.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,o.Z)("h4",{className:"modal-title"},void 0,pgettext("profile delete title","Delete user account"))),this.getModalBody()))}},ke=s(59801);let Ce=function(e){return{tick:e.tick,user:e.auth,profile:e.profile}};var Se,Ee,Te,Le,Pe,Oe=class extends l().Component{constructor(){super(...arguments),(0,n.Z)(this,"showAvatarDialog",(()=>{ke.Z.show((0,i.$j)(Ce)(Ne))})),(0,n.Z)(this,"showRenameDialog",(()=>{ke.Z.show((0,i.$j)(Ce)(ye))})),(0,n.Z)(this,"showDeleteDialog",(()=>{ke.Z.show((0,i.$j)(Ce)(we))}))}render(){const{moderation:e}=this.props;return(0,o.Z)("ul",{className:"dropdown-menu dropdown-menu-right",role:"menu"},void 0,!!e.avatar&&(0,o.Z)("li",{},void 0,(0,o.Z)("button",{type:"button",className:"btn btn-link",onClick:this.showAvatarDialog},void 0,X||(X=(0,o.Z)("span",{className:"material-icon"},void 0,"portrait")),pgettext("profile moderation menu","Avatar controls"))),!!e.rename&&(0,o.Z)("li",{},void 0,(0,o.Z)("button",{type:"button",className:"btn btn-link",onClick:this.showRenameDialog},void 0,K||(K=(0,o.Z)("span",{className:"material-icon"},void 0,"credit_card")),pgettext("profile moderation menu","Change username"))),!!e.delete&&(0,o.Z)("li",{},void 0,(0,o.Z)("button",{type:"button",className:"btn btn-link",onClick:this.showDeleteDialog},void 0,J||(J=(0,o.Z)("span",{className:"material-icon"},void 0,"clear")),pgettext("profile moderation menu","Delete account"))))}},Ie=s(24678),Ae=e=>{let{profile:t}=e;return(0,o.Z)("ul",{className:"profile-data-list"},void 0,!1===t.is_active&&(0,o.Z)("li",{className:"user-account-disabled"},void 0,(0,o.Z)("abbr",{title:pgettext("profile data list","This user's account has been disabled by administrator.")},void 0,pgettext("profile data list","Account disabled"))),(0,o.Z)("li",{className:"user-status-display"},void 0,(0,o.Z)(Ie.ZP,{user:t,status:t.status},void 0,(0,o.Z)(Ie.Jj,{user:t,status:t.status}),(0,o.Z)(Ie.pg,{user:t,status:t.status,className:"status-label"}))),t.rank.is_tab?(0,o.Z)("li",{className:"user-rank"},void 0,(0,o.Z)("a",{href:t.rank.url,className:"item-title"},void 0,t.rank.name)):(0,o.Z)("li",{className:"user-rank"},void 0,(0,o.Z)("span",{className:"item-title"},void 0,t.rank.name)),(t.title||t.rank.title)&&(0,o.Z)("li",{className:"user-title"},void 0,t.title||t.rank.title),(0,o.Z)("li",{className:"user-joined-on"},void 0,(0,o.Z)("abbr",{title:interpolate(pgettext("profile data list","Joined on %(joined_on)s"),{joined_on:t.joined_on.format("LL, LT")},!0)},void 0,interpolate(pgettext("profile data list","Joined %(joined_on)s"),{joined_on:t.joined_on.fromNow()},!0))),t.email&&(0,o.Z)("li",{className:"user-email"},void 0,(0,o.Z)("a",{href:"mailto:"+t.email,className:"item-title"},void 0,t.email)))};const Re=()=>(0,o.Z)("button",{className:"btn btn-default btn-icon btn-outline dropdown-toggle",type:"button",title:pgettext("profile options btn","Options"),"data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false"},void 0,Pe||(Pe=(0,o.Z)("span",{className:"material-icon"},void 0,"settings")));var De=e=>{let{profile:t,user:s,moderation:a,message:i,follow:n}=e;return(0,o.Z)(ue.sP,{},void 0,(0,o.Z)(ue.mr,{styleName:t.rank.css_class?"rank-"+t.rank.css_class:"profile"},void 0,(0,o.Z)(ue.gC,{styleName:t.rank.css_class?"rank-"+t.rank.css_class:"profile"},void 0,(0,o.Z)("div",{className:"profile-page-header"},void 0,(0,o.Z)("div",{className:"profile-page-header-avatar"},void 0,(0,o.Z)(ce.ZP,{className:"user-avatar hidden-sm hidden-md hidden-lg",user:t,size:200,size2x:400}),(0,o.Z)(ce.ZP,{className:"user-avatar hidden-xs hidden-md hidden-lg",user:t,size:64,size2x:128}),(0,o.Z)(ce.ZP,{className:"user-avatar hidden-xs hidden-sm",user:t,size:128,size2x:256})),(0,o.Z)("h1",{},void 0,t.username))),(0,o.Z)(ue.eA,{className:"profile-page-header-details"},void 0,(0,o.Z)(pe.gq,{},void 0,(0,o.Z)(pe.kw,{auto:!0},void 0,(0,o.Z)(pe.Z6,{},void 0,(0,o.Z)(Ae,{profile:t}))),i&&(0,o.Z)(pe.kw,{},void 0,(0,o.Z)(pe.Z6,{},void 0,(0,o.Z)(ve,{className:"btn btn-default btn-block btn-outline",profile:t,user:s})),a.available&&!n&&(0,o.Z)(pe.Z6,{shrink:!0},void 0,(0,o.Z)("div",{className:"dropdown"},void 0,Se||(Se=(0,o.Z)(Re,{})),(0,o.Z)(Oe,{profile:t,moderation:a})))),n&&(0,o.Z)(pe.kw,{},void 0,(0,o.Z)(pe.Z6,{},void 0,(0,o.Z)(he,{className:"btn btn-block btn-outline",profile:t})),a.available&&(0,o.Z)(pe.Z6,{shrink:!0},void 0,(0,o.Z)("div",{className:"dropdown"},void 0,Ee||(Ee=(0,o.Z)(Re,{})),(0,o.Z)(Oe,{profile:t,moderation:a})))),a.available&&!n&&!i&&(0,o.Z)(pe.kw,{},void 0,(0,o.Z)(pe.Z6,{className:"hidden-xs",shrink:!0},void 0,(0,o.Z)("div",{className:"dropdown"},void 0,Te||(Te=(0,o.Z)(Re,{})),(0,o.Z)(Oe,{profile:t,moderation:a}))),(0,o.Z)(pe.Z6,{className:"hidden-sm hidden-md hidden-lg"},void 0,(0,o.Z)("div",{className:"dropdown"},void 0,(0,o.Z)("button",{className:"btn btn-default btn-block btn-outline dropdown-toggle",type:"button","data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false"},void 0,Le||(Le=(0,o.Z)("span",{className:"material-icon"},void 0,"settings")),pgettext("profile options btn","Options")),(0,o.Z)(Oe,{profile:t,moderation:a}))))))))},je=s(69987),Ue=s(94417),ze=e=>{let{baseUrl:t,page:s,pages:a}=e;return(0,o.Z)("div",{className:"nav-container"},void 0,(0,o.Z)("div",{className:"dropdown hidden-sm hidden-md hidden-lg"},void 0,(0,o.Z)("button",{className:"btn btn-default btn-block btn-outline dropdown-toggle",type:"button","data-toggle":"dropdown","aria-haspopup":"true","aria-expanded":"false"},void 0,(0,o.Z)("span",{className:"material-icon"},void 0,s.icon),s.name),(0,o.Z)("ul",{className:"dropdown-menu stick-to-bottom"},void 0,a.map((e=>(0,o.Z)("li",{},e.component,(0,o.Z)(je.rU,{to:t+e.component+"/"},void 0,(0,o.Z)("span",{className:"material-icon"},void 0,e.icon),e.name)))))),(0,o.Z)("ul",{className:"nav nav-pills hidden-xs",role:"menu"},void 0,a.map((e=>(0,o.Z)(Ue.Z,{path:t+e.component+"/"},e.component,(0,o.Z)(je.rU,{to:t+e.component+"/"},void 0,(0,o.Z)("span",{className:"material-icon"},void 0,e.icon),e.name))))))},Me=class extends re.Z{constructor(e){super(e),(0,n.Z)(this,"update",(e=>{z.Z.dispatch((0,le.ZB)(e))})),this.startPolling(e.profile.api.index)}startPolling(e){m.Z.start({poll:"user-profile",url:e,frequency:9e4,update:this.update})}render(){const e=h.Z.get("PROFILE").url,t=h.Z.get("PROFILE_PAGES"),s=t.filter((t=>{const s=e+t.component+"/";return this.props.location.pathname===s}))[0],{profile:a,user:i}=this.props,n=Be(a,i),r=!!i.acl.can_start_private_threads&&a.id!==i.id,l=!!a.acl.can_follow&&a.id!==i.id;return(0,o.Z)("div",{className:"page page-user-profile"},void 0,(0,o.Z)(De,{profile:this.props.profile,user:this.props.user,moderation:n,message:r,follow:l}),(0,o.Z)(de.Z,{},void 0,(0,o.Z)(ze,{baseUrl:e,page:s,pages:t}),this.props.children))}};const Be=(e,t)=>{const s={available:!1,rename:!1,avatar:!1,delete:!1};return t.is_anonymous||(s.rename=e.acl.can_rename,s.avatar=e.acl.can_moderate_avatar,s.delete=e.acl.can_delete,s.available=!!(s.rename||s.avatar||s.delete)),s};function qe(e){return{isAuthenticated:e.auth.user.id===e.profile.id,tick:e.tick.tick,user:e.auth.user,users:e.users,posts:e.posts,profile:e.profile,profileDetails:e["profile-details"],"username-history":e["username-history"]}}const Fe={posts:function(e){let t=null;t=e.user.id===e.profile.id?pgettext("profile posts","You have posted no messages."):interpolate(pgettext("profile posts","%(username)s posted no messages."),{username:e.profile.username},!0);let s=null;if(e.posts.isLoaded)if(e.profile.id===e.user.id){const t=npgettext("profile posts","You have posted %(posts)s message.","You have posted %(posts)s messages.",e.profile.posts);s=interpolate(t,{posts:e.profile.posts},!0)}else{const t=npgettext("profile posts","%(username)s has posted %(posts)s message.","%(username)s has posted %(posts)s messages.",e.profile.posts);s=interpolate(t,{username:e.profile.username,posts:e.profile.posts},!0)}else s=pgettext("profile posts","Loading...");return l().createElement(M,(0,R.Z)({api:e.profile.api.posts,emptyMessage:t,header:s,title:pgettext("profile posts title","Posts")},e))},threads:function(e){let t=null;t=e.user.id===e.profile.id?pgettext("profile threads","You have no started threads."):interpolate(pgettext("profile threads","%(username)s started no threads."),{username:e.profile.username},!0);let s=null;if(e.posts.isLoaded)if(e.profile.id===e.user.id){const t=npgettext("profile threads","You have started %(threads)s thread.","You have started %(threads)s threads.",e.profile.threads);s=interpolate(t,{threads:e.profile.threads},!0)}else{const t=npgettext("profile threads","%(username)s has started %(threads)s thread.","%(username)s has started %(threads)s threads.",e.profile.threads);s=interpolate(t,{username:e.profile.username,threads:e.profile.threads},!0)}else s=pgettext("profile threads","Loading...");return l().createElement(M,(0,R.Z)({api:e.profile.api.threads,emptyMessage:t,header:s,title:pgettext("profile threads title","Threads")},e))},followers:ae,follows:class extends ae{setSpecialProps(){this.PRELOADED_DATA_KEY="PROFILE_FOLLOWS",this.TITLE=pgettext("profile follows title","Follows"),this.API_FILTER="follows"}getLabel(){if(this.state.isLoaded){if(this.state.search){let e=npgettext("profile follows","Found %(users)s user.","Found %(users)s users.",this.state.count);return interpolate(e,{users:this.state.count},!0)}if(this.props.profile.id===this.props.user.id){let e=npgettext("profile follows","You are following %(users)s user.","You are following %(users)s users.",this.state.count);return interpolate(e,{users:this.state.count},!0)}{let e=npgettext("profile follows","%(username)s is following %(users)s user.","%(username)s is following %(users)s users.",this.state.count);return interpolate(e,{username:this.props.profile.username,users:this.state.count},!0)}}return pgettext("profile follows","Loading...")}getEmptyMessage(){return this.state.search?pgettext("profile follows","Search returned no users matching specified criteria."):this.props.user.id===this.props.profile.id?pgettext("profile follows","You are not following any users."):interpolate(pgettext("profile follows","%(username)s is not following any users."),{username:this.props.profile.username},!0)}},details:A,"username-history":ne,"ban-details":g};function He(){let e=[];return h.Z.get("PROFILE_PAGES").forEach((function(t){e.push(Object.assign({},t,{path:h.Z.get("PROFILE").url+t.component+"/",component:(0,i.$j)(qe)(Fe[t.component])}))})),e}var Ye=s(39633);h.Z.addInitializer({name:"component:profile",initializer:function(e){e.has("PROFILE")&&e.has("PROFILE_PAGES")&&(0,Ye.Z)({root:h.Z.get("PROFILE").url,component:(0,i.$j)(qe)(Me),paths:He()})},after:"reducer:profile-hydrate"})},32488:function(e,t,s){"use strict";var a,i=s(32233),o=s(4942),n=s(22928),r=s(57588),l=s.n(r),d=s(82211),c=s(43345),p=s(78657),u=s(53904),h=s(55210),m=s(93051);class v extends c.Z{constructor(e){super(e),this.state={isLoading:!1,email:"",validators:{email:[h.Do()]}}}clean(){return!!this.isValid()||(u.Z.error(pgettext("request activation link form","Enter a valid email address.")),!1)}send(){return p.Z.post(i.Z.get("SEND_ACTIVATION_API"),{email:this.state.email})}handleSuccess(e){this.props.callback(e)}handleError(e){["already_active","inactive_admin"].indexOf(e.code)>-1?u.Z.info(e.detail):403===e.status&&e.ban?(0,m.Z)(e.ban):u.Z.apiError(e)}render(){return(0,n.Z)("div",{className:"well well-form well-form-request-activation-link"},void 0,(0,n.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,n.Z)("div",{className:"form-group"},void 0,(0,n.Z)("div",{className:"control-input"},void 0,(0,n.Z)("input",{type:"text",className:"form-control",placeholder:pgettext("request activation link form field","Your e-mail address"),disabled:this.state.isLoading,onChange:this.bindInput("email"),value:this.state.email}))),(0,n.Z)(d.Z,{className:"btn-primary btn-block",loading:this.state.isLoading},void 0,pgettext("request activation link form btn","Send link"))))}}class g extends l().Component{getMessage(){return interpolate(pgettext("request activation link form","Activation link was sent to %(email)s"),{email:this.props.user.email},!0)}render(){return(0,n.Z)("div",{className:"well well-form well-form-request-activation-link well-done"},void 0,(0,n.Z)("div",{className:"done-message"},void 0,a||(a=(0,n.Z)("div",{className:"message-icon"},void 0,(0,n.Z)("span",{className:"material-icon"},void 0,"check"))),(0,n.Z)("div",{className:"message-body"},void 0,(0,n.Z)("p",{},void 0,this.getMessage())),(0,n.Z)("button",{className:"btn btn-primary btn-block",type:"button",onClick:this.props.callback},void 0,pgettext("request activation link form btn","Request another link"))))}}var Z=class extends l().Component{constructor(e){super(e),(0,o.Z)(this,"complete",(e=>{this.setState({complete:e})})),(0,o.Z)(this,"reset",(()=>{this.setState({complete:!1})})),this.state={complete:!1}}render(){return this.state.complete?(0,n.Z)(g,{user:this.state.complete,callback:this.reset}):(0,n.Z)(v,{callback:this.complete})}},f=s(4869);i.Z.addInitializer({name:"component:request-activation-link",initializer:function(){document.getElementById("request-activation-link-mount")&&(0,f.Z)(Z,"request-activation-link-mount",!1)},after:"store"})},11768:function(e,t,s){"use strict";var a,i,o=s(32233),n=s(4942),r=s(22928),l=s(57588),d=s.n(l),c=s(73935),p=s.n(c),u=s(82211),h=s(43345),m=s(78657),v=s(53904),g=s(55210),Z=s(93051);class f extends h.Z{constructor(e){super(e),this.state={isLoading:!1,email:"",validators:{email:[g.Do()]}}}clean(){return!!this.isValid()||(v.Z.error(pgettext("request password reset form","Enter a valid email address.")),!1)}send(){return m.Z.post(o.Z.get("SEND_PASSWORD_RESET_API"),{email:this.state.email})}handleSuccess(e){this.props.callback(e)}handleError(e){["inactive_user","inactive_admin"].indexOf(e.code)>-1?this.props.showInactivePage(e):403===e.status&&e.ban?(0,Z.Z)(e.ban):v.Z.apiError(e)}render(){return(0,r.Z)("div",{className:"well well-form well-form-request-password-reset"},void 0,(0,r.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,r.Z)("div",{className:"form-group"},void 0,(0,r.Z)("div",{className:"control-input"},void 0,(0,r.Z)("input",{type:"text",className:"form-control",placeholder:pgettext("request password reset form field","Your e-mail address"),disabled:this.state.isLoading,onChange:this.bindInput("email"),value:this.state.email}))),(0,r.Z)(u.Z,{className:"btn-primary btn-block",loading:this.state.isLoading},void 0,pgettext("request password reset form btn","Send link"))))}}class b extends d().Component{getMessage(){return interpolate(pgettext("request password reset form","Reset password link was sent to %(email)s"),{email:this.props.user.email},!0)}render(){return(0,r.Z)("div",{className:"well well-form well-form-request-password-reset well-done"},void 0,(0,r.Z)("div",{className:"done-message"},void 0,a||(a=(0,r.Z)("div",{className:"message-icon"},void 0,(0,r.Z)("span",{className:"material-icon"},void 0,"check"))),(0,r.Z)("div",{className:"message-body"},void 0,(0,r.Z)("p",{},void 0,this.getMessage())),(0,r.Z)("button",{type:"button",className:"btn btn-primary btn-block",onClick:this.props.callback},void 0,pgettext("request password reset form btn","Request another link"))))}}class _ extends d().Component{getActivateButton(){return"inactive_user"===this.props.activation?(0,r.Z)("p",{},void 0,(0,r.Z)("a",{href:o.Z.get("REQUEST_ACTIVATION_URL")},void 0,pgettext("request password reset form error","Activate your account."))):null}render(){return(0,r.Z)("div",{className:"page page-message page-message-info page-forgotten-password-inactive"},void 0,(0,r.Z)("div",{className:"container"},void 0,(0,r.Z)("div",{className:"message-panel"},void 0,i||(i=(0,r.Z)("div",{className:"message-icon"},void 0,(0,r.Z)("span",{className:"material-icon"},void 0,"info_outline"))),(0,r.Z)("div",{className:"message-body"},void 0,(0,r.Z)("p",{className:"lead"},void 0,pgettext("request password reset form error","Your account is inactive.")),(0,r.Z)("p",{},void 0,this.props.message),this.getActivateButton()))))}}var N=class extends d().Component{constructor(e){super(e),(0,n.Z)(this,"complete",(e=>{this.setState({complete:e})})),(0,n.Z)(this,"reset",(()=>{this.setState({complete:!1})})),this.state={complete:!1}}showInactivePage(e){p().render((0,r.Z)(_,{activation:e.code,message:e.detail}),document.getElementById("page-mount"))}render(){return this.state.complete?(0,r.Z)(b,{callback:this.reset,user:this.state.complete}):(0,r.Z)(f,{callback:this.complete,showInactivePage:this.showInactivePage})}},x=s(4869);o.Z.addInitializer({name:"component:request-password-reset",initializer:function(){document.getElementById("request-password-reset-mount")&&(0,x.Z)(N,"request-password-reset-mount",!1)},after:"store"})},61323:function(e,t,s){"use strict";var a,i=s(32233),o=s(4942),n=s(22928),r=s(57588),l=s.n(r),d=s(73935),c=s.n(d),p=s(82211),u=s(43345),h=s(14467),m=s(78657),v=s(98274),g=s(59801),Z=s(53904),f=s(93051),b=s(19755);class _ extends u.Z{constructor(e){super(e),this.state={isLoading:!1,password:""}}clean(){return!!this.state.password.trim().length||(Z.Z.error(pgettext("password reset form","Enter new password.")),!1)}send(){return m.Z.post(i.Z.get("CHANGE_PASSWORD_API"),{password:this.state.password})}handleSuccess(e){this.props.callback(e)}handleError(e){403===e.status&&e.ban?(0,f.Z)(e.ban):Z.Z.apiError(e)}render(){return(0,n.Z)("div",{className:"well well-form well-form-reset-password"},void 0,(0,n.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,n.Z)("div",{className:"form-group"},void 0,(0,n.Z)("div",{className:"control-input"},void 0,(0,n.Z)("input",{type:"password",className:"form-control",placeholder:pgettext("password reset form field","Enter new password"),disabled:this.state.isLoading,onChange:this.bindInput("password"),value:this.state.password}))),(0,n.Z)(p.Z,{className:"btn-primary btn-block",loading:this.state.isLoading},void 0,pgettext("password reset form btn","Change password"))))}}class N extends l().Component{getMessage(){return interpolate(pgettext("password reset form","%(username)s, your password has been changed successfully."),{username:this.props.user.username},!0)}showSignIn(){g.Z.show(h.Z)}render(){return(0,n.Z)("div",{className:"page page-message page-message-success page-forgotten-password-changed"},void 0,(0,n.Z)("div",{className:"container"},void 0,(0,n.Z)("div",{className:"message-panel"},void 0,a||(a=(0,n.Z)("div",{className:"message-icon"},void 0,(0,n.Z)("span",{className:"material-icon"},void 0,"check"))),(0,n.Z)("div",{className:"message-body"},void 0,(0,n.Z)("p",{className:"lead"},void 0,this.getMessage()),(0,n.Z)("p",{},void 0,pgettext("password reset form","You will have to sign in using new password before continuing.")),(0,n.Z)("p",{},void 0,(0,n.Z)("button",{type:"button",className:"btn btn-primary",onClick:this.showSignIn},void 0,pgettext("password reset form btn","Sign in")))))))}}var x=class extends l().Component{constructor(){super(...arguments),(0,o.Z)(this,"complete",(e=>{v.Z.softSignOut(),b('#hidden-login-form input[name="redirect_to"]').remove(),c().render((0,n.Z)(N,{user:e}),document.getElementById("page-mount"))}))}render(){return(0,n.Z)(_,{callback:this.complete})}},y=s(4869);i.Z.addInitializer({name:"component:reset-password-form",initializer:function(){document.getElementById("reset-password-form-mount")&&(0,y.Z)(x,"reset-password-form-mount",!1)},after:"store"})},64752:function(e,t,s){"use strict";var a,i=s(22928),o=(s(57588),s(73935)),n=s.n(o),r=s(37424),l=s(62989),d=s(90287);misago.addInitializer({name:"component:search-overlay",initializer:function(e){const t=document.getElementById("search-mount");n().render((0,i.Z)(r.zt,{store:d.Z.getStore()},void 0,a||(a=(0,i.Z)(l.F,{}))),t)},after:"store"})},40949:function(e,t,s){"use strict";var a,i=s(37424),o=s(22928),n=s(87462),r=s(57588),l=s.n(r),d=s(59131),c=s(4942),p=s(32233),u=s(43345),h=s(21981),m=s(16427),v=s(6935),g=s(78657),Z=s(53904),f=s(90287),b=s(98936),_=s(99755),N=class extends u.Z{constructor(e){super(e),(0,c.Z)(this,"onQueryChange",(e=>{this.changeValue("query",e.target.value)})),this.state={isLoading:!1,query:e.search.query}}componentDidMount(){this.state.query.length&&this.handleSubmit()}clean(){return!!this.state.query.trim().length||(Z.Z.error(pgettext("search form","You have to enter search query.")),!1)}send(){f.Z.dispatch((0,m.Vx)({isLoading:!0}));const e=this.state.query.trim();let t=window.location.href;const s=t.indexOf("?q=");return s>0&&(t=t.substring(0,s+3)),window.history.pushState({},"",t+encodeURIComponent(e)),g.Z.get(p.Z.get("SEARCH_API"),{q:e})}handleSuccess(e){f.Z.dispatch((0,m.Vx)({query:this.state.query.trim(),isLoading:!1,providers:e})),e.forEach((e=>{"users"===e.id?f.Z.dispatch((0,v.ZB)(e.results.results)):"threads"===e.id&&f.Z.dispatch((0,h.zD)(e.results))}))}handleError(e){Z.Z.apiError(e),f.Z.dispatch((0,m.Vx)({isLoading:!1}))}render(){return(0,o.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,o.Z)(_.sP,{},void 0,(0,o.Z)(_.mr,{styleName:"site-search"},void 0,(0,o.Z)(_.gC,{styleName:"site-search"},void 0,(0,o.Z)("h1",{},void 0,pgettext("search form title","Search"))),(0,o.Z)(_.eA,{className:"page-header-search-form"},void 0,(0,o.Z)(b.gq,{},void 0,(0,o.Z)(b.kw,{auto:!0},void 0,(0,o.Z)(b.Z6,{},void 0,(0,o.Z)("input",{className:"form-control",disabled:this.state.isLoading,type:"text",value:this.state.query,placeholder:pgettext("search form input","Search"),onChange:this.onQueryChange})),(0,o.Z)(b.Z6,{shrink:!0},void 0,(0,o.Z)("button",{className:"btn btn-secondary btn-icon btn-outline",title:pgettext("search form btn","Search"),disabled:this.state.isLoading},void 0,a||(a=(0,o.Z)("span",{className:"material-icon"},void 0,"search"))))))))))}},x=s(69987);function y(e){return(0,o.Z)("div",{className:"list-group nav-side"},void 0,e.providers.map((e=>(0,o.Z)(x.rU,{activeClassName:"active",className:"list-group-item",to:e.url},e.id,(0,o.Z)("span",{className:"material-icon"},void 0,e.icon),e.name,(0,o.Z)(w,{results:e.results})))))}function w(e){if(!e.results)return null;let t=e.results.count;return t>1e6?t=Math.ceil(t/1e6)+"KK":t>1e3&&(t=Math.ceil(t/1e3)+"K"),(0,o.Z)("span",{className:"badge"},void 0,t)}function k(e){return(0,o.Z)("div",{className:"page page-search"},void 0,(0,o.Z)(N,{provider:e.provider,search:e.search}),(0,o.Z)(d.Z,{},void 0,(0,o.Z)("div",{className:"row"},void 0,(0,o.Z)("div",{className:"col-md-3"},void 0,(0,o.Z)(y,{providers:e.search.providers})),(0,o.Z)("div",{className:"col-md-9"},void 0,e.children,(0,o.Z)(C,{provider:e.provider,search:e.search})))))}function C(e){let t=null;if(e.search.providers.forEach((s=>{s.id===e.provider.id&&(t=s.time)})),null===t)return null;const s=pgettext("search time","Search took %(time)s s to complete");return(0,o.Z)("footer",{className:"search-footer"},void 0,(0,o.Z)("p",{},void 0,interpolate(s,{time:t},!0)))}var S=s(11005),E=s(82211);function T(e){return(0,o.Z)("div",{},void 0,(0,o.Z)(S.Z,{isReady:!0,posts:e.results}),l().createElement(L,e))}s(69092);class L extends l().Component{constructor(){super(...arguments),(0,c.Z)(this,"onClick",(()=>{f.Z.dispatch((0,h.Vx)({isBusy:!0})),g.Z.get(this.props.provider.api,{q:this.props.query,page:this.props.next}).then((e=>{e.forEach((e=>{"threads"===e.id&&(f.Z.dispatch((0,h.R3)(e.results)),f.Z.dispatch((0,m.P0)(e)))})),f.Z.dispatch((0,h.Vx)({isBusy:!1}))}),(e=>{Z.Z.apiError(e),f.Z.dispatch((0,h.Vx)({isBusy:!1}))}))}))}render(){return this.props.more?(0,o.Z)("div",{className:"pager-more"},void 0,(0,o.Z)(E.Z,{className:"btn btn-default btn-outline",loading:this.props.isBusy,onClick:this.onClick},void 0,pgettext("search threads btn","Show more"))):null}}function P(e){let{children:t,loading:s,posts:a,query:i}=e;return a&&a.count?t:i.length?(0,o.Z)("p",{className:"lead"},void 0,s?pgettext("search threads","Loading results..."):pgettext("search threads","No threads matching search query have been found.")):(0,o.Z)("p",{className:"lead"},void 0,pgettext("search threads","Enter at least two characters to search threads."))}var O=s(40429);function I(e){let{children:t,loading:s,query:a,users:i}=e;return i.length?t:a.length?(0,o.Z)("p",{className:"lead"},void 0,s?pgettext("search users","Loading results..."):pgettext("search users","No users matching search query have been found.")):(0,o.Z)("p",{className:"lead"},void 0,pgettext("search users","Enter at least two characters to search users."))}const A={threads:function(e){return(0,o.Z)(k,{provider:e.route.provider,search:e.search},void 0,(0,o.Z)(P,{loading:e.search.isLoading,query:e.search.query,posts:e.posts},void 0,l().createElement(T,(0,n.Z)({provider:e.route.provider,query:e.search.query},e.posts))))},users:function(e){return(0,o.Z)(k,{provider:e.route.provider,search:e.search},void 0,(0,o.Z)(I,{loading:e.search.isLoading,query:e.search.query,users:e.users},void 0,(0,o.Z)(O.Z,{cols:3,isReady:!e.search.isLoading,users:e.users})))}};function R(e){return{posts:e.posts,search:e.search,tick:e.tick.tick,user:e.auth.user,users:e.users}}var D=s(39633);p.Z.addInitializer({name:"component:search",initializer:function(e){var t;"misago:search"===e.get("CURRENT_LINK")&&(0,D.Z)({paths:(t=p.Z.get("SEARCH_PROVIDERS"),t.map((e=>({path:e.url,component:(0,i.$j)(R)(A[e.id]),provider:e}))))})},after:"store"})},78679:function(e,t,s){"use strict";var a,i=s(22928),o=(s(57588),s(73935)),n=s.n(o),r=s(37424),l=s(6333),d=s(90287);misago.addInitializer({name:"component:site-nav-overlay",initializer:function(e){const t=document.getElementById("site-nav-mount");n().render((0,i.Z)(r.zt,{store:d.Z.getStore()},void 0,a||(a=(0,i.Z)(l.Or,{}))),t)},after:"store"})},61814:function(e,t,s){"use strict";var a=s(37424),i=s(32233),o=s(22928),n=s(57588),r=s.n(n);const l={info:"alert-info",success:"alert-success",warning:"alert-warning",error:"alert-danger"};class d extends r().Component{getSnackbarClass(){let e="alerts-snackbar";return this.props.isVisible?e+=" in":e+=" out",e}render(){return(0,o.Z)("div",{className:this.getSnackbarClass()},void 0,(0,o.Z)("p",{className:"alert "+l[this.props.type]},void 0,this.props.message))}}function c(e){return e.snackbar}var p=s(4869);i.Z.addInitializer({name:"component:snackbar",initializer:function(){(0,p.Z)((0,a.$j)(c)(d),"snackbar-mount")},after:"snackbar"})},95920:function(e,t,s){"use strict";var a=s(57588),i=s.n(a),o=s(22928),n=s(4942),r=s(32233),l=s(26106),d=s(82211),c=s(43345),p=s(96359),u=s(78657),h=s(53904),m=s(55210),v=s(59131),g=s(99755),Z=e=>{let{backendName:t}=e;const s=pgettext("social auth title","Sign in with %(backend)s"),a=interpolate(s,{backend:t},!0);return(0,o.Z)(g.sP,{},void 0,(0,o.Z)(g.mr,{styleName:"social-auth"},void 0,(0,o.Z)(g.gC,{styleName:"social-auth"},void 0,(0,o.Z)("h1",{},void 0,a))))};class f extends c.Z{constructor(e){super(e),(0,n.Z)(this,"handlePrivacyPolicyChange",(e=>{const t=e.target.value;this.handleToggleAgreement("privacyPolicy",t)})),(0,n.Z)(this,"handleTermsOfServiceChange",(e=>{const t=e.target.value;this.handleToggleAgreement("termsOfService",t)})),(0,n.Z)(this,"handleToggleAgreement",((e,t)=>{this.setState(((s,a)=>{if(null===s[e])return{errors:{...s.errors,[e]:null},[e]:t};const i=this.state.validators[e][0];return{errors:{...s.errors,[e]:[i(null)]},[e]:null}}))}));const t={email:[m.Do()],username:[m.lG()]};r.Z.get("TERMS_OF_SERVICE_ID")&&(t.termsOfService=[m.fT()]),r.Z.get("PRIVACY_POLICY_ID")&&(t.privacyPolicy=[m.jA()]),this.state={email:e.email||"",emailProtected:!!e.email,username:e.username||"",termsOfService:null,privacyPolicy:null,validators:t,errors:{},isLoading:!1}}clean(){if(this.validate(),-1!==[this.state.email.trim().length,this.state.username.trim().length].indexOf(0))return h.Z.error(pgettext("social auth form","Fill out all fields.")),!1;const{validators:e}=this.state;return r.Z.get("TERMS_OF_SERVICE_ID")&&null===this.state.termsOfService?(h.Z.error(e.termsOfService[0](null)),!1):!r.Z.get("PRIVACY_POLICY_ID")||null!==this.state.privacyPolicy||(h.Z.error(e.privacyPolicy[0](null)),!1)}send(){return u.Z.post(this.props.url,{email:this.state.email,username:this.state.username,terms_of_service:this.state.termsOfService,privacy_policy:this.state.privacyPolicy})}handleSuccess(e){const{onRegistrationComplete:t}=this.props;t(e)}handleError(e){if(200===e.status){const{onRegistrationComplete:e}=this.props,{username:t}=this.state;e({activation:"active",step:"done",username:t})}else if(400===e.status){const t={errors:e};e.email&&(t.emailProtected=!1),this.setState(t)}else h.Z.apiError(e)}render(){const{backend_name:e}=this.props,{email:t,emailProtected:s,username:a,isLoading:i}=this.state;let n=null;if(s){const t=pgettext("social auth form","Your e-mail address has been verified by %(backend)s.");n=interpolate(t,{backend:e},!0)}return(0,o.Z)("div",{className:"page page-social-auth page-social-auth-register"},void 0,(0,o.Z)(Z,{backendName:e}),(0,o.Z)(v.Z,{},void 0,(0,o.Z)("div",{className:"row"},void 0,(0,o.Z)("div",{className:"col-md-6 col-md-offset-3"},void 0,(0,o.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,o.Z)("div",{className:"panel panel-default panel-form"},void 0,(0,o.Z)("div",{className:"panel-heading"},void 0,(0,o.Z)("h3",{className:"panel-title"},void 0,pgettext("social auth form title","Complete your details"))),(0,o.Z)("div",{className:"panel-body"},void 0,(0,o.Z)(p.Z,{for:"id_username",label:pgettext("social auth form field","Username"),validation:this.state.errors.username},void 0,(0,o.Z)("input",{type:"text",id:"id_username",className:"form-control",disabled:i,onChange:this.bindInput("username"),value:a})),(0,o.Z)(p.Z,{for:"id_email",label:pgettext("social auth form field","E-mail address"),helpText:n,validation:s?null:this.state.errors.email},void 0,(0,o.Z)("input",{type:"email",id:"id_email",className:"form-control",disabled:i||s,onChange:this.bindInput("email"),value:t})),(0,o.Z)(l.Z,{errors:this.state.errors,privacyPolicy:this.state.privacyPolicy,termsOfService:this.state.termsOfService,onPrivacyPolicyChange:this.handlePrivacyPolicyChange,onTermsOfServiceChange:this.handleTermsOfServiceChange})),(0,o.Z)("div",{className:"panel-footer"},void 0,(0,o.Z)(d.Z,{className:"btn-primary",loading:this.state.isLoading},void 0,pgettext("social auth form btn","Sign in")))))))))}}var b=e=>{let{activation:t,backend_name:s,username:a}=e,i="",n="";return n="user"===t?pgettext("social auth complete","%(username)s, your account has been created but you need to activate it before you will be able to sign in."):"admin"===t?pgettext("social auth complete","%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in."):pgettext("social auth complete","%(username)s, your account has been created and you have been signed in to it."),i="active"===t?"check":"info_outline",(0,o.Z)("div",{className:"page page-social-auth page-social-auth-register"},void 0,(0,o.Z)(Z,{backendName:s}),(0,o.Z)(v.Z,{},void 0,(0,o.Z)("div",{className:"row"},void 0,(0,o.Z)("div",{className:"col-md-6 col-md-offset-3"},void 0,(0,o.Z)("div",{className:"panel panel-default panel-form"},void 0,(0,o.Z)("div",{className:"panel-heading"},void 0,(0,o.Z)("h3",{className:"panel-title"},void 0,pgettext("social auth complete title","Registration completed!"))),(0,o.Z)("div",{className:"panel-body panel-message-body"},void 0,(0,o.Z)("div",{className:"message-icon"},void 0,(0,o.Z)("span",{className:"material-icon"},void 0,i)),(0,o.Z)("div",{className:"message-body"},void 0,(0,o.Z)("p",{className:"lead"},void 0,interpolate(n,{username:a},!0)),(0,o.Z)("p",{className:"help-block"},void 0,(0,o.Z)("a",{className:"btn btn-default",href:r.Z.get("MISAGO_PATH")},void 0,pgettext("social auth complete link","Return to forum index"))))))))))};class _ extends i().Component{constructor(e){super(e),(0,n.Z)(this,"handleRegistrationComplete",(e=>{let{activation:t,email:s,step:a,username:i}=e;this.setState({activation:t,email:s,step:a,username:i})})),this.state={step:e.step,activation:e.activation||"",email:e.email||"",username:e.username||""}}render(){const{backend_name:e,url:t}=this.props,{activation:s,email:a,step:i,username:n}=this.state;return"register"===i?(0,o.Z)(f,{backend_name:e,email:a,url:t,username:n,onRegistrationComplete:this.handleRegistrationComplete}):(0,o.Z)(b,{activation:s,backend_name:e,email:a,url:t,username:n})}}var N=s(4869);r.Z.addInitializer({name:"component:social-auth",initializer:function(e){if("misago:social-complete"===e.get("CURRENT_LINK")){const t=e.get("SOCIAL_AUTH_FORM");(0,N.Z)(i().createElement(_,t),"page-mount")}},after:"store"})},84333:function(e,t,s){"use strict";var a,i,o,n=s(37424),r=s(22928),l=s(4942),d=s(57588),c=s.n(d),p=s(87462),u=s(43345),h=s(96359),m=s(8154),v=s(7738),g=s(78657),Z=s(59801),f=s(53904),b=s(90287),_=class extends u.Z{constructor(e){super(e),(0,l.Z)(this,"onUsernameChange",(e=>{this.changeValue("username",e.target.value)})),this.state={isLoading:!1,username:""}}clean(){return!!this.state.username.trim().length||(f.Z.error(pgettext("add private thread participant","You have to enter user name.")),!1)}send(){return g.Z.patch(this.props.thread.api.index,[{op:"add",path:"participants",value:this.state.username},{op:"add",path:"acl",value:1}])}handleSuccess(e){b.Z.dispatch((0,v.y8)(e)),b.Z.dispatch(m.gx(e.participants)),f.Z.success(pgettext("add private thread participant","New participant has been added to thread.")),Z.Z.hide()}render(){return(0,r.Z)("div",{className:"modal-dialog modal-sm",role:"document"},void 0,(0,r.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,r.Z)("div",{className:"modal-content"},void 0,a||(a=(0,r.Z)(N,{})),(0,r.Z)("div",{className:"modal-body"},void 0,(0,r.Z)(h.Z,{for:"id_username",label:pgettext("add private thread participant field","User to add")},void 0,(0,r.Z)("input",{id:"id_username",className:"form-control",disabled:this.state.isLoading,onChange:this.onUsernameChange,type:"text",value:this.state.username}))),(0,r.Z)("div",{className:"modal-footer"},void 0,(0,r.Z)("button",{className:"btn btn-block btn-primary",disabled:this.state.isLoading},void 0,pgettext("add private thread participant btn","Add participant")),(0,r.Z)("button",{className:"btn btn-block btn-default","data-dismiss":"modal",disabled:this.state.isLoading,type:"button"},void 0,pgettext("add private thread participant btn","Cancel"))))))}};function N(e){return(0,r.Z)("div",{className:"modal-header"},void 0,(0,r.Z)("button",{"aria-label":pgettext("modal","Close"),className:"close","data-dismiss":"modal",type:"button"},void 0,i||(i=(0,r.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,r.Z)("h4",{className:"modal-title"},void 0,pgettext("add private thread participant modal title","Add participant")))}var x,y,w,k=class extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{Z.Z.show((0,r.Z)(_,{thread:this.props.thread}))}))}render(){return this.props.thread.acl.can_add_participants?(0,r.Z)("div",{className:"col-xs-12 col-sm-3"},void 0,(0,r.Z)("button",{className:"btn btn-default btn-block",onClick:this.onClick,type:"button"},void 0,o||(o=(0,r.Z)("span",{className:"material-icon"},void 0,"person_add")),pgettext("add participant btn","Add participant"))):null}},C=s(32233),S=class extends c().Component{constructor(e){super(e),(0,l.Z)(this,"onClick",(()=>{let e=!1;if(this.isUser)e=window.confirm(pgettext("private thread owner change","Are you sure you want to take over this thread?"));else{const t=pgettext("private thread owner change","Are you sure you want to change thread owner to %(user)s?");e=window.confirm(interpolate(t,{user:this.props.participant.username},!0))}var t,s;e&&(t=this.props.thread,s=this.props.participant,g.Z.patch(t.api.index,[{op:"replace",path:"owner",value:s.id},{op:"add",path:"acl",value:1}]).then((e=>{b.Z.dispatch((0,v.y8)(e)),b.Z.dispatch(m.gx(e.participants));const t=pgettext("thread participants actions","%(user)s has been made new thread owner.");f.Z.success(interpolate(t,{user:s.username},!0))}),(e=>{f.Z.apiError(e)})))})),this.isUser=e.participant.id===e.user.id}render(){return this.props.participant.is_owner?null:this.props.thread.acl.can_change_owner?(0,r.Z)("li",{},void 0,(0,r.Z)("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},void 0,pgettext("private thread owner change btn","Make owner"))):null}},E=class extends c().Component{constructor(e){super(e),(0,l.Z)(this,"onClick",(()=>{let e=!1;if(this.isUser)e=window.confirm(pgettext("private thread leave","Are you sure you want to leave this thread?"));else{const t=pgettext("private thread leave","Are you sure you want to remove %(user)s from this thread?");e=window.confirm(interpolate(t,{user:this.props.participant.username},!0))}var t,s;e&&(this.isUser?(t=this.props.thread,s=this.props.participant,g.Z.patch(t.api.index,[{op:"remove",path:"participants",value:s.id}]).then((()=>{f.Z.success(pgettext("thread participants actions","You have left this thread.")),window.setTimeout((()=>{window.location=C.Z.get("PRIVATE_THREADS_URL")}),3e3)}),(e=>{f.Z.apiError(e)}))):function(e,t){g.Z.patch(e.api.index,[{op:"remove",path:"participants",value:t.id},{op:"add",path:"acl",value:1}]).then((e=>{b.Z.dispatch((0,v.y8)(e)),b.Z.dispatch(m.gx(e.participants));const s=pgettext("thread participants actions","%(user)s has been removed from this thread.");f.Z.success(interpolate(s,{user:t.username},!0))}),(e=>{f.Z.apiError(e)}))}(this.props.thread,this.props.participant))})),this.isUser=e.participant.id===e.user.id}render(){const e=this.props.user.acl.can_moderate_private_threads;return this.props.userIsOwner||this.isUser||e?(0,r.Z)("li",{},void 0,(0,r.Z)("button",{className:"btn btn-link",onClick:this.onClick,type:"button"},void 0,this.isUser?pgettext("private thread leave btn","Leave thread"):pgettext("private thread leave btn","Remove"))):null}},T=s(19605);function L(e){const t=e.participant;let s="btn btn-default";return t.is_owner&&(s="btn btn-primary"),s+=" btn-user btn-block",(0,r.Z)("div",{className:"col-xs-12 col-sm-3 col-md-2 participant-card"},void 0,(0,r.Z)("div",{className:"dropdown"},void 0,(0,r.Z)("button",{"aria-haspopup":"true","aria-expanded":"false",className:s,"data-toggle":"dropdown",type:"button"},void 0,(0,r.Z)(T.ZP,{size:"34",user:t}),(0,r.Z)("span",{className:"btn-text"},void 0,t.username)),(0,r.Z)("ul",{className:"dropdown-menu stick-to-bottom"},void 0,(0,r.Z)(P,{isOwner:t.is_owner}),x||(x=(0,r.Z)("li",{className:"dropdown-header"})),(0,r.Z)("li",{},void 0,(0,r.Z)("a",{href:t.url},void 0,pgettext("thread participants profile link","See profile"))),y||(y=(0,r.Z)("li",{role:"separator",className:"divider"})),c().createElement(S,e),c().createElement(E,e))))}function P(e){let{isOwner:t}=e;return t?(0,r.Z)("li",{className:"dropdown-header dropdown-header-owner"},void 0,w||(w=(0,r.Z)("span",{className:"material-icon"},void 0,"start")),(0,r.Z)("span",{className:"icon-text"},void 0,pgettext("thread participants owner status","Thread owner"))):null}function O(e){let{participants:t,thread:s,user:a,userIsOwner:i}=e;return(0,r.Z)("div",{className:"participants-cards"},void 0,(0,r.Z)("div",{className:"row"},void 0,t.map((e=>(0,r.Z)(L,{participant:e,thread:s,user:a,userIsOwner:i},e.id)))))}function I(e){return e.participants.length?(0,r.Z)("div",{className:"panel panel-default panel-participants"},void 0,(0,r.Z)("div",{className:"panel-body"},void 0,c().createElement(O,(0,p.Z)({userIsOwner:A(e.user,e.participants)},e)),(0,r.Z)("div",{className:"row"},void 0,(0,r.Z)(k,{thread:e.thread}),(0,r.Z)("div",{className:"col-xs-12 col-sm-9"},void 0,(0,r.Z)("p",{},void 0,function(e){const t=e.length,s=npgettext("thread participants stat","This thread has %(users)s participant.","This thread has %(users)s participants.",t);return interpolate(s,{users:t},!0)}(e.participants)))))):null}function A(e,t){return t[0].id===e.id}var R,D=s(30381),j=s.n(D);function U(e){return(0,r.Z)("div",{className:"poll-choices-bars"},void 0,e.poll.choices.map((t=>(0,r.Z)(z,{choice:t,poll:e.poll},t.hash))))}function z(e){let t=0;return e.choice.votes&&e.poll.votes&&(t=Math.ceil(100*e.choice.votes/e.poll.votes)),(0,r.Z)("dl",{className:"dl-horizontal"},void 0,(0,r.Z)("dt",{},void 0,e.choice.label),(0,r.Z)("dd",{},void 0,(0,r.Z)("div",{className:"progress"},void 0,(0,r.Z)("div",{className:"progress-bar",role:"progressbar","aria-valuenow":t,"aria-valuemin":"0","aria-valuemax":"100",style:{width:t+"%"}},void 0,(0,r.Z)("span",{className:"sr-only"},void 0,B(e.votes,e.proc)))),(0,r.Z)("ul",{className:"list-unstyled list-inline poll-chart"},void 0,(0,r.Z)(M,{proc:t,votes:e.choice.votes}),(0,r.Z)(q,{selected:e.choice.selected}))))}function M(e){return(0,r.Z)("li",{className:"poll-chart-votes"},void 0,B(e.votes,e.proc))}function B(e,t){const s=npgettext("thread poll","%(votes)s vote, %(proc)s% of total.","%(votes)s votes, %(proc)s% of total.",e);return interpolate(s,{votes:e,proc:t},!0)}function q(e){return e.selected?(0,r.Z)("li",{className:"poll-chart-selected"},void 0,R||(R=(0,r.Z)("span",{className:"material-icon"},void 0,"check_box")),pgettext("thread poll","You've voted on this choice.")):null}var F,H,Y,V=s(30337),G=s(3784),$=class extends c().Component{constructor(e){super(e),this.state={isLoading:!0,error:null,data:[]}}componentDidMount(){g.Z.get(this.props.poll.api.votes).then((e=>{const t=e.map((e=>Object.assign({},e,{voters:e.voters.map((e=>Object.assign({},e,{voted_on:j()(e.voted_on)})))})));this.setState({isLoading:!1,data:t})}),(e=>{this.setState({isLoading:!1,error:e.detail})}))}render(){return(0,r.Z)("div",{className:"modal-dialog"+(this.state.error?" modal-message":" modal-sm"),role:"document"},void 0,(0,r.Z)("div",{className:"modal-content"},void 0,(0,r.Z)("div",{className:"modal-header"},void 0,(0,r.Z)("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":pgettext("modal","Close")},void 0,F||(F=(0,r.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,r.Z)("h4",{className:"modal-title"},void 0,pgettext("thread poll","Poll votes"))),(0,r.Z)(W,{data:this.state.data,error:this.state.error,isLoading:this.state.isLoading})))}};function W(e){return e.isLoading?H||(H=(0,r.Z)(G.Z,{})):e.error?(0,r.Z)(V.Z,{icon:"error_outline",message:e.error}):(0,r.Z)(Q,{data:e.data})}function Q(e){return(0,r.Z)("div",{className:"modal-body modal-poll-votes"},void 0,(0,r.Z)("ul",{className:"list-unstyled votes-details"},void 0,e.data.map((e=>c().createElement(X,(0,p.Z)({key:e.hash},e))))))}function X(e){return(0,r.Z)("li",{},void 0,(0,r.Z)("h4",{},void 0,e.label),(0,r.Z)(K,{votes:e.votes}),(0,r.Z)(J,{voters:e.voters}),Y||(Y=(0,r.Z)("hr",{})))}function K(e){const t=npgettext("thread poll","%(votes)s user has voted for this choice.","%(votes)s users have voted for this choice.",e.votes),s=interpolate(t,{votes:e.votes},!0);return(0,r.Z)("p",{},void 0,s)}function J(e){return e.voters.length?(0,r.Z)("ul",{className:"list-unstyled"},void 0,e.voters.map((e=>c().createElement(ee,(0,p.Z)({key:e.username},e))))):null}function ee(e){return e.url?(0,r.Z)("li",{},void 0,(0,r.Z)("a",{className:"item-title",href:e.url},void 0,e.username)," ",(0,r.Z)(te,{voted_on:e.voted_on})):(0,r.Z)("li",{},void 0,(0,r.Z)("strong",{},void 0,e.username)," ",(0,r.Z)(te,{voted_on:e.voted_on}))}function te(e){return(0,r.Z)("abbr",{className:"text-muted",title:e.voted_on.format("LLL")},void 0,e.voted_on.fromNow())}var se=s(59752),ae=s(64646);function ie(e){const{isPollOver:t,poll:s,showVoting:a,thread:i}=e;if(!function(e,t,s){return s.is_public||t.can_delete||t.can_edit||t.can_see_votes||t.can_vote&&!e&&(!s.hasSelectedChoices||s.allow_revotes)}(t,s.acl,s))return null;const o=[],n=s.acl.can_vote,l=!s.hasSelectedChoices||s.allow_revotes;return n&&l&&o.push(0),(s.is_public||s.acl.can_see_votes)&&o.push(1),s.acl.can_edit&&o.push(2),s.acl.can_delete&&o.push(3),(0,r.Z)("div",{className:"row poll-options"},void 0,(0,r.Z)(ne,{controls:o,isPollOver:t,poll:s,showVoting:a}),(0,r.Z)(re,{controls:o,poll:s}),(0,r.Z)(le,{controls:o,poll:s,thread:i,onClick:e.edit}),(0,r.Z)(de,{controls:o,poll:s}))}function oe(e,t){let s="col-xs-6";return 1===e.length&&(s="col-xs-12"),3===e.length&&e[0]===t&&(s="col-xs-12"),s+" col-sm-3 col-md-2"}function ne(e){const t=e.poll.acl.can_vote,s=!e.poll.hasSelectedChoices||e.poll.allow_revotes;return t&&s?(0,r.Z)("div",{className:oe(e.controls,0)},void 0,(0,r.Z)("button",{className:"btn btn-default btn-block btn-sm",disabled:e.poll.isBusy,onClick:e.showVoting,type:"button"},void 0,pgettext("thread poll","Vote"))):null}class re extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{Z.Z.show((0,r.Z)($,{poll:this.props.poll}))}))}render(){return this.props.poll.is_public||this.props.poll.acl.can_see_votes?(0,r.Z)("div",{className:oe(this.props.controls,1)},void 0,(0,r.Z)("button",{className:"btn btn-default btn-block btn-sm",disabled:this.props.poll.isBusy,onClick:this.onClick,type:"button"},void 0,pgettext("thread poll","See votes"))):null}}function le(e){return e.poll.acl.can_edit?(0,r.Z)("div",{className:oe(e.controls,2)},void 0,(0,r.Z)("button",{className:"btn btn-default btn-block btn-sm",disabled:e.poll.isBusy,onClick:e.onClick,type:"button"},void 0,pgettext("thread poll","Edit"))):null}class de extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{if(!window.confirm(pgettext("thread poll","Are you sure you want to delete this poll? This action is not reversible.")))return!1;b.Z.dispatch(se.n6()),g.Z.delete(this.props.poll.api.index).then(this.handleSuccess,this.handleError)})),(0,l.Z)(this,"handleSuccess",(e=>{f.Z.success(pgettext("thread poll","Poll has been deleted")),b.Z.dispatch(se.Od()),b.Z.dispatch(v.y8(e))})),(0,l.Z)(this,"handleError",(e=>{f.Z.apiError(e),b.Z.dispatch(se.Ar())}))}render(){return this.props.poll.acl.can_delete?(0,r.Z)("div",{className:oe(this.props.controls,3)},void 0,(0,r.Z)("button",{className:"btn btn-default btn-block btn-sm",disabled:this.props.poll.isBusy,onClick:this.onClick,type:"button"},void 0,pgettext("thread poll","Delete"))):null}}var ce=s(89627);const pe='%(relative)s';function ue(e){return(0,r.Z)("ul",{className:"list-unstyled list-inline poll-details"},void 0,(0,r.Z)(fe,{votes:e.poll.votes}),(0,r.Z)(ge,{poll:e.poll}),(0,r.Z)(be,{poll:e.poll}),(0,r.Z)(he,{poll:e.poll}))}function he(e){const t=interpolate((0,ce.Z)(pgettext("thread poll","Started by %(poster)s %(posted_on)s.")),{poster:me(e.poll),posted_on:ve(e.poll)},!0);return(0,r.Z)("li",{className:"poll-info-creation",dangerouslySetInnerHTML:{__html:t}})}function me(e){return e.url.poster?interpolate('%(user)s',{url:(0,ce.Z)(e.url.poster),user:(0,ce.Z)(e.poster_name)},!0):interpolate('%(user)s',{user:(0,ce.Z)(e.poster_name)},!0)}function ve(e){return interpolate(pe,{absolute:(0,ce.Z)(e.posted_on.format("LLL")),relative:(0,ce.Z)(e.posted_on.fromNow())},!0)}function ge(e){if(!e.poll.length)return null;const t=interpolate((0,ce.Z)(pgettext("thread poll","Voting ends %(ends_on)s.")),{ends_on:Ze(e.poll)},!0);return(0,r.Z)("li",{className:"poll-info-ends-on",dangerouslySetInnerHTML:{__html:t}})}function Ze(e){return interpolate(pe,{absolute:(0,ce.Z)(e.endsOn.format("LLL")),relative:(0,ce.Z)(e.endsOn.fromNow())},!0)}function fe(e){const t=npgettext("thread poll","%(votes)s vote.","%(votes)s votes.",e.votes),s=interpolate(t,{votes:e.votes},!0);return(0,r.Z)("li",{className:"poll-info-votes"},void 0,s)}function be(e){return e.poll.is_public?(0,r.Z)("li",{className:"poll-info-public"},void 0,pgettext("thread poll","Voting is public.")):null}function _e(e){return(0,r.Z)("div",{className:"panel panel-default panel-poll"},void 0,(0,r.Z)("div",{className:"panel-body"},void 0,(0,r.Z)("h2",{},void 0,e.poll.question),(0,r.Z)(ue,{poll:e.poll}),(0,r.Z)(U,{poll:e.poll}),(0,r.Z)(ie,{isPollOver:e.isPollOver,poll:e.poll,edit:e.edit,showVoting:e.showVoting,thread:e.thread})))}function Ne(e){return(0,r.Z)("ul",{className:"list-unstyled list-inline poll-help"},void 0,(0,r.Z)(xe,{choicesLeft:e.choicesLeft}),(0,r.Z)(ye,{poll:e.poll}))}function xe(e){let{choicesLeft:t}=e;if(0===t)return(0,r.Z)("li",{className:"poll-help-choices-left"},void 0,pgettext("thread poll","You can't select any more choices."));const s=npgettext("thread poll","You can select %(choices)s more choice.","You can select %(choices)s more choices.",t),a=interpolate(s,{choices:t},!0);return(0,r.Z)("li",{className:"poll-help-choices-left"},void 0,a)}function ye(e){return e.poll.allow_revotes?(0,r.Z)("li",{className:"poll-help-allow-revotes"},void 0,pgettext("thread poll","You can change your vote later.")):(0,r.Z)("li",{className:"poll-help-no-revotes"},void 0,pgettext("thread poll","Votes are final."))}function we(e){return(0,r.Z)("ul",{className:"list-unstyled poll-select-choices"},void 0,e.choices.map((t=>(0,r.Z)(ke,{choice:t,toggleChoice:e.toggleChoice},t.hash))))}class ke extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{this.props.toggleChoice(this.props.choice.hash)}))}render(){return(0,r.Z)("li",{className:"poll-select-choice"},void 0,(0,r.Z)("button",{className:this.props.choice.selected?"btn btn-selected":"btn",onClick:this.onClick,type:"button"},void 0,(0,r.Z)("span",{className:"material-icon"},void 0,this.props.choice.selected?"check_box":"check_box_outline_blank"),(0,r.Z)("strong",{},void 0,this.props.choice.label)))}}function Ce(e,t){let s=[];for(const e in t){const a=t[e];a.selected&&s.push(a)}return e.allowed_choices-s.length}var Se,Ee=s(82211),Te=class extends u.Z{constructor(e){super(e),(0,l.Z)(this,"toggleChoice",(e=>{const t=function(e,t){for(const s in e){const a=e[s];if(a.hash===t)return a}return null}(this.state.choices,e);let s=null;s=t.selected?this.deselectChoice(t,e):this.selectChoice(t,e),this.setState({choices:s,choicesLeft:Ce(this.props.poll,s)})})),(0,l.Z)(this,"selectChoice",((e,t)=>{if(!Ce(this.props.poll,this.state.choices))for(const e in this.state.choices.slice()){const s=this.state.choices[e];if(s.selected&&s.hash!=t){s.selected=!1;break}}return this.state.choices.map((e=>Object.assign({},e,{selected:e.hash==t||e.selected})))})),(0,l.Z)(this,"deselectChoice",((e,t)=>this.state.choices.map((e=>Object.assign({},e,{selected:e.hash!=t&&e.selected}))))),this.state={isLoading:!1,choices:e.poll.choices,choicesLeft:Ce(e.poll,e.poll.choices)}}clean(){return this.state.choicesLeft!==this.props.poll.allowed_choices||(f.Z.error(pgettext("thread poll vote","You need to select at least one choice")),!1)}send(){let e=[];for(const t in this.state.choices.slice()){const s=this.state.choices[t];s.selected&&e.push(s.hash)}return g.Z.post(this.props.poll.api.votes,e)}handleSuccess(e){b.Z.dispatch(se.gx(e)),f.Z.success(pgettext("thread poll vote","Your vote has been saved.")),this.props.showResults()}handleError(e){400===e.status?f.Z.error(e.detail):f.Z.apiError(e)}render(){const e=[];return this.props.poll.acl.can_vote&&e.push(0),(this.props.poll.is_public||this.props.poll.acl.can_see_votes)&&e.push(1),this.props.poll.acl.can_edit&&e.push(2),this.props.poll.acl.can_delete&&e.push(3),(0,r.Z)("div",{className:"panel panel-default panel-poll"},void 0,(0,r.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,r.Z)("div",{className:"panel-body"},void 0,(0,r.Z)("h2",{},void 0,this.props.poll.question),(0,r.Z)(ue,{poll:this.props.poll}),(0,r.Z)(we,{choices:this.state.choices,toggleChoice:this.toggleChoice}),(0,r.Z)(Ne,{choicesLeft:this.state.choicesLeft,poll:this.props.poll})),(0,r.Z)("div",{className:"panel-footer"},void 0,(0,r.Z)("div",{className:"row"},void 0,(0,r.Z)("div",{className:oe(e,0)},void 0,(0,r.Z)(Ee.Z,{className:"btn-primary btn-block btn-sm",loading:this.state.isLoading},void 0,pgettext("thread poll vote btn","Save your vote"))),(0,r.Z)("div",{className:oe(e,1)},void 0,(0,r.Z)("button",{className:"btn btn-default btn-block btn-sm",disabled:this.state.isLoading,onClick:this.props.showResults,type:"button"},void 0,pgettext("thread poll vote btn","See results"))),(0,r.Z)(le,{controls:e,poll:this.props.poll,thread:this.props.thread,onClick:this.props.edit}),(0,r.Z)(de,{controls:e,poll:this.props.poll})))))}},Le=class extends c().Component{constructor(e){super(e),(0,l.Z)(this,"showResults",(()=>{this.setState({showResults:!0})})),(0,l.Z)(this,"showVoting",(()=>{this.setState({showResults:!1})}));let t=!0;e.user.id&&!e.poll.hasSelectedChoices&&(t=!1),this.state={showResults:t}}render(){if(!this.props.thread.poll)return null;const e=function(e){return!!e.length&&j()().isAfter(e.endsOn)}(this.props.poll);return e||!this.props.poll.acl.can_vote||this.state.showResults?c().createElement(_e,(0,p.Z)({isPollOver:e,showVoting:this.showVoting},this.props)):c().createElement(Te,(0,p.Z)({showResults:this.showResults},this.props))}},Pe=s(54031),Oe=class extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onAdd",(()=>{let e=this.props.choices.slice();e.push({hash:(0,Pe.ZP)(12),label:""}),this.props.setChoices(e)})),(0,l.Z)(this,"onChange",((e,t)=>{const s=this.props.choices.map((s=>(s.hash===e&&(s.label=t),s)));this.props.setChoices(s)})),(0,l.Z)(this,"onDelete",(e=>{const t=this.props.choices.filter((t=>t.hash!==e));this.props.setChoices(t)}))}render(){return(0,r.Z)("div",{className:"poll-choices-control"},void 0,(0,r.Z)("ul",{className:"list-group"},void 0,this.props.choices.map((e=>(0,r.Z)(Ie,{canDelete:this.props.choices.length>2,choice:e,disabled:this.props.disabled,onChange:this.onChange,onDelete:this.onDelete},e.hash)))),(0,r.Z)("button",{className:"btn btn-default btn-sm",disabled:this.props.disabled,onClick:this.onAdd,type:"button"},void 0,pgettext("thread poll","Add choice")))}};class Ie extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onChange",(e=>{this.props.onChange(this.props.choice.hash,e.target.value)})),(0,l.Z)(this,"onDelete",(()=>{(0===this.props.choice.label.length||window.confirm(pgettext("thread poll","Are you sure you want to remove this choice?")))&&this.props.onDelete(this.props.choice.hash)}))}render(){return(0,r.Z)("li",{className:"list-group-item"},void 0,(0,r.Z)("button",{className:"btn",disabled:!this.props.canDelete||this.props.disabled,onClick:this.onDelete,title:pgettext("thread poll","Remove this choice"),type:"button"},void 0,Se||(Se=(0,r.Z)("span",{className:"material-icon"},void 0,"close"))),(0,r.Z)("input",{disabled:this.props.disabled,maxLength:"255",placeholder:pgettext("thread poll","Poll choice"),type:"text",onChange:this.onChange,value:this.props.choice.label}))}}var Ae=s(7227),Re=class extends u.Z{constructor(e){super(e),(0,l.Z)(this,"setChoices",(e=>{this.setState((t=>({choices:e,errors:Object.assign({},t.errors,{choices:null})})))})),(0,l.Z)(this,"onCancel",(()=>{let e=!1;if(""===this.state.question&&this.state.choices&&this.state.choices.every((e=>""===e.label))&&0===this.state.length&&1===this.state.allowed_choices)return this.props.close();e=this.props.poll?window.confirm(pgettext("thread poll","Are you sure you want to discard changes?")):window.confirm(pgettext("thread poll","Are you sure you want to discard new poll?")),e&&this.props.close()}));const t=e.poll.id?e.poll:{question:"",choices:[{hash:"choice-10000",label:""},{hash:"choice-20000",label:""}],length:0,allowed_choices:1,allow_revotes:0,is_public:0};this.state={isLoading:!1,isEdit:!!t.id,question:t.question,choices:t.choices,length:t.length,allowed_choices:t.allowed_choices,allow_revotes:t.allow_revotes,is_public:t.is_public,validators:{question:[],choices:[],length:[],allowed_choices:[]},errors:{}}}send(){const e={question:this.state.question,choices:this.state.choices,length:this.state.length,allowed_choices:this.state.allowed_choices,allow_revotes:this.state.allow_revotes,is_public:this.state.is_public};return this.state.isEdit?g.Z.put(this.props.poll.api.index,e):g.Z.post(this.props.thread.api.poll,e)}handleSuccess(e){b.Z.dispatch(se.gx(e)),this.state.isEdit?f.Z.success(pgettext("thread poll","Poll has been edited.")):f.Z.success(pgettext("thread poll","Poll has been posted.")),this.props.close()}handleError(e){400===e.status?(e.non_field_errors&&(e.allowed_choices=e.non_field_errors),this.setState({errors:Object.assign({},e)}),f.Z.error(gettext("Form contains errors."))):f.Z.apiError(e)}render(){return(0,r.Z)("div",{className:"poll-form"},void 0,(0,r.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,r.Z)("div",{className:"panel panel-default panel-form"},void 0,(0,r.Z)("div",{className:"panel-heading"},void 0,(0,r.Z)("h3",{className:"panel-title"},void 0,this.state.isEdit?pgettext("thread poll","Edit poll"):pgettext("thread poll","Add poll"))),(0,r.Z)("div",{className:"panel-body"},void 0,(0,r.Z)("fieldset",{},void 0,(0,r.Z)("legend",{},void 0,pgettext("thread poll","Question and choices")),(0,r.Z)(h.Z,{label:pgettext("thread poll","Poll question"),for:"id_questions",validation:this.state.errors.question},void 0,(0,r.Z)("input",{className:"form-control",disabled:this.state.isLoading,id:"id_questions",onChange:this.bindInput("question"),type:"text",maxLength:"255",value:this.state.question})),(0,r.Z)(h.Z,{label:pgettext("thread poll","Available choices"),validation:this.state.errors.choices},void 0,(0,r.Z)(Oe,{choices:this.state.choices,disabled:this.state.isLoading,setChoices:this.setChoices}))),(0,r.Z)("fieldset",{},void 0,(0,r.Z)("legend",{},void 0,pgettext("thread poll","Voting")),(0,r.Z)("div",{className:"row"},void 0,(0,r.Z)("div",{className:"col-xs-12 col-sm-6"},void 0,(0,r.Z)(h.Z,{label:pgettext("thread poll","Poll length"),helpText:pgettext("thread poll","Enter number of days for which voting in this poll should be possible or zero to run this poll indefinitely."),for:"id_length",validation:this.state.errors.length},void 0,(0,r.Z)("input",{className:"form-control",disabled:this.state.isLoading,id:"id_length",onChange:this.bindInput("length"),type:"text",value:this.state.length}))),(0,r.Z)("div",{className:"col-xs-12 col-sm-6"},void 0,(0,r.Z)(h.Z,{label:pgettext("thread poll","Allowed choices"),for:"id_allowed_choices",validation:this.state.errors.allowed_choices},void 0,(0,r.Z)("input",{className:"form-control",disabled:this.state.isLoading,id:"id_allowed_choices",onChange:this.bindInput("allowed_choices"),type:"text",maxLength:"255",value:this.state.allowed_choices})))),(0,r.Z)("div",{className:"row"},void 0,(0,r.Z)(De,{bindInput:this.bindInput,disabled:this.state.isLoading,isEdit:this.state.isEdit,value:this.state.is_public}),(0,r.Z)("div",{className:"col-xs-12 col-sm-6"},void 0,(0,r.Z)(h.Z,{label:pgettext("thread poll","Allow vote changes"),for:"id_allow_revotes"},void 0,(0,r.Z)(Ae.Z,{id:"id_allow_revotes",disabled:this.state.isLoading,iconOn:"check",iconOff:"close",labelOn:pgettext("thread poll","Allow participants to change their vote"),labelOff:pgettext("thread poll","Don't allow participants to change their vote"),onChange:this.bindInput("allow_revotes"),value:this.state.allow_revotes})))))),(0,r.Z)("div",{className:"panel-footer text-right"},void 0,(0,r.Z)("button",{className:"btn btn-default",disabled:this.state.isLoading,onClick:this.onCancel,type:"button"},void 0,pgettext("thread poll","Cancel"))," ",(0,r.Z)(Ee.Z,{className:"btn-primary",loading:this.state.isLoading},void 0,this.state.isEdit?pgettext("thread poll","Save changes"):pgettext("thread poll","Post poll"))))))}};function De(e){return e.isEdit?null:(0,r.Z)("div",{className:"col-xs-12 col-sm-6"},void 0,(0,r.Z)(h.Z,{label:pgettext("thread poll","Make voting public"),helpText:pgettext("thread poll","Making voting public will allow everyone to access detailed list of votes, showing which users voted for which choices and at which times. This option can't be changed after poll's creation. Moderators may see voting details for all polls."),for:"id_is_public"},void 0,(0,r.Z)(Ae.Z,{id:"id_is_public",disabled:e.disabled,iconOn:"visibility",iconOff:"visibility_off",labelOn:pgettext("thread poll","Votes are public"),labelOff:pgettext("thread poll","Votes are hidden"),onChange:e.bindInput("is_public"),value:e.value})))}const je={changed_title:"edit",pinned_globally:"bookmark",pinned_locally:"bookmark_border",unpinned:"panorama_fish_eye",moved:"arrow_forward",merged:"call_merge",approved:"done",opened:"lock_open",closed:"lock_outline",unhid:"visibility",hid:"visibility_off",changed_owner:"grade",tookover:"grade",added_participant:"person_add",owner_left:"person_outline",participant_left:"person_outline",removed_participant:"remove_circle_outline"};var Ue=e=>(0,r.Z)("span",{className:"event-icon-bg"},void 0,(0,r.Z)("span",{className:"material-icon"},void 0,je[e.post.event_type])),ze=s(92747);function Me(e){return e.post.acl.can_hide?(0,r.Z)("li",{className:"event-controls"},void 0,c().createElement(Be,e),c().createElement(qe,e),c().createElement(Fe,e)):null}class Be extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{b.Z.dispatch(ze.r$(this.props.post,{is_hidden:!0,hidden_on:j()(),hidden_by_name:this.props.user.username,url:Object.assign(this.props.post.url,{hidden_by:this.props.user.url})})),g.Z.patch(this.props.post.api.index,[{op:"replace",path:"is-hidden",value:!0}]).then((e=>{b.Z.dispatch(ze.r$(this.props.post,e))}),(e=>{400===e.status?f.Z.error(e.detail[0]):f.Z.apiError(e),b.Z.dispatch(ze.r$(this.props.post,{is_hidden:!1}))}))}))}render(){return this.props.post.is_hidden?null:(0,r.Z)("button",{type:"button",className:"btn btn-link",onClick:this.onClick},void 0,pgettext("event hide btn","Hide"))}}class qe extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{b.Z.dispatch(ze.r$(this.props.post,{is_hidden:!1})),g.Z.patch(this.props.post.api.index,[{op:"replace",path:"is-hidden",value:!1}]).then((e=>{b.Z.dispatch(ze.r$(this.props.post,e))}),(e=>{400===e.status?f.Z.error(e.detail[0]):f.Z.apiError(e),b.Z.dispatch(ze.r$(this.props.post,{is_hidden:!0}))}))}))}render(){return this.props.post.is_hidden?(0,r.Z)("button",{type:"button",className:"btn btn-link",onClick:this.onClick},void 0,pgettext("event reveal btn","Unhide")):null}}class Fe extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{window.confirm(gettext("event delete","Are you sure you wish to delete this event? This action is not reversible!"))&&this.delete()})),(0,l.Z)(this,"delete",(()=>{b.Z.dispatch(ze.r$(this.props.post,{isDeleted:!0})),g.Z.delete(this.props.post.api.index).then((()=>{f.Z.success(pgettext("event delete","Event has been deleted."))}),(e=>{400===e.status?f.Z.error(e.detail[0]):f.Z.apiError(e),b.Z.dispatch(ze.r$(this.props.post,{isDeleted:!1}))}))}))}render(){return(0,r.Z)("button",{type:"button",className:"btn btn-link",onClick:this.onClick},void 0,pgettext("event delete btn","Delete"))}}const He='%(user)s',Ye='%(user)s';function Ve(e){return(0,r.Z)("ul",{className:"list-inline event-info"},void 0,c().createElement(Ge,e),c().createElement($e,e),c().createElement(Me,e))}function Ge(e){if(e.post.is_hidden){let t=null;t=e.post.url.hidden_by?interpolate(Ye,{url:(0,ce.Z)(e.post.url.hidden_by),user:(0,ce.Z)(e.post.hidden_by_name)},!0):interpolate(He,{user:(0,ce.Z)(e.post.hidden_by_name)},!0);const s=interpolate('%(relative)s',{absolute:(0,ce.Z)(e.post.hidden_on.format("LLL")),relative:(0,ce.Z)(e.post.hidden_on.fromNow())},!0),a=interpolate((0,ce.Z)(pgettext("event info","Hidden by %(event_by)s %(event_on)s.")),{event_by:t,event_on:s},!0);return(0,r.Z)("li",{className:"event-hidden-message",dangerouslySetInnerHTML:{__html:a}})}return null}function $e(e){let t=null;t=e.post.poster?interpolate(Ye,{url:(0,ce.Z)(e.post.poster.url),user:(0,ce.Z)(e.post.poster_name)},!0):interpolate(He,{user:(0,ce.Z)(e.post.poster_name)},!0);const s=interpolate('%(relative)s',{url:(0,ce.Z)(e.post.url.index),absolute:(0,ce.Z)(e.post.posted_on.format("LLL")),relative:(0,ce.Z)(e.post.posted_on.fromNow())},!0),a=interpolate((0,ce.Z)(pgettext("event info","By %(event_by)s %(event_on)s.")),{event_by:t,event_on:s},!0);return(0,r.Z)("li",{className:"event-posters",dangerouslySetInnerHTML:{__html:a}})}const We={pinned_globally:pgettext("event message","Thread has been pinned globally."),pinned_locally:pgettext("event message","Thread has been pinned locally."),unpinned:pgettext("event message","Thread has been unpinned."),approved:pgettext("event message","Thread has been approved."),opened:pgettext("event message","Thread has been opened."),closed:pgettext("event message","Thread has been closed."),unhid:pgettext("event message","Thread has been revealed."),hid:pgettext("event message","Thread has been made hidden."),tookover:pgettext("event message","Took thread over."),owner_left:pgettext("event message","Owner has left thread. This thread is now closed."),participant_left:pgettext("event message","Participant has left thread.")},Qe='%(name)s',Xe='%(name)s';function Ke(e){return We[e.post.event_type]?(0,r.Z)("p",{className:"event-message"},void 0,We[e.post.event_type]):"changed_title"===e.post.event_type?c().createElement(Je,e):"moved"===e.post.event_type?c().createElement(et,e):"merged"===e.post.event_type?c().createElement(tt,e):"changed_owner"===e.post.event_type?c().createElement(st,e):"added_participant"===e.post.event_type?c().createElement(at,e):"removed_participant"===e.post.event_type?c().createElement(it,e):null}function Je(e){const t=(0,ce.Z)(pgettext("event message","Thread title has been changed from %(old_title)s.")),s=interpolate(Xe,{name:(0,ce.Z)(e.post.event_context.old_title)},!0),a=interpolate(t,{old_title:s},!0);return(0,r.Z)("p",{className:"event-message",dangerouslySetInnerHTML:{__html:a}})}function et(e){const t=(0,ce.Z)(pgettext("event message","Thread has been moved from %(from_category)s.")),s=interpolate(Qe,{url:(0,ce.Z)(e.post.event_context.from_category.url),name:(0,ce.Z)(e.post.event_context.from_category.name)},!0),a=interpolate(t,{from_category:s},!0);return(0,r.Z)("p",{className:"event-message",dangerouslySetInnerHTML:{__html:a}})}function tt(e){const t=(0,ce.Z)(pgettext("event message","The %(merged_thread)s thread has been merged into this thread.")),s=interpolate(Xe,{name:(0,ce.Z)(e.post.event_context.merged_thread)},!0),a=interpolate(t,{merged_thread:s},!0);return(0,r.Z)("p",{className:"event-message",dangerouslySetInnerHTML:{__html:a}})}function st(e){const t=(0,ce.Z)(pgettext("event message","Changed thread owner to %(user)s.")),s=interpolate(Qe,{url:(0,ce.Z)(e.post.event_context.user.url),name:(0,ce.Z)(e.post.event_context.user.username)},!0),a=interpolate(t,{user:s},!0);return(0,r.Z)("p",{className:"event-message",dangerouslySetInnerHTML:{__html:a}})}function at(e){const t=(0,ce.Z)(pgettext("event message","Added %(user)s to thread.")),s=interpolate(Qe,{url:(0,ce.Z)(e.post.event_context.user.url),name:(0,ce.Z)(e.post.event_context.user.username)},!0),a=interpolate(t,{user:s},!0);return(0,r.Z)("p",{className:"event-message",dangerouslySetInnerHTML:{__html:a}})}function it(e){const t=(0,ce.Z)(pgettext("event message","Removed %(user)s from thread.")),s=interpolate(Qe,{url:(0,ce.Z)(e.post.event_context.user.url),name:(0,ce.Z)(e.post.event_context.user.username)},!0),a=interpolate(t,{user:s},!0);return(0,r.Z)("p",{className:"event-message",dangerouslySetInnerHTML:{__html:a}})}function ot(e){let{post:t}=e;return t.is_read?null:(0,r.Z)("div",{className:"event-label"},void 0,(0,r.Z)("span",{className:"label label-unread"},void 0,pgettext("event unread label","New event")))}var nt=class extends c().Component{constructor(e){super(e),(0,l.Z)(this,"initialize",(e=>{this.initialized=!0,this.observer=new IntersectionObserver((e=>e.forEach(this.callback))),this.observer.observe(e)})),(0,l.Z)(this,"callback",(e=>{!e.isIntersecting||this.props.post.is_read||this.primed||(window.setTimeout((()=>{g.Z.post(this.props.post.api.read)}),0),this.primed=!0,this.destroy())})),this.initialized=!1,this.primed=!1,this.observer=null}destroy(){this.observer&&(this.observer.disconnect(),this.observer=null)}componentWillUnmount(){this.destroy()}render(){const e=!this.initialized&&!this.primed&&!this.props.post.is_read;return c().createElement("div",{className:this.props.className,ref:t=>{t&&e&&this.initialize(t)}},this.props.children)}};function rt(e){let t="event";return e.post.isDeleted?t="hide":e.post.is_hidden&&(t="event post-hidden"),(0,r.Z)("li",{id:"post-"+e.post.id,className:t},void 0,(0,r.Z)(ot,{post:e.post}),(0,r.Z)("div",{className:"event-body"},void 0,(0,r.Z)("div",{className:"event-icon"},void 0,c().createElement(Ue,e)),(0,r.Z)(nt,{className:"event-content",post:e.post},void 0,c().createElement(Ke,e),c().createElement(Ve,e))))}var lt=s(69130),dt=s(48772);function ct(e){return(0,r.Z)("div",{className:"col-xs-12 col-md-6"},void 0,c().createElement(pt,e),(0,r.Z)("div",{className:"post-attachment"},void 0,(0,r.Z)("a",{href:e.attachment.url.index,className:"attachment-name item-title",target:"_blank"},void 0,e.attachment.filename),c().createElement(mt,e)))}function pt(e){return e.attachment.is_image?(0,r.Z)("div",{className:"post-attachment-preview"},void 0,c().createElement(ht,e)):(0,r.Z)("div",{className:"post-attachment-preview"},void 0,c().createElement(ut,e))}function ut(e){return(0,r.Z)("a",{href:e.attachment.url.index,className:"material-icon"},void 0,"insert_drive_file")}function ht(e){const t=e.attachment.url.thumb||e.attachment.url.index;return(0,r.Z)("a",{className:"post-thumbnail",href:e.attachment.url.index,target:"_blank",style:{backgroundImage:'url("'+(0,ce.Z)(t)+'")'}})}function mt(e){let t=null;t=e.attachment.url.uploader?interpolate('%(user)s',{url:(0,ce.Z)(e.attachment.url.uploader),user:(0,ce.Z)(e.attachment.uploader_name)},!0):interpolate('%(user)s',{user:(0,ce.Z)(e.attachment.uploader_name)},!0);const s=interpolate('%(relative)s',{absolute:(0,ce.Z)(e.attachment.uploaded_on.format("LLL")),relative:(0,ce.Z)(e.attachment.uploaded_on.fromNow())},!0),a=interpolate((0,ce.Z)(pgettext("post attachment","%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s.")),{filetype:e.attachment.filetype,size:(0,dt.Z)(e.attachment.size),uploader:t,uploaded_on:s},!0);return(0,r.Z)("p",{className:"post-attachment-description",dangerouslySetInnerHTML:{__html:a}})}function vt(e){return function(e){return(!e.is_hidden||e.acl.can_see_hidden)&&e.attachments}(e.post)?(0,r.Z)("div",{className:"post-attachments"},void 0,(0,lt.Z)(e.post.attachments,2).map((e=>{const t=e.map((e=>e?e.id:0)).join("_");return(0,r.Z)(gt,{row:e},t)}))):null}function gt(e){return(0,r.Z)("div",{className:"row"},void 0,e.row.map((e=>(0,r.Z)(ct,{attachment:e},e?e.id:0))))}var Zt,ft,bt,_t,Nt,xt,yt,wt=s(69092);function kt(e){return e.post.is_hidden&&!e.post.acl.can_see_hidden?c().createElement(St,e):e.post.content?c().createElement(Ct,e):c().createElement(Et,e)}function Ct(e){let{post:t}=e;const s="@"+(t.poster?t.poster.username:t.poster_name);return(0,r.Z)(nt,{className:"post-body",post:t},void 0,(0,r.Z)(wt.Z,{author:s,markup:t.content}))}function St(e){let t=null;t=e.post.hidden_by?interpolate('%(user)s',{url:(0,ce.Z)(e.post.url.hidden_by),user:(0,ce.Z)(e.post.hidden_by_name)},!0):interpolate('%(user)s',{user:(0,ce.Z)(e.post.hidden_by_name)},!0);const s=interpolate('%(relative)s',{absolute:(0,ce.Z)(e.post.hidden_on.format("LLL")),relative:(0,ce.Z)(e.post.hidden_on.fromNow())},!0),a=interpolate((0,ce.Z)(pgettext("post body hidden","Hidden by %(hidden_by)s %(hidden_on)s.")),{hidden_by:t,hidden_on:s},!0);return(0,r.Z)(nt,{className:"post-body post-body-hidden",post:e.post},void 0,(0,r.Z)("p",{className:"lead"},void 0,pgettext("post body hidden","This post is hidden. You cannot see its contents.")),(0,r.Z)("p",{className:"text-muted",dangerouslySetInnerHTML:{__html:a}}))}function Et(e){return(0,r.Z)(nt,{className:"post-body post-body-invalid",post:e.post},void 0,(0,r.Z)("p",{className:"lead"},void 0,pgettext("post body invalid","This post's contents cannot be displayed.")),(0,r.Z)("p",{className:"text-muted"},void 0,pgettext("post body invalid","This error is caused by invalid post content manipulation.")))}function Tt(e){let{post:t,thread:s,user:a}=e;if(!It(t)||t.id!==s.best_answer)return null;let i=null;return i=a.id&&s.best_answer_marked_by===a.id?interpolate(pgettext("post best answer flag","Marked as best answer by you %(marked_on)s."),{marked_on:s.best_answer_marked_on.fromNow()},!0):interpolate(pgettext("post best answer flag","Marked as best answer by %(marked_by)s %(marked_on)s."),{marked_by:s.best_answer_marked_by_name,marked_on:s.best_answer_marked_on.fromNow()},!0),(0,r.Z)("div",{className:"post-status-message post-status-best-answer"},void 0,Zt||(Zt=(0,r.Z)("span",{className:"material-icon"},void 0,"check_box")),(0,r.Z)("p",{},void 0,i))}function Lt(e){return It(e.post)&&e.post.is_hidden?(0,r.Z)("div",{className:"post-status-message post-status-hidden"},void 0,ft||(ft=(0,r.Z)("span",{className:"material-icon"},void 0,"visibility_off")),(0,r.Z)("p",{},void 0,pgettext("post hidden flag","This post is hidden. Only users with permission may see its contents."))):null}function Pt(e){return It(e.post)&&e.post.is_unapproved?(0,r.Z)("div",{className:"post-status-message post-status-unapproved"},void 0,bt||(bt=(0,r.Z)("span",{className:"material-icon"},void 0,"remove_circle_outline")),(0,r.Z)("p",{},void 0,pgettext("post unapproved flag","This post is unapproved. Only users with permission to approve posts and its author may see its contents."))):null}function Ot(e){return It(e.post)&&e.post.is_protected?(0,r.Z)("div",{className:"post-status-message post-status-protected visible-xs-block"},void 0,_t||(_t=(0,r.Z)("span",{className:"material-icon"},void 0,"lock_outline")),(0,r.Z)("p",{},void 0,pgettext("post protected flag","This post is protected. Only moderators may change it."))):null}function It(e){return!e.is_hidden||e.acl.can_see_hidden}function At(e,t,s){g.Z.patch(e.post.api.index,t).then((t=>{b.Z.dispatch(ze.r$(e.post,t))}),(t=>{400===t.status?f.Z.error(t.detail[0]):f.Z.apiError(t),b.Z.dispatch(ze.r$(e.post,s))}))}function Rt(e){const{post:t,user:s}=e;b.Z.dispatch(v.Vx({best_answer:t.id,best_answer_is_protected:t.is_protected,best_answer_marked_on:j()(),best_answer_marked_by:s.id,best_answer_marked_by_name:s.username,best_answer_marked_by_slug:s.slug})),Dt(e,[{op:"replace",path:"best-answer",value:t.id},{op:"add",path:"acl",value:!0}],{best_answer:e.thread.best_answer,best_answer_is_protected:e.thread.best_answer_is_protected,best_answer_marked_on:e.thread.best_answer_marked_on,best_answer_marked_by:e.thread.best_answer_marked_by,best_answer_marked_by_name:e.thread.best_answer_marked_by_name,best_answer_marked_by_slug:e.thread.best_answer_marked_by_slug})}function Dt(e,t,s){g.Z.patch(e.thread.api.index,t).then((e=>{e.best_answer_marked_on&&(e.best_answer_marked_on=j()(e.best_answer_marked_on)),b.Z.dispatch(v.Vx(e))}),(e=>{400===e.status?f.Z.error(e.detail[0]):f.Z.apiError(e),b.Z.dispatch(v.Vx(s))}))}var jt,Ut,zt,Mt,Bt,qt,Ft=class extends c().Component{constructor(e){super(e),this.state={isReady:!1,error:null,likes:[]}}componentDidMount(){g.Z.get(this.props.post.api.likes).then((e=>{this.setState({isReady:!0,likes:e.map(Ht)})}),(e=>{this.setState({isReady:!0,error:e.detail})}))}render(){return this.state.error?(0,r.Z)(Yt,{className:"modal-message"},void 0,(0,r.Z)(V.Z,{message:this.state.error})):this.state.isReady?this.state.likes.length?(0,r.Z)(Yt,{className:"modal-sm",likes:this.state.likes},void 0,(0,r.Z)(Vt,{likes:this.state.likes})):(0,r.Z)(Yt,{className:"modal-message"},void 0,(0,r.Z)(V.Z,{message:pgettext("post likes modal","No users have liked this post.")})):Nt||(Nt=(0,r.Z)(Yt,{className:"modal-sm"},void 0,(0,r.Z)(G.Z,{})))}};function Ht(e){return Object.assign({},e,{liked_on:j()(e.liked_on)})}function Yt(e){let{className:t,children:s,likes:a}=e,i=pgettext("post likes modal title","Post Likes");if(a){const e=a.length,t=npgettext("post likes modal","%(likes)s like","%(likes)s likes",e);i=interpolate(t,{likes:e},!0)}return(0,r.Z)("div",{className:"modal-dialog "+(t||""),role:"document"},void 0,(0,r.Z)("div",{className:"modal-content"},void 0,(0,r.Z)("div",{className:"modal-header"},void 0,(0,r.Z)("button",{"aria-label":pgettext("modal","Close"),className:"close","data-dismiss":"modal",type:"button"},void 0,xt||(xt=(0,r.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,r.Z)("h4",{className:"modal-title"},void 0,i)),s))}function Vt(e){return(0,r.Z)("div",{className:"modal-body modal-post-likers"},void 0,(0,r.Z)("ul",{className:"media-list"},void 0,e.likes.map((e=>c().createElement(Gt,(0,p.Z)({key:e.id},e))))))}function Gt(e){if(e.url){const t={id:e.liker_id,avatars:e.avatars};return(0,r.Z)("li",{className:"media"},void 0,(0,r.Z)("div",{className:"media-left"},void 0,(0,r.Z)("a",{className:"user-avatar",href:e.url},void 0,(0,r.Z)(T.ZP,{size:"50",user:t}))),(0,r.Z)("div",{className:"media-body"},void 0,(0,r.Z)("a",{className:"item-title",href:e.url},void 0,e.username)," ",(0,r.Z)($t,{likedOn:e.liked_on})))}return(0,r.Z)("li",{className:"media"},void 0,yt||(yt=(0,r.Z)("div",{className:"media-left"},void 0,(0,r.Z)("span",{className:"user-avatar"},void 0,(0,r.Z)(T.ZP,{size:"50"})))),(0,r.Z)("div",{className:"media-body"},void 0,(0,r.Z)("strong",{},void 0,e.username)," ",(0,r.Z)($t,{likedOn:e.liked_on})))}function $t(e){return(0,r.Z)("span",{className:"text-muted",title:e.likedOn.format("LLL")},void 0,e.likedOn.fromNow())}function Wt(e){return function(e){return(!e.is_hidden||e.acl.can_see_hidden)&&(e.acl.can_reply||e.acl.can_edit||e.acl.can_see_likes&&(e.last_likes||[]).length||e.acl.can_like)}(e.post)?(0,r.Z)("div",{className:"post-footer"},void 0,c().createElement(Qt,e),c().createElement(Xt,e),c().createElement(Kt,e),c().createElement(Jt,(0,p.Z)({lastLikes:e.post.last_likes,likes:e.post.likes},e)),c().createElement(es,(0,p.Z)({likes:e.post.likes},e)),c().createElement(ss,e),c().createElement(as,e),c().createElement(is,e)):null}class Qt extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{Rt(this.props)}))}render(){const{post:e,thread:t}=this.props;return t.acl.can_mark_best_answer&&e.acl.can_mark_as_best_answer?t.best_answer&&!t.acl.can_change_best_answer?null:(0,r.Z)("button",{className:"hidden-xs btn btn-default btn-sm pull-left",disabled:this.props.post.isBusy||e.id===t.best_answer,onClick:this.onClick,type:"button"},void 0,jt||(jt=(0,r.Z)("span",{className:"material-icon"},void 0,"check_box")),pgettext("post footer btn","Best answer")):null}}class Xt extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{Rt(this.props)}))}render(){const{post:e,thread:t}=this.props;return t.acl.can_mark_best_answer&&e.acl.can_mark_as_best_answer?t.best_answer&&!t.acl.can_change_best_answer?null:(0,r.Z)("button",{className:"visible-xs-inline-block btn btn-default btn-sm pull-left",disabled:this.props.post.isBusy||e.id===t.best_answer,onClick:this.onClick,type:"button"},void 0,Ut||(Ut=(0,r.Z)("span",{className:"material-icon"},void 0,"check_box"))):null}}class Kt extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{this.props.post.is_liked?function(e){b.Z.dispatch(ze.r$(e.post,{is_liked:!1,likes:e.post.likes-1,last_likes:e.post.last_likes.filter((t=>!t.id||t.id!==e.user.id))}));const t={is_liked:e.post.is_liked,likes:e.post.likes,last_likes:e.post.last_likes};At(e,[{op:"replace",path:"is-liked",value:!1}],t)}(this.props):function(e){const t=e.post.last_likes||[],s=[e.user].concat(t),a=s.length>3?s.slice(0,-1):s;b.Z.dispatch(ze.r$(e.post,{is_liked:!0,likes:e.post.likes+1,last_likes:a})),At(e,[{op:"replace",path:"is-liked",value:!0}],{is_liked:e.post.is_liked,likes:e.post.likes,last_likes:e.post.last_likes})}(this.props)}))}render(){if(!this.props.post.acl.can_like)return null;let e="btn btn-default btn-sm pull-left";return this.props.post.is_liked&&(e="btn btn-success btn-sm pull-left"),(0,r.Z)("button",{className:e,disabled:this.props.post.isBusy,onClick:this.onClick,type:"button"},void 0,this.props.post.is_liked?pgettext("post footer btn","Liked"):pgettext("post footer btn","Like"))}}class Jt extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{Z.Z.show((0,r.Z)(Ft,{post:this.props.post}))}))}render(){const e=(this.props.post.last_likes||[]).length>0;return this.props.post.acl.can_see_likes&&e?2===this.props.post.acl.can_see_likes?(0,r.Z)("button",{className:"btn btn-link btn-sm pull-left hidden-xs",onClick:this.onClick,type:"button"},void 0,ts(this.props.likes,this.props.lastLikes)):(0,r.Z)("p",{className:"pull-left hidden-xs"},void 0,ts(this.props.likes,this.props.lastLikes)):null}}class es extends Jt{render(){const e=(this.props.post.last_likes||[]).length>0;return this.props.post.acl.can_see_likes&&e?2===this.props.post.acl.can_see_likes?(0,r.Z)("button",{className:"btn btn-link btn-sm likes-compact pull-left visible-xs-block",onClick:this.onClick,type:"button"},void 0,zt||(zt=(0,r.Z)("span",{className:"material-icon"},void 0,"favorite")),this.props.likes):(0,r.Z)("p",{className:"likes-compact pull-left visible-xs-block"},void 0,Mt||(Mt=(0,r.Z)("span",{className:"material-icon"},void 0,"favorite")),this.props.likes):null}}function ts(e,t){const s=t.slice(0,3).map((e=>e.username));if(1==s.length)return interpolate(pgettext("post likes","%(user)s likes this."),{user:s[0]},!0);const a=e-s.length,i=s.slice(0,-1).join(", "),o=s.slice(-1)[0],n=interpolate(pgettext("post likes","%(users)s and %(last_user)s"),{users:i,last_user:o},!0);if(0===a)return interpolate(pgettext("post likes","%(users)s like this."),{users:n},!0);const r=npgettext("post likes","%(users)s and %(likes)s other user like this.","%(users)s and %(likes)s other users like this.",a);return interpolate(r,{users:s.join(", "),likes:a},!0)}class ss extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{ae.Z.open({mode:"REPLY",thread:this.props.thread,config:this.props.thread.api.editor,submit:this.props.thread.api.posts.index})}))}render(){return this.props.post.acl.can_reply?(0,r.Z)("button",{className:"btn btn-default btn-sm pull-right",type:"button",onClick:this.onClick},void 0,pgettext("post footer btn","Reply")):null}}class as extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{ae.Z.open({mode:"QUOTE",thread:this.props.thread,config:this.props.thread.api.editor,submit:this.props.thread.api.posts.index,context:{reply:this.props.post.id}})}))}render(){return this.props.post.acl.can_reply?(0,r.Z)("button",{className:"btn btn-default btn-sm pull-right",type:"button",onClick:this.onClick},void 0,pgettext("post footer btn","Quote")):null}}class is extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{ae.Z.open({mode:"EDIT",thread:this.props.thread,post:this.props.post,config:this.props.post.api.editor,submit:this.props.post.api.index})}))}render(){return this.props.post.acl.can_edit?(0,r.Z)("button",{className:"hidden-xs btn btn-default btn-sm pull-right",type:"button",onClick:this.onClick},void 0,pgettext("post footer btn","Edit")):null}}var os=class extends u.Z{constructor(e){super(e),(0,l.Z)(this,"onUrlChange",(e=>{this.changeValue("url",e.target.value)})),this.state={isLoading:!1,url:"",validators:{url:[]},errors:{}}}clean(){return!!this.state.url.trim().length||(f.Z.error(pgettext("post move modal","You have to enter link to the other thread.")),!1)}send(){return g.Z.post(this.props.thread.api.posts.move,{new_thread:this.state.url,posts:[this.props.post.id]})}handleSuccess(e){b.Z.dispatch(ze.r$(this.props.post,{isDeleted:!0})),Z.Z.hide(),f.Z.success(pgettext("post move modal","Selected post was moved to the other thread."))}handleError(e){400===e.status?f.Z.error(e.detail):f.Z.apiError(e)}render(){return(0,r.Z)("div",{className:"modal-dialog",role:"document"},void 0,(0,r.Z)("form",{onSubmit:this.handleSubmit},void 0,(0,r.Z)("div",{className:"modal-content"},void 0,Bt||(Bt=(0,r.Z)(ns,{})),(0,r.Z)("div",{className:"modal-body"},void 0,(0,r.Z)(h.Z,{for:"id_url",label:pgettext("post move modal field","Link to thread you want to move post to")},void 0,(0,r.Z)("input",{className:"form-control",disabled:this.state.isLoading,id:"id_url",onChange:this.onUrlChange,value:this.state.url}))),(0,r.Z)("div",{className:"modal-footer"},void 0,(0,r.Z)("button",{className:"btn btn-primary",disabled:this.state.isLoading},void 0,pgettext("post move modal btn","Move post"))))))}};function ns(e){return(0,r.Z)("div",{className:"modal-header"},void 0,(0,r.Z)("button",{"aria-label":pgettext("modal","Close"),className:"close","data-dismiss":"modal",type:"button"},void 0,qt||(qt=(0,r.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,r.Z)("h4",{className:"modal-title"},void 0,pgettext("post move modal title","Move post")))}function rs(e){return(0,r.Z)("div",{className:"modal-body post-changelog-diff"},void 0,(0,r.Z)("ul",{className:"list-unstyled"},void 0,e.diff.map(((e,t)=>(0,r.Z)(ls,{item:e},t)))))}function ls(e){return"?"===e.item[0]?null:(0,r.Z)("li",{className:ds(e.item)},void 0,e.item.substr(2))}function ds(e){let t="diff-item";return"-"===e[0]?t+=" diff-item-sub":"+"===e[0]&&(t+=" diff-item-add"),t}var cs,ps,us,hs,ms,vs=class extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"onClick",(()=>{this.props.revertEdit(this.props.edit.id)}))}render(){return this.props.canRevert?(0,r.Z)("div",{className:"modal-footer visible-xs-block"},void 0,(0,r.Z)(Ee.Z,{className:"btn-default btn-sm btn-block",disabled:this.props.disabled,onClick:this.onClick,title:pgettext("post revert btn","Revert post to state from before this edit.")},void 0,pgettext("post revert btn","Revert"))):null}},gs=class extends c().Component{constructor(){super(...arguments),(0,l.Z)(this,"goLast",(()=>{this.props.goToEdit()})),(0,l.Z)(this,"goForward",(()=>{this.props.goToEdit(this.props.edit.next)})),(0,l.Z)(this,"goBack",(()=>{this.props.goToEdit(this.props.edit.previous)})),(0,l.Z)(this,"revertEdit",(()=>{this.props.revertEdit(this.props.edit.id)}))}render(){return(0,r.Z)("div",{className:"modal-toolbar post-changelog-toolbar"},void 0,(0,r.Z)("div",{className:"row"},void 0,(0,r.Z)("div",{className:"col-xs-12 col-sm-4"},void 0,(0,r.Z)("div",{className:"row"},void 0,(0,r.Z)("div",{className:"col-xs-4"},void 0,(0,r.Z)(Zs,{disabled:this.props.disabled,edit:this.props.edit,onClick:this.goBack})),(0,r.Z)("div",{className:"col-xs-4"},void 0,(0,r.Z)(fs,{disabled:this.props.disabled,edit:this.props.edit,onClick:this.goForward})),(0,r.Z)("div",{className:"col-xs-4"},void 0,(0,r.Z)(bs,{disabled:this.props.disabled,edit:this.props.edit,onClick:this.goLast})))),(0,r.Z)("div",{className:"col-xs-12 col-sm-5 xs-margin-top-half post-change-label"},void 0,(0,r.Z)(Ns,{edit:this.props.edit})),(0,r.Z)(_s,{canRevert:this.props.canRevert,disabled:this.props.disabled,onClick:this.revertEdit})))}};function Zs(e){return(0,r.Z)(Ee.Z,{className:"btn-default btn-block btn-icon btn-sm",disabled:e.disabled||!e.edit.previous,onClick:e.onClick,title:pgettext("post history modal btn","See previous change")},void 0,cs||(cs=(0,r.Z)("span",{className:"material-icon"},void 0,"chevron_left")))}function fs(e){return(0,r.Z)(Ee.Z,{className:"btn-default btn-block btn-icon btn-sm",disabled:e.disabled||!e.edit.next,onClick:e.onClick,title:pgettext("post history modal btn","See next change")},void 0,ps||(ps=(0,r.Z)("span",{className:"material-icon"},void 0,"chevron_right")))}function bs(e){return(0,r.Z)(Ee.Z,{className:"btn-default btn-block btn-icon btn-sm",disabled:e.disabled||!e.edit.next,onClick:e.onClick,title:pgettext("post history modal btn","See previous change")},void 0,us||(us=(0,r.Z)("span",{className:"material-icon"},void 0,"last_page")))}function _s(e){return e.canRevert?(0,r.Z)("div",{className:"col-sm-3 hidden-xs"},void 0,(0,r.Z)(Ee.Z,{className:"btn-default btn-sm btn-block",disabled:e.disabled,onClick:e.onClick,title:pgettext("post history modal btn","Revert post to state from before this edit.")},void 0,pgettext("post history modal btn","Revert"))):null}function Ns(e){let t=null;t=e.edit.url.editor?interpolate('%(user)s',{url:(0,ce.Z)(e.edit.url.editor),user:(0,ce.Z)(e.edit.editor_name)},!0):interpolate('%(user)s',{user:(0,ce.Z)(e.edit.editor_name)},!0);const s=interpolate('%(relative)s',{absolute:(0,ce.Z)(e.edit.edited_on.format("LLL")),relative:(0,ce.Z)(e.edit.edited_on.fromNow())},!0),a=interpolate((0,ce.Z)(pgettext("post history modal","By %(edited_by)s %(edited_on)s.")),{edited_by:t,edited_on:s},!0);return(0,r.Z)("p",{dangerouslySetInnerHTML:{__html:a}})}function xs(e){return Object.assign({},e,{edited_on:j()(e.edited_on)})}var ys=class extends c().Component{constructor(e){var t;super(e),t=this,(0,l.Z)(this,"goToEdit",(function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;t.setState({isBusy:!0});let s=t.props.post.api.edits;null!==e&&(s+="?edit="+e),g.Z.get(s).then((e=>{t.setState({isReady:!0,isBusy:!1,edit:xs(e)})}),(e=>{t.setState({isReady:!0,isBusy:!1,error:e.detail})}))})),(0,l.Z)(this,"revertEdit",(e=>{if(this.state.isBusy)return;if(!window.confirm(pgettext("post revert","Are you sure you with to revert this post to the state from before this edit?")))return;this.setState({isBusy:!0});const t=this.props.post.api.edits+"?edit="+e;g.Z.post(t).then((e=>{const t=ze.ZB(e);b.Z.dispatch(ze.r$(e,t)),f.Z.success(pgettext("post revert","Post has been reverted to previous state.")),Z.Z.hide()}),(e=>{f.Z.apiError(e),this.setState({isBusy:!1})}))})),this.state={isReady:!1,isBusy:!0,canRevert:e.post.acl.can_edit,error:null,edit:null}}componentDidMount(){this.goToEdit()}render(){return this.state.error?(0,r.Z)(ws,{className:"modal-dialog modal-message"},void 0,(0,r.Z)(V.Z,{message:this.state.error})):this.state.isReady?(0,r.Z)(ws,{},void 0,(0,r.Z)(gs,{canRevert:this.state.canRevert,disabled:this.state.isBusy,edit:this.state.edit,goToEdit:this.goToEdit,revertEdit:this.revertEdit}),(0,r.Z)(rs,{diff:this.state.edit.diff}),(0,r.Z)(vs,{canRevert:this.state.canRevert,disabled:this.state.isBusy,edit:this.state.edit,revertEdit:this.revertEdit})):hs||(hs=(0,r.Z)(ws,{},void 0,(0,r.Z)(G.Z,{})))}};function ws(e){return(0,r.Z)("div",{className:e.className||"modal-dialog",role:"document"},void 0,(0,r.Z)("div",{className:"modal-content"},void 0,(0,r.Z)("div",{className:"modal-header"},void 0,(0,r.Z)("button",{"aria-label":pgettext("modal","Close"),className:"close","data-dismiss":"modal",type:"button"},void 0,ms||(ms=(0,r.Z)("span",{"aria-hidden":"true"},void 0,"×"))),(0,r.Z)("h4",{className:"modal-title"},void 0,pgettext("post history modal title","Post edits history"))),e.children))}var ks,Cs,Ss,Es,Ts,Ls,Ps,Os,Is,As,Rs,Ds,js,Us,zs,Ms,Bs,qs,Fs,Hs,Ys=s(57026),Vs=s(60471),Gs=s(55210);function $s(e){return c().createElement(Ws,(0,p.Z)({},e,{Form:Qs}))}class Ws extends c().Component{constructor(e){super(e),this.state={isLoaded:!1,isError:!1,categories:[]}}componentDidMount(){g.Z.get(misago.get("THREAD_EDITOR_API")).then((e=>{const t=e.map((e=>Object.assign(e,{disabled:!1===e.post,label:e.name,value:e.id,post:e.post})));this.setState({isLoaded:!0,categories:t})}),(e=>{this.setState({isError:e.detail})}))}render(){return this.state.isError?(0,r.Z)(Ks,{message:this.state.isError}):this.state.isLoaded?c().createElement(Qs,(0,p.Z)({},this.props,{categories:this.state.categories})):ks||(ks=(0,r.Z)(Xs,{}))}}class Qs extends u.Z{constructor(e){super(e),(0,l.Z)(this,"onCategoryChange",(e=>{const t=e.target.value,s={category:t};this.acl[t].can_pin_threads
    \n \n )\n }\n}\n\nfunction ariaProps(isOpen) {\n return {\n \"aria-haspopup\": \"true\",\n \"aria-expanded\": isOpen ? \"true\" : \"false\",\n }\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function DropdownDivider({ className }) {\n return
  • \n}\n","import React from \"react\"\n\nexport default function DropdownFooter({ children, listItem }) {\n if (listItem) {\n return
  • {children}
  • \n }\n\n return
    {children}
    \n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function DropdownHeader({ className, children }) {\n return (\n
    {children}
    \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function DropdownMenuItem({ className, children }) {\n return (\n
  • {children}
  • \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function DropdownPills({ className, children }) {\n return (\n
    {children}
    \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function DropdownSubheader({ className, children }) {\n return (\n
  • {children}
  • \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst FlexRow = ({ children, className }) => (\n
    {children}
    \n)\n\nexport default FlexRow\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst FlexRowCol = ({ children, className, shrink }) => (\n \n {children}\n \n)\n\nexport default FlexRowCol\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst FlexRowSection = ({ auto, children, className }) => (\n \n {children}\n \n)\n\nexport default FlexRowSection\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function ListGroup({ className, children }) {\n return
      {children}
    \n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function ListGroupItem({ className, children }) {\n return (\n
  • {children}
  • \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport ListGroupItem from \"./ListGroupItem\"\n\nexport default function ListGroupEmpty({ className, icon, message }) {\n return (\n \n {!!icon && (\n
    \n {icon}\n
    \n )}\n

    {message}

    \n
    \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport ListGroupItem from \"./ListGroupItem\"\n\nexport default function ListGroupError({ className, icon, message, detail }) {\n return (\n \n {!!icon && (\n
    \n {icon}\n
    \n )}\n

    {message}

    \n {!!detail &&

    {detail}

    }\n
    \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport ListGroupItem from \"./ListGroupItem\"\n\nexport default function ListGroupLoading({ className, message }) {\n return (\n \n

    {message}

    \n
    \n
    \n
    \n
    \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport ListGroupItem from \"./ListGroupItem\"\n\nexport default function ListGroupMessage({ className, icon, message, detail }) {\n return (\n \n {!!icon && (\n
    \n {icon}\n
    \n )}\n

    {message}

    \n {!!detail &&

    {detail}

    }\n
    \n )\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport { updateAuthenticatedUser } from \"../../reducers/auth\"\nimport { ApiFetch } from \"../Api\"\n\nfunction NotificationsFetch({\n children,\n filter,\n query,\n dispatch,\n unreadNotifications,\n disabled,\n}) {\n return (\n {\n if (data.unreadNotifications != unreadNotifications) {\n dispatch(\n updateAuthenticatedUser({\n unreadNotifications: data.unreadNotifications,\n })\n )\n }\n }}\n >\n {({ data, loading, error, refetch }) => {\n return children({ data, loading, error, refetch })\n }}\n \n )\n}\n\nfunction getApiUrl(filter, query) {\n let api = misago.get(\"NOTIFICATIONS_API\") + \"?limit=30\"\n api += \"&filter=\" + filter\n\n if (query) {\n if (query.after) {\n api += \"&after=\" + query.after\n }\n if (query.before) {\n api += \"&before=\" + query.before\n }\n }\n\n return api\n}\n\nfunction selectState({ auth }) {\n if (!auth.user) {\n return { unreadNotifications: null }\n }\n\n return {\n unreadNotifications: auth.user.unreadNotifications,\n }\n}\n\nconst NotificationsFetchConnected = connect(selectState)(NotificationsFetch)\n\nexport default NotificationsFetchConnected\n","import NotificationsFetch from \"./NotificationsFetch\"\n\nexport default NotificationsFetch\n","import React from \"react\"\nimport { ListGroupEmpty } from \"../ListGroup\"\n\nexport default function NotificationsListEmpty({ filter }) {\n return (\n \n )\n}\n\nfunction emptyMessage(filter) {\n if (filter === \"read\") {\n return pgettext(\n \"notifications list\",\n \"You don't have any read notifications.\"\n )\n } else if (filter === \"unread\") {\n return pgettext(\n \"notifications list\",\n \"You don't have any unread notifications.\"\n )\n }\n\n return pgettext(\"notifications list\", \"You don't have any notifications.\")\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { ListGroup } from \"../ListGroup\"\n\nexport default function NotificationsListGroup({ className, children }) {\n return (\n
    \n {children}\n
    \n )\n}\n","import React from \"react\"\nimport Avatar from \"../avatar\"\n\nexport default function NotificationsListItemActor({ notification }) {\n if (!!notification.actor) {\n return (\n \n \n \n )\n }\n\n return (\n \n \n
    \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function NotificationsListItemMessage({ notification }) {\n return (\n \n )\n}\n","import React from \"react\"\n\nexport default function NotificationsListItemReadStatus({ notification }) {\n if (notification.isRead) {\n return (\n \n \n \n )\n }\n\n return (\n \n \n \n )\n}\n","import React from \"react\"\nimport Timestamp from \"../Timestamp\"\n\nexport default function NotificationsListItemTimestamp({ notification }) {\n return (\n
    \n \n
    \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { ListGroupItem } from \"../ListGroup\"\nimport NotificationsListItemActor from \"./NotificationsListItemActor\"\nimport NotificationsListItemMessage from \"./NotificationsListItemMessage\"\nimport NotificationsListItemReadStatus from \"./NotificationsListItemReadStatus\"\nimport NotificationsListItemTimestamp from \"./NotificationsListItemTimestamp\"\n\nexport default function NotificationsListItem({ notification }) {\n return (\n \n
    \n
    \n \n
    \n
    \n \n
    \n
    \n
    \n
    \n \n
    \n
    \n \n
    \n
    \n \n )\n}\n","import React from \"react\"\nimport NotificationsListEmpty from \"./NotificationsListEmpty\"\nimport NotificationsListGroup from \"./NotificationsListGroup\"\nimport NotificationsListItem from \"./NotificationsListItem\"\n\nexport default function NotificationsList({ filter, items }) {\n return (\n 0\n ? \"notifications-list-ready\"\n : \"notifications-list-pending\"\n }\n >\n {items.length === 0 && }\n {items.map((notification) => (\n \n ))}\n \n )\n}\n","import React from \"react\"\nimport { ListGroupError } from \"../ListGroup\"\nimport NotificationsListGroup from \"./NotificationsListGroup\"\n\nexport default function NotificationsListError({ error }) {\n const detail = errorDetail(error)\n\n return (\n \n \n \n )\n}\n\nfunction errorDetail(error) {\n if (error.status === 0) {\n return gettext(\n \"Check your internet connection and try refreshing the site.\"\n )\n }\n\n if (error.data && error.data.detail) {\n return error.data.detail\n }\n}\n","import React from \"react\"\nimport { ListGroupLoading } from \"../ListGroup\"\nimport NotificationsListGroup from \"./NotificationsListGroup\"\n\nexport default function NotificationsListLoading() {\n return (\n \n \n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { connect } from \"react-redux\"\nimport { close } from \"../../reducers/overlay\"\n\nconst BODY_CLASS = \"has-overlay\"\n\nclass Overlay extends React.Component {\n constructor(props) {\n super(props)\n\n this.scrollOrigin = null\n }\n\n componentDidUpdate(prevProps) {\n if (prevProps.open !== this.props.open) {\n if (this.props.open) {\n this.scrollOrigin = window.pageYOffset\n document.body.classList.add(BODY_CLASS)\n if (this.props.onOpen) {\n this.props.onOpen()\n }\n } else {\n document.body.classList.remove(BODY_CLASS)\n window.scrollTo(0, this.scrollOrigin)\n this.scrollOrigin = null\n }\n }\n }\n\n closeOnNavigation = (event) => {\n if (event.target.closest(\"a\")) {\n this.props.dispatch(close())\n }\n }\n\n render() {\n return (\n \n {this.props.children}\n \n )\n }\n}\n\nconst OverlayConnected = connect()(Overlay)\n\nexport default OverlayConnected\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport { close } from \"../../reducers/overlay\"\n\nexport function OverlayHeader({ children, dispatch }) {\n return (\n
    \n
    {children}
    \n dispatch(close())}\n >\n close\n \n
    \n )\n}\n\nconst OverlayHeaderConnected = connect()(OverlayHeader)\n\nexport default OverlayHeaderConnected\n","import React from \"react\"\n\nconst PageContainer = ({ children }) => (\n
    {children}
    \n)\n\nexport default PageContainer\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst PageHeader = ({ children, className, styleName }) => (\n \n
    \n
    \n
    \n {children}\n
    \n
    \n
    \n)\n\nexport default PageHeader\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst PageHeaderBanner = ({ children, className, styleName }) => (\n \n
    \n
    {children}
    \n
    \n \n)\n\nexport default PageHeaderBanner\n","import React from \"react\"\n\nconst PageHeaderContainer = ({ children }) => (\n
    {children}
    \n)\n\nexport default PageHeaderContainer\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst PageHeaderDetails = ({ children, className }) => (\n
    {children}
    \n)\n\nexport default PageHeaderDetails\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst PageHeaderHTMLMessage = ({ className, message }) => (\n \n)\n\nexport default PageHeaderHTMLMessage\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst PageHeaderMessage = ({ children, className }) => (\n
    {children}
    \n)\n\nexport default PageHeaderMessage\n","import React from \"react\"\nimport PageHeader from \"./PageHeader\"\nimport PageHeaderBanner from \"./PageHeaderBanner\"\nimport PageHeaderContainer from \"./PageHeaderContainer\"\nimport PageHeaderDetails from \"./PageHeaderDetails\"\n\nconst PageHeaderPlain = ({ styleName, header, message }) => (\n \n \n \n

    {header}

    \n
    \n {message && (\n {message}\n )}\n
    \n
    \n)\n\nexport default PageHeaderPlain\n","import React from \"react\"\nimport zxcvbn from \"misago/services/zxcvbn\"\n\nexport const STYLES = [\n \"progress-bar-danger\",\n \"progress-bar-warning\",\n \"progress-bar-warning\",\n \"progress-bar-primary\",\n \"progress-bar-success\",\n]\n\nexport const LABELS = [\n pgettext(\"password strength indicator\", \"Entered password is very weak.\"),\n pgettext(\"password strength indicator\", \"Entered password is weak.\"),\n pgettext(\"password strength indicator\", \"Entered password is average.\"),\n pgettext(\"password strength indicator\", \"Entered password is strong.\"),\n pgettext(\"password strength indicator\", \"Entered password is very strong.\"),\n]\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this._score = 0\n this._password = null\n this._inputs = []\n\n this.state = {\n loaded: false,\n }\n }\n\n componentDidMount() {\n zxcvbn.load().then(() => {\n this.setState({ loaded: true })\n })\n }\n\n getScore(password, inputs) {\n let cacheStale = false\n\n if (password !== this._password) {\n cacheStale = true\n }\n\n if (inputs.length !== this._inputs.length) {\n cacheStale = true\n } else {\n inputs.map((value, i) => {\n if (value.trim() !== this._inputs[i]) {\n cacheStale = true\n }\n })\n }\n\n if (cacheStale) {\n this._score = zxcvbn.scorePassword(password, inputs)\n this._password = password\n this._inputs = inputs.map(function (value) {\n return value.trim()\n })\n }\n\n return this._score\n }\n\n render() {\n if (!this.state.loaded) return null\n\n let score = this.getScore(this.props.password, this.props.inputs)\n\n return (\n
    \n
    \n \n {LABELS[score]}\n
    \n
    \n

    {LABELS[score]}

    \n \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport PasswordStrength from \"misago/components/password-strength\"\nimport RegisterLegalFootnote from \"misago/components/RegisterLegalFootnote\"\nimport StartSocialAuth from \"misago/components/StartSocialAuth\"\nimport misago from \"misago\"\nimport ajax from \"misago/services/ajax\"\nimport auth from \"misago/services/auth\"\nimport captcha from \"misago/services/captcha\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport showBannedPage from \"misago/utils/banned-page\"\nimport * as validators from \"misago/utils/validators\"\n\nexport class RegisterForm extends Form {\n constructor(props) {\n super(props)\n\n const { username, password } = this.props.criteria\n\n let passwordMinLength = 0\n password.forEach((item) => {\n if (item.name === \"MinimumLengthValidator\") {\n passwordMinLength = item.min_length\n }\n })\n\n const formValidators = {\n username: [\n validators.usernameContent(),\n validators.usernameMinLength(username.min_length),\n validators.usernameMaxLength(username.max_length),\n ],\n email: [validators.email()],\n password: [validators.passwordMinLength(passwordMinLength)],\n captcha: captcha.validator(),\n }\n\n if (!!misago.get(\"TERMS_OF_SERVICE_ID\")) {\n formValidators.termsOfService = [validators.requiredTermsOfService()]\n }\n\n if (!!misago.get(\"PRIVACY_POLICY_ID\")) {\n formValidators.privacyPolicy = [validators.requiredPrivacyPolicy()]\n }\n\n this.state = {\n isLoading: false,\n\n username: \"\",\n email: \"\",\n password: \"\",\n captcha: \"\",\n\n termsOfService: null,\n privacyPolicy: null,\n\n validators: formValidators,\n errors: {},\n }\n }\n\n clean() {\n if (this.isValid()) {\n return true\n } else {\n snackbar.error(gettext(\"Form contains errors.\"))\n this.setState({\n errors: this.validate(),\n })\n return false\n }\n }\n\n send() {\n return ajax.post(misago.get(\"USERS_API\"), {\n username: this.state.username,\n email: this.state.email,\n password: this.state.password,\n captcha: this.state.captcha,\n terms_of_service: this.state.termsOfService,\n privacy_policy: this.state.privacyPolicy,\n })\n }\n\n handleSuccess(apiResponse) {\n this.props.callback(apiResponse)\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n this.setState({\n errors: Object.assign({}, this.state.errors, rejection),\n })\n\n if (rejection.__all__ && rejection.__all__.length > 0) {\n snackbar.error(rejection.__all__[0])\n } else {\n snackbar.error(gettext(\"Form contains errors.\"))\n }\n } else if (rejection.status === 403 && rejection.ban) {\n showBannedPage(rejection.ban)\n modal.hide()\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n handlePrivacyPolicyChange = (event) => {\n const value = event.target.value\n this.handleToggleAgreement(\"privacyPolicy\", value)\n }\n\n handleTermsOfServiceChange = (event) => {\n const value = event.target.value\n this.handleToggleAgreement(\"termsOfService\", value)\n }\n\n handleToggleAgreement = (agreement, value) => {\n this.setState((prevState, props) => {\n if (prevState[agreement] === null) {\n const errors = { ...prevState.errors, [agreement]: null }\n return { errors, [agreement]: value }\n }\n\n const validator = this.state.validators[agreement][0]\n const errors = { ...prevState.errors, [agreement]: [validator(null)] }\n return { errors, [agreement]: null }\n })\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"register modal title\", \"Register\")}\n

    \n
    \n \n \n \n
    \n \n\n \n \n \n\n \n \n \n\n \n }\n >\n \n \n\n {captcha.component({\n form: this,\n })}\n\n \n
    \n
    \n \n {pgettext(\"register modal btn\", \"Cancel\")}\n \n \n
    \n \n
    \n
    \n )\n }\n}\n\nexport class RegisterComplete extends React.Component {\n getLead() {\n if (this.props.activation === \"user\") {\n return pgettext(\n \"register modal\",\n \"%(username)s, your account has been created but you need to activate it before you will be able to sign in.\"\n )\n } else if (this.props.activation === \"admin\") {\n return pgettext(\n \"register modal\",\n \"%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in.\"\n )\n }\n }\n\n getSubscript() {\n if (this.props.activation === \"user\") {\n return pgettext(\n \"register modal\",\n \"We have sent an e-mail to %(email)s with link that you have to click to activate your account.\"\n )\n } else if (this.props.activation === \"admin\") {\n return pgettext(\n \"register modal\",\n \"We will send an e-mail to %(email)s when this takes place.\"\n )\n }\n }\n\n render() {\n return (\n \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"register modal title\", \"Registration complete\")}\n

    \n
    \n
    \n
    \n info_outline\n
    \n
    \n

    \n {interpolate(\n this.getLead(),\n { username: this.props.username },\n true\n )}\n

    \n

    \n {interpolate(\n this.getSubscript(),\n { email: this.props.email },\n true\n )}\n

    \n \n {pgettext(\"register modal dismiss\", \"Ok\")}\n \n
    \n
    \n
    \n \n )\n }\n}\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n complete: false,\n }\n }\n\n completeRegistration = (apiResponse) => {\n if (apiResponse.activation === \"active\") {\n modal.hide()\n auth.signIn(apiResponse)\n } else {\n this.setState({\n complete: apiResponse,\n })\n }\n }\n\n render() {\n if (this.state.complete) {\n return (\n \n )\n }\n\n return \n }\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport ajax from \"../../services/ajax\"\nimport captcha from \"../../services/captcha\"\nimport modal from \"../../services/modal\"\nimport snackbar from \"../../services/snackbar\"\nimport Loader from \"../loader\"\nimport RegisterForm from \"../register.js\"\n\nexport default class RegisterButton extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n isLoaded: false,\n\n criteria: null,\n }\n }\n\n showRegisterForm = () => {\n if (this.props.onClick) {\n this.props.onClick()\n }\n\n if (misago.get(\"SETTINGS\").account_activation === \"closed\") {\n snackbar.info(\n pgettext(\"register form\", \"New registrations are currently disabled.\")\n )\n } else if (this.state.isLoaded) {\n modal.show()\n } else {\n this.setState({ isLoading: true })\n\n Promise.all([\n captcha.load(),\n ajax.get(misago.get(\"AUTH_CRITERIA_API\")),\n ]).then(\n (result) => {\n this.setState({\n isLoading: false,\n isLoaded: true,\n criteria: result[1],\n })\n\n modal.show()\n },\n () => {\n this.setState({ isLoading: false })\n\n snackbar.error(\n pgettext(\n \"register form\",\n \"Registration is currently unavailable due to an error.\"\n )\n )\n }\n )\n }\n }\n\n render() {\n return (\n \n {pgettext(\"cta\", \"Register\")}\n {this.state.isLoading ? : null}\n \n )\n }\n}\n","import RegisterButton from \"./RegisterButton\"\n\nexport default RegisterButton\n","import React from \"react\"\nimport misago from \"misago\"\nimport escapeHtml from \"misago/utils/escape-html\"\n\nconst AGREEMENT_URL = '%(agreement)s'\n\nconst RegisterLegalFootnote = (props) => {\n const {\n errors,\n privacyPolicy,\n termsOfService,\n onPrivacyPolicyChange,\n onTermsOfServiceChange,\n } = props\n\n const termsOfServiceId = misago.get(\"TERMS_OF_SERVICE_ID\")\n const termsOfServiceUrl = misago.get(\"TERMS_OF_SERVICE_URL\")\n\n const privacyPolicyId = misago.get(\"PRIVACY_POLICY_ID\")\n const privacyPolicyUrl = misago.get(\"PRIVACY_POLICY_URL\")\n\n if (!termsOfServiceId && !privacyPolicyId) return null\n\n return (\n
    \n \n \n
    \n )\n}\n\nconst LegalAgreement = (props) => {\n const { agreement, checked, errors, url, value, onChange } = props\n\n if (!url) return null\n\n const agreementHtml = interpolate(\n AGREEMENT_URL,\n { agreement: escapeHtml(agreement), url: escapeHtml(url) },\n true\n )\n const label = interpolate(\n pgettext(\n \"register form agreement prompt\",\n \"I have read and accept %(agreement)s.\"\n ),\n { agreement: agreementHtml },\n true\n )\n\n return (\n
    \n \n {errors &&\n errors.map((error, i) => (\n
    \n {error}\n
    \n ))}\n
    \n )\n}\n\nexport default RegisterLegalFootnote\n","import React from \"react\"\nimport { ListGroup } from \"../ListGroup\"\n\nexport default function SearchResultsList({ children }) {\n return {children}\n}\n","import React from \"react\"\nimport { ListGroupMessage } from \"../ListGroup\"\nimport SearchResultsList from \"./SearchResultsList\"\n\nexport default function SearchMessage() {\n return (\n \n \n \n )\n}\n","import React from \"react\"\nimport { ListGroupItem } from \"../ListGroup\"\nimport Timestamp from \"../Timestamp\"\n\nexport default function SearchResultPost({ post }) {\n return (\n \n \n
    \n
    {post.thread.title}
    \n \n
      \n
    • \n {post.category.name}\n
    • \n
    • {post.poster ? post.poster.username : post.poster_name}
    • \n
    • \n \n
    • \n
    \n
    \n
    \n
    \n )\n}\n","import React from \"react\"\nimport Avatar from \"../avatar\"\nimport { ListGroupItem } from \"../ListGroup\"\nimport Timestamp from \"../Timestamp\"\n\nexport default function SearchResultUser({ user }) {\n const title = user.title || user.rank.title\n\n return (\n \n \n \n
    \n
    {user.username}
    \n
      \n {!!title && (\n
    • \n {title}\n
    • \n )}\n
    • {user.rank.name}
    • \n
    • \n \n
    • \n
    \n
    \n
    \n
    \n )\n}\n","import React from \"react\"\nimport { ListGroupItem } from \"../ListGroup\"\nimport SearchResultsList from \"./SearchResultsList\"\nimport SearchResultPost from \"./SearchResultPost\"\nimport SearchResultUser from \"./SearchResultUser\"\n\nexport default function SearchResults({ query, results }) {\n const threads = results[0]\n const users = results[1]\n\n const { count } = threads.results\n\n return (\n \n {users.results.results.map((user) => (\n \n ))}\n {threads.results.results.map((post) => (\n \n ))}\n {count > 0 && (\n \n \n {npgettext(\n \"search results list\",\n \"See all %(count)s result.\",\n \"See all %(count)s results.\",\n threads.results.count\n ).replace(\"%(count)s\", threads.results.count)}\n \n \n )}\n \n )\n}\n","import React from \"react\"\nimport { ListGroupEmpty } from \"../ListGroup\"\nimport SearchResultsList from \"./SearchResultsList\"\n\nexport default function SearchResultsEmpty() {\n return (\n \n \n \n )\n}\n","import React from \"react\"\nimport { ListGroupError } from \"../ListGroup\"\nimport SearchResultsList from \"./SearchResultsList\"\n\nexport default function SearchResultsError({ error }) {\n return (\n \n \n \n )\n}\n\nfunction errorDetail(error) {\n if (error.status === 0) {\n return gettext(\n \"Check your internet connection and try refreshing the site.\"\n )\n }\n\n if (error.data && error.data.detail) {\n return error.data.detail\n }\n}\n","import React from \"react\"\nimport { ListGroupLoading } from \"../ListGroup\"\nimport SearchResultsList from \"./SearchResultsList\"\n\nexport default function SearchResultsLoading() {\n return (\n \n \n \n )\n}\n","import React from \"react\"\nimport { ApiFetch } from \"../Api\"\nimport SearchMessage from \"./SearchMessage\"\nimport SearchResults from \"./SearchResults\"\nimport SearchResultsEmpty from \"./SearchResultsEmpty\"\nimport SearchResultsError from \"./SearchResultsError\"\nimport SearchResultsLoading from \"./SearchResultsLoading\"\n\nconst DEBOUNCE = 750\nconst CACHE = {}\n\nexport default class SearchFetch extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n query: this.props.query.trim(),\n }\n\n this.debounce = null\n }\n\n componentDidUpdate() {\n const query = this.props.query.trim()\n\n if (this.state.query != query) {\n if (this.debounce) {\n window.clearTimeout(this.debounce)\n }\n\n this.debounce = window.setTimeout(() => {\n this.setState({ query })\n }, DEBOUNCE)\n }\n }\n\n componentWillUnmount() {\n if (this.debounce) {\n window.clearTimeout(this.debounce)\n }\n }\n\n render() {\n return (\n \n {({ data, loading, error }) => {\n if (this.state.query.length < 3) {\n return \n }\n\n if (loading) {\n return \n }\n\n if (error) {\n return \n }\n\n if (isResultEmpty(data)) {\n return \n }\n\n if (data !== null) {\n return \n }\n\n return null\n }}\n \n )\n }\n}\n\nfunction getSearchUrl(query) {\n return misago.get(\"SEARCH_API\") + \"?q=\" + encodeURIComponent(query)\n}\n\nfunction isResultEmpty(results) {\n if (results === null) {\n return true\n }\n\n let resultsCount = 0\n results.forEach((result) => {\n resultsCount += result.results.count\n })\n return resultsCount === 0\n}\n","import React from \"react\"\n\nexport default function SearchInput({ query, setQuery }) {\n return (\n
    \n setQuery(event.target.value)}\n />\n
    \n )\n}\n","import React from \"react\"\n\nexport default class SearchQuery extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n query: \"\",\n }\n }\n\n setQuery = (query) => {\n this.setState({ query })\n }\n\n render() {\n return this.props.children({\n query: this.state.query,\n setQuery: this.setQuery,\n })\n }\n}\n","import React from \"react\"\nimport SearchFetch from \"./SearchFetch\"\nimport SearchInput from \"./SearchInput\"\nimport SearchQuery from \"./SearchQuery\"\n\nexport default function SearchDropdown() {\n return (\n \n {({ query, setQuery }) => {\n return (\n
    \n \n \n
    \n )\n }}\n
    \n )\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport { Overlay, OverlayHeader } from \"../Overlay\"\nimport SearchFetch from \"./SearchFetch\"\nimport SearchInput from \"./SearchInput\"\nimport SearchQuery from \"./SearchQuery\"\n\nfunction SearchOverlay({ open }) {\n return (\n {\n window.setTimeout(() => {\n document.querySelector(\"#search-mount .form-control-search\").focus()\n }, 0)\n }}\n >\n {pgettext(\"cta\", \"Search\")}\n \n {({ query, setQuery }) => {\n return (\n
    \n \n
    \n \n
    \n
    \n )\n }}\n
    \n \n )\n}\n\nfunction select(state) {\n return { open: state.overlay.search }\n}\n\nconst SearchOverlayConnected = connect(select)(SearchOverlay)\n\nexport default SearchOverlayConnected\n","import SignInButton from \"./SignInButton\"\n\nexport default SignInButton\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport modal from \"../../services/modal\"\nimport SignInModal from \"../sign-in\"\n\nexport default function SignInButton({ block, className, onClick }) {\n const settings = misago.get(\"SETTINGS\")\n\n if (settings.DELEGATE_AUTH) {\n return (\n \n {pgettext(\"cta\", \"Sign in\")}\n \n )\n }\n\n return (\n {\n if (onClick) {\n onClick()\n }\n\n modal.show()\n }}\n >\n {pgettext(\"cta\", \"Sign in\")}\n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { connect } from \"react-redux\"\nimport {\n DropdownDivider,\n DropdownHeader,\n DropdownMenuItem,\n DropdownPills,\n DropdownSubheader,\n} from \"../Dropdown\"\nimport RegisterButton from \"../RegisterButton\"\nimport SignInButton from \"../SignInButton\"\n\nfunction SiteNavMenu({ isAnonymous, close, dropdown, overlay }) {\n const baseUrl = misago.get(\"MISAGO_PATH\")\n const settings = misago.get(\"SETTINGS\")\n const extraItems = misago.get(\"extraMenuItems\")\n const extraFooterItems = misago.get(\"extraFooterItems\")\n const categories = misago.get(\"categoriesMap\")\n const users = misago.get(\"usersLists\")\n const authDelegated = settings.enable_oauth2_client\n\n const topNav = []\n if (misago.get(\"THREADS_ON_INDEX\")) {\n topNav.push({ title: pgettext(\"site nav\", \"Threads\"), url: baseUrl })\n topNav.push({\n title: pgettext(\"site nav\", \"Categories\"),\n url: baseUrl + \"categories/\",\n })\n } else {\n topNav.push({ title: pgettext(\"site nav\", \"Categories\"), url: baseUrl })\n topNav.push({\n title: pgettext(\"site nav\", \"Threads\"),\n url: baseUrl + \"threads/\",\n })\n }\n\n topNav.push({\n title: pgettext(\"site nav\", \"Search\"),\n url: baseUrl + \"search/\",\n })\n\n const footerNav = []\n\n const tosTitle = misago.get(\"TERMS_OF_SERVICE_TITLE\")\n const tosUrl = misago.get(\"TERMS_OF_SERVICE_URL\")\n if (tosTitle && tosUrl) {\n footerNav.push({\n title: tosTitle,\n url: tosUrl,\n })\n }\n\n const privacyTitle = misago.get(\"PRIVACY_POLICY_TITLE\")\n const privacyUrl = misago.get(\"PRIVACY_POLICY_URL\")\n if (privacyTitle && privacyUrl) {\n footerNav.push({\n title: privacyTitle,\n url: privacyUrl,\n })\n }\n\n return (\n \n {isAnonymous && (\n \n {pgettext(\"cta\", \"You are not signed in\")}\n \n )}\n {isAnonymous && (\n \n \n {!authDelegated && }\n \n )}\n {settings.forum_name}\n {topNav.map((item) => (\n \n {item.title}\n \n ))}\n {extraItems.map((item, index) => (\n \n \n {item.title}\n \n \n ))}\n {!!users.length && }\n {!!users.length && (\n \n {pgettext(\"site nav section\", \"Users\")}\n \n )}\n {users.map((item) => (\n \n {item.name}\n \n ))}\n \n \n {pgettext(\"site nav section\", \"Categories\")}\n \n {categories.map((category) => (\n \n \n {category.name}\n \n {category.shortName || category.name}\n \n \n \n ))}\n {(!!footerNav.length || !!extraFooterItems.length) && (\n \n )}\n {(!!footerNav.length || !!extraFooterItems.length) && (\n \n {pgettext(\"site nav section\", \"Footer\")}\n \n )}\n {extraFooterItems.map((item, index) => (\n \n \n {item.title}\n \n \n ))}\n {footerNav.map((item) => (\n \n {item.title}\n \n ))}\n \n )\n}\n\nfunction select(state) {\n return {\n isAnonymous: !state.auth.user.id,\n }\n}\n\nconst SiteNavMenuConnected = connect(select)(SiteNavMenu)\n\nexport default SiteNavMenuConnected\n","import React from \"react\"\nimport SiteNavMenu from \"./SiteNavMenu\"\n\nexport default function SiteNavDropdown({ close }) {\n return \n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport { close } from \"../../reducers/overlay\"\nimport { Overlay, OverlayHeader } from \"../Overlay\"\nimport SiteNavMenu from \"./SiteNavMenu\"\n\nexport function SiteNavOverlay({ dispatch, isOpen }) {\n return (\n \n {pgettext(\"site nav title\", \"Menu\")}\n dispatch(close())} overlay />\n \n )\n}\n\nfunction select(state) {\n return {\n isOpen: state.overlay.siteNav,\n }\n}\n\nconst SiteNavOverlayConnected = connect(select)(SiteNavOverlay)\n\nexport default SiteNavOverlayConnected\n","import React from \"react\"\nimport misago from \"misago\"\n\nconst StartSocialAuth = (props) => {\n const { buttonClassName, buttonLabel, formLabel, header, labelClassName } =\n props\n const socialAuth = misago.get(\"SOCIAL_AUTH\")\n\n if (socialAuth.length === 0) return null\n\n return (\n
    \n \n
    \n {socialAuth.map(({ pk, name, button_text, button_color, url }) => {\n const className = \"btn btn-block btn-default btn-social-\" + pk\n const style = button_color ? { color: button_color } : null\n const finalButtonLabel =\n button_text || interpolate(buttonLabel, { site: name }, true)\n\n return (\n \n )\n })}\n
    \n
    \n \n
    \n )\n}\n\nconst FormHeader = ({ className, text }) => {\n if (!text) return null\n return
    {text}
    \n}\n\nexport default StartSocialAuth\n","import React from \"react\"\n\nconst ThreadFlags = ({ thread }) => (\n
      \n {thread.weight == 2 && (\n \n bookmark\n \n )}\n {thread.weight == 1 && (\n \n bookmark_outline\n \n )}\n {thread.best_answer && (\n \n check_circle\n \n )}\n {thread.has_poll && (\n
    • \n poll\n
    • \n )}\n {(thread.is_unapproved || thread.has_unapproved_posts) && (\n \n visibility\n \n )}\n {thread.is_closed && (\n \n lock\n \n )}\n {thread.is_hidden && (\n \n visibility_off\n \n )}\n
    \n)\n\nexport default ThreadFlags\n","import React from \"react\"\n\nconst ThreadReplies = ({ thread }) => (\n \n chat_bubble_outline\n {thread.replies > 980\n ? Math.round(thread.replies / 1000) + \"K\"\n : thread.replies}\n \n)\n\nexport default ThreadReplies\n","export const locale = window.misago_locale || \"en-us\"\n\nexport const momentAgo = pgettext(\"time ago\", \"moment ago\")\nexport const momentAgoNarrow = pgettext(\"time ago\", \"now\")\nexport const dayAt = pgettext(\"day at time\", \"%(day)s at %(time)s\")\nexport const tomorrowAt = pgettext(\"day at time\", \"Tomorrow at %(time)s\")\nexport const yesterdayAt = pgettext(\"day at time\", \"Yesterday at %(time)s\")\n\nexport const minuteCompact = pgettext(\"short minutes\", \"%(time)sm\")\nexport const hourCompact = pgettext(\"short hours\", \"%(time)sh\")\nexport const dayCompact = pgettext(\"short days\", \"%(time)sd\")\n\nexport const relativeNumeric = new Intl.RelativeTimeFormat(locale, {\n numeric: \"always\",\n style: \"long\",\n})\n\nexport const relativeAuto = new Intl.RelativeTimeFormat(locale, {\n numeric: \"auto\",\n style: \"long\",\n})\n\nexport const fullDateTime = new Intl.DateTimeFormat(locale, {\n dateStyle: \"full\",\n timeStyle: \"medium\",\n})\n\nexport const thisYearDate = new Intl.DateTimeFormat(locale, {\n month: \"long\",\n day: \"numeric\",\n})\n\nexport const thisYearDateNarrow = new Intl.DateTimeFormat(locale, {\n month: \"short\",\n day: \"numeric\",\n})\n\nexport const otherYearDate = new Intl.DateTimeFormat(locale, {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n})\n\nexport const otherYearDateNarrow = new Intl.DateTimeFormat(locale, {\n year: \"2-digit\",\n month: \"short\",\n})\n\nexport const weekday = new Intl.DateTimeFormat(locale, {\n weekday: \"long\",\n})\n\nexport const shortTime = new Intl.DateTimeFormat(locale, { timeStyle: \"short\" })\n\nexport function formatNarrow(date) {\n const now = new Date()\n const absDiff = Math.abs(Math.round((date - now) / 1000))\n\n if (absDiff < 60) {\n return momentAgoNarrow\n }\n\n if (absDiff < 60 * 55) {\n const minutes = Math.ceil(absDiff / 60)\n return minuteCompact.replace(\"%(time)s\", minutes)\n }\n\n if (absDiff < 3600 * 24) {\n const hours = Math.ceil(absDiff / 3600)\n return hourCompact.replace(\"%(time)s\", hours)\n }\n\n if (absDiff < 86400 * 7) {\n const days = Math.ceil(absDiff / 86400)\n return dayCompact.replace(\"%(time)s\", days)\n }\n\n if (date.getFullYear() === now.getFullYear()) {\n return thisYearDateNarrow.format(date)\n }\n\n return otherYearDateNarrow.format(date)\n}\n\nexport function formatRelative(date) {\n const now = new Date()\n const diff = Math.round((date - now) / 1000)\n const absDiff = Math.abs(diff)\n const sign = diff < 1 ? -1 : 1\n\n if (absDiff < 90) {\n return momentAgo\n }\n\n if (absDiff < 60 * 47) {\n const minutes = Math.ceil(absDiff / 60) * sign\n return relativeNumeric.format(minutes, \"minute\")\n }\n\n if (absDiff < 3600 * 3) {\n const hours = Math.ceil(absDiff / 3600) * sign\n return relativeNumeric.format(hours, \"hour\")\n }\n\n if (isSameDay(now, date)) {\n return shortTime.format(date)\n }\n\n if (isYesterday(date)) {\n return yesterdayAt.replace(\"%(time)s\", shortTime.format(date))\n }\n\n if (isTomorrow(date)) {\n return tomorrowAt.replace(\"%(time)s\", shortTime.format(date))\n }\n\n if (diff < 0 && absDiff < 3600 * 24 * 6) {\n const day = weekday.format(date)\n return formatDayAtTime(day, date)\n }\n\n if (now.getFullYear() == date.getFullYear()) {\n return thisYearDate.format(date)\n }\n\n return otherYearDate.format(date)\n}\n\nexport function isSameDay(now, date) {\n return (\n now.getFullYear() == date.getFullYear() &&\n now.getMonth() == date.getMonth() &&\n now.getDate() == date.getDate()\n )\n}\n\nexport function isYesterday(date) {\n const yesterday = new Date()\n yesterday.setDate(yesterday.getDate() - 1)\n return isSameDay(yesterday, date)\n}\n\nexport function isTomorrow(date) {\n const yesterday = new Date()\n yesterday.setDate(yesterday.getDate() + 1)\n return isSameDay(yesterday, date)\n}\n\nexport function formatDayAtTime(day, date) {\n return dayAt\n .replace(\"%(day)s\", day)\n .replace(\"%(time)s\", shortTime.format(date))\n}\n","import React from \"react\"\nimport {\n formatNarrow,\n formatRelative,\n fullDateTime,\n} from \"../../datetimeFormats\"\n\nclass Timestamp extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = { tick: 0 }\n this.date = new Date(props.datetime)\n this.timeout = null\n }\n\n componentDidMount() {\n this.scheduleNextUpdate()\n }\n\n componentWillUnmount() {\n if (this.timeout) {\n window.clearTimeout(this.timeout)\n }\n }\n\n scheduleNextUpdate = () => {\n const now = new Date()\n const diff = Math.ceil(Math.abs(Math.round((this.date - now) / 1000)))\n\n if (diff < 3600) {\n this.timeout = window.setTimeout(\n () => {\n this.setState(tick)\n this.scheduleNextUpdate()\n },\n 50 * 1000 // Update every 50 seconds\n )\n } else if (diff < 3600 * 24) {\n this.timeout = window.setTimeout(\n () => {\n this.setState(tick)\n },\n 40 * 60 * 1000 // Update every 40 minutes\n )\n }\n }\n\n render() {\n const displayed = this.props.narrow\n ? formatNarrow(this.date)\n : formatRelative(this.date)\n\n return (\n \n {displayed}\n \n )\n }\n}\n\nfunction tick(state) {\n return { tick: state.tick + 1 }\n}\n\nexport default Timestamp\n","import Timestamp from \"./Timestamp\"\n\nexport default Timestamp\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst Toolbar = ({ children, className }) => (\n \n)\n\nexport default Toolbar\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst ToolbarItem = ({ children, className, shrink }) => (\n \n {children}\n \n)\n\nexport default ToolbarItem\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst ToolbarSection = ({ auto, children, className }) => (\n \n {children}\n \n)\n\nexport default ToolbarSection\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst ToolbarSpacer = ({ className }) => (\n
    \n)\n\nexport default ToolbarSpacer\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport Button from \"misago/components/button\"\nimport Loader from \"misago/components/loader\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n }\n }\n\n callApi(avatarType) {\n if (this.state.isLoading) {\n return false\n }\n\n this.setState({\n isLoading: true,\n })\n\n ajax\n .post(this.props.user.api.avatar, {\n avatar: avatarType,\n })\n .then(\n (response) => {\n this.setState({\n isLoading: false,\n })\n\n snackbar.success(response.detail)\n this.props.onComplete(response)\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n this.setState({\n isLoading: false,\n })\n } else {\n this.props.showError(rejection)\n }\n }\n )\n }\n\n setGravatar = () => {\n this.callApi(\"gravatar\")\n }\n\n setGenerated = () => {\n this.callApi(\"generated\")\n }\n\n getGravatarButton() {\n if (this.props.options.gravatar) {\n return (\n \n {pgettext(\"avatar modal btn\", \"Download my Gravatar\")}\n \n )\n } else {\n return null\n }\n }\n\n getCropButton() {\n if (!this.props.options.crop_src) return null\n\n return (\n \n {pgettext(\"avatar modal btn\", \"Re-crop uploaded image\")}\n \n )\n }\n\n getUploadButton() {\n if (!this.props.options.upload) return null\n\n return (\n \n {pgettext(\"avatar modal btn\", \"Upload new image\")}\n \n )\n }\n\n getGalleryButton() {\n if (!this.props.options.galleries) return null\n\n return (\n \n {pgettext(\"avatar modal btn\", \"Pick avatar from gallery\")}\n \n )\n }\n\n getAvatarPreview() {\n let userPeview = {\n id: this.props.user.id,\n avatars: this.props.options.avatars,\n }\n\n if (this.state.isLoading) {\n return (\n
    \n \n \n
    \n )\n }\n\n return (\n
    \n \n
    \n )\n }\n\n render() {\n return (\n
    \n
    \n
    {this.getAvatarPreview()}
    \n
    \n {this.getGravatarButton()}\n\n \n {pgettext(\"avatar modal btn\", \"Generate my individual avatar\")}\n \n\n {this.getCropButton()}\n {this.getUploadButton()}\n {this.getGalleryButton()}\n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport Button from \"misago/components/button\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n deviceRatio: 1,\n }\n }\n\n getAvatarSize() {\n if (this.props.upload) {\n return this.props.options.crop_tmp.size\n } else {\n return this.props.options.crop_src.size\n }\n }\n\n getImagePath() {\n if (this.props.upload) {\n return this.props.dataUrl\n } else {\n return this.props.options.crop_src.url\n }\n }\n\n componentDidMount() {\n let cropit = $(\".crop-form\")\n let cropperWidth = this.getAvatarSize()\n\n const initialWidth = cropit.width()\n while (initialWidth < cropperWidth) {\n cropperWidth = cropperWidth / 2\n }\n\n const deviceRatio = this.getAvatarSize() / cropperWidth\n\n cropit.width(cropperWidth)\n\n cropit.cropit({\n width: cropperWidth,\n height: cropperWidth,\n exportZoom: deviceRatio,\n imageState: {\n src: this.getImagePath(),\n },\n onImageLoaded: () => {\n if (this.props.upload) {\n // center uploaded image\n let zoomLevel = cropit.cropit(\"zoom\")\n let imageSize = cropit.cropit(\"imageSize\")\n\n // is it wider than taller?\n if (imageSize.width > imageSize.height) {\n let displayedWidth = imageSize.width * zoomLevel\n let offsetX = (displayedWidth - this.getAvatarSize()) / -2\n\n cropit.cropit(\"offset\", {\n x: offsetX,\n y: 0,\n })\n } else if (imageSize.width < imageSize.height) {\n let displayedHeight = imageSize.height * zoomLevel\n let offsetY = (displayedHeight - this.getAvatarSize()) / -2\n\n cropit.cropit(\"offset\", {\n x: 0,\n y: offsetY,\n })\n } else {\n cropit.cropit(\"offset\", {\n x: 0,\n y: 0,\n })\n }\n } else {\n // use preserved crop\n let crop = this.props.options.crop_src.crop\n\n if (crop) {\n cropit.cropit(\"zoom\", crop.zoom)\n cropit.cropit(\"offset\", {\n x: crop.x,\n y: crop.y,\n })\n }\n }\n },\n })\n }\n\n componentWillUnmount() {\n $(\".crop-form\").cropit(\"disable\")\n }\n\n cropAvatar = () => {\n if (this.state.isLoading) {\n return false\n }\n\n this.setState({\n isLoading: true,\n })\n\n let avatarType = this.props.upload ? \"crop_tmp\" : \"crop_src\"\n let cropit = $(\".crop-form\")\n\n const deviceRatio = cropit.cropit(\"exportZoom\")\n const cropitOffset = cropit.cropit(\"offset\")\n\n ajax\n .post(this.props.user.api.avatar, {\n avatar: avatarType,\n crop: {\n offset: {\n x: cropitOffset.x * deviceRatio,\n y: cropitOffset.y * deviceRatio,\n },\n zoom: cropit.cropit(\"zoom\") * deviceRatio,\n },\n })\n .then(\n (data) => {\n this.props.onComplete(data)\n snackbar.success(data.detail)\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n this.setState({\n isLoading: false,\n })\n } else {\n this.props.showError(rejection)\n }\n }\n )\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n \n
    \n
    \n
    \n
    \n \n {this.props.upload\n ? pgettext(\"avatar crop modal btn\", \"Set avatar\")\n : pgettext(\"avatar crop modal btn\", \"Crop image\")}\n \n\n \n {pgettext(\"avatar crop modal btn\", \"Cancel\")}\n \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport AvatarCrop from \"misago/components/change-avatar/crop\"\nimport Button from \"misago/components/button\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport fileSize from \"misago/utils/file-size\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n image: null,\n preview: null,\n progress: 0,\n uploaded: null,\n dataUrl: null,\n }\n }\n\n validateFile(image) {\n if (image.size > this.props.options.upload.limit) {\n return interpolate(\n pgettext(\n \"avatar upload modal\",\n \"Selected file is too big. (%(filesize)s)\"\n ),\n {\n filesize: fileSize(image.size),\n },\n true\n )\n }\n\n let invalidTypeMsg = pgettext(\n \"avatar upload modal\",\n \"Selected file type is not supported.\"\n )\n if (\n this.props.options.upload.allowed_mime_types.indexOf(image.type) === -1\n ) {\n return invalidTypeMsg\n }\n\n let extensionFound = false\n let loweredFilename = image.name.toLowerCase()\n this.props.options.upload.allowed_extensions.map(function (extension) {\n if (loweredFilename.substr(extension.length * -1) === extension) {\n extensionFound = true\n }\n })\n\n if (!extensionFound) {\n return invalidTypeMsg\n }\n\n return false\n }\n\n pickFile = () => {\n document.getElementById(\"avatar-hidden-upload\").click()\n }\n\n uploadFile = () => {\n let image = document.getElementById(\"avatar-hidden-upload\").files[0]\n if (!image) return\n\n let validationError = this.validateFile(image)\n if (validationError) {\n snackbar.error(validationError)\n return\n }\n\n this.setState({\n image,\n preview: URL.createObjectURL(image),\n progress: 0,\n })\n\n let data = new FormData()\n data.append(\"avatar\", \"upload\")\n data.append(\"image\", image)\n\n ajax\n .upload(this.props.user.api.avatar, data, (progress) => {\n this.setState({\n progress,\n })\n })\n .then(\n (data) => {\n this.setState({\n options: data,\n uploaded: data.detail,\n })\n\n snackbar.info(\n pgettext(\n \"avatar upload modal\",\n \"Your image has been uploaded and you may now crop it.\"\n )\n )\n },\n (rejection) => {\n if (rejection.status === 400 || rejection.status === 413) {\n snackbar.error(rejection.detail)\n this.setState({\n isLoading: false,\n image: null,\n progress: 0,\n })\n } else {\n this.props.showError(rejection)\n }\n }\n )\n }\n\n getUploadRequirements(options) {\n let extensions = options.allowed_extensions.map(function (extension) {\n return extension.substr(1)\n })\n\n return interpolate(\n pgettext(\"avatar upload modal\", \"%(files)s files smaller than %(limit)s\"),\n {\n files: extensions.join(\", \"),\n limit: fileSize(options.limit),\n },\n true\n )\n }\n\n getUploadButton() {\n return (\n
    \n \n

    \n {this.getUploadRequirements(this.props.options.upload)}\n

    \n
    \n )\n }\n\n getUploadProgressLabel() {\n return interpolate(\n pgettext(\"avatar upload modal field\", \"%(progress)s % complete\"),\n {\n progress: this.state.progress,\n },\n true\n )\n }\n\n getUploadProgress() {\n return (\n
    \n
    \n \n\n
    \n \n {this.getUploadProgressLabel()}\n
    \n
    \n
    \n
    \n )\n }\n\n renderUpload() {\n return (\n
    \n \n {this.state.image ? this.getUploadProgress() : this.getUploadButton()}\n
    \n
    \n \n {pgettext(\"avatar upload modal btn\", \"Cancel\")}\n \n
    \n
    \n
    \n )\n }\n\n renderCrop() {\n return (\n \n )\n }\n\n render() {\n if (this.state.uploaded) return this.renderCrop()\n\n return this.renderUpload()\n }\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport Button from \"misago/components/button\"\nimport misago from \"misago/index\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport batch from \"misago/utils/batch\"\n\nexport class GalleryItem extends React.Component {\n select = () => {\n this.props.select(this.props.id)\n }\n\n getClassName() {\n if (this.props.selection === this.props.id) {\n if (this.props.disabled) {\n return \"btn btn-avatar btn-disabled avatar-selected\"\n } else {\n return \"btn btn-avatar avatar-selected\"\n }\n } else if (this.props.disabled) {\n return \"btn btn-avatar btn-disabled\"\n } else {\n return \"btn btn-avatar\"\n }\n }\n\n render() {\n return (\n \n \n \n )\n }\n}\n\nexport class Gallery extends React.Component {\n render() {\n return (\n
    \n

    {this.props.name}

    \n\n
    \n {batch(this.props.images, 4, null).map((row, i) => {\n return (\n
    \n {row.map((item, i) => {\n return (\n
    \n {item ? (\n \n ) : (\n
    \n )}\n
    \n )\n })}\n
    \n )\n })}\n
    \n
    \n )\n }\n}\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n selection: null,\n isLoading: false,\n }\n }\n\n select = (image) => {\n this.setState({\n selection: image,\n })\n }\n\n save = () => {\n if (this.state.isLoading) {\n return false\n }\n\n this.setState({\n isLoading: true,\n })\n\n ajax\n .post(this.props.user.api.avatar, {\n avatar: \"galleries\",\n image: this.state.selection,\n })\n .then(\n (response) => {\n this.setState({\n isLoading: false,\n })\n\n snackbar.success(response.detail)\n this.props.onComplete(response)\n this.props.showIndex()\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n this.setState({\n isLoading: false,\n })\n } else {\n this.props.showError(rejection)\n }\n }\n )\n }\n\n render() {\n return (\n
    \n
    \n {this.props.options.galleries.map((item, i) => {\n return (\n \n )\n })}\n
    \n
    \n
    \n
    \n \n {this.state.selection\n ? pgettext(\"avatar gallery modal btn\", \"Save choice\")\n : pgettext(\"avatar gallery modal btn\", \"Select avatar\")}\n \n\n \n {pgettext(\"avatar gallery modal btn\", \"Cancel\")}\n \n
    \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport AvatarIndex from \"misago/components/change-avatar/index\"\nimport AvatarCrop from \"misago/components/change-avatar/crop\"\nimport AvatarUpload from \"misago/components/change-avatar/upload\"\nimport AvatarGallery from \"misago/components/change-avatar/gallery\"\nimport Loader from \"misago/components/modal-loader\"\nimport { updateAvatar } from \"misago/reducers/users\"\nimport ajax from \"misago/services/ajax\"\nimport store from \"misago/services/store\"\n\nexport class ChangeAvatarError extends React.Component {\n getErrorReason() {\n if (this.props.reason) {\n return

    \n } else {\n return null\n }\n }\n\n render() {\n return (\n

    \n
    \n remove_circle_outline\n
    \n
    \n

    {this.props.message}

    \n {this.getErrorReason()}\n \n {pgettext(\"avatar modal dismiss\", \"Ok\")}\n \n
    \n
    \n )\n }\n}\n\nexport default class extends React.Component {\n componentDidMount() {\n ajax.get(this.props.user.api.avatar).then(\n (options) => {\n this.setState({\n component: AvatarIndex,\n options: options,\n error: null,\n })\n },\n (rejection) => {\n this.showError(rejection)\n }\n )\n }\n\n showError = (error) => {\n this.setState({\n error,\n })\n }\n\n showIndex = () => {\n this.setState({\n component: AvatarIndex,\n })\n }\n\n showUpload = () => {\n this.setState({\n component: AvatarUpload,\n })\n }\n\n showCrop = () => {\n this.setState({\n component: AvatarCrop,\n })\n }\n\n showGallery = () => {\n this.setState({\n component: AvatarGallery,\n })\n }\n\n completeFlow = (options) => {\n store.dispatch(updateAvatar(this.props.user, options.avatars))\n\n this.setState({\n component: AvatarIndex,\n options,\n })\n }\n\n getBody() {\n if (this.state) {\n if (this.state.error) {\n return (\n \n )\n } else {\n return (\n \n )\n }\n } else {\n return \n }\n }\n\n getClassName() {\n if (this.state && this.state.error) {\n return \"modal-dialog modal-message modal-change-avatar\"\n } else {\n return \"modal-dialog modal-change-avatar\"\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"avatar modal title\", \"Change your avatar\")}\n

    \n
    \n\n {this.getBody()}\n
    \n
    \n )\n }\n}\n\nexport function select(state) {\n return {\n user: state.auth.user,\n }\n}\n","export default function logout() {\n document.getElementById(\"hidden-logout-form\").submit()\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { connect } from \"react-redux\"\nimport modal from \"../../services/modal\"\nimport ChangeAvatarModal, {\n select as selectAvatar,\n} from \"../change-avatar/root\"\nimport {\n DropdownDivider,\n DropdownFooter,\n DropdownMenuItem,\n DropdownSubheader,\n} from \"../Dropdown\"\nimport logout from \"./logout\"\n\nclass UserNavMenu extends React.Component {\n constructor(props) {\n super(props)\n\n if (props.dropdown) {\n // Collapse options on dropdown\n this.state = {\n options: props.options.slice(0, 2),\n optionsMore: props.options.length > 2,\n }\n } else {\n // Reveal all options on mobile overlay\n this.state = {\n options: props.options,\n optionsMore: false,\n }\n }\n }\n\n changeAvatar = () => {\n this.props.close()\n modal.show(connect(selectAvatar)(ChangeAvatarModal))\n }\n\n revealOptions = () => {\n this.setState({\n options: this.props.options,\n optionsMore: false,\n })\n }\n\n render() {\n const { user, close, dropdown, overlay } = this.props\n\n if (!user) {\n return null\n }\n\n const adminUrl = misago.get(\"ADMIN_URL\")\n\n return (\n \n
  • \n \n {user.username}\n {pgettext(\"user nav\", \"Go to your profile\")}\n \n
  • \n \n \n \n \n {user.unreadNotifications\n ? \"notifications_active\"\n : \"notifications_none\"}\n \n {pgettext(\"user nav\", \"Notifications\")}\n {!!user.unreadNotifications && (\n {user.unreadNotifications}\n )}\n \n \n {!!user.showPrivateThreads && (\n \n \n inbox\n {pgettext(\"user nav\", \"Private threads\")}\n {!!user.unreadPrivateThreads && (\n {user.unreadPrivateThreads}\n )}\n \n \n )}\n {!!adminUrl && (\n \n \n security\n {pgettext(\"user nav\", \"Admin control panel\")}\n \n \n )}\n \n \n {pgettext(\"user nav section\", \"Change options\")}\n \n \n \n portrait\n {pgettext(\"user nav\", \"Change avatar\")}\n \n \n {this.state.options.map((item) => (\n \n \n {item.icon}\n {item.name}\n \n \n ))}\n \n \n more_vertical\n {pgettext(\"user nav\", \"See more\")}\n \n \n {!!dropdown && (\n \n {\n logout()\n close()\n }}\n type=\"button\"\n >\n {pgettext(\"user nav\", \"Log out\")}\n \n \n )}\n \n )\n }\n}\n\nfunction select(state) {\n const user = state.auth.user\n if (!user.id) {\n return { user: null }\n }\n\n return {\n user: {\n username: user.username,\n unreadNotifications: user.unreadNotifications,\n unreadPrivateThreads: user.unread_private_threads,\n showPrivateThreads: user.acl.can_use_private_threads,\n url: user.url,\n },\n options: [...misago.get(\"userOptions\")],\n }\n}\n\nconst UserNavMenuConnected = connect(select)(UserNavMenu)\n\nexport default UserNavMenuConnected\n","import React from \"react\"\nimport UserNavMenu from \"./UserNavMenu\"\n\nexport default function UserNavDropdown({ close }) {\n return \n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport { close } from \"../../reducers/overlay\"\nimport { DropdownFooter } from \"../Dropdown\"\nimport { Overlay, OverlayHeader } from \"../Overlay\"\nimport UserNavMenu from \"./UserNavMenu\"\nimport logout from \"./logout\"\n\nexport function UserNavOverlay({ dispatch, isOpen }) {\n return (\n \n \n {pgettext(\"user nav title\", \"Your options\")}\n \n dispatch(close())} overlay />\n \n {\n logout()\n dispatch(close())\n }}\n type=\"button\"\n >\n {pgettext(\"user nav\", \"Log out\")}\n \n \n \n )\n}\n\nfunction select(state) {\n return {\n isOpen: state.overlay.userNav,\n }\n}\n\nconst UserNavOverlayConnected = connect(select)(UserNavOverlay)\n\nexport default UserNavOverlayConnected\n","import React from \"react\"\nimport misago from \"misago\"\n\nexport default function (props) {\n const size = props.size || 100\n const size2x = props.size2x || size * 2\n\n return (\n \n )\n}\n\nexport function getSrc(user, size) {\n if (user && user.id) {\n // just avatar hash, size and user id\n return resolveAvatarForSize(user.avatars, size).url\n } else {\n // just append avatar size to file to produce no-avatar placeholder\n return misago.get(\"BLANK_AVATAR_URL\")\n }\n}\n\nexport function resolveAvatarForSize(avatars, size) {\n let avatar = avatars[0]\n avatars.forEach((av) => {\n if (av.size >= size) {\n avatar = av\n }\n })\n return avatar\n}\n","import React from \"react\"\nimport Loader from \"./loader\"\n\nexport default class Button extends React.Component {\n render() {\n let className = \"btn \" + this.props.className\n let disabled = this.props.disabled\n\n if (this.props.loading) {\n className += \" btn-loading\"\n disabled = true\n }\n\n return (\n \n {this.props.children}\n {this.props.loading ? : null}\n \n )\n }\n}\n\nButton.defaultProps = {\n className: \"btn-default\",\n\n type: \"submit\",\n\n loading: false,\n disabled: false,\n\n onClick: null,\n}\n","import React from \"react\"\n\nexport default function (props) {\n return (\n \n {props.choices.map((item) => {\n return (\n \n {\"- - \".repeat(item.level) + item.label}\n \n )\n })}\n \n )\n}\n","import React from \"react\"\nimport PanelMessage from \"misago/components/panel-message\"\n\nexport default function ({ display }) {\n if (!display) return null\n\n return (\n \n )\n}\n","import React from \"react\"\nimport Loader from \"misago/components/loader\"\n\nexport default function ({ display }) {\n if (!display) return null\n\n return (\n
    \n \n
    \n )\n}\n","import React from \"react\"\nimport Select from \"misago/components/select\"\n\nexport default class extends React.Component {\n onChange = (ev) => {\n const { field, onChange } = this.props\n onChange(field.fieldname, ev.target.value)\n }\n\n render() {\n const { disabled, field, value } = this.props\n const { input } = field\n\n if (input.type === \"select\") {\n return (\n \n )\n }\n\n if (input.type === \"textarea\") {\n return (\n \n )\n }\n\n if (input.type === \"text\") {\n return (\n \n )\n }\n\n return null\n }\n}\n","import React from \"react\"\nimport FieldInput from \"./field-input\"\nimport FormGroup from \"misago/components/form-group\"\n\nexport default function ({ disabled, errors, fields, name, onChange, value }) {\n return (\n
    \n {name}\n {fields.map((field) => {\n return (\n \n \n \n )\n })}\n
    \n )\n}\n","import React from \"react\"\nimport Fieldset from \"./fieldset\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n errors: {},\n }\n\n const groups = props.groups.length\n for (let i = 0; i < groups; i++) {\n const group = props.groups[i]\n const fields = group.fields.length\n for (let f = 0; f < fields; f++) {\n const fieldname = group.fields[f].fieldname\n const initial = group.fields[f].initial\n this.state[fieldname] = initial\n }\n }\n }\n\n send() {\n const data = Object.assign({}, this.state, {\n errors: null,\n isLoading: null,\n })\n\n return ajax.post(this.props.api, data)\n }\n\n handleSuccess(data) {\n this.props.onSuccess(data)\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n snackbar.error(gettext(\"Form contains errors.\"))\n this.setState({ errors: rejection })\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n onChange = (name, value) => {\n this.setState({\n [name]: value,\n })\n }\n\n render() {\n return (\n
    \n
    \n {this.props.groups.map((group, i) => {\n return (\n \n )\n })}\n
    \n
    \n {\" \"}\n \n
    \n
    \n )\n }\n}\n\nexport function CancelButton({ onCancel, disabled }) {\n if (!onCancel) return null\n\n return (\n \n {pgettext(\"user profile details form btn\", \"Cancel\")}\n \n )\n}\n","import React from \"react\"\nimport Blankslate from \"./blankslate\"\nimport Loader from \"./loader\"\nimport Form from \"./form\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n loading: true,\n groups: null,\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.api).then(\n (groups) => {\n this.setState({\n loading: false,\n\n groups,\n })\n },\n (rejection) => {\n snackbar.apiError(rejection)\n if (this.props.cancel) {\n this.props.cancel()\n }\n }\n )\n }\n\n render() {\n const { groups, loading } = this.state\n\n return (\n
    \n
    \n

    \n {pgettext(\"user profile details form title\", \"Edit details\")}\n

    \n
    \n \n \n \n
    \n )\n }\n}\n\nexport function FormDisplay({ api, display, groups, onCancel, onSuccess }) {\n if (!display) return null\n\n return (\n
    \n )\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n isValidated() {\n return typeof this.props.validation !== \"undefined\"\n }\n\n getClassName() {\n let className = \"form-group\"\n if (this.isValidated()) {\n className += \" has-feedback\"\n if (this.props.validation === null) {\n className += \" has-success\"\n } else {\n className += \" has-error\"\n }\n }\n return className\n }\n\n getFeedback() {\n if (this.props.validation) {\n return (\n
    \n {this.props.validation.map((error, i) => {\n return

    {error}

    \n })}\n
    \n )\n } else {\n return null\n }\n }\n\n getFeedbackDescription() {\n if (this.isValidated()) {\n return (\n \n {this.props.validation\n ? pgettext(\"field validation status\", \"(error)\")\n : pgettext(\"field validation status\", \"(success)\")}\n \n )\n } else {\n return null\n }\n }\n\n getHelpText() {\n if (this.props.helpText) {\n return

    {this.props.helpText}

    \n } else {\n return null\n }\n }\n\n render() {\n return (\n
    \n \n {this.props.label + \":\"}\n \n
    \n {this.props.children}\n {this.getFeedbackDescription()}\n {this.getFeedback()}\n {this.getHelpText()}\n {this.props.extra || null}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport { required } from \"misago/utils/validators\"\nimport snackbar from \"misago/services/snackbar\"\n\nlet validateRequired = required()\n\nexport default class extends React.Component {\n validate() {\n let errors = {}\n if (!this.state.validators) {\n return errors\n }\n\n let validators = {\n required: this.state.validators.required || this.state.validators,\n optional: this.state.validators.optional || {},\n }\n\n let validatedFields = []\n\n // add required fields to validation\n for (let name in validators.required) {\n if (\n validators.required.hasOwnProperty(name) &&\n validators.required[name]\n ) {\n validatedFields.push(name)\n }\n }\n\n // add optional fields to validation\n for (let name in validators.optional) {\n if (\n validators.optional.hasOwnProperty(name) &&\n validators.optional[name]\n ) {\n validatedFields.push(name)\n }\n }\n\n // validate fields values\n for (let i in validatedFields) {\n let name = validatedFields[i]\n let fieldErrors = this.validateField(name, this.state[name])\n\n if (fieldErrors === null) {\n errors[name] = null\n } else if (fieldErrors) {\n errors[name] = fieldErrors\n }\n }\n\n return errors\n }\n\n isValid() {\n let errors = this.validate()\n for (let field in errors) {\n if (errors.hasOwnProperty(field)) {\n if (errors[field] !== null) {\n return false\n }\n }\n }\n\n return true\n }\n\n validateField(name, value) {\n let errors = []\n if (!this.state.validators) {\n return errors\n }\n\n let validators = {\n required: (this.state.validators.required || this.state.validators)[name],\n optional: (this.state.validators.optional || {})[name],\n }\n\n let requiredError = validateRequired(value) || false\n\n if (validators.required) {\n if (requiredError) {\n errors = [requiredError]\n } else {\n for (let i in validators.required) {\n let validationError = validators.required[i](value)\n if (validationError) {\n errors.push(validationError)\n }\n }\n }\n\n return errors.length ? errors : null\n } else if (requiredError === false && validators.optional) {\n for (let i in validators.optional) {\n let validationError = validators.optional[i](value)\n if (validationError) {\n errors.push(validationError)\n }\n }\n\n return errors.length ? errors : null\n }\n\n return false // false === field wasn't validated\n }\n\n bindInput = (name) => {\n return (event) => {\n this.changeValue(name, event.target.value)\n }\n }\n\n changeValue = (name, value) => {\n let newState = {\n [name]: value,\n }\n\n const formErrors = this.state.errors || {}\n formErrors[name] = this.validateField(name, newState[name])\n newState.errors = formErrors\n\n this.setState(newState)\n }\n\n clean() {\n return true\n }\n\n send() {\n return null\n }\n\n handleSuccess(success) {\n return\n }\n\n handleError(rejection) {\n snackbar.apiError(rejection)\n }\n\n handleSubmit = (event) => {\n // we don't reload page on submissions\n if (event) {\n event.preventDefault()\n }\n\n if (this.state.isLoading) {\n return\n }\n\n if (this.clean()) {\n this.setState({ isLoading: true })\n let promise = this.send()\n\n if (promise) {\n promise.then(\n (success) => {\n this.setState({ isLoading: false })\n this.handleSuccess(success)\n },\n (rejection) => {\n this.setState({ isLoading: false })\n this.handleError(rejection)\n }\n )\n } else {\n this.setState({ isLoading: false })\n }\n }\n }\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n isActive() {\n if (this.props.isControlled) {\n return this.props.isActive\n } else {\n if (this.props.path) {\n return document.location.pathname.indexOf(this.props.path) === 0\n } else {\n return false\n }\n }\n }\n\n getClassName() {\n if (this.isActive()) {\n return (\n (this.props.className || \"\") +\n \" \" +\n (this.props.activeClassName || \"active\")\n )\n } else {\n return this.props.className || \"\"\n }\n }\n\n render() {\n return
  • {this.props.children}
  • \n }\n}\n","import React from \"react\"\n\nexport default function (props) {\n return (\n
    \n
    \n
    \n )\n}\n","import React from \"react\"\nimport Button from \"./button\"\nimport Form from \"./form\"\nimport FormGroup from \"./form-group\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n bestAnswer: \"0\",\n poll: \"0\",\n }\n }\n\n clean() {\n if (this.props.polls && this.state.poll === \"0\") {\n const confirmation = window.confirm(\n pgettext(\n \"merge threads conflict form\",\n \"Are you sure you want to delete all polls?\"\n )\n )\n return confirmation\n }\n\n return true\n }\n\n send() {\n const data = Object.assign({}, this.props.data, {\n best_answer: this.state.bestAnswer,\n poll: this.state.poll,\n })\n\n return ajax.post(this.props.api, data)\n }\n\n handleSuccess = (success) => {\n this.props.onSuccess(success)\n modal.hide()\n }\n\n handleError = (rejection) => {\n this.props.onError(rejection)\n }\n\n onBestAnswerChange = (event) => {\n this.changeValue(\"bestAnswer\", event.target.value)\n }\n\n onPollChange = (event) => {\n this.changeValue(\"poll\", event.target.value)\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"merge threads conflict modal title\", \"Merge threads\")}\n

    \n
    \n \n
    \n \n \n
    \n
    \n \n {pgettext(\"merge threads conflict btn\", \"Cancel\")}\n \n \n
    \n \n
    \n
    \n )\n }\n}\n\nexport function BestAnswerSelect({ choices, onChange, value }) {\n if (!choices) return null\n\n return (\n \n \n {choices.map((choice) => {\n return (\n \n )\n })}\n \n \n )\n}\n\nexport function PollSelect({ choices, onChange, value }) {\n if (!choices) return null\n\n return (\n \n \n {choices.map((choice) => {\n return (\n \n )\n })}\n \n \n )\n}\n","const ytRegExp = new RegExp(\n \"^.*(?:(?:youtu.be/|v/|vi/|u/w/|embed/)|(?:(?:watch)??v(?:i)?=|&v(?:i)?=))([^#&?]*).*\"\n)\n\nexport class OneBox {\n constructor() {\n this._youtube = {}\n }\n\n render = (element) => {\n if (!element) return\n this.highlightCode(element)\n this.embedYoutubePlayers(element)\n }\n\n highlightCode(element) {\n import(\"highlight\").then(({ default: hljs }) => {\n const codeblocks = element.querySelectorAll(\"pre>code\")\n for (let i = 0; i < codeblocks.length; i++) {\n hljs.highlightElement(codeblocks[i])\n }\n })\n }\n\n embedYoutubePlayers(element) {\n const anchors = element.querySelectorAll(\"p>a\")\n for (let i = 0; i < anchors.length; i++) {\n const a = anchors[i]\n const p = a.parentNode\n const onlyChild = p.childNodes.length === 1\n\n if (!this._youtube[a.href]) {\n this._youtube[a.href] = parseYoutubeUrl(a.href)\n }\n\n const youtubeMovie = this._youtube[a.href]\n if (onlyChild && !!youtubeMovie && youtubeMovie.data !== false) {\n this.swapYoutubePlayer(a, youtubeMovie)\n }\n }\n }\n\n swapYoutubePlayer(element, youtube) {\n let url = \"https://www.youtube.com/embed/\"\n url += youtube.video\n url += \"?feature=oembed\"\n if (youtube.start) {\n url += \"&start=\" + youtube.start\n }\n\n const player = $(\n '\"\n )\n $(element).replaceWith(player)\n player.wrap('
    ')\n }\n}\n\nexport default new OneBox()\n\nexport function parseYoutubeUrl(url) {\n const cleanedUrl = cleanUrl(url)\n const video = getVideoIdFromUrl(cleanedUrl)\n\n if (!video) return null\n\n let start = 0\n if (cleanedUrl.indexOf(\"?\") > 0) {\n const query = cleanedUrl.substr(cleanedUrl.indexOf(\"?\") + 1)\n const timebit = query.split(\"&\").filter((i) => {\n return i.substr(0, 2) === \"t=\"\n })[0]\n\n if (timebit) {\n const bits = timebit.substr(2).split(\"m\")\n if (bits[0].substr(-1) === \"s\") {\n start += parseInt(bits[0].substr(0, bits[0].length - 1))\n } else {\n start += parseInt(bits[0]) * 60\n if (!!bits[1] && bits[1].substr(-1) === \"s\") {\n start += parseInt(bits[1].substr(0, bits[1].length - 1))\n }\n }\n }\n }\n\n return {\n start,\n video,\n }\n}\n\nexport function cleanUrl(url) {\n let clean = url\n\n if (url.substr(0, 8) === \"https://\") {\n clean = clean.substr(8)\n } else if (url.substr(0, 7) === \"http://\") {\n clean = clean.substr(7)\n }\n\n if (clean.substr(0, 4) === \"www.\") {\n clean = clean.substr(4)\n }\n\n return clean\n}\n\nexport function getVideoIdFromUrl(url) {\n if (url.indexOf(\"youtu\") === -1) return null\n\n const video = url.match(ytRegExp)\n if (video) {\n return video[1]\n }\n return null\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport onebox from \"misago/services/one-box\"\n\nexport default class extends React.Component {\n componentDidMount() {\n onebox.render(this.documentNode)\n $(this.documentNode).find(\".spoiler-reveal\").click(revealSpoiler)\n }\n\n componentDidUpdate(prevProps, prevState) {\n onebox.render(this.documentNode)\n $(this.documentNode).find(\".spoiler-reveal\").click(revealSpoiler)\n }\n\n shouldComponentUpdate(nextProps, nextState) {\n return nextProps.markup !== this.props.markup\n }\n\n render() {\n return (\n {\n this.documentNode = node\n }}\n />\n )\n }\n}\n\nfunction revealSpoiler(event) {\n var btn = event.target\n $(btn).parent().parent().addClass(\"revealed\")\n}\n","import React from \"react\"\nimport Loader from \"misago/components/loader\"\n\nexport default class extends React.Component {\n render() {\n return (\n
    \n \n
    \n )\n }\n}\n","import React from \"react\"\nimport PanelMessage from \"misago/components/panel-message\"\n\nexport default class extends PanelMessage {\n getHelpText() {\n if (this.props.helpText) {\n return

    {this.props.helpText}

    \n } else {\n return null\n }\n }\n\n render() {\n return (\n
    \n
    \n \n {this.props.icon || \"info_outline\"}\n \n
    \n
    \n

    {this.props.message}

    \n {this.getHelpText()}\n \n {pgettext(\"modal message dismiss btn\", \"Ok\")}\n \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Loader from \"misago/components/loader\"\n\nexport default class extends React.Component {\n render() {\n return (\n
    \n \n
    \n )\n }\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n getHelpText() {\n if (this.props.helpText) {\n return

    {this.props.helpText}

    \n } else {\n return null\n }\n }\n\n render() {\n return (\n
    \n
    \n \n {this.props.icon || \"info_outline\"}\n \n
    \n
    \n

    {this.props.message}

    \n {this.getHelpText()}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport MisagoMarkup from \"misago/components/misago-markup\"\n\nexport default function (props) {\n if (props.post.content) {\n return \n } else {\n return \n }\n}\n\nexport function Default(props) {\n return (\n
    \n \n
    \n )\n}\n\nexport function Invalid(props) {\n return (\n
    \n

    \n {pgettext(\n \"posts feed item body\",\n \"This post's contents cannot be displayed.\"\n )}\n

    \n

    \n {pgettext(\n \"posts feed item body\",\n \"This error is caused by invalid post content manipulation.\"\n )}\n

    \n
    \n )\n}\n","import React from \"react\"\n\nexport default function ({ post }) {\n const { category, thread } = post\n\n const tooltip = interpolate(\n pgettext(\"posts feed item header\", \"posted %(posted_on)s\"),\n {\n posted_on: post.posted_on.format(\"LL, LT\"),\n },\n true\n )\n\n return (\n
    \n \n {thread.title}\n \n \n {category.name}\n \n \n {post.posted_on.fromNow()}\n \n
    \n )\n}\n","import React from \"react\"\n\nexport default function ({ post }) {\n return (\n \n \n {pgettext(\"go to post link\", \"See post\")}\n \n chevron_right\n \n )\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport GoToButton from \"./button\"\n\nexport default function ({ post }) {\n return (\n
    \n \n
    \n
    \n \n \n \n
    \n
    \n
    \n {post.poster_name}\n
    \n \n {pgettext(\"post removed poster username\", \"Removed user\")}\n \n
    \n
    \n
    \n )\n}\n","import React from \"react\"\n\nexport default function ({ rank, title }) {\n let userTitle = title || rank.title || rank.name\n\n let className = \"user-title\"\n if (rank.css_class) {\n className += \" user-title-\" + rank.css_class\n }\n\n if (rank.is_tab) {\n return (\n \n {userTitle}\n \n )\n }\n\n return {userTitle}\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport GoToButton from \"./button\"\nimport UserTitle from \"./user-title\"\n\nexport default function ({ post, poster }) {\n return (\n
    \n \n
    \n
    \n \n \n \n
    \n \n
    \n
    \n )\n}\n","import React from \"react\"\nimport Anonymous from \"./anonymous\"\nimport Registered from \"./registered\"\n\nexport default function ({ post, poster }) {\n if (poster && poster.id) {\n return \n }\n\n return \n}\n","import React from \"react\"\nimport Body from \"./body\"\nimport Header from \"./header\"\nimport PostSide from \"./post-side\"\n\nexport default function ({ post, poster }) {\n const user = poster || post.poster\n\n let className = \"post\"\n if (user && user.rank.css_class) {\n className += \" post-\" + user.rank.css_class\n }\n\n return (\n
  • \n
    \n
    \n
    \n \n
    \n \n
    \n
    \n
    \n
  • \n )\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport * as random from \"misago/utils/random\"\n\nexport default function () {\n return (\n
      \n
    • \n
      \n
      \n
      \n
      \n
      \n
      \n \n \n \n
      \n
      \n
      \n \n \n  \n \n \n
      \n \n \n  \n \n \n
      \n
      \n
      \n
      \n \n  \n \n
      \n
      \n
      \n

      \n \n  \n \n  \n \n  \n \n  \n \n  \n \n

      \n
      \n
      \n
      \n
      \n
      \n
    • \n
    \n )\n}\n","import React from \"react\"\nimport Post from \"./post\"\nimport Preview from \"./preview\"\n\nexport default function ({ isReady, posts, poster }) {\n if (!isReady) {\n return \n }\n\n return (\n
      \n {posts.map((post) => {\n return \n })}\n
    \n )\n}\n","import React from \"react\"\nimport posting from \"../../services/posting\"\nimport { getGlobalState, getQuoteMarkup } from \"../posting\"\n\nexport default class PostingQuoteSelection extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n range: null,\n rect: null,\n }\n\n this.element = null\n }\n\n selected = () => {\n if (this.element) {\n const range = getQuoteSelection(this.element) || null\n const rect = range ? range.getBoundingClientRect() : null\n\n this.setState({ range, rect })\n }\n }\n\n reply = () => {\n if (!posting.isOpen()) {\n const content = getQuoteMarkup(this.state.range)\n posting.open(Object.assign({}, this.props.posting, { default: content }))\n\n this.setState({ range: null, rect: null })\n\n window.setTimeout(focusEditor, 1000)\n } else {\n const globalState = getGlobalState()\n if (globalState && !globalState.disabled) {\n globalState.quote(getQuoteMarkup(this.state.range))\n this.setState({ range: null, rect: null })\n focusEditor()\n }\n }\n }\n\n render = () => (\n
    \n {\n if (element) {\n this.element = element\n }\n }}\n onMouseUp={this.selected}\n onTouchEnd={this.selected}\n >\n {this.props.children}\n
    \n {!!this.state.rect && (\n \n
    \n
    \n \n {pgettext(\"post reply\", \"Quote\")}\n \n
    \n
    \n )}\n
    \n )\n}\n\nfunction focusEditor() {\n const textarea = document.querySelector(\"#posting-mount textarea\")\n textarea.focus()\n textarea.selectionStart = textarea.selectionEnd = textarea.value.length\n}\n\nconst getQuoteSelection = (container) => {\n if (typeof window.getSelection === \"undefined\") return\n\n // Validate that selection is of valid type and has one range\n const selection = window.getSelection()\n if (!selection) return\n if (selection.type !== \"Range\") return\n if (selection.rangeCount !== 1) return\n\n // Validate that selection is within the container and post's article\n const range = selection.getRangeAt(0)\n if (!isRangeContained(range, container)) return\n if (!isPostContained(range)) return\n if (!isAnyTextSelected(range.cloneContents())) return\n\n return range\n}\n\nconst isRangeContained = (range, container) => {\n const node = range.commonAncestorContainer\n if (node === container) return true\n\n let p = node.parentNode\n while (p) {\n if (p === container) return true\n p = p.parentNode\n }\n\n return false\n}\n\nconst isPostContained = (range) => {\n const element = range.commonAncestorContainer\n if (element.nodeName === \"ARTICLE\") return true\n if (element.dataset && element.dataset.noquote === \"1\") return false\n let p = element.parentNode\n while (p) {\n if (p.dataset && p.dataset.noquote === \"1\") return false\n if (p.nodeName === \"ARTICLE\") return true\n p = p.parentNode\n }\n return false\n}\n\nconst isAnyTextSelected = (node) => {\n for (let i = 0; i < node.childNodes.length; i++) {\n const child = node.childNodes[i]\n if (child.nodeType === Node.TEXT_NODE) {\n if (child.textContent && child.textContent.trim().length > 0) return true\n }\n if (child.nodeName === \"IMG\") return true\n if (isAnyTextSelected(child)) return true\n }\n\n return false\n}\n","const getQuoteMarkup = (range) => {\n const metadata = getQuoteMetadata(range)\n let markup = convertNodesToMarkup(range.cloneContents().childNodes, [])\n let prefix = metadata ? `[quote=\"${metadata}\"]\\n` : \"[quote]\\n\"\n let suffix = \"\\n[/quote]\\n\\n\"\n\n const codeBlock = getQuoteCodeBlock(range)\n if (codeBlock) {\n prefix += codeBlock.syntax ? `[code=${codeBlock.syntax}]\\n` : \"[code]\\n\"\n suffix = \"\\n[/code]\" + suffix\n } else if (isNodeInlineCodeBlock(range)) {\n markup = markup.trim()\n prefix += \"`\"\n suffix = \"`\" + suffix\n } else {\n markup = markup.trim()\n }\n\n return prefix + markup + suffix\n}\n\nexport default getQuoteMarkup\n\nconst getQuoteMetadata = (range) => {\n const node = range.commonAncestorContainer\n if (isNodeElementWithQuoteMetadata(node)) {\n return getQuoteMetadataFromNode(node)\n }\n\n let p = node.parentNode\n while (p) {\n if (isNodeElementWithQuoteMetadata(p)) {\n return getQuoteMetadataFromNode(p)\n }\n p = p.parentNode\n }\n\n return \"\"\n}\n\nconst isNodeElementWithQuoteMetadata = (node) => {\n if (node.nodeType !== Node.ELEMENT_NODE) return false\n if (node.nodeName === \"ARTICLE\") return true\n if (node.nodeName === \"BLOCKQUOTE\") {\n return node.dataset && node.dataset.block === \"quote\"\n }\n\n return false\n}\n\nconst getQuoteMetadataFromNode = (element) => {\n if (element.dataset) {\n return element.dataset.author || null\n }\n return null\n}\n\nconst getQuoteCodeBlock = (range) => {\n const node = range.commonAncestorContainer\n if (isNodeCodeBlock(node)) {\n return getNodeCodeBlockMeta(node)\n }\n\n let p = node.parentNode\n while (p) {\n if (isNodeCodeBlock(p)) {\n return getNodeCodeBlockMeta(p)\n }\n p = p.parentNode\n }\n\n return null\n}\n\nconst isNodeCodeBlock = (node) => {\n return node.nodeName === \"PRE\"\n}\n\nconst isNodeInlineCodeBlock = (range) => {\n const node = range.commonAncestorContainer\n if (node.nodeName === \"CODE\") {\n return true\n }\n\n let p = node.parentNode\n while (p) {\n if (isNodeElementWithQuoteMetadata(p)) {\n return false\n }\n\n if (p.nodeName === \"CODE\") {\n return true\n }\n\n p = p.parentNode\n }\n\n return false\n}\n\nconst getNodeCodeBlockMeta = (node) => {\n if (!node.dataset) {\n return { syntax: null }\n }\n\n return { syntax: node.dataset.syntax || null }\n}\n\nconst convertNodesToMarkup = (nodes, stack) => {\n let markup = \"\"\n for (let i = 0; i < nodes.length; i++) {\n const node = nodes[i]\n markup += convertNodeToMarkup(node, stack)\n }\n return markup\n}\n\nconst SIMPLE_NODE_MAPPINGS = {\n H1: [\"\\n\\n# \", \"\"],\n H2: [\"\\n\\n## \", \"\"],\n H3: [\"\\n\\n### \", \"\"],\n H4: [\"\\n\\n#### \", \"\"],\n H5: [\"\\n\\n##### \", \"\"],\n H6: [\"\\n\\n###### \", \"\"],\n STRONG: [\"**\", \"**\"],\n EM: [\"*\", \"*\"],\n DEL: [\"~~\", \"~~\"],\n B: [\"[b]\", \"[/b]\"],\n U: [\"[u]\", \"[/u]\"],\n I: [\"[i]\", \"[/i]\"],\n SUB: [\"[sub]\", \"[/sub]\"],\n SUP: [\"[sup]\", \"[/sup]\"],\n}\n\nconst convertNodeToMarkup = (node, stack) => {\n const dataset = node.dataset || {}\n\n if (node.nodeType === Node.TEXT_NODE) {\n return node.textContent || \"\"\n }\n\n if (node.nodeType === Node.ELEMENT_NODE) {\n if (dataset.quote) {\n return dataset.quote || \"\"\n }\n if (dataset.noquote === \"1\") return \"\"\n }\n\n if (\n node.nodeType === Node.ELEMENT_NODE &&\n dataset.quote &&\n dataset.quote.trim()\n ) {\n return \"\"\n }\n\n if (node.nodeName === \"HR\") {\n return \"\\n\\n- - -\"\n }\n\n if (SIMPLE_NODE_MAPPINGS[node.nodeName]) {\n const [prefix, suffix] = SIMPLE_NODE_MAPPINGS[node.nodeName]\n return (\n prefix +\n convertNodesToMarkup(node.childNodes, [...stack, node.nodeName]) +\n suffix\n )\n }\n\n if (node.nodeName === \"A\") {\n const href = node.href\n const text = convertNodesToMarkup(node.childNodes, [\n ...stack,\n node.nodeName,\n ])\n if (text) {\n return `[${text}](${href})`\n } else {\n return `!(${href})`\n }\n }\n\n if (node.nodeName === \"IMG\") {\n const src = node.src\n const alt = node.alt\n if (alt) {\n return `![${alt}](${src})`\n } else {\n return `!(${src})`\n }\n }\n\n if (node.nodeName === \"DIV\" || node.nodeName === \"ASIDE\") {\n const block = dataset.block && dataset.block.toUpperCase()\n if (block && SIMPLE_NODE_MAPPINGS[block]) {\n const [prefix, suffix] = SIMPLE_NODE_MAPPINGS[block]\n return (\n prefix +\n convertNodesToMarkup(node.childNodes, [...stack, block]) +\n suffix\n )\n } else {\n return convertNodesToMarkup(node.childNodes, stack)\n }\n }\n\n if (node.nodeName === \"BLOCKQUOTE\") {\n if (dataset.block === \"spoiler\") {\n const content = convertNodesToMarkup(node.childNodes, [\n ...stack,\n \"SPOILER\",\n ]).trim()\n\n if (!content) return \"\"\n\n let markup = \"\\n[spoiler]\\n\"\n markup += content\n markup += \"\\n[/spoiler]\"\n return markup\n }\n\n const content = convertNodesToMarkup(node.childNodes, [\n ...stack,\n \"QUOTE\",\n ]).trim()\n\n if (!content) return \"\"\n\n const metadata = getQuoteMetadataFromNode(node)\n let markup = metadata ? `\\n[quote=${metadata}]\\n` : \"\\n\\n[quote]\\n\"\n markup += content\n markup += \"\\n[/quote]\"\n return markup\n }\n\n if (node.nodeName === \"PRE\") {\n const syntax = dataset.syntax || null\n const code = node.querySelector(\"code\")\n const content = code ? code.innerText || \"\" : \"\"\n\n if (!content.trim()) return \"\"\n\n return \"\\n[code\" + (syntax ? \"=\" + syntax : \"\") + \"]\" + content + \"[/code]\"\n }\n\n if (node.nodeName === \"CODE\") {\n return \"`\" + node.innerText + \"`\"\n }\n\n if (node.nodeName === \"P\") {\n return (\n \"\\n\" + convertNodesToMarkup(node.childNodes, [...stack, node.nodeName])\n )\n }\n\n if (node.nodeName === \"UL\" || node.nodeName === \"OL\") {\n const level = stack.filter((item) => item === \"OL\" || item === \"UL\").length\n const prefix = level === 0 ? \"\\n\" : \"\"\n return (\n prefix + convertNodesToMarkup(node.childNodes, [...stack, node.nodeName])\n )\n }\n\n if (node.nodeName === \"LI\") {\n let prefix = \"\"\n const level = stack.filter((item) => item === \"OL\" || item === \"UL\").length\n for (let i = 1; i < level; i++) {\n prefix += \" \"\n }\n\n const ordered = stack[stack.length - 1] === \"OL\"\n if (ordered) {\n prefix += dataset.index ? dataset.index + \". \" : \"1. \"\n } else {\n prefix += \"- \"\n }\n\n const content = convertNodesToMarkup(node.childNodes, [\n ...stack,\n node.nodeName,\n ])\n if (!content.trim()) return \"\"\n\n return \"\\n\" + prefix + content\n }\n\n if (node.nodeName === \"SPAN\") {\n return convertNodesToMarkup(node.childNodes, stack)\n }\n\n return \"\"\n}\n","export function getGlobalState() {\n return window.misagoReply\n}\n\nexport function setGlobalState(disabled, quote) {\n window.misagoReply = { disabled, quote }\n}\n\nexport function clearGlobalState() {\n window.misagoReply = null\n}\n","import moment from \"moment\"\n\nexport function clean(attachments) {\n return attachments\n .filter((attachment) => {\n return attachment.id && !attachment.isRemoved\n })\n .map((a) => {\n return a.id\n })\n}\n\nexport function hydrate(attachments) {\n return attachments.map((attachment) => {\n return Object.assign({}, attachment, {\n uploaded_on: moment(attachment.uploaded_on),\n })\n })\n}\n","import React from \"react\"\nimport formatFilesize from \"../../utils/file-size\"\n\nexport default function MarkupAttachmentModal({ attachment }) {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"markup editor\", \"Attachment details\")}\n

    \n
    \n
    \n {!!attachment.is_image && (\n
    \n \n \"\"\n \n
    \n )}\n
    \n {attachment.filename}\n
    \n
    \n
    \n \n {attachment.filetype + \", \" + formatFilesize(attachment.size)}\n \n
    \n {pgettext(\"markup editor\", \"Type and size\")}\n
    \n
    \n
    \n \n \n {attachment.uploaded_on.fromNow()}\n \n \n
    \n {pgettext(\"markup editor\", \"Uploaded at\")}\n
    \n
    \n
    \n {attachment.url.uploader ? (\n \n {attachment.uploader_name}\n \n ) : (\n {attachment.uploader_name}\n )}\n
    \n {pgettext(\"markup editor\", \"Uploader\")}\n
    \n
    \n
    \n
    \n
    \n \n {pgettext(\"modal\", \"Close\")}\n \n
    \n
    \n
    \n )\n}\n","const wrapSelection = (selection, update, prefix, suffix, def) => {\n const text = selection.text || def || \"\"\n let newValue = selection.prefix\n newValue += prefix + text + suffix\n newValue += selection.suffix\n update(newValue)\n\n window.setTimeout(() => {\n focus(selection.textarea)\n\n const caret = selection.start + prefix.length\n selection.textarea.setSelectionRange(caret, caret + text.length)\n }, 250)\n}\n\nconst replaceSelection = (selection, update, text) => {\n let newValue = selection.prefix\n newValue += text\n newValue += selection.suffix\n update(newValue)\n\n window.setTimeout(() => {\n focus(selection.textarea)\n\n const caret = selection.end + text.length\n selection.textarea.setSelectionRange(caret, caret)\n }, 250)\n}\n\nconst getSelection = (textarea) => {\n if (document.selection) {\n textarea.focus()\n const range = document.selection.createRange()\n const length = range.text.length\n range.moveStart(\"character\", -textarea.value.length)\n return createRange(textarea, range.text.length - length, range.text.length)\n }\n\n if (textarea.selectionStart || textarea.selectionStart == \"0\") {\n return createRange(textarea, textarea.selectionStart, textarea.selectionEnd)\n }\n}\n\nconst createRange = (textarea, start, end) => {\n return {\n textarea: textarea,\n start: start,\n end: end,\n text: textarea.value.substring(start, end),\n prefix: textarea.value.substring(0, start),\n suffix: textarea.value.substring(end),\n }\n}\n\nexport function focus(textarea) {\n const scroll = textarea.scrollTop\n textarea.focus()\n textarea.scrollTop = scroll\n}\n\nexport { getSelection, replaceSelection, wrapSelection }\n","import React from \"react\"\nimport modal from \"../../services/modal\"\nimport snackbar from \"../../services/snackbar\"\nimport formatFilesize from \"../../utils/file-size\"\nimport MarkupAttachmentModal from \"./MarkupAttachmentModal\"\nimport { getSelection, replaceSelection } from \"./operations\"\n\nconst MarkupEditorAttachment = ({\n attachment,\n disabled,\n element,\n setState,\n update,\n}) => (\n
    \n
    \n
    \n {attachment.id ? (\n {\n event.preventDefault()\n modal.show()\n }}\n >\n {attachment.filename}\n \n ) : (\n {attachment.filename}\n )}\n
    \n
      \n {!attachment.id &&
    • {attachment.progress + \"%\"}
    • }\n {!!attachment.filetype &&
    • {attachment.filetype}
    • }\n {attachment.size > 0 &&
    • {formatFilesize(attachment.size)}
    • }\n
    \n
    \n
    \n {!!attachment.id && (\n
    \n {\n const markup = getAttachmentMarkup(attachment)\n const selection = getSelection(element)\n replaceSelection(selection, update, markup)\n }}\n >\n flip_to_front\n \n {\n setState(({ attachments }) => {\n const confirm = window.confirm(\n pgettext(\"markup editor\", \"Remove this attachment?\")\n )\n\n if (confirm) {\n return {\n attachments: attachments.filter(\n ({ id }) => id !== attachment.id\n ),\n }\n }\n })\n }}\n >\n close\n \n
    \n )}\n {!attachment.id && !!attachment.key && (\n
    \n {attachment.error && (\n {\n snackbar.error(\n interpolate(\n pgettext(\"markup editor\", \"%(filename)s: %(error)s\"),\n { filename: attachment.filename, error: attachment.error },\n true\n )\n )\n }}\n >\n warning\n \n )}\n {\n setState(({ attachments }) => {\n return {\n attachments: attachments.filter(\n ({ key }) => key !== attachment.key\n ),\n }\n })\n }}\n >\n close\n \n
    \n )}\n
    \n
    \n)\n\nexport default MarkupEditorAttachment\n\nfunction getAttachmentMarkup(attachment) {\n let markup = \"[\"\n\n if (attachment.is_image) {\n markup += \"![\" + attachment.filename + \"]\"\n markup += \"(\" + (attachment.url.thumb || attachment.url.index) + \"?shva=1)\"\n } else {\n markup += attachment.filename\n }\n\n markup += \"](\" + attachment.url.index + \"?shva=1)\"\n return markup\n}\n","import React from \"react\"\nimport MarkupEditorAttachment from \"./MarkupEditorAttachment\"\n\nconst MarkupEditorAttachments = ({\n attachments,\n disabled,\n element,\n setState,\n update,\n}) => (\n
    \n
    \n {attachments.map((attachment) => (\n \n ))}\n
    \n
    \n)\n\nexport default MarkupEditorAttachments\n","import React from \"react\"\nimport Button from \"../button\"\n\nconst MarkupEditorFooter = ({\n canProtect,\n disabled,\n empty,\n preview,\n isProtected,\n submitText,\n showPreview,\n closePreview,\n enableProtection,\n disableProtection,\n}) => (\n
    \n {!!canProtect && (\n {\n if (isProtected) {\n disableProtection()\n } else {\n enableProtection()\n }\n }}\n >\n \n {isProtected ? \"lock\" : \"lock_open\"}\n \n \n )}\n {!!canProtect && (\n
    \n {\n if (isProtected) {\n disableProtection()\n } else {\n enableProtection()\n }\n }}\n >\n \n {isProtected ? \"lock\" : \"lock_open\"}\n \n {isProtected\n ? pgettext(\"markup editor\", \"Protected\")\n : pgettext(\"markup editor\", \"Protect\")}\n \n
    \n )}\n
    \n {preview ? (\n \n {pgettext(\"markup editor\", \"Edit\")}\n \n ) : (\n \n {pgettext(\"markup editor\", \"Preview\")}\n \n )}\n \n
    \n)\n\nexport default MarkupEditorFooter\n","import React from \"react\"\nimport modal from \"../../services/modal\"\nimport FormGroup from \"../form-group\"\nimport { replaceSelection } from \"./operations\"\n\nclass MarkupCodeModal extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n error: null,\n syntax: \"\",\n text: props.selection.text,\n }\n }\n\n handleSubmit = (ev) => {\n ev.preventDefault()\n\n const { selection, update } = this.props\n const syntax = this.state.syntax.trim()\n const text = this.state.text.trim()\n\n if (text.length === 0) {\n this.setState({ error: gettext(\"This field is required.\") })\n return false\n }\n\n const prefix = selection.prefix.trim().length ? \"\\n\\n\" : \"\"\n\n replaceSelection(\n Object.assign({}, selection, { text }),\n update,\n prefix + \"```\" + syntax + \"\\n\" + text + \"\\n```\\n\\n\"\n )\n\n modal.hide()\n\n return false\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    {pgettext(\"markup editor\", \"Code\")}

    \n
    \n
    \n
    \n \n \n this.setState({ syntax: event.target.value })\n }\n >\n \n {LANGUAGES.map(({ value, name }) => (\n \n ))}\n \n \n \n \n this.setState({ text: event.target.value })\n }\n />\n \n
    \n
    \n \n {gettext(\"Cancel\")}\n \n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nconst LANGUAGES = [\n { value: \"bash\", name: \"Bash\" },\n { value: \"c\", name: \"C\" },\n { value: \"c#\", name: \"C#\" },\n { value: \"c++\", name: \"C++\" },\n { value: \"css\", name: \"CSS\" },\n { value: \"diff\", name: \"Diff\" },\n { value: \"go\", name: \"Go\" },\n { value: \"graphql\", name: \"GraphQL\" },\n { value: \"html,\", name: \"HTML\" },\n { value: \"xml\", name: \"XML\" },\n { value: \"json\", name: \"JSON\" },\n { value: \"java\", name: \"Java\" },\n { value: \"javascript\", name: \"JavaScript\" },\n { value: \"kotlin\", name: \"Kotlin\" },\n { value: \"less\", name: \"Less\" },\n { value: \"lua\", name: \"Lua\" },\n { value: \"makefile\", name: \"Makefile\" },\n { value: \"markdown\", name: \"Markdown\" },\n { value: \"objective-C\", name: \"Objective-C\" },\n { value: \"php\", name: \"PHP\" },\n { value: \"perl\", name: \"Perl\" },\n { value: \"plain\", name: \"Plain\" },\n { value: \"text\", name: \"text\" },\n { value: \"python\", name: \"Python\" },\n { value: \"repl\", name: \"REPL\" },\n { value: \"r\", name: \"R\" },\n { value: \"ruby\", name: \"Ruby\" },\n { value: \"rust\", name: \"Rust\" },\n { value: \"scss\", name: \"SCSS\" },\n { value: \"sql\", name: \"SQL\" },\n { value: \"shell\", name: \"Shell Session\" },\n { value: \"swift\", name: \"Swift\" },\n { value: \"toml\", name: \"TOML\" },\n { value: \"ini\", name: \"INI\" },\n { value: \"typescript\", name: \"TypeScript\" },\n { value: \"visualbasic\", name: \"Visual Basic .NET\" },\n { value: \"webassembly\", name: \"WebAssembly\" },\n { value: \"yaml\", name: \"YAML\" },\n]\n\nexport default MarkupCodeModal\n","import React from \"react\"\nimport formatFilesize from \"../../utils/file-size\"\n\nexport default function MarkupFormattingHelpModal() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"markup help\", \"Formatting help\")}\n

    \n
    \n
    \n

    {pgettext(\"markup help\", \"Emphasis text\")}

    \n \n \n {pgettext(\"markup help\", \"This text will have emphasis\")}\n \n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Bold text\")}

    \n \n \n {pgettext(\"markup help\", \"This text will be bold\")}\n \n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Removed text\")}

    \n \n \n {pgettext(\"markup help\", \"This text will be removed\")}\n \n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Bold text (BBCode)\")}

    \n \n {pgettext(\"markup help\", \"This text will be bold\")}\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Underlined text (BBCode)\")}

    \n \n {pgettext(\"markup help\", \"This text will be underlined\")}\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Italics text (BBCode)\")}

    \n \n {pgettext(\"markup help\", \"This text will be in italics\")}\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Link\")}

    \n \"\n result={\n

    \n example.com\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Link with text\")}

    \n \n {pgettext(\"markup help\", \"Link text\")}\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Link (BBCode)\")}

    \n \n example.com\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Link with text (BBCode)\")}

    \n \n {pgettext(\"markup help\", \"Link text\")}\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Image\")}

    \n \n \"\"\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Image with alternate text\")}

    \n \n \n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Image (BBCode)\")}

    \n \n \"\"\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Mention user by their name\")}

    \n \n @username\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Heading 1\")}

    \n {pgettext(\"markup help\", \"First level heading\")}}\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Heading 2\")}

    \n {pgettext(\"markup help\", \"Second level heading\")}}\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Heading 3\")}

    \n {pgettext(\"markup help\", \"Third level heading\")}}\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Heading 4\")}

    \n {pgettext(\"markup help\", \"Fourth level heading\")}}\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Heading 5\")}

    \n {pgettext(\"markup help\", \"Fifth level heading\")}}\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Unordered list\")}

    \n \n
  • Lorem ipsum
  • \n
  • Dolor met
  • \n
  • Vulputate lectus
  • \n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Ordered list\")}

    \n \n
  • Lorem ipsum
  • \n
  • Dolor met
  • \n
  • Vulputate lectus
  • \n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Quote text\")}

    \n \" + pgettext(\"markup help\", \"Quoted text\")}\n result={\n
    \n

    {pgettext(\"markup help\", \"Quoted text\")}

    \n
    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Quote text (BBCode)\")}

    \n \n
    \n {gettext(\"Quoted message:\")}\n
    \n
    \n

    {pgettext(\"markup help\", \"Quoted text\")}

    \n
    \n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Quote text with author (BBCode)\")}

    \n \n
    \n {pgettext(\"markup help\", \"Quote author has written:\")}\n
    \n
    \n

    {pgettext(\"markup help\", \"Quoted text\")}

    \n
    \n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Spoiler\")}

    \n \n {pgettext(\"markup help\", \"Secret text\")}\n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Inline code\")}

    \n \n {pgettext(\"markup help\", \"Inline code\")}\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Code block\")}

    \n \n alert(\"Hello world!\");\n \n }\n />\n\n
    \n\n

    \n {pgettext(\"markup help\", \"Code block with syntax highlighting\")}\n

    \n \n \n print(\"Hello world!\");\n \n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Code block (BBCode)\")}

    \n \n alert(\"Hello world!\");\n \n }\n />\n\n
    \n\n

    \n {pgettext(\n \"markup help\",\n \"Code block with syntax highlighting (BBCode)\"\n )}\n

    \n \n \n print(\"Hello world!\");\n \n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Horizontal rule\")}

    \n \n

    Lorem ipsum

    \n
    \n

    Dolor met

    \n
    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Horizontal rule (BBCode)\")}

    \n \n

    Lorem ipsum

    \n
    \n

    Dolor met

    \n
    \n }\n />\n
    \n
    \n \n {pgettext(\"modal\", \"Close\")}\n \n
    \n
    \n
    \n )\n}\n\nfunction ExampleFormatting({ markup, result }) {\n return (\n
    \n
    \n
    \n          {markup}\n        
    \n
    \n
    \n
    {result}
    \n
    \n
    \n )\n}\n\nclass ExampleFormattingSpoiler extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n reveal: false,\n }\n }\n\n render() {\n return (\n \n
    \n

    {this.props.children}

    \n
    \n {!this.state.reveal && (\n
    \n {\n this.setState({ reveal: true })\n }}\n >\n {gettext(\"Reveal spoiler\")}\n \n
    \n )}\n \n )\n }\n}\n","const URL_PATTERN = new RegExp(\"^(((ftps?)|(https?))://)\", \"i\")\n\nexport default function isUrl(str) {\n return URL_PATTERN.test(str.trim())\n}\n","import React from \"react\"\nimport modal from \"../../services/modal\"\nimport FormGroup from \"../form-group\"\nimport isUrl from \"./isUrl\"\nimport { replaceSelection } from \"./operations\"\n\nclass MarkupImageModal extends React.Component {\n constructor(props) {\n super(props)\n\n const text = props.selection.text.trim()\n const textUrl = isUrl(text)\n\n this.state = {\n error: null,\n text: textUrl ? \"\" : text,\n url: textUrl ? text : \"\",\n }\n }\n\n handleSubmit = (ev) => {\n ev.preventDefault()\n\n const { selection, update } = this.props\n const text = this.state.text.trim()\n const url = this.state.url.trim()\n\n if (url.length === 0) {\n this.setState({ error: gettext(\"This field is required.\") })\n return false\n }\n\n if (text.length > 0) {\n replaceSelection(selection, update, \"![\" + text + \"](\" + url + \")\")\n } else {\n replaceSelection(selection, update, \"!(\" + url + \")\")\n }\n\n modal.hide()\n\n return false\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"markup editor\", \"Image\")}\n

    \n
    \n
    \n
    \n \n \n this.setState({ text: event.target.value })\n }\n />\n \n \n \n this.setState({ url: event.target.value })\n }\n />\n \n
    \n
    \n \n {gettext(\"Cancel\")}\n \n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport default MarkupImageModal\n","import React from \"react\"\nimport modal from \"../../services/modal\"\nimport FormGroup from \"../form-group\"\nimport isUrl from \"./isUrl\"\nimport { replaceSelection } from \"./operations\"\n\nclass MarkupLinkModal extends React.Component {\n constructor(props) {\n super(props)\n\n const text = props.selection.text.trim()\n const textUrl = isUrl(text)\n\n this.state = {\n error: null,\n text: textUrl ? \"\" : text,\n url: textUrl ? text : \"\",\n }\n }\n\n handleSubmit = (ev) => {\n ev.preventDefault()\n\n const { selection, update } = this.props\n const text = this.state.text.trim()\n const url = this.state.url.trim()\n\n if (url.length === 0) {\n this.setState({ error: gettext(\"This field is required.\") })\n return false\n }\n\n if (text.length > 0) {\n replaceSelection(selection, update, \"[\" + text + \"](\" + url + \")\")\n } else {\n replaceSelection(selection, update, \"<\" + url + \">\")\n }\n\n modal.hide()\n\n return false\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    {pgettext(\"markup editor\", \"Link\")}

    \n
    \n
    \n
    \n \n \n this.setState({ text: event.target.value })\n }\n />\n \n \n \n this.setState({ url: event.target.value })\n }\n />\n \n
    \n
    \n \n {gettext(\"Cancel\")}\n \n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport default MarkupLinkModal\n","import React from \"react\"\nimport modal from \"../../services/modal\"\nimport FormGroup from \"../form-group\"\nimport { replaceSelection } from \"./operations\"\n\nclass MarkupQuoteModal extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n error: null,\n author: \"\",\n text: props.selection.text,\n }\n }\n\n handleSubmit = (ev) => {\n ev.preventDefault()\n\n const { selection, update } = this.props\n const author = this.state.author.trim()\n const text = this.state.text.trim()\n\n if (text.length === 0) {\n this.setState({ error: gettext(\"This field is required.\") })\n return false\n }\n\n const prefix = selection.prefix.trim().length ? \"\\n\\n\" : \"\"\n\n if (author) {\n replaceSelection(\n selection,\n update,\n prefix + '[quote=\"' + author + '\"]\\n' + text + \"\\n[/quote]\\n\\n\"\n )\n } else {\n replaceSelection(\n selection,\n update,\n prefix + \"[quote]\\n\" + text + \"\\n[/quote]\\n\\n\"\n )\n }\n\n modal.hide()\n\n return false\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"markup editor\", \"Quote\")}\n

    \n
    \n
    \n
    \n \n \n this.setState({ author: event.target.value })\n }\n />\n \n \n \n this.setState({ text: event.target.value })\n }\n />\n \n
    \n
    \n \n {gettext(\"Cancel\")}\n \n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport default MarkupQuoteModal\n","import React from \"react\"\n\nconst MarkupEditorButton = ({ disabled, icon, title, onClick }) => (\n \n {icon}\n \n)\n\nexport default MarkupEditorButton\n","import moment from \"moment\"\nimport misago from \"../../\"\nimport ajax from \"../../services/ajax\"\nimport snackbar from \"../../services/snackbar\"\nimport formatFilesize from \"../../utils/file-size\"\nimport getRandomString from \"../../utils/getRandomString\"\n\nconst ID_LEN = 32\n\nconst uploadFile = (file, setState) => {\n const maxSize = misago.get(\"user\").acl.max_attachment_size * 1024\n\n if (file.size > maxSize) {\n snackbar.error(\n interpolate(\n pgettext(\n \"markup editor\",\n \"File %(filename)s is bigger than %(limit)s.\"\n ),\n { filename: file.name, limit: formatFilesize(maxSize) },\n true\n )\n )\n\n return\n }\n\n let upload = {\n id: null,\n key: getRandomString(ID_LEN),\n error: null,\n uploaded_on: null,\n progress: 0,\n filename: file.name,\n filetype: null,\n is_image: false,\n size: file.size,\n url: null,\n uploader_name: null,\n }\n\n setState(({ attachments }) => {\n return { attachments: [upload].concat(attachments) }\n })\n\n const refreshState = () => {\n setState(({ attachments }) => {\n return { attachments: attachments.concat() }\n })\n }\n\n const data = new FormData()\n data.append(\"upload\", file)\n\n ajax\n .upload(misago.get(\"ATTACHMENTS_API\"), data, (progress) => {\n upload.progress = progress\n refreshState()\n })\n .then(\n (data) => {\n Object.assign(upload, data, { uploaded_on: moment(data.uploaded_on) })\n refreshState()\n },\n (rejection) => {\n if (rejection.status === 400 || rejection.status === 413) {\n upload.error = rejection.detail\n snackbar.error(rejection.detail)\n refreshState()\n } else {\n snackbar.apiError(rejection)\n }\n }\n )\n}\n\nexport default uploadFile\n","import React from \"react\"\nimport misago from \"../../\"\nimport modal from \"../../services/modal\"\nimport MarkupCodeModal from \"./MarkupCodeModal\"\nimport MarkupFormattingHelpModal from \"./MarkupFormattingHelpModal\"\nimport MarkupImageModal from \"./MarkupImageModal\"\nimport MarkupLinkModal from \"./MarkupLinkModal\"\nimport MarkupQuoteModal from \"./MarkupQuoteModal\"\nimport MarkupEditorButton from \"./MarkupEditorButton\"\nimport { getSelection, replaceSelection, wrapSelection } from \"./operations\"\nimport uploadFile from \"./uploadFile\"\n\nconst MarkupEditorToolbar = ({\n disabled,\n element,\n update,\n updateAttachments,\n}) => {\n const actions = [\n {\n name: pgettext(\"markup editor\", \"Strong\"),\n icon: \"format_bold\",\n onClick: () => {\n wrapSelection(\n getSelection(element),\n update,\n \"**\",\n \"**\",\n pgettext(\"example markup\", \"Strong text\")\n )\n },\n },\n {\n name: pgettext(\"markup editor\", \"Emphasis\"),\n icon: \"format_italic\",\n onClick: () => {\n wrapSelection(\n getSelection(element),\n update,\n \"*\",\n \"*\",\n pgettext(\"example markup\", \"Text with emphasis\")\n )\n },\n },\n {\n name: pgettext(\"markup editor\", \"Strikethrough\"),\n icon: \"format_strikethrough\",\n onClick: () => {\n wrapSelection(\n getSelection(element),\n update,\n \"~~\",\n \"~~\",\n pgettext(\"example markup\", \"Text with strikethrough\")\n )\n },\n },\n {\n name: pgettext(\"markup editor\", \"Horizontal ruler\"),\n icon: \"remove\",\n onClick: () => {\n replaceSelection(getSelection(element), update, \"\\n\\n- - -\\n\\n\")\n },\n },\n {\n name: pgettext(\"markup editor\", \"Link\"),\n icon: \"insert_link\",\n onClick: () => {\n const selection = getSelection(element)\n modal.show(\n \n )\n },\n },\n {\n name: pgettext(\"markup editor\", \"Image\"),\n icon: \"insert_photo\",\n onClick: () => {\n const selection = getSelection(element)\n modal.show(\n \n )\n },\n },\n {\n name: pgettext(\"markup editor\", \"Quote\"),\n icon: \"format_quote\",\n onClick: () => {\n const selection = getSelection(element)\n modal.show(\n \n )\n },\n },\n {\n name: pgettext(\"markup editor\", \"Spoiler\"),\n icon: \"visibility_off\",\n onClick: () => {\n insertSpoiler(element, update)\n },\n },\n {\n name: pgettext(\"markup editor\", \"Code\"),\n icon: \"code\",\n onClick: () => {\n const selection = getSelection(element)\n modal.show(\n \n )\n },\n },\n ]\n\n if (misago.get(\"user\").acl.max_attachment_size) {\n actions.push({\n name: pgettext(\"markup editor\", \"Upload file\"),\n icon: \"file_upload\",\n onClick: () => uploadFiles(updateAttachments),\n })\n }\n\n return (\n
    \n
    \n {actions.map(({ name, icon, onClick }) => (\n \n ))}\n
    \n
    \n
    \n \n more_vert\n \n
      \n {actions.map(({ name, icon, onClick }) => (\n
    • \n \n {icon}\n {name}\n \n
    • \n ))}\n
    \n
    \n {\n modal.show()\n }}\n />\n
    \n
    \n )\n}\n\nconst insertSpoiler = (element, update) => {\n const selection = getSelection(element)\n const prefix = selection.prefix.trim().length ? \"\\n\\n\" : \"\"\n\n wrapSelection(\n selection,\n update,\n prefix + \"[spoiler]\\n\",\n \"\\n[/spoiler]\\n\\n\",\n pgettext(\"markup editor\", \"Spoiler text\")\n )\n}\n\nconst uploadFiles = (setState) => {\n const input = document.createElement(\"input\")\n input.type = \"file\"\n input.multiple = \"multiple\"\n\n input.addEventListener(\"change\", function () {\n for (let i = 0; i < input.files.length; i++) {\n uploadFile(input.files[i], setState)\n }\n })\n\n input.click()\n}\n\nexport default MarkupEditorToolbar\n","import React from \"react\"\nimport classnames from \"classnames\"\n\nimport misago from \"../../\"\nimport ajax from \"../../services/ajax\"\nimport snackbar from \"../../services/snackbar\"\nimport MisagoMarkup from \"../misago-markup\"\nimport MarkupEditorAttachments from \"./MarkupEditorAttachments\"\nimport MarkupEditorFooter from \"./MarkupEditorFooter\"\nimport MarkupEditorToolbar from \"./MarkupEditorToolbar\"\nimport uploadFile from \"./uploadFile\"\n\nclass MarkupEditor extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n element: null,\n focused: false,\n loading: false,\n preview: false,\n parsed: null,\n }\n }\n\n showPreview = () => {\n if (this.state.loading) return\n\n this.setState({ loading: true, preview: true, element: null })\n\n ajax.post(misago.get(\"PARSE_MARKUP_API\"), { post: this.props.value }).then(\n (data) => {\n this.setState({ loading: false, parsed: data.parsed })\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n } else {\n snackbar.apiError(rejection)\n }\n\n this.setState({ loading: false, preview: false })\n }\n )\n }\n\n closePreview = () => {\n this.setState({ loading: false, preview: false })\n }\n\n onDrop = (event) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (!event.dataTransfer.files) return\n\n const { onAttachmentsChange: setState } = this.props\n\n if (misago.get(\"user\").acl.max_attachment_size) {\n for (let i = 0; i < event.dataTransfer.files.length; i++) {\n const file = event.dataTransfer.files[i]\n uploadFile(file, setState)\n }\n }\n }\n\n onPaste = (event) => {\n const { onAttachmentsChange: setState } = this.props\n\n const files = []\n for (let i = 0; i < event.clipboardData.items.length; i++) {\n const item = event.clipboardData.items[i]\n if (item.kind === \"file\") {\n files.push(item.getAsFile())\n }\n }\n\n if (files.length) {\n event.preventDefault()\n event.stopPropagation()\n\n if (misago.get(\"user\").acl.max_attachment_size) {\n for (let i = 0; i < files.length; i++) {\n uploadFile(files[i], setState)\n }\n }\n }\n }\n\n render = () => (\n \n this.props.onChange({ target: { value } })}\n updateAttachments={this.props.onAttachmentsChange}\n />\n {this.state.preview ? (\n
    \n {this.state.loading ? (\n
    \n
    \n \n
    \n
    \n ) : (\n \n )}\n
    \n ) : (\n {\n if (element && this.state.element !== element) {\n this.setState({ element })\n setMentions(this.props, element)\n }\n }}\n onChange={this.props.onChange}\n onDrop={this.onDrop}\n onFocus={() => this.setState({ focused: true })}\n onPaste={this.onPaste}\n onBlur={() => this.setState({ focused: false })}\n />\n )}\n {this.props.attachments.length > 0 && (\n this.props.onChange({ target: { value } })}\n />\n )}\n \n
    \n )\n}\n\nfunction setMentions(props, element) {\n $(element).atwho({\n at: \"@\",\n displayTpl: '
  • \"\"${username}
  • ',\n insertTpl: \"@${username}\",\n searchKey: \"username\",\n callbacks: {\n remoteFilter: function (query, callback) {\n $.getJSON(misago.get(\"MENTION_API\"), { q: query }, callback)\n },\n },\n })\n\n $(element).on(\"inserted.atwho\", (event, _storage, source, controller) => {\n const { query } = controller\n const username = source.target.innerText.trim()\n const prefix = event.target.value.substr(0, query.headPos)\n const suffix = event.target.value.substr(query.endPos)\n\n event.target.value = prefix + username + suffix\n props.onChange(event)\n\n const caret = query.headPos + username.length\n event.target.setSelectionRange(caret, caret)\n event.target.focus()\n })\n}\n\nexport default MarkupEditor\n","import MarkupEditor from \"./MarkupEditor\"\n\nexport default MarkupEditor\n","import React from \"react\"\nimport classnames from \"classnames\"\n\nconst CLASS_ACTIVE = \"posting-active\"\nconst CLASS_DEFAULT = \"posting-default\"\nconst CLASS_MINIMIZED = \"posting-minimized\"\nconst CLASS_FULLSCREEN = \"posting-fullscreen\"\n\nclass PostingDialog extends React.Component {\n componentDidMount() {\n document.body.classList.add(CLASS_ACTIVE, CLASS_DEFAULT)\n }\n\n componentWillUnmount() {\n document.body.classList.remove(\n CLASS_ACTIVE,\n CLASS_DEFAULT,\n CLASS_MINIMIZED,\n CLASS_FULLSCREEN\n )\n }\n\n componentWillReceiveProps({ fullscreen, minimized }) {\n if (minimized) {\n document.body.classList.remove(CLASS_DEFAULT, CLASS_FULLSCREEN)\n document.body.classList.add(CLASS_MINIMIZED)\n } else {\n if (fullscreen) {\n document.body.classList.remove(CLASS_DEFAULT, CLASS_MINIMIZED)\n document.body.classList.add(CLASS_FULLSCREEN)\n } else {\n document.body.classList.remove(CLASS_FULLSCREEN, CLASS_MINIMIZED)\n document.body.classList.add(CLASS_DEFAULT)\n }\n }\n }\n\n render() {\n const { children, fullscreen, minimized } = this.props\n\n return (\n \n
    {children}
    \n \n )\n }\n}\n\nexport default PostingDialog\n","import React from \"react\"\n\nconst PostingDialogBody = ({ children }) => (\n
    {children}
    \n)\n\nexport default PostingDialogBody\n","import React from \"react\"\n\nconst PostingDialogError = ({ close, message }) => (\n
    \n
    \n error_outlined\n
    \n
    \n

    {message}

    \n \n
    \n
    \n)\n\nexport default PostingDialogError\n","import React from \"react\"\n\nconst PostingDialogHeader = ({\n children,\n close,\n fullscreen,\n minimize,\n minimized,\n fullscreenEnter,\n fullscreenExit,\n open,\n}) => (\n
    \n
    {children}
    \n {minimized ? (\n \n expand_less\n \n ) : (\n \n expand_more\n \n )}\n {fullscreen ? (\n \n fullscreen_exit\n \n ) : (\n \n fullscreen\n \n )}\n \n close\n \n
    \n)\n\nexport default PostingDialogHeader\n","import React from \"react\"\n\nexport default function PostingThreadOptions({\n isClosed,\n isHidden,\n isPinned,\n disabled,\n options,\n close,\n open,\n hide,\n unhide,\n pinGlobally,\n pinLocally,\n unpin,\n}) {\n const icons = getIcons(isClosed, isHidden, isPinned)\n\n return (\n
    \n \n {icons.length > 0 ? (\n \n {icons.map((icon) => (\n \n {icon}\n \n ))}\n \n ) : (\n more_horiz\n )}\n \n
      \n {options.pin === 2 && isPinned !== 2 && (\n
    • \n \n bookmark\n {pgettext(\"post thread\", \"Pinned globally\")}\n \n
    • \n )}\n {options.pin >= isPinned && isPinned !== 1 && (\n
    • \n \n bookmark_outline\n {pgettext(\"post thread\", \"Pinned locally\")}\n \n
    • \n )}\n {options.pin >= isPinned && isPinned !== 0 && (\n
    • \n \n radio_button_unchecked\n {pgettext(\"post thread\", \"Not pinned\")}\n \n
    • \n )}\n {options.close && !!isClosed && (\n
    • \n \n lock_outline\n {pgettext(\"post thread\", \"Open\")}\n \n
    • \n )}\n {options.close && !isClosed && (\n
    • \n \n lock\n {pgettext(\"post thread\", \"Closed\")}\n \n
    • \n )}\n {options.hide && !!isHidden && (\n
    • \n \n visibility\n {pgettext(\"post thread\", \"Visible\")}\n \n
    • \n )}\n {options.hide && !isHidden && (\n
    • \n \n visibility_off\n {pgettext(\"post thread\", \"Hidden\")}\n \n
    • \n )}\n
    \n
    \n )\n}\n\nfunction getIcons(closed, hidden, pinned) {\n const icons = []\n if (pinned === 2) icons.push(\"bookmark\")\n if (pinned === 1) icons.push(\"bookmark_outline\")\n if (closed) icons.push(\"lock\")\n if (hidden) icons.push(\"visibility_off\")\n return icons\n}\n","import React from \"react\"\nimport CategorySelect from \"misago/components/category-select\"\nimport Form from \"misago/components/form\"\nimport * as attachments from \"./utils/attachments\"\nimport { getPostValidators, getTitleValidators } from \"./utils/validators\"\nimport ajax from \"misago/services/ajax\"\nimport posting from \"misago/services/posting\"\nimport snackbar from \"misago/services/snackbar\"\nimport MarkupEditor from \"../MarkupEditor\"\nimport { Toolbar, ToolbarItem, ToolbarSection } from \"../Toolbar\"\nimport PostingDialog from \"./PostingDialog\"\nimport PostingDialogBody from \"./PostingDialogBody\"\nimport PostingDialogError from \"./PostingDialogError\"\nimport PostingDialogHeader from \"./PostingDialogHeader\"\nimport PostingThreadOptions from \"./PostingThreadOptions\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isReady: false,\n isLoading: false,\n\n error: null,\n\n minimized: false,\n fullscreen: false,\n\n options: null,\n\n title: \"\",\n category: props.category || null,\n categories: [],\n post: \"\",\n attachments: [],\n close: false,\n hide: false,\n pin: 0,\n\n validators: {\n title: getTitleValidators(),\n post: getPostValidators(),\n },\n errors: {},\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.config).then(this.loadSuccess, this.loadError)\n }\n\n loadSuccess = (data) => {\n let category = null\n let options = null\n\n // hydrate categories, extract posting options\n const categories = data.map((item) => {\n // pick first category that allows posting and if it may, override it with initial one\n if (\n item.post !== false &&\n (!category || item.id == this.state.category)\n ) {\n category = item.id\n options = item.post\n }\n\n return Object.assign(item, {\n disabled: item.post === false,\n label: item.name,\n value: item.id,\n })\n })\n\n this.setState({\n isReady: true,\n options,\n\n categories,\n category,\n })\n }\n\n loadError = (rejection) => {\n this.setState({\n error: rejection.detail,\n })\n }\n\n onCancel = () => {\n const formEmpty = !!(\n this.state.post.length === 0 &&\n this.state.title.length === 0 &&\n this.state.attachments.length === 0\n )\n\n if (formEmpty) {\n this.minimize()\n return posting.close()\n }\n\n const cancel = window.confirm(\n pgettext(\"post thread\", \"Are you sure you want to discard thread?\")\n )\n if (cancel) {\n this.minimize()\n posting.close()\n }\n }\n\n onTitleChange = (event) => {\n this.changeValue(\"title\", event.target.value)\n }\n\n onCategoryChange = (event) => {\n const category = this.state.categories.find((item) => {\n return event.target.value == item.value\n })\n\n // if selected pin is greater than allowed, reduce it\n let pin = this.state.pin\n if (category.post.pin && category.post.pin < pin) {\n pin = category.post.pin\n }\n\n this.setState({\n category: category.id,\n categoryOptions: category.post,\n\n pin,\n })\n }\n\n onPostChange = (event) => {\n this.changeValue(\"post\", event.target.value)\n }\n\n onAttachmentsChange = (attachments) => {\n this.setState(attachments)\n }\n\n onClose = () => {\n this.changeValue(\"close\", true)\n }\n\n onOpen = () => {\n this.changeValue(\"close\", false)\n }\n\n onPinGlobally = () => {\n this.changeValue(\"pin\", 2)\n }\n\n onPinLocally = () => {\n this.changeValue(\"pin\", 1)\n }\n\n onUnpin = () => {\n this.changeValue(\"pin\", 0)\n }\n\n onHide = () => {\n this.changeValue(\"hide\", true)\n }\n\n onUnhide = () => {\n this.changeValue(\"hide\", false)\n }\n\n close = () => {\n this.minimize()\n posting.close()\n }\n\n minimize = () => {\n this.setState({ fullscreen: false, minimized: true })\n }\n\n open = () => {\n this.setState({ minimized: false })\n if (this.state.fullscreen) {\n }\n }\n\n fullscreenEnter = () => {\n this.setState({ fullscreen: true, minimized: false })\n }\n\n fullscreenExit = () => {\n this.setState({ fullscreen: false, minimized: false })\n }\n\n clean() {\n if (!this.state.title.trim().length) {\n snackbar.error(\n pgettext(\"posting form\", \"You have to enter thread title.\")\n )\n return false\n }\n\n if (!this.state.post.trim().length) {\n snackbar.error(pgettext(\"posting form\", \"You have to enter a message.\"))\n return false\n }\n\n const errors = this.validate()\n\n if (errors.title) {\n snackbar.error(errors.title[0])\n return false\n }\n\n if (errors.post) {\n snackbar.error(errors.post[0])\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.post(this.props.submit, {\n title: this.state.title,\n category: this.state.category,\n post: this.state.post,\n attachments: attachments.clean(this.state.attachments),\n close: this.state.close,\n hide: this.state.hide,\n pin: this.state.pin,\n })\n }\n\n handleSuccess(success) {\n this.setState({ isLoading: true })\n this.close()\n\n snackbar.success(pgettext(\"post thread\", \"Your thread has been posted.\"))\n window.location = success.url\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n const errors = [].concat(\n rejection.non_field_errors || [],\n rejection.category || [],\n rejection.title || [],\n rejection.post || [],\n rejection.attachments || []\n )\n\n snackbar.error(errors[0])\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n const dialogProps = {\n minimized: this.state.minimized,\n minimize: this.minimize,\n open: this.open,\n\n fullscreen: this.state.fullscreen,\n fullscreenEnter: this.fullscreenEnter,\n fullscreenExit: this.fullscreenExit,\n\n close: this.onCancel,\n }\n\n if (this.state.error) {\n return (\n \n \n \n )\n }\n\n if (!this.state.isReady) {\n return (\n \n
    \n \n \n \n \n \n \n \n \n \n \n \n \n {}}\n onChange={() => {}}\n />\n
    \n
    \n )\n }\n\n const showOptions = !!(\n this.state.options.close ||\n this.state.options.hide ||\n this.state.options.pin\n )\n\n return (\n \n
    \n \n \n \n \n \n \n \n \n \n \n {showOptions && (\n \n \n \n )}\n \n \n \n \n
    \n )\n }\n}\n\nconst PostingDialogStart = ({\n children,\n close,\n minimized,\n minimize,\n open,\n fullscreen,\n fullscreenEnter,\n fullscreenExit,\n}) => (\n \n \n {pgettext(\"post thread\", \"Start new thread\")}\n \n {children}\n \n)\n","export default function (usernames) {\n const normalisedNames = usernames\n .split(\",\")\n .map((i) => i.trim().toLowerCase())\n const removedBlanks = normalisedNames.filter((i) => i.length > 0)\n const removedDuplicates = removedBlanks.filter((name, pos) => {\n return removedBlanks.indexOf(name) == pos\n })\n\n return removedDuplicates\n}\n","import React from \"react\"\nimport Form from \"misago/components/form\"\nimport * as attachments from \"./utils/attachments\"\nimport cleanUsernames from \"./utils/usernames\"\nimport { getPostValidators, getTitleValidators } from \"./utils/validators\"\nimport ajax from \"misago/services/ajax\"\nimport posting from \"misago/services/posting\"\nimport snackbar from \"misago/services/snackbar\"\nimport MarkupEditor from \"../MarkupEditor\"\nimport { Toolbar, ToolbarItem, ToolbarSection } from \"../Toolbar\"\nimport PostingDialog from \"./PostingDialog\"\nimport PostingDialogBody from \"./PostingDialogBody\"\nimport PostingDialogHeader from \"./PostingDialogHeader\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n const to = (props.to || []).map((user) => user.username).join(\", \")\n\n this.state = {\n isLoading: false,\n\n error: null,\n\n minimized: false,\n fullscreen: false,\n\n to: to,\n title: \"\",\n post: \"\",\n attachments: [],\n\n validators: {\n title: getTitleValidators(),\n post: getPostValidators(),\n },\n errors: {},\n }\n }\n\n onCancel = () => {\n const formEmpty = !!(\n this.state.post.length === 0 &&\n this.state.title.length === 0 &&\n this.state.to.length === 0 &&\n this.state.attachments.length === 0\n )\n\n if (formEmpty) {\n return this.close()\n }\n\n const cancel = window.confirm(\n pgettext(\n \"post thread\",\n \"Are you sure you want to discard private thread?\"\n )\n )\n if (cancel) {\n this.close()\n }\n }\n\n onToChange = (event) => {\n this.changeValue(\"to\", event.target.value)\n }\n\n onTitleChange = (event) => {\n this.changeValue(\"title\", event.target.value)\n }\n\n onPostChange = (event) => {\n this.changeValue(\"post\", event.target.value)\n }\n\n onAttachmentsChange = (attachments) => {\n this.setState(attachments)\n }\n\n clean() {\n if (!cleanUsernames(this.state.to).length) {\n snackbar.error(\n pgettext(\"posting form\", \"You have to enter at least one recipient.\")\n )\n return false\n }\n\n if (!this.state.title.trim().length) {\n snackbar.error(\n pgettext(\"posting form\", \"You have to enter thread title.\")\n )\n return false\n }\n\n if (!this.state.post.trim().length) {\n snackbar.error(pgettext(\"posting form\", \"You have to enter a message.\"))\n return false\n }\n\n const errors = this.validate()\n\n if (errors.title) {\n snackbar.error(errors.title[0])\n return false\n }\n\n if (errors.post) {\n snackbar.error(errors.post[0])\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.post(this.props.submit, {\n to: cleanUsernames(this.state.to),\n title: this.state.title,\n post: this.state.post,\n attachments: attachments.clean(this.state.attachments),\n })\n }\n\n handleSuccess(success) {\n this.setState({ isLoading: true })\n this.close()\n\n snackbar.success(pgettext(\"post thread\", \"Your thread has been posted.\"))\n window.location = success.url\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n const errors = [].concat(\n rejection.non_field_errors || [],\n rejection.to || [],\n rejection.title || [],\n rejection.post || [],\n rejection.attachments || []\n )\n\n snackbar.error(errors[0])\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n close = () => {\n this.minimize()\n posting.close()\n }\n\n minimize = () => {\n this.setState({ fullscreen: false, minimized: true })\n }\n\n open = () => {\n this.setState({ minimized: false })\n if (this.state.fullscreen) {\n }\n }\n\n fullscreenEnter = () => {\n this.setState({ fullscreen: true, minimized: false })\n }\n\n fullscreenExit = () => {\n this.setState({ fullscreen: false, minimized: false })\n }\n\n render() {\n const dialogProps = {\n minimized: this.state.minimized,\n minimize: this.minimize,\n open: this.open,\n\n fullscreen: this.state.fullscreen,\n fullscreenEnter: this.fullscreenEnter,\n fullscreenExit: this.fullscreenExit,\n\n close: this.onCancel,\n }\n\n return (\n \n
    \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    \n )\n }\n}\n\nconst PostingDialogStartPrivate = ({\n children,\n close,\n minimized,\n minimize,\n open,\n fullscreen,\n fullscreenEnter,\n fullscreenExit,\n}) => (\n \n \n {pgettext(\"post thread\", \"Start private thread\")}\n \n {children}\n \n)\n","import React from \"react\"\nimport Form from \"misago/components/form\"\nimport * as attachments from \"./utils/attachments\"\nimport { getPostValidators } from \"./utils/validators\"\nimport ajax from \"misago/services/ajax\"\nimport posting from \"misago/services/posting\"\nimport snackbar from \"misago/services/snackbar\"\nimport MarkupEditor from \"../MarkupEditor\"\nimport PostingDialog from \"./PostingDialog\"\nimport PostingDialogBody from \"./PostingDialogBody\"\nimport PostingDialogError from \"./PostingDialogError\"\nimport PostingDialogHeader from \"./PostingDialogHeader\"\nimport { clearGlobalState, setGlobalState } from \"./globalState\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isReady: false,\n isLoading: false,\n\n error: null,\n\n minimized: false,\n fullscreen: false,\n\n post: this.props.default || \"\",\n attachments: [],\n\n validators: {\n post: getPostValidators(),\n },\n errors: {},\n }\n\n this.quoteText = \"\"\n }\n\n componentDidMount() {\n ajax\n .get(this.props.config, this.props.context || null)\n .then(this.loadSuccess, this.loadError)\n\n setGlobalState(false, this.onQuote)\n }\n\n componentWillUnmount() {\n clearGlobalState()\n }\n\n componentWillReceiveProps(nextProps) {\n const context = this.props.context\n const newContext = nextProps.context\n\n // User clicked \"reply\" instead of \"quote\"\n if (context && newContext && !newContext.reply) return\n\n ajax\n .get(nextProps.config, nextProps.context || null)\n .then(this.appendData, snackbar.apiError)\n }\n\n loadSuccess = (data) => {\n this.setState({\n isReady: true,\n\n post: data.post\n ? '[quote=\"@' + data.poster + '\"]\\n' + data.post + \"\\n[/quote]\"\n : this.state.post,\n })\n\n this.quoteText = data.post\n ? '[quote=\"@' + data.poster + '\"]\\n' + data.post + \"\\n[/quote]\"\n : this.state.post\n }\n\n loadError = (rejection) => {\n this.setState({\n error: rejection.detail,\n })\n }\n\n appendData = (data) => {\n const newPost = data.post\n ? '[quote=\"@' + data.poster + '\"]\\n' + data.post + \"\\n[/quote]\\n\\n\"\n : \"\"\n\n this.setState((prevState, props) => {\n if (prevState.post.length > 0) {\n return {\n post: prevState.post.trim() + \"\\n\\n\" + newPost,\n }\n }\n\n return {\n post: newPost,\n }\n })\n\n this.open()\n }\n\n onCancel = () => {\n // If only the quote text is on editor user didn't add anything\n // so no changes to discard\n const onlyQuoteTextInEditor = this.state.post === this.quoteText\n\n if (onlyQuoteTextInEditor && this.state.attachments.length === 0) {\n return this.close()\n }\n\n const cancel = window.confirm(\n pgettext(\"post reply\", \"Are you sure you want to discard your reply?\")\n )\n if (cancel) {\n this.close()\n }\n }\n\n onPostChange = (event) => {\n this.changeValue(\"post\", event.target.value)\n }\n\n onAttachmentsChange = (attachments) => {\n this.setState(attachments)\n }\n\n onQuote = (quote) => {\n this.setState(({ post }) => {\n if (post.length > 0) {\n return { post: post.trim() + \"\\n\\n\" + quote }\n }\n\n return { post: quote }\n })\n\n this.open()\n }\n\n clean() {\n if (!this.state.post.trim().length) {\n snackbar.error(pgettext(\"posting form\", \"You have to enter a message.\"))\n return false\n }\n\n const errors = this.validate()\n\n if (errors.post) {\n snackbar.error(errors.post[0])\n return false\n }\n\n return true\n }\n\n send() {\n setGlobalState(true, this.onQuote)\n\n return ajax.post(this.props.submit, {\n post: this.state.post,\n attachments: attachments.clean(this.state.attachments),\n })\n }\n\n handleSuccess(success) {\n this.setState({ isLoading: true })\n this.close()\n\n setGlobalState(false, this.onQuote)\n\n snackbar.success(pgettext(\"post reply\", \"Your reply has been posted.\"))\n window.location = success.url.index\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n const errors = [].concat(\n rejection.non_field_errors || [],\n rejection.post || [],\n rejection.attachments || []\n )\n\n snackbar.error(errors[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n setGlobalState(false, this.onQuote)\n }\n\n close = () => {\n this.minimize()\n posting.close()\n }\n\n minimize = () => {\n this.setState({ fullscreen: false, minimized: true })\n }\n\n open = () => {\n this.setState({ minimized: false })\n if (this.state.fullscreen) {\n }\n }\n\n fullscreenEnter = () => {\n this.setState({ fullscreen: true, minimized: false })\n }\n\n fullscreenExit = () => {\n this.setState({ fullscreen: false, minimized: false })\n }\n\n render() {\n const dialogProps = {\n thread: this.props.thread,\n\n minimized: this.state.minimized,\n minimize: this.minimize,\n open: this.open,\n\n fullscreen: this.state.fullscreen,\n fullscreenEnter: this.fullscreenEnter,\n fullscreenExit: this.fullscreenExit,\n\n close: this.onCancel,\n }\n\n if (this.state.error) {\n return (\n \n \n \n )\n }\n\n if (!this.state.isReady) {\n return (\n \n
    \n {}}\n onChange={() => {}}\n />\n
    \n
    \n )\n }\n\n return (\n \n \n \n \n \n )\n }\n}\n\nconst PostingDialogReply = ({\n children,\n close,\n minimized,\n minimize,\n open,\n fullscreen,\n fullscreenEnter,\n fullscreenExit,\n thread,\n}) => (\n \n \n {interpolate(\n pgettext(\"post reply\", \"Reply to: %(thread)s\"),\n { thread: thread.title },\n true\n )}\n \n {children}\n \n)\n","import React from \"react\"\nimport Form from \"misago/components/form\"\nimport * as attachments from \"./utils/attachments\"\nimport { getPostValidators } from \"./utils/validators\"\nimport ajax from \"misago/services/ajax\"\nimport posting from \"misago/services/posting\"\nimport snackbar from \"misago/services/snackbar\"\nimport MarkupEditor from \"../MarkupEditor\"\nimport PostingDialog from \"./PostingDialog\"\nimport PostingDialogBody from \"./PostingDialogBody\"\nimport PostingDialogError from \"./PostingDialogError\"\nimport PostingDialogHeader from \"./PostingDialogHeader\"\nimport { clearGlobalState, setGlobalState } from \"./globalState\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isReady: false,\n isLoading: false,\n\n error: false,\n\n minimized: false,\n fullscreen: false,\n\n post: \"\",\n attachments: [],\n protect: false,\n\n canProtect: false,\n\n validators: {\n post: getPostValidators(),\n },\n errors: {},\n }\n\n this.originalPost = \"\"\n }\n\n componentDidMount() {\n ajax.get(this.props.config).then(this.loadSuccess, this.loadError)\n\n setGlobalState(false, this.onQuote)\n }\n\n componentWillUnmount() {\n clearGlobalState()\n }\n\n componentWillReceiveProps(nextProps) {\n const context = this.props.context\n const newContext = nextProps.context\n\n if (context && newContext && context.reply === newContext.reply) return\n\n ajax\n .get(nextProps.config, nextProps.context || null)\n .then(this.appendData, snackbar.apiError)\n }\n\n loadSuccess = (data) => {\n this.originalPost = data.post\n\n this.setState({\n isReady: true,\n\n post: data.post,\n attachments: attachments.hydrate(data.attachments),\n protect: data.is_protected,\n\n canProtect: data.can_protect,\n })\n }\n\n loadError = (rejection) => {\n this.setState({\n error: rejection.detail,\n })\n }\n\n appendData = (data) => {\n const newPost = data.post\n ? '[quote=\"@' + data.poster + '\"]\\n' + data.post + \"\\n[/quote]\\n\\n\"\n : \"\"\n\n this.setState((prevState, props) => {\n if (prevState.post.length > 0) {\n return {\n post: prevState.post.trim() + \"\\n\\n\" + newPost,\n }\n }\n\n return {\n post: newPost,\n }\n })\n\n this.open()\n }\n\n onCancel = () => {\n const originalPostSameAsCurrentPost =\n this.state.originalPost === this.state.post\n const noAttachementsAdded = this.state.attachments.length === 0\n\n if (originalPostSameAsCurrentPost && noAttachementsAdded) {\n return this.close()\n }\n\n const cancel = window.confirm(\n pgettext(\"edit reply\", \"Are you sure you want to discard changes?\")\n )\n if (cancel) {\n this.close()\n }\n }\n\n onProtect = () => {\n this.setState({\n protect: true,\n })\n }\n\n onUnprotect = () => {\n this.setState({\n protect: false,\n })\n }\n\n onPostChange = (event) => {\n this.changeValue(\"post\", event.target.value)\n }\n\n onAttachmentsChange = (attachments) => {\n this.setState(attachments)\n }\n\n onQuote = (quote) => {\n this.setState(({ post }) => {\n if (post.length > 0) {\n return { post: post.trim() + \"\\n\\n\" + quote }\n }\n\n return { post: quote }\n })\n\n this.open()\n }\n\n clean() {\n if (!this.state.post.trim().length) {\n snackbar.error(pgettext(\"posting form\", \"You have to enter a message.\"))\n return false\n }\n\n const errors = this.validate()\n\n if (errors.post) {\n snackbar.error(errors.post[0])\n return false\n }\n\n return true\n }\n\n send() {\n setGlobalState(true, this.onQuote)\n\n return ajax.put(this.props.submit, {\n post: this.state.post,\n attachments: attachments.clean(this.state.attachments),\n protect: this.state.protect,\n })\n }\n\n handleSuccess(success) {\n this.setState({ isLoading: true })\n this.close()\n\n setGlobalState(false, this.onQuote)\n\n snackbar.success(pgettext(\"edit reply\", \"Reply has been edited.\"))\n window.location = success.url.index\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n const errors = [].concat(\n rejection.non_field_errors || [],\n rejection.category || [],\n rejection.title || [],\n rejection.post || [],\n rejection.attachments || []\n )\n\n snackbar.error(errors[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n setGlobalState(false, this.onQuote)\n }\n\n close = () => {\n this.minimize()\n posting.close()\n }\n\n minimize = () => {\n this.setState({ fullscreen: false, minimized: true })\n }\n\n open = () => {\n this.setState({ minimized: false })\n if (this.state.fullscreen) {\n }\n }\n\n fullscreenEnter = () => {\n this.setState({ fullscreen: true, minimized: false })\n }\n\n fullscreenExit = () => {\n this.setState({ fullscreen: false, minimized: false })\n }\n\n render() {\n const dialogProps = {\n post: this.props.post,\n\n minimized: this.state.minimized,\n minimize: this.minimize,\n open: this.open,\n\n fullscreen: this.state.fullscreen,\n fullscreenEnter: this.fullscreenEnter,\n fullscreenExit: this.fullscreenExit,\n\n close: this.onCancel,\n }\n\n if (this.state.error) {\n return (\n \n \n \n )\n }\n\n if (!this.state.isReady) {\n return (\n \n
    \n {}}\n onChange={() => {}}\n />\n
    \n
    \n )\n }\n\n return (\n \n \n this.setState({ protect: true })}\n disableProtection={() => this.setState({ protect: false })}\n value={this.state.post}\n submitText={pgettext(\"edit reply submit\", \"Edit reply\")}\n disabled={this.state.isLoading}\n onAttachmentsChange={this.onAttachmentsChange}\n onChange={this.onPostChange}\n />\n \n \n )\n }\n}\n\nconst PostingDialogEditReply = ({\n children,\n close,\n minimized,\n minimize,\n open,\n fullscreen,\n fullscreenEnter,\n fullscreenExit,\n post,\n}) => (\n \n \n {interpolate(\n pgettext(\"edit reply\", \"Edit reply by %(poster)s from %(date)s\"),\n {\n poster: post.poster ? post.poster.username : post.poster_name,\n date: post.posted_on.fromNow(),\n },\n true\n )}\n \n {children}\n \n)\n","import React from \"react\"\nimport PostingQuoteSelection from \"./PostingQuoteSelection\"\nimport getQuoteMarkup from \"./getQuoteMarkup\"\nimport { clearGlobalState, getGlobalState, setGlobalState } from \"./globalState\"\nimport Start from \"./start\"\nimport StartPrivate from \"./start-private\"\nimport Reply from \"./reply\"\nimport Edit from \"./edit\"\n\nexport default function (props) {\n switch (props.mode) {\n case \"START\":\n return \n\n case \"START_PRIVATE\":\n return \n\n case \"REPLY\":\n return \n\n case \"EDIT\":\n return \n\n default:\n return null\n }\n}\n\nexport {\n PostingQuoteSelection,\n clearGlobalState,\n getGlobalState,\n getQuoteMarkup,\n setGlobalState,\n}\n","import { maxLength, minLength } from \"misago/utils/validators\"\nimport misago from \"misago\"\n\nexport function getTitleValidators() {\n return [getTitleLengthMin(), getTitleLengthMax()]\n}\n\nexport function getPostValidators() {\n if (misago.get(\"SETTINGS\").post_length_max) {\n return [validatePostLengthMin(), validatePostLengthMax()]\n } else {\n return [validatePostLengthMin()]\n }\n}\n\nexport function getTitleLengthMin() {\n return minLength(\n misago.get(\"SETTINGS\").thread_title_length_min,\n (limitValue, length) => {\n const message = npgettext(\n \"thread title length validator\",\n \"Thread title should be at least %(limit_value)s character long (it has %(show_value)s).\",\n \"Thread title should be at least %(limit_value)s characters long (it has %(show_value)s).\",\n limitValue\n )\n\n return interpolate(\n message,\n {\n limit_value: limitValue,\n show_value: length,\n },\n true\n )\n }\n )\n}\n\nexport function getTitleLengthMax() {\n return maxLength(\n misago.get(\"SETTINGS\").thread_title_length_max,\n (limitValue, length) => {\n const message = npgettext(\n \"thread title length validator\",\n \"Thread title cannot be longer than %(limit_value)s character (it has %(show_value)s).\",\n \"Thread title cannot be longer than %(limit_value)s characters (it has %(show_value)s).\",\n limitValue\n )\n\n return interpolate(\n message,\n {\n limit_value: limitValue,\n show_value: length,\n },\n true\n )\n }\n )\n}\n\nexport function validatePostLengthMin() {\n return minLength(\n misago.get(\"SETTINGS\").post_length_min,\n (limitValue, length) => {\n const message = npgettext(\n \"post length validator\",\n \"Posted message should be at least %(limit_value)s character long (it has %(show_value)s).\",\n \"Posted message should be at least %(limit_value)s characters long (it has %(show_value)s).\",\n limitValue\n )\n\n return interpolate(\n message,\n {\n limit_value: limitValue,\n show_value: length,\n },\n true\n )\n }\n )\n}\n\nexport function validatePostLengthMax() {\n return maxLength(\n misago.get(\"SETTINGS\").post_length_max || 1000000,\n (limitValue, length) => {\n const message = npgettext(\n \"post length validator\",\n \"Posted message cannot be longer than %(limit_value)s character (it has %(show_value)s).\",\n \"Posted message cannot be longer than %(limit_value)s characters (it has %(show_value)s).\",\n limitValue\n )\n\n return interpolate(\n message,\n {\n limit_value: limitValue,\n show_value: length,\n },\n true\n )\n }\n )\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n getChoice() {\n let choice = null\n this.props.choices.map((item) => {\n if (item.value === this.props.value) {\n choice = item\n }\n })\n return choice\n }\n\n getIcon() {\n return this.getChoice().icon\n }\n\n getLabel() {\n return this.getChoice().label\n }\n\n change = (value) => {\n return () => {\n this.props.onChange({\n target: {\n value: value,\n },\n })\n }\n }\n\n render() {\n return (\n
    \n \n \n {this.getLabel()}\n \n
      \n {this.props.choices.map((item, i) => {\n return (\n
    • \n \n \n {item.label}\n \n
    • \n )\n })}\n
    \n
    \n )\n }\n}\n\nexport function Icon({ icon }) {\n if (!icon) return null\n\n return {icon}\n}\n","import React from \"react\"\nimport misago from \"misago/index\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport StartSocialAuth from \"misago/components/StartSocialAuth\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport showBannedPage from \"misago/utils/banned-page\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n showActivation: false,\n\n username: \"\",\n password: \"\",\n\n validators: {\n username: [],\n password: [],\n },\n }\n }\n\n clean() {\n if (!this.isValid()) {\n snackbar.error(pgettext(\"sign in modal\", \"Fill out both fields.\"))\n return false\n } else {\n return true\n }\n }\n\n send() {\n return ajax.post(misago.get(\"AUTH_API\"), {\n username: this.state.username,\n password: this.state.password,\n })\n }\n\n handleSuccess() {\n let form = $(\"#hidden-login-form\")\n\n form.append('')\n form.append('')\n\n // fill out form with user credentials and submit it, this will tell\n // Misago to redirect user back to right page, and will trigger browser's\n // key ring feature\n form.find('input[type=\"hidden\"]').val(ajax.getCsrfToken())\n form.find('input[name=\"redirect_to\"]').val(window.location.pathname)\n form.find('input[name=\"username\"]').val(this.state.username)\n form.find('input[name=\"password\"]').val(this.state.password)\n form.submit()\n\n // keep form loading\n this.setState({\n isLoading: true,\n })\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n if (rejection.code === \"inactive_admin\") {\n snackbar.info(rejection.detail)\n } else if (rejection.code === \"inactive_user\") {\n snackbar.info(rejection.detail)\n this.setState({\n showActivation: true,\n })\n } else if (rejection.code === \"banned\") {\n showBannedPage(rejection.detail)\n modal.hide()\n } else {\n snackbar.error(rejection.detail)\n }\n } else if (rejection.status === 403 && rejection.ban) {\n showBannedPage(rejection.ban)\n modal.hide()\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n getActivationButton() {\n if (!this.state.showActivation) return null\n\n return (\n \n {pgettext(\"sign in modal btn\", \"Activate account\")}\n \n )\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"sign in modal title\", \"Sign in\")}\n

    \n
    \n
    \n
    \n \n\n
    \n
    \n \n
    \n
    \n\n
    \n
    \n \n
    \n
    \n
    \n
    \n {this.getActivationButton()}\n \n {pgettext(\"sign in modal btn\", \"Sign in\")}\n \n \n {pgettext(\"sign in modal btn\", \"Forgot password?\")}\n \n
    \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n getClass() {\n return getStatusClassName(this.props.status)\n }\n\n render() {\n return {this.props.children}\n }\n}\n\nexport class StatusIcon extends React.Component {\n getIcon() {\n if (this.props.status.is_banned) {\n return \"remove_circle_outline\"\n } else if (this.props.status.is_hidden) {\n return \"help_outline\"\n } else if (this.props.status.is_online_hidden) {\n return \"label\"\n } else if (this.props.status.is_offline_hidden) {\n return \"label_outline\"\n } else if (this.props.status.is_online) {\n return \"lens\"\n } else if (this.props.status.is_offline) {\n return \"panorama_fish_eye\"\n }\n }\n\n render() {\n return {this.getIcon()}\n }\n}\n\nexport class StatusLabel extends React.Component {\n getHelp() {\n return getStatusDescription(this.props.user, this.props.status)\n }\n\n getLabel() {\n if (this.props.status.is_banned) {\n return pgettext(\"user status\", \"Banned\")\n } else if (this.props.status.is_hidden) {\n return pgettext(\"user status\", \"Hidden\")\n } else if (this.props.status.is_online_hidden) {\n return pgettext(\"user status\", \"Online (hidden)\")\n } else if (this.props.status.is_offline_hidden) {\n return pgettext(\"user status\", \"Offline (hidden)\")\n } else if (this.props.status.is_online) {\n return pgettext(\"user status\", \"Online\")\n } else if (this.props.status.is_offline) {\n return pgettext(\"user status\", \"Offline\")\n }\n }\n\n render() {\n return (\n \n {this.getLabel()}\n \n )\n }\n}\n\nexport function getStatusClassName(status) {\n let className = \"\"\n if (status.is_banned) {\n className = \"banned\"\n } else if (status.is_hidden) {\n className = \"offline\"\n } else if (status.is_online_hidden) {\n className = \"online\"\n } else if (status.is_offline_hidden) {\n className = \"offline\"\n } else if (status.is_online) {\n className = \"online\"\n } else if (status.is_offline) {\n className = \"offline\"\n }\n\n return \"user-status user-\" + className\n}\n\nexport function getStatusDescription(user, status) {\n if (status.is_banned) {\n if (status.banned_until) {\n return interpolate(\n pgettext(\"user status\", \"%(username)s is banned until %(ban_expires)s\"),\n {\n username: user.username,\n ban_expires: status.banned_until.format(\"LL, LT\"),\n },\n true\n )\n } else {\n return interpolate(\n pgettext(\"user status\", \"%(username)s is banned\"),\n {\n username: user.username,\n },\n true\n )\n }\n } else if (status.is_hidden) {\n return interpolate(\n pgettext(\"user status\", \"%(username)s is hiding presence\"),\n {\n username: user.username,\n },\n true\n )\n } else if (status.is_online_hidden) {\n return interpolate(\n pgettext(\"user status\", \"%(username)s is online (hidden)\"),\n {\n username: user.username,\n },\n true\n )\n } else if (status.is_offline_hidden) {\n return interpolate(\n pgettext(\n \"user status\",\n \"%(username)s was last seen %(last_click)s (hidden)\"\n ),\n {\n username: user.username,\n last_click: status.last_click.fromNow(),\n },\n true\n )\n } else if (status.is_online) {\n return interpolate(\n pgettext(\"user status\", \"%(username)s is online\"),\n {\n username: user.username,\n },\n true\n )\n } else if (status.is_offline) {\n return interpolate(\n pgettext(\"user status\", \"%(username)s was last seen %(last_click)s\"),\n {\n username: user.username,\n last_click: status.last_click.fromNow(),\n },\n true\n )\n }\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n getEmptyMessage() {\n if (this.props.emptyMessage) {\n return this.props.emptyMessage\n } else {\n return pgettext(\n \"username history empty\",\n \"No name changes have been recorded for your account.\"\n )\n }\n }\n\n render() {\n return (\n
    \n
      \n
    • \n {this.getEmptyMessage()}\n
    • \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\n\nexport default class extends React.Component {\n renderUserAvatar() {\n if (this.props.change.changed_by) {\n return (\n \n \n \n )\n } else {\n return (\n \n \n \n )\n }\n }\n\n renderUsername() {\n if (this.props.change.changed_by) {\n return (\n \n {this.props.change.changed_by.username}\n \n )\n } else {\n return (\n \n {this.props.change.changed_by_username}\n \n )\n }\n }\n\n render() {\n return (\n
  • \n
    {this.renderUserAvatar()}
    \n
    {this.renderUsername()}
    \n
    \n {this.props.change.old_username}\n arrow_forward\n {this.props.change.new_username}\n
    \n
    \n \n {this.props.change.changed_on.fromNow()}\n \n
    \n
  • \n )\n }\n}\n","import React from \"react\"\nimport Change from \"misago/components/username-history/change\"\n\nexport default class extends React.Component {\n render() {\n return (\n
    \n
      \n {this.props.changes.map((change) => {\n return \n })}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport * as random from \"misago/utils/random\"\n\nexport default class extends React.Component {\n shouldComponentUpdate() {\n return false\n }\n\n getClassName() {\n if (this.props.hiddenOnMobile) {\n return \"list-group-item hidden-xs hidden-sm\"\n } else {\n return \"list-group-item\"\n }\n }\n\n render() {\n return (\n
  • \n
    \n \n \n \n
    \n
    \n \n  \n \n
    \n
    \n \n  \n \n arrow_forward\n \n  \n \n
    \n
    \n \n  \n \n
    \n
  • \n )\n }\n}\n","import React from \"react\"\nimport ChangePreview from \"misago/components/username-history/change-preview\"\n\nexport default class extends React.Component {\n shouldComponentUpdate() {\n return false\n }\n\n render() {\n return (\n
    \n
      \n {[0, 1, 2].map((i) => {\n return 0} key={i} />\n })}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport ListEmpty from \"misago/components/username-history/list-empty\"\nimport ListReady from \"misago/components/username-history/list-ready\"\nimport ListPreview from \"misago/components/username-history/list-preview\"\n\nexport default class extends React.Component {\n render() {\n if (this.props.isLoaded) {\n if (this.props.changes.length) {\n return \n } else {\n return \n }\n } else {\n return \n }\n }\n}\n","import React from \"react\"\nimport UserStatus, { StatusLabel } from \"misago/components/user-status\"\n\nexport default function ({ showStatus, user }) {\n return (\n
      \n \n \n
    • \n \n \n \n
    \n )\n}\n\nexport function Status({ showStatus, user }) {\n if (!showStatus) return null\n\n return (\n
  • \n \n \n \n
  • \n )\n}\n\nexport function JoinDate({ user }) {\n const { joined_on } = user\n\n let title = interpolate(\n pgettext(\"users list item\", \"Joined on %(joined_on)s\"),\n {\n joined_on: joined_on.format(\"LL, LT\"),\n },\n true\n )\n\n let message = interpolate(\n pgettext(\"users list item\", \"Joined %(joined_on)s\"),\n {\n joined_on: joined_on.fromNow(),\n },\n true\n )\n\n return (\n
  • \n {message}\n
  • \n )\n}\n\nexport function Posts({ user }) {\n const className = getStatClassName(\"user-stat-posts\", user.posts)\n const message = npgettext(\n \"users list item\",\n \"%(posts)s post\",\n \"%(posts)s posts\",\n user.posts\n )\n\n return (\n
  • \n {interpolate(\n message,\n {\n posts: user.posts,\n },\n true\n )}\n
  • \n )\n}\n\nexport function Threads({ user }) {\n const className = getStatClassName(\"user-stat-threads\", user.threads)\n const message = npgettext(\n \"users list item\",\n \"%(threads)s thread\",\n \"%(threads)s threads\",\n user.threads\n )\n\n return (\n
  • \n {interpolate(\n message,\n {\n threads: user.threads,\n },\n true\n )}\n
  • \n )\n}\n\nexport function Followers({ user }) {\n const className = getStatClassName(\"user-stat-followers\", user.followers)\n const message = npgettext(\n \"users list item\",\n \"%(followers)s follower\",\n \"%(followers)s followers\",\n user.followers\n )\n\n return (\n
  • \n {interpolate(\n message,\n {\n followers: user.followers,\n },\n true\n )}\n
  • \n )\n}\n\nexport function getStatClassName(className, stat) {\n if (stat === 0) {\n return className + \" user-stat-empty\"\n }\n return className\n}\n","import React from \"react\"\n\nexport default function ({ rank, title }) {\n let userTitle = title || rank.title || rank.name\n\n let className = \"user-title\"\n if (rank.css_class) {\n className += \" user-title-\" + rank.css_class\n }\n\n if (rank.is_tab) {\n return (\n \n {userTitle}\n \n )\n }\n\n return {userTitle}\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport Stats from \"./stats\"\nimport UserTitle from \"./user-title\"\n\nexport default function ({ showStatus, user }) {\n const { rank } = user\n\n let className = \"panel user-card\"\n if (rank.css_class) {\n className += \" user-card-\" + rank.css_class\n }\n\n return (\n
    \n
    \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    \n
    \n \n \n \n
    \n\n \n
    \n \n
    \n\n
    \n \n
    \n
    \n
    \n
    \n
    \n )\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport * as random from \"misago/utils/random\"\n\nexport default class extends React.Component {\n shouldComponentUpdate() {\n return false\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    \n
    \n \n \n \n
    \n\n
    \n \n  \n \n
    \n
    \n \n  \n \n
    \n\n
    \n
      \n
    • \n \n  \n \n
    • \n
    • \n \n  \n \n
    • \n
    • \n
    • \n \n  \n \n
    • \n
    • \n \n  \n \n
    • \n
    \n
    \n
    \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Card from \"./card\"\n\nexport default function ({ colClassName, cols }) {\n const list = Array.apply(null, { length: cols }).map(Number.call, Number)\n\n return (\n
    \n
    \n {list.map((i) => {\n let className = colClassName\n if (i !== 0) className += \" hidden-xs\"\n if (i === 3) className += \" hidden-sm\"\n\n return (\n
    \n \n
    \n )\n })}\n
    \n
    \n )\n}\n","import React from \"react\"\nimport Card from \"./card\"\nimport Preview from \"./preview\"\n\nexport default function ({ cols, isReady, showStatus, users }) {\n let colClassName = \"col-xs-12 col-sm-4\"\n if (cols === 4) {\n colClassName += \" col-md-3\"\n }\n\n if (!isReady) {\n return \n }\n\n return (\n
    \n
    \n {users.map((user) => {\n return (\n
    \n \n
    \n )\n })}\n
    \n
    \n )\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n dropdown: false,\n }\n }\n\n toggleNav = () => {\n this.setState({\n dropdown: !this.state.dropdown,\n })\n }\n\n hideNav = () => {\n this.setState({\n dropdown: false,\n })\n }\n\n getCompactNavClassName() {\n if (this.state.dropdown) {\n return \"compact-nav open\"\n } else {\n return \"compact-nav\"\n }\n }\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n getClassName() {\n if (this.props.value) {\n return \"btn btn-yes-no btn-yes-no-on\"\n } else {\n return \"btn btn-yes-no btn-yes-no-off\"\n }\n }\n\n getIcon() {\n if (!!this.props.value) {\n return this.props.iconOn || \"check_box\"\n } else {\n return this.props.iconOff || \"check_box_outline_blank\"\n }\n }\n\n getLabel() {\n if (!!this.props.value) {\n return this.props.labelOn || pgettext(\"yesno switch choice\", \"yes\")\n } else {\n return this.props.labelOff || pgettext(\"yesno switch choice\", \"no\")\n }\n }\n\n toggle = () => {\n this.props.onChange({\n target: {\n value: !this.props.value,\n },\n })\n }\n\n render() {\n return (\n \n {this.getIcon()}\n {this.getLabel()}\n \n )\n }\n}\n","class OrderedList {\n constructor(items) {\n this.isOrdered = false\n this._items = items || []\n }\n\n add(key, item, order) {\n this._items.push({\n key: key,\n item: item,\n\n after: order ? order.after || null : null,\n before: order ? order.before || null : null,\n })\n }\n\n get(key, value) {\n for (var i = 0; i < this._items.length; i++) {\n if (this._items[i].key === key) {\n return this._items[i].item\n }\n }\n\n return value\n }\n\n has(key) {\n return this.get(key) !== undefined\n }\n\n values() {\n var values = []\n for (var i = 0; i < this._items.length; i++) {\n values.push(this._items[i].item)\n }\n return values\n }\n\n order(values_only) {\n if (!this.isOrdered) {\n this._items = this._order(this._items)\n this.isOrdered = true\n }\n\n if (values_only || typeof values_only === \"undefined\") {\n return this.values()\n } else {\n return this._items\n }\n }\n\n orderedValues() {\n return this.order(true)\n }\n\n _order(unordered) {\n // Index of unordered items\n var index = []\n unordered.forEach(function (item) {\n index.push(item.key)\n })\n\n // Ordered items\n var ordered = []\n var ordering = []\n\n // First pass: register items that\n // don't specify their order\n unordered.forEach(function (item) {\n if (!item.after && !item.before) {\n ordered.push(item)\n ordering.push(item.key)\n }\n })\n\n // Second pass: register items that\n // specify their before to \"_end\"\n unordered.forEach(function (item) {\n if (item.before === \"_end\") {\n ordered.push(item)\n ordering.push(item.key)\n }\n })\n\n // Third pass: keep iterating items\n // until we hit iterations limit or finish\n // ordering list\n function insertItem(item) {\n var insertAt = -1\n if (ordering.indexOf(item.key) === -1) {\n if (item.after) {\n insertAt = ordering.indexOf(item.after)\n if (insertAt !== -1) {\n insertAt += 1\n }\n } else if (item.before) {\n insertAt = ordering.indexOf(item.before)\n }\n\n if (insertAt !== -1) {\n ordered.splice(insertAt, 0, item)\n ordering.splice(insertAt, 0, item.key)\n }\n }\n }\n\n var iterations = 200\n while (iterations > 0 && index.length !== ordering.length) {\n iterations -= 1\n unordered.forEach(insertItem)\n }\n\n return ordered\n }\n}\n\nexport default OrderedList\n","import \"bootstrap/js/transition\"\nimport \"bootstrap/js/affix\"\nimport \"bootstrap/js/modal\"\nimport \"bootstrap/js/dropdown\"\nimport \"at-js\"\nimport \"cropit\"\nimport \"jquery-caret\"\nimport OrderedList from \"misago/utils/ordered-list\"\nimport \"misago/style/index.less\"\n\nexport class Misago {\n constructor() {\n this._initializers = []\n this._context = {}\n }\n\n addInitializer(initializer) {\n this._initializers.push({\n key: initializer.name,\n\n item: initializer.initializer,\n\n after: initializer.after,\n before: initializer.before,\n })\n }\n\n init(context) {\n this._context = context\n\n var initOrder = new OrderedList(this._initializers).orderedValues()\n initOrder.forEach((initializer) => {\n initializer(this)\n })\n }\n\n // context accessors\n has(key) {\n return !!this._context[key]\n }\n\n get(key, fallback) {\n if (this.has(key)) {\n return this._context[key]\n } else {\n return fallback || undefined\n }\n }\n\n pop(key) {\n if (this.has(key)) {\n let value = this._context[key]\n this._context[key] = null\n return value\n } else {\n return undefined\n }\n }\n}\n\n// create singleton\nvar misago = new Misago()\n\n// expose it globally\nwindow.misago = misago\n\n// and export it for tests and stuff\nexport default misago\n","import misago from \"misago/index\"\nimport ajax from \"misago/services/ajax\"\n\nexport default function initializer() {\n ajax.init(misago.get(\"CSRF_COOKIE_NAME\"))\n}\n\nmisago.addInitializer({\n name: \"ajax\",\n initializer: initializer,\n})\n","import misago from \"misago/index\"\nimport { patch } from \"misago/reducers/auth\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nconst AUTH_SYNC_RATE = 45 // sync user with backend every 45 seconds\n\nexport default function initializer(context) {\n if (context.get(\"isAuthenticated\")) {\n window.setInterval(function () {\n ajax.get(context.get(\"AUTH_API\")).then(\n function (data) {\n store.dispatch(patch(data))\n },\n function (rejection) {\n snackbar.apiError(rejection)\n }\n )\n }, AUTH_SYNC_RATE * 1000)\n }\n}\n\nmisago.addInitializer({\n name: \"auth-sync\",\n initializer: initializer,\n after: \"auth\",\n})\n","import misago from \"misago/index\"\nimport auth from \"misago/services/auth\"\nimport modal from \"misago/services/modal\"\nimport store from \"misago/services/store\"\nimport storage from \"misago/services/local-storage\"\n\nexport default function initializer() {\n auth.init(store, storage, modal)\n}\n\nmisago.addInitializer({\n name: \"auth\",\n initializer: initializer,\n after: \"store\",\n})\n","import misago from \"misago/index\"\nimport ajax from \"misago/services/ajax\"\nimport captcha from \"misago/services/captcha\"\nimport include from \"misago/services/include\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default function initializer(context) {\n captcha.init(context, ajax, include, snackbar)\n}\n\nmisago.addInitializer({\n name: \"captcha\",\n initializer: initializer,\n})\n","import React from \"react\"\nimport ajax from \"misago/services/ajax\"\n\nexport default class AcceptAgreement extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = { submiting: false }\n }\n\n handleDecline = () => {\n if (this.state.submiting) return\n\n const confirmation = window.confirm(\n pgettext(\n \"accept agreement prompt\",\n \"Declining will result in immediate deactivation and deletion of your account. This action is not reversible.\"\n )\n )\n if (!confirmation) return\n\n this.setState({ submiting: true })\n\n ajax.post(this.props.api, { accept: false }).then(() => {\n window.location.reload(true)\n })\n }\n\n handleAccept = () => {\n if (this.state.submiting) return\n\n this.setState({ submiting: true })\n\n ajax.post(this.props.api, { accept: true }).then(() => {\n window.location.reload(true)\n })\n }\n\n render() {\n return (\n
    \n \n {pgettext(\"accept agreement choice\", \"Decline\")}\n \n \n {pgettext(\"accept agreement choice\", \"Accept and continue\")}\n \n
    \n )\n }\n}\n","import React from \"react\"\nimport misago from \"misago/index\"\nimport AcceptAgreement from \"misago/components/accept-agreement\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer(context) {\n if (document.getElementById(\"required-agreement-mount\")) {\n mount(\n ,\n \"required-agreement-mount\",\n false\n )\n }\n}\n\nmisago.addInitializer({\n name: \"component:accept-agreement\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\n\nexport default class extends React.Component {\n refresh() {\n window.location.reload()\n }\n\n getMessage() {\n if (this.props.signedIn) {\n return interpolate(\n pgettext(\n \"auth message\",\n \"You have signed in as %(username)s. Please refresh the page before continuing.\"\n ),\n { username: this.props.signedIn.username },\n true\n )\n } else if (this.props.signedOut) {\n return interpolate(\n pgettext(\n \"auth message\",\n \"%(username)s, you have been signed out. Please refresh the page before continuing.\"\n ),\n { username: this.props.user.username },\n true\n )\n }\n }\n\n render() {\n let className = \"auth-message\"\n if (this.props.signedIn || this.props.signedOut) {\n className += \" show\"\n }\n\n return (\n
    \n
    \n

    {this.getMessage()}

    \n

    \n \n {pgettext(\"auth message\", \"Reload page\")}\n \n \n {\" \" + pgettext(\"auth message\", \"or press F5 key.\")}\n \n

    \n
    \n
    \n )\n }\n}\n\nexport function select(state) {\n return {\n user: state.auth.user,\n signedIn: state.auth.signedIn,\n signedOut: state.auth.signedOut,\n }\n}\n","import { connect } from \"react-redux\"\nimport misago from \"misago/index\"\nimport AuthMessage, { select } from \"misago/components/auth-message\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer() {\n mount(connect(select)(AuthMessage), \"auth-message-mount\")\n}\n\nmisago.addInitializer({\n name: \"component:auth-message\",\n initializer: initializer,\n after: \"store\",\n})\n","import misago from \"misago/index\"\nimport showBannedPage from \"misago/utils/banned-page\"\n\nexport default function initializer(context) {\n if (context.has(\"BAN_MESSAGE\")) {\n showBannedPage(context.get(\"BAN_MESSAGE\"), false)\n }\n}\n\nmisago.addInitializer({\n name: \"component:banmed-page\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\n\nexport default function (props) {\n return (\n
    \n
      \n
    • \n

      \n {pgettext(\n \"categories list\",\n \"No categories exist or you don't have permission to see them.\"\n )}\n

      \n
    • \n
    \n
    \n )\n}\n","import React from \"react\"\n\nexport default function ({ category }) {\n if (!category.description) return null\n\n return (\n \n )\n}\n","import React from \"react\"\n\nexport default function ({ category }) {\n return (\n
    \n {getIcon(category)}\n
    \n )\n}\n\nexport function getClassName(category) {\n if (category.is_read) {\n return \"read-status item-read\"\n }\n\n return \"read-status item-new\"\n}\n\nexport function getTitle(category) {\n if (category.is_closed) {\n if (category.is_read) {\n return gettext(\n \"category status\",\n \"This category has no new posts. (closed)\"\n )\n }\n\n return gettext(\"category status\", \"This category has new posts. (closed)\")\n }\n\n if (category.is_read) {\n return gettext(\"category status\", \"This category has no new posts.\")\n }\n\n return gettext(\"category status\", \"This category has new posts.\")\n}\n\nexport function getIcon(category) {\n if (category.is_closed) {\n if (category.is_read) {\n return \"lock_outline\"\n }\n\n return \"lock\"\n }\n\n if (category.is_read) {\n return \"chat_bubble_outline\"\n }\n\n return \"chat_bubble\"\n}\n","import React from \"react\"\nimport Description from \"./description\"\nimport Icon from \"./icon\"\n\nexport default function ({ category }) {\n return (\n
    \n
    \n
    \n \n
    \n
    \n

    \n {category.name}\n

    \n \n
    \n
    \n
    \n )\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\n\nexport default function ({ category }) {\n return (\n
    \n \n \n \n \n
    \n )\n}\n\nexport function LastThread({ category }) {\n if (!category.acl.can_browse) return null\n if (!category.acl.can_see_all_threads) return null\n if (!category.last_thread_title) return null\n\n return (\n
    \n
    \n \n
    \n
    \n
    \n \n {category.last_thread_title}\n \n
    \n \n
    \n
    \n )\n}\n\nexport function LastPosterAvatar({ category }) {\n if (category.last_poster) {\n return (\n \n \n \n )\n }\n\n return (\n \n \n \n )\n}\n\nexport function LastPosterName({ category }) {\n if (category.last_poster) {\n return (\n \n {category.last_poster_name}\n \n )\n }\n\n return {category.last_poster_name}\n}\n\nexport function Empty({ category }) {\n if (!category.acl.can_browse) return null\n if (!category.acl.can_see_all_threads) return null\n if (category.last_thread_title) return null\n\n return (\n \n )\n}\n\nexport function Private({ category }) {\n if (!category.acl.can_browse) return null\n if (category.acl.can_see_all_threads) return null\n\n return (\n \n )\n}\n\nexport function Protected({ category }) {\n if (category.acl.can_browse) return null\n\n return (\n \n )\n}\n\nexport function Message({ message }) {\n return (\n
    \n
    \n info_outline\n
    \n
    \n

    {message}

    \n
    \n
    \n )\n}\n","import React from \"react\"\n\nexport default function ({ category }) {\n return (\n
    \n
      \n \n \n
    \n
    \n )\n}\n\nexport function Threads({ threads }) {\n const message = npgettext(\n \"category stats\",\n \"%(threads)s thread\",\n \"%(threads)s threads\",\n threads\n )\n\n return (\n
  • \n {interpolate(\n message,\n {\n threads: threads,\n },\n true\n )}\n
  • \n )\n}\n\nexport function Posts({ posts }) {\n const message = npgettext(\n \"category stats\",\n \"%(posts)s post\",\n \"%(posts)s posts\",\n posts\n )\n\n return (\n
  • \n {interpolate(\n message,\n {\n posts: posts,\n },\n true\n )}\n
  • \n )\n}\n","import React from \"react\"\n\nexport default function ({ category }) {\n let className = \"btn btn-default btn-block btn-sm btn-subcategory\"\n if (!category.is_read) {\n className += \" btn-subcategory-new\"\n }\n\n return (\n \n )\n}\n\nexport function getIcon(category) {\n if (category.is_closed) {\n if (category.is_read) {\n return \"lock_outline\"\n }\n\n return \"lock\"\n }\n\n if (category.is_read) {\n return \"chat_bubble_outline\"\n }\n\n return \"chat_bubble\"\n}\n","import React from \"react\"\nimport ListItem from \"./list-item\"\n\nexport default function ({ category, isFirst }) {\n if (isFirst) return null\n if (category.subcategories.length === 0) return null\n\n return (\n
    \n {category.subcategories.map((category) => {\n return \n })}\n
    \n )\n}\n","import React from \"react\"\nimport Main from \"./main\"\nimport LastThread from \"./last-thread\"\nimport Stats from \"./stats\"\nimport Subcategories from \"./subcategories\"\n\nexport default function ({ category, isFirst }) {\n let className = \"list-group-item\"\n\n if (category.description) {\n className += \" list-group-category-has-description\"\n } else {\n className += \" list-group-category-no-description\"\n }\n\n if (isFirst) {\n className += \" list-group-item-first\"\n }\n if (category.css_class) {\n className += \" list-group-category-has-flavor\"\n className += \" list-group-item-category-\" + category.css_class\n }\n\n return (\n
  • \n
    \n
    \n \n \n
    \n \n
  • \n )\n}\n","import React from \"react\"\nimport ListItem from \"./list-item\"\n\nexport default function ({ category }) {\n let className = \"list-group list-group-category\"\n if (category.css_class) {\n className += \" list-group-category-has-flavor\"\n className += \" list-group-category-\" + category.css_class\n }\n\n return (\n
      \n \n {category.subcategories.map((category) => {\n return (\n \n )\n })}\n
    \n )\n}\n","import React from \"react\"\nimport Category from \"./category\"\n\nexport default function ({ categories }) {\n return (\n
    \n {categories.map((category) => {\n return \n })}\n
    \n )\n}\n","import moment from \"moment\"\nimport React from \"react\"\nimport Blankslate from \"./blankslate\"\nimport CategoriesList from \"./categories-list\"\nimport misago from \"misago/index\"\nimport polls from \"misago/services/polls\"\n\nconst hydrate = function (category) {\n return Object.assign({}, category, {\n last_post_on: category.last_post_on ? moment(category.last_post_on) : null,\n subcategories: category.subcategories.map(hydrate),\n })\n}\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n categories: misago.get(\"CATEGORIES\").map(hydrate),\n }\n\n this.startPolling(misago.get(\"CATEGORIES_API\"))\n }\n\n startPolling(api) {\n polls.start({\n poll: \"categories\",\n url: api,\n frequency: 180 * 1000,\n update: this.update,\n })\n }\n\n update = (data) => {\n this.setState({\n categories: data.map(hydrate),\n })\n }\n\n render() {\n const { categories } = this.state\n\n if (categories.length === 0) {\n return \n }\n\n return \n }\n}\n\nexport function select(store) {\n return {\n tick: store.tick.tick,\n }\n}\n","import { connect } from \"react-redux\"\nimport Categories, { select } from \"misago/components/categories\"\nimport misago from \"misago/index\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer() {\n if (document.getElementById(\"categories-mount\")) {\n mount(connect(select)(Categories), \"categories-mount\")\n }\n}\n\nmisago.addInitializer({\n name: \"component:categories\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport modal from \"../services/modal\"\nimport SignInModal from \"./sign-in\"\n\nclass SignInModalAutoOpen extends React.Component {\n componentDidMount() {\n const query = window.document.location.search\n if (query === \"?modal=login\") {\n window.setTimeout(() => modal.show(), 300)\n }\n }\n\n render() {\n return null\n }\n}\n\nexport default SignInModalAutoOpen\n","import React from \"react\"\n\nexport default function NavbarBranding({ logo, logoXs, text, url }) {\n if (logo) {\n return (\n
    \n \n {text}\n \n
    \n )\n }\n\n return (\n
    \n {!!logoXs && (\n \n {text}\n \n )}\n {!!text && (\n \n {text}\n \n )}\n
    \n )\n}\n","import React from \"react\"\n\nexport default function NavbarExtraMenu({ items }) {\n return (\n
      \n {items.map((item, index) => (\n
    • \n \n {item.title}\n \n
    • \n ))}\n
    \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { DropdownFooter, DropdownHeader, DropdownPills } from \"../Dropdown\"\n\nexport default function NotificationsDropdownBody({\n children,\n showAll,\n showUnread,\n unread,\n}) {\n return (\n
    \n \n {pgettext(\"notifications title\", \"Notifications\")}\n \n \n \n {pgettext(\"notifications dropdown\", \"All\")}\n \n \n {pgettext(\"notifications dropdown\", \"Unread\")}\n \n \n {children}\n \n \n {pgettext(\"notifications\", \"See all notifications\")}\n \n \n
    \n )\n}\n\nfunction NotificationsDropdownBodyPill({ active, children, onClick }) {\n return (\n \n {children}\n \n )\n}\n","import React from \"react\"\nimport NotificationsFetch from \"../NotificationsFetch\"\nimport {\n NotificationsList,\n NotificationsListError,\n NotificationsListLoading,\n} from \"../NotificationsList\"\nimport NotificationsDropdownBody from \"./NotificationsDropdownBody\"\n\nexport default class NotificationsDropdown extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n unread: false,\n url: \"\",\n }\n }\n\n getApiUrl() {\n let url = misago.get(\"NOTIFICATIONS_API\") + \"?limit=20\"\n url += this.state.unread ? \"&filter=unread\" : \"\"\n return url\n }\n\n render = () => (\n this.setState({ unread: false })}\n showUnread={() => this.setState({ unread: true })}\n >\n \n {({ data, loading, error }) => {\n if (loading) {\n return \n }\n\n if (error) {\n return \n }\n\n return (\n \n )\n }}\n \n \n )\n}\n","import NotificationsDropdown from \"./NotificationsDropdown\"\n\nexport default NotificationsDropdown\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function NavbarNotificationsToggle({\n id,\n className,\n badge,\n url,\n active,\n onClick,\n}) {\n const title = !!badge\n ? pgettext(\"navbar\", \"You have unread notifications!\")\n : pgettext(\"navbar\", \"Open notifications\")\n\n return (\n \n {!!badge && {badge}}\n \n {!!badge ? \"notifications_active\" : \"notifications_none\"}\n \n \n )\n}\n","import React from \"react\"\nimport { Dropdown } from \"../Dropdown\"\nimport NotificationsDropdown from \"../NotificationsDropdown\"\nimport NavbarNotificationsToggle from \"./NavbarNotificationsToggle\"\n\nexport default function NavbarNotificationsDropdown({\n id,\n className,\n badge,\n url,\n}) {\n return (\n (\n {\n event.preventDefault()\n toggle()\n }}\n />\n )}\n menuClassName=\"notifications-dropdown\"\n menuAlignRight\n >\n {({ isOpen }) => }\n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function NavbarPrivateThreads({\n id,\n className,\n badge,\n url,\n active,\n onClick,\n}) {\n const title = !!badge\n ? pgettext(\"navbar\", \"You have unread private threads!\")\n : pgettext(\"navbar\", \"Open private threads\")\n\n return (\n \n {!!badge && {badge}}\n inbox\n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function NavbarSearchToggle({\n id,\n className,\n url,\n active,\n onClick,\n}) {\n return (\n \n search\n \n )\n}\n","import React from \"react\"\nimport { Dropdown } from \"../Dropdown\"\nimport { SearchDropdown } from \"../Search\"\nimport NavbarSearchToggle from \"./NavbarSearchToggle\"\n\nexport default function NavbarSearchDropdown({ id, className, url }) {\n return (\n (\n {\n event.preventDefault()\n toggle()\n\n window.setTimeout(() => {\n document\n .querySelector(\".search-dropdown .form-control-search\")\n .focus()\n }, 0)\n }}\n />\n )}\n menuClassName=\"search-dropdown\"\n menuAlignRight\n >\n {() => }\n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function NavbarSiteNavToggle({\n id,\n className,\n active,\n onClick,\n}) {\n return (\n \n menu\n \n )\n}\n","import React from \"react\"\nimport { Dropdown } from \"../Dropdown\"\nimport NavbarSiteNavToggle from \"./NavbarSiteNavToggle\"\nimport { SiteNavDropdown } from \"../SiteNav\"\n\nexport default function NavbarSiteNavDropdown({ id, className }) {\n return (\n (\n \n )}\n menuClassName=\"site-nav-dropdown\"\n menuAlignRight\n >\n {({ isOpen, close }) => }\n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport Avatar from \"../avatar\"\n\nexport default function NavbarUserNavToggle({\n id,\n className,\n user,\n active,\n onClick,\n}) {\n return (\n \n \n \n )\n}\n","import React from \"react\"\nimport { Dropdown } from \"../Dropdown\"\nimport NavbarUserNavToggle from \"./NavbarUserNavToggle\"\nimport { UserNavDropdown } from \"../UserNav\"\n\nexport default function NavbarUserNavDropdown({ id, className, user }) {\n return (\n (\n {\n event.preventDefault()\n toggle()\n }}\n />\n )}\n menuClassName=\"user-nav-dropdown\"\n menuAlignRight\n >\n {({ isOpen, close }) => }\n \n )\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport * as overlay from \"../../reducers/overlay\"\nimport RegisterButton from \"../RegisterButton\"\nimport SignInButton from \"../SignInButton\"\nimport SignInModalAutoOpen from \"../SignInModalAutoOpen\"\nimport NavbarBranding from \"./NavbarBranding\"\nimport NavbarExtraMenu from \"./NavbarExtraMenu\"\nimport NavbarNotificationsDropdown from \"./NavbarNotificationsDropdown\"\nimport NavbarNotificationsToggle from \"./NavbarNotificationsToggle\"\nimport NavbarPrivateThreads from \"./NavbarPrivateThreads\"\nimport NavbarSearchDropdown from \"./NavbarSearchDropdown\"\nimport NavbarSearchToggle from \"./NavbarSearchToggle\"\nimport NavbarSiteNavDropdown from \"./NavbarSiteNavDropdown\"\nimport NavbarSiteNavToggle from \"./NavbarSiteNavToggle\"\nimport NavbarUserNavDropdown from \"./NavbarUserNavDropdown\"\nimport NavbarUserNavToggle from \"./NavbarUserNavToggle\"\n\nexport function Navbar({\n dispatch,\n branding,\n extraMenuItems,\n authDelegated,\n user,\n searchUrl,\n notificationsUrl,\n privateThreadsUrl,\n showSearch,\n showPrivateThreads,\n}) {\n return (\n
    \n \n
    \n {extraMenuItems.length > 0 && (\n \n )}\n {!!showSearch && (\n \n )}\n {!!showSearch && (\n {\n dispatch(overlay.openSearch())\n event.preventDefault()\n }}\n />\n )}\n \n {\n dispatch(overlay.openSiteNav())\n }}\n />\n {!!showPrivateThreads && (\n \n )}\n {!!user && (\n \n )}\n {!!user && (\n {\n dispatch(overlay.openNotifications())\n event.preventDefault()\n }}\n />\n )}\n {!!user && (\n \n )}\n {!!user && (\n {\n dispatch(overlay.openUserNav())\n event.preventDefault()\n }}\n />\n )}\n {!user && }\n {!user && !authDelegated && (\n \n )}\n {!user && !authDelegated && }\n
    \n
    \n )\n}\n\nfunction select(state) {\n const settings = misago.get(\"SETTINGS\")\n const user = state.auth.user\n\n return {\n branding: {\n logo: settings.logo,\n logoXs: settings.logo_small,\n text: settings.logo_text,\n url: misago.get(\"MISAGO_PATH\"),\n },\n extraMenuItems: misago.get(\"extraMenuItems\"),\n\n user: !user.id\n ? null\n : {\n id: user.id,\n username: user.username,\n email: user.email,\n avatars: user.avatars,\n unreadNotifications: user.unreadNotifications,\n unreadPrivateThreads: user.unread_private_threads,\n url: user.url,\n },\n\n searchUrl: misago.get(\"SEARCH_URL\"),\n notificationsUrl: misago.get(\"NOTIFICATIONS_URL\"),\n privateThreadsUrl: misago.get(\"PRIVATE_THREADS_URL\"),\n\n authDelegated: settings.enable_oauth2_client,\n showSearch: !!user.acl.can_search,\n showPrivateThreads: !!user && !!user.acl.can_use_private_threads,\n }\n}\n\nconst NavbarConnected = connect(select)(Navbar)\n\nexport default NavbarConnected\n","import Navbar from \"./Navbar\"\n\nexport default Navbar\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport { Provider } from \"react-redux\"\nimport Navbar from \"../../components/Navbar\"\nimport store from \"../../services/store\"\n\nexport default function initializer(context) {\n const root = document.getElementById(\"misago-navbar\")\n ReactDOM.render(\n \n \n ,\n root\n )\n}\n\nmisago.addInitializer({\n name: \"component:navbar\",\n initializer: initializer,\n after: \"store\",\n})\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { DropdownFooter, DropdownPills } from \"../Dropdown\"\nimport { Overlay, OverlayHeader } from \"../Overlay\"\n\nexport default function NotificationsOverlayBody({\n children,\n open,\n showAll,\n showUnread,\n unread,\n}) {\n return (\n \n \n {pgettext(\"notifications title\", \"Notifications\")}\n \n \n \n {pgettext(\"notifications dropdown\", \"All\")}\n \n \n {pgettext(\"notifications dropdown\", \"Unread\")}\n \n \n {children}\n \n \n {pgettext(\"notifications\", \"See all notifications\")}\n \n \n \n )\n}\n\nfunction NotificationsOverlayBodyPill({ active, children, onClick }) {\n return (\n \n {children}\n \n )\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport NotificationsFetch from \"../NotificationsFetch\"\nimport {\n NotificationsList,\n NotificationsListError,\n NotificationsListLoading,\n} from \"../NotificationsList\"\nimport NotificationsOverlayBody from \"./NotificationsOverlayBody\"\n\nclass NotificationsOverlay extends React.Component {\n constructor(props) {\n super(props)\n\n this.body = document.body\n\n this.state = {\n unread: false,\n url: \"\",\n }\n }\n\n getApiUrl() {\n let url = misago.get(\"NOTIFICATIONS_API\") + \"?limit=20\"\n url += this.state.unread ? \"&filter=unread\" : \"\"\n return url\n }\n\n componentDidUpdate(prevProps, prevState) {\n if (prevProps.open !== this.props.open) {\n if (this.props.open) {\n this.body.classList.add(\"notifications-fullscreen\")\n } else {\n this.body.classList.remove(\"notifications-fullscreen\")\n }\n }\n }\n\n render = () => (\n this.setState({ unread: false })}\n showUnread={() => this.setState({ unread: true })}\n >\n \n {({ data, loading, error }) => {\n if (loading) {\n return \n }\n\n if (error) {\n return \n }\n\n return (\n \n )\n }}\n \n \n )\n}\n\nfunction select(state) {\n return { open: state.overlay.notifications }\n}\n\nconst NotificationsOverlayConnected = connect(select)(NotificationsOverlay)\n\nexport default NotificationsOverlayConnected\n","import NotificationsOverlay from \"./NotificationsOverlay\"\n\nexport default NotificationsOverlay\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport { Provider } from \"react-redux\"\nimport NotificationsOverlay from \"../../components/NotificationsOverlay\"\nimport store from \"../../services/store\"\n\nexport default function initializer(context) {\n if (context.get(\"isAuthenticated\")) {\n const root = document.getElementById(\"notifications-mount\")\n ReactDOM.render(\n \n \n ,\n root\n )\n }\n}\n\nmisago.addInitializer({\n name: \"component:notifications-overlay\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport { PageHeaderPlain } from \"../PageHeader\"\n\nexport default function NotificationsHeader() {\n return (\n \n )\n}\n","import PageTitle from \"./PageTitle\"\n\nexport default PageTitle\n","export default function PageTitle({ title, subtitle }) {\n const parts = []\n if (subtitle) {\n parts.push(subtitle)\n }\n if (title) {\n parts.push(title)\n }\n parts.push(misago.get(\"SETTINGS\").forum_name)\n\n document.title = parts.join(\" | \")\n return null\n}\n","import React from \"react\"\n\nexport default function PillsNav({ children }) {\n return
      {children}
    \n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { Link } from \"react-router\"\n\nexport default function PillsNavLink({ active, link, icon, children }) {\n return (\n
  • \n \n {!!icon && {icon}}\n {children}\n \n
  • \n )\n}\n","import React from \"react\"\nimport { PillsNav, PillsNavLink } from \"../PillsNav\"\nimport { Toolbar, ToolbarSection, ToolbarItem } from \"../Toolbar\"\n\nexport default function NotificationsPills({ filter }) {\n const basename = misago.get(\"NOTIFICATIONS_URL\")\n\n return (\n \n \n \n \n \n {pgettext(\"notifications nav\", \"All\")}\n \n \n {pgettext(\"notifications nav\", \"Unread\")}\n \n \n {pgettext(\"notifications nav\", \"Read\")}\n \n \n \n \n \n )\n}\n","import React from \"react\"\nimport { Link } from \"react-router\"\n\nexport default function NotificationsPagination({ baseUrl, data, disabled }) {\n return (\n
    \n \n {pgettext(\"notifications pagination\", \"Latest\")}\n \n \n {pgettext(\"notifications pagination\", \"Newer\")}\n \n \n {pgettext(\"notifications pagination\", \"Older\")}\n \n
    \n )\n}\n\nfunction NotificationsPaginationLink({ disabled, children, url }) {\n if (disabled) {\n return (\n \n )\n }\n\n return (\n \n {children}\n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport Button from \"../button\"\nimport { Toolbar, ToolbarSection, ToolbarItem, ToolbarSpacer } from \"../Toolbar\"\nimport NotificationsPagination from \"./NotificationsPagination\"\n\nexport default function NotificationsToolbar({\n baseUrl,\n data,\n disabled,\n bottom,\n markAllAsRead,\n}) {\n return (\n \n \n \n \n \n \n \n \n \n \n done_all\n {pgettext(\"notifications\", \"Mark all as read\")}\n \n \n \n \n )\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport { updateAuthenticatedUser } from \"../../reducers/auth\"\nimport snackbar from \"../../services/snackbar\"\nimport { ApiMutation } from \"../Api\"\nimport NotificationsFetch from \"../NotificationsFetch\"\nimport PageTitle from \"../PageTitle\"\nimport PageContainer from \"../PageContainer\"\nimport {\n NotificationsList,\n NotificationsListError,\n NotificationsListLoading,\n} from \"../NotificationsList\"\nimport NotificationsPills from \"./NotificationsPills\"\nimport NotificationsToolbar from \"./NotificationsToolbar\"\n\nfunction NotificationsRoute({ dispatch, location, route }) {\n const { query } = location\n const { filter } = route.props\n\n const baseUrl = getBaseUrl(filter)\n\n return (\n \n \n\n \n\n \n {({ data, loading, error, refetch }) => (\n \n {(readAll, { loading: mutating }) => {\n const toolbarProps = {\n baseUrl,\n data,\n disabled:\n loading || mutating || !data || data.results.length === 0,\n markAllAsRead: async () => {\n const confirmed = window.confirm(\n pgettext(\"notifications\", \"Mark all notifications as read?\")\n )\n\n if (confirmed) {\n readAll({\n onSuccess: async () => {\n refetch()\n dispatch(\n updateAuthenticatedUser({ unreadNotifications: null })\n )\n snackbar.success(\n pgettext(\n \"notifications\",\n \"All notifications have been marked as read.\"\n )\n )\n },\n onError: snackbar.apiError,\n })\n }\n },\n }\n\n if (loading || mutating) {\n return (\n
    \n \n \n \n
    \n )\n }\n\n if (error) {\n return (\n
    \n \n \n \n
    \n )\n }\n\n if (data) {\n if (!data.hasPrevious && query) {\n window.history.replaceState({}, \"\", baseUrl)\n }\n\n return (\n
    \n \n \n \n
    \n )\n }\n\n return null\n }}\n
    \n )}\n
    \n
    \n )\n}\n\nfunction getSubtitle(filter) {\n if (filter === \"unread\") {\n return pgettext(\"notifications title\", \"Unread notifications\")\n } else if (filter === \"read\") {\n return pgettext(\"notifications title\", \"Read notifications\")\n } else {\n return null\n }\n}\n\nfunction getBaseUrl(filter) {\n let url = misago.get(\"NOTIFICATIONS_URL\")\n if (filter !== \"all\") {\n url += filter + \"/\"\n }\n return url\n}\n\nconst NotificationsRouteConnected = connect()(NotificationsRoute)\n\nexport default NotificationsRouteConnected\n","import Notifications from \"./Notifications\"\nimport NotificationsFetch from \"../NotificationsFetch/NotificationsFetch\"\n\nexport default Notifications\n\nexport { NotificationsFetch }\n","import React from \"react\"\nimport { Router, browserHistory } from \"react-router\"\nimport NotificationsHeader from \"./NotificationsHeader\"\nimport NotificationsRoute from \"./NotificationsRoute\"\n\nexport default function Notifications() {\n const basename = misago.get(\"NOTIFICATIONS_URL\")\n\n return (\n
    \n \n \n
    \n )\n}\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport { Provider } from \"react-redux\"\nimport Notifications from \"../../components/Notifications\"\nimport store from \"../../services/store\"\n\nexport default function initializer(context) {\n const basename = misago.get(\"NOTIFICATIONS_URL\")\n if (\n document.location.pathname.startsWith(basename) &&\n !document.location.pathname.startsWith(basename + \"disable-email/\") &&\n context.get(\"isAuthenticated\")\n ) {\n const root = document.getElementById(\"page-mount\")\n ReactDOM.render(\n \n \n ,\n root\n )\n }\n}\n\nmisago.addInitializer({\n name: \"component:notifications\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport { Link } from \"react-router\"\nimport Li from \"misago/components/li\"\n\nexport function SideNav(props) {\n return (\n
    \n {props.options.map((option) => {\n return (\n \n {option.icon}\n {option.name}\n \n )\n })}\n
    \n )\n}\n\nexport function CompactNav(props) {\n return (\n
      \n {props.options.map((option) => {\n return (\n \n \n {option.icon}\n {option.name}\n \n \n )\n })}\n
    \n )\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport ajax from \"misago/services/ajax\"\nimport title from \"misago/services/page-title\"\nimport snackbar from \"misago/services/snackbar\"\nimport misago from \"misago\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n password: \"\",\n }\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"delete your account title\", \"Delete account\"),\n parent: pgettext(\"forum options\", \"Change your options\"),\n })\n }\n\n onPasswordChange = (event) => {\n this.setState({ password: event.target.value })\n }\n\n handleSubmit = (event) => {\n event.preventDefault()\n\n const { isLoading, password } = this.state\n const { user } = this.props\n\n if (password.length == 0) {\n snackbar.error(\n pgettext(\n \"delete your account form\",\n \"Enter your password to confirm account deletion.\"\n )\n )\n return false\n }\n\n if (isLoading) return false\n this.setState({ isLoading: true })\n\n ajax.post(user.api.delete, { password }).then(\n (success) => {\n window.location.href = misago.get(\"MISAGO_PATH\")\n },\n (rejection) => {\n this.setState({ isLoading: false })\n if (rejection.password) {\n snackbar.error(rejection.password[0])\n } else {\n snackbar.apiError(rejection)\n }\n }\n )\n }\n\n render() {\n return (\n
    \n
    \n
    \n

    \n {pgettext(\"delete your account title\", \"Delete account\")}\n

    \n
    \n
    \n

    \n {pgettext(\n \"delete your account form\",\n \"You are going to delete your account. This action is nonreversible, and will result in following data being deleted:\"\n )}\n

    \n\n

    \n -{\" \"}\n {pgettext(\n \"delete your account form\",\n \"Stored IP addresses associated with content that you have posted will be deleted.\"\n )}\n

    \n

    \n -{\" \"}\n {pgettext(\n \"delete your account form\",\n \"Your username will become available for other user to rename to or for new user to register their account with.\"\n )}\n

    \n

    \n -{\" \"}\n {pgettext(\n \"delete your account form\",\n \"Your e-mail will become available for use in new account registration.\"\n )}\n

    \n\n
    \n\n

    \n {pgettext(\n \"delete your account form\",\n \"All your posted content will NOT be deleted, but username associated with it will be changed to one shared by all deleted accounts.\"\n )}\n

    \n
    \n
    \n
    \n \n \n \n \n
    \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Form from \"misago/components/edit-details\"\nimport title from \"misago/services/page-title\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends React.Component {\n componentDidMount() {\n title.set({\n title: pgettext(\"edit details\", \"Edit details\"),\n parent: pgettext(\"forum options\", \"Change your options\"),\n })\n }\n\n onSuccess = () => {\n snackbar.info(\n pgettext(\"profile details form\", \"Your details have been updated.\")\n )\n }\n\n render() {\n return (\n
    \n )\n }\n}\n","import React from \"react\"\nimport moment from \"moment\"\nimport Button from \"misago/components/button\"\nimport ajax from \"misago/services/ajax\"\nimport title from \"misago/services/page-title\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class DownloadData extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n isSubmiting: false,\n downloads: [],\n }\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"download your data title\", \"Download your data\"),\n parent: pgettext(\"forum options\", \"Change your options\"),\n })\n\n this.handleLoadDownloads()\n }\n\n handleLoadDownloads = () => {\n ajax.get(this.props.user.api.data_downloads).then(\n (data) => {\n this.setState({\n isLoading: false,\n downloads: data,\n })\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n }\n\n handleRequestDataDownload = () => {\n this.setState({ isSubmiting: true })\n ajax.post(this.props.user.api.request_data_download).then(\n () => {\n this.handleLoadDownloads()\n snackbar.success(\n pgettext(\n \"download your data\",\n \"Your request for data download has been registered.\"\n )\n )\n this.setState({ isSubmiting: false })\n },\n (rejection) => {\n snackbar.apiError(rejection)\n this.setState({ isSubmiting: false })\n }\n )\n }\n\n render() {\n return (\n
    \n
    \n
    \n

    \n {pgettext(\"download your data title\", \"Download your data\")}\n

    \n
    \n
    \n

    \n {pgettext(\n \"download your data\",\n 'To download your data from the site, click the \"Request data download\" button. Depending on amount of data to be archived and number of users wanting to download their data at same time it may take up to few days for your download to be prepared. An e-mail with notification will be sent to you when your data is ready to be downloaded.'\n )}\n

    \n\n

    \n {pgettext(\n \"download your data\",\n \"The download will only be available for limited amount of time, after which it will be deleted from the site and marked as expired.\"\n )}\n

    \n
    \n \n \n \n \n \n \n \n \n {this.state.downloads.map((item) => {\n return (\n \n \n \n \n )\n })}\n {this.state.downloads.length == 0 ? (\n \n \n \n ) : null}\n \n
    {pgettext(\"download your data table\", \"Requested on\")}\n {pgettext(\"download your data table\", \"Download\")}\n
    \n {moment(item.requested_on).fromNow()}\n \n \n
    \n {pgettext(\n \"download your data table\",\n \"You have no data downloads.\"\n )}\n
    \n
    \n \n {pgettext(\"download your data btn\", \"Request data download\")}\n \n
    \n
    \n
    \n )\n }\n}\n\nconst rowStyle = {\n verticalAlign: \"middle\",\n}\n\nconst STATUS_PENDING = 0\nconst STATUS_PROCESSING = 1\n\nconst DownloadButton = ({ exportFile, status }) => {\n if (status === STATUS_PENDING || status === STATUS_PROCESSING) {\n return (\n \n {pgettext(\"download your data table btn\", \"Download is being prepared\")}\n \n )\n }\n\n if (exportFile) {\n return (\n \n {pgettext(\"download your data table btn\", \"Download your data\")}\n \n )\n }\n\n return (\n \n {pgettext(\"download your data table btn\", \"Download is expired\")}\n \n )\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport Select from \"misago/components/select\"\nimport YesNoSwitch from \"misago/components/yes-no-switch\"\nimport { patch } from \"misago/reducers/auth\"\nimport ajax from \"misago/services/ajax\"\nimport title from \"misago/services/page-title\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nconst WATCH_CHOICES = [\n {\n value: 0,\n icon: \"notifications_none\",\n label: pgettext(\"watch thread choice\", \"No\"),\n },\n {\n value: 1,\n icon: \"notifications\",\n label: pgettext(\"watch thread choice\", \"Yes, with on site notifications\"),\n },\n {\n value: 2,\n icon: \"mail\",\n label: pgettext(\n \"watch thread choice\",\n \"Yes, with on site and e-mail notifications\"\n ),\n },\n]\n\nconst NOTIFICATION_CHOICES = [\n {\n value: 0,\n icon: \"notifications_none\",\n label: pgettext(\"notification preference\", \"Don't notify\"),\n },\n {\n value: 1,\n icon: \"notifications\",\n label: pgettext(\"notification preference\", \"Notify on site\"),\n },\n {\n value: 2,\n icon: \"mail\",\n label: pgettext(\n \"notification preference\",\n \"Notify on site and with e-mail\"\n ),\n },\n]\n\nexport default class ForumOptionsForm extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n is_hiding_presence: props.user.is_hiding_presence,\n limits_private_thread_invites_to:\n props.user.limits_private_thread_invites_to,\n\n watch_started_threads: props.user.watch_started_threads,\n watch_replied_threads: props.user.watch_replied_threads,\n watch_new_private_threads_by_followed:\n props.user.watch_new_private_threads_by_followed,\n watch_new_private_threads_by_other_users:\n props.user.watch_new_private_threads_by_other_users,\n notify_new_private_threads_by_followed:\n props.user.notify_new_private_threads_by_followed,\n notify_new_private_threads_by_other_users:\n props.user.notify_new_private_threads_by_other_users,\n\n errors: {},\n }\n\n this.privateThreadInvitesChoices = [\n {\n value: 0,\n icon: \"help_outline\",\n label: pgettext(\n \"private threads preference\",\n \"Anybody can invite me to their private threads\"\n ),\n },\n {\n value: 1,\n icon: \"done_all\",\n label: pgettext(\n \"private threads preference\",\n \"Only those I follow can invite me to their private threads\"\n ),\n },\n {\n value: 2,\n icon: \"highlight_off\",\n label: pgettext(\n \"private threads preference\",\n \"Nobody can invite me to their private threads\"\n ),\n },\n ]\n }\n\n send() {\n return ajax.post(this.props.user.api.options, {\n is_hiding_presence: this.state.is_hiding_presence,\n limits_private_thread_invites_to:\n this.state.limits_private_thread_invites_to,\n\n watch_started_threads: this.state.watch_started_threads,\n watch_replied_threads: this.state.watch_replied_threads,\n watch_new_private_threads_by_followed:\n this.state.watch_new_private_threads_by_followed,\n watch_new_private_threads_by_other_users:\n this.state.watch_new_private_threads_by_other_users,\n notify_new_private_threads_by_followed:\n this.state.notify_new_private_threads_by_followed,\n notify_new_private_threads_by_other_users:\n this.state.notify_new_private_threads_by_other_users,\n })\n }\n\n handleSuccess() {\n store.dispatch(\n patch({\n is_hiding_presence: this.state.is_hiding_presence,\n limits_private_thread_invites_to:\n this.state.limits_private_thread_invites_to,\n\n watch_started_threads: this.state.watch_started_threads,\n watch_replied_threads: this.state.watch_replied_threads,\n watch_new_private_threads_by_followed:\n this.state.watch_new_private_threads_by_followed,\n watch_new_private_threads_by_other_users:\n this.state.watch_new_private_threads_by_other_users,\n notify_new_private_threads_by_followed:\n this.state.notify_new_private_threads_by_followed,\n notify_new_private_threads_by_other_users:\n this.state.notify_new_private_threads_by_other_users,\n })\n )\n snackbar.success(\n pgettext(\"forum options form\", \"Your forum options have been updated.\")\n )\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n snackbar.error(\n pgettext(\"forum options form\", \"Please reload the page and try again.\")\n )\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"forum options title\", \"Forum options\"),\n parent: pgettext(\"forum options\", \"Change your options\"),\n })\n }\n\n render() {\n return (\n \n
    \n
    \n

    \n {pgettext(\"forum options form title\", \"Change forum options\")}\n

    \n
    \n
    \n
    \n \n {pgettext(\"forum options form\", \"Privacy settings\")}\n \n\n
    \n\n
    \n \n {pgettext(\"notifications options\", \"Notifications preferences\")}\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    \n
    \n
    \n \n
    \n
    \n \n )\n }\n}\n","import React from \"react\"\nimport PanelLoader from \"misago/components/panel-loader\"\n\nexport default function () {\n return (\n
    \n
    \n

    \n {pgettext(\"change username title\", \"Change username\")}\n

    \n
    \n \n
    \n )\n}\n","import React from \"react\"\nimport PanelMessage from \"misago/components/panel-message\"\n\nexport default class extends React.Component {\n getHelpText() {\n if (this.props.options.next_on) {\n return interpolate(\n pgettext(\n \"change username\",\n \"You will be able to change your username %(next_change)s.\"\n ),\n { next_change: this.props.options.next_on.fromNow() },\n true\n )\n } else {\n return pgettext(\n \"change username\",\n \"You have used up available name changes.\"\n )\n }\n }\n\n render() {\n return (\n
    \n
    \n

    \n {pgettext(\"change username title\", \"Change username\")}\n

    \n
    \n \n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport * as validators from \"misago/utils/validators\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n username: \"\",\n\n validators: {\n username: [\n validators.usernameContent(),\n validators.usernameMinLength(props.options.length_min),\n validators.usernameMaxLength(props.options.length_max),\n ],\n },\n\n isLoading: false,\n }\n }\n\n getHelpText() {\n let phrases = []\n\n if (this.props.options.changes_left > 0) {\n let message = npgettext(\n \"change username form\",\n \"You can change your username %(changes_left)s more time.\",\n \"You can change your username %(changes_left)s more times.\",\n this.props.options.changes_left\n )\n\n phrases.push(\n interpolate(\n message,\n {\n changes_left: this.props.options.changes_left,\n },\n true\n )\n )\n }\n\n if (this.props.user.acl.name_changes_expire > 0) {\n let message = npgettext(\n \"change username form\",\n \"Used changes become available again after %(name_changes_expire)s day.\",\n \"Used changes become available again after %(name_changes_expire)s days.\",\n this.props.user.acl.name_changes_expire\n )\n\n phrases.push(\n interpolate(\n message,\n {\n name_changes_expire: this.props.user.acl.name_changes_expire,\n },\n true\n )\n )\n }\n\n return phrases.length ? phrases.join(\" \") : null\n }\n\n clean() {\n let errors = this.validate()\n if (errors.username) {\n snackbar.error(errors.username[0])\n return false\n }\n if (this.state.username.trim() === this.props.user.username) {\n snackbar.info(\n pgettext(\n \"change username form\",\n \"Your new username is same as current one.\"\n )\n )\n return false\n } else {\n return true\n }\n }\n\n send() {\n return ajax.post(this.props.user.api.username, {\n username: this.state.username,\n })\n }\n\n handleSuccess(success) {\n this.setState({\n username: \"\",\n })\n\n this.props.complete(success.username, success.slug, success.options)\n }\n\n handleError(rejection) {\n snackbar.apiError(rejection)\n }\n\n render() {\n return (\n
    \n
    \n
    \n

    \n {pgettext(\"change username title\", \"Change username\")}\n

    \n
    \n
    \n \n \n \n
    \n
    \n \n
    \n
    \n
    \n )\n }\n}\n","import moment from \"moment\"\nimport React from \"react\"\nimport FormLoading from \"misago/components/options/change-username/form-loading\"\nimport FormLocked from \"misago/components/options/change-username/form-locked\"\nimport Form from \"misago/components/options/change-username/form\"\nimport UsernameHistory from \"misago/components/username-history/root\"\nimport misago from \"misago/index\"\nimport { hydrate, addNameChange } from \"misago/reducers/username-history\"\nimport { updateUsername } from \"misago/reducers/users\"\nimport ajax from \"misago/services/ajax\"\nimport title from \"misago/services/page-title\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoaded: false,\n options: null,\n }\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"change username title\", \"Change username\"),\n parent: pgettext(\"forum options\", \"Change your options\"),\n })\n\n Promise.all([\n ajax.get(this.props.user.api.username),\n ajax.get(misago.get(\"USERNAME_CHANGES_API\"), {\n user: this.props.user.id,\n }),\n ]).then((data) => {\n store.dispatch(hydrate(data[1].results))\n\n this.setState({\n isLoaded: true,\n options: {\n changes_left: data[0].changes_left,\n length_min: data[0].length_min,\n length_max: data[0].length_max,\n next_on: data[0].next_on ? moment(data[0].next_on) : null,\n },\n })\n })\n }\n\n onComplete = (username, slug, options) => {\n this.setState({\n options,\n })\n\n store.dispatch(\n addNameChange({ username, slug }, this.props.user, this.props.user)\n )\n store.dispatch(updateUsername(this.props.user, username, slug))\n\n snackbar.success(\n pgettext(\n \"change username\",\n \"Your username has been changed successfully.\"\n )\n )\n }\n\n getChangeForm() {\n if (!this.state.isLoaded) {\n return \n }\n\n if (this.state.options.changes_left === 0) {\n return \n }\n\n return (\n \n )\n }\n\n render() {\n return (\n
    \n {this.getChangeForm()}\n \n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport * as validators from \"misago/utils/validators\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n new_email: \"\",\n password: \"\",\n\n validators: {\n new_email: [validators.email()],\n password: [],\n },\n\n isLoading: false,\n }\n }\n\n clean() {\n let errors = this.validate()\n let lengths = [\n this.state.new_email.trim().length,\n this.state.password.trim().length,\n ]\n\n if (lengths.indexOf(0) !== -1) {\n snackbar.error(pgettext(\"change email form\", \"Fill out all fields.\"))\n return false\n }\n\n if (errors.new_email) {\n snackbar.error(errors.new_email[0])\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.post(this.props.user.api.change_email, {\n new_email: this.state.new_email,\n password: this.state.password,\n })\n }\n\n handleSuccess(response) {\n this.setState({\n new_email: \"\",\n password: \"\",\n })\n\n snackbar.success(response.detail)\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n if (rejection.new_email) {\n snackbar.error(rejection.new_email)\n } else {\n snackbar.error(rejection.password)\n }\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n return (\n
    \n \n \n
    \n
    \n

    \n {pgettext(\"change email title\", \"Change e-mail address\")}\n

    \n
    \n
    \n \n \n \n\n
    \n\n \n \n \n
    \n
    \n \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n new_password: \"\",\n repeat_password: \"\",\n password: \"\",\n\n validators: {\n new_password: [],\n repeat_password: [],\n password: [],\n },\n\n isLoading: false,\n }\n }\n\n clean() {\n let errors = this.validate()\n let lengths = [\n this.state.new_password.trim().length,\n this.state.repeat_password.trim().length,\n this.state.password.trim().length,\n ]\n\n if (lengths.indexOf(0) !== -1) {\n snackbar.error(gettext(\"Fill out all fields.\"))\n return false\n }\n\n if (errors.new_password) {\n snackbar.error(errors.new_password[0])\n return false\n }\n\n if (this.state.new_password !== this.state.repeat_password) {\n snackbar.error(\n pgettext(\"change password form\", \"New passwords are different.\")\n )\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.post(this.props.user.api.change_password, {\n new_password: this.state.new_password,\n password: this.state.password,\n })\n }\n\n handleSuccess(response) {\n this.setState({\n new_password: \"\",\n repeat_password: \"\",\n password: \"\",\n })\n\n snackbar.success(response.detail)\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n if (rejection.new_password) {\n snackbar.error(rejection.new_password)\n } else {\n snackbar.error(rejection.password)\n }\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n return (\n
    \n \n \n
    \n
    \n

    \n {pgettext(\"change password title\", \"Change password\")}\n

    \n
    \n
    \n \n \n \n\n \n \n \n\n
    \n\n \n \n \n
    \n
    \n \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport misago from \"misago/index\"\n\nconst UnusablePasswordMessage = () => {\n return (\n
    \n
    \n

    \n {pgettext(\n \"change sign in credentials title\",\n \"Change email or password\"\n )}\n

    \n
    \n
    \n
    \n info_outline\n
    \n
    \n

    \n {pgettext(\n \"change sign in credentials\",\n \"You need to set a password for your account to be able to change your username or email.\"\n )}\n

    \n

    \n \n {pgettext(\"change sign in credentials link\", \"Set password\")}\n \n

    \n
    \n
    \n
    \n )\n}\n\nexport default UnusablePasswordMessage\n","import React from \"react\"\nimport ChangeEmail from \"misago/components/options/sign-in-credentials/change-email\"\nimport ChangePassword from \"misago/components/options/sign-in-credentials/change-password\"\nimport misago from \"misago/index\"\nimport title from \"misago/services/page-title\"\nimport UnusablePasswordMessage from \"./UnusablePasswordMessage\"\n\nexport default class extends React.Component {\n componentDidMount() {\n title.set({\n title: pgettext(\n \"change sign in credentials title\",\n \"Change email or password\"\n ),\n parent: pgettext(\"forum options\", \"Change your options\"),\n })\n }\n\n render() {\n if (!this.props.user.has_usable_password) {\n return \n }\n\n return (\n \n )\n }\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport { SideNav, CompactNav } from \"misago/components/options/navs\"\nimport DeleteAccount from \"misago/components/options/delete-account\"\nimport EditDetails from \"misago/components/options/edit-details\"\nimport DownloadData from \"misago/components/options/download-data\"\nimport ChangeForumOptions from \"misago/components/options/forum-options\"\nimport ChangeUsername from \"misago/components/options/change-username/root\"\nimport ChangeSignInCredentials from \"misago/components/options/sign-in-credentials/root\"\nimport WithDropdown from \"misago/components/with-dropdown\"\nimport misago from \"misago/index\"\nimport { FlexRow, FlexRowCol, FlexRowSection } from \"../FlexRow\"\nimport PageContainer from \"../PageContainer\"\nimport {\n PageHeader,\n PageHeaderBanner,\n PageHeaderContainer,\n} from \"../PageHeader\"\n\nexport default class extends WithDropdown {\n render() {\n const page = misago.get(\"USER_OPTIONS\").filter((page) => {\n const url = misago.get(\"USERCP_URL\") + page.component + \"/\"\n return this.props.location.pathname.substr(0, url.length) === url\n })[0]\n\n return (\n
    \n \n \n \n \n \n \n

    {pgettext(\"forum options\", \"Change your options\")}

    \n
    \n \n
    \n \n menu\n \n \n
    \n
    \n
    \n \n \n
    \n \n {page.icon}\n {page.name}\n \n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n \n
    \n
    \n \n
    \n
    {this.props.children}
    \n
    \n
    \n
    \n )\n }\n}\n\nexport function select(store) {\n return {\n tick: store.tick.tick,\n user: store.auth.user,\n \"username-history\": store[\"username-history\"],\n }\n}\n\nexport function paths() {\n const paths = [\n {\n path: misago.get(\"USERCP_URL\") + \"forum-options/\",\n component: connect(select)(ChangeForumOptions),\n },\n {\n path: misago.get(\"USERCP_URL\") + \"edit-details/\",\n component: connect(select)(EditDetails),\n },\n ]\n\n const delegateAuth = misago.get(\"SETTINGS\").DELEGATE_AUTH\n if (!delegateAuth) {\n paths.push({\n path: misago.get(\"USERCP_URL\") + \"change-username/\",\n component: connect(select)(ChangeUsername),\n })\n paths.push({\n path: misago.get(\"USERCP_URL\") + \"sign-in-credentials/\",\n component: connect(select)(ChangeSignInCredentials),\n })\n }\n\n if (misago.get(\"ENABLE_DOWNLOAD_OWN_DATA\")) {\n paths.push({\n path: misago.get(\"USERCP_URL\") + \"download-data/\",\n component: connect(select)(DownloadData),\n })\n }\n\n if (!delegateAuth && misago.get(\"ENABLE_DELETE_OWN_ACCOUNT\")) {\n paths.push({\n path: misago.get(\"USERCP_URL\") + \"delete-account/\",\n component: connect(select)(DeleteAccount),\n })\n }\n\n return paths\n}\n","import Options, { paths } from \"misago/components/options/root\"\nimport misago from \"misago/index\"\nimport mount from \"misago/utils/routed-component\"\n\nexport default function initializer(context) {\n if (context.has(\"USER_OPTIONS\")) {\n mount({\n root: misago.get(\"USERCP_URL\"),\n component: Options,\n paths: paths(),\n })\n }\n}\n\nmisago.addInitializer({\n name: \"component:options\",\n initializer: initializer,\n after: \"store\",\n})\n","import moment from \"moment\"\nimport React from \"react\"\nimport PanelLoader from \"misago/components/panel-loader\"\nimport PanelMessage from \"misago/components/panel-message\"\nimport misago from \"misago/index\"\nimport polls from \"misago/services/polls\"\nimport title from \"misago/services/page-title\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n if (misago.has(\"PROFILE_BAN\")) {\n this.initWithPreloadedData(misago.pop(\"PROFILE_BAN\"))\n } else {\n this.initWithoutPreloadedData()\n }\n\n this.startPolling(props.profile.api.ban)\n }\n\n initWithPreloadedData(ban) {\n if (ban.expires_on) {\n ban.expires_on = moment(ban.expires_on)\n }\n\n this.state = {\n isLoaded: true,\n ban,\n }\n }\n\n initWithoutPreloadedData() {\n this.state = {\n isLoaded: false,\n }\n }\n\n startPolling(api) {\n polls.start({\n poll: \"ban-details\",\n url: api,\n frequency: 90 * 1000,\n update: this.update,\n error: this.error,\n })\n }\n\n update = (ban) => {\n if (ban.expires_on) {\n ban.expires_on = moment(ban.expires_on)\n }\n\n this.setState({\n isLoaded: true,\n error: null,\n\n ban,\n })\n }\n\n error = (error) => {\n this.setState({\n isLoaded: true,\n error: error.detail,\n ban: null,\n })\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"profile ban details title\", \"Ban details\"),\n parent: this.props.profile.username,\n })\n }\n\n componentWillUnmount() {\n polls.stop(\"ban-details\")\n }\n\n getUserMessage() {\n if (this.state.ban.user_message) {\n return (\n
    \n

    {pgettext(\"profile ban details\", \"User-shown ban message\")}

    \n \n
    \n )\n } else {\n return null\n }\n }\n\n getStaffMessage() {\n if (this.state.ban.staff_message) {\n return (\n
    \n

    {pgettext(\"profile ban details\", \"Team-shown ban message\")}

    \n \n
    \n )\n } else {\n return null\n }\n }\n\n getExpirationMessage() {\n if (this.state.ban.expires_on) {\n if (this.state.ban.expires_on.isAfter(moment())) {\n let title = interpolate(\n pgettext(\n \"profile ban details\",\n \"This ban expires on %(expires_on)s.\"\n ),\n {\n expires_on: this.state.ban.expires_on.format(\"LL, LT\"),\n },\n true\n )\n\n let message = interpolate(\n pgettext(\"profile ban details\", \"This ban expires %(expires_on)s.\"),\n {\n expires_on: this.state.ban.expires_on.fromNow(),\n },\n true\n )\n\n return {message}\n } else {\n return pgettext(\"profile ban details\", \"This ban has expired.\")\n }\n } else {\n return interpolate(\n pgettext(\"profile ban details\", \"%(username)s's ban is permanent.\"),\n {\n username: this.props.profile.username,\n },\n true\n )\n }\n }\n\n getPanelBody() {\n if (this.state.ban) {\n if (Object.keys(this.state.ban).length) {\n return (\n
    \n {this.getUserMessage()}\n {this.getStaffMessage()}\n\n
    \n

    {pgettext(\"profile ban details\", \"Ban expiration\")}

    \n

    {this.getExpirationMessage()}

    \n
    \n
    \n )\n } else {\n return (\n
    \n \n
    \n )\n }\n } else if (this.state.error) {\n return (\n
    \n \n
    \n )\n } else {\n return (\n
    \n \n
    \n )\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n

    \n {pgettext(\"profile ban details title\", \"Ban details\")}\n

    \n
    \n\n {this.getPanelBody()}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Form from \"misago/components/edit-details\"\n\nexport default function ({ api, display, onCancel, onSuccess }) {\n if (!display) return null\n\n return
    \n}\n","import React from \"react\"\n\nexport default function ({ isAuthenticated, profile }) {\n let message = null\n if (isAuthenticated) {\n message = pgettext(\n \"profile details empty\",\n \"You are not sharing any details with others.\"\n )\n } else {\n message = interpolate(\n pgettext(\n \"profile details empty\",\n \"%(username)s is not sharing any details with others.\"\n ),\n {\n username: profile.username,\n },\n true\n )\n }\n\n return (\n
    \n
    {message}
    \n
    \n )\n}\n","import React from \"react\"\n\nexport default function ({ html, text, url }) {\n if (html) {\n return (\n \n )\n }\n\n return (\n
    \n \n
    \n )\n}\n\nexport function SafeValue({ text, url }) {\n if (url) {\n return (\n

    \n \n {text || url}\n \n

    \n )\n }\n\n if (text) {\n return

    {text}

    \n }\n\n return null\n}\n","import React from \"react\"\nimport FieldValue from \"./field-value\"\n\nexport default function (props) {\n return (\n
    \n {props.name}:\n \n
    \n )\n}\n","import React from \"react\"\nimport Field from \"./field\"\n\nexport default function ({ fields, name }) {\n return (\n
    \n
    \n

    {name}

    \n
    \n
    \n
    \n {fields.map(({ fieldname, html, name, text, url }) => {\n return (\n \n )\n })}\n
    \n
    \n
    \n )\n}\n","import React from \"react\"\nimport EmptyMessage from \"./empty-message\"\nimport Group from \"./group\"\nimport Loader from \"misago/components/loader\"\n\nexport default function ({\n display,\n groups,\n isAuthenticated,\n loading,\n profile,\n}) {\n if (!display) return null\n\n if (loading) {\n return \n }\n\n if (!groups.length) {\n return \n }\n\n return (\n
    \n {groups.map((group, i) => {\n return \n })}\n
    \n )\n}\n","import React from \"react\"\nimport { Toolbar, ToolbarItem, ToolbarSection } from \"../../Toolbar\"\n\nconst ProfileDetailsHeader = ({ onEdit, showEditButton }) => (\n \n \n \n

    {pgettext(\"profile details title\", \"Details\")}

    \n
    \n
    \n {showEditButton && (\n \n \n \n {pgettext(\"profile details edit btn\", \"Edit\")}\n \n \n \n )}\n
    \n)\n\nexport default ProfileDetailsHeader\n","import React from \"react\"\nimport { load } from \"misago/reducers/profile-details\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends React.Component {\n componentDidMount() {\n const { data, dispatch, user } = this.props\n if (data && data.id === user.id) return\n\n ajax.get(this.props.user.api.details).then(\n (data) => {\n dispatch(load(data))\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n }\n\n render() {\n return this.props.children\n }\n}\n","import React from \"react\"\nimport Form from \"./form\"\nimport GroupsList from \"./groups-list\"\nimport Header from \"./header\"\nimport ProfileDetailsData from \"misago/data/profile-details\"\nimport { load as loadDetails } from \"misago/reducers/profile-details\"\nimport title from \"misago/services/page-title\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n editing: false,\n }\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"profile details title\", \"Details\"),\n parent: this.props.profile.username,\n })\n }\n\n onCancel = () => {\n this.setState({ editing: false })\n }\n\n onEdit = () => {\n this.setState({ editing: true })\n }\n\n onSuccess = (newDetails) => {\n const { dispatch, isAuthenticated, profile } = this.props\n\n let message = null\n if (isAuthenticated) {\n message = pgettext(\n \"profile details form\",\n \"Your details have been updated.\"\n )\n } else {\n message = interpolate(\n pgettext(\n \"profile details form\",\n \"%(username)s's details have been updated.\"\n ),\n {\n username: profile.username,\n },\n true\n )\n }\n\n snackbar.info(message)\n dispatch(loadDetails(newDetails))\n this.setState({ editing: false })\n }\n\n render() {\n const { dispatch, isAuthenticated, profile, profileDetails } = this.props\n const loading = profileDetails.id !== profile.id\n\n return (\n \n
    \n \n \n \n
    \n \n )\n }\n}\n","import React from \"react\"\nimport PostFeed from \"misago/components/post-feed\"\nimport Button from \"misago/components/button\"\nimport * as posts from \"misago/reducers/posts\"\nimport title from \"misago/services/page-title\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\nimport { Toolbar, ToolbarItem, ToolbarSection } from \"../../Toolbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n }\n }\n\n loadItems(start = 0) {\n ajax\n .get(this.props.api, {\n start: start || 0,\n })\n .then(\n (data) => {\n if (start === 0) {\n store.dispatch(posts.load(data))\n } else {\n store.dispatch(posts.append(data))\n }\n\n this.setState({\n isLoading: false,\n })\n },\n (rejection) => {\n this.setState({\n isLoading: false,\n })\n\n snackbar.apiError(rejection)\n }\n )\n }\n\n loadMore = () => {\n this.setState({\n isLoading: true,\n })\n\n this.loadItems(this.props.posts.next)\n }\n\n componentDidMount() {\n title.set({\n title: this.props.title,\n parent: this.props.profile.username,\n })\n\n this.loadItems()\n }\n\n render() {\n return (\n
    \n \n \n \n

    {this.props.header}

    \n
    \n
    \n
    \n \n
    \n )\n }\n}\n\nexport function Feed(props) {\n if (props.posts.isLoaded && !props.posts.results.length) {\n return

    {props.emptyMessage}

    \n }\n\n return (\n
    \n \n \n
    \n )\n}\n\nexport function LoadMoreButton(props) {\n if (!props.next) return null\n\n return (\n
    \n \n {pgettext(\"profile load more btn\", \"Show older activity\")}\n \n
    \n )\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n getClassName() {\n if (this.props.className) {\n return \"form-search \" + this.props.className\n } else {\n return \"form-search\"\n }\n }\n\n render() {\n return (\n
    \n \n search\n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Search from \"misago/components/quick-search\"\nimport UsersList from \"misago/components/users-list\"\nimport misago from \"misago/index\"\nimport { hydrate, append } from \"misago/reducers/users\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\nimport title from \"misago/services/page-title\"\nimport { Toolbar, ToolbarItem, ToolbarSection } from \"../Toolbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.setSpecialProps()\n\n if (misago.has(this.PRELOADED_DATA_KEY)) {\n this.initWithPreloadedData(misago.pop(this.PRELOADED_DATA_KEY))\n } else {\n this.initWithoutPreloadedData()\n }\n }\n\n setSpecialProps() {\n this.PRELOADED_DATA_KEY = \"PROFILE_FOLLOWERS\"\n this.TITLE = pgettext(\"profile followers title\", \"Followers\")\n this.API_FILTER = \"followers\"\n }\n\n initWithPreloadedData(data) {\n this.state = {\n isLoaded: true,\n isBusy: false,\n\n search: \"\",\n\n count: data.count,\n more: data.more,\n\n page: data.page,\n pages: data.pages,\n }\n\n store.dispatch(hydrate(data.results))\n }\n\n initWithoutPreloadedData() {\n this.state = {\n isLoaded: false,\n isBusy: false,\n\n search: \"\",\n\n count: 0,\n more: 0,\n\n page: 1,\n pages: 1,\n }\n\n this.loadUsers()\n }\n\n loadUsers(page = 1, search = null) {\n const apiUrl = this.props.profile.api[this.API_FILTER]\n\n ajax\n .get(\n apiUrl,\n {\n search: search,\n page: page || 1,\n },\n \"user-\" + this.API_FILTER\n )\n .then(\n (data) => {\n if (page === 1) {\n store.dispatch(hydrate(data.results))\n } else {\n store.dispatch(append(data.results))\n }\n\n this.setState({\n isLoaded: true,\n isBusy: false,\n\n count: data.count,\n more: data.more,\n\n page: data.page,\n pages: data.pages,\n })\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n }\n\n componentDidMount() {\n title.set({\n title: this.TITLE,\n parent: this.props.profile.username,\n })\n }\n\n loadMore = () => {\n this.setState({\n isBusy: true,\n })\n\n this.loadUsers(this.state.page + 1, this.state.search)\n }\n\n search = (ev) => {\n this.setState({\n isLoaded: false,\n isBusy: true,\n\n search: ev.target.value,\n\n count: 0,\n more: 0,\n\n page: 1,\n pages: 1,\n })\n\n this.loadUsers(1, ev.target.value)\n }\n\n getLabel() {\n if (!this.state.isLoaded) {\n return pgettext(\"Loading...\")\n } else if (this.state.search) {\n let message = npgettext(\n \"profile followers\",\n \"Found %(users)s user.\",\n \"Found %(users)s users.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n users: this.state.count,\n },\n true\n )\n } else if (this.props.profile.id === this.props.user.id) {\n let message = npgettext(\n \"profile followers\",\n \"You have %(users)s follower.\",\n \"You have %(users)s followers.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n users: this.state.count,\n },\n true\n )\n } else {\n let message = npgettext(\n \"profile followers\",\n \"%(username)s has %(users)s follower.\",\n \"%(username)s has %(users)s followers.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n username: this.props.profile.username,\n users: this.state.count,\n },\n true\n )\n }\n }\n\n getEmptyMessage() {\n if (this.state.search) {\n return pgettext(\n \"profile followers\",\n \"Search returned no users matching specified criteria.\"\n )\n } else if (this.props.user.id === this.props.profile.id) {\n return pgettext(\"profile followers\", \"You have no followers.\")\n } else {\n return interpolate(\n pgettext(\"profile followers\", \"%(username)s has no followers.\"),\n {\n username: this.props.profile.username,\n },\n true\n )\n }\n }\n\n getMoreButton() {\n if (!this.state.more) return null\n\n return (\n
    \n \n {interpolate(\n pgettext(\"profile followers\", \"Show more (%(more)s)\"),\n {\n more: this.state.more,\n },\n true\n )}\n \n
    \n )\n }\n\n getListBody() {\n if (this.state.isLoaded && this.state.count === 0) {\n return

    {this.getEmptyMessage()}

    \n }\n\n return (\n
    \n \n\n {this.getMoreButton()}\n
    \n )\n }\n\n getClassName() {\n return \"profile-\" + this.API_FILTER\n }\n\n render() {\n return (\n
    \n \n \n \n

    {this.getLabel()}

    \n
    \n
    \n \n \n \n \n \n
    \n\n {this.getListBody()}\n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Search from \"misago/components/quick-search\"\nimport UsernameHistory from \"misago/components/username-history/root\"\nimport misago from \"misago/index\"\nimport { hydrate, append } from \"misago/reducers/username-history\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\nimport title from \"misago/services/page-title\"\nimport { Toolbar, ToolbarItem, ToolbarSection } from \"../Toolbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n if (misago.has(\"PROFILE_NAME_HISTORY\")) {\n this.initWithPreloadedData(misago.pop(\"PROFILE_NAME_HISTORY\"))\n } else {\n this.initWithoutPreloadedData()\n }\n }\n\n initWithPreloadedData(data) {\n this.state = {\n isLoaded: true,\n isBusy: false,\n\n search: \"\",\n\n count: data.count,\n more: data.more,\n\n page: data.page,\n pages: data.pages,\n }\n\n store.dispatch(hydrate(data.results))\n }\n\n initWithoutPreloadedData() {\n this.state = {\n isLoaded: false,\n isBusy: false,\n\n search: \"\",\n\n count: 0,\n more: 0,\n\n page: 1,\n pages: 1,\n }\n\n this.loadChanges()\n }\n\n loadChanges(page = 1, search = null) {\n ajax\n .get(\n misago.get(\"USERNAME_CHANGES_API\"),\n {\n user: this.props.profile.id,\n search: search,\n page: page || 1,\n },\n \"search-username-history\"\n )\n .then(\n (data) => {\n if (page === 1) {\n store.dispatch(hydrate(data.results))\n } else {\n store.dispatch(append(data.results))\n }\n\n this.setState({\n isLoaded: true,\n isBusy: false,\n\n count: data.count,\n more: data.more,\n\n page: data.page,\n pages: data.pages,\n })\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"profile username history title\", \"Username history\"),\n parent: this.props.profile.username,\n })\n }\n\n loadMore = () => {\n this.setState({\n isBusy: true,\n })\n\n this.loadChanges(this.state.page + 1, this.state.search)\n }\n\n search = (ev) => {\n this.setState({\n isLoaded: false,\n isBusy: true,\n\n search: ev.target.value,\n\n count: 0,\n more: 0,\n\n page: 1,\n pages: 1,\n })\n\n this.loadChanges(1, ev.target.value)\n }\n\n getLabel() {\n if (!this.state.isLoaded) {\n return pgettext(\"profile username history\", \"Loading...\")\n } else if (this.state.search) {\n let message = npgettext(\n \"profile username history\",\n \"Found %(changes)s username change.\",\n \"Found %(changes)s username changes.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n changes: this.state.count,\n },\n true\n )\n } else if (this.props.profile.id === this.props.user.id) {\n let message = npgettext(\n \"profile username history\",\n \"Your username was changed %(changes)s time.\",\n \"Your username was changed %(changes)s times.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n changes: this.state.count,\n },\n true\n )\n } else {\n let message = npgettext(\n \"profile username history\",\n \"%(username)s's username was changed %(changes)s time.\",\n \"%(username)s's username was changed %(changes)s times.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n username: this.props.profile.username,\n changes: this.state.count,\n },\n true\n )\n }\n }\n\n getEmptyMessage() {\n if (this.state.search) {\n return pgettext(\n \"profile username history\",\n \"Search returned no username changes matching specified criteria.\"\n )\n } else if (this.props.user.id === this.props.profile.id) {\n return pgettext(\n \"profile username history\",\n \"No name changes have been recorded for your account.\"\n )\n } else {\n return interpolate(\n pgettext(\n \"profile username history\",\n \"%(username)s's username was never changed.\"\n ),\n {\n username: this.props.profile.username,\n },\n true\n )\n }\n }\n\n getMoreButton() {\n if (!this.state.more) return null\n\n return (\n
    \n \n {interpolate(\n pgettext(\"profile username history\", \"Show older (%(more)s)\"),\n {\n more: this.state.more,\n },\n true\n )}\n \n
    \n )\n }\n\n render() {\n return (\n
    \n \n \n \n

    {this.getLabel()}

    \n
    \n
    \n \n \n \n \n \n
    \n\n \n\n {this.getMoreButton()}\n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport { patch } from \"misago/reducers/profile\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n }\n }\n\n getClassName() {\n if (this.props.profile.is_followed) {\n return this.props.className + \" btn-default btn-following\"\n } else {\n return this.props.className + \" btn-default btn-follow\"\n }\n }\n\n getIcon() {\n if (this.props.profile.is_followed) {\n return \"favorite\"\n } else {\n return \"favorite_border\"\n }\n }\n\n getLabel() {\n if (this.props.profile.is_followed) {\n return pgettext(\"user profile follow btn\", \"Following\")\n } else {\n return pgettext(\"user profile follow btn\", \"Follow\")\n }\n }\n\n action = () => {\n this.setState({\n isLoading: true,\n })\n\n if (this.props.profile.is_followed) {\n store.dispatch(\n patch({\n is_followed: false,\n followers: this.props.profile.followers - 1,\n })\n )\n } else {\n store.dispatch(\n patch({\n is_followed: true,\n followers: this.props.profile.followers + 1,\n })\n )\n }\n\n ajax.post(this.props.profile.api.follow).then(\n (data) => {\n this.setState({\n isLoading: false,\n })\n\n store.dispatch(patch(data))\n },\n (rejection) => {\n this.setState({\n isLoading: false,\n })\n snackbar.apiError(rejection)\n }\n )\n }\n\n render() {\n return (\n \n {this.getIcon()}\n {this.getLabel()}\n \n )\n }\n}\n","import React from \"react\"\nimport posting from \"misago/services/posting\"\nimport misago from \"misago\"\n\nexport default class extends React.Component {\n onClick = () => {\n posting.open({\n mode: \"START_PRIVATE\",\n submit: misago.get(\"PRIVATE_THREADS_API\"),\n\n to: [this.props.profile],\n })\n }\n\n render() {\n const canMessage = this.props.user.acl.can_start_private_threads\n const isProfileOwner = this.props.user.id === this.props.profile.id\n\n if (!canMessage || isProfileOwner) return null\n\n return (\n \n comment\n {pgettext(\"profile message btn\", \"Message\")}\n \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport Loader from \"misago/components/modal-loader\"\nimport YesNoSwitch from \"misago/components/yes-no-switch\"\nimport ModalMessage from \"misago/components/modal-message\"\nimport { updateAvatar } from \"misago/reducers/users\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoaded: false,\n isLoading: false,\n error: null,\n\n is_avatar_locked: \"\",\n avatar_lock_user_message: \"\",\n avatar_lock_staff_message: \"\",\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.profile.api.moderate_avatar).then(\n (options) => {\n this.setState({\n isLoaded: true,\n\n is_avatar_locked: options.is_avatar_locked,\n avatar_lock_user_message: options.avatar_lock_user_message || \"\",\n avatar_lock_staff_message: options.avatar_lock_staff_message || \"\",\n })\n },\n (rejection) => {\n this.setState({\n isLoaded: true,\n error: rejection.detail,\n })\n }\n )\n }\n\n clean() {\n if (this.isValid()) {\n return true\n } else {\n snackbar.error(this.validate().username[0])\n return false\n }\n }\n\n send() {\n return ajax.post(this.props.profile.api.moderate_avatar, {\n is_avatar_locked: this.state.is_avatar_locked,\n avatar_lock_user_message: this.state.avatar_lock_user_message,\n avatar_lock_staff_message: this.state.avatar_lock_staff_message,\n })\n }\n\n handleSuccess(apiResponse) {\n store.dispatch(updateAvatar(this.props.profile, apiResponse.avatar_hash))\n snackbar.success(\n pgettext(\n \"profile avatar moderation\",\n \"Avatar controls have been changed.\"\n )\n )\n }\n\n getFormBody() {\n return (\n \n
    \n \n \n \n\n \n \n \n\n \n \n \n
    \n
    \n \n {pgettext(\"profile avatar moderation btn\", \"Close\")}\n \n \n
    \n \n )\n }\n\n getModalBody() {\n if (this.state.error) {\n return (\n \n )\n } else if (this.state.isLoaded) {\n return this.getFormBody()\n } else {\n return \n }\n }\n\n getClassName() {\n if (this.state.error) {\n return \"modal-dialog modal-message modal-avatar-controls\"\n } else {\n return \"modal-dialog modal-avatar-controls\"\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"profile avatar moderation title\", \"Avatar controls\")}\n

    \n
    \n {this.getModalBody()}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport Loader from \"misago/components/modal-loader\"\nimport ModalMessage from \"misago/components/modal-message\"\nimport { addNameChange } from \"misago/reducers/username-history\"\nimport { updateUsername } from \"misago/reducers/users\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\nimport * as validators from \"misago/utils/validators\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoaded: false,\n isLoading: false,\n error: null,\n\n username: \"\",\n validators: {\n username: [validators.usernameContent()],\n },\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.profile.api.moderate_username).then(\n () => {\n this.setState({\n isLoaded: true,\n })\n },\n (rejection) => {\n this.setState({\n isLoaded: true,\n error: rejection.detail,\n })\n }\n )\n }\n\n clean() {\n if (this.isValid()) {\n return true\n } else {\n snackbar.error(this.validate().username[0])\n return false\n }\n }\n\n send() {\n return ajax.post(this.props.profile.api.moderate_username, {\n username: this.state.username,\n })\n }\n\n handleSuccess(apiResponse) {\n this.setState({\n username: \"\",\n })\n\n store.dispatch(\n addNameChange(apiResponse, this.props.profile, this.props.user)\n )\n store.dispatch(\n updateUsername(this.props.profile, apiResponse.username, apiResponse.slug)\n )\n\n snackbar.success(\n pgettext(\"profile username moderation\", \"Username has been changed.\")\n )\n }\n\n getFormBody() {\n return (\n
    \n
    \n \n \n \n
    \n
    \n \n {pgettext(\"profile username moderation btn\", \"Cancel\")}\n \n \n
    \n
    \n )\n }\n\n getModalBody() {\n if (this.state.error) {\n return (\n \n )\n } else if (this.state.isLoaded) {\n return this.getFormBody()\n } else {\n return \n }\n }\n\n getClassName() {\n if (this.state.error) {\n return \"modal-dialog modal-message modal-rename-user\"\n } else {\n return \"modal-dialog modal-rename-user\"\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"profile username moderation title\", \"Change username\")}\n

    \n
    \n {this.getModalBody()}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport Loader from \"misago/components/modal-loader\"\nimport ModalMessage from \"misago/components/modal-message\"\nimport YesNoSwitch from \"misago/components/yes-no-switch\"\nimport misago from \"misago/index\"\nimport ajax from \"misago/services/ajax\"\nimport polls from \"misago/services/polls\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoaded: false,\n isLoading: false,\n isDeleted: false,\n error: null,\n\n countdown: 5,\n confirm: false,\n\n with_content: false,\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.profile.api.delete).then(\n () => {\n this.setState({\n isLoaded: true,\n })\n\n this.countdown()\n },\n (rejection) => {\n this.setState({\n isLoaded: true,\n error: rejection.detail,\n })\n }\n )\n }\n\n countdown = () => {\n window.setTimeout(() => {\n if (this.state.countdown > 1) {\n this.setState({\n countdown: this.state.countdown - 1,\n })\n this.countdown()\n } else if (!this.state.confirm) {\n this.setState({\n confirm: true,\n })\n }\n }, 1000)\n }\n\n send() {\n return ajax.post(this.props.profile.api.delete, {\n with_content: this.state.with_content,\n })\n }\n\n handleSuccess() {\n polls.stop(\"user-profile\")\n\n if (this.state.with_content) {\n this.setState({\n isDeleted: interpolate(\n pgettext(\n \"profile delete\",\n \"%(username)s's account, threads, posts and other content has been deleted.\"\n ),\n {\n username: this.props.profile.username,\n },\n true\n ),\n })\n } else {\n this.setState({\n isDeleted: interpolate(\n pgettext(\n \"profile delete\",\n \"%(username)s's account has been deleted and other content has been hidden.\"\n ),\n {\n username: this.props.profile.username,\n },\n true\n ),\n })\n }\n }\n\n getButtonLabel() {\n if (this.state.confirm) {\n return interpolate(\n pgettext(\"profile delete btn\", \"Delete %(username)s\"),\n {\n username: this.props.profile.username,\n },\n true\n )\n } else {\n return interpolate(\n pgettext(\"profile delete btn\", \"Please wait... (%(countdown)ss)\"),\n {\n countdown: this.state.countdown,\n },\n true\n )\n }\n }\n\n getForm() {\n return (\n
    \n
    \n \n \n \n
    \n
    \n \n {pgettext(\"profile delete btn\", \"Cancel\")}\n \n\n \n {this.getButtonLabel()}\n \n
    \n
    \n )\n }\n\n getDeletedBody() {\n return (\n
    \n
    \n info_outline\n
    \n \n
    \n )\n }\n\n getModalBody() {\n if (this.state.error) {\n return (\n \n )\n } else if (this.state.isLoaded) {\n if (this.state.isDeleted) {\n return this.getDeletedBody()\n } else {\n return this.getForm()\n }\n } else {\n return \n }\n }\n\n getClassName() {\n if (this.state.error || this.state.isDeleted) {\n return \"modal-dialog modal-message modal-delete-account\"\n } else {\n return \"modal-dialog modal-delete-account\"\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"profile delete title\", \"Delete user account\")}\n

    \n
    \n {this.getModalBody()}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport AvatarControls from \"misago/components/profile/moderation/avatar-controls\"\nimport ChangeUsername from \"misago/components/profile/moderation/change-username\"\nimport DeleteAccount from \"misago/components/profile/moderation/delete-account\"\nimport modal from \"misago/services/modal\"\n\nlet select = function (store) {\n return {\n tick: store.tick,\n user: store.auth,\n profile: store.profile,\n }\n}\n\nexport default class extends React.Component {\n showAvatarDialog = () => {\n modal.show(connect(select)(AvatarControls))\n }\n\n showRenameDialog = () => {\n modal.show(connect(select)(ChangeUsername))\n }\n\n showDeleteDialog = () => {\n modal.show(connect(select)(DeleteAccount))\n }\n\n render() {\n const { moderation } = this.props\n\n return (\n
      \n {!!moderation.avatar && (\n
    • \n \n portrait\n {pgettext(\"profile moderation menu\", \"Avatar controls\")}\n \n
    • \n )}\n {!!moderation.rename && (\n
    • \n \n credit_card\n {pgettext(\"profile moderation menu\", \"Change username\")}\n \n
    • \n )}\n {!!moderation.delete && (\n
    • \n \n clear\n {pgettext(\"profile moderation menu\", \"Delete account\")}\n \n
    • \n )}\n
    \n )\n }\n}\n","import React from \"react\"\nimport Status, { StatusIcon, StatusLabel } from \"../user-status\"\n\nconst ProfileDataList = ({ profile }) => (\n
      \n {profile.is_active === false && (\n
    • \n \n {pgettext(\"profile data list\", \"Account disabled\")}\n \n
    • \n )}\n
    • \n \n \n \n \n
    • \n {profile.rank.is_tab ? (\n
    • \n \n {profile.rank.name}\n \n
    • \n ) : (\n
    • \n {profile.rank.name}\n
    • \n )}\n {(profile.title || profile.rank.title) && (\n
    • {profile.title || profile.rank.title}
    • \n )}\n
    • \n \n {interpolate(\n pgettext(\"profile data list\", \"Joined %(joined_on)s\"),\n {\n joined_on: profile.joined_on.fromNow(),\n },\n true\n )}\n \n
    • \n {profile.email && (\n
    • \n \n {profile.email}\n \n
    • \n )}\n
    \n)\n\nexport default ProfileDataList\n","import React from \"react\"\nimport Avatar from \"../avatar\"\nimport { FlexRow, FlexRowCol, FlexRowSection } from \"../FlexRow\"\nimport {\n PageHeader,\n PageHeaderBanner,\n PageHeaderContainer,\n PageHeaderDetails,\n} from \"../PageHeader\"\nimport FollowButton from \"./follow-button\"\nimport MessageButton from \"./message-button\"\nimport ModerationOptions from \"./moderation/nav\"\nimport ProfileDataList from \"./ProfileDataList\"\n\nconst ProfileHeader = ({ profile, user, moderation, message, follow }) => (\n \n \n \n
    \n
    \n \n \n \n
    \n

    {profile.username}

    \n
    \n \n \n \n \n \n \n \n \n {message && (\n \n \n \n \n {moderation.available && !follow && (\n \n
    \n \n \n
    \n
    \n )}\n
    \n )}\n {follow && (\n \n \n \n \n {moderation.available && (\n \n
    \n \n \n
    \n
    \n )}\n
    \n )}\n {moderation.available && !follow && !message && (\n \n \n
    \n \n \n
    \n
    \n \n
    \n \n settings\n {pgettext(\"profile options btn\", \"Options\")}\n \n \n
    \n
    \n
    \n )}\n
    \n
    \n \n
    \n)\n\nconst ProfileModerationButton = () => (\n \n settings\n \n)\n\nexport default ProfileHeader\n","import React from \"react\"\nimport { Link } from \"react-router\"\nimport Li from \"misago/components/li\"\n\nconst ProfileNav = ({ baseUrl, page, pages }) => (\n
    \n
    \n \n {page.icon}\n {page.name}\n \n
      \n {pages.map((page) => (\n
    • \n \n {page.icon}\n {page.name}\n \n
    • \n ))}\n
    \n
    \n
      \n {pages.map((page) => (\n
    • \n \n {page.icon}\n {page.name}\n \n
    • \n ))}\n
    \n
    \n)\n\nexport default ProfileNav\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport BanDetails from \"./ban-details\"\nimport Details from \"./details\"\nimport { Posts, Threads } from \"./feed\"\nimport Followers from \"./followers\"\nimport Follows from \"./follows\"\nimport UsernameHistory from \"./username-history\"\nimport WithDropdown from \"misago/components/with-dropdown\"\nimport misago from \"misago\"\nimport { hydrate } from \"misago/reducers/profile\"\nimport polls from \"misago/services/polls\"\nimport store from \"misago/services/store\"\nimport PageContainer from \"../PageContainer\"\nimport ProfileHeader from \"./ProfileHeader\"\nimport ProfileNav from \"./ProfileNav\"\n\nexport default class extends WithDropdown {\n constructor(props) {\n super(props)\n\n this.startPolling(props.profile.api.index)\n }\n\n startPolling(api) {\n polls.start({\n poll: \"user-profile\",\n url: api,\n frequency: 90 * 1000,\n update: this.update,\n })\n }\n\n update = (data) => {\n store.dispatch(hydrate(data))\n }\n\n render() {\n const baseUrl = misago.get(\"PROFILE\").url\n const pages = misago.get(\"PROFILE_PAGES\")\n const page = pages.filter((page) => {\n const url = baseUrl + page.component + \"/\"\n return this.props.location.pathname === url\n })[0]\n const { profile, user } = this.props\n const moderation = getModeration(profile, user)\n const message =\n !!user.acl.can_start_private_threads && profile.id !== user.id\n const follow = !!profile.acl.can_follow && profile.id !== user.id\n\n return (\n
    \n \n \n \n\n {this.props.children}\n \n
    \n )\n }\n}\n\nconst getModeration = (profile, user) => {\n const moderation = {\n available: false,\n rename: false,\n avatar: false,\n delete: false,\n }\n\n if (user.is_anonymous) return moderation\n\n moderation.rename = profile.acl.can_rename\n moderation.avatar = profile.acl.can_moderate_avatar\n moderation.delete = profile.acl.can_delete\n moderation.available = !!(\n moderation.rename ||\n moderation.avatar ||\n moderation.delete\n )\n\n return moderation\n}\n\nexport function select(store) {\n return {\n isAuthenticated: store.auth.user.id === store.profile.id,\n\n tick: store.tick.tick,\n user: store.auth.user,\n users: store.users,\n posts: store.posts,\n profile: store.profile,\n profileDetails: store[\"profile-details\"],\n \"username-history\": store[\"username-history\"],\n }\n}\n\nconst COMPONENTS = {\n posts: Posts,\n threads: Threads,\n followers: Followers,\n follows: Follows,\n details: Details,\n \"username-history\": UsernameHistory,\n \"ban-details\": BanDetails,\n}\n\nexport function paths() {\n let paths = []\n misago.get(\"PROFILE_PAGES\").forEach(function (item) {\n paths.push(\n Object.assign({}, item, {\n path: misago.get(\"PROFILE\").url + item.component + \"/\",\n component: connect(select)(COMPONENTS[item.component]),\n })\n )\n })\n\n return paths\n}\n","import React from \"react\"\nimport Route from \"./route\"\n\nexport function Threads(props) {\n let emptyMessage = null\n if (props.user.id === props.profile.id) {\n emptyMessage = pgettext(\"profile threads\", \"You have no started threads.\")\n } else {\n emptyMessage = interpolate(\n pgettext(\"profile threads\", \"%(username)s started no threads.\"),\n {\n username: props.profile.username,\n },\n true\n )\n }\n\n let header = null\n if (!props.posts.isLoaded) {\n header = pgettext(\"profile threads\", \"Loading...\")\n } else if (props.profile.id === props.user.id) {\n const message = npgettext(\n \"profile threads\",\n \"You have started %(threads)s thread.\",\n \"You have started %(threads)s threads.\",\n props.profile.threads\n )\n\n header = interpolate(\n message,\n {\n threads: props.profile.threads,\n },\n true\n )\n } else {\n const message = npgettext(\n \"profile threads\",\n \"%(username)s has started %(threads)s thread.\",\n \"%(username)s has started %(threads)s threads.\",\n props.profile.threads\n )\n\n header = interpolate(\n message,\n {\n username: props.profile.username,\n threads: props.profile.threads,\n },\n true\n )\n }\n\n return (\n \n )\n}\n\nexport function Posts(props) {\n let emptyMessage = null\n if (props.user.id === props.profile.id) {\n emptyMessage = pgettext(\"profile posts\", \"You have posted no messages.\")\n } else {\n emptyMessage = interpolate(\n pgettext(\"profile posts\", \"%(username)s posted no messages.\"),\n {\n username: props.profile.username,\n },\n true\n )\n }\n\n let header = null\n if (!props.posts.isLoaded) {\n header = pgettext(\"profile posts\", \"Loading...\")\n } else if (props.profile.id === props.user.id) {\n const message = npgettext(\n \"profile posts\",\n \"You have posted %(posts)s message.\",\n \"You have posted %(posts)s messages.\",\n props.profile.posts\n )\n\n header = interpolate(\n message,\n {\n posts: props.profile.posts,\n },\n true\n )\n } else {\n const message = npgettext(\n \"profile posts\",\n \"%(username)s has posted %(posts)s message.\",\n \"%(username)s has posted %(posts)s messages.\",\n props.profile.posts\n )\n\n header = interpolate(\n message,\n {\n username: props.profile.username,\n posts: props.profile.posts,\n },\n true\n )\n }\n\n return (\n \n )\n}\n","import React from \"react\"\nimport Followers from \"misago/components/profile/followers\"\n\nexport default class extends Followers {\n setSpecialProps() {\n this.PRELOADED_DATA_KEY = \"PROFILE_FOLLOWS\"\n this.TITLE = pgettext(\"profile follows title\", \"Follows\")\n this.API_FILTER = \"follows\"\n }\n\n getLabel() {\n if (!this.state.isLoaded) {\n return pgettext(\"profile follows\", \"Loading...\")\n } else if (this.state.search) {\n let message = npgettext(\n \"profile follows\",\n \"Found %(users)s user.\",\n \"Found %(users)s users.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n users: this.state.count,\n },\n true\n )\n } else if (this.props.profile.id === this.props.user.id) {\n let message = npgettext(\n \"profile follows\",\n \"You are following %(users)s user.\",\n \"You are following %(users)s users.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n users: this.state.count,\n },\n true\n )\n } else {\n let message = npgettext(\n \"profile follows\",\n \"%(username)s is following %(users)s user.\",\n \"%(username)s is following %(users)s users.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n username: this.props.profile.username,\n users: this.state.count,\n },\n true\n )\n }\n }\n\n getEmptyMessage() {\n if (this.state.search) {\n return pgettext(\n \"profile follows\",\n \"Search returned no users matching specified criteria.\"\n )\n } else if (this.props.user.id === this.props.profile.id) {\n return pgettext(\"profile follows\", \"You are not following any users.\")\n } else {\n return interpolate(\n pgettext(\"profile follows\", \"%(username)s is not following any users.\"),\n {\n username: this.props.profile.username,\n },\n true\n )\n }\n }\n}\n","import { connect } from \"react-redux\"\nimport Profile, { paths, select } from \"misago/components/profile/root\"\nimport misago from \"misago/index\"\nimport mount from \"misago/utils/routed-component\"\n\nexport default function initializer(context) {\n if (context.has(\"PROFILE\") && context.has(\"PROFILE_PAGES\")) {\n mount({\n root: misago.get(\"PROFILE\").url,\n component: connect(select)(Profile),\n paths: paths(),\n })\n }\n}\n\nmisago.addInitializer({\n name: \"component:profile\",\n initializer: initializer,\n after: \"reducer:profile-hydrate\",\n})\n","import React from \"react\"\nimport misago from \"misago/index\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport * as validators from \"misago/utils/validators\"\nimport showBannedPage from \"misago/utils/banned-page\"\n\nexport class RequestLinkForm extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n email: \"\",\n\n validators: {\n email: [validators.email()],\n },\n }\n }\n\n clean() {\n if (this.isValid()) {\n return true\n } else {\n snackbar.error(\n pgettext(\"request activation link form\", \"Enter a valid email address.\")\n )\n return false\n }\n }\n\n send() {\n return ajax.post(misago.get(\"SEND_ACTIVATION_API\"), {\n email: this.state.email,\n })\n }\n\n handleSuccess(apiResponse) {\n this.props.callback(apiResponse)\n }\n\n handleError(rejection) {\n if ([\"already_active\", \"inactive_admin\"].indexOf(rejection.code) > -1) {\n snackbar.info(rejection.detail)\n } else if (rejection.status === 403 && rejection.ban) {\n showBannedPage(rejection.ban)\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n \n
    \n
    \n\n \n {pgettext(\"request activation link form btn\", \"Send link\")}\n \n \n
    \n )\n }\n}\n\nexport class LinkSent extends React.Component {\n getMessage() {\n return interpolate(\n pgettext(\n \"request activation link form\",\n \"Activation link was sent to %(email)s\"\n ),\n {\n email: this.props.user.email,\n },\n true\n )\n }\n\n render() {\n return (\n
    \n
    \n
    \n check\n
    \n
    \n

    {this.getMessage()}

    \n
    \n \n {pgettext(\n \"request activation link form btn\",\n \"Request another link\"\n )}\n \n
    \n
    \n )\n }\n}\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n complete: false,\n }\n }\n\n complete = (apiResponse) => {\n this.setState({\n complete: apiResponse,\n })\n }\n\n reset = () => {\n this.setState({\n complete: false,\n })\n }\n\n render() {\n if (this.state.complete) {\n return \n } else {\n return \n }\n }\n}\n","import misago from \"misago/index\"\nimport RequestActivationLink from \"misago/components/request-activation-link\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer() {\n if (document.getElementById(\"request-activation-link-mount\")) {\n mount(RequestActivationLink, \"request-activation-link-mount\", false)\n }\n}\n\nmisago.addInitializer({\n name: \"component:request-activation-link\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport misago from \"misago/index\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport * as validators from \"misago/utils/validators\"\nimport showBannedPage from \"misago/utils/banned-page\"\n\nexport class RequestResetForm extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n email: \"\",\n\n validators: {\n email: [validators.email()],\n },\n }\n }\n\n clean() {\n if (this.isValid()) {\n return true\n } else {\n snackbar.error(\n pgettext(\"request password reset form\", \"Enter a valid email address.\")\n )\n return false\n }\n }\n\n send() {\n return ajax.post(misago.get(\"SEND_PASSWORD_RESET_API\"), {\n email: this.state.email,\n })\n }\n\n handleSuccess(apiResponse) {\n this.props.callback(apiResponse)\n }\n\n handleError(rejection) {\n if ([\"inactive_user\", \"inactive_admin\"].indexOf(rejection.code) > -1) {\n this.props.showInactivePage(rejection)\n } else if (rejection.status === 403 && rejection.ban) {\n showBannedPage(rejection.ban)\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n \n
    \n
    \n\n \n {pgettext(\"request password reset form btn\", \"Send link\")}\n \n \n
    \n )\n }\n}\n\nexport class LinkSent extends React.Component {\n getMessage() {\n return interpolate(\n pgettext(\n \"request password reset form\",\n \"Reset password link was sent to %(email)s\"\n ),\n {\n email: this.props.user.email,\n },\n true\n )\n }\n\n render() {\n return (\n
    \n
    \n
    \n check\n
    \n
    \n

    {this.getMessage()}

    \n
    \n \n {pgettext(\n \"request password reset form btn\",\n \"Request another link\"\n )}\n \n
    \n
    \n )\n }\n}\n\nexport class AccountInactivePage extends React.Component {\n getActivateButton() {\n if (this.props.activation === \"inactive_user\") {\n return (\n

    \n \n {pgettext(\n \"request password reset form error\",\n \"Activate your account.\"\n )}\n \n

    \n )\n } else {\n return null\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n info_outline\n
    \n\n
    \n

    \n {pgettext(\n \"request password reset form error\",\n \"Your account is inactive.\"\n )}\n

    \n

    {this.props.message}

    \n {this.getActivateButton()}\n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n complete: false,\n }\n }\n\n complete = (apiResponse) => {\n this.setState({\n complete: apiResponse,\n })\n }\n\n reset = () => {\n this.setState({\n complete: false,\n })\n }\n\n showInactivePage(apiResponse) {\n ReactDOM.render(\n ,\n document.getElementById(\"page-mount\")\n )\n }\n\n render() {\n if (this.state.complete) {\n return \n }\n\n return (\n \n )\n }\n}\n","import misago from \"misago/index\"\nimport RequestPasswordReset from \"misago/components/request-password-reset\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer() {\n if (document.getElementById(\"request-password-reset-mount\")) {\n mount(RequestPasswordReset, \"request-password-reset-mount\", false)\n }\n}\n\nmisago.addInitializer({\n name: \"component:request-password-reset\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport misago from \"misago/index\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport SignInModal from \"misago/components/sign-in.js\"\nimport ajax from \"misago/services/ajax\"\nimport auth from \"misago/services/auth\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport showBannedPage from \"misago/utils/banned-page\"\n\nexport class ResetPasswordForm extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n password: \"\",\n }\n }\n\n clean() {\n if (this.state.password.trim().length) {\n return true\n } else {\n snackbar.error(pgettext(\"password reset form\", \"Enter new password.\"))\n return false\n }\n }\n\n send() {\n return ajax.post(misago.get(\"CHANGE_PASSWORD_API\"), {\n password: this.state.password,\n })\n }\n\n handleSuccess(apiResponse) {\n this.props.callback(apiResponse)\n }\n\n handleError(rejection) {\n if (rejection.status === 403 && rejection.ban) {\n showBannedPage(rejection.ban)\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n \n
    \n
    \n\n \n {pgettext(\"password reset form btn\", \"Change password\")}\n \n \n
    \n )\n }\n}\n\nexport class PasswordChangedPage extends React.Component {\n getMessage() {\n return interpolate(\n pgettext(\n \"password reset form\",\n \"%(username)s, your password has been changed successfully.\"\n ),\n {\n username: this.props.user.username,\n },\n true\n )\n }\n\n showSignIn() {\n modal.show(SignInModal)\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n check\n
    \n\n
    \n

    {this.getMessage()}

    \n

    \n {pgettext(\n \"password reset form\",\n \"You will have to sign in using new password before continuing.\"\n )}\n

    \n

    \n \n {pgettext(\"password reset form btn\", \"Sign in\")}\n \n

    \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport default class extends React.Component {\n complete = (apiResponse) => {\n auth.softSignOut()\n\n // nuke \"redirect_to\" field so we don't end\n // coming back to error page after sign in\n $('#hidden-login-form input[name=\"redirect_to\"]').remove()\n\n ReactDOM.render(\n ,\n document.getElementById(\"page-mount\")\n )\n }\n\n render() {\n return \n }\n}\n","import misago from \"misago\"\nimport ResetPasswordForm from \"misago/components/reset-password-form\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer() {\n if (document.getElementById(\"reset-password-form-mount\")) {\n mount(ResetPasswordForm, \"reset-password-form-mount\", false)\n }\n}\n\nmisago.addInitializer({\n name: \"component:reset-password-form\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport { Provider } from \"react-redux\"\nimport { SearchOverlay } from \"../../components/Search\"\nimport store from \"../../services/store\"\n\nexport default function initializer(context) {\n const root = document.getElementById(\"search-mount\")\n ReactDOM.render(\n \n \n ,\n root\n )\n}\n\nmisago.addInitializer({\n name: \"component:search-overlay\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport misago from \"misago\"\nimport Form from \"misago/components/form\"\nimport { load as updatePosts } from \"misago/reducers/posts\"\nimport { update as updateSearch } from \"misago/reducers/search\"\nimport { hydrate as updateUsers } from \"misago/reducers/users\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\nimport { FlexRow, FlexRowCol, FlexRowSection } from \"../FlexRow\"\nimport {\n PageHeader,\n PageHeaderContainer,\n PageHeaderBanner,\n PageHeaderDetails,\n} from \"../PageHeader\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n query: props.search.query,\n }\n }\n\n componentDidMount() {\n if (this.state.query.length) {\n this.handleSubmit()\n }\n }\n\n onQueryChange = (event) => {\n this.changeValue(\"query\", event.target.value)\n }\n\n clean() {\n if (!this.state.query.trim().length) {\n snackbar.error(pgettext(\"search form\", \"You have to enter search query.\"))\n return false\n }\n\n return true\n }\n\n send() {\n store.dispatch(\n updateSearch({\n isLoading: true,\n })\n )\n\n const query = this.state.query.trim()\n\n let url = window.location.href\n const urlQuery = url.indexOf(\"?q=\")\n if (urlQuery > 0) {\n url = url.substring(0, urlQuery + 3)\n }\n window.history.pushState({}, \"\", url + encodeURIComponent(query))\n\n return ajax.get(misago.get(\"SEARCH_API\"), { q: query })\n }\n\n handleSuccess(providers) {\n store.dispatch(\n updateSearch({\n query: this.state.query.trim(),\n isLoading: false,\n providers,\n })\n )\n\n providers.forEach((provider) => {\n if (provider.id === \"users\") {\n store.dispatch(updateUsers(provider.results.results))\n } else if (provider.id === \"threads\") {\n store.dispatch(updatePosts(provider.results))\n }\n })\n }\n\n handleError(rejection) {\n snackbar.apiError(rejection)\n\n store.dispatch(\n updateSearch({\n isLoading: false,\n })\n )\n }\n\n render() {\n return (\n
    \n \n \n \n

    {pgettext(\"search form title\", \"Search\")}

    \n
    \n \n \n \n \n \n \n \n \n search\n \n \n \n \n \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport { Link } from \"react-router\"\n\nexport default function (props) {\n return (\n
    \n {props.providers.map((provider) => {\n return (\n \n {provider.icon}\n {provider.name}\n \n \n )\n })}\n
    \n )\n}\n\nexport function Badge(props) {\n if (!props.results) return null\n\n let count = props.results.count\n if (count > 1000000) {\n count = Math.ceil(count / 1000000) + \"KK\"\n } else if (count > 1000) {\n count = Math.ceil(count / 1000) + \"K\"\n }\n\n return {count}\n}\n","import React from \"react\"\nimport PageContainer from \"../PageContainer\"\nimport SearchForm from \"./form\"\nimport SideNav from \"./sidenav\"\n\nexport default function (props) {\n return (\n
    \n \n \n
    \n
    \n \n
    \n
    \n {props.children}\n \n
    \n
    \n
    \n
    \n )\n}\n\nexport function SearchTime(props) {\n let time = null\n props.search.providers.forEach((p) => {\n if (p.id === props.provider.id) {\n time = p.time\n }\n })\n\n if (time === null) return null\n\n const copy = pgettext(\"search time\", \"Search took %(time)s s to complete\")\n\n return (\n
    \n

    {interpolate(copy, { time }, true)}

    \n
    \n )\n}\n","import React from \"react\"\nimport PostFeed from \"misago/components/post-feed\"\nimport Button from \"misago/components/button\"\nimport MisagoMarkup from \"misago/components/misago-markup\"\nimport {\n update as updatePosts,\n append as appendPosts,\n} from \"misago/reducers/posts\"\nimport { updateProvider } from \"misago/reducers/search\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default function (props) {\n return (\n
    \n \n \n
    \n )\n}\n\nexport class LoadMore extends React.Component {\n onClick = () => {\n store.dispatch(\n updatePosts({\n isBusy: true,\n })\n )\n\n ajax\n .get(this.props.provider.api, {\n q: this.props.query,\n page: this.props.next,\n })\n .then(\n (providers) => {\n providers.forEach((provider) => {\n if (provider.id !== \"threads\") return\n store.dispatch(appendPosts(provider.results))\n store.dispatch(updateProvider(provider))\n })\n\n store.dispatch(\n updatePosts({\n isBusy: false,\n })\n )\n },\n (rejection) => {\n snackbar.apiError(rejection)\n\n store.dispatch(\n updatePosts({\n isBusy: false,\n })\n )\n }\n )\n }\n\n render() {\n if (!this.props.more) return null\n\n return (\n
    \n \n {pgettext(\"search threads btn\", \"Show more\")}\n \n
    \n )\n }\n}\n","import React from \"react\"\nimport SearchPage from \"../page\"\nimport Results from \"./results\"\n\nexport default function (props) {\n return (\n \n \n \n
    \n \n )\n}\n\nexport function Blankslate({ children, loading, posts, query }) {\n if (posts && posts.count) return children\n\n if (query.length) {\n return (\n

    \n {loading\n ? pgettext(\"search threads\", \"Loading results...\")\n : pgettext(\n \"search threads\",\n \"No threads matching search query have been found.\"\n )}\n

    \n )\n }\n\n return (\n

    \n {pgettext(\n \"search threads\",\n \"Enter at least two characters to search threads.\"\n )}\n

    \n )\n}\n","import React from \"react\"\nimport SearchPage from \"../page\"\nimport UsersList from \"misago/components/users-list\"\n\nexport default function (props) {\n return (\n \n \n \n \n \n )\n}\n\nexport function Blankslate({ children, loading, query, users }) {\n if (users.length) return children\n\n if (query.length) {\n return (\n

    \n {loading\n ? pgettext(\"search users\", \"Loading results...\")\n : pgettext(\n \"search users\",\n \"No users matching search query have been found.\"\n )}\n

    \n )\n }\n\n return (\n

    \n {pgettext(\n \"search users\",\n \"Enter at least two characters to search users.\"\n )}\n

    \n )\n}\n","import { connect } from \"react-redux\"\nimport SearchThreads from \"./threads\"\nimport SearchUsers from \"./users\"\n\nconst components = {\n threads: SearchThreads,\n users: SearchUsers,\n}\n\nexport function select(store) {\n return {\n posts: store.posts,\n search: store.search,\n tick: store.tick.tick,\n user: store.auth.user,\n users: store.users,\n }\n}\n\nexport default function (providers) {\n return providers.map((provider) => {\n return {\n path: provider.url,\n component: connect(select)(components[provider.id]),\n provider: provider,\n }\n })\n}\n","import paths from \"misago/components/search-route\"\nimport misago from \"misago\"\nimport mount from \"misago/utils/routed-component\"\n\nexport default function initializer(context) {\n if (context.get(\"CURRENT_LINK\") === \"misago:search\") {\n mount({\n paths: paths(misago.get(\"SEARCH_PROVIDERS\")),\n })\n }\n}\n\nmisago.addInitializer({\n name: \"component:search\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport { Provider } from \"react-redux\"\nimport { SiteNavOverlay } from \"../../components/SiteNav\"\nimport store from \"../../services/store\"\n\nexport default function initializer(context) {\n const root = document.getElementById(\"site-nav-mount\")\n ReactDOM.render(\n \n \n ,\n root\n )\n}\n\nmisago.addInitializer({\n name: \"component:site-nav-overlay\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\n\nconst TYPES_CLASSES = {\n info: \"alert-info\",\n success: \"alert-success\",\n warning: \"alert-warning\",\n error: \"alert-danger\",\n}\n\nexport class Snackbar extends React.Component {\n getSnackbarClass() {\n let snackbarClass = \"alerts-snackbar\"\n if (this.props.isVisible) {\n snackbarClass += \" in\"\n } else {\n snackbarClass += \" out\"\n }\n return snackbarClass\n }\n\n render() {\n return (\n
    \n

    \n {this.props.message}\n

    \n
    \n )\n }\n}\n\nexport function select(state) {\n return state.snackbar\n}\n","import { connect } from \"react-redux\"\nimport misago from \"misago/index\"\nimport { Snackbar, select } from \"misago/components/snackbar\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer() {\n mount(connect(select)(Snackbar), \"snackbar-mount\")\n}\n\nmisago.addInitializer({\n name: \"component:snackbar\",\n initializer: initializer,\n after: \"snackbar\",\n})\n","import React from \"react\"\nimport {\n PageHeader,\n PageHeaderBanner,\n PageHeaderContainer,\n} from \"../PageHeader\"\n\nconst Header = ({ backendName }) => {\n const pageTitleTpl = pgettext(\"social auth title\", \"Sign in with %(backend)s\")\n const pageTitle = interpolate(pageTitleTpl, { backend: backendName }, true)\n\n return (\n \n \n \n

    {pageTitle}

    \n
    \n
    \n
    \n )\n}\n\nexport default Header\n","import React from \"react\"\nimport misago from \"misago\"\nimport RegisterLegalFootnote from \"misago/components/RegisterLegalFootnote\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport * as validators from \"misago/utils/validators\"\nimport PageContainer from \"../PageContainer\"\nimport Header from \"./header\"\n\nexport default class Register extends Form {\n constructor(props) {\n super(props)\n\n const formValidators = {\n email: [validators.email()],\n username: [validators.usernameContent()],\n }\n\n if (!!misago.get(\"TERMS_OF_SERVICE_ID\")) {\n formValidators.termsOfService = [validators.requiredTermsOfService()]\n }\n\n if (!!misago.get(\"PRIVACY_POLICY_ID\")) {\n formValidators.privacyPolicy = [validators.requiredPrivacyPolicy()]\n }\n\n this.state = {\n email: props.email || \"\",\n emailProtected: !!props.email,\n username: props.username || \"\",\n\n termsOfService: null,\n privacyPolicy: null,\n\n validators: formValidators,\n errors: {},\n\n isLoading: false,\n }\n }\n\n clean() {\n let errors = this.validate()\n let lengths = [\n this.state.email.trim().length,\n this.state.username.trim().length,\n ]\n\n if (lengths.indexOf(0) !== -1) {\n snackbar.error(pgettext(\"social auth form\", \"Fill out all fields.\"))\n return false\n }\n\n const { validators } = this.state\n\n const checkTermsOfService = !!misago.get(\"TERMS_OF_SERVICE_ID\")\n if (checkTermsOfService && this.state.termsOfService === null) {\n snackbar.error(validators.termsOfService[0](null))\n return false\n }\n\n const checkPrivacyPolicy = !!misago.get(\"PRIVACY_POLICY_ID\")\n if (checkPrivacyPolicy && this.state.privacyPolicy === null) {\n snackbar.error(validators.privacyPolicy[0](null))\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.post(this.props.url, {\n email: this.state.email,\n username: this.state.username,\n terms_of_service: this.state.termsOfService,\n privacy_policy: this.state.privacyPolicy,\n })\n }\n\n handleSuccess(response) {\n const { onRegistrationComplete } = this.props\n onRegistrationComplete(response)\n }\n\n handleError(rejection) {\n if (rejection.status === 200) {\n // We've entered \"errored\" state because response is HTML instead of exptected JSON\n const { onRegistrationComplete } = this.props\n const { username } = this.state\n onRegistrationComplete({ activation: \"active\", step: \"done\", username })\n } else if (rejection.status === 400) {\n const stateUpdate = { errors: rejection }\n if (rejection.email) {\n stateUpdate.emailProtected = false\n }\n this.setState(stateUpdate)\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n handlePrivacyPolicyChange = (event) => {\n const value = event.target.value\n this.handleToggleAgreement(\"privacyPolicy\", value)\n }\n\n handleTermsOfServiceChange = (event) => {\n const value = event.target.value\n this.handleToggleAgreement(\"termsOfService\", value)\n }\n\n handleToggleAgreement = (agreement, value) => {\n this.setState((prevState, props) => {\n if (prevState[agreement] === null) {\n const errors = { ...prevState.errors, [agreement]: null }\n return { errors, [agreement]: value }\n }\n\n const validator = this.state.validators[agreement][0]\n const errors = { ...prevState.errors, [agreement]: [validator(null)] }\n return { errors, [agreement]: null }\n })\n }\n\n render() {\n const { backend_name } = this.props\n const { email, emailProtected, username, isLoading } = this.state\n\n let emailHelpText = null\n if (emailProtected) {\n const emailHelpTextTpl = pgettext(\n \"social auth form\",\n \"Your e-mail address has been verified by %(backend)s.\"\n )\n emailHelpText = interpolate(\n emailHelpTextTpl,\n { backend: backend_name },\n true\n )\n }\n\n return (\n
    \n
    \n \n
    \n
    \n
    \n
    \n
    \n

    \n {pgettext(\n \"social auth form title\",\n \"Complete your details\"\n )}\n

    \n
    \n
    \n \n \n \n \n \n \n \n
    \n
    \n \n {pgettext(\"social auth form btn\", \"Sign in\")}\n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport misago from \"misago\"\nimport PageContainer from \"../PageContainer\"\nimport Header from \"./header\"\n\nconst Complete = ({ activation, backend_name, username }) => {\n let icon = \"\"\n let message = \"\"\n if (activation === \"user\") {\n message = pgettext(\n \"social auth complete\",\n \"%(username)s, your account has been created but you need to activate it before you will be able to sign in.\"\n )\n } else if (activation === \"admin\") {\n message = pgettext(\n \"social auth complete\",\n \"%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in.\"\n )\n } else {\n message = pgettext(\n \"social auth complete\",\n \"%(username)s, your account has been created and you have been signed in to it.\"\n )\n }\n\n if (activation === \"active\") {\n icon = \"check\"\n } else {\n icon = \"info_outline\"\n }\n\n return (\n
    \n
    \n \n
    \n
    \n
    \n
    \n

    \n {pgettext(\n \"social auth complete title\",\n \"Registration completed!\"\n )}\n

    \n
    \n
    \n
    \n {icon}\n
    \n
    \n

    \n {interpolate(message, { username }, true)}\n

    \n

    \n \n {pgettext(\n \"social auth complete link\",\n \"Return to forum index\"\n )}\n \n

    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n )\n}\n\nexport default Complete\n","import React from \"react\"\nimport Register from \"./register\"\nimport Complete from \"./complete\"\n\nexport default class SocialAuth extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n step: props.step,\n\n activation: props.activation || \"\",\n email: props.email || \"\",\n username: props.username || \"\",\n }\n }\n\n handleRegistrationComplete = ({ activation, email, step, username }) => {\n this.setState({ activation, email, step, username })\n }\n\n render() {\n const { backend_name, url } = this.props\n const { activation, email, step, username } = this.state\n\n if (step === \"register\") {\n return (\n \n )\n }\n\n return (\n \n )\n }\n}\n","import React from \"react\"\nimport SocialAuth from \"misago/components/social-auth\"\nimport misago from \"misago\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer(context) {\n if (context.get(\"CURRENT_LINK\") === \"misago:social-complete\") {\n const props = context.get(\"SOCIAL_AUTH_FORM\")\n mount(, \"page-mount\")\n }\n}\n\nmisago.addInitializer({\n name: \"component:social-auth\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport Form from \"./form\"\nimport FormGroup from \"misago/components/form-group\"\nimport * as participants from \"misago/reducers/participants\"\nimport { updateAcl } from \"misago/reducers/thread\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n username: \"\",\n }\n }\n\n onUsernameChange = (event) => {\n this.changeValue(\"username\", event.target.value)\n }\n\n clean() {\n if (!this.state.username.trim().length) {\n snackbar.error(\n pgettext(\n \"add private thread participant\",\n \"You have to enter user name.\"\n )\n )\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.patch(this.props.thread.api.index, [\n { op: \"add\", path: \"participants\", value: this.state.username },\n { op: \"add\", path: \"acl\", value: 1 },\n ])\n }\n\n handleSuccess(data) {\n store.dispatch(updateAcl(data))\n store.dispatch(participants.replace(data.participants))\n\n snackbar.success(\n pgettext(\n \"add private thread participant\",\n \"New participant has been added to thread.\"\n )\n )\n\n modal.hide()\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n
    \n \n \n \n
    \n
    \n \n {pgettext(\n \"add private thread participant btn\",\n \"Add participant\"\n )}\n \n \n {pgettext(\"add private thread participant btn\", \"Cancel\")}\n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport function ModalHeader(props) {\n return (\n
    \n \n ×\n \n

    \n {pgettext(\n \"add private thread participant modal title\",\n \"Add participant\"\n )}\n

    \n
    \n )\n}\n","import React from \"react\"\nimport AddParticipantModal from \"misago/components/add-participant\"\nimport modal from \"misago/services/modal\"\n\nexport default class extends React.Component {\n onClick = () => {\n modal.show()\n }\n\n render() {\n if (!this.props.thread.acl.can_add_participants) return null\n\n return (\n
    \n \n person_add\n {pgettext(\"add participant btn\", \"Add participant\")}\n \n
    \n )\n }\n}\n","import React from \"react\"\nimport { changeOwner } from \"./actions\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.isUser = props.participant.id === props.user.id\n }\n\n onClick = () => {\n let confirmed = false\n if (this.isUser) {\n confirmed = window.confirm(\n pgettext(\n \"private thread owner change\",\n \"Are you sure you want to take over this thread?\"\n )\n )\n } else {\n const message = pgettext(\n \"private thread owner change\",\n \"Are you sure you want to change thread owner to %(user)s?\"\n )\n confirmed = window.confirm(\n interpolate(\n message,\n {\n user: this.props.participant.username,\n },\n true\n )\n )\n }\n\n if (!confirmed) return\n\n changeOwner(this.props.thread, this.props.participant)\n }\n\n render() {\n if (this.props.participant.is_owner) return null\n if (!this.props.thread.acl.can_change_owner) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n","import * as participants from \"misago/reducers/participants\"\nimport { updateAcl } from \"misago/reducers/thread\"\nimport misago from \"misago\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport function leave(thread, participant) {\n ajax\n .patch(thread.api.index, [\n { op: \"remove\", path: \"participants\", value: participant.id },\n ])\n .then(\n () => {\n snackbar.success(\n pgettext(\"thread participants actions\", \"You have left this thread.\")\n )\n window.setTimeout(() => {\n window.location = misago.get(\"PRIVATE_THREADS_URL\")\n }, 3 * 1000)\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n}\n\nexport function remove(thread, participant) {\n ajax\n .patch(thread.api.index, [\n { op: \"remove\", path: \"participants\", value: participant.id },\n { op: \"add\", path: \"acl\", value: 1 },\n ])\n .then(\n (data) => {\n store.dispatch(updateAcl(data))\n store.dispatch(participants.replace(data.participants))\n\n const message = pgettext(\n \"thread participants actions\",\n \"%(user)s has been removed from this thread.\"\n )\n snackbar.success(\n interpolate(\n message,\n {\n user: participant.username,\n },\n true\n )\n )\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n}\n\nexport function changeOwner(thread, participant) {\n ajax\n .patch(thread.api.index, [\n { op: \"replace\", path: \"owner\", value: participant.id },\n { op: \"add\", path: \"acl\", value: 1 },\n ])\n .then(\n (data) => {\n store.dispatch(updateAcl(data))\n store.dispatch(participants.replace(data.participants))\n\n const message = pgettext(\n \"thread participants actions\",\n \"%(user)s has been made new thread owner.\"\n )\n snackbar.success(\n interpolate(\n message,\n {\n user: participant.username,\n },\n true\n )\n )\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n}\n","import React from \"react\"\nimport { remove, leave } from \"./actions\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.isUser = props.participant.id === props.user.id\n }\n\n onClick = () => {\n let confirmed = false\n if (this.isUser) {\n confirmed = window.confirm(\n pgettext(\n \"private thread leave\",\n \"Are you sure you want to leave this thread?\"\n )\n )\n } else {\n const message = pgettext(\n \"private thread leave\",\n \"Are you sure you want to remove %(user)s from this thread?\"\n )\n confirmed = window.confirm(\n interpolate(\n message,\n {\n user: this.props.participant.username,\n },\n true\n )\n )\n }\n\n if (!confirmed) return\n\n if (this.isUser) {\n leave(this.props.thread, this.props.participant)\n } else {\n remove(this.props.thread, this.props.participant)\n }\n }\n\n render() {\n const isModerator = this.props.user.acl.can_moderate_private_threads\n\n if (!(this.props.userIsOwner || this.isUser || isModerator)) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n","import React from \"react\"\nimport MakeOwner from \"./make-owner\"\nimport Remove from \"./remove\"\nimport Avatar from \"misago/components/avatar\"\n\nexport default function (props) {\n const participant = props.participant\n\n let className = \"btn btn-default\"\n if (participant.is_owner) {\n className = \"btn btn-primary\"\n }\n className += \" btn-user btn-block\"\n\n return (\n
    \n
    \n \n \n {participant.username}\n \n \n
    \n
    \n )\n}\n\nexport function UserStatus({ isOwner }) {\n if (!isOwner) return null\n\n return (\n
  • \n start\n \n {pgettext(\"thread participants owner status\", \"Thread owner\")}\n \n
  • \n )\n}\n","import React from \"react\"\nimport Card from \"./card\"\n\nexport default function ({ participants, thread, user, userIsOwner }) {\n return (\n
    \n
    \n {participants.map((participant) => {\n return (\n \n )\n })}\n
    \n
    \n )\n}\n","import React from \"react\"\nimport AddParticipant from \"./add-participant\"\nimport CardsList from \"./cards-list\"\nimport * as utils from \"./utils\"\n\nexport default function (props) {\n if (!props.participants.length) return null\n\n return (\n
    \n
    \n \n
    \n \n
    \n

    {utils.getParticipantsCopy(props.participants)}

    \n
    \n
    \n
    \n
    \n )\n}\n\nexport function getUserIsOwner(user, participants) {\n return participants[0].id === user.id\n}\n","export function getParticipantsCopy(participants) {\n const count = participants.length\n const message = npgettext(\n \"thread participants stat\",\n \"This thread has %(users)s participant.\",\n \"This thread has %(users)s participants.\",\n count\n )\n\n return interpolate(\n message,\n {\n users: count,\n },\n true\n )\n}\n","import React from \"react\"\n\nexport default function (props) {\n return (\n
    \n {props.poll.choices.map((choice) => {\n return (\n \n )\n })}\n
    \n )\n}\n\nexport function PollChoice(props) {\n let proc = 0\n if (props.choice.votes && props.poll.votes) {\n proc = Math.ceil((props.choice.votes * 100) / props.poll.votes)\n }\n\n return (\n
    \n
    {props.choice.label}
    \n
    \n
    \n \n \n {getVotesLabel(props.votes, props.proc)}\n \n
    \n \n
      \n \n \n
    \n
    \n
    \n )\n}\n\nexport function ChoiceVotes(props) {\n return (\n
  • \n {getVotesLabel(props.votes, props.proc)}\n
  • \n )\n}\n\nexport function getVotesLabel(votes, proc) {\n const message = npgettext(\n \"thread poll\",\n \"%(votes)s vote, %(proc)s% of total.\",\n \"%(votes)s votes, %(proc)s% of total.\",\n votes\n )\n\n return interpolate(\n message,\n {\n votes: votes,\n proc: proc,\n },\n true\n )\n}\n\nexport function UserChoice(props) {\n if (!props.selected) return null\n\n return (\n
  • \n check_box\n {pgettext(\"thread poll\", \"You've voted on this choice.\")}\n
  • \n )\n}\n","import React from \"react\"\nimport moment from \"moment\"\nimport Message from \"misago/components/modal-message\"\nimport Loader from \"misago/components/modal-loader\"\nimport ajax from \"misago/services/ajax\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: true,\n error: null,\n data: [],\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.poll.api.votes).then(\n (data) => {\n const hydratedData = data.map((choice) => {\n return Object.assign({}, choice, {\n voters: choice.voters.map((voter) => {\n return Object.assign({}, voter, {\n voted_on: moment(voter.voted_on),\n })\n }),\n })\n })\n\n this.setState({\n isLoading: false,\n data: hydratedData,\n })\n },\n (rejection) => {\n this.setState({\n isLoading: false,\n error: rejection.detail,\n })\n }\n )\n }\n\n render() {\n return (\n \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"thread poll\", \"Poll votes\")}\n

    \n
    \n\n \n
    \n \n )\n }\n}\n\nexport function ModalBody(props) {\n if (props.isLoading) {\n return \n } else if (props.error) {\n return \n }\n\n return \n}\n\nexport function ChoicesList(props) {\n return (\n
    \n
      \n {props.data.map((choice) => {\n return \n })}\n
    \n
    \n )\n}\n\nexport function ChoiceDetails(props) {\n return (\n
  • \n

    {props.label}

    \n \n \n
    \n
  • \n )\n}\n\nexport function VotesCount(props) {\n const message = npgettext(\n \"thread poll\",\n \"%(votes)s user has voted for this choice.\",\n \"%(votes)s users have voted for this choice.\",\n props.votes\n )\n\n const label = interpolate(\n message,\n {\n votes: props.votes,\n },\n true\n )\n\n return

    {label}

    \n}\n\nexport function VotesList(props) {\n if (!props.voters.length) return null\n\n return (\n
      \n {props.voters.map((user) => {\n return \n })}\n
    \n )\n}\n\nexport function Voter(props) {\n if (props.url) {\n return (\n
  • \n \n {props.username}\n {\" \"}\n \n
  • \n )\n }\n\n return (\n
  • \n {props.username} \n
  • \n )\n}\n\nexport function VoteDate(props) {\n return (\n \n {props.voted_on.fromNow()}\n \n )\n}\n","import React from \"react\"\nimport Modal from \"./modal\"\nimport * as poll from \"misago/reducers/poll\"\nimport * as thread from \"misago/reducers/thread\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\nimport posting from \"misago/services/posting\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default function (props) {\n const { isPollOver, poll, showVoting, thread } = props\n\n if (!isVisible(isPollOver, poll.acl, poll)) return null\n\n const controls = []\n\n const canVote = poll.acl.can_vote\n const canChangeVote = !poll.hasSelectedChoices || poll.allow_revotes\n\n if (canVote && canChangeVote) controls.push(0)\n if (poll.is_public || poll.acl.can_see_votes) controls.push(1)\n if (poll.acl.can_edit) controls.push(2)\n if (poll.acl.can_delete) controls.push(3)\n\n return (\n
    \n \n \n \n \n
    \n )\n}\n\nexport function isVisible(isPollOver, acl, poll) {\n return (\n poll.is_public ||\n acl.can_delete ||\n acl.can_edit ||\n acl.can_see_votes ||\n (acl.can_vote &&\n !isPollOver &&\n (!poll.hasSelectedChoices || poll.allow_revotes))\n )\n}\n\nexport function getClassName(controls, control) {\n let className = \"col-xs-6\"\n\n if (controls.length === 1) {\n className = \"col-xs-12\"\n }\n\n if (controls.length === 3 && controls[0] === control) {\n className = \"col-xs-12\"\n }\n\n return className + \" col-sm-3 col-md-2\"\n}\n\nexport function ChangeVote(props) {\n const canVote = props.poll.acl.can_vote\n const canChangeVote =\n !props.poll.hasSelectedChoices || props.poll.allow_revotes\n\n if (!(canVote && canChangeVote)) return null\n\n return (\n
    \n \n {pgettext(\"thread poll\", \"Vote\")}\n \n
    \n )\n}\n\nexport class SeeVotes extends React.Component {\n onClick = () => {\n modal.show()\n }\n\n render() {\n const seeVotes =\n this.props.poll.is_public || this.props.poll.acl.can_see_votes\n if (!seeVotes) return null\n\n return (\n
    \n \n {pgettext(\"thread poll\", \"See votes\")}\n \n
    \n )\n }\n}\n\nexport function Edit(props) {\n if (!props.poll.acl.can_edit) return null\n\n return (\n
    \n \n {pgettext(\"thread poll\", \"Edit\")}\n \n
    \n )\n}\n\nexport class Delete extends React.Component {\n onClick = () => {\n const deletePoll = window.confirm(\n pgettext(\n \"thread poll\",\n \"Are you sure you want to delete this poll? This action is not reversible.\"\n )\n )\n if (!deletePoll) return false\n\n store.dispatch(poll.busy())\n\n ajax\n .delete(this.props.poll.api.index)\n .then(this.handleSuccess, this.handleError)\n }\n\n handleSuccess = (newThreadAcl) => {\n snackbar.success(pgettext(\"thread poll\", \"Poll has been deleted\"))\n store.dispatch(poll.remove())\n store.dispatch(thread.updateAcl(newThreadAcl))\n }\n\n handleError = (rejection) => {\n snackbar.apiError(rejection)\n store.dispatch(poll.release())\n }\n\n render() {\n if (!this.props.poll.acl.can_delete) return null\n\n return (\n
    \n \n {pgettext(\"thread poll\", \"Delete\")}\n \n
    \n )\n }\n}\n","import React from \"react\"\nimport escapeHtml from \"misago/utils/escape-html\"\n\nconst DATE_ABBR = '%(relative)s'\nconst USER_SPAN = '%(user)s'\nconst USER_URL = '%(user)s'\n\nexport default function (props) {\n return (\n
      \n \n \n \n \n
    \n )\n}\n\nexport function PollCreation(props) {\n const message = interpolate(\n escapeHtml(pgettext(\"thread poll\", \"Started by %(poster)s %(posted_on)s.\")),\n {\n poster: getPoster(props.poll),\n posted_on: getPostedOn(props.poll),\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function getPoster(poll) {\n if (poll.url.poster) {\n return interpolate(\n USER_URL,\n {\n url: escapeHtml(poll.url.poster),\n user: escapeHtml(poll.poster_name),\n },\n true\n )\n }\n\n return interpolate(\n USER_SPAN,\n {\n user: escapeHtml(poll.poster_name),\n },\n true\n )\n}\n\nexport function getPostedOn(poll) {\n return interpolate(\n DATE_ABBR,\n {\n absolute: escapeHtml(poll.posted_on.format(\"LLL\")),\n relative: escapeHtml(poll.posted_on.fromNow()),\n },\n true\n )\n}\n\nexport function PollLength(props) {\n if (!props.poll.length) {\n return null\n }\n\n const message = interpolate(\n escapeHtml(pgettext(\"thread poll\", \"Voting ends %(ends_on)s.\")),\n {\n ends_on: getEndsOn(props.poll),\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function getEndsOn(poll) {\n return interpolate(\n DATE_ABBR,\n {\n absolute: escapeHtml(poll.endsOn.format(\"LLL\")),\n relative: escapeHtml(poll.endsOn.fromNow()),\n },\n true\n )\n}\n\nexport function PollVotes(props) {\n const message = npgettext(\n \"thread poll\",\n \"%(votes)s vote.\",\n \"%(votes)s votes.\",\n props.votes\n )\n const label = interpolate(\n message,\n {\n votes: props.votes,\n },\n true\n )\n\n return
  • {label}
  • \n}\n\nexport function PollIsPublic(props) {\n if (!props.poll.is_public) {\n return null\n }\n\n return (\n
  • \n {pgettext(\"thread poll\", \"Voting is public.\")}\n
  • \n )\n}\n","import React from \"react\"\nimport Chart from \"./chart\"\nimport Options from \"./options\"\nimport PollInfo from \"../info\"\n\nexport default function (props) {\n return (\n
    \n
    \n

    {props.poll.question}

    \n \n \n \n
    \n
    \n )\n}\n","import React from \"react\"\nimport escapeHtml from \"misago/utils/escape-html\"\n\nconst DATE_ABBR = '%(relative)s'\nconst USER_SPAN = '%(user)s'\nconst USER_URL = '%(user)s'\n\nexport default function (props) {\n return (\n
      \n \n \n
    \n )\n}\n\nexport function PollChoicesLeft({ choicesLeft }) {\n if (choicesLeft === 0) {\n return (\n
  • \n {pgettext(\"thread poll\", \"You can't select any more choices.\")}\n
  • \n )\n }\n\n const message = npgettext(\n \"thread poll\",\n \"You can select %(choices)s more choice.\",\n \"You can select %(choices)s more choices.\",\n choicesLeft\n )\n\n const label = interpolate(\n message,\n {\n choices: choicesLeft,\n },\n true\n )\n\n return
  • {label}
  • \n}\n\nexport function PollAllowRevote(props) {\n if (props.poll.allow_revotes) {\n return (\n
  • \n {pgettext(\"thread poll\", \"You can change your vote later.\")}\n
  • \n )\n }\n\n return (\n
  • \n {pgettext(\"thread poll\", \"Votes are final.\")}\n
  • \n )\n}\n","import React from \"react\"\n\nexport default function (props) {\n return (\n
      \n {props.choices.map((choice) => {\n return (\n \n )\n })}\n
    \n )\n}\n\nexport class ChoiceSelect extends React.Component {\n onClick = () => {\n this.props.toggleChoice(this.props.choice.hash)\n }\n\n render() {\n return (\n
  • \n \n \n {this.props.choice.selected\n ? \"check_box\"\n : \"check_box_outline_blank\"}\n \n {this.props.choice.label}\n \n
  • \n )\n }\n}\n","export function getChoiceFromHash(choices, hash) {\n for (const i in choices) {\n const choice = choices[i]\n if (choice.hash === hash) {\n return choice\n }\n }\n\n return null\n}\n\nexport function getChoicesLeft(poll, choices) {\n let selection = []\n for (const i in choices) {\n const choice = choices[i]\n if (choice.selected) {\n selection.push(choice)\n }\n }\n\n return poll.allowed_choices - selection.length\n}\n","import React from \"react\"\nimport ChoicesHelp from \"./help\"\nimport ChoicesSelect from \"./select\"\nimport { getChoicesLeft, getChoiceFromHash } from \"./utils\"\nimport PollInfo from \"../info\"\nimport { Delete, Edit, getClassName } from \"../results/options\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport * as poll from \"misago/reducers/poll\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n choices: props.poll.choices,\n choicesLeft: getChoicesLeft(props.poll, props.poll.choices),\n }\n }\n\n toggleChoice = (hash) => {\n const choice = getChoiceFromHash(this.state.choices, hash)\n\n let choices = null\n if (!choice.selected) {\n choices = this.selectChoice(choice, hash)\n } else {\n choices = this.deselectChoice(choice, hash)\n }\n\n this.setState({\n choices,\n choicesLeft: getChoicesLeft(this.props.poll, choices),\n })\n }\n\n selectChoice = (choice, hash) => {\n const choicesLeft = getChoicesLeft(this.props.poll, this.state.choices)\n\n if (!choicesLeft) {\n for (const i in this.state.choices.slice()) {\n const item = this.state.choices[i]\n if (item.selected && item.hash != hash) {\n item.selected = false\n break\n }\n }\n }\n\n return this.state.choices.map((choice) => {\n return Object.assign({}, choice, {\n selected: choice.hash == hash ? true : choice.selected,\n })\n })\n }\n\n deselectChoice = (choice, hash) => {\n return this.state.choices.map((choice) => {\n return Object.assign({}, choice, {\n selected: choice.hash == hash ? false : choice.selected,\n })\n })\n }\n\n clean() {\n if (this.state.choicesLeft === this.props.poll.allowed_choices) {\n snackbar.error(\n pgettext(\"thread poll vote\", \"You need to select at least one choice\")\n )\n return false\n }\n\n return true\n }\n\n send() {\n let data = []\n for (const i in this.state.choices.slice()) {\n const item = this.state.choices[i]\n if (item.selected) {\n data.push(item.hash)\n }\n }\n\n return ajax.post(this.props.poll.api.votes, data)\n }\n\n handleSuccess(data) {\n store.dispatch(poll.replace(data))\n snackbar.success(pgettext(\"thread poll vote\", \"Your vote has been saved.\"))\n\n this.props.showResults()\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n const controls = []\n\n if (this.props.poll.acl.can_vote) controls.push(0)\n if (this.props.poll.is_public || this.props.poll.acl.can_see_votes)\n controls.push(1)\n if (this.props.poll.acl.can_edit) controls.push(2)\n if (this.props.poll.acl.can_delete) controls.push(3)\n\n return (\n
    \n
    \n
    \n

    {this.props.poll.question}

    \n \n \n \n
    \n
    \n
    \n
    \n \n {pgettext(\"thread poll vote btn\", \"Save your vote\")}\n \n
    \n
    \n \n {pgettext(\"thread poll vote btn\", \"See results\")}\n \n
    \n \n \n
    \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport moment from \"moment\"\nimport Results from \"./results\"\nimport Voting from \"./voting\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n let showResults = true\n if (props.user.id && !props.poll.hasSelectedChoices) {\n showResults = false\n }\n\n this.state = {\n showResults,\n }\n }\n\n showResults = () => {\n this.setState({\n showResults: true,\n })\n }\n\n showVoting = () => {\n this.setState({\n showResults: false,\n })\n }\n\n render() {\n if (!this.props.thread.poll) return null\n\n const isPollOver = getIsPollOver(this.props.poll)\n\n if (\n !isPollOver &&\n this.props.poll.acl.can_vote &&\n !this.state.showResults\n ) {\n return \n } else {\n return (\n \n )\n }\n }\n}\n\nexport function getIsPollOver(poll) {\n if (poll.length) {\n return moment().isAfter(poll.endsOn)\n }\n return false\n}\n","import React from \"react\"\nimport getRandomString from \"../../../utils/getRandomString\"\n\nconst HASH_LENGTH = 12\n\nexport default class extends React.Component {\n onAdd = () => {\n let choices = this.props.choices.slice()\n choices.push({\n hash: getRandomString(HASH_LENGTH),\n label: \"\",\n })\n\n this.props.setChoices(choices)\n }\n\n onChange = (hash, label) => {\n const choices = this.props.choices.map((choice) => {\n if (choice.hash === hash) {\n choice.label = label\n }\n\n return choice\n })\n this.props.setChoices(choices)\n }\n\n onDelete = (hash) => {\n const choices = this.props.choices.filter((choice) => {\n return choice.hash !== hash\n })\n this.props.setChoices(choices)\n }\n\n render() {\n return (\n
    \n
      \n {this.props.choices.map((choice) => {\n return (\n 2}\n choice={choice}\n disabled={this.props.disabled}\n key={choice.hash}\n onChange={this.onChange}\n onDelete={this.onDelete}\n />\n )\n })}\n
    \n \n {pgettext(\"thread poll\", \"Add choice\")}\n \n
    \n )\n }\n}\n\nexport class PollChoice extends React.Component {\n onChange = (event) => {\n this.props.onChange(this.props.choice.hash, event.target.value)\n }\n\n onDelete = () => {\n const deleteItem =\n this.props.choice.label.length === 0\n ? true\n : window.confirm(\n pgettext(\n \"thread poll\",\n \"Are you sure you want to remove this choice?\"\n )\n )\n if (deleteItem) {\n this.props.onDelete(this.props.choice.hash)\n }\n }\n\n render() {\n return (\n
  • \n \n close\n \n \n
  • \n )\n }\n}\n","import React from \"react\"\nimport ChoicesControl from \"./choices-control\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport YesNoSwitch from \"misago/components/yes-no-switch\"\nimport * as poll from \"misago/reducers/poll\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n const poll = props.poll.id\n ? props.poll\n : {\n question: \"\",\n choices: [\n {\n hash: \"choice-10000\",\n label: \"\",\n },\n {\n hash: \"choice-20000\",\n label: \"\",\n },\n ],\n length: 0,\n allowed_choices: 1,\n allow_revotes: 0,\n is_public: 0,\n }\n\n this.state = {\n isLoading: false,\n isEdit: !!poll.id,\n\n question: poll.question,\n choices: poll.choices,\n length: poll.length,\n allowed_choices: poll.allowed_choices,\n allow_revotes: poll.allow_revotes,\n is_public: poll.is_public,\n\n validators: {\n question: [],\n choices: [],\n length: [],\n allowed_choices: [],\n },\n\n errors: {},\n }\n }\n\n setChoices = (choices) => {\n this.setState((state) => {\n return {\n choices,\n errors: Object.assign({}, state.errors, { choices: null }),\n }\n })\n }\n\n onCancel = () => {\n let cancel = false\n\n // Nothing added to the poll so no changes to discard\n const formEmpty = !!(\n this.state.question === \"\" &&\n this.state.choices &&\n this.state.choices.every((choice) => choice.label === \"\") &&\n this.state.length === 0 &&\n this.state.allowed_choices === 1\n )\n\n if (formEmpty) {\n return this.props.close()\n }\n\n if (!!this.props.poll) {\n cancel = window.confirm(\n pgettext(\"thread poll\", \"Are you sure you want to discard changes?\")\n )\n } else {\n cancel = window.confirm(\n pgettext(\"thread poll\", \"Are you sure you want to discard new poll?\")\n )\n }\n\n if (cancel) {\n this.props.close()\n }\n }\n\n send() {\n const data = {\n question: this.state.question,\n choices: this.state.choices,\n length: this.state.length,\n allowed_choices: this.state.allowed_choices,\n allow_revotes: this.state.allow_revotes,\n is_public: this.state.is_public,\n }\n\n if (this.state.isEdit) {\n return ajax.put(this.props.poll.api.index, data)\n }\n\n return ajax.post(this.props.thread.api.poll, data)\n }\n\n handleSuccess(data) {\n store.dispatch(poll.replace(data))\n\n if (this.state.isEdit) {\n snackbar.success(pgettext(\"thread poll\", \"Poll has been edited.\"))\n } else {\n snackbar.success(pgettext(\"thread poll\", \"Poll has been posted.\"))\n }\n\n this.props.close()\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n if (rejection.non_field_errors) {\n rejection.allowed_choices = rejection.non_field_errors\n }\n\n this.setState({\n errors: Object.assign({}, rejection),\n })\n\n snackbar.error(gettext(\"Form contains errors.\"))\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n

    \n {this.state.isEdit\n ? pgettext(\"thread poll\", \"Edit poll\")\n : pgettext(\"thread poll\", \"Add poll\")}\n

    \n
    \n
    \n
    \n \n {pgettext(\"thread poll\", \"Question and choices\")}\n \n\n \n \n \n\n \n \n \n
    \n\n
    \n {pgettext(\"thread poll\", \"Voting\")}\n\n
    \n
    \n \n \n \n
    \n
    \n \n \n \n
    \n
    \n\n
    \n \n
    \n \n \n \n
    \n
    \n
    \n
    \n
    \n \n {pgettext(\"thread poll\", \"Cancel\")}\n {\" \"}\n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport function PollPublicSwitch(props) {\n if (props.isEdit) return null\n\n return (\n
    \n \n \n \n
    \n )\n}\n","import React from \"react\"\n\nconst ICON = {\n changed_title: \"edit\",\n\n pinned_globally: \"bookmark\",\n pinned_locally: \"bookmark_border\",\n unpinned: \"panorama_fish_eye\",\n\n moved: \"arrow_forward\",\n merged: \"call_merge\",\n\n approved: \"done\",\n\n opened: \"lock_open\",\n closed: \"lock_outline\",\n\n unhid: \"visibility\",\n hid: \"visibility_off\",\n\n changed_owner: \"grade\",\n tookover: \"grade\",\n\n added_participant: \"person_add\",\n\n owner_left: \"person_outline\",\n participant_left: \"person_outline\",\n removed_participant: \"remove_circle_outline\",\n}\n\nconst EventIcon = (props) => (\n \n {ICON[props.post.event_type]}\n \n)\n\nexport default EventIcon\n","import React from \"react\"\nimport moment from \"moment\"\nimport * as post from \"misago/reducers/post\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default function (props) {\n if (isVisible(props.post.acl)) {\n return (\n
  • \n \n \n \n
  • \n )\n } else {\n return null\n }\n}\n\nexport function isVisible(acl) {\n return acl.can_hide\n}\n\nexport class Hide extends React.Component {\n onClick = () => {\n store.dispatch(\n post.patch(this.props.post, {\n is_hidden: true,\n hidden_on: moment(),\n hidden_by_name: this.props.user.username,\n url: Object.assign(this.props.post.url, {\n hidden_by: this.props.user.url,\n }),\n })\n )\n\n const op = { op: \"replace\", path: \"is-hidden\", value: true }\n\n ajax.patch(this.props.post.api.index, [op]).then(\n (patch) => {\n store.dispatch(post.patch(this.props.post, patch))\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n store.dispatch(\n post.patch(this.props.post, {\n is_hidden: false,\n })\n )\n }\n )\n }\n\n render() {\n if (!this.props.post.is_hidden) {\n return (\n \n )\n } else {\n return null\n }\n }\n}\n\nexport class Unhide extends React.Component {\n onClick = () => {\n store.dispatch(\n post.patch(this.props.post, {\n is_hidden: false,\n })\n )\n\n const op = { op: \"replace\", path: \"is-hidden\", value: false }\n\n ajax.patch(this.props.post.api.index, [op]).then(\n (patch) => {\n store.dispatch(post.patch(this.props.post, patch))\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n store.dispatch(\n post.patch(this.props.post, {\n is_hidden: true,\n })\n )\n }\n )\n }\n\n render() {\n if (this.props.post.is_hidden) {\n return (\n \n )\n } else {\n return null\n }\n }\n}\n\nexport class Delete extends React.Component {\n onClick = () => {\n const decision = window.confirm(\n gettext(\n \"event delete\",\n \"Are you sure you wish to delete this event? This action is not reversible!\"\n )\n )\n if (decision) {\n this.delete()\n }\n }\n\n delete = () => {\n store.dispatch(\n post.patch(this.props.post, {\n isDeleted: true,\n })\n )\n\n ajax.delete(this.props.post.api.index).then(\n () => {\n snackbar.success(pgettext(\"event delete\", \"Event has been deleted.\"))\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n store.dispatch(\n post.patch(this.props.post, {\n isDeleted: false,\n })\n )\n }\n )\n }\n\n render() {\n return (\n \n )\n }\n}\n","import React from \"react\"\nimport escapeHtml from \"misago/utils/escape-html\"\nimport Controls from \"./controls\"\n\nconst DATE_ABBR = '%(relative)s'\nconst DATE_URL = '%(relative)s'\nconst USER_SPAN = '%(user)s'\nconst USER_URL = '%(user)s'\n\nexport default function (props) {\n return (\n
      \n \n \n \n
    \n )\n}\n\nexport function Hidden(props) {\n if (props.post.is_hidden) {\n let user = null\n if (props.post.url.hidden_by) {\n user = interpolate(\n USER_URL,\n {\n url: escapeHtml(props.post.url.hidden_by),\n user: escapeHtml(props.post.hidden_by_name),\n },\n true\n )\n } else {\n user = interpolate(\n USER_SPAN,\n {\n user: escapeHtml(props.post.hidden_by_name),\n },\n true\n )\n }\n\n const date = interpolate(\n DATE_ABBR,\n {\n absolute: escapeHtml(props.post.hidden_on.format(\"LLL\")),\n relative: escapeHtml(props.post.hidden_on.fromNow()),\n },\n true\n )\n\n const message = interpolate(\n escapeHtml(\n pgettext(\"event info\", \"Hidden by %(event_by)s %(event_on)s.\")\n ),\n {\n event_by: user,\n event_on: date,\n },\n true\n )\n\n return (\n \n )\n } else {\n return null\n }\n}\n\nexport function Poster(props) {\n let user = null\n if (props.post.poster) {\n user = interpolate(\n USER_URL,\n {\n url: escapeHtml(props.post.poster.url),\n user: escapeHtml(props.post.poster_name),\n },\n true\n )\n } else {\n user = interpolate(\n USER_SPAN,\n {\n user: escapeHtml(props.post.poster_name),\n },\n true\n )\n }\n\n const date = interpolate(\n DATE_URL,\n {\n url: escapeHtml(props.post.url.index),\n absolute: escapeHtml(props.post.posted_on.format(\"LLL\")),\n relative: escapeHtml(props.post.posted_on.fromNow()),\n },\n true\n )\n\n const message = interpolate(\n escapeHtml(pgettext(\"event info\", \"By %(event_by)s %(event_on)s.\")),\n {\n event_by: user,\n event_on: date,\n },\n true\n )\n\n return (\n \n )\n}\n","import React from \"react\"\nimport escapeHtml from \"misago/utils/escape-html\"\n\nconst MESSAGE = {\n pinned_globally: pgettext(\n \"event message\",\n \"Thread has been pinned globally.\"\n ),\n pinned_locally: pgettext(\"event message\", \"Thread has been pinned locally.\"),\n unpinned: pgettext(\"event message\", \"Thread has been unpinned.\"),\n\n approved: pgettext(\"event message\", \"Thread has been approved.\"),\n\n opened: pgettext(\"event message\", \"Thread has been opened.\"),\n closed: pgettext(\"event message\", \"Thread has been closed.\"),\n\n unhid: pgettext(\"event message\", \"Thread has been revealed.\"),\n hid: pgettext(\"event message\", \"Thread has been made hidden.\"),\n\n tookover: pgettext(\"event message\", \"Took thread over.\"),\n\n owner_left: pgettext(\n \"event message\",\n \"Owner has left thread. This thread is now closed.\"\n ),\n participant_left: pgettext(\"event message\", \"Participant has left thread.\"),\n}\n\nconst ITEM_LINK = '%(name)s'\nconst ITEM_SPAN = '%(name)s'\n\nexport default function (props) {\n if (MESSAGE[props.post.event_type]) {\n return

    {MESSAGE[props.post.event_type]}

    \n } else if (props.post.event_type === \"changed_title\") {\n return \n } else if (props.post.event_type === \"moved\") {\n return \n } else if (props.post.event_type === \"merged\") {\n return \n } else if (props.post.event_type === \"changed_owner\") {\n return \n } else if (props.post.event_type === \"added_participant\") {\n return \n } else if (props.post.event_type === \"removed_participant\") {\n return \n } else {\n return null\n }\n}\n\nexport function ChangedTitle(props) {\n const msgstring = escapeHtml(\n pgettext(\n \"event message\",\n \"Thread title has been changed from %(old_title)s.\"\n )\n )\n const oldTitle = interpolate(\n ITEM_SPAN,\n {\n name: escapeHtml(props.post.event_context.old_title),\n },\n true\n )\n const message = interpolate(\n msgstring,\n {\n old_title: oldTitle,\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function Moved(props) {\n const msgstring = escapeHtml(\n pgettext(\"event message\", \"Thread has been moved from %(from_category)s.\")\n )\n const fromCategory = interpolate(\n ITEM_LINK,\n {\n url: escapeHtml(props.post.event_context.from_category.url),\n name: escapeHtml(props.post.event_context.from_category.name),\n },\n true\n )\n\n const message = interpolate(\n msgstring,\n {\n from_category: fromCategory,\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function Merged(props) {\n const msgstring = escapeHtml(\n pgettext(\n \"event message\",\n \"The %(merged_thread)s thread has been merged into this thread.\"\n )\n )\n const mergedThread = interpolate(\n ITEM_SPAN,\n {\n name: escapeHtml(props.post.event_context.merged_thread),\n },\n true\n )\n\n const message = interpolate(\n msgstring,\n {\n merged_thread: mergedThread,\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function ChangedOwner(props) {\n const msgstring = escapeHtml(\n pgettext(\"event message\", \"Changed thread owner to %(user)s.\")\n )\n const newOwner = interpolate(\n ITEM_LINK,\n {\n url: escapeHtml(props.post.event_context.user.url),\n name: escapeHtml(props.post.event_context.user.username),\n },\n true\n )\n\n const message = interpolate(\n msgstring,\n {\n user: newOwner,\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function AddedParticipant(props) {\n const msgstring = escapeHtml(\n pgettext(\"event message\", \"Added %(user)s to thread.\")\n )\n const newOwner = interpolate(\n ITEM_LINK,\n {\n url: escapeHtml(props.post.event_context.user.url),\n name: escapeHtml(props.post.event_context.user.username),\n },\n true\n )\n\n const message = interpolate(\n msgstring,\n {\n user: newOwner,\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function RemovedParticipant(props) {\n const msgstring = escapeHtml(\n pgettext(\"event message\", \"Removed %(user)s from thread.\")\n )\n const newOwner = interpolate(\n ITEM_LINK,\n {\n url: escapeHtml(props.post.event_context.user.url),\n name: escapeHtml(props.post.event_context.user.username),\n },\n true\n )\n\n const message = interpolate(\n msgstring,\n {\n user: newOwner,\n },\n true\n )\n\n return (\n \n )\n}\n","import React from \"react\"\n\nexport default function ({ post }) {\n if (post.is_read) return null\n\n return (\n
    \n \n {pgettext(\"event unread label\", \"New event\")}\n \n
    \n )\n}\n","import React from \"react\"\nimport ajax from \"misago/services/ajax\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.initialized = false\n this.primed = false\n this.observer = null\n }\n\n initialize = (element) => {\n this.initialized = true\n\n this.observer = new IntersectionObserver((entries) =>\n entries.forEach(this.callback)\n )\n this.observer.observe(element)\n }\n\n callback = (entry) => {\n if (!entry.isIntersecting || this.props.post.is_read || this.primed) {\n return\n }\n\n window.setTimeout(() => {\n ajax.post(this.props.post.api.read)\n }, 0)\n\n this.primed = true\n this.destroy()\n }\n\n destroy() {\n if (this.observer) {\n this.observer.disconnect()\n this.observer = null\n }\n }\n\n componentWillUnmount() {\n this.destroy()\n }\n\n render() {\n const ready = !this.initialized && !this.primed && !this.props.post.is_read\n\n return (\n {\n if (node && ready) {\n this.initialize(node)\n }\n }}\n >\n {this.props.children}\n \n )\n }\n}\n","import React from \"react\"\nimport Icon from \"./icon\"\nimport Info from \"./info\"\nimport Message from \"./message\"\nimport UnreadLabel from \"./unread-label\"\nimport Waypoint from \"../waypoint\"\n\nexport default function (props) {\n let className = \"event\"\n if (props.post.isDeleted) {\n className = \"hide\"\n } else if (props.post.is_hidden) {\n className = \"event post-hidden\"\n }\n\n return (\n
  • \n \n
    \n
    \n \n
    \n \n \n \n \n
    \n
  • \n )\n}\n","import React from \"react\"\nimport misago from \"misago\"\nimport escapeHtml from \"misago/utils/escape-html\"\nimport formatFilesize from \"misago/utils/file-size\"\n\nconst DATE_ABBR = '%(relative)s'\nconst USER_SPAN = '%(user)s'\nconst USER_URL = '%(user)s'\n\nexport default function (props) {\n return (\n
    \n \n
    \n \n {props.attachment.filename}\n \n \n
    \n
    \n )\n}\n\nexport function AttachmentPreview(props) {\n if (props.attachment.is_image) {\n return (\n
    \n \n
    \n )\n } else {\n return (\n
    \n \n
    \n )\n }\n}\n\nexport function AttachmentIcon(props) {\n return (\n \n insert_drive_file\n \n )\n}\n\nexport function AttachmentThumbnail(props) {\n const url = props.attachment.url.thumb || props.attachment.url.index\n return (\n \n )\n}\n\nexport function AttachmentDetails(props) {\n let user = null\n if (props.attachment.url.uploader) {\n user = interpolate(\n USER_URL,\n {\n url: escapeHtml(props.attachment.url.uploader),\n user: escapeHtml(props.attachment.uploader_name),\n },\n true\n )\n } else {\n user = interpolate(\n USER_SPAN,\n {\n user: escapeHtml(props.attachment.uploader_name),\n },\n true\n )\n }\n\n const date = interpolate(\n DATE_ABBR,\n {\n absolute: escapeHtml(props.attachment.uploaded_on.format(\"LLL\")),\n relative: escapeHtml(props.attachment.uploaded_on.fromNow()),\n },\n true\n )\n\n const message = interpolate(\n escapeHtml(\n pgettext(\n \"post attachment\",\n \"%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s.\"\n )\n ),\n {\n filetype: props.attachment.filetype,\n size: formatFilesize(props.attachment.size),\n uploader: user,\n uploaded_on: date,\n },\n true\n )\n\n return (\n \n )\n}\n","import React from \"react\"\nimport batch from \"misago/utils/batch\"\nimport Attachment from \"./attachment\"\n\nexport default function (props) {\n if (!isVisible(props.post)) {\n return null\n }\n\n return (\n
    \n {batch(props.post.attachments, 2).map((row) => {\n const key = row\n .map((a) => {\n return a ? a.id : 0\n })\n .join(\"_\")\n return \n })}\n
    \n )\n}\n\nexport function isVisible(post) {\n return (!post.is_hidden || post.acl.can_see_hidden) && post.attachments\n}\n\nexport function Row(props) {\n return (\n
    \n {props.row.map((attachment) => {\n return (\n \n )\n })}\n
    \n )\n}\n","import React from \"react\"\nimport Waypoint from \"../waypoint\"\nimport MisagoMarkup from \"misago/components/misago-markup\"\nimport escapeHtml from \"misago/utils/escape-html\"\n\nconst HIDDEN_BY_URL = '%(user)s'\nconst HIDDEN_BY_SPAN = '%(user)s'\nconst HIDDEN_ON =\n '%(relative)s'\n\nexport default function (props) {\n if (props.post.is_hidden && !props.post.acl.can_see_hidden) {\n return \n } else if (props.post.content) {\n return \n } else {\n return \n }\n}\n\nexport function Default({ post }) {\n const poster = \"@\" + (post.poster ? post.poster.username : post.poster_name)\n\n return (\n \n \n \n )\n}\n\nexport function Hidden(props) {\n let user = null\n if (props.post.hidden_by) {\n user = interpolate(\n HIDDEN_BY_URL,\n {\n url: escapeHtml(props.post.url.hidden_by),\n user: escapeHtml(props.post.hidden_by_name),\n },\n true\n )\n } else {\n user = interpolate(\n HIDDEN_BY_SPAN,\n {\n user: escapeHtml(props.post.hidden_by_name),\n },\n true\n )\n }\n\n const date = interpolate(\n HIDDEN_ON,\n {\n absolute: escapeHtml(props.post.hidden_on.format(\"LLL\")),\n relative: escapeHtml(props.post.hidden_on.fromNow()),\n },\n true\n )\n\n const message = interpolate(\n escapeHtml(\n pgettext(\"post body hidden\", \"Hidden by %(hidden_by)s %(hidden_on)s.\")\n ),\n {\n hidden_by: user,\n hidden_on: date,\n },\n true\n )\n\n return (\n \n

    \n {pgettext(\n \"post body hidden\",\n \"This post is hidden. You cannot see its contents.\"\n )}\n

    \n

    \n \n )\n}\n\nexport function Invalid(props) {\n return (\n \n

    \n {pgettext(\n \"post body invalid\",\n \"This post's contents cannot be displayed.\"\n )}\n

    \n

    \n {pgettext(\n \"post body invalid\",\n \"This error is caused by invalid post content manipulation.\"\n )}\n

    \n
    \n )\n}\n","import React from \"react\"\n\nexport function FlagBestAnswer({ post, thread, user }) {\n if (!(isVisible(post) && post.id === thread.best_answer)) {\n return null\n }\n\n let message = null\n if (user.id && thread.best_answer_marked_by === user.id) {\n message = interpolate(\n pgettext(\n \"post best answer flag\",\n \"Marked as best answer by you %(marked_on)s.\"\n ),\n {\n marked_on: thread.best_answer_marked_on.fromNow(),\n },\n true\n )\n } else {\n message = interpolate(\n pgettext(\n \"post best answer flag\",\n \"Marked as best answer by %(marked_by)s %(marked_on)s.\"\n ),\n {\n marked_by: thread.best_answer_marked_by_name,\n marked_on: thread.best_answer_marked_on.fromNow(),\n },\n true\n )\n }\n\n return (\n
    \n check_box\n

    {message}

    \n
    \n )\n}\n\nexport function FlagHidden(props) {\n if (!(isVisible(props.post) && props.post.is_hidden)) {\n return null\n }\n\n return (\n
    \n visibility_off\n

    \n {pgettext(\n \"post hidden flag\",\n \"This post is hidden. Only users with permission may see its contents.\"\n )}\n

    \n
    \n )\n}\n\nexport function FlagUnapproved(props) {\n if (!(isVisible(props.post) && props.post.is_unapproved)) {\n return null\n }\n\n return (\n
    \n remove_circle_outline\n

    \n {pgettext(\n \"post unapproved flag\",\n \"This post is unapproved. Only users with permission to approve posts and its author may see its contents.\"\n )}\n

    \n
    \n )\n}\n\nexport function FlagProtected(props) {\n if (!(isVisible(props.post) && props.post.is_protected)) {\n return null\n }\n\n return (\n
    \n lock_outline\n

    \n {pgettext(\n \"post protected flag\",\n \"This post is protected. Only moderators may change it.\"\n )}\n

    \n
    \n )\n}\n\nexport function isVisible(post) {\n return !post.is_hidden || post.acl.can_see_hidden\n}\n","import moment from \"moment\"\nimport * as thread from \"misago/reducers/thread\"\nimport * as post from \"misago/reducers/post\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport function approve(props) {\n store.dispatch(\n post.patch(props.post, {\n is_unapproved: false,\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-unapproved\", value: false }]\n\n const previousState = {\n is_unapproved: props.post.is_unapproved,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function protect(props) {\n store.dispatch(\n post.patch(props.post, {\n is_protected: true,\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-protected\", value: true }]\n\n const previousState = {\n is_protected: props.post.is_protected,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function unprotect(props) {\n store.dispatch(\n post.patch(props.post, {\n is_protected: false,\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-protected\", value: false }]\n\n const previousState = {\n is_protected: props.post.is_protected,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function hide(props) {\n store.dispatch(\n post.patch(props.post, {\n is_hidden: true,\n hidden_on: moment(),\n hidden_by_name: props.user.username,\n url: Object.assign(props.post.url, {\n hidden_by: props.user.url,\n }),\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-hidden\", value: true }]\n\n const previousState = {\n is_hidden: props.post.is_hidden,\n hidden_on: props.post.hidden_on,\n hidden_by_name: props.post.hidden_by_name,\n url: props.post.url,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function unhide(props) {\n store.dispatch(\n post.patch(props.post, {\n is_hidden: false,\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-hidden\", value: false }]\n\n const previousState = {\n is_hidden: props.post.is_hidden,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function like(props) {\n const lastLikes = props.post.last_likes || []\n const concatedLikes = [props.user].concat(lastLikes)\n const finalLikes =\n concatedLikes.length > 3 ? concatedLikes.slice(0, -1) : concatedLikes\n\n store.dispatch(\n post.patch(props.post, {\n is_liked: true,\n likes: props.post.likes + 1,\n last_likes: finalLikes,\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-liked\", value: true }]\n\n const previousState = {\n is_liked: props.post.is_liked,\n likes: props.post.likes,\n last_likes: props.post.last_likes,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function unlike(props) {\n store.dispatch(\n post.patch(props.post, {\n is_liked: false,\n likes: props.post.likes - 1,\n last_likes: props.post.last_likes.filter((user) => {\n return !user.id || user.id !== props.user.id\n }),\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-liked\", value: false }]\n\n const previousState = {\n is_liked: props.post.is_liked,\n likes: props.post.likes,\n last_likes: props.post.last_likes,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function patch(props, ops, previousState) {\n ajax.patch(props.post.api.index, ops).then(\n (newState) => {\n store.dispatch(post.patch(props.post, newState))\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n store.dispatch(post.patch(props.post, previousState))\n }\n )\n}\n\nexport function remove(props) {\n let confirmed = window.confirm(\n pgettext(\n \"post delete\",\n \"Are you sure you want to delete this post? This action is not reversible!\"\n )\n )\n if (!confirmed) {\n return\n }\n\n store.dispatch(\n post.patch(props.post, {\n isDeleted: true,\n })\n )\n\n ajax.delete(props.post.api.index).then(\n () => {\n snackbar.success(pgettext(\"post delete\", \"Post has been deleted.\"))\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n } else {\n snackbar.apiError(rejection)\n }\n\n store.dispatch(\n post.patch(props.post, {\n isDeleted: false,\n })\n )\n }\n )\n}\n\nexport function markAsBestAnswer(props) {\n const { post, user } = props\n\n store.dispatch(\n thread.update({\n best_answer: post.id,\n best_answer_is_protected: post.is_protected,\n best_answer_marked_on: moment(),\n best_answer_marked_by: user.id,\n best_answer_marked_by_name: user.username,\n best_answer_marked_by_slug: user.slug,\n })\n )\n\n const ops = [\n { op: \"replace\", path: \"best-answer\", value: post.id },\n { op: \"add\", path: \"acl\", value: true },\n ]\n\n const previousState = {\n best_answer: props.thread.best_answer,\n best_answer_is_protected: props.thread.best_answer_is_protected,\n best_answer_marked_on: props.thread.best_answer_marked_on,\n best_answer_marked_by: props.thread.best_answer_marked_by,\n best_answer_marked_by_name: props.thread.best_answer_marked_by_name,\n best_answer_marked_by_slug: props.thread.best_answer_marked_by_slug,\n }\n\n patchThread(props, ops, previousState)\n}\n\nexport function unmarkBestAnswer(props) {\n const { post } = props\n\n store.dispatch(\n thread.update({\n best_answer: null,\n best_answer_is_protected: false,\n best_answer_marked_on: null,\n best_answer_marked_by: null,\n best_answer_marked_by_name: null,\n best_answer_marked_by_slug: null,\n })\n )\n\n const ops = [\n { op: \"remove\", path: \"best-answer\", value: post.id },\n { op: \"add\", path: \"acl\", value: true },\n ]\n\n const previousState = {\n best_answer: props.thread.best_answer,\n best_answer_is_protected: props.thread.best_answer_is_protected,\n best_answer_marked_on: props.thread.best_answer_marked_on,\n best_answer_marked_by: props.thread.best_answer_marked_by,\n best_answer_marked_by_name: props.thread.best_answer_marked_by_name,\n best_answer_marked_by_slug: props.thread.best_answer_marked_by_slug,\n }\n\n patchThread(props, ops, previousState)\n}\n\nexport function patchThread(props, ops, previousState) {\n ajax.patch(props.thread.api.index, ops).then(\n (newState) => {\n if (newState.best_answer_marked_on) {\n newState.best_answer_marked_on = moment(newState.best_answer_marked_on)\n }\n store.dispatch(thread.update(newState))\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n store.dispatch(thread.update(previousState))\n }\n )\n}\n","import React from \"react\"\nimport moment from \"moment\"\nimport Avatar from \"misago/components/avatar\"\nimport Message from \"misago/components/modal-message\"\nimport Loader from \"misago/components/modal-loader\"\nimport ajax from \"misago/services/ajax\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isReady: false,\n\n error: null,\n likes: [],\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.post.api.likes).then(\n (data) => {\n this.setState({\n isReady: true,\n likes: data.map(hydrateLike),\n })\n },\n (rejection) => {\n this.setState({\n isReady: true,\n error: rejection.detail,\n })\n }\n )\n }\n\n render() {\n if (this.state.error) {\n return (\n \n \n \n )\n } else if (this.state.isReady) {\n if (this.state.likes.length) {\n return (\n \n \n \n )\n }\n\n return (\n \n \n \n )\n }\n\n return (\n \n \n \n )\n }\n}\n\nexport function hydrateLike(data) {\n return Object.assign({}, data, {\n liked_on: moment(data.liked_on),\n })\n}\n\nexport function ModalDialog({ className, children, likes }) {\n let title = pgettext(\"post likes modal title\", \"Post Likes\")\n if (likes) {\n const likesCount = likes.length\n const message = npgettext(\n \"post likes modal\",\n \"%(likes)s like\",\n \"%(likes)s likes\",\n likesCount\n )\n\n title = interpolate(message, { likes: likesCount }, true)\n }\n\n return (\n
    \n
    \n
    \n \n ×\n \n

    {title}

    \n
    \n {children}\n
    \n
    \n )\n}\n\nexport function LikesList(props) {\n return (\n
    \n
      \n {props.likes.map((like) => {\n return \n })}\n
    \n
    \n )\n}\n\nexport function LikeDetails(props) {\n if (props.url) {\n const user = {\n id: props.liker_id,\n avatars: props.avatars,\n }\n\n return (\n
  • \n
    \n \n \n \n
    \n
    \n \n {props.username}\n {\" \"}\n \n
    \n
  • \n )\n }\n\n return (\n
  • \n
    \n \n \n \n
    \n
    \n {props.username} \n
    \n
  • \n )\n}\n\nexport function LikeDate(props) {\n return (\n \n {props.likedOn.fromNow()}\n \n )\n}\n","import React from \"react\"\nimport * as actions from \"./controls/actions\"\nimport LikesModal from \"misago/components/post-likes\"\nimport modal from \"misago/services/modal\"\nimport posting from \"misago/services/posting\"\n\nexport default function (props) {\n if (!isVisible(props.post)) return null\n\n return (\n
    \n \n \n \n \n \n \n \n \n
    \n )\n}\n\nexport function isVisible(post) {\n return (\n (!post.is_hidden || post.acl.can_see_hidden) &&\n (post.acl.can_reply ||\n post.acl.can_edit ||\n (post.acl.can_see_likes && (post.last_likes || []).length) ||\n post.acl.can_like)\n )\n}\n\nexport class MarkAsBestAnswer extends React.Component {\n onClick = () => {\n actions.markAsBestAnswer(this.props)\n }\n\n render() {\n const { post, thread } = this.props\n\n if (!thread.acl.can_mark_best_answer) return null\n if (!post.acl.can_mark_as_best_answer) return null\n if (thread.best_answer && !thread.acl.can_change_best_answer) return null\n\n return (\n \n check_box\n {pgettext(\"post footer btn\", \"Best answer\")}\n \n )\n }\n}\n\nexport class MarkAsBestAnswerCompact extends React.Component {\n onClick = () => {\n actions.markAsBestAnswer(this.props)\n }\n\n render() {\n const { post, thread } = this.props\n\n if (!thread.acl.can_mark_best_answer) return null\n if (!post.acl.can_mark_as_best_answer) return null\n if (thread.best_answer && !thread.acl.can_change_best_answer) return null\n\n return (\n \n check_box\n \n )\n }\n}\n\nexport class Like extends React.Component {\n onClick = () => {\n if (this.props.post.is_liked) {\n actions.unlike(this.props)\n } else {\n actions.like(this.props)\n }\n }\n\n render() {\n if (!this.props.post.acl.can_like) return null\n\n let className = \"btn btn-default btn-sm pull-left\"\n if (this.props.post.is_liked) {\n className = \"btn btn-success btn-sm pull-left\"\n }\n\n return (\n \n {this.props.post.is_liked\n ? pgettext(\"post footer btn\", \"Liked\")\n : pgettext(\"post footer btn\", \"Like\")}\n \n )\n }\n}\n\nexport class Likes extends React.Component {\n onClick = () => {\n modal.show()\n }\n\n render() {\n const hasLikes = (this.props.post.last_likes || []).length > 0\n if (!this.props.post.acl.can_see_likes || !hasLikes) return null\n\n if (this.props.post.acl.can_see_likes === 2) {\n return (\n \n {getLikesMessage(this.props.likes, this.props.lastLikes)}\n \n )\n }\n\n return (\n

    \n {getLikesMessage(this.props.likes, this.props.lastLikes)}\n

    \n )\n }\n}\n\nexport class LikesCompact extends Likes {\n render() {\n const hasLikes = (this.props.post.last_likes || []).length > 0\n if (!this.props.post.acl.can_see_likes || !hasLikes) return null\n\n if (this.props.post.acl.can_see_likes === 2) {\n return (\n \n favorite\n {this.props.likes}\n \n )\n }\n\n return (\n

    \n favorite\n {this.props.likes}\n

    \n )\n }\n}\n\nexport function getLikesMessage(likes, users) {\n const usernames = users.slice(0, 3).map((u) => u.username)\n\n if (usernames.length == 1) {\n return interpolate(\n pgettext(\"post likes\", \"%(user)s likes this.\"),\n {\n user: usernames[0],\n },\n true\n )\n }\n\n const hiddenLikes = likes - usernames.length\n\n const otherUsers = usernames.slice(0, -1).join(\", \")\n const lastUser = usernames.slice(-1)[0]\n\n const usernamesList = interpolate(\n pgettext(\"post likes\", \"%(users)s and %(last_user)s\"),\n {\n users: otherUsers,\n last_user: lastUser,\n },\n true\n )\n\n if (hiddenLikes === 0) {\n return interpolate(\n pgettext(\"post likes\", \"%(users)s like this.\"),\n {\n users: usernamesList,\n },\n true\n )\n }\n\n const message = npgettext(\n \"post likes\",\n \"%(users)s and %(likes)s other user like this.\",\n \"%(users)s and %(likes)s other users like this.\",\n hiddenLikes\n )\n\n return interpolate(\n message,\n {\n users: usernames.join(\", \"),\n likes: hiddenLikes,\n },\n true\n )\n}\n\nexport class Reply extends React.Component {\n onClick = () => {\n posting.open({\n mode: \"REPLY\",\n\n thread: this.props.thread,\n config: this.props.thread.api.editor,\n submit: this.props.thread.api.posts.index,\n })\n }\n\n render() {\n if (this.props.post.acl.can_reply) {\n return (\n \n {pgettext(\"post footer btn\", \"Reply\")}\n \n )\n } else {\n return null\n }\n }\n}\n\nexport class Quote extends React.Component {\n onClick = () => {\n posting.open({\n mode: \"QUOTE\",\n\n thread: this.props.thread,\n config: this.props.thread.api.editor,\n submit: this.props.thread.api.posts.index,\n\n context: {\n reply: this.props.post.id,\n },\n })\n }\n\n render() {\n if (this.props.post.acl.can_reply) {\n return (\n \n {pgettext(\"post footer btn\", \"Quote\")}\n \n )\n } else {\n return null\n }\n }\n}\n\nexport class Edit extends React.Component {\n onClick = () => {\n posting.open({\n mode: \"EDIT\",\n\n thread: this.props.thread,\n post: this.props.post,\n config: this.props.post.api.editor,\n submit: this.props.post.api.index,\n })\n }\n\n render() {\n if (this.props.post.acl.can_edit) {\n return (\n \n {pgettext(\"post footer btn\", \"Edit\")}\n \n )\n } else {\n return null\n }\n }\n}\n","import React from \"react\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport * as post from \"misago/reducers/post\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n url: \"\",\n\n validators: {\n url: [],\n },\n errors: {},\n }\n }\n\n clean() {\n if (!this.state.url.trim().length) {\n snackbar.error(\n pgettext(\n \"post move modal\",\n \"You have to enter link to the other thread.\"\n )\n )\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.post(this.props.thread.api.posts.move, {\n new_thread: this.state.url,\n posts: [this.props.post.id],\n })\n }\n\n handleSuccess(success) {\n store.dispatch(\n post.patch(this.props.post, {\n isDeleted: true,\n })\n )\n\n modal.hide()\n\n snackbar.success(\n pgettext(\n \"post move modal\",\n \"Selected post was moved to the other thread.\"\n )\n )\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n onUrlChange = (event) => {\n this.changeValue(\"url\", event.target.value)\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n
    \n \n \n \n
    \n
    \n \n {pgettext(\"post move modal btn\", \"Move post\")}\n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport function ModalHeader(props) {\n return (\n
    \n \n ×\n \n

    \n {pgettext(\"post move modal title\", \"Move post\")}\n

    \n
    \n )\n}\n","import React from \"react\"\n\nexport default function (props) {\n return (\n
    \n
      \n {props.diff.map((item, i) => {\n return \n })}\n
    \n
    \n )\n}\n\nexport function DiffItem(props) {\n if (props.item[0] === \"?\") return null\n\n return (\n
  • {cleanItem(props.item)}
  • \n )\n}\n\nexport function getItemClassName(item) {\n let className = \"diff-item\"\n if (item[0] === \"-\") {\n className += \" diff-item-sub\"\n } else if (item[0] === \"+\") {\n className += \" diff-item-add\"\n }\n return className\n}\n\nexport function cleanItem(item) {\n return item.substr(2)\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\n\nexport default class extends React.Component {\n onClick = () => {\n this.props.revertEdit(this.props.edit.id)\n }\n\n render() {\n if (!this.props.canRevert) return null\n\n return (\n
    \n \n {pgettext(\"post revert btn\", \"Revert\")}\n \n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport escapeHtml from \"misago/utils/escape-html\"\n\nconst DATE_ABBR = '%(relative)s'\nconst USER_SPAN = '%(user)s'\nconst USER_URL = '%(user)s'\n\nexport default class extends React.Component {\n goLast = () => {\n this.props.goToEdit()\n }\n\n goForward = () => {\n this.props.goToEdit(this.props.edit.next)\n }\n\n goBack = () => {\n this.props.goToEdit(this.props.edit.previous)\n }\n\n revertEdit = () => {\n this.props.revertEdit(this.props.edit.id)\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n
    \n \n
    \n
    \n \n
    \n
    \n \n
    \n
    \n
    \n
    \n
    \n \n
    \n
    \n )\n }\n}\n\nexport function GoBackBtn(props) {\n return (\n \n chevron_left\n \n )\n}\n\nexport function GoForwardBtn(props) {\n return (\n \n chevron_right\n \n )\n}\n\nexport function GoLastBtn(props) {\n return (\n \n last_page\n \n )\n}\n\nexport function RevertBtn(props) {\n if (!props.canRevert) return null\n\n return (\n
    \n \n {pgettext(\"post history modal btn\", \"Revert\")}\n \n
    \n )\n}\n\nexport function Label(props) {\n let user = null\n if (props.edit.url.editor) {\n user = interpolate(\n USER_URL,\n {\n url: escapeHtml(props.edit.url.editor),\n user: escapeHtml(props.edit.editor_name),\n },\n true\n )\n } else {\n user = interpolate(\n USER_SPAN,\n {\n user: escapeHtml(props.edit.editor_name),\n },\n true\n )\n }\n\n const date = interpolate(\n DATE_ABBR,\n {\n absolute: escapeHtml(props.edit.edited_on.format(\"LLL\")),\n relative: escapeHtml(props.edit.edited_on.fromNow()),\n },\n true\n )\n\n const message = interpolate(\n escapeHtml(\n pgettext(\"post history modal\", \"By %(edited_by)s %(edited_on)s.\")\n ),\n {\n edited_by: user,\n edited_on: date,\n },\n true\n )\n\n return

    \n}\n","import moment from \"moment\"\n\nexport function hydrateEdit(json) {\n return Object.assign({}, json, {\n edited_on: moment(json.edited_on),\n })\n}\n","import React from \"react\"\nimport Diff from \"./diff\"\nimport Footer from \"./footer\"\nimport Toolbar from \"./toolbar\"\nimport { hydrateEdit } from \"./utils\"\nimport Message from \"misago/components/modal-message\"\nimport Loader from \"misago/components/modal-loader\"\nimport * as post from \"misago/reducers/post\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isReady: false,\n isBusy: true,\n\n canRevert: props.post.acl.can_edit,\n\n error: null,\n edit: null,\n }\n }\n\n componentDidMount() {\n this.goToEdit()\n }\n\n goToEdit = (edit = null) => {\n this.setState({\n isBusy: true,\n })\n\n let url = this.props.post.api.edits\n if (edit !== null) {\n url += \"?edit=\" + edit\n }\n\n ajax.get(url).then(\n (data) => {\n this.setState({\n isReady: true,\n isBusy: false,\n edit: hydrateEdit(data),\n })\n },\n (rejection) => {\n this.setState({\n isReady: true,\n isBusy: false,\n error: rejection.detail,\n })\n }\n )\n }\n\n revertEdit = (edit) => {\n if (this.state.isBusy) return\n\n const confirmation = window.confirm(\n pgettext(\n \"post revert\",\n \"Are you sure you with to revert this post to the state from before this edit?\"\n )\n )\n if (!confirmation) return\n\n this.setState({\n isBusy: true,\n })\n\n const url = this.props.post.api.edits + \"?edit=\" + edit\n ajax.post(url).then(\n (data) => {\n const hydratedPost = post.hydrate(data)\n store.dispatch(post.patch(data, hydratedPost))\n\n snackbar.success(\n pgettext(\"post revert\", \"Post has been reverted to previous state.\")\n )\n modal.hide()\n },\n (rejection) => {\n snackbar.apiError(rejection)\n\n this.setState({\n isBusy: false,\n })\n }\n )\n }\n\n render() {\n if (this.state.error) {\n return (\n \n \n \n )\n } else if (this.state.isReady) {\n return (\n \n \n \n \n \n )\n }\n\n return (\n \n \n \n )\n }\n}\n\nexport function ModalDialog(props) {\n return (\n

    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"post history modal title\", \"Post edits history\")}\n

    \n
    \n {props.children}\n
    \n
    \n )\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport CategorySelect from \"misago/components/category-select\"\nimport ModalLoader from \"misago/components/modal-loader\"\nimport Select from \"misago/components/select\"\nimport * as post from \"misago/reducers/post\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\nimport * as validators from \"misago/utils/validators\"\n\nexport default function (props) {\n return \n}\n\nexport class PostingConfig extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoaded: false,\n isError: false,\n\n categories: [],\n }\n }\n\n componentDidMount() {\n ajax.get(misago.get(\"THREAD_EDITOR_API\")).then(\n (data) => {\n // hydrate categories, extract posting options\n const categories = data.map((item) => {\n return Object.assign(item, {\n disabled: item.post === false,\n label: item.name,\n value: item.id,\n post: item.post,\n })\n })\n\n this.setState({\n isLoaded: true,\n categories,\n })\n },\n (rejection) => {\n this.setState({\n isError: rejection.detail,\n })\n }\n )\n }\n\n render() {\n if (this.state.isError) {\n return \n } else if (this.state.isLoaded) {\n return (\n \n )\n } else {\n return \n }\n }\n}\n\nexport class ModerationForm extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n title: \"\",\n category: null,\n categories: props.categories,\n weight: 0,\n is_hidden: 0,\n is_closed: false,\n\n validators: {\n title: [validators.required()],\n },\n\n errors: {},\n }\n\n this.isHiddenChoices = [\n {\n value: 0,\n icon: \"visibility\",\n label: pgettext(\"thread hidden switch choice\", \"No\"),\n },\n {\n value: 1,\n icon: \"visibility_off\",\n label: pgettext(\"thread hidden switch choice\", \"Yes\"),\n },\n ]\n\n this.isClosedChoices = [\n {\n value: false,\n icon: \"lock_outline\",\n label: pgettext(\"thread closed switch choice\", \"No\"),\n },\n {\n value: true,\n icon: \"lock\",\n label: pgettext(\"thread closed switch choice\", \"Yes\"),\n },\n ]\n\n this.acl = {}\n this.props.categories.forEach((category) => {\n if (category.post) {\n if (!this.state.category) {\n this.state.category = category.id\n }\n\n this.acl[category.id] = {\n can_pin_threads: category.post.pin,\n can_close_threads: category.post.close,\n can_hide_threads: category.post.hide,\n }\n }\n })\n }\n\n clean() {\n if (this.isValid()) {\n return true\n } else {\n snackbar.error(gettext(\"Form contains errors.\"))\n this.setState({\n errors: this.validate(),\n })\n return false\n }\n }\n\n send() {\n return ajax.post(this.props.thread.api.posts.split, {\n title: this.state.title,\n category: this.state.category,\n weight: this.state.weight,\n is_hidden: this.state.is_hidden,\n is_closed: this.state.is_closed,\n posts: [this.props.post.id],\n })\n }\n\n handleSuccess(apiResponse) {\n store.dispatch(\n post.patch(this.props.post, {\n isDeleted: true,\n })\n )\n\n modal.hide()\n\n snackbar.success(\n pgettext(\"post split modal\", \"Selected post was split into new thread.\")\n )\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n this.setState({\n errors: Object.assign({}, this.state.errors, rejection),\n })\n snackbar.error(gettext(\"Form contains errors.\"))\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n onCategoryChange = (ev) => {\n const categoryId = ev.target.value\n const newState = {\n category: categoryId,\n }\n\n if (this.acl[categoryId].can_pin_threads < newState.weight) {\n newState.weight = 0\n }\n\n if (!this.acl[categoryId].can_hide_threads) {\n newState.is_hidden = 0\n }\n\n if (!this.acl[categoryId].can_close_threads) {\n newState.is_closed = false\n }\n\n this.setState(newState)\n }\n\n getWeightChoices() {\n const choices = [\n {\n value: 0,\n icon: \"remove\",\n label: pgettext(\"thread weight choice\", \"Not pinned\"),\n },\n {\n value: 1,\n icon: \"bookmark_border\",\n label: pgettext(\"thread weight choice\", \"Pinned locally\"),\n },\n ]\n\n if (this.acl[this.state.category].can_pin_threads == 2) {\n choices.push({\n value: 2,\n icon: \"bookmark\",\n label: pgettext(\"thread weight choice\", \"Pinned globally\"),\n })\n }\n\n return choices\n }\n\n renderWeightField() {\n if (this.acl[this.state.category].can_pin_threads) {\n return (\n \n \n \n )\n } else {\n return null\n }\n }\n\n renderHiddenField() {\n if (this.acl[this.state.category].can_hide_threads) {\n return (\n \n \n \n )\n } else {\n return null\n }\n }\n\n renderClosedField() {\n if (this.acl[this.state.category].can_close_threads) {\n return (\n \n \n \n )\n } else {\n return null\n }\n }\n\n render() {\n return (\n \n
    \n
    \n \n \n \n
    \n\n \n \n \n
    \n\n {this.renderWeightField()}\n {this.renderHiddenField()}\n {this.renderClosedField()}\n
    \n
    \n \n
    \n \n \n )\n }\n}\n\nexport function Loader() {\n return (\n \n \n \n )\n}\n\nexport function Error(props) {\n return (\n \n
    \n info_outline\n
    \n
    \n

    \n {pgettext(\n \"post split modal\",\n \"You can't move this post at the moment.\"\n )}\n

    \n

    {props.message}

    \n
    \n
    \n )\n}\n\nexport function Modal(props) {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"posts split modal title\", \"Split post into new thread\")}\n

    \n
    \n {props.children}\n
    \n
    \n )\n}\n","import React from \"react\"\nimport modal from \"misago/services/modal\"\nimport posting from \"misago/services/posting\"\nimport * as moderation from \"./actions\"\nimport MoveModal from \"./move\"\nimport PostChangelog from \"misago/components/post-changelog\"\nimport SplitModal from \"./split\"\n\nexport default function (props) {\n return (\n
      \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    \n )\n}\n\nexport class Permalink extends React.Component {\n onClick = () => {\n let permaUrl = window.location.protocol + \"//\"\n permaUrl += window.location.host\n permaUrl += this.props.post.url.index\n\n prompt(pgettext(\"post permalink\", \"Permament link to this post:\"), permaUrl)\n }\n\n render() {\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Edit extends React.Component {\n onClick = () => {\n posting.open({\n mode: \"EDIT\",\n\n thread: this.props.thread,\n post: this.props.post,\n config: this.props.post.api.editor,\n submit: this.props.post.api.index,\n })\n }\n\n render() {\n if (!this.props.post.acl.can_edit) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class MarkAsBestAnswer extends React.Component {\n onClick = () => {\n moderation.markAsBestAnswer(this.props)\n }\n\n render() {\n const { post, thread } = this.props\n\n if (!thread.acl.can_mark_best_answer) return null\n if (!post.acl.can_mark_as_best_answer) return null\n if (post.id === thread.best_answer) return null\n if (thread.best_answer && !thread.acl.can_change_best_answer) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class UnmarkMarkBestAnswer extends React.Component {\n onClick = () => {\n moderation.unmarkBestAnswer(this.props)\n }\n\n render() {\n const { post, thread } = this.props\n\n if (post.id !== thread.best_answer) return null\n if (!thread.acl.can_unmark_best_answer) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class PostEdits extends React.Component {\n onClick = () => {\n modal.show()\n }\n\n render() {\n const isHidden =\n this.props.post.is_hidden && !this.props.post.acl.can_see_hidden\n const isUnedited = this.props.post.edits === 0\n if (isHidden || isUnedited) return null\n\n const message = npgettext(\n \"post edits\",\n \"This post was edited %(edits)s time.\",\n \"This post was edited %(edits)s times.\",\n this.props.post.edits\n )\n\n const title = interpolate(\n message,\n {\n edits: this.props.post.edits,\n },\n true\n )\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Approve extends React.Component {\n onClick = () => {\n moderation.approve(this.props)\n }\n\n render() {\n if (!this.props.post.acl.can_approve) return null\n if (!this.props.post.is_unapproved) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Move extends React.Component {\n onClick = () => {\n modal.show()\n }\n\n render() {\n if (!this.props.post.acl.can_move) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Split extends React.Component {\n onClick = () => {\n modal.show()\n }\n\n render() {\n if (!this.props.post.acl.can_move) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Protect extends React.Component {\n onClick = () => {\n moderation.protect(this.props)\n }\n\n render() {\n if (!this.props.post.acl.can_protect) return null\n if (this.props.post.is_protected) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Unprotect extends React.Component {\n onClick = () => {\n moderation.unprotect(this.props)\n }\n\n render() {\n if (!this.props.post.acl.can_protect) return null\n if (!this.props.post.is_protected) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Hide extends React.Component {\n onClick = () => {\n moderation.hide(this.props)\n }\n\n render() {\n const { post, thread } = this.props\n\n if (post.id === thread.best_answer) return null\n if (!post.acl.can_hide) return null\n if (post.is_hidden) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Unhide extends React.Component {\n onClick = () => {\n moderation.unhide(this.props)\n }\n\n render() {\n if (!this.props.post.acl.can_unhide) return null\n if (!this.props.post.is_hidden) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Delete extends React.Component {\n onClick = () => {\n moderation.remove(this.props)\n }\n\n render() {\n const { post, thread } = this.props\n\n if (post.id === thread.best_answer) return null\n if (!post.acl.can_delete) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n","import React from \"react\"\nimport Dropdown from \"./dropdown\"\n\nexport default function (props) {\n return (\n
    \n \n expand_more\n \n \n
    \n )\n}\n","import React from \"react\"\nimport * as posts from \"misago/reducers/posts\"\nimport store from \"misago/services/store\"\n\nexport default class extends React.Component {\n onClick = () => {\n if (this.props.post.isSelected) {\n store.dispatch(posts.deselect(this.props.post))\n } else {\n store.dispatch(posts.select(this.props.post))\n }\n }\n\n render() {\n if (\n !(this.props.thread.acl.can_merge_posts || isVisible(this.props.post.acl))\n ) {\n return null\n }\n\n return (\n
    \n \n \n {this.props.post.isSelected\n ? \"check_box\"\n : \"check_box_outline_blank\"}\n \n \n
    \n )\n }\n}\n\nexport function isVisible(acl) {\n return (\n acl.can_approve ||\n acl.can_hide ||\n acl.can_protect ||\n acl.can_unhide ||\n acl.can_delete ||\n acl.can_move\n )\n}\n","import React from \"react\"\nimport Controls from \"./controls\"\nimport Select from \"./select\"\nimport {\n StatusIcon,\n getStatusClassName,\n getStatusDescription,\n} from \"misago/components/user-status\"\nimport PostChangelog from \"misago/components/post-changelog\"\nimport modal from \"misago/services/modal\"\n\nexport default function (props) {\n return (\n
    \n \n \n \n \n \n \n \n \n \n
    \n
    \n \n \n \n
    \n
    \n {post.poster_name}\n\n \n {pgettext(\"post removed poster username\", \"Removed user\")}\n \n
    \n
    \n
    \n )\n}\n","export default function ({ title, rank }) {\n return rank.is_tab || !!title || !!rank.title\n}\n","import React from \"react\"\nimport hasVisibleTitle from \"./has-visible-title\"\n\nexport default function ({ poster }) {\n const message = npgettext(\n \"poster stats\",\n \"%(posts)s post\",\n \"%(posts)s posts\",\n poster.posts\n )\n\n let className = \"user-postcount\"\n if (hasVisibleTitle(poster)) {\n className += \" hidden-xs hidden-sm\"\n }\n\n return (\n \n {interpolate(\n message,\n {\n posts: poster.posts,\n },\n true\n )}\n \n )\n}\n","import React from \"react\"\nimport UserStatus, { StatusLabel } from \"misago/components/user-status\"\nimport hasVisibleTitle from \"./has-visible-title\"\n\nexport default function ({ poster }) {\n let className = \"hidden-xs\"\n if (hasVisibleTitle(poster)) {\n className += \" hidden-sm\"\n }\n\n return (\n \n \n \n \n \n )\n}\n","import React from \"react\"\n\nexport default function ({ rank, title }) {\n let userTitle = title || rank.title\n if (!userTitle && rank.is_tab) {\n userTitle = rank.name\n }\n\n if (!userTitle) return null\n\n let className = \"user-title\"\n if (rank.css_class) {\n className += \" user-title-\" + rank.css_class\n }\n\n if (rank.is_tab) {\n return (\n \n )\n }\n\n return
    {userTitle}
    \n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport Controls from \"misago/components/posts-list/post/controls\"\nimport Select from \"misago/components/posts-list/post/select\"\nimport UserStatus, { StatusIcon } from \"misago/components/user-status\"\nimport UserPostcount from \"./user-postcount\"\nimport UserStatusLabel from \"./user-status\"\nimport UserTitle from \"./user-title\"\n\nexport default function ({ post, thread }) {\n const { poster } = post\n\n return (\n
    \n \n \n
    \n \n\n \n \n \n\n \n \n \n\n \n }\n >\n \n \n\n {captcha.component({\n form: this,\n })}\n\n \n
    \n
    \n \n {pgettext(\"register modal btn\", \"Cancel\")}\n \n \n
    \n \n
    \n
    \n )\n }\n}\n\nexport class RegisterComplete extends React.Component {\n getLead() {\n if (this.props.activation === \"user\") {\n return pgettext(\n \"account activation required\",\n \"%(username)s, your account has been created but you need to activate it before you will be able to sign in.\"\n )\n } else if (this.props.activation === \"admin\") {\n return pgettext(\n \"account activation required\",\n \"%(username)s, your account has been created but the site administrator will have to activate it before you will be able to sign in.\"\n )\n }\n }\n\n getSubscript() {\n if (this.props.activation === \"user\") {\n return pgettext(\n \"account activation required\",\n \"We have sent an e-mail to %(email)s with link that you have to click to activate your account.\"\n )\n } else if (this.props.activation === \"admin\") {\n return pgettext(\n \"account activation required\",\n \"We will send an e-mail to %(email)s when this takes place.\"\n )\n }\n }\n\n render() {\n return (\n \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"register modal title\", \"Registration complete\")}\n

    \n
    \n
    \n
    \n info_outline\n
    \n
    \n

    \n {interpolate(\n this.getLead(),\n { username: this.props.username },\n true\n )}\n

    \n

    \n {interpolate(\n this.getSubscript(),\n { email: this.props.email },\n true\n )}\n

    \n \n {pgettext(\"register modal dismiss\", \"Ok\")}\n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n complete: false,\n }\n }\n\n completeRegistration = (apiResponse) => {\n if (apiResponse.activation === \"active\") {\n modal.hide()\n auth.signIn(apiResponse)\n } else {\n this.setState({\n complete: apiResponse,\n })\n }\n }\n\n render() {\n if (this.state.complete) {\n return (\n \n )\n }\n\n return \n }\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport ajax from \"../../services/ajax\"\nimport captcha from \"../../services/captcha\"\nimport modal from \"../../services/modal\"\nimport snackbar from \"../../services/snackbar\"\nimport Loader from \"../loader\"\nimport RegisterForm from \"../register.js\"\n\nexport default class RegisterButton extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n isLoaded: false,\n\n criteria: null,\n }\n }\n\n showRegisterForm = () => {\n if (this.props.onClick) {\n this.props.onClick()\n }\n\n if (misago.get(\"SETTINGS\").account_activation === \"closed\") {\n snackbar.info(\n pgettext(\n \"register form\",\n \"Registration form is currently disabled by the site administrator.\"\n )\n )\n } else if (this.state.isLoaded) {\n modal.show()\n } else {\n this.setState({ isLoading: true })\n\n Promise.all([\n captcha.load(),\n ajax.get(misago.get(\"AUTH_CRITERIA_API\")),\n ]).then(\n (result) => {\n this.setState({\n isLoading: false,\n isLoaded: true,\n criteria: result[1],\n })\n\n modal.show()\n },\n () => {\n this.setState({ isLoading: false })\n\n snackbar.error(\n pgettext(\n \"register form\",\n \"Registration form is currently unavailable due to an error.\"\n )\n )\n }\n )\n }\n }\n\n render() {\n return (\n \n {pgettext(\"cta\", \"Register\")}\n {this.state.isLoading ? : null}\n \n )\n }\n}\n","import RegisterButton from \"./RegisterButton\"\n\nexport default RegisterButton\n","import React from \"react\"\nimport misago from \"misago\"\nimport escapeHtml from \"misago/utils/escape-html\"\n\nconst AGREEMENT_URL = '%(agreement)s'\n\nconst RegisterLegalFootnote = (props) => {\n const {\n errors,\n privacyPolicy,\n termsOfService,\n onPrivacyPolicyChange,\n onTermsOfServiceChange,\n } = props\n\n const termsOfServiceId = misago.get(\"TERMS_OF_SERVICE_ID\")\n const termsOfServiceUrl = misago.get(\"TERMS_OF_SERVICE_URL\")\n\n const privacyPolicyId = misago.get(\"PRIVACY_POLICY_ID\")\n const privacyPolicyUrl = misago.get(\"PRIVACY_POLICY_URL\")\n\n if (!termsOfServiceId && !privacyPolicyId) return null\n\n return (\n
    \n \n \n
    \n )\n}\n\nconst LegalAgreement = (props) => {\n const { agreement, checked, errors, url, value, onChange } = props\n\n if (!url) return null\n\n const agreementHtml = interpolate(\n AGREEMENT_URL,\n { agreement: escapeHtml(agreement), url: escapeHtml(url) },\n true\n )\n const label = interpolate(\n pgettext(\n \"register form agreement prompt\",\n \"I have read and accept %(agreement)s.\"\n ),\n { agreement: agreementHtml },\n true\n )\n\n return (\n
    \n \n {errors &&\n errors.map((error, i) => (\n
    \n {error}\n
    \n ))}\n
    \n )\n}\n\nexport default RegisterLegalFootnote\n","import React from \"react\"\nimport { ListGroup } from \"../ListGroup\"\n\nexport default function SearchResultsList({ children }) {\n return {children}\n}\n","import React from \"react\"\nimport { ListGroupMessage } from \"../ListGroup\"\nimport SearchResultsList from \"./SearchResultsList\"\n\nexport default function SearchMessage() {\n return (\n \n \n \n )\n}\n","import React from \"react\"\nimport { ListGroupItem } from \"../ListGroup\"\nimport Timestamp from \"../Timestamp\"\n\nexport default function SearchResultPost({ post }) {\n return (\n \n \n
    \n
    {post.thread.title}
    \n \n
      \n
    • \n {post.category.name}\n
    • \n
    • {post.poster ? post.poster.username : post.poster_name}
    • \n
    • \n \n
    • \n
    \n
    \n
    \n
    \n )\n}\n","import React from \"react\"\nimport Avatar from \"../avatar\"\nimport { ListGroupItem } from \"../ListGroup\"\nimport Timestamp from \"../Timestamp\"\n\nexport default function SearchResultUser({ user }) {\n const title = user.title || user.rank.title\n\n return (\n \n \n \n
    \n
    {user.username}
    \n
      \n {!!title && (\n
    • \n {title}\n
    • \n )}\n
    • {user.rank.name}
    • \n
    • \n \n
    • \n
    \n
    \n
    \n
    \n )\n}\n","import React from \"react\"\nimport { ListGroupItem } from \"../ListGroup\"\nimport SearchResultsList from \"./SearchResultsList\"\nimport SearchResultPost from \"./SearchResultPost\"\nimport SearchResultUser from \"./SearchResultUser\"\n\nexport default function SearchResults({ query, results }) {\n const threads = results[0]\n const users = results[1]\n\n const { count } = threads.results\n\n return (\n \n {users.results.results.map((user) => (\n \n ))}\n {threads.results.results.map((post) => (\n \n ))}\n {count > 0 && (\n \n \n {npgettext(\n \"search results list\",\n \"See all %(count)s result.\",\n \"See all %(count)s results.\",\n threads.results.count\n ).replace(\"%(count)s\", threads.results.count)}\n \n \n )}\n \n )\n}\n","import React from \"react\"\nimport { ListGroupEmpty } from \"../ListGroup\"\nimport SearchResultsList from \"./SearchResultsList\"\n\nexport default function SearchResultsEmpty() {\n return (\n \n \n \n )\n}\n","import React from \"react\"\nimport { ListGroupError } from \"../ListGroup\"\nimport SearchResultsList from \"./SearchResultsList\"\n\nexport default function SearchResultsError({ error }) {\n return (\n \n \n \n )\n}\n\nfunction errorDetail(error) {\n if (error.status === 0) {\n return gettext(\n \"Check your internet connection and try refreshing the site.\"\n )\n }\n\n if (error.data && error.data.detail) {\n return error.data.detail\n }\n}\n","import React from \"react\"\nimport { ListGroupLoading } from \"../ListGroup\"\nimport SearchResultsList from \"./SearchResultsList\"\n\nexport default function SearchResultsLoading() {\n return (\n \n \n \n )\n}\n","import React from \"react\"\nimport { ApiFetch } from \"../Api\"\nimport SearchMessage from \"./SearchMessage\"\nimport SearchResults from \"./SearchResults\"\nimport SearchResultsEmpty from \"./SearchResultsEmpty\"\nimport SearchResultsError from \"./SearchResultsError\"\nimport SearchResultsLoading from \"./SearchResultsLoading\"\n\nconst DEBOUNCE = 750\nconst CACHE = {}\n\nexport default class SearchFetch extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n query: this.props.query.trim(),\n }\n\n this.debounce = null\n }\n\n componentDidUpdate() {\n const query = this.props.query.trim()\n\n if (this.state.query != query) {\n if (this.debounce) {\n window.clearTimeout(this.debounce)\n }\n\n this.debounce = window.setTimeout(() => {\n this.setState({ query })\n }, DEBOUNCE)\n }\n }\n\n componentWillUnmount() {\n if (this.debounce) {\n window.clearTimeout(this.debounce)\n }\n }\n\n render() {\n return (\n \n {({ data, loading, error }) => {\n if (this.state.query.length < 3) {\n return \n }\n\n if (loading) {\n return \n }\n\n if (error) {\n return \n }\n\n if (isResultEmpty(data)) {\n return \n }\n\n if (data !== null) {\n return \n }\n\n return null\n }}\n \n )\n }\n}\n\nfunction getSearchUrl(query) {\n return misago.get(\"SEARCH_API\") + \"?q=\" + encodeURIComponent(query)\n}\n\nfunction isResultEmpty(results) {\n if (results === null) {\n return true\n }\n\n let resultsCount = 0\n results.forEach((result) => {\n resultsCount += result.results.count\n })\n return resultsCount === 0\n}\n","import React from \"react\"\n\nexport default function SearchInput({ query, setQuery }) {\n return (\n
    \n setQuery(event.target.value)}\n />\n
    \n )\n}\n","import React from \"react\"\n\nexport default class SearchQuery extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n query: \"\",\n }\n }\n\n setQuery = (query) => {\n this.setState({ query })\n }\n\n render() {\n return this.props.children({\n query: this.state.query,\n setQuery: this.setQuery,\n })\n }\n}\n","import React from \"react\"\nimport SearchFetch from \"./SearchFetch\"\nimport SearchInput from \"./SearchInput\"\nimport SearchQuery from \"./SearchQuery\"\n\nexport default function SearchDropdown() {\n return (\n \n {({ query, setQuery }) => {\n return (\n
    \n \n \n
    \n )\n }}\n
    \n )\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport { Overlay, OverlayHeader } from \"../Overlay\"\nimport SearchFetch from \"./SearchFetch\"\nimport SearchInput from \"./SearchInput\"\nimport SearchQuery from \"./SearchQuery\"\n\nfunction SearchOverlay({ open }) {\n return (\n {\n window.setTimeout(() => {\n document.querySelector(\"#search-mount .form-control-search\").focus()\n }, 0)\n }}\n >\n {pgettext(\"cta\", \"Search\")}\n \n {({ query, setQuery }) => {\n return (\n
    \n \n
    \n \n
    \n
    \n )\n }}\n
    \n \n )\n}\n\nfunction select(state) {\n return { open: state.overlay.search }\n}\n\nconst SearchOverlayConnected = connect(select)(SearchOverlay)\n\nexport default SearchOverlayConnected\n","import SignInButton from \"./SignInButton\"\n\nexport default SignInButton\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport modal from \"../../services/modal\"\nimport SignInModal from \"../sign-in\"\n\nexport default function SignInButton({ block, className, onClick }) {\n const settings = misago.get(\"SETTINGS\")\n\n if (settings.DELEGATE_AUTH) {\n return (\n \n {pgettext(\"cta\", \"Sign in\")}\n \n )\n }\n\n return (\n {\n if (onClick) {\n onClick()\n }\n\n modal.show()\n }}\n >\n {pgettext(\"cta\", \"Sign in\")}\n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { connect } from \"react-redux\"\nimport {\n DropdownDivider,\n DropdownHeader,\n DropdownMenuItem,\n DropdownPills,\n DropdownSubheader,\n} from \"../Dropdown\"\nimport RegisterButton from \"../RegisterButton\"\nimport SignInButton from \"../SignInButton\"\n\nfunction SiteNavMenu({ isAnonymous, close, dropdown, overlay }) {\n const baseUrl = misago.get(\"MISAGO_PATH\")\n const settings = misago.get(\"SETTINGS\")\n const extraItems = misago.get(\"extraMenuItems\")\n const extraFooterItems = misago.get(\"extraFooterItems\")\n const categories = misago.get(\"categoriesMap\")\n const users = misago.get(\"usersLists\")\n const authDelegated = settings.enable_oauth2_client\n\n const topNav = []\n if (misago.get(\"THREADS_ON_INDEX\")) {\n topNav.push({ title: pgettext(\"site nav\", \"Threads\"), url: baseUrl })\n topNav.push({\n title: pgettext(\"site nav\", \"Categories\"),\n url: baseUrl + \"categories/\",\n })\n } else {\n topNav.push({ title: pgettext(\"site nav\", \"Categories\"), url: baseUrl })\n topNav.push({\n title: pgettext(\"site nav\", \"Threads\"),\n url: baseUrl + \"threads/\",\n })\n }\n\n topNav.push({\n title: pgettext(\"site nav\", \"Search\"),\n url: baseUrl + \"search/\",\n })\n\n const footerNav = []\n\n const tosTitle = misago.get(\"TERMS_OF_SERVICE_TITLE\")\n const tosUrl = misago.get(\"TERMS_OF_SERVICE_URL\")\n if (tosTitle && tosUrl) {\n footerNav.push({\n title: tosTitle,\n url: tosUrl,\n })\n }\n\n const privacyTitle = misago.get(\"PRIVACY_POLICY_TITLE\")\n const privacyUrl = misago.get(\"PRIVACY_POLICY_URL\")\n if (privacyTitle && privacyUrl) {\n footerNav.push({\n title: privacyTitle,\n url: privacyUrl,\n })\n }\n\n return (\n \n {isAnonymous && (\n \n {pgettext(\"cta\", \"You are not signed in\")}\n \n )}\n {isAnonymous && (\n \n \n {!authDelegated && }\n \n )}\n {settings.forum_name}\n {topNav.map((item) => (\n \n {item.title}\n \n ))}\n {extraItems.map((item, index) => (\n \n \n {item.title}\n \n \n ))}\n {!!users.length && }\n {!!users.length && (\n \n {pgettext(\"site nav section\", \"Users\")}\n \n )}\n {users.map((item) => (\n \n {item.name}\n \n ))}\n \n \n {pgettext(\"site nav section\", \"Categories\")}\n \n {categories.map((category) => (\n \n \n {category.name}\n \n {category.shortName || category.name}\n \n \n \n ))}\n {(!!footerNav.length || !!extraFooterItems.length) && (\n \n )}\n {(!!footerNav.length || !!extraFooterItems.length) && (\n \n {pgettext(\"site nav section\", \"Footer\")}\n \n )}\n {extraFooterItems.map((item, index) => (\n \n \n {item.title}\n \n \n ))}\n {footerNav.map((item) => (\n \n {item.title}\n \n ))}\n \n )\n}\n\nfunction select(state) {\n return {\n isAnonymous: !state.auth.user.id,\n }\n}\n\nconst SiteNavMenuConnected = connect(select)(SiteNavMenu)\n\nexport default SiteNavMenuConnected\n","import React from \"react\"\nimport SiteNavMenu from \"./SiteNavMenu\"\n\nexport default function SiteNavDropdown({ close }) {\n return \n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport { close } from \"../../reducers/overlay\"\nimport { Overlay, OverlayHeader } from \"../Overlay\"\nimport SiteNavMenu from \"./SiteNavMenu\"\n\nexport function SiteNavOverlay({ dispatch, isOpen }) {\n return (\n \n {pgettext(\"site nav title\", \"Menu\")}\n dispatch(close())} overlay />\n \n )\n}\n\nfunction select(state) {\n return {\n isOpen: state.overlay.siteNav,\n }\n}\n\nconst SiteNavOverlayConnected = connect(select)(SiteNavOverlay)\n\nexport default SiteNavOverlayConnected\n","import React from \"react\"\nimport misago from \"misago\"\n\nconst StartSocialAuth = (props) => {\n const { buttonClassName, buttonLabel, formLabel, header, labelClassName } =\n props\n const socialAuth = misago.get(\"SOCIAL_AUTH\")\n\n if (socialAuth.length === 0) return null\n\n return (\n
    \n \n
    \n {socialAuth.map(({ pk, name, button_text, button_color, url }) => {\n const className = \"btn btn-block btn-default btn-social-\" + pk\n const style = button_color ? { color: button_color } : null\n const finalButtonLabel =\n button_text || interpolate(buttonLabel, { site: name }, true)\n\n return (\n \n )\n })}\n
    \n
    \n \n
    \n )\n}\n\nconst FormHeader = ({ className, text }) => {\n if (!text) return null\n return
    {text}
    \n}\n\nexport default StartSocialAuth\n","import React from \"react\"\n\nconst ThreadFlags = ({ thread }) => (\n
      \n {thread.weight == 2 && (\n \n bookmark\n \n )}\n {thread.weight == 1 && (\n \n bookmark_outline\n \n )}\n {thread.best_answer && (\n \n check_circle\n \n )}\n {thread.has_poll && (\n
    • \n poll\n
    • \n )}\n {(thread.is_unapproved || thread.has_unapproved_posts) && (\n \n visibility\n \n )}\n {thread.is_closed && (\n \n lock\n \n )}\n {thread.is_hidden && (\n \n visibility_off\n \n )}\n
    \n)\n\nexport default ThreadFlags\n","import React from \"react\"\n\nconst ThreadReplies = ({ thread }) => (\n \n chat_bubble_outline\n {thread.replies > 980\n ? Math.round(thread.replies / 1000) + \"K\"\n : thread.replies}\n \n)\n\nexport default ThreadReplies\n","export const locale = window.misago_locale || \"en-us\"\n\nexport const momentAgo = pgettext(\"time ago\", \"moment ago\")\nexport const momentAgoNarrow = pgettext(\"time ago\", \"now\")\nexport const dayAt = pgettext(\"day at time\", \"%(day)s at %(time)s\")\nexport const tomorrowAt = pgettext(\"day at time\", \"Tomorrow at %(time)s\")\nexport const yesterdayAt = pgettext(\"day at time\", \"Yesterday at %(time)s\")\n\nexport const minuteCompact = pgettext(\"short minutes\", \"%(time)sm\")\nexport const hourCompact = pgettext(\"short hours\", \"%(time)sh\")\nexport const dayCompact = pgettext(\"short days\", \"%(time)sd\")\n\nexport const relativeNumeric = new Intl.RelativeTimeFormat(locale, {\n numeric: \"always\",\n style: \"long\",\n})\n\nexport const relativeAuto = new Intl.RelativeTimeFormat(locale, {\n numeric: \"auto\",\n style: \"long\",\n})\n\nexport const fullDateTime = new Intl.DateTimeFormat(locale, {\n dateStyle: \"full\",\n timeStyle: \"medium\",\n})\n\nexport const thisYearDate = new Intl.DateTimeFormat(locale, {\n month: \"long\",\n day: \"numeric\",\n})\n\nexport const thisYearDateNarrow = new Intl.DateTimeFormat(locale, {\n month: \"short\",\n day: \"numeric\",\n})\n\nexport const otherYearDate = new Intl.DateTimeFormat(locale, {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n})\n\nexport const otherYearDateNarrow = new Intl.DateTimeFormat(locale, {\n year: \"2-digit\",\n month: \"short\",\n})\n\nexport const weekday = new Intl.DateTimeFormat(locale, {\n weekday: \"long\",\n})\n\nexport const shortTime = new Intl.DateTimeFormat(locale, { timeStyle: \"short\" })\n\nexport function formatNarrow(date) {\n const now = new Date()\n const absDiff = Math.abs(Math.round((date - now) / 1000))\n\n if (absDiff < 60) {\n return momentAgoNarrow\n }\n\n if (absDiff < 60 * 55) {\n const minutes = Math.ceil(absDiff / 60)\n return minuteCompact.replace(\"%(time)s\", minutes)\n }\n\n if (absDiff < 3600 * 24) {\n const hours = Math.ceil(absDiff / 3600)\n return hourCompact.replace(\"%(time)s\", hours)\n }\n\n if (absDiff < 86400 * 7) {\n const days = Math.ceil(absDiff / 86400)\n return dayCompact.replace(\"%(time)s\", days)\n }\n\n if (date.getFullYear() === now.getFullYear()) {\n return thisYearDateNarrow.format(date)\n }\n\n return otherYearDateNarrow.format(date)\n}\n\nexport function formatRelative(date) {\n const now = new Date()\n const diff = Math.round((date - now) / 1000)\n const absDiff = Math.abs(diff)\n const sign = diff < 1 ? -1 : 1\n\n if (absDiff < 90) {\n return momentAgo\n }\n\n if (absDiff < 60 * 47) {\n const minutes = Math.ceil(absDiff / 60) * sign\n return relativeNumeric.format(minutes, \"minute\")\n }\n\n if (absDiff < 3600 * 3) {\n const hours = Math.ceil(absDiff / 3600) * sign\n return relativeNumeric.format(hours, \"hour\")\n }\n\n if (isSameDay(now, date)) {\n return shortTime.format(date)\n }\n\n if (isYesterday(date)) {\n return yesterdayAt.replace(\"%(time)s\", shortTime.format(date))\n }\n\n if (isTomorrow(date)) {\n return tomorrowAt.replace(\"%(time)s\", shortTime.format(date))\n }\n\n if (diff < 0 && absDiff < 3600 * 24 * 6) {\n const day = weekday.format(date)\n return formatDayAtTime(day, date)\n }\n\n if (now.getFullYear() == date.getFullYear()) {\n return thisYearDate.format(date)\n }\n\n return otherYearDate.format(date)\n}\n\nexport function isSameDay(now, date) {\n return (\n now.getFullYear() == date.getFullYear() &&\n now.getMonth() == date.getMonth() &&\n now.getDate() == date.getDate()\n )\n}\n\nexport function isYesterday(date) {\n const yesterday = new Date()\n yesterday.setDate(yesterday.getDate() - 1)\n return isSameDay(yesterday, date)\n}\n\nexport function isTomorrow(date) {\n const yesterday = new Date()\n yesterday.setDate(yesterday.getDate() + 1)\n return isSameDay(yesterday, date)\n}\n\nexport function formatDayAtTime(day, date) {\n return dayAt\n .replace(\"%(day)s\", day)\n .replace(\"%(time)s\", shortTime.format(date))\n}\n","import React from \"react\"\nimport {\n formatNarrow,\n formatRelative,\n fullDateTime,\n} from \"../../datetimeFormats\"\n\nclass Timestamp extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = { tick: 0 }\n this.date = new Date(props.datetime)\n this.timeout = null\n }\n\n componentDidMount() {\n this.scheduleNextUpdate()\n }\n\n componentWillUnmount() {\n if (this.timeout) {\n window.clearTimeout(this.timeout)\n }\n }\n\n scheduleNextUpdate = () => {\n const now = new Date()\n const diff = Math.ceil(Math.abs(Math.round((this.date - now) / 1000)))\n\n if (diff < 3600) {\n this.timeout = window.setTimeout(\n () => {\n this.setState(tick)\n this.scheduleNextUpdate()\n },\n 50 * 1000 // Update every 50 seconds\n )\n } else if (diff < 3600 * 24) {\n this.timeout = window.setTimeout(\n () => {\n this.setState(tick)\n },\n 40 * 60 * 1000 // Update every 40 minutes\n )\n }\n }\n\n render() {\n const displayed = this.props.narrow\n ? formatNarrow(this.date)\n : formatRelative(this.date)\n\n return (\n \n {displayed}\n \n )\n }\n}\n\nfunction tick(state) {\n return { tick: state.tick + 1 }\n}\n\nexport default Timestamp\n","import Timestamp from \"./Timestamp\"\n\nexport default Timestamp\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst Toolbar = ({ children, className }) => (\n \n)\n\nexport default Toolbar\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst ToolbarItem = ({ children, className, shrink }) => (\n \n {children}\n \n)\n\nexport default ToolbarItem\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst ToolbarSection = ({ auto, children, className }) => (\n \n {children}\n \n)\n\nexport default ToolbarSection\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nconst ToolbarSpacer = ({ className }) => (\n
    \n)\n\nexport default ToolbarSpacer\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport Button from \"misago/components/button\"\nimport Loader from \"misago/components/loader\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n }\n }\n\n callApi(avatarType) {\n if (this.state.isLoading) {\n return false\n }\n\n this.setState({\n isLoading: true,\n })\n\n ajax\n .post(this.props.user.api.avatar, {\n avatar: avatarType,\n })\n .then(\n (response) => {\n this.setState({\n isLoading: false,\n })\n\n snackbar.success(response.detail)\n this.props.onComplete(response)\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n this.setState({\n isLoading: false,\n })\n } else {\n this.props.showError(rejection)\n }\n }\n )\n }\n\n setGravatar = () => {\n this.callApi(\"gravatar\")\n }\n\n setGenerated = () => {\n this.callApi(\"generated\")\n }\n\n getGravatarButton() {\n if (this.props.options.gravatar) {\n return (\n \n {pgettext(\"avatar modal btn\", \"Download my Gravatar\")}\n \n )\n } else {\n return null\n }\n }\n\n getCropButton() {\n if (!this.props.options.crop_src) return null\n\n return (\n \n {pgettext(\"avatar modal btn\", \"Re-crop uploaded image\")}\n \n )\n }\n\n getUploadButton() {\n if (!this.props.options.upload) return null\n\n return (\n \n {pgettext(\"avatar modal btn\", \"Upload new image\")}\n \n )\n }\n\n getGalleryButton() {\n if (!this.props.options.galleries) return null\n\n return (\n \n {pgettext(\"avatar modal btn\", \"Pick avatar from gallery\")}\n \n )\n }\n\n getAvatarPreview() {\n let userPeview = {\n id: this.props.user.id,\n avatars: this.props.options.avatars,\n }\n\n if (this.state.isLoading) {\n return (\n
    \n \n \n
    \n )\n }\n\n return (\n
    \n \n
    \n )\n }\n\n render() {\n return (\n
    \n
    \n
    {this.getAvatarPreview()}
    \n
    \n {this.getGravatarButton()}\n\n \n {pgettext(\"avatar modal btn\", \"Generate my individual avatar\")}\n \n\n {this.getCropButton()}\n {this.getUploadButton()}\n {this.getGalleryButton()}\n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport Button from \"misago/components/button\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n deviceRatio: 1,\n }\n }\n\n getAvatarSize() {\n if (this.props.upload) {\n return this.props.options.crop_tmp.size\n } else {\n return this.props.options.crop_src.size\n }\n }\n\n getImagePath() {\n if (this.props.upload) {\n return this.props.dataUrl\n } else {\n return this.props.options.crop_src.url\n }\n }\n\n componentDidMount() {\n let cropit = $(\".crop-form\")\n let cropperWidth = this.getAvatarSize()\n\n const initialWidth = cropit.width()\n while (initialWidth < cropperWidth) {\n cropperWidth = cropperWidth / 2\n }\n\n const deviceRatio = this.getAvatarSize() / cropperWidth\n\n cropit.width(cropperWidth)\n\n cropit.cropit({\n width: cropperWidth,\n height: cropperWidth,\n exportZoom: deviceRatio,\n imageState: {\n src: this.getImagePath(),\n },\n onImageLoaded: () => {\n if (this.props.upload) {\n // center uploaded image\n let zoomLevel = cropit.cropit(\"zoom\")\n let imageSize = cropit.cropit(\"imageSize\")\n\n // is it wider than taller?\n if (imageSize.width > imageSize.height) {\n let displayedWidth = imageSize.width * zoomLevel\n let offsetX = (displayedWidth - this.getAvatarSize()) / -2\n\n cropit.cropit(\"offset\", {\n x: offsetX,\n y: 0,\n })\n } else if (imageSize.width < imageSize.height) {\n let displayedHeight = imageSize.height * zoomLevel\n let offsetY = (displayedHeight - this.getAvatarSize()) / -2\n\n cropit.cropit(\"offset\", {\n x: 0,\n y: offsetY,\n })\n } else {\n cropit.cropit(\"offset\", {\n x: 0,\n y: 0,\n })\n }\n } else {\n // use preserved crop\n let crop = this.props.options.crop_src.crop\n\n if (crop) {\n cropit.cropit(\"zoom\", crop.zoom)\n cropit.cropit(\"offset\", {\n x: crop.x,\n y: crop.y,\n })\n }\n }\n },\n })\n }\n\n componentWillUnmount() {\n $(\".crop-form\").cropit(\"disable\")\n }\n\n cropAvatar = () => {\n if (this.state.isLoading) {\n return false\n }\n\n this.setState({\n isLoading: true,\n })\n\n let avatarType = this.props.upload ? \"crop_tmp\" : \"crop_src\"\n let cropit = $(\".crop-form\")\n\n const deviceRatio = cropit.cropit(\"exportZoom\")\n const cropitOffset = cropit.cropit(\"offset\")\n\n ajax\n .post(this.props.user.api.avatar, {\n avatar: avatarType,\n crop: {\n offset: {\n x: cropitOffset.x * deviceRatio,\n y: cropitOffset.y * deviceRatio,\n },\n zoom: cropit.cropit(\"zoom\") * deviceRatio,\n },\n })\n .then(\n (data) => {\n this.props.onComplete(data)\n snackbar.success(data.detail)\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n this.setState({\n isLoading: false,\n })\n } else {\n this.props.showError(rejection)\n }\n }\n )\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n \n
    \n
    \n
    \n
    \n \n {this.props.upload\n ? pgettext(\"avatar crop modal btn\", \"Set avatar\")\n : pgettext(\"avatar crop modal btn\", \"Crop image\")}\n \n\n \n {pgettext(\"avatar crop modal btn\", \"Cancel\")}\n \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport AvatarCrop from \"misago/components/change-avatar/crop\"\nimport Button from \"misago/components/button\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport fileSize from \"misago/utils/file-size\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n image: null,\n preview: null,\n progress: 0,\n uploaded: null,\n dataUrl: null,\n }\n }\n\n validateFile(image) {\n if (image.size > this.props.options.upload.limit) {\n return interpolate(\n pgettext(\n \"avatar upload modal\",\n \"Selected file is too big. (%(filesize)s)\"\n ),\n {\n filesize: fileSize(image.size),\n },\n true\n )\n }\n\n let invalidTypeMsg = pgettext(\n \"avatar upload modal\",\n \"Selected file type is not supported.\"\n )\n if (\n this.props.options.upload.allowed_mime_types.indexOf(image.type) === -1\n ) {\n return invalidTypeMsg\n }\n\n let extensionFound = false\n let loweredFilename = image.name.toLowerCase()\n this.props.options.upload.allowed_extensions.map(function (extension) {\n if (loweredFilename.substr(extension.length * -1) === extension) {\n extensionFound = true\n }\n })\n\n if (!extensionFound) {\n return invalidTypeMsg\n }\n\n return false\n }\n\n pickFile = () => {\n document.getElementById(\"avatar-hidden-upload\").click()\n }\n\n uploadFile = () => {\n let image = document.getElementById(\"avatar-hidden-upload\").files[0]\n if (!image) return\n\n let validationError = this.validateFile(image)\n if (validationError) {\n snackbar.error(validationError)\n return\n }\n\n this.setState({\n image,\n preview: URL.createObjectURL(image),\n progress: 0,\n })\n\n let data = new FormData()\n data.append(\"avatar\", \"upload\")\n data.append(\"image\", image)\n\n ajax\n .upload(this.props.user.api.avatar, data, (progress) => {\n this.setState({\n progress,\n })\n })\n .then(\n (data) => {\n this.setState({\n options: data,\n uploaded: data.detail,\n })\n\n snackbar.info(\n pgettext(\n \"avatar upload modal\",\n \"Your image has been uploaded and you may now crop it.\"\n )\n )\n },\n (rejection) => {\n if (rejection.status === 400 || rejection.status === 413) {\n snackbar.error(rejection.detail)\n this.setState({\n isLoading: false,\n image: null,\n progress: 0,\n })\n } else {\n this.props.showError(rejection)\n }\n }\n )\n }\n\n getUploadRequirements(options) {\n let extensions = options.allowed_extensions.map(function (extension) {\n return extension.substr(1)\n })\n\n return interpolate(\n pgettext(\"avatar upload modal\", \"%(files)s files smaller than %(limit)s\"),\n {\n files: extensions.join(\", \"),\n limit: fileSize(options.limit),\n },\n true\n )\n }\n\n getUploadButton() {\n return (\n
    \n \n

    \n {this.getUploadRequirements(this.props.options.upload)}\n

    \n
    \n )\n }\n\n getUploadProgressLabel() {\n return interpolate(\n pgettext(\"avatar upload modal field\", \"%(progress)s % complete\"),\n {\n progress: this.state.progress,\n },\n true\n )\n }\n\n getUploadProgress() {\n return (\n
    \n
    \n \n\n
    \n \n {this.getUploadProgressLabel()}\n
    \n
    \n
    \n
    \n )\n }\n\n renderUpload() {\n return (\n
    \n \n {this.state.image ? this.getUploadProgress() : this.getUploadButton()}\n
    \n
    \n \n {pgettext(\"avatar upload modal btn\", \"Cancel\")}\n \n
    \n
    \n
    \n )\n }\n\n renderCrop() {\n return (\n \n )\n }\n\n render() {\n if (this.state.uploaded) return this.renderCrop()\n\n return this.renderUpload()\n }\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport Button from \"misago/components/button\"\nimport misago from \"misago/index\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport batch from \"misago/utils/batch\"\n\nexport class GalleryItem extends React.Component {\n select = () => {\n this.props.select(this.props.id)\n }\n\n getClassName() {\n if (this.props.selection === this.props.id) {\n if (this.props.disabled) {\n return \"btn btn-avatar btn-disabled avatar-selected\"\n } else {\n return \"btn btn-avatar avatar-selected\"\n }\n } else if (this.props.disabled) {\n return \"btn btn-avatar btn-disabled\"\n } else {\n return \"btn btn-avatar\"\n }\n }\n\n render() {\n return (\n \n \n \n )\n }\n}\n\nexport class Gallery extends React.Component {\n render() {\n return (\n
    \n

    {this.props.name}

    \n\n
    \n {batch(this.props.images, 4, null).map((row, i) => {\n return (\n
    \n {row.map((item, i) => {\n return (\n
    \n {item ? (\n \n ) : (\n
    \n )}\n
    \n )\n })}\n
    \n )\n })}\n
    \n
    \n )\n }\n}\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n selection: null,\n isLoading: false,\n }\n }\n\n select = (image) => {\n this.setState({\n selection: image,\n })\n }\n\n save = () => {\n if (this.state.isLoading) {\n return false\n }\n\n this.setState({\n isLoading: true,\n })\n\n ajax\n .post(this.props.user.api.avatar, {\n avatar: \"galleries\",\n image: this.state.selection,\n })\n .then(\n (response) => {\n this.setState({\n isLoading: false,\n })\n\n snackbar.success(response.detail)\n this.props.onComplete(response)\n this.props.showIndex()\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n this.setState({\n isLoading: false,\n })\n } else {\n this.props.showError(rejection)\n }\n }\n )\n }\n\n render() {\n return (\n
    \n
    \n {this.props.options.galleries.map((item, i) => {\n return (\n \n )\n })}\n
    \n
    \n
    \n
    \n \n {this.state.selection\n ? pgettext(\"avatar gallery modal btn\", \"Save choice\")\n : pgettext(\"avatar gallery modal btn\", \"Select avatar\")}\n \n\n \n {pgettext(\"avatar gallery modal btn\", \"Cancel\")}\n \n
    \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport AvatarIndex from \"misago/components/change-avatar/index\"\nimport AvatarCrop from \"misago/components/change-avatar/crop\"\nimport AvatarUpload from \"misago/components/change-avatar/upload\"\nimport AvatarGallery from \"misago/components/change-avatar/gallery\"\nimport Loader from \"misago/components/modal-loader\"\nimport { updateAvatar } from \"misago/reducers/users\"\nimport ajax from \"misago/services/ajax\"\nimport store from \"misago/services/store\"\n\nexport class ChangeAvatarError extends React.Component {\n getErrorReason() {\n if (this.props.reason) {\n return

    \n } else {\n return null\n }\n }\n\n render() {\n return (\n

    \n
    \n remove_circle_outline\n
    \n
    \n

    {this.props.message}

    \n {this.getErrorReason()}\n \n {pgettext(\"avatar modal dismiss\", \"Ok\")}\n \n
    \n
    \n )\n }\n}\n\nexport default class extends React.Component {\n componentDidMount() {\n ajax.get(this.props.user.api.avatar).then(\n (options) => {\n this.setState({\n component: AvatarIndex,\n options: options,\n error: null,\n })\n },\n (rejection) => {\n this.showError(rejection)\n }\n )\n }\n\n showError = (error) => {\n this.setState({\n error,\n })\n }\n\n showIndex = () => {\n this.setState({\n component: AvatarIndex,\n })\n }\n\n showUpload = () => {\n this.setState({\n component: AvatarUpload,\n })\n }\n\n showCrop = () => {\n this.setState({\n component: AvatarCrop,\n })\n }\n\n showGallery = () => {\n this.setState({\n component: AvatarGallery,\n })\n }\n\n completeFlow = (options) => {\n store.dispatch(updateAvatar(this.props.user, options.avatars))\n\n this.setState({\n component: AvatarIndex,\n options,\n })\n }\n\n getBody() {\n if (this.state) {\n if (this.state.error) {\n return (\n \n )\n } else {\n return (\n \n )\n }\n } else {\n return \n }\n }\n\n getClassName() {\n if (this.state && this.state.error) {\n return \"modal-dialog modal-message modal-change-avatar\"\n } else {\n return \"modal-dialog modal-change-avatar\"\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"avatar modal title\", \"Change your avatar\")}\n

    \n
    \n\n {this.getBody()}\n
    \n
    \n )\n }\n}\n\nexport function select(state) {\n return {\n user: state.auth.user,\n }\n}\n","export default function logout() {\n document.getElementById(\"hidden-logout-form\").submit()\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { connect } from \"react-redux\"\nimport modal from \"../../services/modal\"\nimport ChangeAvatarModal, {\n select as selectAvatar,\n} from \"../change-avatar/root\"\nimport {\n DropdownDivider,\n DropdownFooter,\n DropdownMenuItem,\n DropdownSubheader,\n} from \"../Dropdown\"\nimport logout from \"./logout\"\n\nclass UserNavMenu extends React.Component {\n constructor(props) {\n super(props)\n\n if (props.dropdown) {\n // Collapse options on dropdown\n this.state = {\n options: props.options.slice(0, 2),\n optionsMore: props.options.length > 2,\n }\n } else {\n // Reveal all options on mobile overlay\n this.state = {\n options: props.options,\n optionsMore: false,\n }\n }\n }\n\n changeAvatar = () => {\n this.props.close()\n modal.show(connect(selectAvatar)(ChangeAvatarModal))\n }\n\n revealOptions = () => {\n this.setState({\n options: this.props.options,\n optionsMore: false,\n })\n }\n\n render() {\n const { user, close, dropdown, overlay } = this.props\n\n if (!user) {\n return null\n }\n\n const adminUrl = misago.get(\"ADMIN_URL\")\n\n return (\n \n
  • \n \n {user.username}\n {pgettext(\"user nav\", \"Go to your profile\")}\n \n
  • \n \n \n \n \n {user.unreadNotifications\n ? \"notifications_active\"\n : \"notifications_none\"}\n \n {pgettext(\"user nav\", \"Notifications\")}\n {!!user.unreadNotifications && (\n {user.unreadNotifications}\n )}\n \n \n {!!user.showPrivateThreads && (\n \n \n inbox\n {pgettext(\"user nav\", \"Private threads\")}\n {!!user.unreadPrivateThreads && (\n {user.unreadPrivateThreads}\n )}\n \n \n )}\n {!!adminUrl && (\n \n \n security\n {pgettext(\"user nav\", \"Admin control panel\")}\n \n \n )}\n \n \n {pgettext(\"user nav section\", \"Change options\")}\n \n \n \n portrait\n {pgettext(\"user nav\", \"Change avatar\")}\n \n \n {this.state.options.map((item) => (\n \n \n {item.icon}\n {item.name}\n \n \n ))}\n \n \n more_vertical\n {pgettext(\"user nav\", \"See more\")}\n \n \n {!!dropdown && (\n \n {\n logout()\n close()\n }}\n type=\"button\"\n >\n {pgettext(\"user nav\", \"Log out\")}\n \n \n )}\n \n )\n }\n}\n\nfunction select(state) {\n const user = state.auth.user\n if (!user.id) {\n return { user: null }\n }\n\n return {\n user: {\n username: user.username,\n unreadNotifications: user.unreadNotifications,\n unreadPrivateThreads: user.unread_private_threads,\n showPrivateThreads: user.acl.can_use_private_threads,\n url: user.url,\n },\n options: [...misago.get(\"userOptions\")],\n }\n}\n\nconst UserNavMenuConnected = connect(select)(UserNavMenu)\n\nexport default UserNavMenuConnected\n","import React from \"react\"\nimport UserNavMenu from \"./UserNavMenu\"\n\nexport default function UserNavDropdown({ close }) {\n return \n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport { close } from \"../../reducers/overlay\"\nimport { DropdownFooter } from \"../Dropdown\"\nimport { Overlay, OverlayHeader } from \"../Overlay\"\nimport UserNavMenu from \"./UserNavMenu\"\nimport logout from \"./logout\"\n\nexport function UserNavOverlay({ dispatch, isOpen }) {\n return (\n \n \n {pgettext(\"user nav title\", \"Your options\")}\n \n dispatch(close())} overlay />\n \n {\n logout()\n dispatch(close())\n }}\n type=\"button\"\n >\n {pgettext(\"user nav\", \"Log out\")}\n \n \n \n )\n}\n\nfunction select(state) {\n return {\n isOpen: state.overlay.userNav,\n }\n}\n\nconst UserNavOverlayConnected = connect(select)(UserNavOverlay)\n\nexport default UserNavOverlayConnected\n","import React from \"react\"\nimport misago from \"misago\"\n\nexport default function (props) {\n const size = props.size || 100\n const size2x = props.size2x || size * 2\n\n return (\n \n )\n}\n\nexport function getSrc(user, size) {\n if (user && user.id) {\n // just avatar hash, size and user id\n return resolveAvatarForSize(user.avatars, size).url\n } else {\n // just append avatar size to file to produce no-avatar placeholder\n return misago.get(\"BLANK_AVATAR_URL\")\n }\n}\n\nexport function resolveAvatarForSize(avatars, size) {\n let avatar = avatars[0]\n avatars.forEach((av) => {\n if (av.size >= size) {\n avatar = av\n }\n })\n return avatar\n}\n","import React from \"react\"\nimport Loader from \"./loader\"\n\nexport default class Button extends React.Component {\n render() {\n let className = \"btn \" + this.props.className\n let disabled = this.props.disabled\n\n if (this.props.loading) {\n className += \" btn-loading\"\n disabled = true\n }\n\n return (\n \n {this.props.children}\n {this.props.loading ? : null}\n \n )\n }\n}\n\nButton.defaultProps = {\n className: \"btn-default\",\n\n type: \"submit\",\n\n loading: false,\n disabled: false,\n\n onClick: null,\n}\n","import React from \"react\"\n\nexport default function (props) {\n return (\n \n {props.choices.map((item) => {\n return (\n \n {\"- - \".repeat(item.level) + item.label}\n \n )\n })}\n \n )\n}\n","import React from \"react\"\nimport PanelMessage from \"misago/components/panel-message\"\n\nexport default function ({ display }) {\n if (!display) return null\n\n return (\n \n )\n}\n","import React from \"react\"\nimport Loader from \"misago/components/loader\"\n\nexport default function ({ display }) {\n if (!display) return null\n\n return (\n
    \n \n
    \n )\n}\n","import React from \"react\"\nimport Select from \"misago/components/select\"\n\nexport default class extends React.Component {\n onChange = (ev) => {\n const { field, onChange } = this.props\n onChange(field.fieldname, ev.target.value)\n }\n\n render() {\n const { disabled, field, value } = this.props\n const { input } = field\n\n if (input.type === \"select\") {\n return (\n \n )\n }\n\n if (input.type === \"textarea\") {\n return (\n \n )\n }\n\n if (input.type === \"text\") {\n return (\n \n )\n }\n\n return null\n }\n}\n","import React from \"react\"\nimport FieldInput from \"./field-input\"\nimport FormGroup from \"misago/components/form-group\"\n\nexport default function ({ disabled, errors, fields, name, onChange, value }) {\n return (\n
    \n {name}\n {fields.map((field) => {\n return (\n \n \n \n )\n })}\n
    \n )\n}\n","import React from \"react\"\nimport Fieldset from \"./fieldset\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n errors: {},\n }\n\n const groups = props.groups.length\n for (let i = 0; i < groups; i++) {\n const group = props.groups[i]\n const fields = group.fields.length\n for (let f = 0; f < fields; f++) {\n const fieldname = group.fields[f].fieldname\n const initial = group.fields[f].initial\n this.state[fieldname] = initial\n }\n }\n }\n\n send() {\n const data = Object.assign({}, this.state, {\n errors: null,\n isLoading: null,\n })\n\n return ajax.post(this.props.api, data)\n }\n\n handleSuccess(data) {\n this.props.onSuccess(data)\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n snackbar.error(gettext(\"Form contains errors.\"))\n this.setState({ errors: rejection })\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n onChange = (name, value) => {\n this.setState({\n [name]: value,\n })\n }\n\n render() {\n return (\n
    \n
    \n {this.props.groups.map((group, i) => {\n return (\n \n )\n })}\n
    \n
    \n {\" \"}\n \n
    \n
    \n )\n }\n}\n\nexport function CancelButton({ onCancel, disabled }) {\n if (!onCancel) return null\n\n return (\n \n {pgettext(\"user profile details form btn\", \"Cancel\")}\n \n )\n}\n","import React from \"react\"\nimport Blankslate from \"./blankslate\"\nimport Loader from \"./loader\"\nimport Form from \"./form\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n loading: true,\n groups: null,\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.api).then(\n (groups) => {\n this.setState({\n loading: false,\n\n groups,\n })\n },\n (rejection) => {\n snackbar.apiError(rejection)\n if (this.props.cancel) {\n this.props.cancel()\n }\n }\n )\n }\n\n render() {\n const { groups, loading } = this.state\n\n return (\n
    \n
    \n

    \n {pgettext(\"user profile details form title\", \"Edit details\")}\n

    \n
    \n \n \n \n
    \n )\n }\n}\n\nexport function FormDisplay({ api, display, groups, onCancel, onSuccess }) {\n if (!display) return null\n\n return (\n
    \n )\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n isValidated() {\n return typeof this.props.validation !== \"undefined\"\n }\n\n getClassName() {\n let className = \"form-group\"\n if (this.isValidated()) {\n className += \" has-feedback\"\n if (this.props.validation === null) {\n className += \" has-success\"\n } else {\n className += \" has-error\"\n }\n }\n return className\n }\n\n getFeedback() {\n if (this.props.validation) {\n return (\n
    \n {this.props.validation.map((error, i) => {\n return

    {error}

    \n })}\n
    \n )\n } else {\n return null\n }\n }\n\n getFeedbackDescription() {\n if (this.isValidated()) {\n return (\n \n {this.props.validation\n ? pgettext(\"field validation status\", \"(error)\")\n : pgettext(\"field validation status\", \"(success)\")}\n \n )\n } else {\n return null\n }\n }\n\n getHelpText() {\n if (this.props.helpText) {\n return

    {this.props.helpText}

    \n } else {\n return null\n }\n }\n\n render() {\n return (\n
    \n \n {this.props.label + \":\"}\n \n
    \n {this.props.children}\n {this.getFeedbackDescription()}\n {this.getFeedback()}\n {this.getHelpText()}\n {this.props.extra || null}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport { required } from \"misago/utils/validators\"\nimport snackbar from \"misago/services/snackbar\"\n\nlet validateRequired = required()\n\nexport default class extends React.Component {\n validate() {\n let errors = {}\n if (!this.state.validators) {\n return errors\n }\n\n let validators = {\n required: this.state.validators.required || this.state.validators,\n optional: this.state.validators.optional || {},\n }\n\n let validatedFields = []\n\n // add required fields to validation\n for (let name in validators.required) {\n if (\n validators.required.hasOwnProperty(name) &&\n validators.required[name]\n ) {\n validatedFields.push(name)\n }\n }\n\n // add optional fields to validation\n for (let name in validators.optional) {\n if (\n validators.optional.hasOwnProperty(name) &&\n validators.optional[name]\n ) {\n validatedFields.push(name)\n }\n }\n\n // validate fields values\n for (let i in validatedFields) {\n let name = validatedFields[i]\n let fieldErrors = this.validateField(name, this.state[name])\n\n if (fieldErrors === null) {\n errors[name] = null\n } else if (fieldErrors) {\n errors[name] = fieldErrors\n }\n }\n\n return errors\n }\n\n isValid() {\n let errors = this.validate()\n for (let field in errors) {\n if (errors.hasOwnProperty(field)) {\n if (errors[field] !== null) {\n return false\n }\n }\n }\n\n return true\n }\n\n validateField(name, value) {\n let errors = []\n if (!this.state.validators) {\n return errors\n }\n\n let validators = {\n required: (this.state.validators.required || this.state.validators)[name],\n optional: (this.state.validators.optional || {})[name],\n }\n\n let requiredError = validateRequired(value) || false\n\n if (validators.required) {\n if (requiredError) {\n errors = [requiredError]\n } else {\n for (let i in validators.required) {\n let validationError = validators.required[i](value)\n if (validationError) {\n errors.push(validationError)\n }\n }\n }\n\n return errors.length ? errors : null\n } else if (requiredError === false && validators.optional) {\n for (let i in validators.optional) {\n let validationError = validators.optional[i](value)\n if (validationError) {\n errors.push(validationError)\n }\n }\n\n return errors.length ? errors : null\n }\n\n return false // false === field wasn't validated\n }\n\n bindInput = (name) => {\n return (event) => {\n this.changeValue(name, event.target.value)\n }\n }\n\n changeValue = (name, value) => {\n let newState = {\n [name]: value,\n }\n\n const formErrors = this.state.errors || {}\n formErrors[name] = this.validateField(name, newState[name])\n newState.errors = formErrors\n\n this.setState(newState)\n }\n\n clean() {\n return true\n }\n\n send() {\n return null\n }\n\n handleSuccess(success) {\n return\n }\n\n handleError(rejection) {\n snackbar.apiError(rejection)\n }\n\n handleSubmit = (event) => {\n // we don't reload page on submissions\n if (event) {\n event.preventDefault()\n }\n\n if (this.state.isLoading) {\n return\n }\n\n if (this.clean()) {\n this.setState({ isLoading: true })\n let promise = this.send()\n\n if (promise) {\n promise.then(\n (success) => {\n this.setState({ isLoading: false })\n this.handleSuccess(success)\n },\n (rejection) => {\n this.setState({ isLoading: false })\n this.handleError(rejection)\n }\n )\n } else {\n this.setState({ isLoading: false })\n }\n }\n }\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n isActive() {\n if (this.props.isControlled) {\n return this.props.isActive\n } else {\n if (this.props.path) {\n return document.location.pathname.indexOf(this.props.path) === 0\n } else {\n return false\n }\n }\n }\n\n getClassName() {\n if (this.isActive()) {\n return (\n (this.props.className || \"\") +\n \" \" +\n (this.props.activeClassName || \"active\")\n )\n } else {\n return this.props.className || \"\"\n }\n }\n\n render() {\n return
  • {this.props.children}
  • \n }\n}\n","import React from \"react\"\n\nexport default function (props) {\n return (\n
    \n
    \n
    \n )\n}\n","import React from \"react\"\nimport Button from \"./button\"\nimport Form from \"./form\"\nimport FormGroup from \"./form-group\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n bestAnswer: \"0\",\n poll: \"0\",\n }\n }\n\n clean() {\n if (this.props.polls && this.state.poll === \"0\") {\n const confirmation = window.confirm(\n pgettext(\n \"merge threads conflict form\",\n \"Are you sure you want to delete all polls?\"\n )\n )\n return confirmation\n }\n\n return true\n }\n\n send() {\n const data = Object.assign({}, this.props.data, {\n best_answer: this.state.bestAnswer,\n poll: this.state.poll,\n })\n\n return ajax.post(this.props.api, data)\n }\n\n handleSuccess = (success) => {\n this.props.onSuccess(success)\n modal.hide()\n }\n\n handleError = (rejection) => {\n this.props.onError(rejection)\n }\n\n onBestAnswerChange = (event) => {\n this.changeValue(\"bestAnswer\", event.target.value)\n }\n\n onPollChange = (event) => {\n this.changeValue(\"poll\", event.target.value)\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"merge threads conflict modal title\", \"Merge threads\")}\n

    \n
    \n \n
    \n \n \n
    \n
    \n \n {pgettext(\"merge threads conflict btn\", \"Cancel\")}\n \n \n
    \n \n
    \n
    \n )\n }\n}\n\nexport function BestAnswerSelect({ choices, onChange, value }) {\n if (!choices) return null\n\n return (\n \n \n {choices.map((choice) => {\n return (\n \n )\n })}\n \n \n )\n}\n\nexport function PollSelect({ choices, onChange, value }) {\n if (!choices) return null\n\n return (\n \n \n {choices.map((choice) => {\n return (\n \n )\n })}\n \n \n )\n}\n","const ytRegExp = new RegExp(\n \"^.*(?:(?:youtu.be/|v/|vi/|u/w/|embed/)|(?:(?:watch)??v(?:i)?=|&v(?:i)?=))([^#&?]*).*\"\n)\n\nexport class OneBox {\n constructor() {\n this._youtube = {}\n }\n\n render = (element) => {\n if (!element) return\n this.highlightCode(element)\n this.embedYoutubePlayers(element)\n }\n\n highlightCode(element) {\n import(\"highlight\").then(({ default: hljs }) => {\n const codeblocks = element.querySelectorAll(\"pre>code\")\n for (let i = 0; i < codeblocks.length; i++) {\n hljs.highlightElement(codeblocks[i])\n }\n })\n }\n\n embedYoutubePlayers(element) {\n const anchors = element.querySelectorAll(\"p>a\")\n for (let i = 0; i < anchors.length; i++) {\n const a = anchors[i]\n const p = a.parentNode\n const onlyChild = p.childNodes.length === 1\n\n if (!this._youtube[a.href]) {\n this._youtube[a.href] = parseYoutubeUrl(a.href)\n }\n\n const youtubeMovie = this._youtube[a.href]\n if (onlyChild && !!youtubeMovie && youtubeMovie.data !== false) {\n this.swapYoutubePlayer(a, youtubeMovie)\n }\n }\n }\n\n swapYoutubePlayer(element, youtube) {\n let url = \"https://www.youtube.com/embed/\"\n url += youtube.video\n url += \"?feature=oembed\"\n if (youtube.start) {\n url += \"&start=\" + youtube.start\n }\n\n const player = $(\n '\"\n )\n $(element).replaceWith(player)\n player.wrap('
    ')\n }\n}\n\nexport default new OneBox()\n\nexport function parseYoutubeUrl(url) {\n const cleanedUrl = cleanUrl(url)\n const video = getVideoIdFromUrl(cleanedUrl)\n\n if (!video) return null\n\n let start = 0\n if (cleanedUrl.indexOf(\"?\") > 0) {\n const query = cleanedUrl.substr(cleanedUrl.indexOf(\"?\") + 1)\n const timebit = query.split(\"&\").filter((i) => {\n return i.substr(0, 2) === \"t=\"\n })[0]\n\n if (timebit) {\n const bits = timebit.substr(2).split(\"m\")\n if (bits[0].substr(-1) === \"s\") {\n start += parseInt(bits[0].substr(0, bits[0].length - 1))\n } else {\n start += parseInt(bits[0]) * 60\n if (!!bits[1] && bits[1].substr(-1) === \"s\") {\n start += parseInt(bits[1].substr(0, bits[1].length - 1))\n }\n }\n }\n }\n\n return {\n start,\n video,\n }\n}\n\nexport function cleanUrl(url) {\n let clean = url\n\n if (url.substr(0, 8) === \"https://\") {\n clean = clean.substr(8)\n } else if (url.substr(0, 7) === \"http://\") {\n clean = clean.substr(7)\n }\n\n if (clean.substr(0, 4) === \"www.\") {\n clean = clean.substr(4)\n }\n\n return clean\n}\n\nexport function getVideoIdFromUrl(url) {\n if (url.indexOf(\"youtu\") === -1) return null\n\n const video = url.match(ytRegExp)\n if (video) {\n return video[1]\n }\n return null\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport onebox from \"misago/services/one-box\"\n\nexport default class extends React.Component {\n componentDidMount() {\n onebox.render(this.documentNode)\n $(this.documentNode).find(\".spoiler-reveal\").click(revealSpoiler)\n }\n\n componentDidUpdate(prevProps, prevState) {\n onebox.render(this.documentNode)\n $(this.documentNode).find(\".spoiler-reveal\").click(revealSpoiler)\n }\n\n shouldComponentUpdate(nextProps, nextState) {\n return nextProps.markup !== this.props.markup\n }\n\n render() {\n return (\n {\n this.documentNode = node\n }}\n />\n )\n }\n}\n\nfunction revealSpoiler(event) {\n var btn = event.target\n $(btn).parent().parent().addClass(\"revealed\")\n}\n","import React from \"react\"\nimport Loader from \"misago/components/loader\"\n\nexport default class extends React.Component {\n render() {\n return (\n
    \n \n
    \n )\n }\n}\n","import React from \"react\"\nimport PanelMessage from \"misago/components/panel-message\"\n\nexport default class extends PanelMessage {\n getHelpText() {\n if (this.props.helpText) {\n return

    {this.props.helpText}

    \n } else {\n return null\n }\n }\n\n render() {\n return (\n
    \n
    \n \n {this.props.icon || \"info_outline\"}\n \n
    \n
    \n

    {this.props.message}

    \n {this.getHelpText()}\n \n {pgettext(\"modal message dismiss btn\", \"Ok\")}\n \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Loader from \"misago/components/loader\"\n\nexport default class extends React.Component {\n render() {\n return (\n
    \n \n
    \n )\n }\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n getHelpText() {\n if (this.props.helpText) {\n return

    {this.props.helpText}

    \n } else {\n return null\n }\n }\n\n render() {\n return (\n
    \n
    \n \n {this.props.icon || \"info_outline\"}\n \n
    \n
    \n

    {this.props.message}

    \n {this.getHelpText()}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport MisagoMarkup from \"misago/components/misago-markup\"\n\nexport default function (props) {\n if (props.post.content) {\n return \n } else {\n return \n }\n}\n\nexport function Default(props) {\n return (\n
    \n \n
    \n )\n}\n\nexport function Invalid(props) {\n return (\n
    \n

    \n {pgettext(\n \"post body invalid\",\n \"This post's contents cannot be displayed.\"\n )}\n

    \n

    \n {pgettext(\n \"post body invalid\",\n \"This error is caused by invalid post content manipulation.\"\n )}\n

    \n
    \n )\n}\n","import React from \"react\"\n\nexport default function ({ post }) {\n const { category, thread } = post\n\n const tooltip = interpolate(\n pgettext(\"posts feed item header\", \"posted %(posted_on)s\"),\n {\n posted_on: post.posted_on.format(\"LL, LT\"),\n },\n true\n )\n\n return (\n
    \n \n {thread.title}\n \n \n {category.name}\n \n \n {post.posted_on.fromNow()}\n \n
    \n )\n}\n","import React from \"react\"\n\nexport default function ({ post }) {\n return (\n \n \n {pgettext(\"go to post link\", \"See post\")}\n \n chevron_right\n \n )\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport GoToButton from \"./button\"\n\nexport default function ({ post }) {\n return (\n
    \n \n
    \n
    \n \n \n \n
    \n
    \n
    \n {post.poster_name}\n
    \n \n {pgettext(\"post removed poster username\", \"Removed user\")}\n \n
    \n
    \n
    \n )\n}\n","import React from \"react\"\n\nexport default function ({ rank, title }) {\n let userTitle = title || rank.title || rank.name\n\n let className = \"user-title\"\n if (rank.css_class) {\n className += \" user-title-\" + rank.css_class\n }\n\n if (rank.is_tab) {\n return (\n \n {userTitle}\n \n )\n }\n\n return {userTitle}\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport GoToButton from \"./button\"\nimport UserTitle from \"./user-title\"\n\nexport default function ({ post, poster }) {\n return (\n
    \n \n
    \n
    \n \n \n \n
    \n \n
    \n
    \n )\n}\n","import React from \"react\"\nimport Anonymous from \"./anonymous\"\nimport Registered from \"./registered\"\n\nexport default function ({ post, poster }) {\n if (poster && poster.id) {\n return \n }\n\n return \n}\n","import React from \"react\"\nimport Body from \"./body\"\nimport Header from \"./header\"\nimport PostSide from \"./post-side\"\n\nexport default function ({ post, poster }) {\n const user = poster || post.poster\n\n let className = \"post\"\n if (user && user.rank.css_class) {\n className += \" post-\" + user.rank.css_class\n }\n\n return (\n
  • \n
    \n
    \n
    \n \n
    \n \n
    \n
    \n
    \n
  • \n )\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport * as random from \"misago/utils/random\"\n\nexport default function () {\n return (\n
      \n
    • \n
      \n
      \n
      \n
      \n
      \n
      \n \n \n \n
      \n
      \n
      \n \n \n  \n \n \n
      \n \n \n  \n \n \n
      \n
      \n
      \n
      \n \n  \n \n
      \n
      \n
      \n

      \n \n  \n \n  \n \n  \n \n  \n \n  \n \n

      \n
      \n
      \n
      \n
      \n
      \n
    • \n
    \n )\n}\n","import React from \"react\"\nimport Post from \"./post\"\nimport Preview from \"./preview\"\n\nexport default function ({ isReady, posts, poster }) {\n if (!isReady) {\n return \n }\n\n return (\n
      \n {posts.map((post) => {\n return \n })}\n
    \n )\n}\n","import React from \"react\"\nimport posting from \"../../services/posting\"\nimport { getGlobalState, getQuoteMarkup } from \"../posting\"\n\nexport default class PostingQuoteSelection extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n range: null,\n rect: null,\n }\n\n this.element = null\n }\n\n selected = () => {\n if (this.element) {\n const range = getQuoteSelection(this.element) || null\n const rect = range ? range.getBoundingClientRect() : null\n\n this.setState({ range, rect })\n }\n }\n\n reply = () => {\n if (!posting.isOpen()) {\n const content = getQuoteMarkup(this.state.range)\n posting.open(Object.assign({}, this.props.posting, { default: content }))\n\n this.setState({ range: null, rect: null })\n\n window.setTimeout(focusEditor, 1000)\n } else {\n const globalState = getGlobalState()\n if (globalState && !globalState.disabled) {\n globalState.quote(getQuoteMarkup(this.state.range))\n this.setState({ range: null, rect: null })\n focusEditor()\n }\n }\n }\n\n render = () => (\n
    \n {\n if (element) {\n this.element = element\n }\n }}\n onMouseUp={this.selected}\n onTouchEnd={this.selected}\n >\n {this.props.children}\n
    \n {!!this.state.rect && (\n \n
    \n
    \n \n {pgettext(\"post reply\", \"Quote\")}\n \n
    \n
    \n )}\n
    \n )\n}\n\nfunction focusEditor() {\n const textarea = document.querySelector(\"#posting-mount textarea\")\n textarea.focus()\n textarea.selectionStart = textarea.selectionEnd = textarea.value.length\n}\n\nconst getQuoteSelection = (container) => {\n if (typeof window.getSelection === \"undefined\") return\n\n // Validate that selection is of valid type and has one range\n const selection = window.getSelection()\n if (!selection) return\n if (selection.type !== \"Range\") return\n if (selection.rangeCount !== 1) return\n\n // Validate that selection is within the container and post's article\n const range = selection.getRangeAt(0)\n if (!isRangeContained(range, container)) return\n if (!isPostContained(range)) return\n if (!isAnyTextSelected(range.cloneContents())) return\n\n return range\n}\n\nconst isRangeContained = (range, container) => {\n const node = range.commonAncestorContainer\n if (node === container) return true\n\n let p = node.parentNode\n while (p) {\n if (p === container) return true\n p = p.parentNode\n }\n\n return false\n}\n\nconst isPostContained = (range) => {\n const element = range.commonAncestorContainer\n if (element.nodeName === \"ARTICLE\") return true\n if (element.dataset && element.dataset.noquote === \"1\") return false\n let p = element.parentNode\n while (p) {\n if (p.dataset && p.dataset.noquote === \"1\") return false\n if (p.nodeName === \"ARTICLE\") return true\n p = p.parentNode\n }\n return false\n}\n\nconst isAnyTextSelected = (node) => {\n for (let i = 0; i < node.childNodes.length; i++) {\n const child = node.childNodes[i]\n if (child.nodeType === Node.TEXT_NODE) {\n if (child.textContent && child.textContent.trim().length > 0) return true\n }\n if (child.nodeName === \"IMG\") return true\n if (isAnyTextSelected(child)) return true\n }\n\n return false\n}\n","const getQuoteMarkup = (range) => {\n const metadata = getQuoteMetadata(range)\n let markup = convertNodesToMarkup(range.cloneContents().childNodes, [])\n let prefix = metadata ? `[quote=\"${metadata}\"]\\n` : \"[quote]\\n\"\n let suffix = \"\\n[/quote]\\n\\n\"\n\n const codeBlock = getQuoteCodeBlock(range)\n if (codeBlock) {\n prefix += codeBlock.syntax ? `[code=${codeBlock.syntax}]\\n` : \"[code]\\n\"\n suffix = \"\\n[/code]\" + suffix\n } else if (isNodeInlineCodeBlock(range)) {\n markup = markup.trim()\n prefix += \"`\"\n suffix = \"`\" + suffix\n } else {\n markup = markup.trim()\n }\n\n return prefix + markup + suffix\n}\n\nexport default getQuoteMarkup\n\nconst getQuoteMetadata = (range) => {\n const node = range.commonAncestorContainer\n if (isNodeElementWithQuoteMetadata(node)) {\n return getQuoteMetadataFromNode(node)\n }\n\n let p = node.parentNode\n while (p) {\n if (isNodeElementWithQuoteMetadata(p)) {\n return getQuoteMetadataFromNode(p)\n }\n p = p.parentNode\n }\n\n return \"\"\n}\n\nconst isNodeElementWithQuoteMetadata = (node) => {\n if (node.nodeType !== Node.ELEMENT_NODE) return false\n if (node.nodeName === \"ARTICLE\") return true\n if (node.nodeName === \"BLOCKQUOTE\") {\n return node.dataset && node.dataset.block === \"quote\"\n }\n\n return false\n}\n\nconst getQuoteMetadataFromNode = (element) => {\n if (element.dataset) {\n return element.dataset.author || null\n }\n return null\n}\n\nconst getQuoteCodeBlock = (range) => {\n const node = range.commonAncestorContainer\n if (isNodeCodeBlock(node)) {\n return getNodeCodeBlockMeta(node)\n }\n\n let p = node.parentNode\n while (p) {\n if (isNodeCodeBlock(p)) {\n return getNodeCodeBlockMeta(p)\n }\n p = p.parentNode\n }\n\n return null\n}\n\nconst isNodeCodeBlock = (node) => {\n return node.nodeName === \"PRE\"\n}\n\nconst isNodeInlineCodeBlock = (range) => {\n const node = range.commonAncestorContainer\n if (node.nodeName === \"CODE\") {\n return true\n }\n\n let p = node.parentNode\n while (p) {\n if (isNodeElementWithQuoteMetadata(p)) {\n return false\n }\n\n if (p.nodeName === \"CODE\") {\n return true\n }\n\n p = p.parentNode\n }\n\n return false\n}\n\nconst getNodeCodeBlockMeta = (node) => {\n if (!node.dataset) {\n return { syntax: null }\n }\n\n return { syntax: node.dataset.syntax || null }\n}\n\nconst convertNodesToMarkup = (nodes, stack) => {\n let markup = \"\"\n for (let i = 0; i < nodes.length; i++) {\n const node = nodes[i]\n markup += convertNodeToMarkup(node, stack)\n }\n return markup\n}\n\nconst SIMPLE_NODE_MAPPINGS = {\n H1: [\"\\n\\n# \", \"\"],\n H2: [\"\\n\\n## \", \"\"],\n H3: [\"\\n\\n### \", \"\"],\n H4: [\"\\n\\n#### \", \"\"],\n H5: [\"\\n\\n##### \", \"\"],\n H6: [\"\\n\\n###### \", \"\"],\n STRONG: [\"**\", \"**\"],\n EM: [\"*\", \"*\"],\n DEL: [\"~~\", \"~~\"],\n B: [\"[b]\", \"[/b]\"],\n U: [\"[u]\", \"[/u]\"],\n I: [\"[i]\", \"[/i]\"],\n SUB: [\"[sub]\", \"[/sub]\"],\n SUP: [\"[sup]\", \"[/sup]\"],\n}\n\nconst convertNodeToMarkup = (node, stack) => {\n const dataset = node.dataset || {}\n\n if (node.nodeType === Node.TEXT_NODE) {\n return node.textContent || \"\"\n }\n\n if (node.nodeType === Node.ELEMENT_NODE) {\n if (dataset.quote) {\n return dataset.quote || \"\"\n }\n if (dataset.noquote === \"1\") return \"\"\n }\n\n if (\n node.nodeType === Node.ELEMENT_NODE &&\n dataset.quote &&\n dataset.quote.trim()\n ) {\n return \"\"\n }\n\n if (node.nodeName === \"HR\") {\n return \"\\n\\n- - -\"\n }\n\n if (SIMPLE_NODE_MAPPINGS[node.nodeName]) {\n const [prefix, suffix] = SIMPLE_NODE_MAPPINGS[node.nodeName]\n return (\n prefix +\n convertNodesToMarkup(node.childNodes, [...stack, node.nodeName]) +\n suffix\n )\n }\n\n if (node.nodeName === \"A\") {\n const href = node.href\n const text = convertNodesToMarkup(node.childNodes, [\n ...stack,\n node.nodeName,\n ])\n if (text) {\n return `[${text}](${href})`\n } else {\n return `!(${href})`\n }\n }\n\n if (node.nodeName === \"IMG\") {\n const src = node.src\n const alt = node.alt\n if (alt) {\n return `![${alt}](${src})`\n } else {\n return `!(${src})`\n }\n }\n\n if (node.nodeName === \"DIV\" || node.nodeName === \"ASIDE\") {\n const block = dataset.block && dataset.block.toUpperCase()\n if (block && SIMPLE_NODE_MAPPINGS[block]) {\n const [prefix, suffix] = SIMPLE_NODE_MAPPINGS[block]\n return (\n prefix +\n convertNodesToMarkup(node.childNodes, [...stack, block]) +\n suffix\n )\n } else {\n return convertNodesToMarkup(node.childNodes, stack)\n }\n }\n\n if (node.nodeName === \"BLOCKQUOTE\") {\n if (dataset.block === \"spoiler\") {\n const content = convertNodesToMarkup(node.childNodes, [\n ...stack,\n \"SPOILER\",\n ]).trim()\n\n if (!content) return \"\"\n\n let markup = \"\\n[spoiler]\\n\"\n markup += content\n markup += \"\\n[/spoiler]\"\n return markup\n }\n\n const content = convertNodesToMarkup(node.childNodes, [\n ...stack,\n \"QUOTE\",\n ]).trim()\n\n if (!content) return \"\"\n\n const metadata = getQuoteMetadataFromNode(node)\n let markup = metadata ? `\\n[quote=${metadata}]\\n` : \"\\n\\n[quote]\\n\"\n markup += content\n markup += \"\\n[/quote]\"\n return markup\n }\n\n if (node.nodeName === \"PRE\") {\n const syntax = dataset.syntax || null\n const code = node.querySelector(\"code\")\n const content = code ? code.innerText || \"\" : \"\"\n\n if (!content.trim()) return \"\"\n\n return \"\\n[code\" + (syntax ? \"=\" + syntax : \"\") + \"]\" + content + \"[/code]\"\n }\n\n if (node.nodeName === \"CODE\") {\n return \"`\" + node.innerText + \"`\"\n }\n\n if (node.nodeName === \"P\") {\n return (\n \"\\n\" + convertNodesToMarkup(node.childNodes, [...stack, node.nodeName])\n )\n }\n\n if (node.nodeName === \"UL\" || node.nodeName === \"OL\") {\n const level = stack.filter((item) => item === \"OL\" || item === \"UL\").length\n const prefix = level === 0 ? \"\\n\" : \"\"\n return (\n prefix + convertNodesToMarkup(node.childNodes, [...stack, node.nodeName])\n )\n }\n\n if (node.nodeName === \"LI\") {\n let prefix = \"\"\n const level = stack.filter((item) => item === \"OL\" || item === \"UL\").length\n for (let i = 1; i < level; i++) {\n prefix += \" \"\n }\n\n const ordered = stack[stack.length - 1] === \"OL\"\n if (ordered) {\n prefix += dataset.index ? dataset.index + \". \" : \"1. \"\n } else {\n prefix += \"- \"\n }\n\n const content = convertNodesToMarkup(node.childNodes, [\n ...stack,\n node.nodeName,\n ])\n if (!content.trim()) return \"\"\n\n return \"\\n\" + prefix + content\n }\n\n if (node.nodeName === \"SPAN\") {\n return convertNodesToMarkup(node.childNodes, stack)\n }\n\n return \"\"\n}\n","export function getGlobalState() {\n return window.misagoReply\n}\n\nexport function setGlobalState(disabled, quote) {\n window.misagoReply = { disabled, quote }\n}\n\nexport function clearGlobalState() {\n window.misagoReply = null\n}\n","import moment from \"moment\"\n\nexport function clean(attachments) {\n return attachments\n .filter((attachment) => {\n return attachment.id && !attachment.isRemoved\n })\n .map((a) => {\n return a.id\n })\n}\n\nexport function hydrate(attachments) {\n return attachments.map((attachment) => {\n return Object.assign({}, attachment, {\n uploaded_on: moment(attachment.uploaded_on),\n })\n })\n}\n","import React from \"react\"\nimport formatFilesize from \"../../utils/file-size\"\n\nexport default function MarkupAttachmentModal({ attachment }) {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"markup editor\", \"Attachment details\")}\n

    \n
    \n
    \n {!!attachment.is_image && (\n
    \n \n \"\"\n \n
    \n )}\n
    \n {attachment.filename}\n
    \n
    \n
    \n \n {attachment.filetype + \", \" + formatFilesize(attachment.size)}\n \n
    \n {pgettext(\"markup editor\", \"Type and size\")}\n
    \n
    \n
    \n \n \n {attachment.uploaded_on.fromNow()}\n \n \n
    \n {pgettext(\"markup editor\", \"Uploaded at\")}\n
    \n
    \n
    \n {attachment.url.uploader ? (\n \n {attachment.uploader_name}\n \n ) : (\n {attachment.uploader_name}\n )}\n
    \n {pgettext(\"markup editor\", \"Uploader\")}\n
    \n
    \n
    \n
    \n
    \n \n {pgettext(\"modal\", \"Close\")}\n \n
    \n
    \n
    \n )\n}\n","const wrapSelection = (selection, update, prefix, suffix, def) => {\n const text = selection.text || def || \"\"\n let newValue = selection.prefix\n newValue += prefix + text + suffix\n newValue += selection.suffix\n update(newValue)\n\n window.setTimeout(() => {\n focus(selection.textarea)\n\n const caret = selection.start + prefix.length\n selection.textarea.setSelectionRange(caret, caret + text.length)\n }, 250)\n}\n\nconst replaceSelection = (selection, update, text) => {\n let newValue = selection.prefix\n newValue += text\n newValue += selection.suffix\n update(newValue)\n\n window.setTimeout(() => {\n focus(selection.textarea)\n\n const caret = selection.end + text.length\n selection.textarea.setSelectionRange(caret, caret)\n }, 250)\n}\n\nconst getSelection = (textarea) => {\n if (document.selection) {\n textarea.focus()\n const range = document.selection.createRange()\n const length = range.text.length\n range.moveStart(\"character\", -textarea.value.length)\n return createRange(textarea, range.text.length - length, range.text.length)\n }\n\n if (textarea.selectionStart || textarea.selectionStart == \"0\") {\n return createRange(textarea, textarea.selectionStart, textarea.selectionEnd)\n }\n}\n\nconst createRange = (textarea, start, end) => {\n return {\n textarea: textarea,\n start: start,\n end: end,\n text: textarea.value.substring(start, end),\n prefix: textarea.value.substring(0, start),\n suffix: textarea.value.substring(end),\n }\n}\n\nexport function focus(textarea) {\n const scroll = textarea.scrollTop\n textarea.focus()\n textarea.scrollTop = scroll\n}\n\nexport { getSelection, replaceSelection, wrapSelection }\n","import React from \"react\"\nimport modal from \"../../services/modal\"\nimport snackbar from \"../../services/snackbar\"\nimport formatFilesize from \"../../utils/file-size\"\nimport MarkupAttachmentModal from \"./MarkupAttachmentModal\"\nimport { getSelection, replaceSelection } from \"./operations\"\n\nconst MarkupEditorAttachment = ({\n attachment,\n disabled,\n element,\n setState,\n update,\n}) => (\n
    \n
    \n
    \n {attachment.id ? (\n {\n event.preventDefault()\n modal.show()\n }}\n >\n {attachment.filename}\n \n ) : (\n {attachment.filename}\n )}\n
    \n
      \n {!attachment.id &&
    • {attachment.progress + \"%\"}
    • }\n {!!attachment.filetype &&
    • {attachment.filetype}
    • }\n {attachment.size > 0 &&
    • {formatFilesize(attachment.size)}
    • }\n
    \n
    \n
    \n {!!attachment.id && (\n
    \n {\n const markup = getAttachmentMarkup(attachment)\n const selection = getSelection(element)\n replaceSelection(selection, update, markup)\n }}\n >\n flip_to_front\n \n {\n setState(({ attachments }) => {\n const confirm = window.confirm(\n pgettext(\"markup editor\", \"Remove this attachment?\")\n )\n\n if (confirm) {\n return {\n attachments: attachments.filter(\n ({ id }) => id !== attachment.id\n ),\n }\n }\n })\n }}\n >\n close\n \n
    \n )}\n {!attachment.id && !!attachment.key && (\n
    \n {attachment.error && (\n {\n snackbar.error(\n interpolate(\n pgettext(\"markup editor\", \"%(filename)s: %(error)s\"),\n { filename: attachment.filename, error: attachment.error },\n true\n )\n )\n }}\n >\n warning\n \n )}\n {\n setState(({ attachments }) => {\n return {\n attachments: attachments.filter(\n ({ key }) => key !== attachment.key\n ),\n }\n })\n }}\n >\n close\n \n
    \n )}\n
    \n
    \n)\n\nexport default MarkupEditorAttachment\n\nfunction getAttachmentMarkup(attachment) {\n let markup = \"[\"\n\n if (attachment.is_image) {\n markup += \"![\" + attachment.filename + \"]\"\n markup += \"(\" + (attachment.url.thumb || attachment.url.index) + \"?shva=1)\"\n } else {\n markup += attachment.filename\n }\n\n markup += \"](\" + attachment.url.index + \"?shva=1)\"\n return markup\n}\n","import React from \"react\"\nimport MarkupEditorAttachment from \"./MarkupEditorAttachment\"\n\nconst MarkupEditorAttachments = ({\n attachments,\n disabled,\n element,\n setState,\n update,\n}) => (\n
    \n
    \n {attachments.map((attachment) => (\n \n ))}\n
    \n
    \n)\n\nexport default MarkupEditorAttachments\n","import React from \"react\"\nimport Button from \"../button\"\n\nconst MarkupEditorFooter = ({\n canProtect,\n disabled,\n empty,\n preview,\n isProtected,\n submitText,\n showPreview,\n closePreview,\n enableProtection,\n disableProtection,\n}) => (\n
    \n {!!canProtect && (\n {\n if (isProtected) {\n disableProtection()\n } else {\n enableProtection()\n }\n }}\n >\n \n {isProtected ? \"lock\" : \"lock_open\"}\n \n \n )}\n {!!canProtect && (\n
    \n {\n if (isProtected) {\n disableProtection()\n } else {\n enableProtection()\n }\n }}\n >\n \n {isProtected ? \"lock\" : \"lock_open\"}\n \n {isProtected\n ? pgettext(\"markup editor\", \"Protected\")\n : pgettext(\"markup editor\", \"Protect\")}\n \n
    \n )}\n
    \n {preview ? (\n \n {pgettext(\"markup editor\", \"Edit\")}\n \n ) : (\n \n {pgettext(\"markup editor\", \"Preview\")}\n \n )}\n \n
    \n)\n\nexport default MarkupEditorFooter\n","import React from \"react\"\nimport modal from \"../../services/modal\"\nimport FormGroup from \"../form-group\"\nimport { replaceSelection } from \"./operations\"\n\nclass MarkupCodeModal extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n error: null,\n syntax: \"\",\n text: props.selection.text,\n }\n }\n\n handleSubmit = (ev) => {\n ev.preventDefault()\n\n const { selection, update } = this.props\n const syntax = this.state.syntax.trim()\n const text = this.state.text.trim()\n\n if (text.length === 0) {\n this.setState({ error: gettext(\"This field is required.\") })\n return false\n }\n\n const prefix = selection.prefix.trim().length ? \"\\n\\n\" : \"\"\n\n replaceSelection(\n Object.assign({}, selection, { text }),\n update,\n prefix + \"```\" + syntax + \"\\n\" + text + \"\\n```\\n\\n\"\n )\n\n modal.hide()\n\n return false\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    {pgettext(\"markup editor\", \"Code\")}

    \n
    \n
    \n
    \n \n \n this.setState({ syntax: event.target.value })\n }\n >\n \n {LANGUAGES.map(({ value, name }) => (\n \n ))}\n \n \n \n \n this.setState({ text: event.target.value })\n }\n />\n \n
    \n
    \n \n {pgettext(\"markup editor\", \"Cancel\")}\n \n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nconst LANGUAGES = [\n { value: \"bash\", name: \"Bash\" },\n { value: \"c\", name: \"C\" },\n { value: \"c#\", name: \"C#\" },\n { value: \"c++\", name: \"C++\" },\n { value: \"css\", name: \"CSS\" },\n { value: \"diff\", name: \"Diff\" },\n { value: \"go\", name: \"Go\" },\n { value: \"graphql\", name: \"GraphQL\" },\n { value: \"html,\", name: \"HTML\" },\n { value: \"xml\", name: \"XML\" },\n { value: \"json\", name: \"JSON\" },\n { value: \"java\", name: \"Java\" },\n { value: \"javascript\", name: \"JavaScript\" },\n { value: \"kotlin\", name: \"Kotlin\" },\n { value: \"less\", name: \"Less\" },\n { value: \"lua\", name: \"Lua\" },\n { value: \"makefile\", name: \"Makefile\" },\n { value: \"markdown\", name: \"Markdown\" },\n { value: \"objective-C\", name: \"Objective-C\" },\n { value: \"php\", name: \"PHP\" },\n { value: \"perl\", name: \"Perl\" },\n { value: \"plain\", name: \"Plain\" },\n { value: \"text\", name: \"text\" },\n { value: \"python\", name: \"Python\" },\n { value: \"repl\", name: \"REPL\" },\n { value: \"r\", name: \"R\" },\n { value: \"ruby\", name: \"Ruby\" },\n { value: \"rust\", name: \"Rust\" },\n { value: \"scss\", name: \"SCSS\" },\n { value: \"sql\", name: \"SQL\" },\n { value: \"shell\", name: \"Shell Session\" },\n { value: \"swift\", name: \"Swift\" },\n { value: \"toml\", name: \"TOML\" },\n { value: \"ini\", name: \"INI\" },\n { value: \"typescript\", name: \"TypeScript\" },\n { value: \"visualbasic\", name: \"Visual Basic .NET\" },\n { value: \"webassembly\", name: \"WebAssembly\" },\n { value: \"yaml\", name: \"YAML\" },\n]\n\nexport default MarkupCodeModal\n","import React from \"react\"\nimport formatFilesize from \"../../utils/file-size\"\n\nexport default function MarkupFormattingHelpModal() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"markup help\", \"Formatting help\")}\n

    \n
    \n
    \n

    {pgettext(\"markup help\", \"Emphasis text\")}

    \n \n \n {pgettext(\"markup help\", \"This text will have emphasis\")}\n \n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Bold text\")}

    \n \n \n {pgettext(\"markup help\", \"This text will be bold\")}\n \n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Removed text\")}

    \n \n \n {pgettext(\"markup help\", \"This text will be removed\")}\n \n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Bold text (BBCode)\")}

    \n \n {pgettext(\"markup help\", \"This text will be bold\")}\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Underlined text (BBCode)\")}

    \n \n {pgettext(\"markup help\", \"This text will be underlined\")}\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Italics text (BBCode)\")}

    \n \n {pgettext(\"markup help\", \"This text will be in italics\")}\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Link\")}

    \n \"\n result={\n

    \n example.com\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Link with text\")}

    \n \n {pgettext(\"markup help\", \"Link text\")}\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Link (BBCode)\")}

    \n \n example.com\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Link with text (BBCode)\")}

    \n \n {pgettext(\"markup help\", \"Link text\")}\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Image\")}

    \n \n \"\"\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Image with alternate text\")}

    \n \n \n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Image (BBCode)\")}

    \n \n \"\"\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Mention user by their name\")}

    \n \n @username\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Heading 1\")}

    \n {pgettext(\"markup help\", \"First level heading\")}}\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Heading 2\")}

    \n {pgettext(\"markup help\", \"Second level heading\")}}\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Heading 3\")}

    \n {pgettext(\"markup help\", \"Third level heading\")}}\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Heading 4\")}

    \n {pgettext(\"markup help\", \"Fourth level heading\")}}\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Heading 5\")}

    \n {pgettext(\"markup help\", \"Fifth level heading\")}}\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Unordered list\")}

    \n \n
  • Lorem ipsum
  • \n
  • Dolor met
  • \n
  • Vulputate lectus
  • \n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Ordered list\")}

    \n \n
  • Lorem ipsum
  • \n
  • Dolor met
  • \n
  • Vulputate lectus
  • \n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Quote text\")}

    \n \" + pgettext(\"markup help\", \"Quoted text\")}\n result={\n
    \n

    {pgettext(\"markup help\", \"Quoted text\")}

    \n
    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Quote text (BBCode)\")}

    \n \n
    \n {gettext(\"Quoted message:\")}\n
    \n
    \n

    {pgettext(\"markup help\", \"Quoted text\")}

    \n
    \n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Quote text with author (BBCode)\")}

    \n \n
    \n {pgettext(\"markup help\", \"Quote author has written:\")}\n
    \n
    \n

    {pgettext(\"markup help\", \"Quoted text\")}

    \n
    \n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Spoiler\")}

    \n \n {pgettext(\"markup help\", \"Secret text\")}\n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Inline code\")}

    \n \n {pgettext(\"markup help\", \"Inline code\")}\n

    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Code block\")}

    \n \n alert(\"Hello world!\");\n \n }\n />\n\n
    \n\n

    \n {pgettext(\"markup help\", \"Code block with syntax highlighting\")}\n

    \n \n \n print(\"Hello world!\");\n \n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Code block (BBCode)\")}

    \n \n alert(\"Hello world!\");\n \n }\n />\n\n
    \n\n

    \n {pgettext(\n \"markup help\",\n \"Code block with syntax highlighting (BBCode)\"\n )}\n

    \n \n \n print(\"Hello world!\");\n \n \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Horizontal rule\")}

    \n \n

    Lorem ipsum

    \n
    \n

    Dolor met

    \n
    \n }\n />\n\n
    \n\n

    {pgettext(\"markup help\", \"Horizontal rule (BBCode)\")}

    \n \n

    Lorem ipsum

    \n
    \n

    Dolor met

    \n
    \n }\n />\n
    \n
    \n \n {pgettext(\"modal\", \"Close\")}\n \n
    \n
    \n
    \n )\n}\n\nfunction ExampleFormatting({ markup, result }) {\n return (\n
    \n
    \n
    \n          {markup}\n        
    \n
    \n
    \n
    {result}
    \n
    \n
    \n )\n}\n\nclass ExampleFormattingSpoiler extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n reveal: false,\n }\n }\n\n render() {\n return (\n \n
    \n

    {this.props.children}

    \n
    \n {!this.state.reveal && (\n
    \n {\n this.setState({ reveal: true })\n }}\n >\n {gettext(\"Reveal spoiler\")}\n \n
    \n )}\n \n )\n }\n}\n","const URL_PATTERN = new RegExp(\"^(((ftps?)|(https?))://)\", \"i\")\n\nexport default function isUrl(str) {\n return URL_PATTERN.test(str.trim())\n}\n","import React from \"react\"\nimport modal from \"../../services/modal\"\nimport FormGroup from \"../form-group\"\nimport isUrl from \"./isUrl\"\nimport { replaceSelection } from \"./operations\"\n\nclass MarkupImageModal extends React.Component {\n constructor(props) {\n super(props)\n\n const text = props.selection.text.trim()\n const textUrl = isUrl(text)\n\n this.state = {\n error: null,\n text: textUrl ? \"\" : text,\n url: textUrl ? text : \"\",\n }\n }\n\n handleSubmit = (ev) => {\n ev.preventDefault()\n\n const { selection, update } = this.props\n const text = this.state.text.trim()\n const url = this.state.url.trim()\n\n if (url.length === 0) {\n this.setState({ error: gettext(\"This field is required.\") })\n return false\n }\n\n if (text.length > 0) {\n replaceSelection(selection, update, \"![\" + text + \"](\" + url + \")\")\n } else {\n replaceSelection(selection, update, \"!(\" + url + \")\")\n }\n\n modal.hide()\n\n return false\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"markup editor\", \"Image\")}\n

    \n
    \n
    \n
    \n \n \n this.setState({ text: event.target.value })\n }\n />\n \n \n \n this.setState({ url: event.target.value })\n }\n />\n \n
    \n
    \n \n {pgettext(\"markup editor\", \"Cancel\")}\n \n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport default MarkupImageModal\n","import React from \"react\"\nimport modal from \"../../services/modal\"\nimport FormGroup from \"../form-group\"\nimport isUrl from \"./isUrl\"\nimport { replaceSelection } from \"./operations\"\n\nclass MarkupLinkModal extends React.Component {\n constructor(props) {\n super(props)\n\n const text = props.selection.text.trim()\n const textUrl = isUrl(text)\n\n this.state = {\n error: null,\n text: textUrl ? \"\" : text,\n url: textUrl ? text : \"\",\n }\n }\n\n handleSubmit = (ev) => {\n ev.preventDefault()\n\n const { selection, update } = this.props\n const text = this.state.text.trim()\n const url = this.state.url.trim()\n\n if (url.length === 0) {\n this.setState({ error: gettext(\"This field is required.\") })\n return false\n }\n\n if (text.length > 0) {\n replaceSelection(selection, update, \"[\" + text + \"](\" + url + \")\")\n } else {\n replaceSelection(selection, update, \"<\" + url + \">\")\n }\n\n modal.hide()\n\n return false\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    {pgettext(\"markup editor\", \"Link\")}

    \n
    \n
    \n
    \n \n \n this.setState({ text: event.target.value })\n }\n />\n \n \n \n this.setState({ url: event.target.value })\n }\n />\n \n
    \n
    \n \n {pgettext(\"markup editor\", \"Cancel\")}\n \n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport default MarkupLinkModal\n","import React from \"react\"\nimport modal from \"../../services/modal\"\nimport FormGroup from \"../form-group\"\nimport { replaceSelection } from \"./operations\"\n\nclass MarkupQuoteModal extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n error: null,\n author: \"\",\n text: props.selection.text,\n }\n }\n\n handleSubmit = (ev) => {\n ev.preventDefault()\n\n const { selection, update } = this.props\n const author = this.state.author.trim()\n const text = this.state.text.trim()\n\n if (text.length === 0) {\n this.setState({ error: gettext(\"This field is required.\") })\n return false\n }\n\n const prefix = selection.prefix.trim().length ? \"\\n\\n\" : \"\"\n\n if (author) {\n replaceSelection(\n selection,\n update,\n prefix + '[quote=\"' + author + '\"]\\n' + text + \"\\n[/quote]\\n\\n\"\n )\n } else {\n replaceSelection(\n selection,\n update,\n prefix + \"[quote]\\n\" + text + \"\\n[/quote]\\n\\n\"\n )\n }\n\n modal.hide()\n\n return false\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"markup editor\", \"Quote\")}\n

    \n
    \n
    \n
    \n \n \n this.setState({ author: event.target.value })\n }\n />\n \n \n \n this.setState({ text: event.target.value })\n }\n />\n \n
    \n
    \n \n {pgettext(\"markup editor\", \"Cancel\")}\n \n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport default MarkupQuoteModal\n","import React from \"react\"\n\nconst MarkupEditorButton = ({ disabled, icon, title, onClick }) => (\n \n {icon}\n \n)\n\nexport default MarkupEditorButton\n","import moment from \"moment\"\nimport misago from \"../../\"\nimport ajax from \"../../services/ajax\"\nimport snackbar from \"../../services/snackbar\"\nimport formatFilesize from \"../../utils/file-size\"\nimport getRandomString from \"../../utils/getRandomString\"\n\nconst ID_LEN = 32\n\nconst uploadFile = (file, setState) => {\n const maxSize = misago.get(\"user\").acl.max_attachment_size * 1024\n\n if (file.size > maxSize) {\n snackbar.error(\n interpolate(\n pgettext(\n \"markup editor\",\n \"File %(filename)s is bigger than %(limit)s.\"\n ),\n { filename: file.name, limit: formatFilesize(maxSize) },\n true\n )\n )\n\n return\n }\n\n let upload = {\n id: null,\n key: getRandomString(ID_LEN),\n error: null,\n uploaded_on: null,\n progress: 0,\n filename: file.name,\n filetype: null,\n is_image: false,\n size: file.size,\n url: null,\n uploader_name: null,\n }\n\n setState(({ attachments }) => {\n return { attachments: [upload].concat(attachments) }\n })\n\n const refreshState = () => {\n setState(({ attachments }) => {\n return { attachments: attachments.concat() }\n })\n }\n\n const data = new FormData()\n data.append(\"upload\", file)\n\n ajax\n .upload(misago.get(\"ATTACHMENTS_API\"), data, (progress) => {\n upload.progress = progress\n refreshState()\n })\n .then(\n (data) => {\n Object.assign(upload, data, { uploaded_on: moment(data.uploaded_on) })\n refreshState()\n },\n (rejection) => {\n if (rejection.status === 400 || rejection.status === 413) {\n upload.error = rejection.detail\n snackbar.error(rejection.detail)\n refreshState()\n } else {\n snackbar.apiError(rejection)\n }\n }\n )\n}\n\nexport default uploadFile\n","import React from \"react\"\nimport misago from \"../../\"\nimport modal from \"../../services/modal\"\nimport MarkupCodeModal from \"./MarkupCodeModal\"\nimport MarkupFormattingHelpModal from \"./MarkupFormattingHelpModal\"\nimport MarkupImageModal from \"./MarkupImageModal\"\nimport MarkupLinkModal from \"./MarkupLinkModal\"\nimport MarkupQuoteModal from \"./MarkupQuoteModal\"\nimport MarkupEditorButton from \"./MarkupEditorButton\"\nimport { getSelection, replaceSelection, wrapSelection } from \"./operations\"\nimport uploadFile from \"./uploadFile\"\n\nconst MarkupEditorToolbar = ({\n disabled,\n element,\n update,\n updateAttachments,\n}) => {\n const actions = [\n {\n name: pgettext(\"markup editor\", \"Strong\"),\n icon: \"format_bold\",\n onClick: () => {\n wrapSelection(\n getSelection(element),\n update,\n \"**\",\n \"**\",\n pgettext(\"example markup\", \"Strong text\")\n )\n },\n },\n {\n name: pgettext(\"markup editor\", \"Emphasis\"),\n icon: \"format_italic\",\n onClick: () => {\n wrapSelection(\n getSelection(element),\n update,\n \"*\",\n \"*\",\n pgettext(\"example markup\", \"Text with emphasis\")\n )\n },\n },\n {\n name: pgettext(\"markup editor\", \"Strikethrough\"),\n icon: \"format_strikethrough\",\n onClick: () => {\n wrapSelection(\n getSelection(element),\n update,\n \"~~\",\n \"~~\",\n pgettext(\"example markup\", \"Text with strikethrough\")\n )\n },\n },\n {\n name: pgettext(\"markup editor\", \"Horizontal ruler\"),\n icon: \"remove\",\n onClick: () => {\n replaceSelection(getSelection(element), update, \"\\n\\n- - -\\n\\n\")\n },\n },\n {\n name: pgettext(\"markup editor\", \"Link\"),\n icon: \"insert_link\",\n onClick: () => {\n const selection = getSelection(element)\n modal.show(\n \n )\n },\n },\n {\n name: pgettext(\"markup editor\", \"Image\"),\n icon: \"insert_photo\",\n onClick: () => {\n const selection = getSelection(element)\n modal.show(\n \n )\n },\n },\n {\n name: pgettext(\"markup editor\", \"Quote\"),\n icon: \"format_quote\",\n onClick: () => {\n const selection = getSelection(element)\n modal.show(\n \n )\n },\n },\n {\n name: pgettext(\"markup editor\", \"Spoiler\"),\n icon: \"visibility_off\",\n onClick: () => {\n insertSpoiler(element, update)\n },\n },\n {\n name: pgettext(\"markup editor\", \"Code\"),\n icon: \"code\",\n onClick: () => {\n const selection = getSelection(element)\n modal.show(\n \n )\n },\n },\n ]\n\n if (misago.get(\"user\").acl.max_attachment_size) {\n actions.push({\n name: pgettext(\"markup editor\", \"Upload file\"),\n icon: \"file_upload\",\n onClick: () => uploadFiles(updateAttachments),\n })\n }\n\n return (\n
    \n
    \n {actions.map(({ name, icon, onClick }) => (\n \n ))}\n
    \n
    \n
    \n \n more_vert\n \n
      \n {actions.map(({ name, icon, onClick }) => (\n
    • \n \n {icon}\n {name}\n \n
    • \n ))}\n
    \n
    \n {\n modal.show()\n }}\n />\n
    \n
    \n )\n}\n\nconst insertSpoiler = (element, update) => {\n const selection = getSelection(element)\n const prefix = selection.prefix.trim().length ? \"\\n\\n\" : \"\"\n\n wrapSelection(\n selection,\n update,\n prefix + \"[spoiler]\\n\",\n \"\\n[/spoiler]\\n\\n\",\n pgettext(\"markup editor\", \"Spoiler text\")\n )\n}\n\nconst uploadFiles = (setState) => {\n const input = document.createElement(\"input\")\n input.type = \"file\"\n input.multiple = \"multiple\"\n\n input.addEventListener(\"change\", function () {\n for (let i = 0; i < input.files.length; i++) {\n uploadFile(input.files[i], setState)\n }\n })\n\n input.click()\n}\n\nexport default MarkupEditorToolbar\n","import React from \"react\"\nimport classnames from \"classnames\"\n\nimport misago from \"../../\"\nimport ajax from \"../../services/ajax\"\nimport snackbar from \"../../services/snackbar\"\nimport MisagoMarkup from \"../misago-markup\"\nimport MarkupEditorAttachments from \"./MarkupEditorAttachments\"\nimport MarkupEditorFooter from \"./MarkupEditorFooter\"\nimport MarkupEditorToolbar from \"./MarkupEditorToolbar\"\nimport uploadFile from \"./uploadFile\"\n\nclass MarkupEditor extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n element: null,\n focused: false,\n loading: false,\n preview: false,\n parsed: null,\n }\n }\n\n showPreview = () => {\n if (this.state.loading) return\n\n this.setState({ loading: true, preview: true, element: null })\n\n ajax.post(misago.get(\"PARSE_MARKUP_API\"), { post: this.props.value }).then(\n (data) => {\n this.setState({ loading: false, parsed: data.parsed })\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n } else {\n snackbar.apiError(rejection)\n }\n\n this.setState({ loading: false, preview: false })\n }\n )\n }\n\n closePreview = () => {\n this.setState({ loading: false, preview: false })\n }\n\n onDrop = (event) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (!event.dataTransfer.files) return\n\n const { onAttachmentsChange: setState } = this.props\n\n if (misago.get(\"user\").acl.max_attachment_size) {\n for (let i = 0; i < event.dataTransfer.files.length; i++) {\n const file = event.dataTransfer.files[i]\n uploadFile(file, setState)\n }\n }\n }\n\n onPaste = (event) => {\n const { onAttachmentsChange: setState } = this.props\n\n const files = []\n for (let i = 0; i < event.clipboardData.items.length; i++) {\n const item = event.clipboardData.items[i]\n if (item.kind === \"file\") {\n files.push(item.getAsFile())\n }\n }\n\n if (files.length) {\n event.preventDefault()\n event.stopPropagation()\n\n if (misago.get(\"user\").acl.max_attachment_size) {\n for (let i = 0; i < files.length; i++) {\n uploadFile(files[i], setState)\n }\n }\n }\n }\n\n render = () => (\n \n this.props.onChange({ target: { value } })}\n updateAttachments={this.props.onAttachmentsChange}\n />\n {this.state.preview ? (\n
    \n {this.state.loading ? (\n
    \n
    \n \n
    \n
    \n ) : (\n \n )}\n
    \n ) : (\n {\n if (element && this.state.element !== element) {\n this.setState({ element })\n setMentions(this.props, element)\n }\n }}\n onChange={this.props.onChange}\n onDrop={this.onDrop}\n onFocus={() => this.setState({ focused: true })}\n onPaste={this.onPaste}\n onBlur={() => this.setState({ focused: false })}\n />\n )}\n {this.props.attachments.length > 0 && (\n this.props.onChange({ target: { value } })}\n />\n )}\n \n
    \n )\n}\n\nfunction setMentions(props, element) {\n $(element).atwho({\n at: \"@\",\n displayTpl: '
  • \"\"${username}
  • ',\n insertTpl: \"@${username}\",\n searchKey: \"username\",\n callbacks: {\n remoteFilter: function (query, callback) {\n $.getJSON(misago.get(\"MENTION_API\"), { q: query }, callback)\n },\n },\n })\n\n $(element).on(\"inserted.atwho\", (event, _storage, source, controller) => {\n const { query } = controller\n const username = source.target.innerText.trim()\n const prefix = event.target.value.substr(0, query.headPos)\n const suffix = event.target.value.substr(query.endPos)\n\n event.target.value = prefix + username + suffix\n props.onChange(event)\n\n const caret = query.headPos + username.length\n event.target.setSelectionRange(caret, caret)\n event.target.focus()\n })\n}\n\nexport default MarkupEditor\n","import MarkupEditor from \"./MarkupEditor\"\n\nexport default MarkupEditor\n","import React from \"react\"\nimport classnames from \"classnames\"\n\nconst CLASS_ACTIVE = \"posting-active\"\nconst CLASS_DEFAULT = \"posting-default\"\nconst CLASS_MINIMIZED = \"posting-minimized\"\nconst CLASS_FULLSCREEN = \"posting-fullscreen\"\n\nclass PostingDialog extends React.Component {\n componentDidMount() {\n document.body.classList.add(CLASS_ACTIVE, CLASS_DEFAULT)\n }\n\n componentWillUnmount() {\n document.body.classList.remove(\n CLASS_ACTIVE,\n CLASS_DEFAULT,\n CLASS_MINIMIZED,\n CLASS_FULLSCREEN\n )\n }\n\n componentWillReceiveProps({ fullscreen, minimized }) {\n if (minimized) {\n document.body.classList.remove(CLASS_DEFAULT, CLASS_FULLSCREEN)\n document.body.classList.add(CLASS_MINIMIZED)\n } else {\n if (fullscreen) {\n document.body.classList.remove(CLASS_DEFAULT, CLASS_MINIMIZED)\n document.body.classList.add(CLASS_FULLSCREEN)\n } else {\n document.body.classList.remove(CLASS_FULLSCREEN, CLASS_MINIMIZED)\n document.body.classList.add(CLASS_DEFAULT)\n }\n }\n }\n\n render() {\n const { children, fullscreen, minimized } = this.props\n\n return (\n \n
    {children}
    \n \n )\n }\n}\n\nexport default PostingDialog\n","import React from \"react\"\n\nconst PostingDialogBody = ({ children }) => (\n
    {children}
    \n)\n\nexport default PostingDialogBody\n","import React from \"react\"\n\nconst PostingDialogError = ({ close, message }) => (\n
    \n
    \n error_outlined\n
    \n
    \n

    {message}

    \n \n
    \n
    \n)\n\nexport default PostingDialogError\n","import React from \"react\"\n\nconst PostingDialogHeader = ({\n children,\n close,\n fullscreen,\n minimize,\n minimized,\n fullscreenEnter,\n fullscreenExit,\n open,\n}) => (\n
    \n
    {children}
    \n {minimized ? (\n \n expand_less\n \n ) : (\n \n expand_more\n \n )}\n {fullscreen ? (\n \n fullscreen_exit\n \n ) : (\n \n fullscreen\n \n )}\n \n close\n \n
    \n)\n\nexport default PostingDialogHeader\n","import React from \"react\"\n\nexport default function PostingThreadOptions({\n isClosed,\n isHidden,\n isPinned,\n disabled,\n options,\n close,\n open,\n hide,\n unhide,\n pinGlobally,\n pinLocally,\n unpin,\n}) {\n const icons = getIcons(isClosed, isHidden, isPinned)\n\n return (\n
    \n \n {icons.length > 0 ? (\n \n {icons.map((icon) => (\n \n {icon}\n \n ))}\n \n ) : (\n more_horiz\n )}\n \n
      \n {options.pin === 2 && isPinned !== 2 && (\n
    • \n \n bookmark\n {pgettext(\"post thread\", \"Pinned globally\")}\n \n
    • \n )}\n {options.pin >= isPinned && isPinned !== 1 && (\n
    • \n \n bookmark_outline\n {pgettext(\"post thread\", \"Pinned in category\")}\n \n
    • \n )}\n {options.pin >= isPinned && isPinned !== 0 && (\n
    • \n \n radio_button_unchecked\n {pgettext(\"post thread\", \"Not pinned\")}\n \n
    • \n )}\n {options.close && !!isClosed && (\n
    • \n \n lock_outline\n {pgettext(\"post thread\", \"Open\")}\n \n
    • \n )}\n {options.close && !isClosed && (\n
    • \n \n lock\n {pgettext(\"post thread\", \"Closed\")}\n \n
    • \n )}\n {options.hide && !!isHidden && (\n
    • \n \n visibility\n {pgettext(\"post thread\", \"Visible\")}\n \n
    • \n )}\n {options.hide && !isHidden && (\n
    • \n \n visibility_off\n {pgettext(\"post thread\", \"Hidden\")}\n \n
    • \n )}\n
    \n
    \n )\n}\n\nfunction getIcons(closed, hidden, pinned) {\n const icons = []\n if (pinned === 2) icons.push(\"bookmark\")\n if (pinned === 1) icons.push(\"bookmark_outline\")\n if (closed) icons.push(\"lock\")\n if (hidden) icons.push(\"visibility_off\")\n return icons\n}\n","import React from \"react\"\nimport CategorySelect from \"misago/components/category-select\"\nimport Form from \"misago/components/form\"\nimport * as attachments from \"./utils/attachments\"\nimport { getPostValidators, getTitleValidators } from \"./utils/validators\"\nimport ajax from \"misago/services/ajax\"\nimport posting from \"misago/services/posting\"\nimport snackbar from \"misago/services/snackbar\"\nimport MarkupEditor from \"../MarkupEditor\"\nimport { Toolbar, ToolbarItem, ToolbarSection } from \"../Toolbar\"\nimport PostingDialog from \"./PostingDialog\"\nimport PostingDialogBody from \"./PostingDialogBody\"\nimport PostingDialogError from \"./PostingDialogError\"\nimport PostingDialogHeader from \"./PostingDialogHeader\"\nimport PostingThreadOptions from \"./PostingThreadOptions\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isReady: false,\n isLoading: false,\n\n error: null,\n\n minimized: false,\n fullscreen: false,\n\n options: null,\n\n title: \"\",\n category: props.category || null,\n categories: [],\n post: \"\",\n attachments: [],\n close: false,\n hide: false,\n pin: 0,\n\n validators: {\n title: getTitleValidators(),\n post: getPostValidators(),\n },\n errors: {},\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.config).then(this.loadSuccess, this.loadError)\n }\n\n loadSuccess = (data) => {\n let category = null\n let options = null\n\n // hydrate categories, extract posting options\n const categories = data.map((item) => {\n // pick first category that allows posting and if it may, override it with initial one\n if (\n item.post !== false &&\n (!category || item.id == this.state.category)\n ) {\n category = item.id\n options = item.post\n }\n\n return Object.assign(item, {\n disabled: item.post === false,\n label: item.name,\n value: item.id,\n })\n })\n\n this.setState({\n isReady: true,\n options,\n\n categories,\n category,\n })\n }\n\n loadError = (rejection) => {\n this.setState({\n error: rejection.detail,\n })\n }\n\n onCancel = () => {\n const formEmpty = !!(\n this.state.post.length === 0 &&\n this.state.title.length === 0 &&\n this.state.attachments.length === 0\n )\n\n if (formEmpty) {\n this.minimize()\n return posting.close()\n }\n\n const cancel = window.confirm(\n pgettext(\"post thread\", \"Are you sure you want to discard thread?\")\n )\n if (cancel) {\n this.minimize()\n posting.close()\n }\n }\n\n onTitleChange = (event) => {\n this.changeValue(\"title\", event.target.value)\n }\n\n onCategoryChange = (event) => {\n const category = this.state.categories.find((item) => {\n return event.target.value == item.value\n })\n\n // if selected pin is greater than allowed, reduce it\n let pin = this.state.pin\n if (category.post.pin && category.post.pin < pin) {\n pin = category.post.pin\n }\n\n this.setState({\n category: category.id,\n categoryOptions: category.post,\n\n pin,\n })\n }\n\n onPostChange = (event) => {\n this.changeValue(\"post\", event.target.value)\n }\n\n onAttachmentsChange = (attachments) => {\n this.setState(attachments)\n }\n\n onClose = () => {\n this.changeValue(\"close\", true)\n }\n\n onOpen = () => {\n this.changeValue(\"close\", false)\n }\n\n onPinGlobally = () => {\n this.changeValue(\"pin\", 2)\n }\n\n onPinLocally = () => {\n this.changeValue(\"pin\", 1)\n }\n\n onUnpin = () => {\n this.changeValue(\"pin\", 0)\n }\n\n onHide = () => {\n this.changeValue(\"hide\", true)\n }\n\n onUnhide = () => {\n this.changeValue(\"hide\", false)\n }\n\n close = () => {\n this.minimize()\n posting.close()\n }\n\n minimize = () => {\n this.setState({ fullscreen: false, minimized: true })\n }\n\n open = () => {\n this.setState({ minimized: false })\n if (this.state.fullscreen) {\n }\n }\n\n fullscreenEnter = () => {\n this.setState({ fullscreen: true, minimized: false })\n }\n\n fullscreenExit = () => {\n this.setState({ fullscreen: false, minimized: false })\n }\n\n clean() {\n if (!this.state.title.trim().length) {\n snackbar.error(\n pgettext(\"posting form\", \"You have to enter thread title.\")\n )\n return false\n }\n\n if (!this.state.post.trim().length) {\n snackbar.error(pgettext(\"posting form\", \"You have to enter a message.\"))\n return false\n }\n\n const errors = this.validate()\n\n if (errors.title) {\n snackbar.error(errors.title[0])\n return false\n }\n\n if (errors.post) {\n snackbar.error(errors.post[0])\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.post(this.props.submit, {\n title: this.state.title,\n category: this.state.category,\n post: this.state.post,\n attachments: attachments.clean(this.state.attachments),\n close: this.state.close,\n hide: this.state.hide,\n pin: this.state.pin,\n })\n }\n\n handleSuccess(success) {\n this.setState({ isLoading: true })\n this.close()\n\n snackbar.success(pgettext(\"post thread\", \"Your thread has been posted.\"))\n window.location = success.url\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n const errors = [].concat(\n rejection.non_field_errors || [],\n rejection.category || [],\n rejection.title || [],\n rejection.post || [],\n rejection.attachments || []\n )\n\n snackbar.error(errors[0])\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n const dialogProps = {\n minimized: this.state.minimized,\n minimize: this.minimize,\n open: this.open,\n\n fullscreen: this.state.fullscreen,\n fullscreenEnter: this.fullscreenEnter,\n fullscreenExit: this.fullscreenExit,\n\n close: this.onCancel,\n }\n\n if (this.state.error) {\n return (\n \n \n \n )\n }\n\n if (!this.state.isReady) {\n return (\n \n
    \n \n \n \n \n \n \n \n \n \n \n \n \n {}}\n onChange={() => {}}\n />\n
    \n
    \n )\n }\n\n const showOptions = !!(\n this.state.options.close ||\n this.state.options.hide ||\n this.state.options.pin\n )\n\n return (\n \n
    \n \n \n \n \n \n \n \n \n \n \n {showOptions && (\n \n \n \n )}\n \n \n \n \n
    \n )\n }\n}\n\nconst PostingDialogStart = ({\n children,\n close,\n minimized,\n minimize,\n open,\n fullscreen,\n fullscreenEnter,\n fullscreenExit,\n}) => (\n \n \n {pgettext(\"post thread\", \"Start new thread\")}\n \n {children}\n \n)\n","export default function (usernames) {\n const normalisedNames = usernames\n .split(\",\")\n .map((i) => i.trim().toLowerCase())\n const removedBlanks = normalisedNames.filter((i) => i.length > 0)\n const removedDuplicates = removedBlanks.filter((name, pos) => {\n return removedBlanks.indexOf(name) == pos\n })\n\n return removedDuplicates\n}\n","import React from \"react\"\nimport Form from \"misago/components/form\"\nimport * as attachments from \"./utils/attachments\"\nimport cleanUsernames from \"./utils/usernames\"\nimport { getPostValidators, getTitleValidators } from \"./utils/validators\"\nimport ajax from \"misago/services/ajax\"\nimport posting from \"misago/services/posting\"\nimport snackbar from \"misago/services/snackbar\"\nimport MarkupEditor from \"../MarkupEditor\"\nimport { Toolbar, ToolbarItem, ToolbarSection } from \"../Toolbar\"\nimport PostingDialog from \"./PostingDialog\"\nimport PostingDialogBody from \"./PostingDialogBody\"\nimport PostingDialogHeader from \"./PostingDialogHeader\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n const to = (props.to || []).map((user) => user.username).join(\", \")\n\n this.state = {\n isLoading: false,\n\n error: null,\n\n minimized: false,\n fullscreen: false,\n\n to: to,\n title: \"\",\n post: \"\",\n attachments: [],\n\n validators: {\n title: getTitleValidators(),\n post: getPostValidators(),\n },\n errors: {},\n }\n }\n\n onCancel = () => {\n const formEmpty = !!(\n this.state.post.length === 0 &&\n this.state.title.length === 0 &&\n this.state.to.length === 0 &&\n this.state.attachments.length === 0\n )\n\n if (formEmpty) {\n return this.close()\n }\n\n const cancel = window.confirm(\n pgettext(\n \"post thread\",\n \"Are you sure you want to discard private thread?\"\n )\n )\n if (cancel) {\n this.close()\n }\n }\n\n onToChange = (event) => {\n this.changeValue(\"to\", event.target.value)\n }\n\n onTitleChange = (event) => {\n this.changeValue(\"title\", event.target.value)\n }\n\n onPostChange = (event) => {\n this.changeValue(\"post\", event.target.value)\n }\n\n onAttachmentsChange = (attachments) => {\n this.setState(attachments)\n }\n\n clean() {\n if (!cleanUsernames(this.state.to).length) {\n snackbar.error(\n pgettext(\"posting form\", \"You have to enter at least one recipient.\")\n )\n return false\n }\n\n if (!this.state.title.trim().length) {\n snackbar.error(\n pgettext(\"posting form\", \"You have to enter thread title.\")\n )\n return false\n }\n\n if (!this.state.post.trim().length) {\n snackbar.error(pgettext(\"posting form\", \"You have to enter a message.\"))\n return false\n }\n\n const errors = this.validate()\n\n if (errors.title) {\n snackbar.error(errors.title[0])\n return false\n }\n\n if (errors.post) {\n snackbar.error(errors.post[0])\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.post(this.props.submit, {\n to: cleanUsernames(this.state.to),\n title: this.state.title,\n post: this.state.post,\n attachments: attachments.clean(this.state.attachments),\n })\n }\n\n handleSuccess(success) {\n this.setState({ isLoading: true })\n this.close()\n\n snackbar.success(pgettext(\"post thread\", \"Your thread has been posted.\"))\n window.location = success.url\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n const errors = [].concat(\n rejection.non_field_errors || [],\n rejection.to || [],\n rejection.title || [],\n rejection.post || [],\n rejection.attachments || []\n )\n\n snackbar.error(errors[0])\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n close = () => {\n this.minimize()\n posting.close()\n }\n\n minimize = () => {\n this.setState({ fullscreen: false, minimized: true })\n }\n\n open = () => {\n this.setState({ minimized: false })\n if (this.state.fullscreen) {\n }\n }\n\n fullscreenEnter = () => {\n this.setState({ fullscreen: true, minimized: false })\n }\n\n fullscreenExit = () => {\n this.setState({ fullscreen: false, minimized: false })\n }\n\n render() {\n const dialogProps = {\n minimized: this.state.minimized,\n minimize: this.minimize,\n open: this.open,\n\n fullscreen: this.state.fullscreen,\n fullscreenEnter: this.fullscreenEnter,\n fullscreenExit: this.fullscreenExit,\n\n close: this.onCancel,\n }\n\n return (\n \n
    \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    \n )\n }\n}\n\nconst PostingDialogStartPrivate = ({\n children,\n close,\n minimized,\n minimize,\n open,\n fullscreen,\n fullscreenEnter,\n fullscreenExit,\n}) => (\n \n \n {pgettext(\"post thread\", \"Start private thread\")}\n \n {children}\n \n)\n","import React from \"react\"\nimport Form from \"misago/components/form\"\nimport * as attachments from \"./utils/attachments\"\nimport { getPostValidators } from \"./utils/validators\"\nimport ajax from \"misago/services/ajax\"\nimport posting from \"misago/services/posting\"\nimport snackbar from \"misago/services/snackbar\"\nimport MarkupEditor from \"../MarkupEditor\"\nimport PostingDialog from \"./PostingDialog\"\nimport PostingDialogBody from \"./PostingDialogBody\"\nimport PostingDialogError from \"./PostingDialogError\"\nimport PostingDialogHeader from \"./PostingDialogHeader\"\nimport { clearGlobalState, setGlobalState } from \"./globalState\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isReady: false,\n isLoading: false,\n\n error: null,\n\n minimized: false,\n fullscreen: false,\n\n post: this.props.default || \"\",\n attachments: [],\n\n validators: {\n post: getPostValidators(),\n },\n errors: {},\n }\n\n this.quoteText = \"\"\n }\n\n componentDidMount() {\n ajax\n .get(this.props.config, this.props.context || null)\n .then(this.loadSuccess, this.loadError)\n\n setGlobalState(false, this.onQuote)\n }\n\n componentWillUnmount() {\n clearGlobalState()\n }\n\n componentWillReceiveProps(nextProps) {\n const context = this.props.context\n const newContext = nextProps.context\n\n // User clicked \"reply\" instead of \"quote\"\n if (context && newContext && !newContext.reply) return\n\n ajax\n .get(nextProps.config, nextProps.context || null)\n .then(this.appendData, snackbar.apiError)\n }\n\n loadSuccess = (data) => {\n this.setState({\n isReady: true,\n\n post: data.post\n ? '[quote=\"@' + data.poster + '\"]\\n' + data.post + \"\\n[/quote]\"\n : this.state.post,\n })\n\n this.quoteText = data.post\n ? '[quote=\"@' + data.poster + '\"]\\n' + data.post + \"\\n[/quote]\"\n : this.state.post\n }\n\n loadError = (rejection) => {\n this.setState({\n error: rejection.detail,\n })\n }\n\n appendData = (data) => {\n const newPost = data.post\n ? '[quote=\"@' + data.poster + '\"]\\n' + data.post + \"\\n[/quote]\\n\\n\"\n : \"\"\n\n this.setState((prevState, props) => {\n if (prevState.post.length > 0) {\n return {\n post: prevState.post.trim() + \"\\n\\n\" + newPost,\n }\n }\n\n return {\n post: newPost,\n }\n })\n\n this.open()\n }\n\n onCancel = () => {\n // If only the quote text is on editor user didn't add anything\n // so no changes to discard\n const onlyQuoteTextInEditor = this.state.post === this.quoteText\n\n if (onlyQuoteTextInEditor && this.state.attachments.length === 0) {\n return this.close()\n }\n\n const cancel = window.confirm(\n pgettext(\"post reply\", \"Are you sure you want to discard your reply?\")\n )\n if (cancel) {\n this.close()\n }\n }\n\n onPostChange = (event) => {\n this.changeValue(\"post\", event.target.value)\n }\n\n onAttachmentsChange = (attachments) => {\n this.setState(attachments)\n }\n\n onQuote = (quote) => {\n this.setState(({ post }) => {\n if (post.length > 0) {\n return { post: post.trim() + \"\\n\\n\" + quote }\n }\n\n return { post: quote }\n })\n\n this.open()\n }\n\n clean() {\n if (!this.state.post.trim().length) {\n snackbar.error(pgettext(\"posting form\", \"You have to enter a message.\"))\n return false\n }\n\n const errors = this.validate()\n\n if (errors.post) {\n snackbar.error(errors.post[0])\n return false\n }\n\n return true\n }\n\n send() {\n setGlobalState(true, this.onQuote)\n\n return ajax.post(this.props.submit, {\n post: this.state.post,\n attachments: attachments.clean(this.state.attachments),\n })\n }\n\n handleSuccess(success) {\n this.setState({ isLoading: true })\n this.close()\n\n setGlobalState(false, this.onQuote)\n\n snackbar.success(pgettext(\"post reply\", \"Your reply has been posted.\"))\n window.location = success.url.index\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n const errors = [].concat(\n rejection.non_field_errors || [],\n rejection.post || [],\n rejection.attachments || []\n )\n\n snackbar.error(errors[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n setGlobalState(false, this.onQuote)\n }\n\n close = () => {\n this.minimize()\n posting.close()\n }\n\n minimize = () => {\n this.setState({ fullscreen: false, minimized: true })\n }\n\n open = () => {\n this.setState({ minimized: false })\n if (this.state.fullscreen) {\n }\n }\n\n fullscreenEnter = () => {\n this.setState({ fullscreen: true, minimized: false })\n }\n\n fullscreenExit = () => {\n this.setState({ fullscreen: false, minimized: false })\n }\n\n render() {\n const dialogProps = {\n thread: this.props.thread,\n\n minimized: this.state.minimized,\n minimize: this.minimize,\n open: this.open,\n\n fullscreen: this.state.fullscreen,\n fullscreenEnter: this.fullscreenEnter,\n fullscreenExit: this.fullscreenExit,\n\n close: this.onCancel,\n }\n\n if (this.state.error) {\n return (\n \n \n \n )\n }\n\n if (!this.state.isReady) {\n return (\n \n
    \n {}}\n onChange={() => {}}\n />\n
    \n
    \n )\n }\n\n return (\n \n \n \n \n \n )\n }\n}\n\nconst PostingDialogReply = ({\n children,\n close,\n minimized,\n minimize,\n open,\n fullscreen,\n fullscreenEnter,\n fullscreenExit,\n thread,\n}) => (\n \n \n {interpolate(\n pgettext(\"post reply\", \"Reply to: %(thread)s\"),\n { thread: thread.title },\n true\n )}\n \n {children}\n \n)\n","import React from \"react\"\nimport Form from \"misago/components/form\"\nimport * as attachments from \"./utils/attachments\"\nimport { getPostValidators } from \"./utils/validators\"\nimport ajax from \"misago/services/ajax\"\nimport posting from \"misago/services/posting\"\nimport snackbar from \"misago/services/snackbar\"\nimport MarkupEditor from \"../MarkupEditor\"\nimport PostingDialog from \"./PostingDialog\"\nimport PostingDialogBody from \"./PostingDialogBody\"\nimport PostingDialogError from \"./PostingDialogError\"\nimport PostingDialogHeader from \"./PostingDialogHeader\"\nimport { clearGlobalState, setGlobalState } from \"./globalState\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isReady: false,\n isLoading: false,\n\n error: false,\n\n minimized: false,\n fullscreen: false,\n\n post: \"\",\n attachments: [],\n protect: false,\n\n canProtect: false,\n\n validators: {\n post: getPostValidators(),\n },\n errors: {},\n }\n\n this.originalPost = \"\"\n }\n\n componentDidMount() {\n ajax.get(this.props.config).then(this.loadSuccess, this.loadError)\n\n setGlobalState(false, this.onQuote)\n }\n\n componentWillUnmount() {\n clearGlobalState()\n }\n\n componentWillReceiveProps(nextProps) {\n const context = this.props.context\n const newContext = nextProps.context\n\n if (context && newContext && context.reply === newContext.reply) return\n\n ajax\n .get(nextProps.config, nextProps.context || null)\n .then(this.appendData, snackbar.apiError)\n }\n\n loadSuccess = (data) => {\n this.originalPost = data.post\n\n this.setState({\n isReady: true,\n\n post: data.post,\n attachments: attachments.hydrate(data.attachments),\n protect: data.is_protected,\n\n canProtect: data.can_protect,\n })\n }\n\n loadError = (rejection) => {\n this.setState({\n error: rejection.detail,\n })\n }\n\n appendData = (data) => {\n const newPost = data.post\n ? '[quote=\"@' + data.poster + '\"]\\n' + data.post + \"\\n[/quote]\\n\\n\"\n : \"\"\n\n this.setState((prevState, props) => {\n if (prevState.post.length > 0) {\n return {\n post: prevState.post.trim() + \"\\n\\n\" + newPost,\n }\n }\n\n return {\n post: newPost,\n }\n })\n\n this.open()\n }\n\n onCancel = () => {\n const originalPostSameAsCurrentPost =\n this.state.originalPost === this.state.post\n const noAttachementsAdded = this.state.attachments.length === 0\n\n if (originalPostSameAsCurrentPost && noAttachementsAdded) {\n return this.close()\n }\n\n const cancel = window.confirm(\n pgettext(\"edit reply\", \"Are you sure you want to discard changes?\")\n )\n if (cancel) {\n this.close()\n }\n }\n\n onProtect = () => {\n this.setState({\n protect: true,\n })\n }\n\n onUnprotect = () => {\n this.setState({\n protect: false,\n })\n }\n\n onPostChange = (event) => {\n this.changeValue(\"post\", event.target.value)\n }\n\n onAttachmentsChange = (attachments) => {\n this.setState(attachments)\n }\n\n onQuote = (quote) => {\n this.setState(({ post }) => {\n if (post.length > 0) {\n return { post: post.trim() + \"\\n\\n\" + quote }\n }\n\n return { post: quote }\n })\n\n this.open()\n }\n\n clean() {\n if (!this.state.post.trim().length) {\n snackbar.error(pgettext(\"posting form\", \"You have to enter a message.\"))\n return false\n }\n\n const errors = this.validate()\n\n if (errors.post) {\n snackbar.error(errors.post[0])\n return false\n }\n\n return true\n }\n\n send() {\n setGlobalState(true, this.onQuote)\n\n return ajax.put(this.props.submit, {\n post: this.state.post,\n attachments: attachments.clean(this.state.attachments),\n protect: this.state.protect,\n })\n }\n\n handleSuccess(success) {\n this.setState({ isLoading: true })\n this.close()\n\n setGlobalState(false, this.onQuote)\n\n snackbar.success(pgettext(\"edit reply\", \"Reply has been edited.\"))\n window.location = success.url.index\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n const errors = [].concat(\n rejection.non_field_errors || [],\n rejection.category || [],\n rejection.title || [],\n rejection.post || [],\n rejection.attachments || []\n )\n\n snackbar.error(errors[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n setGlobalState(false, this.onQuote)\n }\n\n close = () => {\n this.minimize()\n posting.close()\n }\n\n minimize = () => {\n this.setState({ fullscreen: false, minimized: true })\n }\n\n open = () => {\n this.setState({ minimized: false })\n if (this.state.fullscreen) {\n }\n }\n\n fullscreenEnter = () => {\n this.setState({ fullscreen: true, minimized: false })\n }\n\n fullscreenExit = () => {\n this.setState({ fullscreen: false, minimized: false })\n }\n\n render() {\n const dialogProps = {\n post: this.props.post,\n\n minimized: this.state.minimized,\n minimize: this.minimize,\n open: this.open,\n\n fullscreen: this.state.fullscreen,\n fullscreenEnter: this.fullscreenEnter,\n fullscreenExit: this.fullscreenExit,\n\n close: this.onCancel,\n }\n\n if (this.state.error) {\n return (\n \n \n \n )\n }\n\n if (!this.state.isReady) {\n return (\n \n
    \n {}}\n onChange={() => {}}\n />\n
    \n
    \n )\n }\n\n return (\n \n \n this.setState({ protect: true })}\n disableProtection={() => this.setState({ protect: false })}\n value={this.state.post}\n submitText={pgettext(\"edit reply submit\", \"Edit reply\")}\n disabled={this.state.isLoading}\n onAttachmentsChange={this.onAttachmentsChange}\n onChange={this.onPostChange}\n />\n \n \n )\n }\n}\n\nconst PostingDialogEditReply = ({\n children,\n close,\n minimized,\n minimize,\n open,\n fullscreen,\n fullscreenEnter,\n fullscreenExit,\n post,\n}) => (\n \n \n {interpolate(\n pgettext(\"edit reply\", \"Edit reply by %(poster)s from %(date)s\"),\n {\n poster: post.poster ? post.poster.username : post.poster_name,\n date: post.posted_on.fromNow(),\n },\n true\n )}\n \n {children}\n \n)\n","import React from \"react\"\nimport PostingQuoteSelection from \"./PostingQuoteSelection\"\nimport getQuoteMarkup from \"./getQuoteMarkup\"\nimport { clearGlobalState, getGlobalState, setGlobalState } from \"./globalState\"\nimport Start from \"./start\"\nimport StartPrivate from \"./start-private\"\nimport Reply from \"./reply\"\nimport Edit from \"./edit\"\n\nexport default function (props) {\n switch (props.mode) {\n case \"START\":\n return \n\n case \"START_PRIVATE\":\n return \n\n case \"REPLY\":\n return \n\n case \"EDIT\":\n return \n\n default:\n return null\n }\n}\n\nexport {\n PostingQuoteSelection,\n clearGlobalState,\n getGlobalState,\n getQuoteMarkup,\n setGlobalState,\n}\n","import { maxLength, minLength } from \"misago/utils/validators\"\nimport misago from \"misago\"\n\nexport function getTitleValidators() {\n return [getTitleLengthMin(), getTitleLengthMax()]\n}\n\nexport function getPostValidators() {\n if (misago.get(\"SETTINGS\").post_length_max) {\n return [validatePostLengthMin(), validatePostLengthMax()]\n } else {\n return [validatePostLengthMin()]\n }\n}\n\nexport function getTitleLengthMin() {\n return minLength(\n misago.get(\"SETTINGS\").thread_title_length_min,\n (limitValue, length) => {\n const message = npgettext(\n \"thread title length validator\",\n \"Thread title should be at least %(limit_value)s character long (it has %(show_value)s).\",\n \"Thread title should be at least %(limit_value)s characters long (it has %(show_value)s).\",\n limitValue\n )\n\n return interpolate(\n message,\n {\n limit_value: limitValue,\n show_value: length,\n },\n true\n )\n }\n )\n}\n\nexport function getTitleLengthMax() {\n return maxLength(\n misago.get(\"SETTINGS\").thread_title_length_max,\n (limitValue, length) => {\n const message = npgettext(\n \"thread title length validator\",\n \"Thread title cannot be longer than %(limit_value)s character (it has %(show_value)s).\",\n \"Thread title cannot be longer than %(limit_value)s characters (it has %(show_value)s).\",\n limitValue\n )\n\n return interpolate(\n message,\n {\n limit_value: limitValue,\n show_value: length,\n },\n true\n )\n }\n )\n}\n\nexport function validatePostLengthMin() {\n return minLength(\n misago.get(\"SETTINGS\").post_length_min,\n (limitValue, length) => {\n const message = npgettext(\n \"post length validator\",\n \"Posted message should be at least %(limit_value)s character long (it has %(show_value)s).\",\n \"Posted message should be at least %(limit_value)s characters long (it has %(show_value)s).\",\n limitValue\n )\n\n return interpolate(\n message,\n {\n limit_value: limitValue,\n show_value: length,\n },\n true\n )\n }\n )\n}\n\nexport function validatePostLengthMax() {\n return maxLength(\n misago.get(\"SETTINGS\").post_length_max || 1000000,\n (limitValue, length) => {\n const message = npgettext(\n \"post length validator\",\n \"Posted message cannot be longer than %(limit_value)s character (it has %(show_value)s).\",\n \"Posted message cannot be longer than %(limit_value)s characters (it has %(show_value)s).\",\n limitValue\n )\n\n return interpolate(\n message,\n {\n limit_value: limitValue,\n show_value: length,\n },\n true\n )\n }\n )\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n getChoice() {\n let choice = null\n this.props.choices.map((item) => {\n if (item.value === this.props.value) {\n choice = item\n }\n })\n return choice\n }\n\n getIcon() {\n return this.getChoice().icon\n }\n\n getLabel() {\n return this.getChoice().label\n }\n\n change = (value) => {\n return () => {\n this.props.onChange({\n target: {\n value: value,\n },\n })\n }\n }\n\n render() {\n return (\n
    \n \n \n {this.getLabel()}\n \n
      \n {this.props.choices.map((item, i) => {\n return (\n
    • \n \n \n {item.label}\n \n
    • \n )\n })}\n
    \n
    \n )\n }\n}\n\nexport function Icon({ icon }) {\n if (!icon) return null\n\n return {icon}\n}\n","import React from \"react\"\nimport misago from \"misago/index\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport StartSocialAuth from \"misago/components/StartSocialAuth\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport showBannedPage from \"misago/utils/banned-page\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n showActivation: false,\n\n username: \"\",\n password: \"\",\n\n validators: {\n username: [],\n password: [],\n },\n }\n }\n\n clean() {\n if (!this.isValid()) {\n snackbar.error(pgettext(\"sign in modal\", \"Fill out both fields.\"))\n return false\n } else {\n return true\n }\n }\n\n send() {\n return ajax.post(misago.get(\"AUTH_API\"), {\n username: this.state.username,\n password: this.state.password,\n })\n }\n\n handleSuccess() {\n let form = $(\"#hidden-login-form\")\n\n form.append('')\n form.append('')\n\n // fill out form with user credentials and submit it, this will tell\n // Misago to redirect user back to right page, and will trigger browser's\n // key ring feature\n form.find('input[type=\"hidden\"]').val(ajax.getCsrfToken())\n form.find('input[name=\"redirect_to\"]').val(window.location.pathname)\n form.find('input[name=\"username\"]').val(this.state.username)\n form.find('input[name=\"password\"]').val(this.state.password)\n form.submit()\n\n // keep form loading\n this.setState({\n isLoading: true,\n })\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n if (rejection.code === \"inactive_admin\") {\n snackbar.info(rejection.detail)\n } else if (rejection.code === \"inactive_user\") {\n snackbar.info(rejection.detail)\n this.setState({\n showActivation: true,\n })\n } else if (rejection.code === \"banned\") {\n showBannedPage(rejection.detail)\n modal.hide()\n } else {\n snackbar.error(rejection.detail)\n }\n } else if (rejection.status === 403 && rejection.ban) {\n showBannedPage(rejection.ban)\n modal.hide()\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n getActivationButton() {\n if (!this.state.showActivation) return null\n\n return (\n \n {pgettext(\"sign in modal btn\", \"Activate account\")}\n \n )\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"sign in modal title\", \"Sign in\")}\n

    \n
    \n
    \n
    \n \n\n
    \n
    \n \n
    \n
    \n\n
    \n
    \n \n
    \n
    \n
    \n
    \n {this.getActivationButton()}\n \n {pgettext(\"sign in modal btn\", \"Sign in\")}\n \n \n {pgettext(\"sign in modal btn\", \"Forgot password?\")}\n \n
    \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n getClass() {\n return getStatusClassName(this.props.status)\n }\n\n render() {\n return {this.props.children}\n }\n}\n\nexport class StatusIcon extends React.Component {\n getIcon() {\n if (this.props.status.is_banned) {\n return \"remove_circle_outline\"\n } else if (this.props.status.is_hidden) {\n return \"help_outline\"\n } else if (this.props.status.is_online_hidden) {\n return \"label\"\n } else if (this.props.status.is_offline_hidden) {\n return \"label_outline\"\n } else if (this.props.status.is_online) {\n return \"lens\"\n } else if (this.props.status.is_offline) {\n return \"panorama_fish_eye\"\n }\n }\n\n render() {\n return {this.getIcon()}\n }\n}\n\nexport class StatusLabel extends React.Component {\n getHelp() {\n return getStatusDescription(this.props.user, this.props.status)\n }\n\n getLabel() {\n if (this.props.status.is_banned) {\n return pgettext(\"user status\", \"Banned\")\n } else if (this.props.status.is_hidden) {\n return pgettext(\"user status\", \"Hidden\")\n } else if (this.props.status.is_online_hidden) {\n return pgettext(\"user status\", \"Online (hidden)\")\n } else if (this.props.status.is_offline_hidden) {\n return pgettext(\"user status\", \"Offline (hidden)\")\n } else if (this.props.status.is_online) {\n return pgettext(\"user status\", \"Online\")\n } else if (this.props.status.is_offline) {\n return pgettext(\"user status\", \"Offline\")\n }\n }\n\n render() {\n return (\n \n {this.getLabel()}\n \n )\n }\n}\n\nexport function getStatusClassName(status) {\n let className = \"\"\n if (status.is_banned) {\n className = \"banned\"\n } else if (status.is_hidden) {\n className = \"offline\"\n } else if (status.is_online_hidden) {\n className = \"online\"\n } else if (status.is_offline_hidden) {\n className = \"offline\"\n } else if (status.is_online) {\n className = \"online\"\n } else if (status.is_offline) {\n className = \"offline\"\n }\n\n return \"user-status user-\" + className\n}\n\nexport function getStatusDescription(user, status) {\n if (status.is_banned) {\n if (status.banned_until) {\n return interpolate(\n pgettext(\"user status\", \"%(username)s is banned until %(ban_expires)s\"),\n {\n username: user.username,\n ban_expires: status.banned_until.format(\"LL, LT\"),\n },\n true\n )\n } else {\n return interpolate(\n pgettext(\"user status\", \"%(username)s is banned\"),\n {\n username: user.username,\n },\n true\n )\n }\n } else if (status.is_hidden) {\n return interpolate(\n pgettext(\"user status\", \"%(username)s is hiding presence\"),\n {\n username: user.username,\n },\n true\n )\n } else if (status.is_online_hidden) {\n return interpolate(\n pgettext(\"user status\", \"%(username)s is online (hidden)\"),\n {\n username: user.username,\n },\n true\n )\n } else if (status.is_offline_hidden) {\n return interpolate(\n pgettext(\n \"user status\",\n \"%(username)s was last seen %(last_click)s (hidden)\"\n ),\n {\n username: user.username,\n last_click: status.last_click.fromNow(),\n },\n true\n )\n } else if (status.is_online) {\n return interpolate(\n pgettext(\"user status\", \"%(username)s is online\"),\n {\n username: user.username,\n },\n true\n )\n } else if (status.is_offline) {\n return interpolate(\n pgettext(\"user status\", \"%(username)s was last seen %(last_click)s\"),\n {\n username: user.username,\n last_click: status.last_click.fromNow(),\n },\n true\n )\n }\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n getEmptyMessage() {\n if (this.props.emptyMessage) {\n return this.props.emptyMessage\n } else {\n return pgettext(\n \"username history empty\",\n \"Your account has no history of name changes.\"\n )\n }\n }\n\n render() {\n return (\n
    \n
      \n
    • \n {this.getEmptyMessage()}\n
    • \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\n\nexport default class extends React.Component {\n renderUserAvatar() {\n if (this.props.change.changed_by) {\n return (\n \n \n \n )\n } else {\n return (\n \n \n \n )\n }\n }\n\n renderUsername() {\n if (this.props.change.changed_by) {\n return (\n \n {this.props.change.changed_by.username}\n \n )\n } else {\n return (\n \n {this.props.change.changed_by_username}\n \n )\n }\n }\n\n render() {\n return (\n
  • \n
    {this.renderUserAvatar()}
    \n
    {this.renderUsername()}
    \n
    \n {this.props.change.old_username}\n arrow_forward\n {this.props.change.new_username}\n
    \n
    \n \n {this.props.change.changed_on.fromNow()}\n \n
    \n
  • \n )\n }\n}\n","import React from \"react\"\nimport Change from \"misago/components/username-history/change\"\n\nexport default class extends React.Component {\n render() {\n return (\n
    \n
      \n {this.props.changes.map((change) => {\n return \n })}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport * as random from \"misago/utils/random\"\n\nexport default class extends React.Component {\n shouldComponentUpdate() {\n return false\n }\n\n getClassName() {\n if (this.props.hiddenOnMobile) {\n return \"list-group-item hidden-xs hidden-sm\"\n } else {\n return \"list-group-item\"\n }\n }\n\n render() {\n return (\n
  • \n
    \n \n \n \n
    \n
    \n \n  \n \n
    \n
    \n \n  \n \n arrow_forward\n \n  \n \n
    \n
    \n \n  \n \n
    \n
  • \n )\n }\n}\n","import React from \"react\"\nimport ChangePreview from \"misago/components/username-history/change-preview\"\n\nexport default class extends React.Component {\n shouldComponentUpdate() {\n return false\n }\n\n render() {\n return (\n
    \n
      \n {[0, 1, 2].map((i) => {\n return 0} key={i} />\n })}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport ListEmpty from \"misago/components/username-history/list-empty\"\nimport ListReady from \"misago/components/username-history/list-ready\"\nimport ListPreview from \"misago/components/username-history/list-preview\"\n\nexport default class extends React.Component {\n render() {\n if (this.props.isLoaded) {\n if (this.props.changes.length) {\n return \n } else {\n return \n }\n } else {\n return \n }\n }\n}\n","import React from \"react\"\nimport UserStatus, { StatusLabel } from \"misago/components/user-status\"\n\nexport default function ({ showStatus, user }) {\n return (\n
      \n \n \n
    • \n \n \n \n
    \n )\n}\n\nexport function Status({ showStatus, user }) {\n if (!showStatus) return null\n\n return (\n
  • \n \n \n \n
  • \n )\n}\n\nexport function JoinDate({ user }) {\n const { joined_on } = user\n\n let title = interpolate(\n pgettext(\"users list item\", \"Joined on %(joined_on)s\"),\n {\n joined_on: joined_on.format(\"LL, LT\"),\n },\n true\n )\n\n let message = interpolate(\n pgettext(\"users list item\", \"Joined %(joined_on)s\"),\n {\n joined_on: joined_on.fromNow(),\n },\n true\n )\n\n return (\n
  • \n {message}\n
  • \n )\n}\n\nexport function Posts({ user }) {\n const className = getStatClassName(\"user-stat-posts\", user.posts)\n const message = npgettext(\n \"users list item\",\n \"%(posts)s post\",\n \"%(posts)s posts\",\n user.posts\n )\n\n return (\n
  • \n {interpolate(\n message,\n {\n posts: user.posts,\n },\n true\n )}\n
  • \n )\n}\n\nexport function Threads({ user }) {\n const className = getStatClassName(\"user-stat-threads\", user.threads)\n const message = npgettext(\n \"users list item\",\n \"%(threads)s thread\",\n \"%(threads)s threads\",\n user.threads\n )\n\n return (\n
  • \n {interpolate(\n message,\n {\n threads: user.threads,\n },\n true\n )}\n
  • \n )\n}\n\nexport function Followers({ user }) {\n const className = getStatClassName(\"user-stat-followers\", user.followers)\n const message = npgettext(\n \"users list item\",\n \"%(followers)s follower\",\n \"%(followers)s followers\",\n user.followers\n )\n\n return (\n
  • \n {interpolate(\n message,\n {\n followers: user.followers,\n },\n true\n )}\n
  • \n )\n}\n\nexport function getStatClassName(className, stat) {\n if (stat === 0) {\n return className + \" user-stat-empty\"\n }\n return className\n}\n","import React from \"react\"\n\nexport default function ({ rank, title }) {\n let userTitle = title || rank.title || rank.name\n\n let className = \"user-title\"\n if (rank.css_class) {\n className += \" user-title-\" + rank.css_class\n }\n\n if (rank.is_tab) {\n return (\n \n {userTitle}\n \n )\n }\n\n return {userTitle}\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport Stats from \"./stats\"\nimport UserTitle from \"./user-title\"\n\nexport default function ({ showStatus, user }) {\n const { rank } = user\n\n let className = \"panel user-card\"\n if (rank.css_class) {\n className += \" user-card-\" + rank.css_class\n }\n\n return (\n
    \n
    \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    \n
    \n \n \n \n
    \n\n \n
    \n \n
    \n\n
    \n \n
    \n
    \n
    \n
    \n
    \n )\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport * as random from \"misago/utils/random\"\n\nexport default class extends React.Component {\n shouldComponentUpdate() {\n return false\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    \n
    \n \n \n \n
    \n\n
    \n \n  \n \n
    \n
    \n \n  \n \n
    \n\n
    \n
      \n
    • \n \n  \n \n
    • \n
    • \n \n  \n \n
    • \n
    • \n
    • \n \n  \n \n
    • \n
    • \n \n  \n \n
    • \n
    \n
    \n
    \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Card from \"./card\"\n\nexport default function ({ colClassName, cols }) {\n const list = Array.apply(null, { length: cols }).map(Number.call, Number)\n\n return (\n
    \n
    \n {list.map((i) => {\n let className = colClassName\n if (i !== 0) className += \" hidden-xs\"\n if (i === 3) className += \" hidden-sm\"\n\n return (\n
    \n \n
    \n )\n })}\n
    \n
    \n )\n}\n","import React from \"react\"\nimport Card from \"./card\"\nimport Preview from \"./preview\"\n\nexport default function ({ cols, isReady, showStatus, users }) {\n let colClassName = \"col-xs-12 col-sm-4\"\n if (cols === 4) {\n colClassName += \" col-md-3\"\n }\n\n if (!isReady) {\n return \n }\n\n return (\n
    \n
    \n {users.map((user) => {\n return (\n
    \n \n
    \n )\n })}\n
    \n
    \n )\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n dropdown: false,\n }\n }\n\n toggleNav = () => {\n this.setState({\n dropdown: !this.state.dropdown,\n })\n }\n\n hideNav = () => {\n this.setState({\n dropdown: false,\n })\n }\n\n getCompactNavClassName() {\n if (this.state.dropdown) {\n return \"compact-nav open\"\n } else {\n return \"compact-nav\"\n }\n }\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n getClassName() {\n if (this.props.value) {\n return \"btn btn-yes-no btn-yes-no-on\"\n } else {\n return \"btn btn-yes-no btn-yes-no-off\"\n }\n }\n\n getIcon() {\n if (!!this.props.value) {\n return this.props.iconOn || \"check_box\"\n } else {\n return this.props.iconOff || \"check_box_outline_blank\"\n }\n }\n\n getLabel() {\n if (!!this.props.value) {\n return this.props.labelOn || pgettext(\"yesno switch choice\", \"yes\")\n } else {\n return this.props.labelOff || pgettext(\"yesno switch choice\", \"no\")\n }\n }\n\n toggle = () => {\n this.props.onChange({\n target: {\n value: !this.props.value,\n },\n })\n }\n\n render() {\n return (\n \n {this.getIcon()}\n {this.getLabel()}\n \n )\n }\n}\n","class OrderedList {\n constructor(items) {\n this.isOrdered = false\n this._items = items || []\n }\n\n add(key, item, order) {\n this._items.push({\n key: key,\n item: item,\n\n after: order ? order.after || null : null,\n before: order ? order.before || null : null,\n })\n }\n\n get(key, value) {\n for (var i = 0; i < this._items.length; i++) {\n if (this._items[i].key === key) {\n return this._items[i].item\n }\n }\n\n return value\n }\n\n has(key) {\n return this.get(key) !== undefined\n }\n\n values() {\n var values = []\n for (var i = 0; i < this._items.length; i++) {\n values.push(this._items[i].item)\n }\n return values\n }\n\n order(values_only) {\n if (!this.isOrdered) {\n this._items = this._order(this._items)\n this.isOrdered = true\n }\n\n if (values_only || typeof values_only === \"undefined\") {\n return this.values()\n } else {\n return this._items\n }\n }\n\n orderedValues() {\n return this.order(true)\n }\n\n _order(unordered) {\n // Index of unordered items\n var index = []\n unordered.forEach(function (item) {\n index.push(item.key)\n })\n\n // Ordered items\n var ordered = []\n var ordering = []\n\n // First pass: register items that\n // don't specify their order\n unordered.forEach(function (item) {\n if (!item.after && !item.before) {\n ordered.push(item)\n ordering.push(item.key)\n }\n })\n\n // Second pass: register items that\n // specify their before to \"_end\"\n unordered.forEach(function (item) {\n if (item.before === \"_end\") {\n ordered.push(item)\n ordering.push(item.key)\n }\n })\n\n // Third pass: keep iterating items\n // until we hit iterations limit or finish\n // ordering list\n function insertItem(item) {\n var insertAt = -1\n if (ordering.indexOf(item.key) === -1) {\n if (item.after) {\n insertAt = ordering.indexOf(item.after)\n if (insertAt !== -1) {\n insertAt += 1\n }\n } else if (item.before) {\n insertAt = ordering.indexOf(item.before)\n }\n\n if (insertAt !== -1) {\n ordered.splice(insertAt, 0, item)\n ordering.splice(insertAt, 0, item.key)\n }\n }\n }\n\n var iterations = 200\n while (iterations > 0 && index.length !== ordering.length) {\n iterations -= 1\n unordered.forEach(insertItem)\n }\n\n return ordered\n }\n}\n\nexport default OrderedList\n","import \"bootstrap/js/transition\"\nimport \"bootstrap/js/affix\"\nimport \"bootstrap/js/modal\"\nimport \"bootstrap/js/dropdown\"\nimport \"at-js\"\nimport \"cropit\"\nimport \"jquery-caret\"\nimport OrderedList from \"misago/utils/ordered-list\"\nimport \"misago/style/index.less\"\n\nexport class Misago {\n constructor() {\n this._initializers = []\n this._context = {}\n }\n\n addInitializer(initializer) {\n this._initializers.push({\n key: initializer.name,\n\n item: initializer.initializer,\n\n after: initializer.after,\n before: initializer.before,\n })\n }\n\n init(context) {\n this._context = context\n\n var initOrder = new OrderedList(this._initializers).orderedValues()\n initOrder.forEach((initializer) => {\n initializer(this)\n })\n }\n\n // context accessors\n has(key) {\n return !!this._context[key]\n }\n\n get(key, fallback) {\n if (this.has(key)) {\n return this._context[key]\n } else {\n return fallback || undefined\n }\n }\n\n pop(key) {\n if (this.has(key)) {\n let value = this._context[key]\n this._context[key] = null\n return value\n } else {\n return undefined\n }\n }\n}\n\n// create singleton\nvar misago = new Misago()\n\n// expose it globally\nwindow.misago = misago\n\n// and export it for tests and stuff\nexport default misago\n","import misago from \"misago/index\"\nimport ajax from \"misago/services/ajax\"\n\nexport default function initializer() {\n ajax.init(misago.get(\"CSRF_COOKIE_NAME\"))\n}\n\nmisago.addInitializer({\n name: \"ajax\",\n initializer: initializer,\n})\n","import misago from \"misago/index\"\nimport { patch } from \"misago/reducers/auth\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nconst AUTH_SYNC_RATE = 45 // sync user with backend every 45 seconds\n\nexport default function initializer(context) {\n if (context.get(\"isAuthenticated\")) {\n window.setInterval(function () {\n ajax.get(context.get(\"AUTH_API\")).then(\n function (data) {\n store.dispatch(patch(data))\n },\n function (rejection) {\n snackbar.apiError(rejection)\n }\n )\n }, AUTH_SYNC_RATE * 1000)\n }\n}\n\nmisago.addInitializer({\n name: \"auth-sync\",\n initializer: initializer,\n after: \"auth\",\n})\n","import misago from \"misago/index\"\nimport auth from \"misago/services/auth\"\nimport modal from \"misago/services/modal\"\nimport store from \"misago/services/store\"\nimport storage from \"misago/services/local-storage\"\n\nexport default function initializer() {\n auth.init(store, storage, modal)\n}\n\nmisago.addInitializer({\n name: \"auth\",\n initializer: initializer,\n after: \"store\",\n})\n","import misago from \"misago/index\"\nimport ajax from \"misago/services/ajax\"\nimport captcha from \"misago/services/captcha\"\nimport include from \"misago/services/include\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default function initializer(context) {\n captcha.init(context, ajax, include, snackbar)\n}\n\nmisago.addInitializer({\n name: \"captcha\",\n initializer: initializer,\n})\n","import React from \"react\"\nimport ajax from \"misago/services/ajax\"\n\nexport default class AcceptAgreement extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = { submiting: false }\n }\n\n handleDecline = () => {\n if (this.state.submiting) return\n\n const confirmation = window.confirm(\n pgettext(\n \"accept agreement prompt\",\n \"Declining will result in immediate deactivation and deletion of your account. This action is not reversible.\"\n )\n )\n if (!confirmation) return\n\n this.setState({ submiting: true })\n\n ajax.post(this.props.api, { accept: false }).then(() => {\n window.location.reload(true)\n })\n }\n\n handleAccept = () => {\n if (this.state.submiting) return\n\n this.setState({ submiting: true })\n\n ajax.post(this.props.api, { accept: true }).then(() => {\n window.location.reload(true)\n })\n }\n\n render() {\n return (\n
    \n \n {pgettext(\"accept agreement choice\", \"Decline\")}\n \n \n {pgettext(\"accept agreement choice\", \"Accept and continue\")}\n \n
    \n )\n }\n}\n","import React from \"react\"\nimport misago from \"misago/index\"\nimport AcceptAgreement from \"misago/components/accept-agreement\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer(context) {\n if (document.getElementById(\"required-agreement-mount\")) {\n mount(\n ,\n \"required-agreement-mount\",\n false\n )\n }\n}\n\nmisago.addInitializer({\n name: \"component:accept-agreement\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\n\nexport default class extends React.Component {\n refresh() {\n window.location.reload()\n }\n\n getMessage() {\n if (this.props.signedIn) {\n return interpolate(\n pgettext(\n \"auth message\",\n \"You have signed in as %(username)s. Please refresh the page before continuing.\"\n ),\n { username: this.props.signedIn.username },\n true\n )\n } else if (this.props.signedOut) {\n return interpolate(\n pgettext(\n \"auth message\",\n \"%(username)s, you have been signed out. Please refresh the page before continuing.\"\n ),\n { username: this.props.user.username },\n true\n )\n }\n }\n\n render() {\n let className = \"auth-message\"\n if (this.props.signedIn || this.props.signedOut) {\n className += \" show\"\n }\n\n return (\n
    \n
    \n

    {this.getMessage()}

    \n

    \n \n {pgettext(\"auth message\", \"Reload page\")}\n \n \n {\" \" + pgettext(\"auth message\", \"or press F5 key.\")}\n \n

    \n
    \n
    \n )\n }\n}\n\nexport function select(state) {\n return {\n user: state.auth.user,\n signedIn: state.auth.signedIn,\n signedOut: state.auth.signedOut,\n }\n}\n","import { connect } from \"react-redux\"\nimport misago from \"misago/index\"\nimport AuthMessage, { select } from \"misago/components/auth-message\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer() {\n mount(connect(select)(AuthMessage), \"auth-message-mount\")\n}\n\nmisago.addInitializer({\n name: \"component:auth-message\",\n initializer: initializer,\n after: \"store\",\n})\n","import misago from \"misago/index\"\nimport showBannedPage from \"misago/utils/banned-page\"\n\nexport default function initializer(context) {\n if (context.has(\"BAN_MESSAGE\")) {\n showBannedPage(context.get(\"BAN_MESSAGE\"), false)\n }\n}\n\nmisago.addInitializer({\n name: \"component:banmed-page\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\n\nexport default function (props) {\n return (\n
    \n
      \n
    • \n

      \n {pgettext(\n \"categories list\",\n \"No categories exist or you don't have permission to see them.\"\n )}\n

      \n
    • \n
    \n
    \n )\n}\n","import React from \"react\"\n\nexport default function ({ category }) {\n if (!category.description) return null\n\n return (\n \n )\n}\n","import React from \"react\"\n\nexport default function ({ category }) {\n return (\n
    \n {getIcon(category)}\n
    \n )\n}\n\nexport function getClassName(category) {\n if (category.is_read) {\n return \"read-status item-read\"\n }\n\n return \"read-status item-new\"\n}\n\nexport function getTitle(category) {\n if (category.is_closed) {\n if (category.is_read) {\n return pgettext(\n \"category status\",\n \"This category has no new posts. (closed)\"\n )\n }\n\n return pgettext(\"category status\", \"This category has new posts. (closed)\")\n }\n\n if (category.is_read) {\n return pgettext(\"category status\", \"This category has no new posts.\")\n }\n\n return pgettext(\"category status\", \"This category has new posts.\")\n}\n\nexport function getIcon(category) {\n if (category.is_closed) {\n if (category.is_read) {\n return \"lock_outline\"\n }\n\n return \"lock\"\n }\n\n if (category.is_read) {\n return \"chat_bubble_outline\"\n }\n\n return \"chat_bubble\"\n}\n","import React from \"react\"\nimport Description from \"./description\"\nimport Icon from \"./icon\"\n\nexport default function ({ category }) {\n return (\n
    \n
    \n
    \n \n
    \n
    \n

    \n {category.name}\n

    \n \n
    \n
    \n
    \n )\n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\n\nexport default function ({ category }) {\n return (\n
    \n \n \n \n \n
    \n )\n}\n\nexport function LastThread({ category }) {\n if (!category.acl.can_browse) return null\n if (!category.acl.can_see_all_threads) return null\n if (!category.last_thread_title) return null\n\n return (\n
    \n
    \n \n
    \n
    \n
    \n \n {category.last_thread_title}\n \n
    \n \n
    \n
    \n )\n}\n\nexport function LastPosterAvatar({ category }) {\n if (category.last_poster) {\n return (\n \n \n \n )\n }\n\n return (\n \n \n \n )\n}\n\nexport function LastPosterName({ category }) {\n if (category.last_poster) {\n return (\n \n {category.last_poster_name}\n \n )\n }\n\n return {category.last_poster_name}\n}\n\nexport function Empty({ category }) {\n if (!category.acl.can_browse) return null\n if (!category.acl.can_see_all_threads) return null\n if (category.last_thread_title) return null\n\n return (\n \n )\n}\n\nexport function Private({ category }) {\n if (!category.acl.can_browse) return null\n if (category.acl.can_see_all_threads) return null\n\n return (\n \n )\n}\n\nexport function Protected({ category }) {\n if (category.acl.can_browse) return null\n\n return (\n \n )\n}\n\nexport function Message({ message }) {\n return (\n
    \n
    \n info_outline\n
    \n
    \n

    {message}

    \n
    \n
    \n )\n}\n","import React from \"react\"\n\nexport default function ({ category }) {\n return (\n
    \n
      \n \n \n
    \n
    \n )\n}\n\nexport function Threads({ threads }) {\n const message = npgettext(\n \"category stats\",\n \"%(threads)s thread\",\n \"%(threads)s threads\",\n threads\n )\n\n return (\n
  • \n {interpolate(\n message,\n {\n threads: threads,\n },\n true\n )}\n
  • \n )\n}\n\nexport function Posts({ posts }) {\n const message = npgettext(\n \"category stats\",\n \"%(posts)s post\",\n \"%(posts)s posts\",\n posts\n )\n\n return (\n
  • \n {interpolate(\n message,\n {\n posts: posts,\n },\n true\n )}\n
  • \n )\n}\n","import React from \"react\"\n\nexport default function ({ category }) {\n let className = \"btn btn-default btn-block btn-sm btn-subcategory\"\n if (!category.is_read) {\n className += \" btn-subcategory-new\"\n }\n\n return (\n \n )\n}\n\nexport function getIcon(category) {\n if (category.is_closed) {\n if (category.is_read) {\n return \"lock_outline\"\n }\n\n return \"lock\"\n }\n\n if (category.is_read) {\n return \"chat_bubble_outline\"\n }\n\n return \"chat_bubble\"\n}\n","import React from \"react\"\nimport ListItem from \"./list-item\"\n\nexport default function ({ category, isFirst }) {\n if (isFirst) return null\n if (category.subcategories.length === 0) return null\n\n return (\n
    \n {category.subcategories.map((category) => {\n return \n })}\n
    \n )\n}\n","import React from \"react\"\nimport Main from \"./main\"\nimport LastThread from \"./last-thread\"\nimport Stats from \"./stats\"\nimport Subcategories from \"./subcategories\"\n\nexport default function ({ category, isFirst }) {\n let className = \"list-group-item\"\n\n if (category.description) {\n className += \" list-group-category-has-description\"\n } else {\n className += \" list-group-category-no-description\"\n }\n\n if (isFirst) {\n className += \" list-group-item-first\"\n }\n if (category.css_class) {\n className += \" list-group-category-has-flavor\"\n className += \" list-group-item-category-\" + category.css_class\n }\n\n return (\n
  • \n
    \n
    \n \n \n
    \n \n
  • \n )\n}\n","import React from \"react\"\nimport ListItem from \"./list-item\"\n\nexport default function ({ category }) {\n let className = \"list-group list-group-category\"\n if (category.css_class) {\n className += \" list-group-category-has-flavor\"\n className += \" list-group-category-\" + category.css_class\n }\n\n return (\n
      \n \n {category.subcategories.map((category) => {\n return (\n \n )\n })}\n
    \n )\n}\n","import React from \"react\"\nimport Category from \"./category\"\n\nexport default function ({ categories }) {\n return (\n
    \n {categories.map((category) => {\n return \n })}\n
    \n )\n}\n","import moment from \"moment\"\nimport React from \"react\"\nimport Blankslate from \"./blankslate\"\nimport CategoriesList from \"./categories-list\"\nimport misago from \"misago/index\"\nimport polls from \"misago/services/polls\"\n\nconst hydrate = function (category) {\n return Object.assign({}, category, {\n last_post_on: category.last_post_on ? moment(category.last_post_on) : null,\n subcategories: category.subcategories.map(hydrate),\n })\n}\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n categories: misago.get(\"CATEGORIES\").map(hydrate),\n }\n\n this.startPolling(misago.get(\"CATEGORIES_API\"))\n }\n\n startPolling(api) {\n polls.start({\n poll: \"categories\",\n url: api,\n frequency: 180 * 1000,\n update: this.update,\n })\n }\n\n update = (data) => {\n this.setState({\n categories: data.map(hydrate),\n })\n }\n\n render() {\n const { categories } = this.state\n\n if (categories.length === 0) {\n return \n }\n\n return \n }\n}\n\nexport function select(store) {\n return {\n tick: store.tick.tick,\n }\n}\n","import { connect } from \"react-redux\"\nimport Categories, { select } from \"misago/components/categories\"\nimport misago from \"misago/index\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer() {\n if (document.getElementById(\"categories-mount\")) {\n mount(connect(select)(Categories), \"categories-mount\")\n }\n}\n\nmisago.addInitializer({\n name: \"component:categories\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport modal from \"../services/modal\"\nimport SignInModal from \"./sign-in\"\n\nclass SignInModalAutoOpen extends React.Component {\n componentDidMount() {\n const query = window.document.location.search\n if (query === \"?modal=login\") {\n window.setTimeout(() => modal.show(), 300)\n }\n }\n\n render() {\n return null\n }\n}\n\nexport default SignInModalAutoOpen\n","import React from \"react\"\n\nexport default function NavbarBranding({ logo, logoXs, text, url }) {\n if (logo) {\n return (\n
    \n \n {text}\n \n
    \n )\n }\n\n return (\n
    \n {!!logoXs && (\n \n {text}\n \n )}\n {!!text && (\n \n {text}\n \n )}\n
    \n )\n}\n","import React from \"react\"\n\nexport default function NavbarExtraMenu({ items }) {\n return (\n
      \n {items.map((item, index) => (\n
    • \n \n {item.title}\n \n
    • \n ))}\n
    \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { DropdownFooter, DropdownHeader, DropdownPills } from \"../Dropdown\"\n\nexport default function NotificationsDropdownBody({\n children,\n showAll,\n showUnread,\n unread,\n}) {\n return (\n
    \n \n {pgettext(\"notifications title\", \"Notifications\")}\n \n \n \n {pgettext(\"notifications dropdown\", \"All\")}\n \n \n {pgettext(\"notifications dropdown\", \"Unread\")}\n \n \n {children}\n \n \n {pgettext(\"notifications\", \"See all notifications\")}\n \n \n
    \n )\n}\n\nfunction NotificationsDropdownBodyPill({ active, children, onClick }) {\n return (\n \n {children}\n \n )\n}\n","import React from \"react\"\nimport NotificationsFetch from \"../NotificationsFetch\"\nimport {\n NotificationsList,\n NotificationsListError,\n NotificationsListLoading,\n} from \"../NotificationsList\"\nimport NotificationsDropdownBody from \"./NotificationsDropdownBody\"\n\nexport default class NotificationsDropdown extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n unread: false,\n url: \"\",\n }\n }\n\n getApiUrl() {\n let url = misago.get(\"NOTIFICATIONS_API\") + \"?limit=20\"\n url += this.state.unread ? \"&filter=unread\" : \"\"\n return url\n }\n\n render = () => (\n this.setState({ unread: false })}\n showUnread={() => this.setState({ unread: true })}\n >\n \n {({ data, loading, error }) => {\n if (loading) {\n return \n }\n\n if (error) {\n return \n }\n\n return (\n \n )\n }}\n \n \n )\n}\n","import NotificationsDropdown from \"./NotificationsDropdown\"\n\nexport default NotificationsDropdown\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function NavbarNotificationsToggle({\n id,\n className,\n badge,\n url,\n active,\n onClick,\n}) {\n const title = !!badge\n ? pgettext(\"navbar\", \"You have unread notifications!\")\n : pgettext(\"navbar\", \"Open notifications\")\n\n return (\n \n {!!badge && {badge}}\n \n {!!badge ? \"notifications_active\" : \"notifications_none\"}\n \n \n )\n}\n","import React from \"react\"\nimport { Dropdown } from \"../Dropdown\"\nimport NotificationsDropdown from \"../NotificationsDropdown\"\nimport NavbarNotificationsToggle from \"./NavbarNotificationsToggle\"\n\nexport default function NavbarNotificationsDropdown({\n id,\n className,\n badge,\n url,\n}) {\n return (\n (\n {\n event.preventDefault()\n toggle()\n }}\n />\n )}\n menuClassName=\"notifications-dropdown\"\n menuAlignRight\n >\n {({ isOpen }) => }\n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function NavbarPrivateThreads({\n id,\n className,\n badge,\n url,\n active,\n onClick,\n}) {\n const title = !!badge\n ? pgettext(\"navbar\", \"You have unread private threads!\")\n : pgettext(\"navbar\", \"Open private threads\")\n\n return (\n \n {!!badge && {badge}}\n inbox\n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function NavbarSearchToggle({\n id,\n className,\n url,\n active,\n onClick,\n}) {\n return (\n \n search\n \n )\n}\n","import React from \"react\"\nimport { Dropdown } from \"../Dropdown\"\nimport { SearchDropdown } from \"../Search\"\nimport NavbarSearchToggle from \"./NavbarSearchToggle\"\n\nexport default function NavbarSearchDropdown({ id, className, url }) {\n return (\n (\n {\n event.preventDefault()\n toggle()\n\n window.setTimeout(() => {\n document\n .querySelector(\".search-dropdown .form-control-search\")\n .focus()\n }, 0)\n }}\n />\n )}\n menuClassName=\"search-dropdown\"\n menuAlignRight\n >\n {() => }\n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\n\nexport default function NavbarSiteNavToggle({\n id,\n className,\n active,\n onClick,\n}) {\n return (\n \n menu\n \n )\n}\n","import React from \"react\"\nimport { Dropdown } from \"../Dropdown\"\nimport NavbarSiteNavToggle from \"./NavbarSiteNavToggle\"\nimport { SiteNavDropdown } from \"../SiteNav\"\n\nexport default function NavbarSiteNavDropdown({ id, className }) {\n return (\n (\n \n )}\n menuClassName=\"site-nav-dropdown\"\n menuAlignRight\n >\n {({ isOpen, close }) => }\n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport Avatar from \"../avatar\"\n\nexport default function NavbarUserNavToggle({\n id,\n className,\n user,\n active,\n onClick,\n}) {\n return (\n \n \n \n )\n}\n","import React from \"react\"\nimport { Dropdown } from \"../Dropdown\"\nimport NavbarUserNavToggle from \"./NavbarUserNavToggle\"\nimport { UserNavDropdown } from \"../UserNav\"\n\nexport default function NavbarUserNavDropdown({ id, className, user }) {\n return (\n (\n {\n event.preventDefault()\n toggle()\n }}\n />\n )}\n menuClassName=\"user-nav-dropdown\"\n menuAlignRight\n >\n {({ isOpen, close }) => }\n \n )\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport * as overlay from \"../../reducers/overlay\"\nimport RegisterButton from \"../RegisterButton\"\nimport SignInButton from \"../SignInButton\"\nimport SignInModalAutoOpen from \"../SignInModalAutoOpen\"\nimport NavbarBranding from \"./NavbarBranding\"\nimport NavbarExtraMenu from \"./NavbarExtraMenu\"\nimport NavbarNotificationsDropdown from \"./NavbarNotificationsDropdown\"\nimport NavbarNotificationsToggle from \"./NavbarNotificationsToggle\"\nimport NavbarPrivateThreads from \"./NavbarPrivateThreads\"\nimport NavbarSearchDropdown from \"./NavbarSearchDropdown\"\nimport NavbarSearchToggle from \"./NavbarSearchToggle\"\nimport NavbarSiteNavDropdown from \"./NavbarSiteNavDropdown\"\nimport NavbarSiteNavToggle from \"./NavbarSiteNavToggle\"\nimport NavbarUserNavDropdown from \"./NavbarUserNavDropdown\"\nimport NavbarUserNavToggle from \"./NavbarUserNavToggle\"\n\nexport function Navbar({\n dispatch,\n branding,\n extraMenuItems,\n authDelegated,\n user,\n searchUrl,\n notificationsUrl,\n privateThreadsUrl,\n showSearch,\n showPrivateThreads,\n}) {\n return (\n
    \n \n
    \n {extraMenuItems.length > 0 && (\n \n )}\n {!!showSearch && (\n \n )}\n {!!showSearch && (\n {\n dispatch(overlay.openSearch())\n event.preventDefault()\n }}\n />\n )}\n \n {\n dispatch(overlay.openSiteNav())\n }}\n />\n {!!showPrivateThreads && (\n \n )}\n {!!user && (\n \n )}\n {!!user && (\n {\n dispatch(overlay.openNotifications())\n event.preventDefault()\n }}\n />\n )}\n {!!user && (\n \n )}\n {!!user && (\n {\n dispatch(overlay.openUserNav())\n event.preventDefault()\n }}\n />\n )}\n {!user && }\n {!user && !authDelegated && (\n \n )}\n {!user && !authDelegated && }\n
    \n
    \n )\n}\n\nfunction select(state) {\n const settings = misago.get(\"SETTINGS\")\n const user = state.auth.user\n\n return {\n branding: {\n logo: settings.logo,\n logoXs: settings.logo_small,\n text: settings.logo_text,\n url: misago.get(\"MISAGO_PATH\"),\n },\n extraMenuItems: misago.get(\"extraMenuItems\"),\n\n user: !user.id\n ? null\n : {\n id: user.id,\n username: user.username,\n email: user.email,\n avatars: user.avatars,\n unreadNotifications: user.unreadNotifications,\n unreadPrivateThreads: user.unread_private_threads,\n url: user.url,\n },\n\n searchUrl: misago.get(\"SEARCH_URL\"),\n notificationsUrl: misago.get(\"NOTIFICATIONS_URL\"),\n privateThreadsUrl: misago.get(\"PRIVATE_THREADS_URL\"),\n\n authDelegated: settings.enable_oauth2_client,\n showSearch: !!user.acl.can_search,\n showPrivateThreads: !!user && !!user.acl.can_use_private_threads,\n }\n}\n\nconst NavbarConnected = connect(select)(Navbar)\n\nexport default NavbarConnected\n","import Navbar from \"./Navbar\"\n\nexport default Navbar\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport { Provider } from \"react-redux\"\nimport Navbar from \"../../components/Navbar\"\nimport store from \"../../services/store\"\n\nexport default function initializer(context) {\n const root = document.getElementById(\"misago-navbar\")\n ReactDOM.render(\n \n \n ,\n root\n )\n}\n\nmisago.addInitializer({\n name: \"component:navbar\",\n initializer: initializer,\n after: \"store\",\n})\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { DropdownFooter, DropdownPills } from \"../Dropdown\"\nimport { Overlay, OverlayHeader } from \"../Overlay\"\n\nexport default function NotificationsOverlayBody({\n children,\n open,\n showAll,\n showUnread,\n unread,\n}) {\n return (\n \n \n {pgettext(\"notifications title\", \"Notifications\")}\n \n \n \n {pgettext(\"notifications dropdown\", \"All\")}\n \n \n {pgettext(\"notifications dropdown\", \"Unread\")}\n \n \n {children}\n \n \n {pgettext(\"notifications\", \"See all notifications\")}\n \n \n \n )\n}\n\nfunction NotificationsOverlayBodyPill({ active, children, onClick }) {\n return (\n \n {children}\n \n )\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport NotificationsFetch from \"../NotificationsFetch\"\nimport {\n NotificationsList,\n NotificationsListError,\n NotificationsListLoading,\n} from \"../NotificationsList\"\nimport NotificationsOverlayBody from \"./NotificationsOverlayBody\"\n\nclass NotificationsOverlay extends React.Component {\n constructor(props) {\n super(props)\n\n this.body = document.body\n\n this.state = {\n unread: false,\n url: \"\",\n }\n }\n\n getApiUrl() {\n let url = misago.get(\"NOTIFICATIONS_API\") + \"?limit=20\"\n url += this.state.unread ? \"&filter=unread\" : \"\"\n return url\n }\n\n componentDidUpdate(prevProps, prevState) {\n if (prevProps.open !== this.props.open) {\n if (this.props.open) {\n this.body.classList.add(\"notifications-fullscreen\")\n } else {\n this.body.classList.remove(\"notifications-fullscreen\")\n }\n }\n }\n\n render = () => (\n this.setState({ unread: false })}\n showUnread={() => this.setState({ unread: true })}\n >\n \n {({ data, loading, error }) => {\n if (loading) {\n return \n }\n\n if (error) {\n return \n }\n\n return (\n \n )\n }}\n \n \n )\n}\n\nfunction select(state) {\n return { open: state.overlay.notifications }\n}\n\nconst NotificationsOverlayConnected = connect(select)(NotificationsOverlay)\n\nexport default NotificationsOverlayConnected\n","import NotificationsOverlay from \"./NotificationsOverlay\"\n\nexport default NotificationsOverlay\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport { Provider } from \"react-redux\"\nimport NotificationsOverlay from \"../../components/NotificationsOverlay\"\nimport store from \"../../services/store\"\n\nexport default function initializer(context) {\n if (context.get(\"isAuthenticated\")) {\n const root = document.getElementById(\"notifications-mount\")\n ReactDOM.render(\n \n \n ,\n root\n )\n }\n}\n\nmisago.addInitializer({\n name: \"component:notifications-overlay\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport { PageHeaderPlain } from \"../PageHeader\"\n\nexport default function NotificationsHeader() {\n return (\n \n )\n}\n","import PageTitle from \"./PageTitle\"\n\nexport default PageTitle\n","export default function PageTitle({ title, subtitle }) {\n const parts = []\n if (subtitle) {\n parts.push(subtitle)\n }\n if (title) {\n parts.push(title)\n }\n parts.push(misago.get(\"SETTINGS\").forum_name)\n\n document.title = parts.join(\" | \")\n return null\n}\n","import React from \"react\"\n\nexport default function PillsNav({ children }) {\n return
      {children}
    \n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport { Link } from \"react-router\"\n\nexport default function PillsNavLink({ active, link, icon, children }) {\n return (\n
  • \n \n {!!icon && {icon}}\n {children}\n \n
  • \n )\n}\n","import React from \"react\"\nimport { PillsNav, PillsNavLink } from \"../PillsNav\"\nimport { Toolbar, ToolbarSection, ToolbarItem } from \"../Toolbar\"\n\nexport default function NotificationsPills({ filter }) {\n const basename = misago.get(\"NOTIFICATIONS_URL\")\n\n return (\n \n \n \n \n \n {pgettext(\"notifications nav\", \"All\")}\n \n \n {pgettext(\"notifications nav\", \"Unread\")}\n \n \n {pgettext(\"notifications nav\", \"Read\")}\n \n \n \n \n \n )\n}\n","import React from \"react\"\nimport { Link } from \"react-router\"\n\nexport default function NotificationsPagination({ baseUrl, data, disabled }) {\n return (\n
    \n \n {pgettext(\"notifications pagination\", \"Latest\")}\n \n \n {pgettext(\"notifications pagination\", \"Newer\")}\n \n \n {pgettext(\"notifications pagination\", \"Older\")}\n \n
    \n )\n}\n\nfunction NotificationsPaginationLink({ disabled, children, url }) {\n if (disabled) {\n return (\n \n )\n }\n\n return (\n \n {children}\n \n )\n}\n","import classnames from \"classnames\"\nimport React from \"react\"\nimport Button from \"../button\"\nimport { Toolbar, ToolbarSection, ToolbarItem, ToolbarSpacer } from \"../Toolbar\"\nimport NotificationsPagination from \"./NotificationsPagination\"\n\nexport default function NotificationsToolbar({\n baseUrl,\n data,\n disabled,\n bottom,\n markAllAsRead,\n}) {\n return (\n \n \n \n \n \n \n \n \n \n \n done_all\n {pgettext(\"notifications\", \"Mark all as read\")}\n \n \n \n \n )\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport { updateAuthenticatedUser } from \"../../reducers/auth\"\nimport snackbar from \"../../services/snackbar\"\nimport { ApiMutation } from \"../Api\"\nimport NotificationsFetch from \"../NotificationsFetch\"\nimport PageTitle from \"../PageTitle\"\nimport PageContainer from \"../PageContainer\"\nimport {\n NotificationsList,\n NotificationsListError,\n NotificationsListLoading,\n} from \"../NotificationsList\"\nimport NotificationsPills from \"./NotificationsPills\"\nimport NotificationsToolbar from \"./NotificationsToolbar\"\n\nfunction NotificationsRoute({ dispatch, location, route }) {\n const { query } = location\n const { filter } = route.props\n\n const baseUrl = getBaseUrl(filter)\n\n return (\n \n \n\n \n\n \n {({ data, loading, error, refetch }) => (\n \n {(readAll, { loading: mutating }) => {\n const toolbarProps = {\n baseUrl,\n data,\n disabled:\n loading || mutating || !data || data.results.length === 0,\n markAllAsRead: async () => {\n const confirmed = window.confirm(\n pgettext(\"notifications\", \"Mark all notifications as read?\")\n )\n\n if (confirmed) {\n readAll({\n onSuccess: async () => {\n refetch()\n dispatch(\n updateAuthenticatedUser({ unreadNotifications: null })\n )\n snackbar.success(\n pgettext(\n \"notifications\",\n \"All notifications have been marked as read.\"\n )\n )\n },\n onError: snackbar.apiError,\n })\n }\n },\n }\n\n if (loading || mutating) {\n return (\n
    \n \n \n \n
    \n )\n }\n\n if (error) {\n return (\n
    \n \n \n \n
    \n )\n }\n\n if (data) {\n if (!data.hasPrevious && query) {\n window.history.replaceState({}, \"\", baseUrl)\n }\n\n return (\n
    \n \n \n \n
    \n )\n }\n\n return null\n }}\n
    \n )}\n
    \n
    \n )\n}\n\nfunction getSubtitle(filter) {\n if (filter === \"unread\") {\n return pgettext(\"notifications title\", \"Unread notifications\")\n } else if (filter === \"read\") {\n return pgettext(\"notifications title\", \"Read notifications\")\n } else {\n return null\n }\n}\n\nfunction getBaseUrl(filter) {\n let url = misago.get(\"NOTIFICATIONS_URL\")\n if (filter !== \"all\") {\n url += filter + \"/\"\n }\n return url\n}\n\nconst NotificationsRouteConnected = connect()(NotificationsRoute)\n\nexport default NotificationsRouteConnected\n","import Notifications from \"./Notifications\"\nimport NotificationsFetch from \"../NotificationsFetch/NotificationsFetch\"\n\nexport default Notifications\n\nexport { NotificationsFetch }\n","import React from \"react\"\nimport { Router, browserHistory } from \"react-router\"\nimport NotificationsHeader from \"./NotificationsHeader\"\nimport NotificationsRoute from \"./NotificationsRoute\"\n\nexport default function Notifications() {\n const basename = misago.get(\"NOTIFICATIONS_URL\")\n\n return (\n
    \n \n \n
    \n )\n}\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport { Provider } from \"react-redux\"\nimport Notifications from \"../../components/Notifications\"\nimport store from \"../../services/store\"\n\nexport default function initializer(context) {\n const basename = misago.get(\"NOTIFICATIONS_URL\")\n if (\n document.location.pathname.startsWith(basename) &&\n !document.location.pathname.startsWith(basename + \"disable-email/\") &&\n context.get(\"isAuthenticated\")\n ) {\n const root = document.getElementById(\"page-mount\")\n ReactDOM.render(\n \n \n ,\n root\n )\n }\n}\n\nmisago.addInitializer({\n name: \"component:notifications\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport { Link } from \"react-router\"\nimport Li from \"misago/components/li\"\n\nexport function SideNav(props) {\n return (\n
    \n {props.options.map((option) => {\n return (\n \n {option.icon}\n {option.name}\n \n )\n })}\n
    \n )\n}\n\nexport function CompactNav(props) {\n return (\n
      \n {props.options.map((option) => {\n return (\n \n \n {option.icon}\n {option.name}\n \n \n )\n })}\n
    \n )\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport ajax from \"misago/services/ajax\"\nimport title from \"misago/services/page-title\"\nimport snackbar from \"misago/services/snackbar\"\nimport misago from \"misago\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n password: \"\",\n }\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"delete your account title\", \"Delete account\"),\n parent: pgettext(\"forum options\", \"Change your options\"),\n })\n }\n\n onPasswordChange = (event) => {\n this.setState({ password: event.target.value })\n }\n\n handleSubmit = (event) => {\n event.preventDefault()\n\n const { isLoading, password } = this.state\n const { user } = this.props\n\n if (password.length == 0) {\n snackbar.error(pgettext(\"delete your account form\", \"Complete the form.\"))\n return false\n }\n\n if (isLoading) return false\n this.setState({ isLoading: true })\n\n ajax.post(user.api.delete, { password }).then(\n (success) => {\n window.location.href = misago.get(\"MISAGO_PATH\")\n },\n (rejection) => {\n this.setState({ isLoading: false })\n if (rejection.password) {\n snackbar.error(rejection.password[0])\n } else {\n snackbar.apiError(rejection)\n }\n }\n )\n }\n\n render() {\n return (\n
    \n
    \n
    \n

    \n {pgettext(\"delete your account title\", \"Delete account\")}\n

    \n
    \n
    \n

    \n {pgettext(\n \"delete your account form\",\n \"This form lets you delete your account. This action is not reversible.\"\n )}\n

    \n

    \n {pgettext(\n \"delete your account form\",\n \"Your account will be deleted together with its profile details, IP addresses and notifications.\"\n )}\n

    \n

    \n {pgettext(\n \"delete your account form\",\n \"Other content will NOT be deleted, but username displayed next to it will be changed to one shared by all deleted accounts.\"\n )}\n

    \n

    \n {pgettext(\n \"delete your account form\",\n \"Your username and e-maill address will become available again for use during registration or for other accounts to change to.\"\n )}\n

    \n
    \n
    \n
    \n \n \n \n \n
    \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Form from \"misago/components/edit-details\"\nimport title from \"misago/services/page-title\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends React.Component {\n componentDidMount() {\n title.set({\n title: pgettext(\"edit details\", \"Edit details\"),\n parent: pgettext(\"forum options\", \"Change your options\"),\n })\n }\n\n onSuccess = () => {\n snackbar.info(\n pgettext(\"profile details form\", \"Your details have been changed.\")\n )\n }\n\n render() {\n return (\n
    \n )\n }\n}\n","import React from \"react\"\nimport moment from \"moment\"\nimport Button from \"misago/components/button\"\nimport ajax from \"misago/services/ajax\"\nimport title from \"misago/services/page-title\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class DownloadData extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n isSubmiting: false,\n downloads: [],\n }\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"download your data title\", \"Download your data\"),\n parent: pgettext(\"forum options\", \"Change your options\"),\n })\n\n this.handleLoadDownloads()\n }\n\n handleLoadDownloads = () => {\n ajax.get(this.props.user.api.data_downloads).then(\n (data) => {\n this.setState({\n isLoading: false,\n downloads: data,\n })\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n }\n\n handleRequestDataDownload = () => {\n this.setState({ isSubmiting: true })\n ajax.post(this.props.user.api.request_data_download).then(\n () => {\n this.handleLoadDownloads()\n snackbar.success(\n pgettext(\n \"download your data\",\n \"Your request for data download has been registered.\"\n )\n )\n this.setState({ isSubmiting: false })\n },\n (rejection) => {\n snackbar.apiError(rejection)\n this.setState({ isSubmiting: false })\n }\n )\n }\n\n render() {\n return (\n
    \n
    \n
    \n

    \n {pgettext(\"download your data title\", \"Download your data\")}\n

    \n
    \n
    \n

    \n {pgettext(\n \"download your data\",\n 'To download your data from the site, click the \"Request data download\" button. Depending on amount of data to be archived and number of users wanting to download their data at same time it may take up to few days for your download to be prepared. An e-mail with notification will be sent to you when your data is ready to be downloaded.'\n )}\n

    \n\n

    \n {pgettext(\n \"download your data\",\n \"The download will only be available for limited amount of time, after which it will be deleted from the site and marked as expired.\"\n )}\n

    \n
    \n \n \n \n \n \n \n \n \n {this.state.downloads.map((item) => {\n return (\n \n \n \n \n )\n })}\n {this.state.downloads.length == 0 ? (\n \n \n \n ) : null}\n \n
    {pgettext(\"download your data table\", \"Requested on\")}\n {pgettext(\"download your data table\", \"Download\")}\n
    \n {moment(item.requested_on).fromNow()}\n \n \n
    \n {pgettext(\n \"download your data table\",\n \"You have no data downloads.\"\n )}\n
    \n
    \n \n {pgettext(\"download your data btn\", \"Request data download\")}\n \n
    \n
    \n
    \n )\n }\n}\n\nconst rowStyle = {\n verticalAlign: \"middle\",\n}\n\nconst STATUS_PENDING = 0\nconst STATUS_PROCESSING = 1\n\nconst DownloadButton = ({ exportFile, status }) => {\n if (status === STATUS_PENDING || status === STATUS_PROCESSING) {\n return (\n \n {pgettext(\"download your data table btn\", \"Download is being prepared\")}\n \n )\n }\n\n if (exportFile) {\n return (\n \n {pgettext(\"download your data table btn\", \"Download your data\")}\n \n )\n }\n\n return (\n \n {pgettext(\"download your data table btn\", \"Download is expired\")}\n \n )\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport Select from \"misago/components/select\"\nimport YesNoSwitch from \"misago/components/yes-no-switch\"\nimport { patch } from \"misago/reducers/auth\"\nimport ajax from \"misago/services/ajax\"\nimport title from \"misago/services/page-title\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nconst WATCH_CHOICES = [\n {\n value: 0,\n icon: \"notifications_none\",\n label: pgettext(\"watch thread choice\", \"No\"),\n },\n {\n value: 1,\n icon: \"notifications\",\n label: pgettext(\"watch thread choice\", \"Yes, with on site notifications\"),\n },\n {\n value: 2,\n icon: \"mail\",\n label: pgettext(\n \"watch thread choice\",\n \"Yes, with on site and e-mail notifications\"\n ),\n },\n]\n\nconst NOTIFICATION_CHOICES = [\n {\n value: 0,\n icon: \"notifications_none\",\n label: pgettext(\"notification preference\", \"Don't notify\"),\n },\n {\n value: 1,\n icon: \"notifications\",\n label: pgettext(\"notification preference\", \"Notify on site\"),\n },\n {\n value: 2,\n icon: \"mail\",\n label: pgettext(\n \"notification preference\",\n \"Notify on site and with e-mail\"\n ),\n },\n]\n\nexport default class ForumOptionsForm extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n is_hiding_presence: props.user.is_hiding_presence,\n limits_private_thread_invites_to:\n props.user.limits_private_thread_invites_to,\n\n watch_started_threads: props.user.watch_started_threads,\n watch_replied_threads: props.user.watch_replied_threads,\n watch_new_private_threads_by_followed:\n props.user.watch_new_private_threads_by_followed,\n watch_new_private_threads_by_other_users:\n props.user.watch_new_private_threads_by_other_users,\n notify_new_private_threads_by_followed:\n props.user.notify_new_private_threads_by_followed,\n notify_new_private_threads_by_other_users:\n props.user.notify_new_private_threads_by_other_users,\n\n errors: {},\n }\n\n this.privateThreadInvitesChoices = [\n {\n value: 0,\n icon: \"help_outline\",\n label: pgettext(\n \"private threads preference\",\n \"Anybody can invite me to their private threads\"\n ),\n },\n {\n value: 1,\n icon: \"done_all\",\n label: pgettext(\n \"private threads preference\",\n \"Only those I follow can invite me to their private threads\"\n ),\n },\n {\n value: 2,\n icon: \"highlight_off\",\n label: pgettext(\n \"private threads preference\",\n \"Nobody can invite me to their private threads\"\n ),\n },\n ]\n }\n\n send() {\n return ajax.post(this.props.user.api.options, {\n is_hiding_presence: this.state.is_hiding_presence,\n limits_private_thread_invites_to:\n this.state.limits_private_thread_invites_to,\n\n watch_started_threads: this.state.watch_started_threads,\n watch_replied_threads: this.state.watch_replied_threads,\n watch_new_private_threads_by_followed:\n this.state.watch_new_private_threads_by_followed,\n watch_new_private_threads_by_other_users:\n this.state.watch_new_private_threads_by_other_users,\n notify_new_private_threads_by_followed:\n this.state.notify_new_private_threads_by_followed,\n notify_new_private_threads_by_other_users:\n this.state.notify_new_private_threads_by_other_users,\n })\n }\n\n handleSuccess() {\n store.dispatch(\n patch({\n is_hiding_presence: this.state.is_hiding_presence,\n limits_private_thread_invites_to:\n this.state.limits_private_thread_invites_to,\n\n watch_started_threads: this.state.watch_started_threads,\n watch_replied_threads: this.state.watch_replied_threads,\n watch_new_private_threads_by_followed:\n this.state.watch_new_private_threads_by_followed,\n watch_new_private_threads_by_other_users:\n this.state.watch_new_private_threads_by_other_users,\n notify_new_private_threads_by_followed:\n this.state.notify_new_private_threads_by_followed,\n notify_new_private_threads_by_other_users:\n this.state.notify_new_private_threads_by_other_users,\n })\n )\n snackbar.success(\n pgettext(\"forum options form\", \"Your forum options have been changed.\")\n )\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n snackbar.error(\n pgettext(\"forum options form\", \"Please reload the page and try again.\")\n )\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"forum options title\", \"Forum options\"),\n parent: pgettext(\"forum options\", \"Change your options\"),\n })\n }\n\n render() {\n return (\n \n
    \n
    \n

    \n {pgettext(\"forum options form title\", \"Change forum options\")}\n

    \n
    \n
    \n
    \n \n {pgettext(\"forum options form\", \"Privacy settings\")}\n \n\n
    \n\n
    \n \n {pgettext(\"notifications options\", \"Notifications preferences\")}\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    \n
    \n
    \n \n
    \n
    \n \n )\n }\n}\n","import React from \"react\"\nimport PanelLoader from \"misago/components/panel-loader\"\n\nexport default function () {\n return (\n
    \n
    \n

    \n {pgettext(\"change username title\", \"Change username\")}\n

    \n
    \n \n
    \n )\n}\n","import React from \"react\"\nimport PanelMessage from \"misago/components/panel-message\"\n\nexport default class extends React.Component {\n getHelpText() {\n if (this.props.options.next_on) {\n return interpolate(\n pgettext(\n \"change username\",\n \"You will be able to change your username %(next_change)s.\"\n ),\n { next_change: this.props.options.next_on.fromNow() },\n true\n )\n } else {\n return pgettext(\n \"change username\",\n \"You have changed your name allowed number of times.\"\n )\n }\n }\n\n render() {\n return (\n
    \n
    \n

    \n {pgettext(\"change username title\", \"Change username\")}\n

    \n
    \n \n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport * as validators from \"misago/utils/validators\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n username: \"\",\n\n validators: {\n username: [\n validators.usernameContent(),\n validators.usernameMinLength(props.options.length_min),\n validators.usernameMaxLength(props.options.length_max),\n ],\n },\n\n isLoading: false,\n }\n }\n\n getHelpText() {\n let phrases = []\n\n if (this.props.options.changes_left > 0) {\n let message = npgettext(\n \"change username form\",\n \"You can change your username %(changes_left)s more time.\",\n \"You can change your username %(changes_left)s more times.\",\n this.props.options.changes_left\n )\n\n phrases.push(\n interpolate(\n message,\n {\n changes_left: this.props.options.changes_left,\n },\n true\n )\n )\n }\n\n if (this.props.user.acl.name_changes_expire > 0) {\n let message = npgettext(\n \"change username form\",\n \"Used changes become available again after %(name_changes_expire)s day.\",\n \"Used changes become available again after %(name_changes_expire)s days.\",\n this.props.user.acl.name_changes_expire\n )\n\n phrases.push(\n interpolate(\n message,\n {\n name_changes_expire: this.props.user.acl.name_changes_expire,\n },\n true\n )\n )\n }\n\n return phrases.length ? phrases.join(\" \") : null\n }\n\n clean() {\n let errors = this.validate()\n if (errors.username) {\n snackbar.error(errors.username[0])\n return false\n }\n if (this.state.username.trim() === this.props.user.username) {\n snackbar.info(\n pgettext(\"change username form\", \"New username is same as current one.\")\n )\n return false\n } else {\n return true\n }\n }\n\n send() {\n return ajax.post(this.props.user.api.username, {\n username: this.state.username,\n })\n }\n\n handleSuccess(success) {\n this.setState({\n username: \"\",\n })\n\n this.props.complete(success.username, success.slug, success.options)\n }\n\n handleError(rejection) {\n snackbar.apiError(rejection)\n }\n\n render() {\n return (\n
    \n
    \n
    \n

    \n {pgettext(\"change username title\", \"Change username\")}\n

    \n
    \n
    \n \n \n \n
    \n
    \n \n
    \n
    \n
    \n )\n }\n}\n","import moment from \"moment\"\nimport React from \"react\"\nimport FormLoading from \"misago/components/options/change-username/form-loading\"\nimport FormLocked from \"misago/components/options/change-username/form-locked\"\nimport Form from \"misago/components/options/change-username/form\"\nimport UsernameHistory from \"misago/components/username-history/root\"\nimport misago from \"misago/index\"\nimport { hydrate, addNameChange } from \"misago/reducers/username-history\"\nimport { updateUsername } from \"misago/reducers/users\"\nimport ajax from \"misago/services/ajax\"\nimport title from \"misago/services/page-title\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoaded: false,\n options: null,\n }\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"change username title\", \"Change username\"),\n parent: pgettext(\"forum options\", \"Change your options\"),\n })\n\n Promise.all([\n ajax.get(this.props.user.api.username),\n ajax.get(misago.get(\"USERNAME_CHANGES_API\"), {\n user: this.props.user.id,\n }),\n ]).then((data) => {\n store.dispatch(hydrate(data[1].results))\n\n this.setState({\n isLoaded: true,\n options: {\n changes_left: data[0].changes_left,\n length_min: data[0].length_min,\n length_max: data[0].length_max,\n next_on: data[0].next_on ? moment(data[0].next_on) : null,\n },\n })\n })\n }\n\n onComplete = (username, slug, options) => {\n this.setState({\n options,\n })\n\n store.dispatch(\n addNameChange({ username, slug }, this.props.user, this.props.user)\n )\n store.dispatch(updateUsername(this.props.user, username, slug))\n\n snackbar.success(\n pgettext(\"change username\", \"Your username has been changed.\")\n )\n }\n\n getChangeForm() {\n if (!this.state.isLoaded) {\n return \n }\n\n if (this.state.options.changes_left === 0) {\n return \n }\n\n return (\n \n )\n }\n\n render() {\n return (\n
    \n {this.getChangeForm()}\n \n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport * as validators from \"misago/utils/validators\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n new_email: \"\",\n password: \"\",\n\n validators: {\n new_email: [validators.email()],\n password: [],\n },\n\n isLoading: false,\n }\n }\n\n clean() {\n let errors = this.validate()\n let lengths = [\n this.state.new_email.trim().length,\n this.state.password.trim().length,\n ]\n\n if (lengths.indexOf(0) !== -1) {\n snackbar.error(pgettext(\"change email form\", \"Fill out all fields.\"))\n return false\n }\n\n if (errors.new_email) {\n snackbar.error(errors.new_email[0])\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.post(this.props.user.api.change_email, {\n new_email: this.state.new_email,\n password: this.state.password,\n })\n }\n\n handleSuccess(response) {\n this.setState({\n new_email: \"\",\n password: \"\",\n })\n\n snackbar.success(response.detail)\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n if (rejection.new_email) {\n snackbar.error(rejection.new_email)\n } else {\n snackbar.error(rejection.password)\n }\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n return (\n
    \n \n \n
    \n
    \n

    \n {pgettext(\"change email title\", \"Change e-mail address\")}\n

    \n
    \n
    \n \n \n \n\n
    \n\n \n \n \n
    \n
    \n \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n new_password: \"\",\n repeat_password: \"\",\n password: \"\",\n\n validators: {\n new_password: [],\n repeat_password: [],\n password: [],\n },\n\n isLoading: false,\n }\n }\n\n clean() {\n let errors = this.validate()\n let lengths = [\n this.state.new_password.trim().length,\n this.state.repeat_password.trim().length,\n this.state.password.trim().length,\n ]\n\n if (lengths.indexOf(0) !== -1) {\n snackbar.error(pgettext(\"change password form\", \"Fill out all fields.\"))\n return false\n }\n\n if (errors.new_password) {\n snackbar.error(errors.new_password[0])\n return false\n }\n\n if (this.state.new_password !== this.state.repeat_password) {\n snackbar.error(\n pgettext(\"change password form\", \"New passwords are different.\")\n )\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.post(this.props.user.api.change_password, {\n new_password: this.state.new_password,\n password: this.state.password,\n })\n }\n\n handleSuccess(response) {\n this.setState({\n new_password: \"\",\n repeat_password: \"\",\n password: \"\",\n })\n\n snackbar.success(response.detail)\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n if (rejection.new_password) {\n snackbar.error(rejection.new_password)\n } else {\n snackbar.error(rejection.password)\n }\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n return (\n
    \n \n \n
    \n
    \n

    \n {pgettext(\"change password title\", \"Change password\")}\n

    \n
    \n
    \n \n \n \n\n \n \n \n\n
    \n\n \n \n \n
    \n
    \n \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport misago from \"misago/index\"\n\nconst UnusablePasswordMessage = () => {\n return (\n
    \n
    \n

    \n {pgettext(\n \"change sign in credentials title\",\n \"Change e-mail or password\"\n )}\n

    \n
    \n
    \n
    \n info_outline\n
    \n
    \n

    \n {pgettext(\n \"change sign in credentials\",\n \"You need to set a password for your account to be able to change your e-mail or password.\"\n )}\n

    \n

    \n \n {pgettext(\"change sign in credentials link\", \"Set password\")}\n \n

    \n
    \n
    \n
    \n )\n}\n\nexport default UnusablePasswordMessage\n","import React from \"react\"\nimport ChangeEmail from \"misago/components/options/sign-in-credentials/change-email\"\nimport ChangePassword from \"misago/components/options/sign-in-credentials/change-password\"\nimport misago from \"misago/index\"\nimport title from \"misago/services/page-title\"\nimport UnusablePasswordMessage from \"./UnusablePasswordMessage\"\n\nexport default class extends React.Component {\n componentDidMount() {\n title.set({\n title: pgettext(\n \"change sign in credentials title\",\n \"Change e-mail or password\"\n ),\n parent: pgettext(\"forum options\", \"Change your options\"),\n })\n }\n\n render() {\n if (!this.props.user.has_usable_password) {\n return \n }\n\n return (\n \n )\n }\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport { SideNav, CompactNav } from \"misago/components/options/navs\"\nimport DeleteAccount from \"misago/components/options/delete-account\"\nimport EditDetails from \"misago/components/options/edit-details\"\nimport DownloadData from \"misago/components/options/download-data\"\nimport ChangeForumOptions from \"misago/components/options/forum-options\"\nimport ChangeUsername from \"misago/components/options/change-username/root\"\nimport ChangeSignInCredentials from \"misago/components/options/sign-in-credentials/root\"\nimport WithDropdown from \"misago/components/with-dropdown\"\nimport misago from \"misago/index\"\nimport { FlexRow, FlexRowCol, FlexRowSection } from \"../FlexRow\"\nimport PageContainer from \"../PageContainer\"\nimport {\n PageHeader,\n PageHeaderBanner,\n PageHeaderContainer,\n} from \"../PageHeader\"\n\nexport default class extends WithDropdown {\n render() {\n const page = misago.get(\"USER_OPTIONS\").filter((page) => {\n const url = misago.get(\"USERCP_URL\") + page.component + \"/\"\n return this.props.location.pathname.substr(0, url.length) === url\n })[0]\n\n return (\n
    \n \n \n \n \n \n \n

    {pgettext(\"forum options\", \"Change your options\")}

    \n
    \n \n
    \n \n menu\n \n \n
    \n
    \n
    \n \n \n
    \n \n {page.icon}\n {page.name}\n \n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n \n
    \n
    \n \n
    \n
    {this.props.children}
    \n
    \n
    \n
    \n )\n }\n}\n\nexport function select(store) {\n return {\n tick: store.tick.tick,\n user: store.auth.user,\n \"username-history\": store[\"username-history\"],\n }\n}\n\nexport function paths() {\n const paths = [\n {\n path: misago.get(\"USERCP_URL\") + \"forum-options/\",\n component: connect(select)(ChangeForumOptions),\n },\n {\n path: misago.get(\"USERCP_URL\") + \"edit-details/\",\n component: connect(select)(EditDetails),\n },\n ]\n\n const delegateAuth = misago.get(\"SETTINGS\").DELEGATE_AUTH\n if (!delegateAuth) {\n paths.push({\n path: misago.get(\"USERCP_URL\") + \"change-username/\",\n component: connect(select)(ChangeUsername),\n })\n paths.push({\n path: misago.get(\"USERCP_URL\") + \"sign-in-credentials/\",\n component: connect(select)(ChangeSignInCredentials),\n })\n }\n\n if (misago.get(\"ENABLE_DOWNLOAD_OWN_DATA\")) {\n paths.push({\n path: misago.get(\"USERCP_URL\") + \"download-data/\",\n component: connect(select)(DownloadData),\n })\n }\n\n if (!delegateAuth && misago.get(\"ENABLE_DELETE_OWN_ACCOUNT\")) {\n paths.push({\n path: misago.get(\"USERCP_URL\") + \"delete-account/\",\n component: connect(select)(DeleteAccount),\n })\n }\n\n return paths\n}\n","import Options, { paths } from \"misago/components/options/root\"\nimport misago from \"misago/index\"\nimport mount from \"misago/utils/routed-component\"\n\nexport default function initializer(context) {\n if (context.has(\"USER_OPTIONS\")) {\n mount({\n root: misago.get(\"USERCP_URL\"),\n component: Options,\n paths: paths(),\n })\n }\n}\n\nmisago.addInitializer({\n name: \"component:options\",\n initializer: initializer,\n after: \"store\",\n})\n","import moment from \"moment\"\nimport React from \"react\"\nimport PanelLoader from \"misago/components/panel-loader\"\nimport PanelMessage from \"misago/components/panel-message\"\nimport misago from \"misago/index\"\nimport polls from \"misago/services/polls\"\nimport title from \"misago/services/page-title\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n if (misago.has(\"PROFILE_BAN\")) {\n this.initWithPreloadedData(misago.pop(\"PROFILE_BAN\"))\n } else {\n this.initWithoutPreloadedData()\n }\n\n this.startPolling(props.profile.api.ban)\n }\n\n initWithPreloadedData(ban) {\n if (ban.expires_on) {\n ban.expires_on = moment(ban.expires_on)\n }\n\n this.state = {\n isLoaded: true,\n ban,\n }\n }\n\n initWithoutPreloadedData() {\n this.state = {\n isLoaded: false,\n }\n }\n\n startPolling(api) {\n polls.start({\n poll: \"ban-details\",\n url: api,\n frequency: 90 * 1000,\n update: this.update,\n error: this.error,\n })\n }\n\n update = (ban) => {\n if (ban.expires_on) {\n ban.expires_on = moment(ban.expires_on)\n }\n\n this.setState({\n isLoaded: true,\n error: null,\n\n ban,\n })\n }\n\n error = (error) => {\n this.setState({\n isLoaded: true,\n error: error.detail,\n ban: null,\n })\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"profile ban details title\", \"Ban details\"),\n parent: this.props.profile.username,\n })\n }\n\n componentWillUnmount() {\n polls.stop(\"ban-details\")\n }\n\n getUserMessage() {\n if (this.state.ban.user_message) {\n return (\n
    \n

    {pgettext(\"profile ban details\", \"User-shown ban message\")}

    \n \n
    \n )\n } else {\n return null\n }\n }\n\n getStaffMessage() {\n if (this.state.ban.staff_message) {\n return (\n
    \n

    {pgettext(\"profile ban details\", \"Team-shown ban message\")}

    \n \n
    \n )\n } else {\n return null\n }\n }\n\n getExpirationMessage() {\n if (this.state.ban.expires_on) {\n if (this.state.ban.expires_on.isAfter(moment())) {\n let title = interpolate(\n pgettext(\n \"profile ban details\",\n \"This ban expires on %(expires_on)s.\"\n ),\n {\n expires_on: this.state.ban.expires_on.format(\"LL, LT\"),\n },\n true\n )\n\n let message = interpolate(\n pgettext(\"profile ban details\", \"This ban expires %(expires_on)s.\"),\n {\n expires_on: this.state.ban.expires_on.fromNow(),\n },\n true\n )\n\n return {message}\n } else {\n return pgettext(\"profile ban details\", \"This ban has expired.\")\n }\n } else {\n return interpolate(\n pgettext(\"profile ban details\", \"%(username)s's ban is permanent.\"),\n {\n username: this.props.profile.username,\n },\n true\n )\n }\n }\n\n getPanelBody() {\n if (this.state.ban) {\n if (Object.keys(this.state.ban).length) {\n return (\n
    \n {this.getUserMessage()}\n {this.getStaffMessage()}\n\n
    \n

    {pgettext(\"profile ban details\", \"Ban expiration\")}

    \n

    {this.getExpirationMessage()}

    \n
    \n
    \n )\n } else {\n return (\n
    \n \n
    \n )\n }\n } else if (this.state.error) {\n return (\n
    \n \n
    \n )\n } else {\n return (\n
    \n \n
    \n )\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n

    \n {pgettext(\"profile ban details title\", \"Ban details\")}\n

    \n
    \n\n {this.getPanelBody()}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Form from \"misago/components/edit-details\"\n\nexport default function ({ api, display, onCancel, onSuccess }) {\n if (!display) return null\n\n return
    \n}\n","import React from \"react\"\n\nexport default function ({ isAuthenticated, profile }) {\n let message = null\n if (isAuthenticated) {\n message = pgettext(\n \"profile details empty\",\n \"You are not sharing any details with others.\"\n )\n } else {\n message = interpolate(\n pgettext(\n \"profile details empty\",\n \"%(username)s is not sharing any details with others.\"\n ),\n {\n username: profile.username,\n },\n true\n )\n }\n\n return (\n
    \n
    {message}
    \n
    \n )\n}\n","import React from \"react\"\n\nexport default function ({ html, text, url }) {\n if (html) {\n return (\n \n )\n }\n\n return (\n
    \n \n
    \n )\n}\n\nexport function SafeValue({ text, url }) {\n if (url) {\n return (\n

    \n \n {text || url}\n \n

    \n )\n }\n\n if (text) {\n return

    {text}

    \n }\n\n return null\n}\n","import React from \"react\"\nimport FieldValue from \"./field-value\"\n\nexport default function (props) {\n return (\n
    \n {props.name}:\n \n
    \n )\n}\n","import React from \"react\"\nimport Field from \"./field\"\n\nexport default function ({ fields, name }) {\n return (\n
    \n
    \n

    {name}

    \n
    \n
    \n
    \n {fields.map(({ fieldname, html, name, text, url }) => {\n return (\n \n )\n })}\n
    \n
    \n
    \n )\n}\n","import React from \"react\"\nimport EmptyMessage from \"./empty-message\"\nimport Group from \"./group\"\nimport Loader from \"misago/components/loader\"\n\nexport default function ({\n display,\n groups,\n isAuthenticated,\n loading,\n profile,\n}) {\n if (!display) return null\n\n if (loading) {\n return \n }\n\n if (!groups.length) {\n return \n }\n\n return (\n
    \n {groups.map((group, i) => {\n return \n })}\n
    \n )\n}\n","import React from \"react\"\nimport { Toolbar, ToolbarItem, ToolbarSection } from \"../../Toolbar\"\n\nconst ProfileDetailsHeader = ({ onEdit, showEditButton }) => (\n \n \n \n

    {pgettext(\"profile details title\", \"Details\")}

    \n
    \n
    \n {showEditButton && (\n \n \n \n {pgettext(\"profile details edit btn\", \"Edit\")}\n \n \n \n )}\n
    \n)\n\nexport default ProfileDetailsHeader\n","import React from \"react\"\nimport { load } from \"misago/reducers/profile-details\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends React.Component {\n componentDidMount() {\n const { data, dispatch, user } = this.props\n if (data && data.id === user.id) return\n\n ajax.get(this.props.user.api.details).then(\n (data) => {\n dispatch(load(data))\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n }\n\n render() {\n return this.props.children\n }\n}\n","import React from \"react\"\nimport Form from \"./form\"\nimport GroupsList from \"./groups-list\"\nimport Header from \"./header\"\nimport ProfileDetailsData from \"misago/data/profile-details\"\nimport { load as loadDetails } from \"misago/reducers/profile-details\"\nimport title from \"misago/services/page-title\"\nimport snackbar from \"misago/services/snackbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n editing: false,\n }\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"profile details title\", \"Details\"),\n parent: this.props.profile.username,\n })\n }\n\n onCancel = () => {\n this.setState({ editing: false })\n }\n\n onEdit = () => {\n this.setState({ editing: true })\n }\n\n onSuccess = (newDetails) => {\n const { dispatch, isAuthenticated, profile } = this.props\n\n let message = null\n if (isAuthenticated) {\n message = pgettext(\n \"profile details form\",\n \"Your details have been changed.\"\n )\n } else {\n message = interpolate(\n pgettext(\n \"profile details form\",\n \"%(username)s's details have been changed.\"\n ),\n {\n username: profile.username,\n },\n true\n )\n }\n\n snackbar.info(message)\n dispatch(loadDetails(newDetails))\n this.setState({ editing: false })\n }\n\n render() {\n const { dispatch, isAuthenticated, profile, profileDetails } = this.props\n const loading = profileDetails.id !== profile.id\n\n return (\n \n
    \n \n \n \n
    \n \n )\n }\n}\n","import React from \"react\"\nimport PostFeed from \"misago/components/post-feed\"\nimport Button from \"misago/components/button\"\nimport * as posts from \"misago/reducers/posts\"\nimport title from \"misago/services/page-title\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\nimport { Toolbar, ToolbarItem, ToolbarSection } from \"../../Toolbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n }\n }\n\n loadItems(start = 0) {\n ajax\n .get(this.props.api, {\n start: start || 0,\n })\n .then(\n (data) => {\n if (start === 0) {\n store.dispatch(posts.load(data))\n } else {\n store.dispatch(posts.append(data))\n }\n\n this.setState({\n isLoading: false,\n })\n },\n (rejection) => {\n this.setState({\n isLoading: false,\n })\n\n snackbar.apiError(rejection)\n }\n )\n }\n\n loadMore = () => {\n this.setState({\n isLoading: true,\n })\n\n this.loadItems(this.props.posts.next)\n }\n\n componentDidMount() {\n title.set({\n title: this.props.title,\n parent: this.props.profile.username,\n })\n\n this.loadItems()\n }\n\n render() {\n return (\n
    \n \n \n \n

    {this.props.header}

    \n
    \n
    \n
    \n \n
    \n )\n }\n}\n\nexport function Feed(props) {\n if (props.posts.isLoaded && !props.posts.results.length) {\n return

    {props.emptyMessage}

    \n }\n\n return (\n
    \n \n \n
    \n )\n}\n\nexport function LoadMoreButton(props) {\n if (!props.next) return null\n\n return (\n
    \n \n {pgettext(\"profile load more btn\", \"Show older activity\")}\n \n
    \n )\n}\n","import React from \"react\"\n\nexport default class extends React.Component {\n getClassName() {\n if (this.props.className) {\n return \"form-search \" + this.props.className\n } else {\n return \"form-search\"\n }\n }\n\n render() {\n return (\n
    \n \n search\n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Search from \"misago/components/quick-search\"\nimport UsersList from \"misago/components/users-list\"\nimport misago from \"misago/index\"\nimport { hydrate, append } from \"misago/reducers/users\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\nimport title from \"misago/services/page-title\"\nimport { Toolbar, ToolbarItem, ToolbarSection } from \"../Toolbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.setSpecialProps()\n\n if (misago.has(this.PRELOADED_DATA_KEY)) {\n this.initWithPreloadedData(misago.pop(this.PRELOADED_DATA_KEY))\n } else {\n this.initWithoutPreloadedData()\n }\n }\n\n setSpecialProps() {\n this.PRELOADED_DATA_KEY = \"PROFILE_FOLLOWERS\"\n this.TITLE = pgettext(\"profile followers title\", \"Followers\")\n this.API_FILTER = \"followers\"\n }\n\n initWithPreloadedData(data) {\n this.state = {\n isLoaded: true,\n isBusy: false,\n\n search: \"\",\n\n count: data.count,\n more: data.more,\n\n page: data.page,\n pages: data.pages,\n }\n\n store.dispatch(hydrate(data.results))\n }\n\n initWithoutPreloadedData() {\n this.state = {\n isLoaded: false,\n isBusy: false,\n\n search: \"\",\n\n count: 0,\n more: 0,\n\n page: 1,\n pages: 1,\n }\n\n this.loadUsers()\n }\n\n loadUsers(page = 1, search = null) {\n const apiUrl = this.props.profile.api[this.API_FILTER]\n\n ajax\n .get(\n apiUrl,\n {\n search: search,\n page: page || 1,\n },\n \"user-\" + this.API_FILTER\n )\n .then(\n (data) => {\n if (page === 1) {\n store.dispatch(hydrate(data.results))\n } else {\n store.dispatch(append(data.results))\n }\n\n this.setState({\n isLoaded: true,\n isBusy: false,\n\n count: data.count,\n more: data.more,\n\n page: data.page,\n pages: data.pages,\n })\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n }\n\n componentDidMount() {\n title.set({\n title: this.TITLE,\n parent: this.props.profile.username,\n })\n }\n\n loadMore = () => {\n this.setState({\n isBusy: true,\n })\n\n this.loadUsers(this.state.page + 1, this.state.search)\n }\n\n search = (ev) => {\n this.setState({\n isLoaded: false,\n isBusy: true,\n\n search: ev.target.value,\n\n count: 0,\n more: 0,\n\n page: 1,\n pages: 1,\n })\n\n this.loadUsers(1, ev.target.value)\n }\n\n getLabel() {\n if (!this.state.isLoaded) {\n return pgettext(\"Loading...\")\n } else if (this.state.search) {\n let message = npgettext(\n \"profile followers\",\n \"Found %(users)s user.\",\n \"Found %(users)s users.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n users: this.state.count,\n },\n true\n )\n } else if (this.props.profile.id === this.props.user.id) {\n let message = npgettext(\n \"profile followers\",\n \"You have %(users)s follower.\",\n \"You have %(users)s followers.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n users: this.state.count,\n },\n true\n )\n } else {\n let message = npgettext(\n \"profile followers\",\n \"%(username)s has %(users)s follower.\",\n \"%(username)s has %(users)s followers.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n username: this.props.profile.username,\n users: this.state.count,\n },\n true\n )\n }\n }\n\n getEmptyMessage() {\n if (this.state.search) {\n return pgettext(\n \"profile followers\",\n \"Search returned no users matching specified criteria.\"\n )\n } else if (this.props.user.id === this.props.profile.id) {\n return pgettext(\"profile followers\", \"You have no followers.\")\n } else {\n return interpolate(\n pgettext(\"profile followers\", \"%(username)s has no followers.\"),\n {\n username: this.props.profile.username,\n },\n true\n )\n }\n }\n\n getMoreButton() {\n if (!this.state.more) return null\n\n return (\n
    \n \n {interpolate(\n pgettext(\"profile followers\", \"Show more (%(more)s)\"),\n {\n more: this.state.more,\n },\n true\n )}\n \n
    \n )\n }\n\n getListBody() {\n if (this.state.isLoaded && this.state.count === 0) {\n return

    {this.getEmptyMessage()}

    \n }\n\n return (\n
    \n \n\n {this.getMoreButton()}\n
    \n )\n }\n\n getClassName() {\n return \"profile-\" + this.API_FILTER\n }\n\n render() {\n return (\n
    \n \n \n \n

    {this.getLabel()}

    \n
    \n
    \n \n \n \n \n \n
    \n\n {this.getListBody()}\n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Search from \"misago/components/quick-search\"\nimport UsernameHistory from \"misago/components/username-history/root\"\nimport misago from \"misago/index\"\nimport { hydrate, append } from \"misago/reducers/username-history\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\nimport title from \"misago/services/page-title\"\nimport { Toolbar, ToolbarItem, ToolbarSection } from \"../Toolbar\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n if (misago.has(\"PROFILE_NAME_HISTORY\")) {\n this.initWithPreloadedData(misago.pop(\"PROFILE_NAME_HISTORY\"))\n } else {\n this.initWithoutPreloadedData()\n }\n }\n\n initWithPreloadedData(data) {\n this.state = {\n isLoaded: true,\n isBusy: false,\n\n search: \"\",\n\n count: data.count,\n more: data.more,\n\n page: data.page,\n pages: data.pages,\n }\n\n store.dispatch(hydrate(data.results))\n }\n\n initWithoutPreloadedData() {\n this.state = {\n isLoaded: false,\n isBusy: false,\n\n search: \"\",\n\n count: 0,\n more: 0,\n\n page: 1,\n pages: 1,\n }\n\n this.loadChanges()\n }\n\n loadChanges(page = 1, search = null) {\n ajax\n .get(\n misago.get(\"USERNAME_CHANGES_API\"),\n {\n user: this.props.profile.id,\n search: search,\n page: page || 1,\n },\n \"search-username-history\"\n )\n .then(\n (data) => {\n if (page === 1) {\n store.dispatch(hydrate(data.results))\n } else {\n store.dispatch(append(data.results))\n }\n\n this.setState({\n isLoaded: true,\n isBusy: false,\n\n count: data.count,\n more: data.more,\n\n page: data.page,\n pages: data.pages,\n })\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n }\n\n componentDidMount() {\n title.set({\n title: pgettext(\"profile username history title\", \"Username history\"),\n parent: this.props.profile.username,\n })\n }\n\n loadMore = () => {\n this.setState({\n isBusy: true,\n })\n\n this.loadChanges(this.state.page + 1, this.state.search)\n }\n\n search = (ev) => {\n this.setState({\n isLoaded: false,\n isBusy: true,\n\n search: ev.target.value,\n\n count: 0,\n more: 0,\n\n page: 1,\n pages: 1,\n })\n\n this.loadChanges(1, ev.target.value)\n }\n\n getLabel() {\n if (!this.state.isLoaded) {\n return pgettext(\"profile username history\", \"Loading...\")\n } else if (this.state.search) {\n let message = npgettext(\n \"profile username history\",\n \"Found %(changes)s username change.\",\n \"Found %(changes)s username changes.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n changes: this.state.count,\n },\n true\n )\n } else if (this.props.profile.id === this.props.user.id) {\n let message = npgettext(\n \"profile username history\",\n \"Your username was changed %(changes)s time.\",\n \"Your username was changed %(changes)s times.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n changes: this.state.count,\n },\n true\n )\n } else {\n let message = npgettext(\n \"profile username history\",\n \"%(username)s's username was changed %(changes)s time.\",\n \"%(username)s's username was changed %(changes)s times.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n username: this.props.profile.username,\n changes: this.state.count,\n },\n true\n )\n }\n }\n\n getEmptyMessage() {\n if (this.state.search) {\n return pgettext(\n \"profile username history\",\n \"Search returned no username changes matching specified criteria.\"\n )\n } else if (this.props.user.id === this.props.profile.id) {\n return pgettext(\n \"username history empty\",\n \"Your account has no history of name changes.\"\n )\n } else {\n return interpolate(\n pgettext(\n \"profile username history\",\n \"%(username)s's username was never changed.\"\n ),\n {\n username: this.props.profile.username,\n },\n true\n )\n }\n }\n\n getMoreButton() {\n if (!this.state.more) return null\n\n return (\n
    \n \n {interpolate(\n pgettext(\"profile username history\", \"Show older (%(more)s)\"),\n {\n more: this.state.more,\n },\n true\n )}\n \n
    \n )\n }\n\n render() {\n return (\n
    \n \n \n \n

    {this.getLabel()}

    \n
    \n
    \n \n \n \n \n \n
    \n\n \n\n {this.getMoreButton()}\n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport { patch } from \"misago/reducers/profile\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n }\n }\n\n getClassName() {\n if (this.props.profile.is_followed) {\n return this.props.className + \" btn-default btn-following\"\n } else {\n return this.props.className + \" btn-default btn-follow\"\n }\n }\n\n getIcon() {\n if (this.props.profile.is_followed) {\n return \"favorite\"\n } else {\n return \"favorite_border\"\n }\n }\n\n getLabel() {\n if (this.props.profile.is_followed) {\n return pgettext(\"user profile follow btn\", \"Following\")\n } else {\n return pgettext(\"user profile follow btn\", \"Follow\")\n }\n }\n\n action = () => {\n this.setState({\n isLoading: true,\n })\n\n if (this.props.profile.is_followed) {\n store.dispatch(\n patch({\n is_followed: false,\n followers: this.props.profile.followers - 1,\n })\n )\n } else {\n store.dispatch(\n patch({\n is_followed: true,\n followers: this.props.profile.followers + 1,\n })\n )\n }\n\n ajax.post(this.props.profile.api.follow).then(\n (data) => {\n this.setState({\n isLoading: false,\n })\n\n store.dispatch(patch(data))\n },\n (rejection) => {\n this.setState({\n isLoading: false,\n })\n snackbar.apiError(rejection)\n }\n )\n }\n\n render() {\n return (\n \n {this.getIcon()}\n {this.getLabel()}\n \n )\n }\n}\n","import React from \"react\"\nimport posting from \"misago/services/posting\"\nimport misago from \"misago\"\n\nexport default class extends React.Component {\n onClick = () => {\n posting.open({\n mode: \"START_PRIVATE\",\n submit: misago.get(\"PRIVATE_THREADS_API\"),\n\n to: [this.props.profile],\n })\n }\n\n render() {\n const canMessage = this.props.user.acl.can_start_private_threads\n const isProfileOwner = this.props.user.id === this.props.profile.id\n\n if (!canMessage || isProfileOwner) return null\n\n return (\n \n comment\n {pgettext(\"profile message btn\", \"Message\")}\n \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport Loader from \"misago/components/modal-loader\"\nimport YesNoSwitch from \"misago/components/yes-no-switch\"\nimport ModalMessage from \"misago/components/modal-message\"\nimport { updateAvatar } from \"misago/reducers/users\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoaded: false,\n isLoading: false,\n error: null,\n\n is_avatar_locked: \"\",\n avatar_lock_user_message: \"\",\n avatar_lock_staff_message: \"\",\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.profile.api.moderate_avatar).then(\n (options) => {\n this.setState({\n isLoaded: true,\n\n is_avatar_locked: options.is_avatar_locked,\n avatar_lock_user_message: options.avatar_lock_user_message || \"\",\n avatar_lock_staff_message: options.avatar_lock_staff_message || \"\",\n })\n },\n (rejection) => {\n this.setState({\n isLoaded: true,\n error: rejection.detail,\n })\n }\n )\n }\n\n clean() {\n if (this.isValid()) {\n return true\n } else {\n snackbar.error(this.validate().username[0])\n return false\n }\n }\n\n send() {\n return ajax.post(this.props.profile.api.moderate_avatar, {\n is_avatar_locked: this.state.is_avatar_locked,\n avatar_lock_user_message: this.state.avatar_lock_user_message,\n avatar_lock_staff_message: this.state.avatar_lock_staff_message,\n })\n }\n\n handleSuccess(apiResponse) {\n store.dispatch(updateAvatar(this.props.profile, apiResponse.avatar_hash))\n snackbar.success(\n pgettext(\n \"profile avatar moderation\",\n \"Avatar controls have been changed.\"\n )\n )\n }\n\n getFormBody() {\n return (\n \n
    \n \n \n \n\n \n \n \n\n \n \n \n
    \n
    \n \n {pgettext(\"profile avatar moderation btn\", \"Close\")}\n \n \n
    \n \n )\n }\n\n getModalBody() {\n if (this.state.error) {\n return (\n \n )\n } else if (this.state.isLoaded) {\n return this.getFormBody()\n } else {\n return \n }\n }\n\n getClassName() {\n if (this.state.error) {\n return \"modal-dialog modal-message modal-avatar-controls\"\n } else {\n return \"modal-dialog modal-avatar-controls\"\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"profile avatar moderation title\", \"Avatar controls\")}\n

    \n
    \n {this.getModalBody()}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport Loader from \"misago/components/modal-loader\"\nimport ModalMessage from \"misago/components/modal-message\"\nimport { addNameChange } from \"misago/reducers/username-history\"\nimport { updateUsername } from \"misago/reducers/users\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\nimport * as validators from \"misago/utils/validators\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoaded: false,\n isLoading: false,\n error: null,\n\n username: \"\",\n validators: {\n username: [validators.usernameContent()],\n },\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.profile.api.moderate_username).then(\n () => {\n this.setState({\n isLoaded: true,\n })\n },\n (rejection) => {\n this.setState({\n isLoaded: true,\n error: rejection.detail,\n })\n }\n )\n }\n\n clean() {\n if (this.isValid()) {\n return true\n } else {\n snackbar.error(this.validate().username[0])\n return false\n }\n }\n\n send() {\n return ajax.post(this.props.profile.api.moderate_username, {\n username: this.state.username,\n })\n }\n\n handleSuccess(apiResponse) {\n this.setState({\n username: \"\",\n })\n\n store.dispatch(\n addNameChange(apiResponse, this.props.profile, this.props.user)\n )\n store.dispatch(\n updateUsername(this.props.profile, apiResponse.username, apiResponse.slug)\n )\n\n snackbar.success(\n pgettext(\"profile username moderation\", \"Username has been changed.\")\n )\n }\n\n getFormBody() {\n return (\n
    \n
    \n \n \n \n
    \n
    \n \n {pgettext(\"profile username moderation btn\", \"Cancel\")}\n \n \n
    \n
    \n )\n }\n\n getModalBody() {\n if (this.state.error) {\n return (\n \n )\n } else if (this.state.isLoaded) {\n return this.getFormBody()\n } else {\n return \n }\n }\n\n getClassName() {\n if (this.state.error) {\n return \"modal-dialog modal-message modal-rename-user\"\n } else {\n return \"modal-dialog modal-rename-user\"\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"profile username moderation title\", \"Change username\")}\n

    \n
    \n {this.getModalBody()}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport Loader from \"misago/components/modal-loader\"\nimport ModalMessage from \"misago/components/modal-message\"\nimport YesNoSwitch from \"misago/components/yes-no-switch\"\nimport misago from \"misago/index\"\nimport ajax from \"misago/services/ajax\"\nimport polls from \"misago/services/polls\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoaded: false,\n isLoading: false,\n isDeleted: false,\n error: null,\n\n countdown: 5,\n confirm: false,\n\n with_content: false,\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.profile.api.delete).then(\n () => {\n this.setState({\n isLoaded: true,\n })\n\n this.countdown()\n },\n (rejection) => {\n this.setState({\n isLoaded: true,\n error: rejection.detail,\n })\n }\n )\n }\n\n countdown = () => {\n window.setTimeout(() => {\n if (this.state.countdown > 1) {\n this.setState({\n countdown: this.state.countdown - 1,\n })\n this.countdown()\n } else if (!this.state.confirm) {\n this.setState({\n confirm: true,\n })\n }\n }, 1000)\n }\n\n send() {\n return ajax.post(this.props.profile.api.delete, {\n with_content: this.state.with_content,\n })\n }\n\n handleSuccess() {\n polls.stop(\"user-profile\")\n\n if (this.state.with_content) {\n this.setState({\n isDeleted: interpolate(\n pgettext(\n \"profile delete\",\n \"%(username)s's account, threads, posts and other content has been deleted.\"\n ),\n {\n username: this.props.profile.username,\n },\n true\n ),\n })\n } else {\n this.setState({\n isDeleted: interpolate(\n pgettext(\n \"profile delete\",\n \"%(username)s's account has been deleted and other content has been hidden.\"\n ),\n {\n username: this.props.profile.username,\n },\n true\n ),\n })\n }\n }\n\n getButtonLabel() {\n if (this.state.confirm) {\n return interpolate(\n pgettext(\"profile delete btn\", \"Delete %(username)s\"),\n {\n username: this.props.profile.username,\n },\n true\n )\n } else {\n return interpolate(\n pgettext(\"profile delete btn\", \"Please wait... (%(countdown)ss)\"),\n {\n countdown: this.state.countdown,\n },\n true\n )\n }\n }\n\n getForm() {\n return (\n
    \n
    \n \n \n \n
    \n
    \n \n {pgettext(\"profile delete btn\", \"Cancel\")}\n \n\n \n {this.getButtonLabel()}\n \n
    \n
    \n )\n }\n\n getDeletedBody() {\n return (\n
    \n
    \n info_outline\n
    \n \n
    \n )\n }\n\n getModalBody() {\n if (this.state.error) {\n return (\n \n )\n } else if (this.state.isLoaded) {\n if (this.state.isDeleted) {\n return this.getDeletedBody()\n } else {\n return this.getForm()\n }\n } else {\n return \n }\n }\n\n getClassName() {\n if (this.state.error || this.state.isDeleted) {\n return \"modal-dialog modal-message modal-delete-account\"\n } else {\n return \"modal-dialog modal-delete-account\"\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"profile delete title\", \"Delete user account\")}\n

    \n
    \n {this.getModalBody()}\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport AvatarControls from \"misago/components/profile/moderation/avatar-controls\"\nimport ChangeUsername from \"misago/components/profile/moderation/change-username\"\nimport DeleteAccount from \"misago/components/profile/moderation/delete-account\"\nimport modal from \"misago/services/modal\"\n\nlet select = function (store) {\n return {\n tick: store.tick,\n user: store.auth,\n profile: store.profile,\n }\n}\n\nexport default class extends React.Component {\n showAvatarDialog = () => {\n modal.show(connect(select)(AvatarControls))\n }\n\n showRenameDialog = () => {\n modal.show(connect(select)(ChangeUsername))\n }\n\n showDeleteDialog = () => {\n modal.show(connect(select)(DeleteAccount))\n }\n\n render() {\n const { moderation } = this.props\n\n return (\n
      \n {!!moderation.avatar && (\n
    • \n \n portrait\n {pgettext(\"profile moderation menu\", \"Avatar controls\")}\n \n
    • \n )}\n {!!moderation.rename && (\n
    • \n \n credit_card\n {pgettext(\"profile moderation menu\", \"Change username\")}\n \n
    • \n )}\n {!!moderation.delete && (\n
    • \n \n clear\n {pgettext(\"profile moderation menu\", \"Delete account\")}\n \n
    • \n )}\n
    \n )\n }\n}\n","import React from \"react\"\nimport Status, { StatusIcon, StatusLabel } from \"../user-status\"\n\nconst ProfileDataList = ({ profile }) => (\n
      \n {profile.is_active === false && (\n
    • \n \n {pgettext(\"profile data list\", \"Account disabled\")}\n \n
    • \n )}\n
    • \n \n \n \n \n
    • \n {profile.rank.is_tab ? (\n
    • \n \n {profile.rank.name}\n \n
    • \n ) : (\n
    • \n {profile.rank.name}\n
    • \n )}\n {(profile.title || profile.rank.title) && (\n
    • {profile.title || profile.rank.title}
    • \n )}\n
    • \n \n {interpolate(\n pgettext(\"profile data list\", \"Joined %(joined_on)s\"),\n {\n joined_on: profile.joined_on.fromNow(),\n },\n true\n )}\n \n
    • \n {profile.email && (\n
    • \n \n {profile.email}\n \n
    • \n )}\n
    \n)\n\nexport default ProfileDataList\n","import React from \"react\"\nimport Avatar from \"../avatar\"\nimport { FlexRow, FlexRowCol, FlexRowSection } from \"../FlexRow\"\nimport {\n PageHeader,\n PageHeaderBanner,\n PageHeaderContainer,\n PageHeaderDetails,\n} from \"../PageHeader\"\nimport FollowButton from \"./follow-button\"\nimport MessageButton from \"./message-button\"\nimport ModerationOptions from \"./moderation/nav\"\nimport ProfileDataList from \"./ProfileDataList\"\n\nconst ProfileHeader = ({ profile, user, moderation, message, follow }) => (\n \n \n \n
    \n
    \n \n \n \n
    \n

    {profile.username}

    \n
    \n \n \n \n \n \n \n \n \n {message && (\n \n \n \n \n {moderation.available && !follow && (\n \n
    \n \n \n
    \n
    \n )}\n
    \n )}\n {follow && (\n \n \n \n \n {moderation.available && (\n \n
    \n \n \n
    \n
    \n )}\n
    \n )}\n {moderation.available && !follow && !message && (\n \n \n
    \n \n \n
    \n
    \n \n
    \n \n settings\n {pgettext(\"profile options btn\", \"Options\")}\n \n \n
    \n
    \n
    \n )}\n
    \n
    \n \n
    \n)\n\nconst ProfileModerationButton = () => (\n \n settings\n \n)\n\nexport default ProfileHeader\n","import React from \"react\"\nimport { Link } from \"react-router\"\nimport Li from \"misago/components/li\"\n\nconst ProfileNav = ({ baseUrl, page, pages }) => (\n
    \n
    \n \n {page.icon}\n {page.name}\n \n
      \n {pages.map((page) => (\n
    • \n \n {page.icon}\n {page.name}\n \n
    • \n ))}\n
    \n
    \n
      \n {pages.map((page) => (\n
    • \n \n {page.icon}\n {page.name}\n \n
    • \n ))}\n
    \n
    \n)\n\nexport default ProfileNav\n","import React from \"react\"\nimport { connect } from \"react-redux\"\nimport BanDetails from \"./ban-details\"\nimport Details from \"./details\"\nimport { Posts, Threads } from \"./feed\"\nimport Followers from \"./followers\"\nimport Follows from \"./follows\"\nimport UsernameHistory from \"./username-history\"\nimport WithDropdown from \"misago/components/with-dropdown\"\nimport misago from \"misago\"\nimport { hydrate } from \"misago/reducers/profile\"\nimport polls from \"misago/services/polls\"\nimport store from \"misago/services/store\"\nimport PageContainer from \"../PageContainer\"\nimport ProfileHeader from \"./ProfileHeader\"\nimport ProfileNav from \"./ProfileNav\"\n\nexport default class extends WithDropdown {\n constructor(props) {\n super(props)\n\n this.startPolling(props.profile.api.index)\n }\n\n startPolling(api) {\n polls.start({\n poll: \"user-profile\",\n url: api,\n frequency: 90 * 1000,\n update: this.update,\n })\n }\n\n update = (data) => {\n store.dispatch(hydrate(data))\n }\n\n render() {\n const baseUrl = misago.get(\"PROFILE\").url\n const pages = misago.get(\"PROFILE_PAGES\")\n const page = pages.filter((page) => {\n const url = baseUrl + page.component + \"/\"\n return this.props.location.pathname === url\n })[0]\n const { profile, user } = this.props\n const moderation = getModeration(profile, user)\n const message =\n !!user.acl.can_start_private_threads && profile.id !== user.id\n const follow = !!profile.acl.can_follow && profile.id !== user.id\n\n return (\n
    \n \n \n \n\n {this.props.children}\n \n
    \n )\n }\n}\n\nconst getModeration = (profile, user) => {\n const moderation = {\n available: false,\n rename: false,\n avatar: false,\n delete: false,\n }\n\n if (user.is_anonymous) return moderation\n\n moderation.rename = profile.acl.can_rename\n moderation.avatar = profile.acl.can_moderate_avatar\n moderation.delete = profile.acl.can_delete\n moderation.available = !!(\n moderation.rename ||\n moderation.avatar ||\n moderation.delete\n )\n\n return moderation\n}\n\nexport function select(store) {\n return {\n isAuthenticated: store.auth.user.id === store.profile.id,\n\n tick: store.tick.tick,\n user: store.auth.user,\n users: store.users,\n posts: store.posts,\n profile: store.profile,\n profileDetails: store[\"profile-details\"],\n \"username-history\": store[\"username-history\"],\n }\n}\n\nconst COMPONENTS = {\n posts: Posts,\n threads: Threads,\n followers: Followers,\n follows: Follows,\n details: Details,\n \"username-history\": UsernameHistory,\n \"ban-details\": BanDetails,\n}\n\nexport function paths() {\n let paths = []\n misago.get(\"PROFILE_PAGES\").forEach(function (item) {\n paths.push(\n Object.assign({}, item, {\n path: misago.get(\"PROFILE\").url + item.component + \"/\",\n component: connect(select)(COMPONENTS[item.component]),\n })\n )\n })\n\n return paths\n}\n","import React from \"react\"\nimport Route from \"./route\"\n\nexport function Threads(props) {\n let emptyMessage = null\n if (props.user.id === props.profile.id) {\n emptyMessage = pgettext(\n \"profile threads\",\n \"You haven't started any threads.\"\n )\n } else {\n emptyMessage = interpolate(\n pgettext(\"profile threads\", \"%(username)s hasn't started any threads\"),\n {\n username: props.profile.username,\n },\n true\n )\n }\n\n let header = null\n if (!props.posts.isLoaded) {\n header = pgettext(\"profile threads\", \"Loading...\")\n } else if (props.profile.id === props.user.id) {\n const message = npgettext(\n \"profile threads\",\n \"You have started %(threads)s thread.\",\n \"You have started %(threads)s threads.\",\n props.profile.threads\n )\n\n header = interpolate(\n message,\n {\n threads: props.profile.threads,\n },\n true\n )\n } else {\n const message = npgettext(\n \"profile threads\",\n \"%(username)s has started %(threads)s thread.\",\n \"%(username)s has started %(threads)s threads.\",\n props.profile.threads\n )\n\n header = interpolate(\n message,\n {\n username: props.profile.username,\n threads: props.profile.threads,\n },\n true\n )\n }\n\n return (\n \n )\n}\n\nexport function Posts(props) {\n let emptyMessage = null\n if (props.user.id === props.profile.id) {\n emptyMessage = pgettext(\"profile posts\", \"You have posted no messages.\")\n } else {\n emptyMessage = interpolate(\n pgettext(\"profile posts\", \"%(username)s posted no messages.\"),\n {\n username: props.profile.username,\n },\n true\n )\n }\n\n let header = null\n if (!props.posts.isLoaded) {\n header = pgettext(\"profile posts\", \"Loading...\")\n } else if (props.profile.id === props.user.id) {\n const message = npgettext(\n \"profile posts\",\n \"You have posted %(posts)s message.\",\n \"You have posted %(posts)s messages.\",\n props.profile.posts\n )\n\n header = interpolate(\n message,\n {\n posts: props.profile.posts,\n },\n true\n )\n } else {\n const message = npgettext(\n \"profile posts\",\n \"%(username)s has posted %(posts)s message.\",\n \"%(username)s has posted %(posts)s messages.\",\n props.profile.posts\n )\n\n header = interpolate(\n message,\n {\n username: props.profile.username,\n posts: props.profile.posts,\n },\n true\n )\n }\n\n return (\n \n )\n}\n","import React from \"react\"\nimport Followers from \"misago/components/profile/followers\"\n\nexport default class extends Followers {\n setSpecialProps() {\n this.PRELOADED_DATA_KEY = \"PROFILE_FOLLOWS\"\n this.TITLE = pgettext(\"profile follows title\", \"Follows\")\n this.API_FILTER = \"follows\"\n }\n\n getLabel() {\n if (!this.state.isLoaded) {\n return pgettext(\"profile follows\", \"Loading...\")\n } else if (this.state.search) {\n let message = npgettext(\n \"profile follows\",\n \"Found %(users)s user.\",\n \"Found %(users)s users.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n users: this.state.count,\n },\n true\n )\n } else if (this.props.profile.id === this.props.user.id) {\n let message = npgettext(\n \"profile follows\",\n \"You are following %(users)s user.\",\n \"You are following %(users)s users.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n users: this.state.count,\n },\n true\n )\n } else {\n let message = npgettext(\n \"profile follows\",\n \"%(username)s is following %(users)s user.\",\n \"%(username)s is following %(users)s users.\",\n this.state.count\n )\n\n return interpolate(\n message,\n {\n username: this.props.profile.username,\n users: this.state.count,\n },\n true\n )\n }\n }\n\n getEmptyMessage() {\n if (this.state.search) {\n return pgettext(\n \"profile follows\",\n \"Search returned no users matching specified criteria.\"\n )\n } else if (this.props.user.id === this.props.profile.id) {\n return pgettext(\"profile follows\", \"You are not following any users.\")\n } else {\n return interpolate(\n pgettext(\"profile follows\", \"%(username)s is not following any users.\"),\n {\n username: this.props.profile.username,\n },\n true\n )\n }\n }\n}\n","import { connect } from \"react-redux\"\nimport Profile, { paths, select } from \"misago/components/profile/root\"\nimport misago from \"misago/index\"\nimport mount from \"misago/utils/routed-component\"\n\nexport default function initializer(context) {\n if (context.has(\"PROFILE\") && context.has(\"PROFILE_PAGES\")) {\n mount({\n root: misago.get(\"PROFILE\").url,\n component: connect(select)(Profile),\n paths: paths(),\n })\n }\n}\n\nmisago.addInitializer({\n name: \"component:profile\",\n initializer: initializer,\n after: \"reducer:profile-hydrate\",\n})\n","import React from \"react\"\nimport misago from \"misago/index\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport * as validators from \"misago/utils/validators\"\nimport showBannedPage from \"misago/utils/banned-page\"\n\nexport class RequestLinkForm extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n email: \"\",\n\n validators: {\n email: [validators.email()],\n },\n }\n }\n\n clean() {\n if (this.isValid()) {\n return true\n } else {\n snackbar.error(\n pgettext(\n \"request activation link form\",\n \"Enter a valid e-mail address.\"\n )\n )\n return false\n }\n }\n\n send() {\n return ajax.post(misago.get(\"SEND_ACTIVATION_API\"), {\n email: this.state.email,\n })\n }\n\n handleSuccess(apiResponse) {\n this.props.callback(apiResponse)\n }\n\n handleError(rejection) {\n if ([\"already_active\", \"inactive_admin\"].indexOf(rejection.code) > -1) {\n snackbar.info(rejection.detail)\n } else if (rejection.status === 403 && rejection.ban) {\n showBannedPage(rejection.ban)\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n \n
    \n
    \n\n \n {pgettext(\"request activation link form btn\", \"Send link\")}\n \n \n
    \n )\n }\n}\n\nexport class LinkSent extends React.Component {\n getMessage() {\n return interpolate(\n pgettext(\n \"request activation link form\",\n \"Activation link was sent to %(email)s\"\n ),\n {\n email: this.props.user.email,\n },\n true\n )\n }\n\n render() {\n return (\n
    \n
    \n
    \n check\n
    \n
    \n

    {this.getMessage()}

    \n
    \n \n {pgettext(\n \"request activation link form btn\",\n \"Request another link\"\n )}\n \n
    \n
    \n )\n }\n}\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n complete: false,\n }\n }\n\n complete = (apiResponse) => {\n this.setState({\n complete: apiResponse,\n })\n }\n\n reset = () => {\n this.setState({\n complete: false,\n })\n }\n\n render() {\n if (this.state.complete) {\n return \n } else {\n return \n }\n }\n}\n","import misago from \"misago/index\"\nimport RequestActivationLink from \"misago/components/request-activation-link\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer() {\n if (document.getElementById(\"request-activation-link-mount\")) {\n mount(RequestActivationLink, \"request-activation-link-mount\", false)\n }\n}\n\nmisago.addInitializer({\n name: \"component:request-activation-link\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport misago from \"misago/index\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport * as validators from \"misago/utils/validators\"\nimport showBannedPage from \"misago/utils/banned-page\"\n\nexport class RequestResetForm extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n email: \"\",\n\n validators: {\n email: [validators.email()],\n },\n }\n }\n\n clean() {\n if (this.isValid()) {\n return true\n } else {\n snackbar.error(\n pgettext(\"request password reset form\", \"Enter a valid e-mail address.\")\n )\n return false\n }\n }\n\n send() {\n return ajax.post(misago.get(\"SEND_PASSWORD_RESET_API\"), {\n email: this.state.email,\n })\n }\n\n handleSuccess(apiResponse) {\n this.props.callback(apiResponse)\n }\n\n handleError(rejection) {\n if ([\"inactive_user\", \"inactive_admin\"].indexOf(rejection.code) > -1) {\n this.props.showInactivePage(rejection)\n } else if (rejection.status === 403 && rejection.ban) {\n showBannedPage(rejection.ban)\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n \n
    \n
    \n\n \n {pgettext(\"request password reset form btn\", \"Send link\")}\n \n \n
    \n )\n }\n}\n\nexport class LinkSent extends React.Component {\n getMessage() {\n return interpolate(\n pgettext(\n \"request password reset form\",\n \"Reset password link was sent to %(email)s\"\n ),\n {\n email: this.props.user.email,\n },\n true\n )\n }\n\n render() {\n return (\n
    \n
    \n
    \n check\n
    \n
    \n

    {this.getMessage()}

    \n
    \n \n {pgettext(\n \"request password reset form btn\",\n \"Request another link\"\n )}\n \n
    \n
    \n )\n }\n}\n\nexport class AccountInactivePage extends React.Component {\n getActivateButton() {\n if (this.props.activation === \"inactive_user\") {\n return (\n

    \n \n {pgettext(\n \"request password reset form error\",\n \"Activate your account.\"\n )}\n \n

    \n )\n } else {\n return null\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n info_outline\n
    \n\n
    \n

    \n {pgettext(\n \"request password reset form error\",\n \"Your account is inactive.\"\n )}\n

    \n

    {this.props.message}

    \n {this.getActivateButton()}\n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n complete: false,\n }\n }\n\n complete = (apiResponse) => {\n this.setState({\n complete: apiResponse,\n })\n }\n\n reset = () => {\n this.setState({\n complete: false,\n })\n }\n\n showInactivePage(apiResponse) {\n ReactDOM.render(\n ,\n document.getElementById(\"page-mount\")\n )\n }\n\n render() {\n if (this.state.complete) {\n return \n }\n\n return (\n \n )\n }\n}\n","import misago from \"misago/index\"\nimport RequestPasswordReset from \"misago/components/request-password-reset\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer() {\n if (document.getElementById(\"request-password-reset-mount\")) {\n mount(RequestPasswordReset, \"request-password-reset-mount\", false)\n }\n}\n\nmisago.addInitializer({\n name: \"component:request-password-reset\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport misago from \"misago/index\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport SignInModal from \"misago/components/sign-in.js\"\nimport ajax from \"misago/services/ajax\"\nimport auth from \"misago/services/auth\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport showBannedPage from \"misago/utils/banned-page\"\n\nexport class ResetPasswordForm extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n password: \"\",\n }\n }\n\n clean() {\n if (this.state.password.trim().length) {\n return true\n } else {\n snackbar.error(pgettext(\"password reset form\", \"Enter new password.\"))\n return false\n }\n }\n\n send() {\n return ajax.post(misago.get(\"CHANGE_PASSWORD_API\"), {\n password: this.state.password,\n })\n }\n\n handleSuccess(apiResponse) {\n this.props.callback(apiResponse)\n }\n\n handleError(rejection) {\n if (rejection.status === 403 && rejection.ban) {\n showBannedPage(rejection.ban)\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n \n
    \n
    \n\n \n {pgettext(\"password reset form btn\", \"Change password\")}\n \n \n
    \n )\n }\n}\n\nexport class PasswordChangedPage extends React.Component {\n getMessage() {\n return interpolate(\n pgettext(\n \"password reset form\",\n \"%(username)s, your password has been changed.\"\n ),\n {\n username: this.props.user.username,\n },\n true\n )\n }\n\n showSignIn() {\n modal.show(SignInModal)\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n check\n
    \n\n
    \n

    {this.getMessage()}

    \n

    \n {pgettext(\n \"password reset form\",\n \"Sign in using new password to continue.\"\n )}\n

    \n

    \n \n {pgettext(\"password reset form btn\", \"Sign in\")}\n \n

    \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport default class extends React.Component {\n complete = (apiResponse) => {\n auth.softSignOut()\n\n // nuke \"redirect_to\" field so we don't end\n // coming back to error page after sign in\n $('#hidden-login-form input[name=\"redirect_to\"]').remove()\n\n ReactDOM.render(\n ,\n document.getElementById(\"page-mount\")\n )\n }\n\n render() {\n return \n }\n}\n","import misago from \"misago\"\nimport ResetPasswordForm from \"misago/components/reset-password-form\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer() {\n if (document.getElementById(\"reset-password-form-mount\")) {\n mount(ResetPasswordForm, \"reset-password-form-mount\", false)\n }\n}\n\nmisago.addInitializer({\n name: \"component:reset-password-form\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport { Provider } from \"react-redux\"\nimport { SearchOverlay } from \"../../components/Search\"\nimport store from \"../../services/store\"\n\nexport default function initializer(context) {\n const root = document.getElementById(\"search-mount\")\n ReactDOM.render(\n \n \n ,\n root\n )\n}\n\nmisago.addInitializer({\n name: \"component:search-overlay\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport misago from \"misago\"\nimport Form from \"misago/components/form\"\nimport { load as updatePosts } from \"misago/reducers/posts\"\nimport { update as updateSearch } from \"misago/reducers/search\"\nimport { hydrate as updateUsers } from \"misago/reducers/users\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\nimport { FlexRow, FlexRowCol, FlexRowSection } from \"../FlexRow\"\nimport {\n PageHeader,\n PageHeaderContainer,\n PageHeaderBanner,\n PageHeaderDetails,\n} from \"../PageHeader\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n query: props.search.query,\n }\n }\n\n componentDidMount() {\n if (this.state.query.length) {\n this.handleSubmit()\n }\n }\n\n onQueryChange = (event) => {\n this.changeValue(\"query\", event.target.value)\n }\n\n clean() {\n if (!this.state.query.trim().length) {\n snackbar.error(pgettext(\"search form\", \"You have to enter search query.\"))\n return false\n }\n\n return true\n }\n\n send() {\n store.dispatch(\n updateSearch({\n isLoading: true,\n })\n )\n\n const query = this.state.query.trim()\n\n let url = window.location.href\n const urlQuery = url.indexOf(\"?q=\")\n if (urlQuery > 0) {\n url = url.substring(0, urlQuery + 3)\n }\n window.history.pushState({}, \"\", url + encodeURIComponent(query))\n\n return ajax.get(misago.get(\"SEARCH_API\"), { q: query })\n }\n\n handleSuccess(providers) {\n store.dispatch(\n updateSearch({\n query: this.state.query.trim(),\n isLoading: false,\n providers,\n })\n )\n\n providers.forEach((provider) => {\n if (provider.id === \"users\") {\n store.dispatch(updateUsers(provider.results.results))\n } else if (provider.id === \"threads\") {\n store.dispatch(updatePosts(provider.results))\n }\n })\n }\n\n handleError(rejection) {\n snackbar.apiError(rejection)\n\n store.dispatch(\n updateSearch({\n isLoading: false,\n })\n )\n }\n\n render() {\n return (\n
    \n \n \n \n

    {pgettext(\"search form title\", \"Search\")}

    \n
    \n \n \n \n \n \n \n \n \n search\n \n \n \n \n \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport { Link } from \"react-router\"\n\nexport default function (props) {\n return (\n
    \n {props.providers.map((provider) => {\n return (\n \n {provider.icon}\n {provider.name}\n \n \n )\n })}\n
    \n )\n}\n\nexport function Badge(props) {\n if (!props.results) return null\n\n let count = props.results.count\n if (count > 1000000) {\n count = Math.ceil(count / 1000000) + \"KK\"\n } else if (count > 1000) {\n count = Math.ceil(count / 1000) + \"K\"\n }\n\n return {count}\n}\n","import React from \"react\"\nimport PageContainer from \"../PageContainer\"\nimport SearchForm from \"./form\"\nimport SideNav from \"./sidenav\"\n\nexport default function (props) {\n return (\n
    \n \n \n
    \n
    \n \n
    \n
    \n {props.children}\n \n
    \n
    \n
    \n
    \n )\n}\n\nexport function SearchTime(props) {\n let time = null\n props.search.providers.forEach((p) => {\n if (p.id === props.provider.id) {\n time = p.time\n }\n })\n\n if (time === null) return null\n\n const copy = pgettext(\"search time\", \"Search took %(time)s s\")\n\n return (\n
    \n

    {interpolate(copy, { time }, true)}

    \n
    \n )\n}\n","import React from \"react\"\nimport PostFeed from \"misago/components/post-feed\"\nimport Button from \"misago/components/button\"\nimport MisagoMarkup from \"misago/components/misago-markup\"\nimport {\n update as updatePosts,\n append as appendPosts,\n} from \"misago/reducers/posts\"\nimport { updateProvider } from \"misago/reducers/search\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default function (props) {\n return (\n
    \n \n \n
    \n )\n}\n\nexport class LoadMore extends React.Component {\n onClick = () => {\n store.dispatch(\n updatePosts({\n isBusy: true,\n })\n )\n\n ajax\n .get(this.props.provider.api, {\n q: this.props.query,\n page: this.props.next,\n })\n .then(\n (providers) => {\n providers.forEach((provider) => {\n if (provider.id !== \"threads\") return\n store.dispatch(appendPosts(provider.results))\n store.dispatch(updateProvider(provider))\n })\n\n store.dispatch(\n updatePosts({\n isBusy: false,\n })\n )\n },\n (rejection) => {\n snackbar.apiError(rejection)\n\n store.dispatch(\n updatePosts({\n isBusy: false,\n })\n )\n }\n )\n }\n\n render() {\n if (!this.props.more) return null\n\n return (\n
    \n \n {pgettext(\"search threads btn\", \"Show more\")}\n \n
    \n )\n }\n}\n","import React from \"react\"\nimport SearchPage from \"../page\"\nimport Results from \"./results\"\n\nexport default function (props) {\n return (\n \n \n \n
    \n \n )\n}\n\nexport function Blankslate({ children, loading, posts, query }) {\n if (posts && posts.count) return children\n\n if (query.length) {\n return (\n

    \n {loading\n ? pgettext(\"search threads\", \"Loading results...\")\n : pgettext(\n \"search threads\",\n \"No threads matching search query have been found.\"\n )}\n

    \n )\n }\n\n return (\n

    \n {pgettext(\n \"search threads\",\n \"Enter at least two characters to search threads.\"\n )}\n

    \n )\n}\n","import React from \"react\"\nimport SearchPage from \"../page\"\nimport UsersList from \"misago/components/users-list\"\n\nexport default function (props) {\n return (\n \n \n \n \n \n )\n}\n\nexport function Blankslate({ children, loading, query, users }) {\n if (users.length) return children\n\n if (query.length) {\n return (\n

    \n {loading\n ? pgettext(\"search users\", \"Loading results...\")\n : pgettext(\n \"search users\",\n \"No users matching search query have been found.\"\n )}\n

    \n )\n }\n\n return (\n

    \n {pgettext(\n \"search users\",\n \"Enter at least two characters to search users.\"\n )}\n

    \n )\n}\n","import { connect } from \"react-redux\"\nimport SearchThreads from \"./threads\"\nimport SearchUsers from \"./users\"\n\nconst components = {\n threads: SearchThreads,\n users: SearchUsers,\n}\n\nexport function select(store) {\n return {\n posts: store.posts,\n search: store.search,\n tick: store.tick.tick,\n user: store.auth.user,\n users: store.users,\n }\n}\n\nexport default function (providers) {\n return providers.map((provider) => {\n return {\n path: provider.url,\n component: connect(select)(components[provider.id]),\n provider: provider,\n }\n })\n}\n","import paths from \"misago/components/search-route\"\nimport misago from \"misago\"\nimport mount from \"misago/utils/routed-component\"\n\nexport default function initializer(context) {\n if (context.get(\"CURRENT_LINK\") === \"misago:search\") {\n mount({\n paths: paths(misago.get(\"SEARCH_PROVIDERS\")),\n })\n }\n}\n\nmisago.addInitializer({\n name: \"component:search\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\nimport { Provider } from \"react-redux\"\nimport { SiteNavOverlay } from \"../../components/SiteNav\"\nimport store from \"../../services/store\"\n\nexport default function initializer(context) {\n const root = document.getElementById(\"site-nav-mount\")\n ReactDOM.render(\n \n \n ,\n root\n )\n}\n\nmisago.addInitializer({\n name: \"component:site-nav-overlay\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\n\nconst TYPES_CLASSES = {\n info: \"alert-info\",\n success: \"alert-success\",\n warning: \"alert-warning\",\n error: \"alert-danger\",\n}\n\nexport class Snackbar extends React.Component {\n getSnackbarClass() {\n let snackbarClass = \"alerts-snackbar\"\n if (this.props.isVisible) {\n snackbarClass += \" in\"\n } else {\n snackbarClass += \" out\"\n }\n return snackbarClass\n }\n\n render() {\n return (\n
    \n

    \n {this.props.message}\n

    \n
    \n )\n }\n}\n\nexport function select(state) {\n return state.snackbar\n}\n","import { connect } from \"react-redux\"\nimport misago from \"misago/index\"\nimport { Snackbar, select } from \"misago/components/snackbar\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer() {\n mount(connect(select)(Snackbar), \"snackbar-mount\")\n}\n\nmisago.addInitializer({\n name: \"component:snackbar\",\n initializer: initializer,\n after: \"snackbar\",\n})\n","import React from \"react\"\nimport {\n PageHeader,\n PageHeaderBanner,\n PageHeaderContainer,\n} from \"../PageHeader\"\n\nconst Header = ({ backendName }) => {\n const pageTitleTpl = pgettext(\"social auth title\", \"Sign in with %(backend)s\")\n const pageTitle = interpolate(pageTitleTpl, { backend: backendName }, true)\n\n return (\n \n \n \n

    {pageTitle}

    \n
    \n
    \n
    \n )\n}\n\nexport default Header\n","import React from \"react\"\nimport misago from \"misago\"\nimport RegisterLegalFootnote from \"misago/components/RegisterLegalFootnote\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport * as validators from \"misago/utils/validators\"\nimport PageContainer from \"../PageContainer\"\nimport Header from \"./header\"\n\nexport default class Register extends Form {\n constructor(props) {\n super(props)\n\n const formValidators = {\n email: [validators.email()],\n username: [validators.usernameContent()],\n }\n\n if (!!misago.get(\"TERMS_OF_SERVICE_ID\")) {\n formValidators.termsOfService = [validators.requiredTermsOfService()]\n }\n\n if (!!misago.get(\"PRIVACY_POLICY_ID\")) {\n formValidators.privacyPolicy = [validators.requiredPrivacyPolicy()]\n }\n\n this.state = {\n email: props.email || \"\",\n emailProtected: !!props.email,\n username: props.username || \"\",\n\n termsOfService: null,\n privacyPolicy: null,\n\n validators: formValidators,\n errors: {},\n\n isLoading: false,\n }\n }\n\n clean() {\n let errors = this.validate()\n let lengths = [\n this.state.email.trim().length,\n this.state.username.trim().length,\n ]\n\n if (lengths.indexOf(0) !== -1) {\n snackbar.error(pgettext(\"social auth form\", \"Fill out all fields.\"))\n return false\n }\n\n const { validators } = this.state\n\n const checkTermsOfService = !!misago.get(\"TERMS_OF_SERVICE_ID\")\n if (checkTermsOfService && this.state.termsOfService === null) {\n snackbar.error(validators.termsOfService[0](null))\n return false\n }\n\n const checkPrivacyPolicy = !!misago.get(\"PRIVACY_POLICY_ID\")\n if (checkPrivacyPolicy && this.state.privacyPolicy === null) {\n snackbar.error(validators.privacyPolicy[0](null))\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.post(this.props.url, {\n email: this.state.email,\n username: this.state.username,\n terms_of_service: this.state.termsOfService,\n privacy_policy: this.state.privacyPolicy,\n })\n }\n\n handleSuccess(response) {\n const { onRegistrationComplete } = this.props\n onRegistrationComplete(response)\n }\n\n handleError(rejection) {\n if (rejection.status === 200) {\n // We've entered \"errored\" state because response is HTML instead of exptected JSON\n const { onRegistrationComplete } = this.props\n const { username } = this.state\n onRegistrationComplete({ activation: \"active\", step: \"done\", username })\n } else if (rejection.status === 400) {\n const stateUpdate = { errors: rejection }\n if (rejection.email) {\n stateUpdate.emailProtected = false\n }\n this.setState(stateUpdate)\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n handlePrivacyPolicyChange = (event) => {\n const value = event.target.value\n this.handleToggleAgreement(\"privacyPolicy\", value)\n }\n\n handleTermsOfServiceChange = (event) => {\n const value = event.target.value\n this.handleToggleAgreement(\"termsOfService\", value)\n }\n\n handleToggleAgreement = (agreement, value) => {\n this.setState((prevState, props) => {\n if (prevState[agreement] === null) {\n const errors = { ...prevState.errors, [agreement]: null }\n return { errors, [agreement]: value }\n }\n\n const validator = this.state.validators[agreement][0]\n const errors = { ...prevState.errors, [agreement]: [validator(null)] }\n return { errors, [agreement]: null }\n })\n }\n\n render() {\n const { backend_name } = this.props\n const { email, emailProtected, username, isLoading } = this.state\n\n let emailHelpText = null\n if (emailProtected) {\n const emailHelpTextTpl = pgettext(\n \"social auth form\",\n \"Your e-mail address has been verified by %(backend)s.\"\n )\n emailHelpText = interpolate(\n emailHelpTextTpl,\n { backend: backend_name },\n true\n )\n }\n\n return (\n
    \n
    \n \n
    \n
    \n
    \n
    \n
    \n

    \n {pgettext(\n \"social auth form title\",\n \"Complete your account\"\n )}\n

    \n
    \n
    \n \n \n \n \n \n \n \n
    \n
    \n \n {pgettext(\"social auth form btn\", \"Sign in\")}\n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport misago from \"misago\"\nimport PageContainer from \"../PageContainer\"\nimport Header from \"./header\"\n\nconst Complete = ({ activation, backend_name, username }) => {\n let icon = \"\"\n let message = \"\"\n if (activation === \"user\") {\n message = pgettext(\n \"account activation required\",\n \"%(username)s, your account has been created but you need to activate it before you will be able to sign in.\"\n )\n } else if (activation === \"admin\") {\n message = pgettext(\n \"account activation required\",\n \"%(username)s, your account has been created but the site administrator will have to activate it before you will be able to sign in.\"\n )\n } else {\n message = pgettext(\n \"social auth complete\",\n \"%(username)s, your account has been created and you have been signed in to it.\"\n )\n }\n\n if (activation === \"active\") {\n icon = \"check\"\n } else {\n icon = \"info_outline\"\n }\n\n return (\n
    \n
    \n \n
    \n
    \n
    \n
    \n

    \n {pgettext(\n \"social auth complete title\",\n \"Registration completed!\"\n )}\n

    \n
    \n
    \n
    \n {icon}\n
    \n
    \n

    \n {interpolate(message, { username }, true)}\n

    \n

    \n \n {pgettext(\n \"social auth complete link\",\n \"Return to forum index\"\n )}\n \n

    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n )\n}\n\nexport default Complete\n","import React from \"react\"\nimport Register from \"./register\"\nimport Complete from \"./complete\"\n\nexport default class SocialAuth extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n step: props.step,\n\n activation: props.activation || \"\",\n email: props.email || \"\",\n username: props.username || \"\",\n }\n }\n\n handleRegistrationComplete = ({ activation, email, step, username }) => {\n this.setState({ activation, email, step, username })\n }\n\n render() {\n const { backend_name, url } = this.props\n const { activation, email, step, username } = this.state\n\n if (step === \"register\") {\n return (\n \n )\n }\n\n return (\n \n )\n }\n}\n","import React from \"react\"\nimport SocialAuth from \"misago/components/social-auth\"\nimport misago from \"misago\"\nimport mount from \"misago/utils/mount-component\"\n\nexport default function initializer(context) {\n if (context.get(\"CURRENT_LINK\") === \"misago:social-complete\") {\n const props = context.get(\"SOCIAL_AUTH_FORM\")\n mount(, \"page-mount\")\n }\n}\n\nmisago.addInitializer({\n name: \"component:social-auth\",\n initializer: initializer,\n after: \"store\",\n})\n","import React from \"react\"\nimport Form from \"./form\"\nimport FormGroup from \"misago/components/form-group\"\nimport * as participants from \"misago/reducers/participants\"\nimport { updateAcl } from \"misago/reducers/thread\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n username: \"\",\n }\n }\n\n onUsernameChange = (event) => {\n this.changeValue(\"username\", event.target.value)\n }\n\n clean() {\n if (!this.state.username.trim().length) {\n snackbar.error(\n pgettext(\n \"add private thread participant\",\n \"You have to enter user name.\"\n )\n )\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.patch(this.props.thread.api.index, [\n { op: \"add\", path: \"participants\", value: this.state.username },\n { op: \"add\", path: \"acl\", value: 1 },\n ])\n }\n\n handleSuccess(data) {\n store.dispatch(updateAcl(data))\n store.dispatch(participants.replace(data.participants))\n\n snackbar.success(\n pgettext(\n \"add private thread participant\",\n \"New participant has been added to thread.\"\n )\n )\n\n modal.hide()\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n
    \n \n \n \n
    \n
    \n \n {pgettext(\n \"add private thread participant btn\",\n \"Add participant\"\n )}\n \n \n {pgettext(\"add private thread participant btn\", \"Cancel\")}\n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport function ModalHeader(props) {\n return (\n
    \n \n ×\n \n

    \n {pgettext(\n \"add private thread participant modal title\",\n \"Add participant\"\n )}\n

    \n
    \n )\n}\n","import React from \"react\"\nimport AddParticipantModal from \"misago/components/add-participant\"\nimport modal from \"misago/services/modal\"\n\nexport default class extends React.Component {\n onClick = () => {\n modal.show()\n }\n\n render() {\n if (!this.props.thread.acl.can_add_participants) return null\n\n return (\n
    \n \n person_add\n {pgettext(\"add participant btn\", \"Add participant\")}\n \n
    \n )\n }\n}\n","import React from \"react\"\nimport { changeOwner } from \"./actions\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.isUser = props.participant.id === props.user.id\n }\n\n onClick = () => {\n let confirmed = false\n if (this.isUser) {\n confirmed = window.confirm(\n pgettext(\n \"private thread owner change\",\n \"Are you sure you want to take over this thread?\"\n )\n )\n } else {\n const message = pgettext(\n \"private thread owner change\",\n \"Are you sure you want to change thread owner to %(user)s?\"\n )\n confirmed = window.confirm(\n interpolate(\n message,\n {\n user: this.props.participant.username,\n },\n true\n )\n )\n }\n\n if (!confirmed) return\n\n changeOwner(this.props.thread, this.props.participant)\n }\n\n render() {\n if (this.props.participant.is_owner) return null\n if (!this.props.thread.acl.can_change_owner) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n","import * as participants from \"misago/reducers/participants\"\nimport { updateAcl } from \"misago/reducers/thread\"\nimport misago from \"misago\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport function leave(thread, participant) {\n ajax\n .patch(thread.api.index, [\n { op: \"remove\", path: \"participants\", value: participant.id },\n ])\n .then(\n () => {\n snackbar.success(\n pgettext(\"thread participants actions\", \"You have left this thread.\")\n )\n window.setTimeout(() => {\n window.location = misago.get(\"PRIVATE_THREADS_URL\")\n }, 3 * 1000)\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n}\n\nexport function remove(thread, participant) {\n ajax\n .patch(thread.api.index, [\n { op: \"remove\", path: \"participants\", value: participant.id },\n { op: \"add\", path: \"acl\", value: 1 },\n ])\n .then(\n (data) => {\n store.dispatch(updateAcl(data))\n store.dispatch(participants.replace(data.participants))\n\n const message = pgettext(\n \"thread participants actions\",\n \"%(user)s has been removed from this thread.\"\n )\n snackbar.success(\n interpolate(\n message,\n {\n user: participant.username,\n },\n true\n )\n )\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n}\n\nexport function changeOwner(thread, participant) {\n ajax\n .patch(thread.api.index, [\n { op: \"replace\", path: \"owner\", value: participant.id },\n { op: \"add\", path: \"acl\", value: 1 },\n ])\n .then(\n (data) => {\n store.dispatch(updateAcl(data))\n store.dispatch(participants.replace(data.participants))\n\n const message = pgettext(\n \"thread participants actions\",\n \"%(user)s has been made new thread owner.\"\n )\n snackbar.success(\n interpolate(\n message,\n {\n user: participant.username,\n },\n true\n )\n )\n },\n (rejection) => {\n snackbar.apiError(rejection)\n }\n )\n}\n","import React from \"react\"\nimport { remove, leave } from \"./actions\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.isUser = props.participant.id === props.user.id\n }\n\n onClick = () => {\n let confirmed = false\n if (this.isUser) {\n confirmed = window.confirm(\n pgettext(\n \"private thread leave\",\n \"Are you sure you want to leave this thread?\"\n )\n )\n } else {\n const message = pgettext(\n \"private thread leave\",\n \"Are you sure you want to remove %(user)s from this thread?\"\n )\n confirmed = window.confirm(\n interpolate(\n message,\n {\n user: this.props.participant.username,\n },\n true\n )\n )\n }\n\n if (!confirmed) return\n\n if (this.isUser) {\n leave(this.props.thread, this.props.participant)\n } else {\n remove(this.props.thread, this.props.participant)\n }\n }\n\n render() {\n const isModerator = this.props.user.acl.can_moderate_private_threads\n\n if (!(this.props.userIsOwner || this.isUser || isModerator)) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n","import React from \"react\"\nimport MakeOwner from \"./make-owner\"\nimport Remove from \"./remove\"\nimport Avatar from \"misago/components/avatar\"\n\nexport default function (props) {\n const participant = props.participant\n\n let className = \"btn btn-default\"\n if (participant.is_owner) {\n className = \"btn btn-primary\"\n }\n className += \" btn-user btn-block\"\n\n return (\n
    \n
    \n \n \n {participant.username}\n \n \n
    \n
    \n )\n}\n\nexport function UserStatus({ isOwner }) {\n if (!isOwner) return null\n\n return (\n
  • \n start\n \n {pgettext(\"thread participants owner status\", \"Thread owner\")}\n \n
  • \n )\n}\n","import React from \"react\"\nimport Card from \"./card\"\n\nexport default function ({ participants, thread, user, userIsOwner }) {\n return (\n
    \n
    \n {participants.map((participant) => {\n return (\n \n )\n })}\n
    \n
    \n )\n}\n","import React from \"react\"\nimport AddParticipant from \"./add-participant\"\nimport CardsList from \"./cards-list\"\nimport * as utils from \"./utils\"\n\nexport default function (props) {\n if (!props.participants.length) return null\n\n return (\n
    \n
    \n \n
    \n \n
    \n

    {utils.getParticipantsCopy(props.participants)}

    \n
    \n
    \n
    \n
    \n )\n}\n\nexport function getUserIsOwner(user, participants) {\n return participants[0].id === user.id\n}\n","export function getParticipantsCopy(participants) {\n const count = participants.length\n const message = npgettext(\n \"thread participants stat\",\n \"This thread has %(users)s participant.\",\n \"This thread has %(users)s participants.\",\n count\n )\n\n return interpolate(\n message,\n {\n users: count,\n },\n true\n )\n}\n","import React from \"react\"\n\nexport default function (props) {\n return (\n
    \n {props.poll.choices.map((choice) => {\n return (\n \n )\n })}\n
    \n )\n}\n\nexport function PollChoice(props) {\n let proc = 0\n if (props.choice.votes && props.poll.votes) {\n proc = Math.ceil((props.choice.votes * 100) / props.poll.votes)\n }\n\n return (\n
    \n
    {props.choice.label}
    \n
    \n
    \n \n \n {getVotesLabel(props.votes, props.proc)}\n \n
    \n \n
      \n \n \n
    \n
    \n
    \n )\n}\n\nexport function ChoiceVotes(props) {\n return (\n
  • \n {getVotesLabel(props.votes, props.proc)}\n
  • \n )\n}\n\nexport function getVotesLabel(votes, proc) {\n const message = npgettext(\n \"thread poll\",\n \"%(votes)s vote, %(proc)s% of total.\",\n \"%(votes)s votes, %(proc)s% of total.\",\n votes\n )\n\n return interpolate(\n message,\n {\n votes: votes,\n proc: proc,\n },\n true\n )\n}\n\nexport function UserChoice(props) {\n if (!props.selected) return null\n\n return (\n
  • \n check_box\n {pgettext(\"thread poll\", \"You've voted on this choice.\")}\n
  • \n )\n}\n","import React from \"react\"\nimport moment from \"moment\"\nimport Message from \"misago/components/modal-message\"\nimport Loader from \"misago/components/modal-loader\"\nimport ajax from \"misago/services/ajax\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: true,\n error: null,\n data: [],\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.poll.api.votes).then(\n (data) => {\n const hydratedData = data.map((choice) => {\n return Object.assign({}, choice, {\n voters: choice.voters.map((voter) => {\n return Object.assign({}, voter, {\n voted_on: moment(voter.voted_on),\n })\n }),\n })\n })\n\n this.setState({\n isLoading: false,\n data: hydratedData,\n })\n },\n (rejection) => {\n this.setState({\n isLoading: false,\n error: rejection.detail,\n })\n }\n )\n }\n\n render() {\n return (\n \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"thread poll\", \"Poll votes\")}\n

    \n
    \n\n \n
    \n \n )\n }\n}\n\nexport function ModalBody(props) {\n if (props.isLoading) {\n return \n } else if (props.error) {\n return \n }\n\n return \n}\n\nexport function ChoicesList(props) {\n return (\n
    \n
      \n {props.data.map((choice) => {\n return \n })}\n
    \n
    \n )\n}\n\nexport function ChoiceDetails(props) {\n return (\n
  • \n

    {props.label}

    \n \n \n
    \n
  • \n )\n}\n\nexport function VotesCount(props) {\n const message = npgettext(\n \"thread poll\",\n \"%(votes)s user has voted for this choice.\",\n \"%(votes)s users have voted for this choice.\",\n props.votes\n )\n\n const label = interpolate(\n message,\n {\n votes: props.votes,\n },\n true\n )\n\n return

    {label}

    \n}\n\nexport function VotesList(props) {\n if (!props.voters.length) return null\n\n return (\n
      \n {props.voters.map((user) => {\n return \n })}\n
    \n )\n}\n\nexport function Voter(props) {\n if (props.url) {\n return (\n
  • \n \n {props.username}\n {\" \"}\n \n
  • \n )\n }\n\n return (\n
  • \n {props.username} \n
  • \n )\n}\n\nexport function VoteDate(props) {\n return (\n \n {props.voted_on.fromNow()}\n \n )\n}\n","import React from \"react\"\nimport Modal from \"./modal\"\nimport * as poll from \"misago/reducers/poll\"\nimport * as thread from \"misago/reducers/thread\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\nimport posting from \"misago/services/posting\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default function (props) {\n const { isPollOver, poll, showVoting, thread } = props\n\n if (!isVisible(isPollOver, poll.acl, poll)) return null\n\n const controls = []\n\n const canVote = poll.acl.can_vote\n const canChangeVote = !poll.hasSelectedChoices || poll.allow_revotes\n\n if (canVote && canChangeVote) controls.push(0)\n if (poll.is_public || poll.acl.can_see_votes) controls.push(1)\n if (poll.acl.can_edit) controls.push(2)\n if (poll.acl.can_delete) controls.push(3)\n\n return (\n
    \n \n \n \n \n
    \n )\n}\n\nexport function isVisible(isPollOver, acl, poll) {\n return (\n poll.is_public ||\n acl.can_delete ||\n acl.can_edit ||\n acl.can_see_votes ||\n (acl.can_vote &&\n !isPollOver &&\n (!poll.hasSelectedChoices || poll.allow_revotes))\n )\n}\n\nexport function getClassName(controls, control) {\n let className = \"col-xs-6\"\n\n if (controls.length === 1) {\n className = \"col-xs-12\"\n }\n\n if (controls.length === 3 && controls[0] === control) {\n className = \"col-xs-12\"\n }\n\n return className + \" col-sm-3 col-md-2\"\n}\n\nexport function ChangeVote(props) {\n const canVote = props.poll.acl.can_vote\n const canChangeVote =\n !props.poll.hasSelectedChoices || props.poll.allow_revotes\n\n if (!(canVote && canChangeVote)) return null\n\n return (\n
    \n \n {pgettext(\"thread poll\", \"Vote\")}\n \n
    \n )\n}\n\nexport class SeeVotes extends React.Component {\n onClick = () => {\n modal.show()\n }\n\n render() {\n const seeVotes =\n this.props.poll.is_public || this.props.poll.acl.can_see_votes\n if (!seeVotes) return null\n\n return (\n
    \n \n {pgettext(\"thread poll\", \"See votes\")}\n \n
    \n )\n }\n}\n\nexport function Edit(props) {\n if (!props.poll.acl.can_edit) return null\n\n return (\n
    \n \n {pgettext(\"thread poll\", \"Edit\")}\n \n
    \n )\n}\n\nexport class Delete extends React.Component {\n onClick = () => {\n const deletePoll = window.confirm(\n pgettext(\n \"thread poll\",\n \"Are you sure you want to delete this poll? This action is not reversible.\"\n )\n )\n if (!deletePoll) return false\n\n store.dispatch(poll.busy())\n\n ajax\n .delete(this.props.poll.api.index)\n .then(this.handleSuccess, this.handleError)\n }\n\n handleSuccess = (newThreadAcl) => {\n snackbar.success(pgettext(\"thread poll\", \"Poll has been deleted\"))\n store.dispatch(poll.remove())\n store.dispatch(thread.updateAcl(newThreadAcl))\n }\n\n handleError = (rejection) => {\n snackbar.apiError(rejection)\n store.dispatch(poll.release())\n }\n\n render() {\n if (!this.props.poll.acl.can_delete) return null\n\n return (\n
    \n \n {pgettext(\"thread poll\", \"Delete\")}\n \n
    \n )\n }\n}\n","import React from \"react\"\nimport escapeHtml from \"misago/utils/escape-html\"\n\nconst DATE_ABBR = '%(relative)s'\nconst USER_SPAN = '%(user)s'\nconst USER_URL = '%(user)s'\n\nexport default function (props) {\n return (\n
      \n \n \n \n \n
    \n )\n}\n\nexport function PollCreation(props) {\n const message = interpolate(\n escapeHtml(pgettext(\"thread poll\", \"Started by %(poster)s %(posted_on)s.\")),\n {\n poster: getPoster(props.poll),\n posted_on: getPostedOn(props.poll),\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function getPoster(poll) {\n if (poll.url.poster) {\n return interpolate(\n USER_URL,\n {\n url: escapeHtml(poll.url.poster),\n user: escapeHtml(poll.poster_name),\n },\n true\n )\n }\n\n return interpolate(\n USER_SPAN,\n {\n user: escapeHtml(poll.poster_name),\n },\n true\n )\n}\n\nexport function getPostedOn(poll) {\n return interpolate(\n DATE_ABBR,\n {\n absolute: escapeHtml(poll.posted_on.format(\"LLL\")),\n relative: escapeHtml(poll.posted_on.fromNow()),\n },\n true\n )\n}\n\nexport function PollLength(props) {\n if (!props.poll.length) {\n return null\n }\n\n const message = interpolate(\n escapeHtml(pgettext(\"thread poll\", \"Voting ends %(ends_on)s.\")),\n {\n ends_on: getEndsOn(props.poll),\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function getEndsOn(poll) {\n return interpolate(\n DATE_ABBR,\n {\n absolute: escapeHtml(poll.endsOn.format(\"LLL\")),\n relative: escapeHtml(poll.endsOn.fromNow()),\n },\n true\n )\n}\n\nexport function PollVotes(props) {\n const message = npgettext(\n \"thread poll\",\n \"%(votes)s vote.\",\n \"%(votes)s votes.\",\n props.votes\n )\n const label = interpolate(\n message,\n {\n votes: props.votes,\n },\n true\n )\n\n return
  • {label}
  • \n}\n\nexport function PollIsPublic(props) {\n if (!props.poll.is_public) {\n return null\n }\n\n return (\n
  • \n {pgettext(\"thread poll\", \"Voting is public.\")}\n
  • \n )\n}\n","import React from \"react\"\nimport Chart from \"./chart\"\nimport Options from \"./options\"\nimport PollInfo from \"../info\"\n\nexport default function (props) {\n return (\n
    \n
    \n

    {props.poll.question}

    \n \n \n \n
    \n
    \n )\n}\n","import React from \"react\"\nimport escapeHtml from \"misago/utils/escape-html\"\n\nconst DATE_ABBR = '%(relative)s'\nconst USER_SPAN = '%(user)s'\nconst USER_URL = '%(user)s'\n\nexport default function (props) {\n return (\n
      \n \n \n
    \n )\n}\n\nexport function PollChoicesLeft({ choicesLeft }) {\n if (choicesLeft === 0) {\n return (\n
  • \n {pgettext(\"thread poll\", \"You can't select any more choices.\")}\n
  • \n )\n }\n\n const message = npgettext(\n \"thread poll\",\n \"You can select %(choices)s more choice.\",\n \"You can select %(choices)s more choices.\",\n choicesLeft\n )\n\n const label = interpolate(\n message,\n {\n choices: choicesLeft,\n },\n true\n )\n\n return
  • {label}
  • \n}\n\nexport function PollAllowRevote(props) {\n if (props.poll.allow_revotes) {\n return (\n
  • \n {pgettext(\"thread poll\", \"You can change your vote later.\")}\n
  • \n )\n }\n\n return (\n
  • \n {pgettext(\"thread poll\", \"Votes are final.\")}\n
  • \n )\n}\n","import React from \"react\"\n\nexport default function (props) {\n return (\n
      \n {props.choices.map((choice) => {\n return (\n \n )\n })}\n
    \n )\n}\n\nexport class ChoiceSelect extends React.Component {\n onClick = () => {\n this.props.toggleChoice(this.props.choice.hash)\n }\n\n render() {\n return (\n
  • \n \n \n {this.props.choice.selected\n ? \"check_box\"\n : \"check_box_outline_blank\"}\n \n {this.props.choice.label}\n \n
  • \n )\n }\n}\n","export function getChoiceFromHash(choices, hash) {\n for (const i in choices) {\n const choice = choices[i]\n if (choice.hash === hash) {\n return choice\n }\n }\n\n return null\n}\n\nexport function getChoicesLeft(poll, choices) {\n let selection = []\n for (const i in choices) {\n const choice = choices[i]\n if (choice.selected) {\n selection.push(choice)\n }\n }\n\n return poll.allowed_choices - selection.length\n}\n","import React from \"react\"\nimport ChoicesHelp from \"./help\"\nimport ChoicesSelect from \"./select\"\nimport { getChoicesLeft, getChoiceFromHash } from \"./utils\"\nimport PollInfo from \"../info\"\nimport { Delete, Edit, getClassName } from \"../results/options\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport * as poll from \"misago/reducers/poll\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n choices: props.poll.choices,\n choicesLeft: getChoicesLeft(props.poll, props.poll.choices),\n }\n }\n\n toggleChoice = (hash) => {\n const choice = getChoiceFromHash(this.state.choices, hash)\n\n let choices = null\n if (!choice.selected) {\n choices = this.selectChoice(choice, hash)\n } else {\n choices = this.deselectChoice(choice, hash)\n }\n\n this.setState({\n choices,\n choicesLeft: getChoicesLeft(this.props.poll, choices),\n })\n }\n\n selectChoice = (choice, hash) => {\n const choicesLeft = getChoicesLeft(this.props.poll, this.state.choices)\n\n if (!choicesLeft) {\n for (const i in this.state.choices.slice()) {\n const item = this.state.choices[i]\n if (item.selected && item.hash != hash) {\n item.selected = false\n break\n }\n }\n }\n\n return this.state.choices.map((choice) => {\n return Object.assign({}, choice, {\n selected: choice.hash == hash ? true : choice.selected,\n })\n })\n }\n\n deselectChoice = (choice, hash) => {\n return this.state.choices.map((choice) => {\n return Object.assign({}, choice, {\n selected: choice.hash == hash ? false : choice.selected,\n })\n })\n }\n\n clean() {\n if (this.state.choicesLeft === this.props.poll.allowed_choices) {\n snackbar.error(\n pgettext(\"thread poll vote\", \"You need to select at least one choice.\")\n )\n return false\n }\n\n return true\n }\n\n send() {\n let data = []\n for (const i in this.state.choices.slice()) {\n const item = this.state.choices[i]\n if (item.selected) {\n data.push(item.hash)\n }\n }\n\n return ajax.post(this.props.poll.api.votes, data)\n }\n\n handleSuccess(data) {\n store.dispatch(poll.replace(data))\n snackbar.success(pgettext(\"thread poll vote\", \"Your vote has been saved.\"))\n\n this.props.showResults()\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n const controls = []\n\n if (this.props.poll.acl.can_vote) controls.push(0)\n if (this.props.poll.is_public || this.props.poll.acl.can_see_votes)\n controls.push(1)\n if (this.props.poll.acl.can_edit) controls.push(2)\n if (this.props.poll.acl.can_delete) controls.push(3)\n\n return (\n
    \n
    \n
    \n

    {this.props.poll.question}

    \n \n \n \n
    \n
    \n
    \n
    \n \n {pgettext(\"thread poll vote btn\", \"Save your vote\")}\n \n
    \n
    \n \n {pgettext(\"thread poll vote btn\", \"See results\")}\n \n
    \n \n \n
    \n
    \n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport moment from \"moment\"\nimport Results from \"./results\"\nimport Voting from \"./voting\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n let showResults = true\n if (props.user.id && !props.poll.hasSelectedChoices) {\n showResults = false\n }\n\n this.state = {\n showResults,\n }\n }\n\n showResults = () => {\n this.setState({\n showResults: true,\n })\n }\n\n showVoting = () => {\n this.setState({\n showResults: false,\n })\n }\n\n render() {\n if (!this.props.thread.poll) return null\n\n const isPollOver = getIsPollOver(this.props.poll)\n\n if (\n !isPollOver &&\n this.props.poll.acl.can_vote &&\n !this.state.showResults\n ) {\n return \n } else {\n return (\n \n )\n }\n }\n}\n\nexport function getIsPollOver(poll) {\n if (poll.length) {\n return moment().isAfter(poll.endsOn)\n }\n return false\n}\n","import React from \"react\"\nimport getRandomString from \"../../../utils/getRandomString\"\n\nconst HASH_LENGTH = 12\n\nexport default class extends React.Component {\n onAdd = () => {\n let choices = this.props.choices.slice()\n choices.push({\n hash: getRandomString(HASH_LENGTH),\n label: \"\",\n })\n\n this.props.setChoices(choices)\n }\n\n onChange = (hash, label) => {\n const choices = this.props.choices.map((choice) => {\n if (choice.hash === hash) {\n choice.label = label\n }\n\n return choice\n })\n this.props.setChoices(choices)\n }\n\n onDelete = (hash) => {\n const choices = this.props.choices.filter((choice) => {\n return choice.hash !== hash\n })\n this.props.setChoices(choices)\n }\n\n render() {\n return (\n
    \n
      \n {this.props.choices.map((choice) => {\n return (\n 2}\n choice={choice}\n disabled={this.props.disabled}\n key={choice.hash}\n onChange={this.onChange}\n onDelete={this.onDelete}\n />\n )\n })}\n
    \n \n {pgettext(\"thread poll\", \"Add choice\")}\n \n
    \n )\n }\n}\n\nexport class PollChoice extends React.Component {\n onChange = (event) => {\n this.props.onChange(this.props.choice.hash, event.target.value)\n }\n\n onDelete = () => {\n const deleteItem =\n this.props.choice.label.length === 0\n ? true\n : window.confirm(\n pgettext(\n \"thread poll\",\n \"Are you sure you want to remove this choice?\"\n )\n )\n if (deleteItem) {\n this.props.onDelete(this.props.choice.hash)\n }\n }\n\n render() {\n return (\n
  • \n \n close\n \n \n
  • \n )\n }\n}\n","import React from \"react\"\nimport ChoicesControl from \"./choices-control\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport YesNoSwitch from \"misago/components/yes-no-switch\"\nimport * as poll from \"misago/reducers/poll\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n const poll = props.poll.id\n ? props.poll\n : {\n question: \"\",\n choices: [\n {\n hash: \"choice-10000\",\n label: \"\",\n },\n {\n hash: \"choice-20000\",\n label: \"\",\n },\n ],\n length: 0,\n allowed_choices: 1,\n allow_revotes: 0,\n is_public: 0,\n }\n\n this.state = {\n isLoading: false,\n isEdit: !!poll.id,\n\n question: poll.question,\n choices: poll.choices,\n length: poll.length,\n allowed_choices: poll.allowed_choices,\n allow_revotes: poll.allow_revotes,\n is_public: poll.is_public,\n\n validators: {\n question: [],\n choices: [],\n length: [],\n allowed_choices: [],\n },\n\n errors: {},\n }\n }\n\n setChoices = (choices) => {\n this.setState((state) => {\n return {\n choices,\n errors: Object.assign({}, state.errors, { choices: null }),\n }\n })\n }\n\n onCancel = () => {\n let cancel = false\n\n // Nothing added to the poll so no changes to discard\n const formEmpty = !!(\n this.state.question === \"\" &&\n this.state.choices &&\n this.state.choices.every((choice) => choice.label === \"\") &&\n this.state.length === 0 &&\n this.state.allowed_choices === 1\n )\n\n if (formEmpty) {\n return this.props.close()\n }\n\n if (!!this.props.poll) {\n cancel = window.confirm(\n pgettext(\"thread poll\", \"Are you sure you want to discard changes?\")\n )\n } else {\n cancel = window.confirm(\n pgettext(\"thread poll\", \"Are you sure you want to discard new poll?\")\n )\n }\n\n if (cancel) {\n this.props.close()\n }\n }\n\n send() {\n const data = {\n question: this.state.question,\n choices: this.state.choices,\n length: this.state.length,\n allowed_choices: this.state.allowed_choices,\n allow_revotes: this.state.allow_revotes,\n is_public: this.state.is_public,\n }\n\n if (this.state.isEdit) {\n return ajax.put(this.props.poll.api.index, data)\n }\n\n return ajax.post(this.props.thread.api.poll, data)\n }\n\n handleSuccess(data) {\n store.dispatch(poll.replace(data))\n\n if (this.state.isEdit) {\n snackbar.success(pgettext(\"thread poll\", \"Poll has been edited.\"))\n } else {\n snackbar.success(pgettext(\"thread poll\", \"Poll has been posted.\"))\n }\n\n this.props.close()\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n if (rejection.non_field_errors) {\n rejection.allowed_choices = rejection.non_field_errors\n }\n\n this.setState({\n errors: Object.assign({}, rejection),\n })\n\n snackbar.error(gettext(\"Form contains errors.\"))\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n

    \n {this.state.isEdit\n ? pgettext(\"thread poll\", \"Edit poll\")\n : pgettext(\"thread poll\", \"Add poll\")}\n

    \n
    \n
    \n
    \n \n {pgettext(\"thread poll\", \"Question and choices\")}\n \n\n \n \n \n\n \n \n \n
    \n\n
    \n {pgettext(\"thread poll\", \"Voting\")}\n\n
    \n
    \n \n \n \n
    \n
    \n \n \n \n
    \n
    \n\n
    \n \n
    \n \n \n \n
    \n
    \n
    \n
    \n
    \n \n {pgettext(\"thread poll\", \"Cancel\")}\n {\" \"}\n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport function PollPublicSwitch(props) {\n if (props.isEdit) return null\n\n return (\n
    \n \n \n \n
    \n )\n}\n","import React from \"react\"\n\nconst ICON = {\n changed_title: \"edit\",\n\n pinned_globally: \"bookmark\",\n pinned_locally: \"bookmark_border\",\n unpinned: \"panorama_fish_eye\",\n\n moved: \"arrow_forward\",\n merged: \"call_merge\",\n\n approved: \"done\",\n\n opened: \"lock_open\",\n closed: \"lock_outline\",\n\n unhid: \"visibility\",\n hid: \"visibility_off\",\n\n changed_owner: \"grade\",\n tookover: \"grade\",\n\n added_participant: \"person_add\",\n\n owner_left: \"person_outline\",\n participant_left: \"person_outline\",\n removed_participant: \"remove_circle_outline\",\n}\n\nconst EventIcon = (props) => (\n \n {ICON[props.post.event_type]}\n \n)\n\nexport default EventIcon\n","import React from \"react\"\nimport moment from \"moment\"\nimport * as post from \"misago/reducers/post\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default function (props) {\n if (isVisible(props.post.acl)) {\n return (\n
  • \n \n \n \n
  • \n )\n } else {\n return null\n }\n}\n\nexport function isVisible(acl) {\n return acl.can_hide\n}\n\nexport class Hide extends React.Component {\n onClick = () => {\n store.dispatch(\n post.patch(this.props.post, {\n is_hidden: true,\n hidden_on: moment(),\n hidden_by_name: this.props.user.username,\n url: Object.assign(this.props.post.url, {\n hidden_by: this.props.user.url,\n }),\n })\n )\n\n const op = { op: \"replace\", path: \"is-hidden\", value: true }\n\n ajax.patch(this.props.post.api.index, [op]).then(\n (patch) => {\n store.dispatch(post.patch(this.props.post, patch))\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n store.dispatch(\n post.patch(this.props.post, {\n is_hidden: false,\n })\n )\n }\n )\n }\n\n render() {\n if (!this.props.post.is_hidden) {\n return (\n \n )\n } else {\n return null\n }\n }\n}\n\nexport class Unhide extends React.Component {\n onClick = () => {\n store.dispatch(\n post.patch(this.props.post, {\n is_hidden: false,\n })\n )\n\n const op = { op: \"replace\", path: \"is-hidden\", value: false }\n\n ajax.patch(this.props.post.api.index, [op]).then(\n (patch) => {\n store.dispatch(post.patch(this.props.post, patch))\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n store.dispatch(\n post.patch(this.props.post, {\n is_hidden: true,\n })\n )\n }\n )\n }\n\n render() {\n if (this.props.post.is_hidden) {\n return (\n \n )\n } else {\n return null\n }\n }\n}\n\nexport class Delete extends React.Component {\n onClick = () => {\n const decision = window.confirm(\n pgettext(\n \"event delete\",\n \"Are you sure you wish to delete this event? This action is not reversible!\"\n )\n )\n if (decision) {\n this.delete()\n }\n }\n\n delete = () => {\n store.dispatch(\n post.patch(this.props.post, {\n isDeleted: true,\n })\n )\n\n ajax.delete(this.props.post.api.index).then(\n () => {\n snackbar.success(pgettext(\"event delete\", \"Event has been deleted.\"))\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n store.dispatch(\n post.patch(this.props.post, {\n isDeleted: false,\n })\n )\n }\n )\n }\n\n render() {\n return (\n \n )\n }\n}\n","import React from \"react\"\nimport escapeHtml from \"misago/utils/escape-html\"\nimport Controls from \"./controls\"\n\nconst DATE_ABBR = '%(relative)s'\nconst DATE_URL = '%(relative)s'\nconst USER_SPAN = '%(user)s'\nconst USER_URL = '%(user)s'\n\nexport default function (props) {\n return (\n
      \n \n \n \n
    \n )\n}\n\nexport function Hidden(props) {\n if (props.post.is_hidden) {\n let user = null\n if (props.post.url.hidden_by) {\n user = interpolate(\n USER_URL,\n {\n url: escapeHtml(props.post.url.hidden_by),\n user: escapeHtml(props.post.hidden_by_name),\n },\n true\n )\n } else {\n user = interpolate(\n USER_SPAN,\n {\n user: escapeHtml(props.post.hidden_by_name),\n },\n true\n )\n }\n\n const date = interpolate(\n DATE_ABBR,\n {\n absolute: escapeHtml(props.post.hidden_on.format(\"LLL\")),\n relative: escapeHtml(props.post.hidden_on.fromNow()),\n },\n true\n )\n\n const message = interpolate(\n escapeHtml(\n pgettext(\"event info\", \"Hidden by %(event_by)s %(event_on)s.\")\n ),\n {\n event_by: user,\n event_on: date,\n },\n true\n )\n\n return (\n \n )\n } else {\n return null\n }\n}\n\nexport function Poster(props) {\n let user = null\n if (props.post.poster) {\n user = interpolate(\n USER_URL,\n {\n url: escapeHtml(props.post.poster.url),\n user: escapeHtml(props.post.poster_name),\n },\n true\n )\n } else {\n user = interpolate(\n USER_SPAN,\n {\n user: escapeHtml(props.post.poster_name),\n },\n true\n )\n }\n\n const date = interpolate(\n DATE_URL,\n {\n url: escapeHtml(props.post.url.index),\n absolute: escapeHtml(props.post.posted_on.format(\"LLL\")),\n relative: escapeHtml(props.post.posted_on.fromNow()),\n },\n true\n )\n\n const message = interpolate(\n escapeHtml(pgettext(\"event info\", \"By %(event_by)s %(event_on)s.\")),\n {\n event_by: user,\n event_on: date,\n },\n true\n )\n\n return (\n \n )\n}\n","import React from \"react\"\nimport escapeHtml from \"misago/utils/escape-html\"\n\nconst MESSAGE = {\n pinned_globally: pgettext(\n \"event message\",\n \"Thread has been pinned globally.\"\n ),\n pinned_locally: pgettext(\n \"event message\",\n \"Thread has been pinned in category.\"\n ),\n unpinned: pgettext(\"event message\", \"Thread has been unpinned.\"),\n\n approved: pgettext(\"event message\", \"Thread has been approved.\"),\n\n opened: pgettext(\"event message\", \"Thread has been opened.\"),\n closed: pgettext(\"event message\", \"Thread has been closed.\"),\n\n unhid: pgettext(\"event message\", \"Thread has been revealed.\"),\n hid: pgettext(\"event message\", \"Thread has been made hidden.\"),\n\n tookover: pgettext(\"event message\", \"Took thread over.\"),\n\n owner_left: pgettext(\n \"event message\",\n \"Owner has left thread. This thread is now closed.\"\n ),\n participant_left: pgettext(\"event message\", \"Participant has left thread.\"),\n}\n\nconst ITEM_LINK = '%(name)s'\nconst ITEM_SPAN = '%(name)s'\n\nexport default function (props) {\n if (MESSAGE[props.post.event_type]) {\n return

    {MESSAGE[props.post.event_type]}

    \n } else if (props.post.event_type === \"changed_title\") {\n return \n } else if (props.post.event_type === \"moved\") {\n return \n } else if (props.post.event_type === \"merged\") {\n return \n } else if (props.post.event_type === \"changed_owner\") {\n return \n } else if (props.post.event_type === \"added_participant\") {\n return \n } else if (props.post.event_type === \"removed_participant\") {\n return \n } else {\n return null\n }\n}\n\nexport function ChangedTitle(props) {\n const msgstring = escapeHtml(\n pgettext(\n \"event message\",\n \"Thread title has been changed from %(old_title)s.\"\n )\n )\n const oldTitle = interpolate(\n ITEM_SPAN,\n {\n name: escapeHtml(props.post.event_context.old_title),\n },\n true\n )\n const message = interpolate(\n msgstring,\n {\n old_title: oldTitle,\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function Moved(props) {\n const msgstring = escapeHtml(\n pgettext(\"event message\", \"Thread has been moved from %(from_category)s.\")\n )\n const fromCategory = interpolate(\n ITEM_LINK,\n {\n url: escapeHtml(props.post.event_context.from_category.url),\n name: escapeHtml(props.post.event_context.from_category.name),\n },\n true\n )\n\n const message = interpolate(\n msgstring,\n {\n from_category: fromCategory,\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function Merged(props) {\n const msgstring = escapeHtml(\n pgettext(\n \"event message\",\n \"The %(merged_thread)s thread has been merged into this thread.\"\n )\n )\n const mergedThread = interpolate(\n ITEM_SPAN,\n {\n name: escapeHtml(props.post.event_context.merged_thread),\n },\n true\n )\n\n const message = interpolate(\n msgstring,\n {\n merged_thread: mergedThread,\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function ChangedOwner(props) {\n const msgstring = escapeHtml(\n pgettext(\"event message\", \"Changed thread owner to %(user)s.\")\n )\n const newOwner = interpolate(\n ITEM_LINK,\n {\n url: escapeHtml(props.post.event_context.user.url),\n name: escapeHtml(props.post.event_context.user.username),\n },\n true\n )\n\n const message = interpolate(\n msgstring,\n {\n user: newOwner,\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function AddedParticipant(props) {\n const msgstring = escapeHtml(\n pgettext(\"event message\", \"Added %(user)s to thread.\")\n )\n const newOwner = interpolate(\n ITEM_LINK,\n {\n url: escapeHtml(props.post.event_context.user.url),\n name: escapeHtml(props.post.event_context.user.username),\n },\n true\n )\n\n const message = interpolate(\n msgstring,\n {\n user: newOwner,\n },\n true\n )\n\n return (\n \n )\n}\n\nexport function RemovedParticipant(props) {\n const msgstring = escapeHtml(\n pgettext(\"event message\", \"Removed %(user)s from thread.\")\n )\n const newOwner = interpolate(\n ITEM_LINK,\n {\n url: escapeHtml(props.post.event_context.user.url),\n name: escapeHtml(props.post.event_context.user.username),\n },\n true\n )\n\n const message = interpolate(\n msgstring,\n {\n user: newOwner,\n },\n true\n )\n\n return (\n \n )\n}\n","import React from \"react\"\n\nexport default function ({ post }) {\n if (post.is_read) return null\n\n return (\n
    \n \n {pgettext(\"event unread label\", \"New event\")}\n \n
    \n )\n}\n","import React from \"react\"\nimport ajax from \"misago/services/ajax\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.initialized = false\n this.primed = false\n this.observer = null\n }\n\n initialize = (element) => {\n this.initialized = true\n\n this.observer = new IntersectionObserver((entries) =>\n entries.forEach(this.callback)\n )\n this.observer.observe(element)\n }\n\n callback = (entry) => {\n if (!entry.isIntersecting || this.props.post.is_read || this.primed) {\n return\n }\n\n window.setTimeout(() => {\n ajax.post(this.props.post.api.read)\n }, 0)\n\n this.primed = true\n this.destroy()\n }\n\n destroy() {\n if (this.observer) {\n this.observer.disconnect()\n this.observer = null\n }\n }\n\n componentWillUnmount() {\n this.destroy()\n }\n\n render() {\n const ready = !this.initialized && !this.primed && !this.props.post.is_read\n\n return (\n {\n if (node && ready) {\n this.initialize(node)\n }\n }}\n >\n {this.props.children}\n \n )\n }\n}\n","import React from \"react\"\nimport Icon from \"./icon\"\nimport Info from \"./info\"\nimport Message from \"./message\"\nimport UnreadLabel from \"./unread-label\"\nimport Waypoint from \"../waypoint\"\n\nexport default function (props) {\n let className = \"event\"\n if (props.post.isDeleted) {\n className = \"hide\"\n } else if (props.post.is_hidden) {\n className = \"event post-hidden\"\n }\n\n return (\n
  • \n \n
    \n
    \n \n
    \n \n \n \n \n
    \n
  • \n )\n}\n","import React from \"react\"\nimport misago from \"misago\"\nimport escapeHtml from \"misago/utils/escape-html\"\nimport formatFilesize from \"misago/utils/file-size\"\n\nconst DATE_ABBR = '%(relative)s'\nconst USER_SPAN = '%(user)s'\nconst USER_URL = '%(user)s'\n\nexport default function (props) {\n return (\n
    \n \n
    \n \n {props.attachment.filename}\n \n \n
    \n
    \n )\n}\n\nexport function AttachmentPreview(props) {\n if (props.attachment.is_image) {\n return (\n
    \n \n
    \n )\n } else {\n return (\n
    \n \n
    \n )\n }\n}\n\nexport function AttachmentIcon(props) {\n return (\n \n insert_drive_file\n \n )\n}\n\nexport function AttachmentThumbnail(props) {\n const url = props.attachment.url.thumb || props.attachment.url.index\n return (\n \n )\n}\n\nexport function AttachmentDetails(props) {\n let user = null\n if (props.attachment.url.uploader) {\n user = interpolate(\n USER_URL,\n {\n url: escapeHtml(props.attachment.url.uploader),\n user: escapeHtml(props.attachment.uploader_name),\n },\n true\n )\n } else {\n user = interpolate(\n USER_SPAN,\n {\n user: escapeHtml(props.attachment.uploader_name),\n },\n true\n )\n }\n\n const date = interpolate(\n DATE_ABBR,\n {\n absolute: escapeHtml(props.attachment.uploaded_on.format(\"LLL\")),\n relative: escapeHtml(props.attachment.uploaded_on.fromNow()),\n },\n true\n )\n\n const message = interpolate(\n escapeHtml(\n pgettext(\n \"post attachment\",\n \"%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s.\"\n )\n ),\n {\n filetype: props.attachment.filetype,\n size: formatFilesize(props.attachment.size),\n uploader: user,\n uploaded_on: date,\n },\n true\n )\n\n return (\n \n )\n}\n","import React from \"react\"\nimport batch from \"misago/utils/batch\"\nimport Attachment from \"./attachment\"\n\nexport default function (props) {\n if (!isVisible(props.post)) {\n return null\n }\n\n return (\n
    \n {batch(props.post.attachments, 2).map((row) => {\n const key = row\n .map((a) => {\n return a ? a.id : 0\n })\n .join(\"_\")\n return \n })}\n
    \n )\n}\n\nexport function isVisible(post) {\n return (!post.is_hidden || post.acl.can_see_hidden) && post.attachments\n}\n\nexport function Row(props) {\n return (\n
    \n {props.row.map((attachment) => {\n return (\n \n )\n })}\n
    \n )\n}\n","import React from \"react\"\nimport Waypoint from \"../waypoint\"\nimport MisagoMarkup from \"misago/components/misago-markup\"\nimport escapeHtml from \"misago/utils/escape-html\"\n\nconst HIDDEN_BY_URL = '%(user)s'\nconst HIDDEN_BY_SPAN = '%(user)s'\nconst HIDDEN_ON =\n '%(relative)s'\n\nexport default function (props) {\n if (props.post.is_hidden && !props.post.acl.can_see_hidden) {\n return \n } else if (props.post.content) {\n return \n } else {\n return \n }\n}\n\nexport function Default({ post }) {\n const poster = \"@\" + (post.poster ? post.poster.username : post.poster_name)\n\n return (\n \n \n \n )\n}\n\nexport function Hidden(props) {\n let user = null\n if (props.post.hidden_by) {\n user = interpolate(\n HIDDEN_BY_URL,\n {\n url: escapeHtml(props.post.url.hidden_by),\n user: escapeHtml(props.post.hidden_by_name),\n },\n true\n )\n } else {\n user = interpolate(\n HIDDEN_BY_SPAN,\n {\n user: escapeHtml(props.post.hidden_by_name),\n },\n true\n )\n }\n\n const date = interpolate(\n HIDDEN_ON,\n {\n absolute: escapeHtml(props.post.hidden_on.format(\"LLL\")),\n relative: escapeHtml(props.post.hidden_on.fromNow()),\n },\n true\n )\n\n const message = interpolate(\n escapeHtml(\n pgettext(\"post body hidden\", \"Hidden by %(hidden_by)s %(hidden_on)s.\")\n ),\n {\n hidden_by: user,\n hidden_on: date,\n },\n true\n )\n\n return (\n \n

    \n {pgettext(\n \"post body hidden\",\n \"This post is hidden. You cannot see its contents.\"\n )}\n

    \n

    \n \n )\n}\n\nexport function Invalid(props) {\n return (\n \n

    \n {pgettext(\n \"post body invalid\",\n \"This post's contents cannot be displayed.\"\n )}\n

    \n

    \n {pgettext(\n \"post body invalid\",\n \"This error is caused by invalid post content manipulation.\"\n )}\n

    \n
    \n )\n}\n","import React from \"react\"\n\nexport function FlagBestAnswer({ post, thread, user }) {\n if (!(isVisible(post) && post.id === thread.best_answer)) {\n return null\n }\n\n let message = null\n if (user.id && thread.best_answer_marked_by === user.id) {\n message = interpolate(\n pgettext(\n \"post best answer flag\",\n \"Marked as best answer by you %(marked_on)s.\"\n ),\n {\n marked_on: thread.best_answer_marked_on.fromNow(),\n },\n true\n )\n } else {\n message = interpolate(\n pgettext(\n \"post best answer flag\",\n \"Marked as best answer by %(marked_by)s %(marked_on)s.\"\n ),\n {\n marked_by: thread.best_answer_marked_by_name,\n marked_on: thread.best_answer_marked_on.fromNow(),\n },\n true\n )\n }\n\n return (\n
    \n check_box\n

    {message}

    \n
    \n )\n}\n\nexport function FlagHidden(props) {\n if (!(isVisible(props.post) && props.post.is_hidden)) {\n return null\n }\n\n return (\n
    \n visibility_off\n

    \n {pgettext(\n \"post hidden flag\",\n \"This post is hidden. Only users with permission may see its contents.\"\n )}\n

    \n
    \n )\n}\n\nexport function FlagUnapproved(props) {\n if (!(isVisible(props.post) && props.post.is_unapproved)) {\n return null\n }\n\n return (\n
    \n remove_circle_outline\n

    \n {pgettext(\n \"post unapproved flag\",\n \"This post is unapproved. Only users with permission to approve posts and its author may see its contents.\"\n )}\n

    \n
    \n )\n}\n\nexport function FlagProtected(props) {\n if (!(isVisible(props.post) && props.post.is_protected)) {\n return null\n }\n\n return (\n
    \n lock_outline\n

    \n {pgettext(\n \"post protected flag\",\n \"This post is protected. Only moderators may change it.\"\n )}\n

    \n
    \n )\n}\n\nexport function isVisible(post) {\n return !post.is_hidden || post.acl.can_see_hidden\n}\n","import moment from \"moment\"\nimport * as thread from \"misago/reducers/thread\"\nimport * as post from \"misago/reducers/post\"\nimport ajax from \"misago/services/ajax\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport function approve(props) {\n store.dispatch(\n post.patch(props.post, {\n is_unapproved: false,\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-unapproved\", value: false }]\n\n const previousState = {\n is_unapproved: props.post.is_unapproved,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function protect(props) {\n store.dispatch(\n post.patch(props.post, {\n is_protected: true,\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-protected\", value: true }]\n\n const previousState = {\n is_protected: props.post.is_protected,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function unprotect(props) {\n store.dispatch(\n post.patch(props.post, {\n is_protected: false,\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-protected\", value: false }]\n\n const previousState = {\n is_protected: props.post.is_protected,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function hide(props) {\n store.dispatch(\n post.patch(props.post, {\n is_hidden: true,\n hidden_on: moment(),\n hidden_by_name: props.user.username,\n url: Object.assign(props.post.url, {\n hidden_by: props.user.url,\n }),\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-hidden\", value: true }]\n\n const previousState = {\n is_hidden: props.post.is_hidden,\n hidden_on: props.post.hidden_on,\n hidden_by_name: props.post.hidden_by_name,\n url: props.post.url,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function unhide(props) {\n store.dispatch(\n post.patch(props.post, {\n is_hidden: false,\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-hidden\", value: false }]\n\n const previousState = {\n is_hidden: props.post.is_hidden,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function like(props) {\n const lastLikes = props.post.last_likes || []\n const concatedLikes = [props.user].concat(lastLikes)\n const finalLikes =\n concatedLikes.length > 3 ? concatedLikes.slice(0, -1) : concatedLikes\n\n store.dispatch(\n post.patch(props.post, {\n is_liked: true,\n likes: props.post.likes + 1,\n last_likes: finalLikes,\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-liked\", value: true }]\n\n const previousState = {\n is_liked: props.post.is_liked,\n likes: props.post.likes,\n last_likes: props.post.last_likes,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function unlike(props) {\n store.dispatch(\n post.patch(props.post, {\n is_liked: false,\n likes: props.post.likes - 1,\n last_likes: props.post.last_likes.filter((user) => {\n return !user.id || user.id !== props.user.id\n }),\n })\n )\n\n const ops = [{ op: \"replace\", path: \"is-liked\", value: false }]\n\n const previousState = {\n is_liked: props.post.is_liked,\n likes: props.post.likes,\n last_likes: props.post.last_likes,\n }\n\n patch(props, ops, previousState)\n}\n\nexport function patch(props, ops, previousState) {\n ajax.patch(props.post.api.index, ops).then(\n (newState) => {\n store.dispatch(post.patch(props.post, newState))\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n store.dispatch(post.patch(props.post, previousState))\n }\n )\n}\n\nexport function remove(props) {\n let confirmed = window.confirm(\n pgettext(\n \"post delete\",\n \"Are you sure you want to delete this post? This action is not reversible!\"\n )\n )\n if (!confirmed) {\n return\n }\n\n store.dispatch(\n post.patch(props.post, {\n isDeleted: true,\n })\n )\n\n ajax.delete(props.post.api.index).then(\n () => {\n snackbar.success(pgettext(\"post delete\", \"Post has been deleted.\"))\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n } else {\n snackbar.apiError(rejection)\n }\n\n store.dispatch(\n post.patch(props.post, {\n isDeleted: false,\n })\n )\n }\n )\n}\n\nexport function markAsBestAnswer(props) {\n const { post, user } = props\n\n store.dispatch(\n thread.update({\n best_answer: post.id,\n best_answer_is_protected: post.is_protected,\n best_answer_marked_on: moment(),\n best_answer_marked_by: user.id,\n best_answer_marked_by_name: user.username,\n best_answer_marked_by_slug: user.slug,\n })\n )\n\n const ops = [\n { op: \"replace\", path: \"best-answer\", value: post.id },\n { op: \"add\", path: \"acl\", value: true },\n ]\n\n const previousState = {\n best_answer: props.thread.best_answer,\n best_answer_is_protected: props.thread.best_answer_is_protected,\n best_answer_marked_on: props.thread.best_answer_marked_on,\n best_answer_marked_by: props.thread.best_answer_marked_by,\n best_answer_marked_by_name: props.thread.best_answer_marked_by_name,\n best_answer_marked_by_slug: props.thread.best_answer_marked_by_slug,\n }\n\n patchThread(props, ops, previousState)\n}\n\nexport function unmarkBestAnswer(props) {\n const { post } = props\n\n store.dispatch(\n thread.update({\n best_answer: null,\n best_answer_is_protected: false,\n best_answer_marked_on: null,\n best_answer_marked_by: null,\n best_answer_marked_by_name: null,\n best_answer_marked_by_slug: null,\n })\n )\n\n const ops = [\n { op: \"remove\", path: \"best-answer\", value: post.id },\n { op: \"add\", path: \"acl\", value: true },\n ]\n\n const previousState = {\n best_answer: props.thread.best_answer,\n best_answer_is_protected: props.thread.best_answer_is_protected,\n best_answer_marked_on: props.thread.best_answer_marked_on,\n best_answer_marked_by: props.thread.best_answer_marked_by,\n best_answer_marked_by_name: props.thread.best_answer_marked_by_name,\n best_answer_marked_by_slug: props.thread.best_answer_marked_by_slug,\n }\n\n patchThread(props, ops, previousState)\n}\n\nexport function patchThread(props, ops, previousState) {\n ajax.patch(props.thread.api.index, ops).then(\n (newState) => {\n if (newState.best_answer_marked_on) {\n newState.best_answer_marked_on = moment(newState.best_answer_marked_on)\n }\n store.dispatch(thread.update(newState))\n },\n (rejection) => {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail[0])\n } else {\n snackbar.apiError(rejection)\n }\n\n store.dispatch(thread.update(previousState))\n }\n )\n}\n","import React from \"react\"\nimport moment from \"moment\"\nimport Avatar from \"misago/components/avatar\"\nimport Message from \"misago/components/modal-message\"\nimport Loader from \"misago/components/modal-loader\"\nimport ajax from \"misago/services/ajax\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isReady: false,\n\n error: null,\n likes: [],\n }\n }\n\n componentDidMount() {\n ajax.get(this.props.post.api.likes).then(\n (data) => {\n this.setState({\n isReady: true,\n likes: data.map(hydrateLike),\n })\n },\n (rejection) => {\n this.setState({\n isReady: true,\n error: rejection.detail,\n })\n }\n )\n }\n\n render() {\n if (this.state.error) {\n return (\n \n \n \n )\n } else if (this.state.isReady) {\n if (this.state.likes.length) {\n return (\n \n \n \n )\n }\n\n return (\n \n \n \n )\n }\n\n return (\n \n \n \n )\n }\n}\n\nexport function hydrateLike(data) {\n return Object.assign({}, data, {\n liked_on: moment(data.liked_on),\n })\n}\n\nexport function ModalDialog({ className, children, likes }) {\n let title = pgettext(\"post likes modal title\", \"Post Likes\")\n if (likes) {\n const likesCount = likes.length\n const message = npgettext(\n \"post likes modal\",\n \"%(likes)s like\",\n \"%(likes)s likes\",\n likesCount\n )\n\n title = interpolate(message, { likes: likesCount }, true)\n }\n\n return (\n
    \n
    \n
    \n \n ×\n \n

    {title}

    \n
    \n {children}\n
    \n
    \n )\n}\n\nexport function LikesList(props) {\n return (\n
    \n
      \n {props.likes.map((like) => {\n return \n })}\n
    \n
    \n )\n}\n\nexport function LikeDetails(props) {\n if (props.url) {\n const user = {\n id: props.liker_id,\n avatars: props.avatars,\n }\n\n return (\n
  • \n
    \n \n \n \n
    \n
    \n \n {props.username}\n {\" \"}\n \n
    \n
  • \n )\n }\n\n return (\n
  • \n
    \n \n \n \n
    \n
    \n {props.username} \n
    \n
  • \n )\n}\n\nexport function LikeDate(props) {\n return (\n \n {props.likedOn.fromNow()}\n \n )\n}\n","import React from \"react\"\nimport * as actions from \"./controls/actions\"\nimport LikesModal from \"misago/components/post-likes\"\nimport modal from \"misago/services/modal\"\nimport posting from \"misago/services/posting\"\n\nexport default function (props) {\n if (!isVisible(props.post)) return null\n\n return (\n
    \n \n \n \n \n \n \n \n \n
    \n )\n}\n\nexport function isVisible(post) {\n return (\n (!post.is_hidden || post.acl.can_see_hidden) &&\n (post.acl.can_reply ||\n post.acl.can_edit ||\n (post.acl.can_see_likes && (post.last_likes || []).length) ||\n post.acl.can_like)\n )\n}\n\nexport class MarkAsBestAnswer extends React.Component {\n onClick = () => {\n actions.markAsBestAnswer(this.props)\n }\n\n render() {\n const { post, thread } = this.props\n\n if (!thread.acl.can_mark_best_answer) return null\n if (!post.acl.can_mark_as_best_answer) return null\n if (thread.best_answer && !thread.acl.can_change_best_answer) return null\n\n return (\n \n check_box\n {pgettext(\"post footer btn\", \"Best answer\")}\n \n )\n }\n}\n\nexport class MarkAsBestAnswerCompact extends React.Component {\n onClick = () => {\n actions.markAsBestAnswer(this.props)\n }\n\n render() {\n const { post, thread } = this.props\n\n if (!thread.acl.can_mark_best_answer) return null\n if (!post.acl.can_mark_as_best_answer) return null\n if (thread.best_answer && !thread.acl.can_change_best_answer) return null\n\n return (\n \n check_box\n \n )\n }\n}\n\nexport class Like extends React.Component {\n onClick = () => {\n if (this.props.post.is_liked) {\n actions.unlike(this.props)\n } else {\n actions.like(this.props)\n }\n }\n\n render() {\n if (!this.props.post.acl.can_like) return null\n\n let className = \"btn btn-default btn-sm pull-left\"\n if (this.props.post.is_liked) {\n className = \"btn btn-success btn-sm pull-left\"\n }\n\n return (\n \n {this.props.post.is_liked\n ? pgettext(\"post footer btn\", \"Liked\")\n : pgettext(\"post footer btn\", \"Like\")}\n \n )\n }\n}\n\nexport class Likes extends React.Component {\n onClick = () => {\n modal.show()\n }\n\n render() {\n const hasLikes = (this.props.post.last_likes || []).length > 0\n if (!this.props.post.acl.can_see_likes || !hasLikes) return null\n\n if (this.props.post.acl.can_see_likes === 2) {\n return (\n \n {getLikesMessage(this.props.likes, this.props.lastLikes)}\n \n )\n }\n\n return (\n

    \n {getLikesMessage(this.props.likes, this.props.lastLikes)}\n

    \n )\n }\n}\n\nexport class LikesCompact extends Likes {\n render() {\n const hasLikes = (this.props.post.last_likes || []).length > 0\n if (!this.props.post.acl.can_see_likes || !hasLikes) return null\n\n if (this.props.post.acl.can_see_likes === 2) {\n return (\n \n favorite\n {this.props.likes}\n \n )\n }\n\n return (\n

    \n favorite\n {this.props.likes}\n

    \n )\n }\n}\n\nexport function getLikesMessage(likes, users) {\n const usernames = users.slice(0, 3).map((u) => u.username)\n\n if (usernames.length == 1) {\n return interpolate(\n pgettext(\"post likes\", \"%(user)s likes this.\"),\n {\n user: usernames[0],\n },\n true\n )\n }\n\n const hiddenLikes = likes - usernames.length\n\n const otherUsers = usernames.slice(0, -1).join(\", \")\n const lastUser = usernames.slice(-1)[0]\n\n const usernamesList = interpolate(\n pgettext(\"post likes\", \"%(users)s and %(last_user)s\"),\n {\n users: otherUsers,\n last_user: lastUser,\n },\n true\n )\n\n if (hiddenLikes === 0) {\n return interpolate(\n pgettext(\"post likes\", \"%(users)s like this.\"),\n {\n users: usernamesList,\n },\n true\n )\n }\n\n const message = npgettext(\n \"post likes\",\n \"%(users)s and %(likes)s other user like this.\",\n \"%(users)s and %(likes)s other users like this.\",\n hiddenLikes\n )\n\n return interpolate(\n message,\n {\n users: usernames.join(\", \"),\n likes: hiddenLikes,\n },\n true\n )\n}\n\nexport class Reply extends React.Component {\n onClick = () => {\n posting.open({\n mode: \"REPLY\",\n\n thread: this.props.thread,\n config: this.props.thread.api.editor,\n submit: this.props.thread.api.posts.index,\n })\n }\n\n render() {\n if (this.props.post.acl.can_reply) {\n return (\n \n {pgettext(\"post footer btn\", \"Reply\")}\n \n )\n } else {\n return null\n }\n }\n}\n\nexport class Quote extends React.Component {\n onClick = () => {\n posting.open({\n mode: \"QUOTE\",\n\n thread: this.props.thread,\n config: this.props.thread.api.editor,\n submit: this.props.thread.api.posts.index,\n\n context: {\n reply: this.props.post.id,\n },\n })\n }\n\n render() {\n if (this.props.post.acl.can_reply) {\n return (\n \n {pgettext(\"post footer btn\", \"Quote\")}\n \n )\n } else {\n return null\n }\n }\n}\n\nexport class Edit extends React.Component {\n onClick = () => {\n posting.open({\n mode: \"EDIT\",\n\n thread: this.props.thread,\n post: this.props.post,\n config: this.props.post.api.editor,\n submit: this.props.post.api.index,\n })\n }\n\n render() {\n if (this.props.post.acl.can_edit) {\n return (\n \n {pgettext(\"post footer btn\", \"Edit\")}\n \n )\n } else {\n return null\n }\n }\n}\n","import React from \"react\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport * as post from \"misago/reducers/post\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n url: \"\",\n\n validators: {\n url: [],\n },\n errors: {},\n }\n }\n\n clean() {\n if (!this.state.url.trim().length) {\n snackbar.error(\n pgettext(\n \"post move modal\",\n \"You have to enter link to the other thread.\"\n )\n )\n return false\n }\n\n return true\n }\n\n send() {\n return ajax.post(this.props.thread.api.posts.move, {\n new_thread: this.state.url,\n posts: [this.props.post.id],\n })\n }\n\n handleSuccess(success) {\n store.dispatch(\n post.patch(this.props.post, {\n isDeleted: true,\n })\n )\n\n modal.hide()\n\n snackbar.success(\n pgettext(\n \"post move modal\",\n \"Selected post was moved to the other thread.\"\n )\n )\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n snackbar.error(rejection.detail)\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n onUrlChange = (event) => {\n this.changeValue(\"url\", event.target.value)\n }\n\n render() {\n return (\n
    \n
    \n
    \n \n
    \n \n \n \n
    \n
    \n \n {pgettext(\"post move modal btn\", \"Move post\")}\n \n
    \n
    \n
    \n
    \n )\n }\n}\n\nexport function ModalHeader(props) {\n return (\n
    \n \n ×\n \n

    \n {pgettext(\"post move modal title\", \"Move post\")}\n

    \n
    \n )\n}\n","import React from \"react\"\n\nexport default function (props) {\n return (\n
    \n
      \n {props.diff.map((item, i) => {\n return \n })}\n
    \n
    \n )\n}\n\nexport function DiffItem(props) {\n if (props.item[0] === \"?\") return null\n\n return (\n
  • {cleanItem(props.item)}
  • \n )\n}\n\nexport function getItemClassName(item) {\n let className = \"diff-item\"\n if (item[0] === \"-\") {\n className += \" diff-item-sub\"\n } else if (item[0] === \"+\") {\n className += \" diff-item-add\"\n }\n return className\n}\n\nexport function cleanItem(item) {\n return item.substr(2)\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\n\nexport default class extends React.Component {\n onClick = () => {\n this.props.revertEdit(this.props.edit.id)\n }\n\n render() {\n if (!this.props.canRevert) return null\n\n return (\n
    \n \n {pgettext(\"post revert btn\", \"Revert\")}\n \n
    \n )\n }\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport escapeHtml from \"misago/utils/escape-html\"\n\nconst DATE_ABBR = '%(relative)s'\nconst USER_SPAN = '%(user)s'\nconst USER_URL = '%(user)s'\n\nexport default class extends React.Component {\n goLast = () => {\n this.props.goToEdit()\n }\n\n goForward = () => {\n this.props.goToEdit(this.props.edit.next)\n }\n\n goBack = () => {\n this.props.goToEdit(this.props.edit.previous)\n }\n\n revertEdit = () => {\n this.props.revertEdit(this.props.edit.id)\n }\n\n render() {\n return (\n
    \n
    \n
    \n
    \n
    \n \n
    \n
    \n \n
    \n
    \n \n
    \n
    \n
    \n
    \n
    \n \n
    \n
    \n )\n }\n}\n\nexport function GoBackBtn(props) {\n return (\n \n chevron_left\n \n )\n}\n\nexport function GoForwardBtn(props) {\n return (\n \n chevron_right\n \n )\n}\n\nexport function GoLastBtn(props) {\n return (\n \n last_page\n \n )\n}\n\nexport function RevertBtn(props) {\n if (!props.canRevert) return null\n\n return (\n
    \n \n {pgettext(\"post revert btn\", \"Revert\")}\n \n
    \n )\n}\n\nexport function Label(props) {\n let user = null\n if (props.edit.url.editor) {\n user = interpolate(\n USER_URL,\n {\n url: escapeHtml(props.edit.url.editor),\n user: escapeHtml(props.edit.editor_name),\n },\n true\n )\n } else {\n user = interpolate(\n USER_SPAN,\n {\n user: escapeHtml(props.edit.editor_name),\n },\n true\n )\n }\n\n const date = interpolate(\n DATE_ABBR,\n {\n absolute: escapeHtml(props.edit.edited_on.format(\"LLL\")),\n relative: escapeHtml(props.edit.edited_on.fromNow()),\n },\n true\n )\n\n const message = interpolate(\n escapeHtml(\n pgettext(\"post history modal\", \"By %(edited_by)s %(edited_on)s.\")\n ),\n {\n edited_by: user,\n edited_on: date,\n },\n true\n )\n\n return

    \n}\n","import moment from \"moment\"\n\nexport function hydrateEdit(json) {\n return Object.assign({}, json, {\n edited_on: moment(json.edited_on),\n })\n}\n","import React from \"react\"\nimport Diff from \"./diff\"\nimport Footer from \"./footer\"\nimport Toolbar from \"./toolbar\"\nimport { hydrateEdit } from \"./utils\"\nimport Message from \"misago/components/modal-message\"\nimport Loader from \"misago/components/modal-loader\"\nimport * as post from \"misago/reducers/post\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\n\nexport default class extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isReady: false,\n isBusy: true,\n\n canRevert: props.post.acl.can_edit,\n\n error: null,\n edit: null,\n }\n }\n\n componentDidMount() {\n this.goToEdit()\n }\n\n goToEdit = (edit = null) => {\n this.setState({\n isBusy: true,\n })\n\n let url = this.props.post.api.edits\n if (edit !== null) {\n url += \"?edit=\" + edit\n }\n\n ajax.get(url).then(\n (data) => {\n this.setState({\n isReady: true,\n isBusy: false,\n edit: hydrateEdit(data),\n })\n },\n (rejection) => {\n this.setState({\n isReady: true,\n isBusy: false,\n error: rejection.detail,\n })\n }\n )\n }\n\n revertEdit = (edit) => {\n if (this.state.isBusy) return\n\n const confirmation = window.confirm(\n pgettext(\n \"post revert\",\n \"Are you sure you with to revert this post to the state from before this edit?\"\n )\n )\n if (!confirmation) return\n\n this.setState({\n isBusy: true,\n })\n\n const url = this.props.post.api.edits + \"?edit=\" + edit\n ajax.post(url).then(\n (data) => {\n const hydratedPost = post.hydrate(data)\n store.dispatch(post.patch(data, hydratedPost))\n\n snackbar.success(\n pgettext(\"post revert\", \"Post has been reverted to previous state.\")\n )\n modal.hide()\n },\n (rejection) => {\n snackbar.apiError(rejection)\n\n this.setState({\n isBusy: false,\n })\n }\n )\n }\n\n render() {\n if (this.state.error) {\n return (\n \n \n \n )\n } else if (this.state.isReady) {\n return (\n \n \n \n \n \n )\n }\n\n return (\n \n \n \n )\n }\n}\n\nexport function ModalDialog(props) {\n return (\n

    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"post history modal title\", \"Post edits history\")}\n

    \n
    \n {props.children}\n
    \n
    \n )\n}\n","import React from \"react\"\nimport Button from \"misago/components/button\"\nimport Form from \"misago/components/form\"\nimport FormGroup from \"misago/components/form-group\"\nimport CategorySelect from \"misago/components/category-select\"\nimport ModalLoader from \"misago/components/modal-loader\"\nimport Select from \"misago/components/select\"\nimport * as post from \"misago/reducers/post\"\nimport ajax from \"misago/services/ajax\"\nimport modal from \"misago/services/modal\"\nimport snackbar from \"misago/services/snackbar\"\nimport store from \"misago/services/store\"\nimport * as validators from \"misago/utils/validators\"\n\nexport default function (props) {\n return \n}\n\nexport class PostingConfig extends React.Component {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoaded: false,\n isError: false,\n\n categories: [],\n }\n }\n\n componentDidMount() {\n ajax.get(misago.get(\"THREAD_EDITOR_API\")).then(\n (data) => {\n // hydrate categories, extract posting options\n const categories = data.map((item) => {\n return Object.assign(item, {\n disabled: item.post === false,\n label: item.name,\n value: item.id,\n post: item.post,\n })\n })\n\n this.setState({\n isLoaded: true,\n categories,\n })\n },\n (rejection) => {\n this.setState({\n isError: rejection.detail,\n })\n }\n )\n }\n\n render() {\n if (this.state.isError) {\n return \n } else if (this.state.isLoaded) {\n return (\n \n )\n } else {\n return \n }\n }\n}\n\nexport class ModerationForm extends Form {\n constructor(props) {\n super(props)\n\n this.state = {\n isLoading: false,\n\n title: \"\",\n category: null,\n categories: props.categories,\n weight: 0,\n is_hidden: 0,\n is_closed: false,\n\n validators: {\n title: [validators.required()],\n },\n\n errors: {},\n }\n\n this.isHiddenChoices = [\n {\n value: 0,\n icon: \"visibility\",\n label: pgettext(\"thread hidden switch choice\", \"No\"),\n },\n {\n value: 1,\n icon: \"visibility_off\",\n label: pgettext(\"thread hidden switch choice\", \"Yes\"),\n },\n ]\n\n this.isClosedChoices = [\n {\n value: false,\n icon: \"lock_outline\",\n label: pgettext(\"thread closed switch choice\", \"No\"),\n },\n {\n value: true,\n icon: \"lock\",\n label: pgettext(\"thread closed switch choice\", \"Yes\"),\n },\n ]\n\n this.acl = {}\n this.props.categories.forEach((category) => {\n if (category.post) {\n if (!this.state.category) {\n this.state.category = category.id\n }\n\n this.acl[category.id] = {\n can_pin_threads: category.post.pin,\n can_close_threads: category.post.close,\n can_hide_threads: category.post.hide,\n }\n }\n })\n }\n\n clean() {\n if (this.isValid()) {\n return true\n } else {\n snackbar.error(gettext(\"Form contains errors.\"))\n this.setState({\n errors: this.validate(),\n })\n return false\n }\n }\n\n send() {\n return ajax.post(this.props.thread.api.posts.split, {\n title: this.state.title,\n category: this.state.category,\n weight: this.state.weight,\n is_hidden: this.state.is_hidden,\n is_closed: this.state.is_closed,\n posts: [this.props.post.id],\n })\n }\n\n handleSuccess(apiResponse) {\n store.dispatch(\n post.patch(this.props.post, {\n isDeleted: true,\n })\n )\n\n modal.hide()\n\n snackbar.success(\n pgettext(\"post split modal\", \"Selected post was split into new thread.\")\n )\n }\n\n handleError(rejection) {\n if (rejection.status === 400) {\n this.setState({\n errors: Object.assign({}, this.state.errors, rejection),\n })\n snackbar.error(gettext(\"Form contains errors.\"))\n } else {\n snackbar.apiError(rejection)\n }\n }\n\n onCategoryChange = (ev) => {\n const categoryId = ev.target.value\n const newState = {\n category: categoryId,\n }\n\n if (this.acl[categoryId].can_pin_threads < newState.weight) {\n newState.weight = 0\n }\n\n if (!this.acl[categoryId].can_hide_threads) {\n newState.is_hidden = 0\n }\n\n if (!this.acl[categoryId].can_close_threads) {\n newState.is_closed = false\n }\n\n this.setState(newState)\n }\n\n getWeightChoices() {\n const choices = [\n {\n value: 0,\n icon: \"remove\",\n label: pgettext(\"thread weight choice\", \"Not pinned\"),\n },\n {\n value: 1,\n icon: \"bookmark_border\",\n label: pgettext(\"thread weight choice\", \"Pinned in category\"),\n },\n ]\n\n if (this.acl[this.state.category].can_pin_threads == 2) {\n choices.push({\n value: 2,\n icon: \"bookmark\",\n label: pgettext(\"thread weight choice\", \"Pinned globally\"),\n })\n }\n\n return choices\n }\n\n renderWeightField() {\n if (this.acl[this.state.category].can_pin_threads) {\n return (\n \n \n \n )\n } else {\n return null\n }\n }\n\n renderHiddenField() {\n if (this.acl[this.state.category].can_hide_threads) {\n return (\n \n \n \n )\n } else {\n return null\n }\n }\n\n renderClosedField() {\n if (this.acl[this.state.category].can_close_threads) {\n return (\n \n \n \n )\n } else {\n return null\n }\n }\n\n render() {\n return (\n \n
    \n
    \n \n \n \n
    \n\n \n \n \n
    \n\n {this.renderWeightField()}\n {this.renderHiddenField()}\n {this.renderClosedField()}\n
    \n
    \n \n
    \n \n \n )\n }\n}\n\nexport function Loader() {\n return (\n \n \n \n )\n}\n\nexport function Error(props) {\n return (\n \n
    \n info_outline\n
    \n
    \n

    \n {pgettext(\n \"post split modal\",\n \"You can't move this post at the moment.\"\n )}\n

    \n

    {props.message}

    \n
    \n
    \n )\n}\n\nexport function Modal(props) {\n return (\n
    \n
    \n
    \n \n ×\n \n

    \n {pgettext(\"posts split modal title\", \"Split post into new thread\")}\n

    \n
    \n {props.children}\n
    \n
    \n )\n}\n","import React from \"react\"\nimport modal from \"misago/services/modal\"\nimport posting from \"misago/services/posting\"\nimport * as moderation from \"./actions\"\nimport MoveModal from \"./move\"\nimport PostChangelog from \"misago/components/post-changelog\"\nimport SplitModal from \"./split\"\n\nexport default function (props) {\n return (\n
      \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    \n )\n}\n\nexport class Permalink extends React.Component {\n onClick = () => {\n let permaUrl = window.location.protocol + \"//\"\n permaUrl += window.location.host\n permaUrl += this.props.post.url.index\n\n prompt(pgettext(\"post permalink\", \"Permament link to this post:\"), permaUrl)\n }\n\n render() {\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Edit extends React.Component {\n onClick = () => {\n posting.open({\n mode: \"EDIT\",\n\n thread: this.props.thread,\n post: this.props.post,\n config: this.props.post.api.editor,\n submit: this.props.post.api.index,\n })\n }\n\n render() {\n if (!this.props.post.acl.can_edit) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class MarkAsBestAnswer extends React.Component {\n onClick = () => {\n moderation.markAsBestAnswer(this.props)\n }\n\n render() {\n const { post, thread } = this.props\n\n if (!thread.acl.can_mark_best_answer) return null\n if (!post.acl.can_mark_as_best_answer) return null\n if (post.id === thread.best_answer) return null\n if (thread.best_answer && !thread.acl.can_change_best_answer) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class UnmarkMarkBestAnswer extends React.Component {\n onClick = () => {\n moderation.unmarkBestAnswer(this.props)\n }\n\n render() {\n const { post, thread } = this.props\n\n if (post.id !== thread.best_answer) return null\n if (!thread.acl.can_unmark_best_answer) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class PostEdits extends React.Component {\n onClick = () => {\n modal.show()\n }\n\n render() {\n const isHidden =\n this.props.post.is_hidden && !this.props.post.acl.can_see_hidden\n const isUnedited = this.props.post.edits === 0\n if (isHidden || isUnedited) return null\n\n const message = npgettext(\n \"post edits\",\n \"This post was edited %(edits)s time.\",\n \"This post was edited %(edits)s times.\",\n this.props.post.edits\n )\n\n const title = interpolate(\n message,\n {\n edits: this.props.post.edits,\n },\n true\n )\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Approve extends React.Component {\n onClick = () => {\n moderation.approve(this.props)\n }\n\n render() {\n if (!this.props.post.acl.can_approve) return null\n if (!this.props.post.is_unapproved) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Move extends React.Component {\n onClick = () => {\n modal.show()\n }\n\n render() {\n if (!this.props.post.acl.can_move) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Split extends React.Component {\n onClick = () => {\n modal.show()\n }\n\n render() {\n if (!this.props.post.acl.can_move) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Protect extends React.Component {\n onClick = () => {\n moderation.protect(this.props)\n }\n\n render() {\n if (!this.props.post.acl.can_protect) return null\n if (this.props.post.is_protected) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Unprotect extends React.Component {\n onClick = () => {\n moderation.unprotect(this.props)\n }\n\n render() {\n if (!this.props.post.acl.can_protect) return null\n if (!this.props.post.is_protected) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Hide extends React.Component {\n onClick = () => {\n moderation.hide(this.props)\n }\n\n render() {\n const { post, thread } = this.props\n\n if (post.id === thread.best_answer) return null\n if (!post.acl.can_hide) return null\n if (post.is_hidden) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Unhide extends React.Component {\n onClick = () => {\n moderation.unhide(this.props)\n }\n\n render() {\n if (!this.props.post.acl.can_unhide) return null\n if (!this.props.post.is_hidden) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n\nexport class Delete extends React.Component {\n onClick = () => {\n moderation.remove(this.props)\n }\n\n render() {\n const { post, thread } = this.props\n\n if (post.id === thread.best_answer) return null\n if (!post.acl.can_delete) return null\n\n return (\n
  • \n \n
  • \n )\n }\n}\n","import React from \"react\"\nimport Dropdown from \"./dropdown\"\n\nexport default function (props) {\n return (\n
    \n \n expand_more\n \n \n
    \n )\n}\n","import React from \"react\"\nimport * as posts from \"misago/reducers/posts\"\nimport store from \"misago/services/store\"\n\nexport default class extends React.Component {\n onClick = () => {\n if (this.props.post.isSelected) {\n store.dispatch(posts.deselect(this.props.post))\n } else {\n store.dispatch(posts.select(this.props.post))\n }\n }\n\n render() {\n if (\n !(this.props.thread.acl.can_merge_posts || isVisible(this.props.post.acl))\n ) {\n return null\n }\n\n return (\n
    \n \n \n {this.props.post.isSelected\n ? \"check_box\"\n : \"check_box_outline_blank\"}\n \n \n
    \n )\n }\n}\n\nexport function isVisible(acl) {\n return (\n acl.can_approve ||\n acl.can_hide ||\n acl.can_protect ||\n acl.can_unhide ||\n acl.can_delete ||\n acl.can_move\n )\n}\n","import React from \"react\"\nimport Controls from \"./controls\"\nimport Select from \"./select\"\nimport {\n StatusIcon,\n getStatusClassName,\n getStatusDescription,\n} from \"misago/components/user-status\"\nimport PostChangelog from \"misago/components/post-changelog\"\nimport modal from \"misago/services/modal\"\n\nexport default function (props) {\n return (\n
    \n \n \n \n \n \n \n \n \n \n
    \n
    \n \n \n \n
    \n
    \n {post.poster_name}\n\n \n {pgettext(\"post removed poster username\", \"Removed user\")}\n \n
    \n
    \n
    \n )\n}\n","export default function ({ title, rank }) {\n return rank.is_tab || !!title || !!rank.title\n}\n","import React from \"react\"\nimport hasVisibleTitle from \"./has-visible-title\"\n\nexport default function ({ poster }) {\n const message = npgettext(\n \"poster stats\",\n \"%(posts)s post\",\n \"%(posts)s posts\",\n poster.posts\n )\n\n let className = \"user-postcount\"\n if (hasVisibleTitle(poster)) {\n className += \" hidden-xs hidden-sm\"\n }\n\n return (\n \n {interpolate(\n message,\n {\n posts: poster.posts,\n },\n true\n )}\n \n )\n}\n","import React from \"react\"\nimport UserStatus, { StatusLabel } from \"misago/components/user-status\"\nimport hasVisibleTitle from \"./has-visible-title\"\n\nexport default function ({ poster }) {\n let className = \"hidden-xs\"\n if (hasVisibleTitle(poster)) {\n className += \" hidden-sm\"\n }\n\n return (\n \n \n \n \n \n )\n}\n","import React from \"react\"\n\nexport default function ({ rank, title }) {\n let userTitle = title || rank.title\n if (!userTitle && rank.is_tab) {\n userTitle = rank.name\n }\n\n if (!userTitle) return null\n\n let className = \"user-title\"\n if (rank.css_class) {\n className += \" user-title-\" + rank.css_class\n }\n\n if (rank.is_tab) {\n return (\n \n )\n }\n\n return
    {userTitle}
    \n}\n","import React from \"react\"\nimport Avatar from \"misago/components/avatar\"\nimport Controls from \"misago/components/posts-list/post/controls\"\nimport Select from \"misago/components/posts-list/post/select\"\nimport UserStatus, { StatusIcon } from \"misago/components/user-status\"\nimport UserPostcount from \"./user-postcount\"\nimport UserStatusLabel from \"./user-status\"\nimport UserTitle from \"./user-title\"\n\nexport default function ({ post, thread }) {\n const { poster } = post\n\n return (\n
    \n