Private instances (#523)

* 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
This commit is contained in:
Dessalines 2021-12-30 10:26:45 -05:00 committed by GitHub
parent 4c6713d3f2
commit b96e16b4e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 849 additions and 65 deletions

@ -1 +1 @@
Subproject commit 0412b6b349e5e8d6ac3ed88801187833e95c72c9
Subproject commit 1e0bb9920cda13bb128c87e85125b98ab8f319b6

View file

@ -1,7 +1,7 @@
{
"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": {
@ -72,7 +72,7 @@
"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",

View file

@ -91,7 +91,11 @@ server.get("/*", async (req, res) => {
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 = {

View file

@ -7,6 +7,8 @@ import {
GetSiteResponse,
GetUnreadCount,
GetUnreadCountResponse,
GetUnreadRegistrationApplicationCount,
GetUnreadRegistrationApplicationCountResponse,
PrivateMessageResponse,
UserOperation,
} from "lemmy-js-client";
@ -41,6 +43,7 @@ interface NavbarState {
expanded: boolean;
unreadInboxCount: number;
unreadReportCount: number;
unreadApplicationCount: number;
searchParam: string;
toggleSearch: boolean;
showDropdown: boolean;
@ -52,11 +55,13 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
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,
@ -115,6 +120,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
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 });
});
}
}
@ -123,6 +133,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
this.userSub.unsubscribe();
this.unreadInboxCountSub.unsubscribe();
this.unreadReportCountSub.unsubscribe();
this.unreadApplicationCountSub.unsubscribe();
}
updateUrl() {
@ -215,6 +226,31 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</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
@ -366,6 +402,31 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</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
@ -537,6 +598,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
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;
@ -586,7 +653,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
let unreadForm: GetUnreadCount = {
auth: authField(),
};
WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
console.log("Fetching reports...");
@ -594,8 +660,18 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
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() {
@ -612,6 +688,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
);
}
sendApplicationUnread() {
UserService.Instance.unreadApplicationCountSub.next(
this.state.unreadApplicationCount
);
}
get canAdmin(): boolean {
return (
UserService.Instance.myUserInfo &&

View file

@ -25,6 +25,9 @@ export class CommentReport extends Component<CommentReportProps, any> {
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;
@ -78,12 +81,8 @@ export class CommentReport extends Component<CommentReportProps, any> {
<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"

View file

@ -17,7 +17,7 @@ import {
import { Icon, Spinner } from "./icon";
interface MarkdownTextAreaProps {
initialContent: string;
initialContent?: string;
finished?: boolean;
buttonTitle?: string;
replyType?: boolean;

View file

@ -0,0 +1,150 @@
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);
}
}

View file

@ -12,6 +12,9 @@ export const SYMBOLS = (
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>

View file

@ -239,6 +239,7 @@ export class Home extends Component<any, HomeState> {
sort: SortType.Hot,
limit: 6,
};
setOptionalAuth(trendingCommunitiesForm, req.auth);
promises.push(req.client.listCommunities(trendingCommunitiesForm));
return promises;

View file

@ -185,8 +185,6 @@ export class Login extends Component<any, State> {
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 {

View file

@ -17,6 +17,7 @@ import {
authField,
isBrowser,
joinLemmyUrl,
mdToHtml,
setIsoData,
toast,
validEmail,
@ -27,6 +28,7 @@ import {
} from "../../utils";
import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea";
const passwordStrengthOptions: Options<string> = [
{
@ -77,6 +79,7 @@ export class Signup extends Component<any, State> {
captcha_uuid: undefined,
captcha_answer: undefined,
honeypot: undefined,
answer: undefined,
},
registerLoading: false,
captcha: undefined,
@ -88,6 +91,7 @@ export class Signup extends Component<any, State> {
super(props, context);
this.state = this.emptyState;
this.handleAnswerChange = this.handleAnswerChange.bind(this);
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
@ -104,7 +108,13 @@ export class Signup extends Component<any, State> {
}
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 {
@ -128,7 +138,7 @@ export class Signup extends Component<any, State> {
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">
@ -159,18 +169,24 @@ export class Signup extends Component<any, State> {
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>
@ -219,6 +235,40 @@ export class Signup extends Component<any, State> {
</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">
@ -286,7 +336,7 @@ export class Signup extends Component<any, State> {
<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>
@ -382,6 +432,11 @@ export class Signup extends Component<any, State> {
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);
@ -434,13 +489,24 @@ export class Signup extends Component<any, 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) {

View file

@ -3,12 +3,7 @@ import { Prompt } from "inferno-router";
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";
@ -24,7 +19,6 @@ interface SiteFormState {
}
export class SiteForm extends Component<SiteFormProps, SiteFormState> {
private id = `site-form-${randomStr()}`;
private emptyState: SiteFormState = {
siteForm: {
enable_downvotes: true,
@ -33,6 +27,10 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
name: null,
icon: null,
banner: null,
require_email_verification: null,
require_application: null,
application_question: null,
private_instance: null,
auth: authField(),
},
loading: false,
@ -43,6 +41,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
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);
@ -51,17 +51,21 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
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(),
};
}
@ -79,6 +83,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
!this.props.site &&
(this.state.siteForm.name ||
this.state.siteForm.sidebar ||
this.state.siteForm.application_question ||
this.state.siteForm.description)
) {
window.onbeforeunload = () => true;
@ -100,6 +105,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
!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")}
@ -162,9 +168,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</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}
@ -173,6 +177,20 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
/>
</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">
@ -255,6 +273,66 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</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
@ -311,6 +389,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
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);
@ -336,6 +419,21 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
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();
}

View file

@ -25,9 +25,11 @@ import { i18n } from "../i18next";
import { InitialFetchRequest } from "../interfaces";
import { UserService, WebSocketService } from "../services";
import {
authField,
fetchLimit,
isBrowser,
setIsoData,
setOptionalAuth,
toast,
wsClient,
wsJsonToRes,
@ -482,6 +484,7 @@ export class Modlog extends Component<any, ModlogState> {
community_id: this.state.communityId,
page: this.state.page,
limit: fetchLimit,
auth: authField(false),
};
WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
@ -507,6 +510,7 @@ export class Modlog extends Component<any, ModlogState> {
if (communityId) {
modlogForm.community_id = Number(communityId);
}
setOptionalAuth(modlogForm, req.auth);
promises.push(req.client.getModlog(modlogForm));
@ -514,6 +518,7 @@ export class Modlog extends Component<any, ModlogState> {
let communityForm: GetCommunity = {
id: Number(communityId),
};
setOptionalAuth(communityForm, req.auth);
promises.push(req.client.getCommunity(communityForm));
}
return promises;

View file

@ -0,0 +1,249 @@
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);
}
}
}

View file

@ -29,11 +29,11 @@ import {
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,

View file

@ -1108,6 +1108,11 @@ export class Settings extends Component<any, SettingsState> {
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) {

View file

@ -0,0 +1,97 @@
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");
}
}
}
}

View file

@ -20,6 +20,9 @@ export class PostReport extends Component<PostReportProps, any> {
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;
@ -70,12 +73,8 @@ export class PostReport extends Component<PostReportProps, any> {
<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"

View file

@ -6,14 +6,16 @@ import { AdminSettings } from "./components/home/admin-settings";
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";
@ -128,6 +130,11 @@ export const routes: IRoutePropsWithFetch[] = [
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,
@ -142,5 +149,9 @@ export const routes: IRoutePropsWithFetch[] = [
path: `/password_change/:token`,
component: PasswordChange,
},
{
path: `/verify_email/:token`,
component: VerifyEmail,
},
{ path: `/instances`, component: Instances },
];

View file

@ -20,6 +20,8 @@ export class UserService {
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) {

View file

@ -18,6 +18,7 @@ import {
PostReportView,
PostView,
PrivateMessageView,
RegistrationApplicationView,
Search,
SearchResponse,
SearchType,
@ -1105,6 +1106,20 @@ export function updateCommentReportRes(
}
}
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) {

View file

@ -4997,10 +4997,10 @@ lcid@^1.0.0:
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"