* Updating translations.
* Adding registration applications.
* Updating translations.
* Adding verify email route.
* Fix missing signup question bug.
* Updating translations.
* A few fixes from comments on lemmy PR.
* v0.15.0-rc.4
* Some suggestions from PR.
* v0.15.0-rc.5
* Adding optional auth to modlog fetches.
* v0.15.0-rc.6
* Hide deny / approve buttons
-Subproject commit 0412b6b349e5e8d6ac3ed88801187833e95c72c9
+Subproject commit 1e0bb9920cda13bb128c87e85125b98ab8f319b6
{
"name": "lemmy-ui",
"description": "An isomorphic UI for lemmy",
- "version": "0.14.5",
+ "version": "0.15.0-rc.6",
"author": "Dessalines <tyhou13@gmx.com>",
"license": "AGPL-3.0",
"scripts": {
"husky": "^7.0.4",
"import-sort-style-module": "^6.0.0",
"iso-639-1": "^2.1.10",
- "lemmy-js-client": "0.14.0-rc.1",
+ "lemmy-js-client": "0.15.0-rc.6",
"lint-staged": "^12.1.2",
"mini-css-extract-plugin": "^2.4.5",
"node-fetch": "^2.6.1",
if (routeData[0] && routeData[0].error) {
let errCode = routeData[0].error;
console.error(errCode);
- return res.redirect(`/404?err=${errCode}`);
+ if (errCode == "instance_is_private") {
+ return res.redirect(`/signup`);
+ } else {
+ return res.redirect(`/404?err=${errCode}`);
+ }
}
let isoData: IsoData = {
GetSiteResponse,
GetUnreadCount,
GetUnreadCountResponse,
+ GetUnreadRegistrationApplicationCount,
+ GetUnreadRegistrationApplicationCountResponse,
PrivateMessageResponse,
UserOperation,
} from "lemmy-js-client";
expanded: boolean;
unreadInboxCount: number;
unreadReportCount: number;
+ unreadApplicationCount: number;
searchParam: string;
toggleSearch: boolean;
showDropdown: boolean;
private userSub: Subscription;
private unreadInboxCountSub: Subscription;
private unreadReportCountSub: Subscription;
+ private unreadApplicationCountSub: Subscription;
private searchTextField: RefObject<HTMLInputElement>;
emptyState: NavbarState = {
isLoggedIn: !!this.props.site_res.my_user,
unreadInboxCount: 0,
unreadReportCount: 0,
+ unreadApplicationCount: 0,
expanded: false,
searchParam: "",
toggleSearch: false,
UserService.Instance.unreadReportCountSub.subscribe(res => {
this.setState({ unreadReportCount: res });
});
+ // Subscribe to unread application count
+ this.unreadApplicationCountSub =
+ UserService.Instance.unreadApplicationCountSub.subscribe(res => {
+ this.setState({ unreadApplicationCount: res });
+ });
}
}
this.userSub.unsubscribe();
this.unreadInboxCountSub.unsubscribe();
this.unreadReportCountSub.unsubscribe();
+ this.unreadApplicationCountSub.unsubscribe();
}
updateUrl() {
</li>
</ul>
)}
+ {UserService.Instance.myUserInfo?.local_user_view.person
+ .admin && (
+ <ul class="navbar-nav ml-1">
+ <li className="nav-item">
+ <NavLink
+ to="/registration_applications"
+ className="p-1 navbar-toggler nav-link border-0"
+ onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
+ title={i18n.t("unread_registration_applications", {
+ count: this.state.unreadApplicationCount,
+ formattedCount: numToSI(
+ this.state.unreadApplicationCount
+ ),
+ })}
+ >
+ <Icon icon="clipboard" />
+ {this.state.unreadApplicationCount > 0 && (
+ <span class="mx-1 badge badge-light">
+ {numToSI(this.state.unreadApplicationCount)}
+ </span>
+ )}
+ </NavLink>
+ </li>
+ </ul>
+ )}
</>
)}
<button
</li>
</ul>
)}
+ {UserService.Instance.myUserInfo?.local_user_view.person
+ .admin && (
+ <ul class="navbar-nav my-2">
+ <li className="nav-item">
+ <NavLink
+ to="/registration_applications"
+ className="nav-link"
+ onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
+ title={i18n.t("unread_registration_applications", {
+ count: this.state.unreadApplicationCount,
+ formattedCount: numToSI(
+ this.state.unreadApplicationCount
+ ),
+ })}
+ >
+ <Icon icon="clipboard" />
+ {this.state.unreadApplicationCount > 0 && (
+ <span class="mx-1 badge badge-light">
+ {numToSI(this.state.unreadApplicationCount)}
+ </span>
+ )}
+ </NavLink>
+ </li>
+ </ul>
+ )}
<ul class="navbar-nav">
<li class="nav-item dropdown">
<button
this.state.unreadReportCount = data.post_reports + data.comment_reports;
this.setState(this.state);
this.sendReportUnread();
+ } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
+ let data =
+ wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg).data;
+ this.state.unreadApplicationCount = data.registration_applications;
+ this.setState(this.state);
+ this.sendApplicationUnread();
} else if (op == UserOperation.GetSite) {
// This is only called on a successful login
let data = wsJsonToRes<GetSiteResponse>(msg).data;
let unreadForm: GetUnreadCount = {
auth: authField(),
};
-
WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
console.log("Fetching reports...");
let reportCountForm: GetReportCount = {
auth: authField(),
};
-
WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
+
+ if (UserService.Instance.myUserInfo?.local_user_view.person.admin) {
+ console.log("Fetching applications...");
+
+ let applicationCountForm: GetUnreadRegistrationApplicationCount = {
+ auth: authField(),
+ };
+ WebSocketService.Instance.send(
+ wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
+ );
+ }
}
get currentLocation() {
);
}
+ sendApplicationUnread() {
+ UserService.Instance.unreadApplicationCountSub.next(
+ this.state.unreadApplicationCount
+ );
+ }
+
get canAdmin(): boolean {
return (
UserService.Instance.myUserInfo &&
render() {
let r = this.props.report;
let comment = r.comment;
+ let tippyContent = i18n.t(
+ r.comment_report.resolved ? "unresolve_report" : "resolve_report"
+ );
// Set the original post data ( a troll could change it )
comment.content = r.comment_report.original_comment_text;
<button
className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleResolveReport)}
- data-tippy-content={
- r.comment_report.resolved ? "unresolve_report" : "resolve_report"
- }
- aria-label={
- r.comment_report.resolved ? "unresolve_report" : "resolve_report"
- }
+ data-tippy-content={tippyContent}
+ aria-label={tippyContent}
>
<Icon
icon="check"
import { Icon, Spinner } from "./icon";
interface MarkdownTextAreaProps {
- initialContent: string;
+ initialContent?: string;
finished?: boolean;
buttonTitle?: string;
replyType?: boolean;
--- /dev/null
+import { Component, linkEvent } from "inferno";
+import { T } from "inferno-i18next-dess";
+import {
+ ApproveRegistrationApplication,
+ RegistrationApplicationView,
+} from "lemmy-js-client";
+import { i18n } from "../../i18next";
+import { WebSocketService } from "../../services";
+import { authField, mdToHtml, wsClient } from "../../utils";
+import { PersonListing } from "../person/person-listing";
+import { MarkdownTextArea } from "./markdown-textarea";
+import { MomentTime } from "./moment-time";
+
+interface RegistrationApplicationProps {
+ application: RegistrationApplicationView;
+}
+
+interface RegistrationApplicationState {
+ denyReason?: string;
+ denyExpanded: boolean;
+}
+
+export class RegistrationApplication extends Component<
+ RegistrationApplicationProps,
+ RegistrationApplicationState
+> {
+ private emptyState: RegistrationApplicationState = {
+ denyReason: this.props.application.registration_application.deny_reason,
+ denyExpanded: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handleDenyReasonChange = this.handleDenyReasonChange.bind(this);
+ }
+
+ render() {
+ let a = this.props.application;
+ let ra = this.props.application.registration_application;
+ let accepted = a.creator_local_user.accepted_application;
+
+ return (
+ <div>
+ <div>
+ {i18n.t("applicant")}: <PersonListing person={a.creator} />
+ </div>
+ <div>
+ {i18n.t("created")}: <MomentTime showAgo data={ra} />
+ </div>
+ <div>{i18n.t("answer")}:</div>
+ <div className="md-div" dangerouslySetInnerHTML={mdToHtml(ra.answer)} />
+
+ {a.admin && (
+ <div>
+ {accepted ? (
+ <T i18nKey="approved_by">
+ #
+ <PersonListing person={a.admin} />
+ </T>
+ ) : (
+ <div>
+ <T i18nKey="denied_by">
+ #
+ <PersonListing person={a.admin} />
+ </T>
+ <div>
+ {i18n.t("deny_reason")}:{" "}
+ <div
+ className="md-div d-inline-flex"
+ dangerouslySetInnerHTML={mdToHtml(ra.deny_reason || "")}
+ />
+ </div>
+ </div>
+ )}
+ </div>
+ )}
+
+ {this.state.denyExpanded && (
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">
+ {i18n.t("deny_reason")}
+ </label>
+ <div class="col-sm-10">
+ <MarkdownTextArea
+ initialContent={this.state.denyReason}
+ onContentChange={this.handleDenyReasonChange}
+ hideNavigationWarnings
+ />
+ </div>
+ </div>
+ )}
+ {(!ra.admin_id || (ra.admin_id && !accepted)) && (
+ <button
+ className="btn btn-secondary mr-2 my-2"
+ onClick={linkEvent(this, this.handleApprove)}
+ aria-label={i18n.t("approve")}
+ >
+ {i18n.t("approve")}
+ </button>
+ )}
+ {(!ra.admin_id || (ra.admin_id && accepted)) && (
+ <button
+ className="btn btn-secondary mr-2"
+ onClick={linkEvent(this, this.handleDeny)}
+ aria-label={i18n.t("deny")}
+ >
+ {i18n.t("deny")}
+ </button>
+ )}
+ </div>
+ );
+ }
+
+ handleApprove(i: RegistrationApplication) {
+ i.setState({ denyExpanded: false });
+ let form: ApproveRegistrationApplication = {
+ id: i.props.application.registration_application.id,
+ deny_reason: "",
+ approve: true,
+ auth: authField(),
+ };
+ WebSocketService.Instance.send(
+ wsClient.approveRegistrationApplication(form)
+ );
+ }
+
+ handleDeny(i: RegistrationApplication) {
+ if (i.state.denyExpanded) {
+ i.setState({ denyExpanded: false });
+ let form: ApproveRegistrationApplication = {
+ id: i.props.application.registration_application.id,
+ approve: false,
+ deny_reason: i.state.denyReason,
+ auth: authField(),
+ };
+ WebSocketService.Instance.send(
+ wsClient.approveRegistrationApplication(form)
+ );
+ } else {
+ i.setState({ denyExpanded: true });
+ }
+ }
+
+ handleDenyReasonChange(val: string) {
+ this.state.denyReason = val;
+ this.setState(this.state);
+ }
+}
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
+ <symbol id="icon-clipboard" viewBox="0 0 24 24">
+ <path d="M7 5c0 0.552 0.225 1.053 0.586 1.414s0.862 0.586 1.414 0.586h6c0.552 0 1.053-0.225 1.414-0.586s0.586-0.862 0.586-1.414h1c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v14c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-12c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM9 1c-0.552 0-1.053 0.225-1.414 0.586s-0.586 0.862-0.586 1.414h-1c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h12c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-14c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879h-1c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586zM9 3h6v2h-6z"></path>
+ </symbol>
<symbol id="icon-shield" viewBox="0 0 24 24">
<path d="M12 20.862c-1.184-0.672-4.42-2.695-6.050-5.549-0.079-0.138-0.153-0.276-0.223-0.417-0.456-0.911-0.727-1.878-0.727-2.896v-6.307l7-2.625 7 2.625v6.307c0 1.018-0.271 1.985-0.726 2.897-0.070 0.14-0.145 0.279-0.223 0.417-1.631 2.854-4.867 4.876-6.050 5.549zM12.447 22.894c0 0 4.989-2.475 7.34-6.589 0.096-0.168 0.188-0.34 0.276-0.515 0.568-1.135 0.937-2.408 0.937-3.79v-7c0-0.426-0.267-0.79-0.649-0.936l-8-3c-0.236-0.089-0.485-0.082-0.702 0l-8 3c-0.399 0.149-0.646 0.527-0.649 0.936v7c0 1.382 0.369 2.655 0.938 3.791 0.087 0.175 0.179 0.346 0.276 0.515 2.351 4.114 7.34 6.589 7.34 6.589 0.292 0.146 0.62 0.136 0.894 0z"></path>
</symbol>
sort: SortType.Hot,
limit: 6,
};
+ setOptionalAuth(trendingCommunitiesForm, req.auth);
promises.push(req.client.listCommunities(trendingCommunitiesForm));
return promises;
if (msg.error) {
toast(i18n.t(msg.error), "danger");
this.state = this.emptyState;
- // Refetch another captcha
- WebSocketService.Instance.send(wsClient.getCaptcha());
this.setState(this.state);
return;
} else {
authField,
isBrowser,
joinLemmyUrl,
+ mdToHtml,
setIsoData,
toast,
validEmail,
} from "../../utils";
import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon";
+import { MarkdownTextArea } from "../common/markdown-textarea";
const passwordStrengthOptions: Options<string> = [
{
captcha_uuid: undefined,
captcha_answer: undefined,
honeypot: undefined,
+ answer: undefined,
},
registerLoading: false,
captcha: undefined,
super(props, context);
this.state = this.emptyState;
+ this.handleAnswerChange = this.handleAnswerChange.bind(this);
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
}
get documentTitle(): string {
- return `${i18n.t("login")} - ${this.state.site_view.site.name}`;
+ return `${this.titleName} - ${this.state.site_view.site.name}`;
+ }
+
+ get titleName(): string {
+ return `${i18n.t(
+ this.state.site_view.site.private_instance ? "apply_to_join" : "sign_up"
+ )}`;
}
get isLemmyMl(): boolean {
registerForm() {
return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
- <h5>{i18n.t("sign_up")}</h5>
+ <h5>{this.titleName}</h5>
<div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-username">
type="email"
id="register-email"
class="form-control"
- placeholder={i18n.t("optional")}
+ placeholder={
+ this.state.site_view.site.require_email_verification
+ ? i18n.t("required")
+ : i18n.t("optional")
+ }
value={this.state.registerForm.email}
autoComplete="email"
onInput={linkEvent(this, this.handleRegisterEmailChange)}
+ required={this.state.site_view.site.require_email_verification}
minLength={3}
/>
- {!validEmail(this.state.registerForm.email) && (
- <div class="mt-2 mb-0 alert alert-light" role="alert">
- <Icon icon="alert-triangle" classes="icon-inline mr-2" />
- {i18n.t("no_password_reset")}
- </div>
- )}
+ {!this.state.site_view.site.require_email_verification &&
+ !validEmail(this.state.registerForm.email) && (
+ <div class="mt-2 mb-0 alert alert-light" role="alert">
+ <Icon icon="alert-triangle" classes="icon-inline mr-2" />
+ {i18n.t("no_password_reset")}
+ </div>
+ )}
</div>
</div>
</div>
</div>
+ {this.state.site_view.site.require_application && (
+ <>
+ <div class="form-group row">
+ <div class="offset-sm-2 col-sm-10">
+ <div class="mt-2 alert alert-light" role="alert">
+ <Icon icon="alert-triangle" classes="icon-inline mr-2" />
+ {i18n.t("fill_out_application")}
+ </div>
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(
+ this.state.site_view.site.application_question || ""
+ )}
+ />
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label
+ class="col-sm-2 col-form-label"
+ htmlFor="application_answer"
+ >
+ {i18n.t("answer")}
+ </label>
+ <div class="col-sm-10">
+ <MarkdownTextArea
+ onContentChange={this.handleAnswerChange}
+ hideNavigationWarnings
+ />
+ </div>
+ </div>
+ </>
+ )}
+
{this.state.captcha && (
<div class="form-group row">
<label class="col-sm-2" htmlFor="register-captcha">
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary">
- {this.state.registerLoading ? <Spinner /> : i18n.t("sign_up")}
+ {this.state.registerLoading ? <Spinner /> : this.titleName}
</button>
</div>
</div>
i.setState(i.state);
}
+ handleAnswerChange(val: string) {
+ this.state.registerForm.answer = val;
+ this.setState(this.state);
+ }
+
handleHoneyPotChange(i: Signup, event: any) {
i.state.registerForm.honeypot = event.target.value;
i.setState(i.state);
let data = wsJsonToRes<LoginResponse>(msg).data;
this.state = this.emptyState;
this.setState(this.state);
- UserService.Instance.login(data);
- WebSocketService.Instance.send(
- wsClient.userJoin({
- auth: authField(),
- })
- );
- this.props.history.push("/communities");
+ // Only log them in if a jwt was set
+ if (data.jwt) {
+ UserService.Instance.login(data);
+ WebSocketService.Instance.send(
+ wsClient.userJoin({
+ auth: authField(),
+ })
+ );
+ this.props.history.push("/communities");
+ } else {
+ if (data.verify_email_sent) {
+ toast(i18n.t("verify_email_sent"));
+ }
+ if (data.registration_created) {
+ toast(i18n.t("registration_application_sent"));
+ }
+ this.props.history.push("/");
+ }
} else if (op == UserOperation.GetCaptcha) {
let data = wsJsonToRes<GetCaptchaResponse>(msg).data;
if (data.ok) {
import { CreateSite, EditSite, Site } from "lemmy-js-client";
import { i18n } from "../../i18next";
import { WebSocketService } from "../../services";
-import {
- authField,
- capitalizeFirstLetter,
- randomStr,
- wsClient,
-} from "../../utils";
+import { authField, capitalizeFirstLetter, wsClient } from "../../utils";
import { Spinner } from "../common/icon";
import { ImageUploadForm } from "../common/image-upload-form";
import { MarkdownTextArea } from "../common/markdown-textarea";
}
export class SiteForm extends Component<SiteFormProps, SiteFormState> {
- private id = `site-form-${randomStr()}`;
private emptyState: SiteFormState = {
siteForm: {
enable_downvotes: true,
name: null,
icon: null,
banner: null,
+ require_email_verification: null,
+ require_application: null,
+ application_question: null,
+ private_instance: null,
auth: authField(),
},
loading: false,
this.state = this.emptyState;
this.handleSiteSidebarChange = this.handleSiteSidebarChange.bind(this);
+ this.handleSiteApplicationQuestionChange =
+ this.handleSiteApplicationQuestionChange.bind(this);
this.handleIconUpload = this.handleIconUpload.bind(this);
this.handleIconRemove = this.handleIconRemove.bind(this);
this.handleBannerRemove = this.handleBannerRemove.bind(this);
if (this.props.site) {
+ let site = this.props.site;
this.state.siteForm = {
- name: this.props.site.name,
- sidebar: this.props.site.sidebar,
- description: this.props.site.description,
- enable_downvotes: this.props.site.enable_downvotes,
- open_registration: this.props.site.open_registration,
- enable_nsfw: this.props.site.enable_nsfw,
- community_creation_admin_only:
- this.props.site.community_creation_admin_only,
- icon: this.props.site.icon,
- banner: this.props.site.banner,
+ name: site.name,
+ sidebar: site.sidebar,
+ description: site.description,
+ enable_downvotes: site.enable_downvotes,
+ open_registration: site.open_registration,
+ enable_nsfw: site.enable_nsfw,
+ community_creation_admin_only: site.community_creation_admin_only,
+ icon: site.icon,
+ banner: site.banner,
+ require_email_verification: site.require_email_verification,
+ require_application: site.require_application,
+ application_question: site.application_question,
+ private_instance: site.private_instance,
auth: authField(),
};
}
!this.props.site &&
(this.state.siteForm.name ||
this.state.siteForm.sidebar ||
+ this.state.siteForm.application_question ||
this.state.siteForm.description)
) {
window.onbeforeunload = () => true;
!this.props.site &&
(this.state.siteForm.name ||
this.state.siteForm.sidebar ||
+ this.state.siteForm.application_question ||
this.state.siteForm.description)
}
message={i18n.t("block_leaving")}
</div>
</div>
<div class="form-group row">
- <label class="col-12 col-form-label" htmlFor={this.id}>
- {i18n.t("sidebar")}
- </label>
+ <label class="col-12 col-form-label">{i18n.t("sidebar")}</label>
<div class="col-12">
<MarkdownTextArea
initialContent={this.state.siteForm.sidebar}
/>
</div>
</div>
+ {this.state.siteForm.require_application && (
+ <div class="form-group row">
+ <label class="col-12 col-form-label">
+ {i18n.t("application_questionnaire")}
+ </label>
+ <div class="col-12">
+ <MarkdownTextArea
+ initialContent={this.state.siteForm.application_question}
+ onContentChange={this.handleSiteApplicationQuestionChange}
+ hideNavigationWarnings
+ />
+ </div>
+ </div>
+ )}
<div class="form-group row">
<div class="col-12">
<div class="form-check">
</div>
</div>
</div>
+ <div class="form-group row">
+ <div class="col-12">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="create-site-require-email-verification"
+ type="checkbox"
+ checked={this.state.siteForm.require_email_verification}
+ onChange={linkEvent(
+ this,
+ this.handleSiteRequireEmailVerification
+ )}
+ />
+ <label
+ class="form-check-label"
+ htmlFor="create-site-require-email-verification"
+ >
+ {i18n.t("require_email_verification")}
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-12">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="create-site-require-application"
+ type="checkbox"
+ checked={this.state.siteForm.require_application}
+ onChange={linkEvent(this, this.handleSiteRequireApplication)}
+ />
+ <label
+ class="form-check-label"
+ htmlFor="create-site-require-application"
+ >
+ {i18n.t("require_registration_application")}
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-12">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="create-site-private-instance"
+ type="checkbox"
+ checked={this.state.siteForm.private_instance}
+ onChange={linkEvent(this, this.handleSitePrivateInstance)}
+ />
+ <label
+ class="form-check-label"
+ htmlFor="create-site-private-instance"
+ >
+ {i18n.t("private_instance")}
+ </label>
+ </div>
+ </div>
+ </div>
<div class="form-group row">
<div class="col-12">
<button
this.setState(this.state);
}
+ handleSiteApplicationQuestionChange(val: string) {
+ this.state.siteForm.application_question = val;
+ this.setState(this.state);
+ }
+
handleSiteDescChange(i: SiteForm, event: any) {
i.state.siteForm.description = event.target.value;
i.setState(i.state);
i.setState(i.state);
}
+ handleSiteRequireApplication(i: SiteForm, event: any) {
+ i.state.siteForm.require_application = event.target.checked;
+ i.setState(i.state);
+ }
+
+ handleSiteRequireEmailVerification(i: SiteForm, event: any) {
+ i.state.siteForm.require_email_verification = event.target.checked;
+ i.setState(i.state);
+ }
+
+ handleSitePrivateInstance(i: SiteForm, event: any) {
+ i.state.siteForm.private_instance = event.target.checked;
+ i.setState(i.state);
+ }
+
handleCancel(i: SiteForm) {
i.props.onCancel();
}
import { InitialFetchRequest } from "../interfaces";
import { UserService, WebSocketService } from "../services";
import {
+ authField,
fetchLimit,
isBrowser,
setIsoData,
+ setOptionalAuth,
toast,
wsClient,
wsJsonToRes,
community_id: this.state.communityId,
page: this.state.page,
limit: fetchLimit,
+ auth: authField(false),
};
WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
if (communityId) {
modlogForm.community_id = Number(communityId);
}
+ setOptionalAuth(modlogForm, req.auth);
promises.push(req.client.getModlog(modlogForm));
let communityForm: GetCommunity = {
id: Number(communityId),
};
+ setOptionalAuth(communityForm, req.auth);
promises.push(req.client.getCommunity(communityForm));
}
return promises;
--- /dev/null
+import { Component, linkEvent } from "inferno";
+import {
+ ListRegistrationApplications,
+ ListRegistrationApplicationsResponse,
+ RegistrationApplicationResponse,
+ RegistrationApplicationView,
+ SiteView,
+ UserOperation,
+} from "lemmy-js-client";
+import { Subscription } from "rxjs";
+import { i18n } from "../../i18next";
+import { InitialFetchRequest } from "../../interfaces";
+import { UserService, WebSocketService } from "../../services";
+import {
+ authField,
+ fetchLimit,
+ isBrowser,
+ setIsoData,
+ setupTippy,
+ toast,
+ updateRegistrationApplicationRes,
+ wsClient,
+ wsJsonToRes,
+ wsSubscribe,
+ wsUserOp,
+} from "../../utils";
+import { HtmlTags } from "../common/html-tags";
+import { Spinner } from "../common/icon";
+import { Paginator } from "../common/paginator";
+import { RegistrationApplication } from "../common/registration-application";
+
+enum UnreadOrAll {
+ Unread,
+ All,
+}
+
+interface RegistrationApplicationsState {
+ applications: RegistrationApplicationView[];
+ page: number;
+ site_view: SiteView;
+ unreadOrAll: UnreadOrAll;
+ loading: boolean;
+}
+
+export class RegistrationApplications extends Component<
+ any,
+ RegistrationApplicationsState
+> {
+ private isoData = setIsoData(this.context);
+ private subscription: Subscription;
+ private emptyState: RegistrationApplicationsState = {
+ unreadOrAll: UnreadOrAll.Unread,
+ applications: [],
+ page: 1,
+ site_view: this.isoData.site_res.site_view,
+ loading: true,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handlePageChange = this.handlePageChange.bind(this);
+
+ if (!UserService.Instance.myUserInfo && isBrowser()) {
+ toast(i18n.t("not_logged_in"), "danger");
+ this.context.router.history.push(`/login`);
+ }
+
+ this.parseMessage = this.parseMessage.bind(this);
+ this.subscription = wsSubscribe(this.parseMessage);
+
+ // Only fetch the data if coming from another route
+ if (this.isoData.path == this.context.router.route.match.url) {
+ this.state.applications =
+ this.isoData.routeData[0].registration_applications || []; // TODO test
+ this.state.loading = false;
+ } else {
+ this.refetch();
+ }
+ }
+
+ componentDidMount() {
+ setupTippy();
+ }
+
+ componentWillUnmount() {
+ if (isBrowser()) {
+ this.subscription.unsubscribe();
+ }
+ }
+
+ get documentTitle(): string {
+ return `@${
+ UserService.Instance.myUserInfo.local_user_view.person.name
+ } ${i18n.t("registration_applications")} - ${
+ this.state.site_view.site.name
+ }`;
+ }
+
+ render() {
+ return (
+ <div class="container">
+ {this.state.loading ? (
+ <h5>
+ <Spinner large />
+ </h5>
+ ) : (
+ <div class="row">
+ <div class="col-12">
+ <HtmlTags
+ title={this.documentTitle}
+ path={this.context.router.route.match.url}
+ />
+ <h5 class="mb-2">{i18n.t("registration_applications")}</h5>
+ {this.selects()}
+ {this.applicationList()}
+ <Paginator
+ page={this.state.page}
+ onChange={this.handlePageChange}
+ />
+ </div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ unreadOrAllRadios() {
+ return (
+ <div class="btn-group btn-group-toggle flex-wrap mb-2">
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
+ `}
+ >
+ <input
+ type="radio"
+ value={UnreadOrAll.Unread}
+ checked={this.state.unreadOrAll == UnreadOrAll.Unread}
+ onChange={linkEvent(this, this.handleUnreadOrAllChange)}
+ />
+ {i18n.t("unread")}
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
+ `}
+ >
+ <input
+ type="radio"
+ value={UnreadOrAll.All}
+ checked={this.state.unreadOrAll == UnreadOrAll.All}
+ onChange={linkEvent(this, this.handleUnreadOrAllChange)}
+ />
+ {i18n.t("all")}
+ </label>
+ </div>
+ );
+ }
+
+ selects() {
+ return (
+ <div className="mb-2">
+ <span class="mr-3">{this.unreadOrAllRadios()}</span>
+ </div>
+ );
+ }
+
+ applicationList() {
+ return (
+ <div>
+ {this.state.applications.map(ra => (
+ <>
+ <hr />
+ <RegistrationApplication
+ key={ra.registration_application.id}
+ application={ra}
+ />
+ </>
+ ))}
+ </div>
+ );
+ }
+
+ handleUnreadOrAllChange(i: RegistrationApplications, event: any) {
+ i.state.unreadOrAll = Number(event.target.value);
+ i.state.page = 1;
+ i.setState(i.state);
+ i.refetch();
+ }
+
+ handlePageChange(page: number) {
+ this.setState({ page });
+ this.refetch();
+ }
+
+ static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
+ let promises: Promise<any>[] = [];
+
+ let form: ListRegistrationApplications = {
+ unread_only: true,
+ page: 1,
+ limit: fetchLimit,
+ auth: req.auth,
+ };
+ promises.push(req.client.listRegistrationApplications(form));
+
+ return promises;
+ }
+
+ refetch() {
+ let unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
+ let form: ListRegistrationApplications = {
+ unread_only: unread_only,
+ page: this.state.page,
+ limit: fetchLimit,
+ auth: authField(),
+ };
+ WebSocketService.Instance.send(wsClient.listRegistrationApplications(form));
+ }
+
+ parseMessage(msg: any) {
+ let op = wsUserOp(msg);
+ console.log(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), "danger");
+ return;
+ } else if (msg.reconnect) {
+ this.refetch();
+ } else if (op == UserOperation.ListRegistrationApplications) {
+ let data = wsJsonToRes<ListRegistrationApplicationsResponse>(msg).data;
+ this.state.applications = data.registration_applications;
+ this.state.loading = false;
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ } else if (op == UserOperation.ApproveRegistrationApplication) {
+ let data = wsJsonToRes<RegistrationApplicationResponse>(msg).data;
+ updateRegistrationApplicationRes(
+ data.registration_application,
+ this.state.applications
+ );
+ let uacs = UserService.Instance.unreadApplicationCountSub;
+ // Minor bug, where if the application switches from deny to approve, the count will still go down
+ uacs.next(uacs.getValue() - 1);
+ this.setState(this.state);
+ }
+ }
+}
wsSubscribe,
wsUserOp,
} from "../../utils";
-import { CommentReport } from "../comment/comment_report";
+import { CommentReport } from "../comment/comment-report";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
import { Paginator } from "../common/paginator";
-import { PostReport } from "../post/post_report";
+import { PostReport } from "../post/post-report";
enum UnreadOrAll {
Unread,
let op = wsUserOp(msg);
console.log(msg);
if (msg.error) {
+ this.setState({
+ saveUserSettingsLoading: false,
+ changePasswordLoading: false,
+ deleteAccountLoading: false,
+ });
toast(i18n.t(msg.error), "danger");
return;
} else if (op == UserOperation.SaveUserSettings) {
--- /dev/null
+import { Component } from "inferno";
+import {
+ SiteView,
+ UserOperation,
+ VerifyEmail as VerifyEmailForm,
+ VerifyEmailResponse,
+} from "lemmy-js-client";
+import { Subscription } from "rxjs";
+import { i18n } from "../../i18next";
+import { WebSocketService } from "../../services";
+import {
+ isBrowser,
+ setIsoData,
+ toast,
+ wsClient,
+ wsJsonToRes,
+ wsSubscribe,
+ wsUserOp,
+} from "../../utils";
+import { HtmlTags } from "../common/html-tags";
+
+interface State {
+ verifyEmailForm: VerifyEmailForm;
+ site_view: SiteView;
+}
+
+export class VerifyEmail extends Component<any, State> {
+ private isoData = setIsoData(this.context);
+ private subscription: Subscription;
+
+ emptyState: State = {
+ verifyEmailForm: {
+ token: this.props.match.params.token,
+ },
+ site_view: this.isoData.site_res.site_view,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ this.parseMessage = this.parseMessage.bind(this);
+ this.subscription = wsSubscribe(this.parseMessage);
+ }
+
+ componentDidMount() {
+ WebSocketService.Instance.send(
+ wsClient.verifyEmail(this.state.verifyEmailForm)
+ );
+ }
+
+ componentWillUnmount() {
+ if (isBrowser()) {
+ this.subscription.unsubscribe();
+ }
+ }
+
+ get documentTitle(): string {
+ return `${i18n.t("verify_email")} - ${this.state.site_view.site.name}`;
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <HtmlTags
+ title={this.documentTitle}
+ path={this.context.router.route.match.url}
+ />
+ <div class="row">
+ <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+ <h5>{i18n.t("verify_email")}</h5>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ parseMessage(msg: any) {
+ let op = wsUserOp(msg);
+ console.log(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), "danger");
+ this.setState(this.state);
+ this.props.history.push("/");
+ return;
+ } else if (op == UserOperation.VerifyEmail) {
+ let data = wsJsonToRes<VerifyEmailResponse>(msg).data;
+ if (data) {
+ toast(i18n.t("email_verified"));
+ this.state = this.emptyState;
+ this.setState(this.state);
+ this.props.history.push("/login");
+ }
+ }
+ }
+}
render() {
let r = this.props.report;
let post = r.post;
+ let tippyContent = i18n.t(
+ r.post_report.resolved ? "unresolve_report" : "resolve_report"
+ );
// Set the original post data ( a troll could change it )
post.name = r.post_report.original_post_name;
<button
className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleResolveReport)}
- data-tippy-content={
- r.post_report.resolved ? "unresolve_report" : "resolve_report"
- }
- aria-label={
- r.post_report.resolved ? "unresolve_report" : "resolve_report"
- }
+ data-tippy-content={tippyContent}
+ aria-label={tippyContent}
>
<Icon
icon="check"
import { Home } from "./components/home/home";
import { Instances } from "./components/home/instances";
import { Login } from "./components/home/login";
-import { PasswordChange } from "./components/home/password_change";
import { Setup } from "./components/home/setup";
import { Signup } from "./components/home/signup";
import { Modlog } from "./components/modlog";
import { Inbox } from "./components/person/inbox";
+import { PasswordChange } from "./components/person/password-change";
import { Profile } from "./components/person/profile";
+import { RegistrationApplications } from "./components/person/registration-applications";
import { Reports } from "./components/person/reports";
import { Settings } from "./components/person/settings";
+import { VerifyEmail } from "./components/person/verify-email";
import { CreatePost } from "./components/post/create-post";
import { Post } from "./components/post/post";
import { CreatePrivateMessage } from "./components/private_message/create-private-message";
component: Reports,
fetchInitialData: req => Reports.fetchInitialData(req),
},
+ {
+ path: `/registration_applications`,
+ component: RegistrationApplications,
+ fetchInitialData: req => RegistrationApplications.fetchInitialData(req),
+ },
{
path: `/search/q/:q/type/:type/sort/:sort/listing_type/:listing_type/community_id/:community_id/creator_id/:creator_id/page/:page`,
component: Search,
path: `/password_change/:token`,
component: PasswordChange,
},
+ {
+ path: `/verify_email/:token`,
+ component: VerifyEmail,
+ },
{ path: `/instances`, component: Instances },
];
new BehaviorSubject<number>(0);
public unreadReportCountSub: BehaviorSubject<number> =
new BehaviorSubject<number>(0);
+ public unreadApplicationCountSub: BehaviorSubject<number> =
+ new BehaviorSubject<number>(0);
private constructor() {
if (this.auth) {
PostReportView,
PostView,
PrivateMessageView,
+ RegistrationApplicationView,
Search,
SearchResponse,
SearchType,
}
}
+export function updateRegistrationApplicationRes(
+ data: RegistrationApplicationView,
+ applications: RegistrationApplicationView[]
+) {
+ let found = applications.find(
+ ra => ra.registration_application.id == data.registration_application.id
+ );
+ if (found) {
+ found.registration_application = data.registration_application;
+ found.admin = data.admin;
+ found.creator_local_user = data.creator_local_user;
+ }
+}
+
export function commentsToFlatNodes(comments: CommentView[]): CommentNodeI[] {
let nodes: CommentNodeI[] = [];
for (let comment of comments) {
dependencies:
invert-kv "^1.0.0"
-lemmy-js-client@0.14.0-rc.1:
- version "0.14.0-rc.1"
- resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.14.0-rc.1.tgz#c714d5f308fa20d5244db3844630f7b197eafa1c"
- integrity sha512-UF3I+80WTYWwQg2+96HTl0O2Yv0wy6rYFjlLNyzfqMXUZBnsr1O/SdJD1/9yAFPFbGkKgWusdncLoGgzFyn8eg==
+lemmy-js-client@0.15.0-rc.6:
+ version "0.15.0-rc.6"
+ resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.15.0-rc.6.tgz#5f8552488ed82b8c0962c158edccb8ce1d56389e"
+ integrity sha512-eSEZ5+F2ScKVtx+wwjdReHirJBNLQL2YdTV4aMCBWaSsxfsXUcz18/urbNxo+fNMc7Q4u0aRd3737yKBeMP9Kw==
levn@^0.4.1:
version "0.4.1"