Adding registration applications.

This commit is contained in:
Dessalines 2021-12-09 11:39:09 -05:00
parent 9948783325
commit 1683a6b857
16 changed files with 696 additions and 59 deletions

View file

@ -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.1",
"lint-staged": "^12.1.2",
"mini-css-extract-plugin": "^2.4.5",
"node-fetch": "^2.6.1",

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="edit" />
{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="edit" />
{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,129 @@
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;
}
export class RegistrationApplication extends Component<
RegistrationApplicationProps,
RegistrationApplicationState
> {
private emptyState: RegistrationApplicationState = {
denyReason: this.props.application.registration_application.deny_reason,
};
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>
)}
</div>
)}
<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>
<button
className="btn btn-secondary mr-2"
onClick={linkEvent(this, this.handleApprove)}
aria-label={i18n.t("approve")}
>
{i18n.t("approve")}
</button>
{!this.props.application.registration_application.deny_reason && (
<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) {
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) {
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)
);
}
handleDenyReasonChange(val: string) {
this.state.denyReason = val;
this.setState(this.state);
}
}

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);
@ -159,18 +163,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 +229,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">
@ -382,6 +426,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 +483,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

@ -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

@ -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,12 +6,13 @@ 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 { 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 { 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 { CreatePost } from "./components/post/create-post";
@ -128,6 +129,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,

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,
@ -1104,6 +1105,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.1:
version "0.15.0-rc.1"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.15.0-rc.1.tgz#7f5f0f069c76c5377a2a8654ce3a10563f405e14"
integrity sha512-Dgq7G2jGLURoswV40DfUNNvxQ/Dg8Jkz9jarwJuOMmBF9MfM/wD1nlaPtvMHlxePXtgDc8Y/Ig84k1OEF8Myqw==
levn@^0.4.1:
version "0.4.1"