Merge branch 'LemmyNet:main' into userpage

This commit is contained in:
mahanstreamer 2021-09-19 18:04:20 -04:00 committed by GitHub
commit 03e0e2d1c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 944 additions and 558 deletions

View file

@ -16,6 +16,7 @@
"warnOnUnsupportedTypeScriptVersion": false
},
"rules": {
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"arrow-body-style": 0,

View file

@ -7,5 +7,9 @@ new_tag="$1"
# sudo docker build . --tag dessalines/lemmy-ui:$new_tag
# sudo docker push dessalines/lemmy-ui:$new_tag
# Upgrade version
yarn version --new-version $new_tag
git push
git tag $new_tag
git push origin $new_tag

@ -1 +1 @@
Subproject commit eee933bd87780e0e2a8700e9b8fe0047f14f428a
Subproject commit 7dd7b98da76477222f9fd9720b4b25e14e3ddc97

View file

@ -1,6 +1,7 @@
{
"name": "lemmy-ui",
"description": "An isomorphic UI for lemmy",
"version": "0.12.2",
"author": "Dessalines <tyhou13@gmx.com>",
"license": "AGPL-3.0",
"scripts": {
@ -15,23 +16,25 @@
},
"repository": "https://github.com/LemmyNet/lemmy-ui",
"dependencies": {
"@typescript-eslint/parser": "^4.28.3",
"@typescript-eslint/parser": "^4.31.1",
"autosize": "^5.0.1",
"check-password-strength": "^2.0.3",
"choices.js": "^9.0.1",
"emoji-short-name": "^1.0.0",
"express": "~4.17.1",
"i18next": "^20.3.3",
"inferno": "^7.4.9",
"inferno-create-element": "^7.4.9",
"i18next": "^20.6.1",
"inferno": "^7.4.10",
"inferno-create-element": "^7.4.10",
"inferno-helmet": "^5.2.1",
"inferno-hydrate": "^7.4.9",
"inferno-i18next": "github:nimbusec-oss/inferno-i18next#semver:^7.4.2",
"inferno-router": "^7.4.9",
"inferno-server": "^7.4.9",
"inferno-hydrate": "^7.4.10",
"inferno-i18next-dess": "^0.0.1",
"inferno-router": "^7.4.10",
"inferno-server": "^7.4.10",
"isomorphic-cookie": "^1.2.4",
"jwt-decode": "^3.1.2",
"markdown-it": "^12.1.0",
"markdown-it-container": "^3.0.0",
"markdown-it-html5-embed": "^1.0.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"moment": "^2.29.1",
@ -42,7 +45,7 @@
"tippy.js": "^6.3.1",
"toastify-js": "^1.11.1",
"tributejs": "^5.1.3",
"ws": "^8.2.0"
"ws": "^8.2.2"
},
"devDependencies": {
"@babel/core": "^7.14.6",

View file

@ -207,6 +207,10 @@ hr {
text-overflow: ellipsis;
}
.overflow-wrap-anywhere {
overflow-wrap: anywhere;
}
#app {
display: flex;
flex-direction: column;

View file

@ -1,6 +1,6 @@
import { Component } from "inferno";
import { Helmet } from "inferno-helmet";
import { Provider } from "inferno-i18next";
import { Provider } from "inferno-i18next-dess";
import { Route, Switch } from "inferno-router";
import { GetSiteResponse } from "lemmy-js-client";
import { i18n } from "../../i18next";

View file

@ -20,14 +20,15 @@ import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services";
import {
authField,
donateLemmyUrl,
fetchLimit,
getLanguage,
isBrowser,
notifyComment,
notifyPrivateMessage,
numToSI,
setTheme,
showAvatars,
supportLemmyUrl,
toast,
wsClient,
wsJsonToRes,
@ -189,7 +190,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
"unread_messages"
)}`}
>
{this.state.unreadCount}
{numToSI(this.state.unreadCount)}
</span>
)}
</button>
@ -240,7 +241,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
<a
className="nav-link"
title={i18n.t("support_lemmy")}
href={supportLemmyUrl}
href={donateLemmyUrl}
>
<Icon icon="heart" classes="small" />
</a>
@ -263,7 +264,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
/^\/search/
) && (
<form
class="form-inline"
class="form-inline mr-2"
onSubmit={linkEvent(this, this.handleSearchSubmit)}
>
<input
@ -309,7 +310,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
"unread_messages"
)}`}
>
{this.state.unreadCount}
{numToSI(this.state.unreadCount)}
</span>
)}
</Link>
@ -375,13 +376,22 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</>
) : (
<ul class="navbar-nav my-2">
<li className="ml-2 nav-item">
<li className="nav-item">
<button
className="btn btn-success"
className="nav-link btn btn-link"
onClick={linkEvent(this, this.handleGotoLogin)}
title={i18n.t("login_sign_up")}
title={i18n.t("login")}
>
{i18n.t("login_sign_up")}
{i18n.t("login")}
</button>
</li>
<li className="nav-item">
<button
className="nav-link btn btn-link"
onClick={linkEvent(this, this.handleGotoSignup)}
title={i18n.t("sign_up")}
>
{i18n.t("sign_up")}
</button>
</li>
</ul>
@ -428,7 +438,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
handleLogoutClick(i: Navbar) {
i.setState({ showDropdown: false, expanded: false });
UserService.Instance.logout();
i.context.router.history.push("/");
window.location.href = "/";
location.reload();
}
@ -481,6 +491,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
i.context.router.history.push(`/login`);
}
handleGotoSignup(i: Navbar) {
i.setState({ showDropdown: false, expanded: false });
i.context.router.history.push(`/signup`);
}
handleShowDropdown(i: Navbar) {
i.state.showDropdown = !i.state.showDropdown;
i.setState(i.state);

View file

@ -1,5 +1,5 @@
import { Component } from "inferno";
import { T } from "inferno-i18next";
import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router";
import {
CommentResponse,

View file

@ -30,6 +30,7 @@ import {
getUnixTime,
isMod,
mdToHtml,
numToSI,
setupTippy,
showScores,
wsClient,
@ -217,9 +218,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
class="mr-1 font-weight-bold"
aria-label={i18n.t("number_of_points", {
count: this.state.score,
formattedCount: this.state.score,
})}
>
{this.state.score}
{numToSI(this.state.score)}
</span>
</a>
<span className="mr-1"></span>
@ -293,7 +295,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<Icon icon="arrow-up1" classes="icon-inline" />
{showScores() &&
this.state.upvotes !== this.state.score && (
<span class="ml-1">{this.state.upvotes}</span>
<span class="ml-1">
{numToSI(this.state.upvotes)}
</span>
)}
</button>
{this.props.enableDownvotes && (
@ -310,7 +314,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<Icon icon="arrow-down1" classes="icon-inline" />
{showScores() &&
this.state.upvotes !== this.state.score && (
<span class="ml-1">{this.state.downvotes}</span>
<span class="ml-1">
{numToSI(this.state.downvotes)}
</span>
)}
</button>
)}
@ -1289,14 +1295,17 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
get pointsTippy(): string {
let points = i18n.t("number_of_points", {
count: this.state.score,
formattedCount: this.state.score,
});
let upvotes = i18n.t("number_of_upvotes", {
count: this.state.upvotes,
formattedCount: this.state.upvotes,
});
let downvotes = i18n.t("number_of_downvotes", {
count: this.state.downvotes,
formattedCount: this.state.downvotes,
});
return `${points}${upvotes}${downvotes}`;

View file

@ -36,7 +36,7 @@ export class ImageUploadForm extends Component<
<form class="d-inline">
<label
htmlFor={this.id}
class="pointer ml-4 text-muted small font-weight-bold"
class="pointer text-muted small font-weight-bold"
>
{!this.props.imageSrc ? (
<span class="btn btn-secondary">{this.props.uploadTitle}</span>

View file

@ -19,6 +19,7 @@ import {
getListingTypeFromPropsNoDefault,
getPageFromProps,
isBrowser,
numToSI,
setIsoData,
setOptionalAuth,
showLocal,
@ -160,13 +161,17 @@ export class Communities extends Component<any, CommunitiesState> {
<td>
<CommunityLink community={cv.community} />
</td>
<td class="text-right">{cv.counts.subscribers}</td>
<td class="text-right">{cv.counts.users_active_month}</td>
<td class="text-right d-none d-lg-table-cell">
{cv.counts.posts}
<td class="text-right">
{numToSI(cv.counts.subscribers)}
</td>
<td class="text-right">
{numToSI(cv.counts.users_active_month)}
</td>
<td class="text-right d-none d-lg-table-cell">
{cv.counts.comments}
{numToSI(cv.counts.posts)}
</td>
<td class="text-right d-none d-lg-table-cell">
{numToSI(cv.counts.comments)}
</td>
<td class="text-right">
{cv.subscribed ? (

View file

@ -121,16 +121,19 @@ export class CommunityForm extends Component<
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
{!this.props.community_view && (
<div class="form-group row">
<label class="col-12 col-form-label" htmlFor="community-name">
<label
class="col-12 col-sm-2 col-form-label"
htmlFor="community-name"
>
{i18n.t("name")}
<span
class="pointer unselectable ml-2 text-muted"
class="position-absolute pointer unselectable ml-2 text-muted"
data-tippy-content={i18n.t("name_explain")}
>
<Icon icon="help-circle" classes="icon-inline" />
</span>
</label>
<div class="col-12">
<div class="col-12 col-sm-10">
<input
type="text"
id="community-name"
@ -146,16 +149,19 @@ export class CommunityForm extends Component<
</div>
)}
<div class="form-group row">
<label class="col-12 col-form-label" htmlFor="community-title">
<label
class="col-12 col-sm-2 col-form-label"
htmlFor="community-title"
>
{i18n.t("display_name")}
<span
class="pointer unselectable ml-2 text-muted"
class="position-absolute pointer unselectable ml-2 text-muted"
data-tippy-content={i18n.t("display_name_explain")}
>
<Icon icon="help-circle" classes="icon-inline" />
</span>
</label>
<div class="col-12">
<div class="col-12 col-sm-10">
<input
type="text"
id="community-title"
@ -168,30 +174,34 @@ export class CommunityForm extends Component<
/>
</div>
</div>
<div class="form-group">
<label>{i18n.t("icon")}</label>
<ImageUploadForm
uploadTitle={i18n.t("upload_icon")}
imageSrc={this.state.communityForm.icon}
onUpload={this.handleIconUpload}
onRemove={this.handleIconRemove}
rounded
/>
</div>
<div class="form-group">
<label>{i18n.t("banner")}</label>
<ImageUploadForm
uploadTitle={i18n.t("upload_banner")}
imageSrc={this.state.communityForm.banner}
onUpload={this.handleBannerUpload}
onRemove={this.handleBannerRemove}
/>
<div class="form-group row">
<label class="col-12 col-sm-2">{i18n.t("icon")}</label>
<div class="col-12 col-sm-10">
<ImageUploadForm
uploadTitle={i18n.t("upload_icon")}
imageSrc={this.state.communityForm.icon}
onUpload={this.handleIconUpload}
onRemove={this.handleIconRemove}
rounded
/>
</div>
</div>
<div class="form-group row">
<label class="col-12 col-form-label" htmlFor={this.id}>
<label class="col-12 col-sm-2">{i18n.t("banner")}</label>
<div class="col-12 col-sm-10">
<ImageUploadForm
uploadTitle={i18n.t("upload_banner")}
imageSrc={this.state.communityForm.banner}
onUpload={this.handleBannerUpload}
onRemove={this.handleBannerRemove}
/>
</div>
</div>
<div class="form-group row">
<label class="col-12 col-sm-2 col-form-label" htmlFor={this.id}>
{i18n.t("sidebar")}
</label>
<div class="col-12">
<div class="col-12 col-sm-10">
<MarkdownTextArea
initialContent={this.state.communityForm.description}
onContentChange={this.handleCommunityDescriptionChange}
@ -201,18 +211,18 @@ export class CommunityForm extends Component<
{this.props.enableNsfw && (
<div class="form-group row">
<div class="col-12">
<legend class="col-form-label col-sm-2 pt-0">
{i18n.t("nsfw")}
</legend>
<div class="col-10">
<div class="form-check">
<input
class="form-check-input"
class="form-check-input position-static"
id="community-nsfw"
type="checkbox"
checked={this.state.communityForm.nsfw}
onChange={linkEvent(this, this.handleCommunityNsfwChange)}
/>
<label class="form-check-label" htmlFor="community-nsfw">
{i18n.t("nsfw")}
</label>
</div>
</div>
</div>

View file

@ -61,7 +61,7 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
{!this.props.hideAvatar && community.icon && showAvatars() && (
<PictrsImage src={community.icon} icon />
)}
<span>{displayName}</span>
<span class="overflow-wrap-anywhere">{displayName}</span>
</>
);
}

View file

@ -336,7 +336,7 @@ export class Community extends Component<any, State> {
return (
<div class="mb-2">
<BannerIconHeader banner={community.banner} icon={community.icon} />
<h5 class="mb-0">{community.title}</h5>
<h5 class="mb-0 overflow-wrap-anywhere">{community.title}</h5>
<CommunityLink
community={community}
realLink

View file

@ -11,7 +11,13 @@ import {
} from "lemmy-js-client";
import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services";
import { authField, getUnixTime, mdToHtml, wsClient } from "../../utils";
import {
authField,
getUnixTime,
mdToHtml,
numToSI,
wsClient,
} from "../../utils";
import { BannerIconHeader } from "../common/banner-icon-header";
import { Icon } from "../common/icon";
import { CommunityForm } from "../community/community-form";
@ -143,67 +149,79 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
return (
<ul class="my-1 list-inline">
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_online", { count: this.props.online })}
{i18n.t("number_online", {
count: this.props.online,
formattedCount: numToSI(this.props.online),
})}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={`${i18n.t("number_of_users", {
data-tippy-content={i18n.t("active_users_in_the_last_day", {
count: counts.users_active_day,
})} ${i18n.t("active_in_the_last")} ${i18n.t("day")}`}
formattedCount: counts.users_active_day,
})}
>
{i18n.t("number_of_users", {
count: counts.users_active_day,
formattedCount: numToSI(counts.users_active_day),
})}{" "}
/ {i18n.t("day")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={`${i18n.t("number_of_users", {
data-tippy-content={i18n.t("active_users_in_the_last_week", {
count: counts.users_active_week,
})} ${i18n.t("active_in_the_last")} ${i18n.t("week")}`}
formattedCount: counts.users_active_week,
})}
>
{i18n.t("number_of_users", {
count: counts.users_active_week,
formattedCount: numToSI(counts.users_active_week),
})}{" "}
/ {i18n.t("week")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={`${i18n.t("number_of_users", {
data-tippy-content={i18n.t("active_users_in_the_last_month", {
count: counts.users_active_month,
})} ${i18n.t("active_in_the_last")} ${i18n.t("month")}`}
formattedCount: counts.users_active_month,
})}
>
{i18n.t("number_of_users", {
count: counts.users_active_month,
formattedCount: numToSI(counts.users_active_month),
})}{" "}
/ {i18n.t("month")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={`${i18n.t("number_of_users", {
data-tippy-content={i18n.t("active_users_in_the_last_six_months", {
count: counts.users_active_half_year,
})} ${i18n.t("active_in_the_last")} ${i18n.t("number_of_months", {
count: 6,
})}`}
formattedCount: counts.users_active_half_year,
})}
>
{i18n.t("number_of_users", {
count: counts.users_active_half_year,
formattedCount: numToSI(counts.users_active_half_year),
})}{" "}
/ {i18n.t("number_of_months", { count: 6 })}
/ {i18n.t("number_of_months", { count: 6, formattedCount: 6 })}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_subscribers", {
count: counts.subscribers,
formattedCount: numToSI(counts.subscribers),
})}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_posts", {
count: counts.posts,
formattedCount: numToSI(counts.posts),
})}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_comments", {
count: counts.comments,
formattedCount: numToSI(counts.comments),
})}
</li>
<li className="list-inline-item">

View file

@ -1,5 +1,5 @@
import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next";
import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router";
import {
AddAdminResponse,
@ -40,6 +40,7 @@ import {
getSortTypeFromProps,
mdToHtml,
notifyPost,
numToSI,
restoreScrollPosition,
saveCommentRes,
saveScrollPosition,
@ -503,72 +504,85 @@ export class Home extends Component<any, HomeState> {
return (
<ul class="my-2 list-inline">
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_online", { count: this.state.siteRes.online })}
{i18n.t("number_online", {
count: this.state.siteRes.online,
formattedCount: numToSI(this.state.siteRes.online),
})}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={`${i18n.t("number_of_users", {
data-tippy-content={i18n.t("active_users_in_the_last_day", {
count: counts.users_active_day,
})} ${i18n.t("active_in_the_last")} ${i18n.t("day")}`}
formattedCount: numToSI(counts.users_active_day),
})}
>
{i18n.t("number_of_users", {
count: counts.users_active_day,
formattedCount: numToSI(counts.users_active_day),
})}{" "}
/ {i18n.t("day")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={`${i18n.t("number_of_users", {
data-tippy-content={i18n.t("active_users_in_the_last_week", {
count: counts.users_active_week,
})} ${i18n.t("active_in_the_last")} ${i18n.t("week")}`}
formattedCount: counts.users_active_week,
})}
>
{i18n.t("number_of_users", {
count: counts.users_active_week,
formattedCount: numToSI(counts.users_active_week),
})}{" "}
/ {i18n.t("week")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={`${i18n.t("number_of_users", {
data-tippy-content={i18n.t("active_users_in_the_last_month", {
count: counts.users_active_month,
})} ${i18n.t("active_in_the_last")} ${i18n.t("month")}`}
formattedCount: counts.users_active_month,
})}
>
{i18n.t("number_of_users", {
count: counts.users_active_month,
formattedCount: numToSI(counts.users_active_month),
})}{" "}
/ {i18n.t("month")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={`${i18n.t("number_of_users", {
data-tippy-content={i18n.t("active_users_in_the_last_six_months", {
count: counts.users_active_half_year,
})} ${i18n.t("active_in_the_last")} ${i18n.t("number_of_months", {
count: 6,
})}`}
formattedCount: counts.users_active_half_year,
})}
>
{i18n.t("number_of_users", {
count: counts.users_active_half_year,
formattedCount: numToSI(counts.users_active_half_year),
})}{" "}
/ {i18n.t("number_of_months", { count: 6 })}
/ {i18n.t("number_of_months", { count: 6, formattedCount: 6 })}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_users", {
count: counts.users,
formattedCount: numToSI(counts.users),
})}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_communities", {
count: counts.communities,
formattedCount: numToSI(counts.communities),
})}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_posts", {
count: counts.posts,
formattedCount: numToSI(counts.posts),
})}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_comments", {
count: counts.comments,
formattedCount: numToSI(counts.comments),
})}
</li>
<li className="list-inline-item">

View file

@ -1,12 +1,9 @@
import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next";
import {
GetCaptchaResponse,
GetSiteResponse,
Login as LoginForm,
LoginResponse,
PasswordReset,
Register,
SiteView,
UserOperation,
} from "lemmy-js-client";
@ -16,7 +13,6 @@ import { UserService, WebSocketService } from "../../services";
import {
authField,
isBrowser,
joinLemmyUrl,
setIsoData,
toast,
validEmail,
@ -26,40 +22,24 @@ import {
wsUserOp,
} from "../../utils";
import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon";
import { Spinner } from "../common/icon";
interface State {
loginForm: LoginForm;
registerForm: Register;
loginLoading: boolean;
registerLoading: boolean;
captcha: GetCaptchaResponse;
captchaPlaying: boolean;
site_view: SiteView;
}
export class Login extends Component<any, State> {
private isoData = setIsoData(this.context);
private subscription: Subscription;
private audio: HTMLAudioElement;
emptyState: State = {
loginForm: {
username_or_email: undefined,
password: undefined,
},
registerForm: {
username: undefined,
password: undefined,
password_verify: undefined,
show_nsfw: false,
captcha_uuid: undefined,
captcha_answer: undefined,
},
loginLoading: false,
registerLoading: false,
captcha: undefined,
captchaPlaying: false,
site_view: this.isoData.site_res.site_view,
};
@ -76,6 +56,13 @@ export class Login extends Component<any, State> {
}
}
componentDidMount() {
// Navigate to home if already logged in
if (UserService.Instance.myUserInfo) {
this.context.router.history.push("/");
}
}
componentWillUnmount() {
if (isBrowser()) {
this.subscription.unsubscribe();
@ -98,8 +85,7 @@ export class Login extends Component<any, State> {
path={this.context.router.route.match.url}
/>
<div class="row">
<div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
<div class="col-12 col-lg-6">{this.registerForm()}</div>
<div class="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div>
</div>
</div>
);
@ -168,187 +154,6 @@ export class Login extends Component<any, State> {
);
}
registerForm() {
return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h5>{i18n.t("sign_up")}</h5>
<div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-username">
{i18n.t("username")}
</label>
<div class="col-sm-10">
<input
type="text"
id="register-username"
class="form-control"
value={this.state.registerForm.username}
onInput={linkEvent(this, this.handleRegisterUsernameChange)}
required
minLength={3}
pattern="[a-zA-Z0-9_]+"
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-email">
{i18n.t("email")}
</label>
<div class="col-sm-10">
<input
type="email"
id="register-email"
class="form-control"
placeholder={i18n.t("optional")}
value={this.state.registerForm.email}
autoComplete="email"
onInput={linkEvent(this, this.handleRegisterEmailChange)}
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>
)}
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-password">
{i18n.t("password")}
</label>
<div class="col-sm-10">
<input
type="password"
id="register-password"
value={this.state.registerForm.password}
autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
maxLength={60}
class="form-control"
required
/>
</div>
</div>
<div class="form-group row">
<label
class="col-sm-2 col-form-label"
htmlFor="register-verify-password"
>
{i18n.t("verify_password")}
</label>
<div class="col-sm-10">
<input
type="password"
id="register-verify-password"
value={this.state.registerForm.password_verify}
autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
maxLength={60}
class="form-control"
required
/>
</div>
</div>
{this.state.captcha && (
<div class="form-group row">
<label class="col-sm-2" htmlFor="register-captcha">
<span class="mr-2">{i18n.t("enter_code")}</span>
<button
type="button"
class="btn btn-secondary"
onClick={linkEvent(this, this.handleRegenCaptcha)}
aria-label={i18n.t("captcha")}
>
<Icon icon="refresh-cw" classes="icon-refresh-cw" />
</button>
</label>
{this.showCaptcha()}
<div class="col-sm-6">
<input
type="text"
class="form-control"
id="register-captcha"
value={this.state.registerForm.captcha_answer}
onInput={linkEvent(
this,
this.handleRegisterCaptchaAnswerChange
)}
required
/>
</div>
</div>
)}
{this.state.site_view.site.enable_nsfw && (
<div class="form-group row">
<div class="col-sm-10">
<div class="form-check">
<input
class="form-check-input"
id="register-show-nsfw"
type="checkbox"
checked={this.state.registerForm.show_nsfw}
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
/>
<label class="form-check-label" htmlFor="register-show-nsfw">
{i18n.t("show_nsfw")}
</label>
</div>
</div>
</div>
)}
{this.isLemmyMl && (
<div class="mt-2 mb-0 alert alert-light" role="alert">
<T i18nKey="lemmy_ml_registration_message">
#<a href={joinLemmyUrl}>#</a>
</T>
</div>
)}
<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")}
</button>
</div>
</div>
</form>
);
}
showCaptcha() {
return (
<div class="col-sm-4">
{this.state.captcha.ok && (
<>
<img
class="rounded-top img-fluid"
src={this.captchaPngSrc()}
style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
alt={i18n.t("captcha")}
/>
{this.state.captcha.ok.wav && (
<button
class="rounded-bottom btn btn-sm btn-secondary btn-block"
style="border-top-right-radius: 0; border-top-left-radius: 0;"
title={i18n.t("play_captcha_audio")}
onClick={linkEvent(this, this.handleCaptchaPlay)}
type="button"
disabled={this.state.captchaPlaying}
>
<Icon icon="play" classes="icon-play" />
</button>
)}
</>
)}
</div>
);
}
handleLoginSubmit(i: Login, event: any) {
event.preventDefault();
i.state.loginLoading = true;
@ -366,53 +171,6 @@ export class Login extends Component<any, State> {
i.setState(i.state);
}
handleRegisterSubmit(i: Login, event: any) {
event.preventDefault();
i.state.registerLoading = true;
i.setState(i.state);
WebSocketService.Instance.send(wsClient.register(i.state.registerForm));
}
handleRegisterUsernameChange(i: Login, event: any) {
i.state.registerForm.username = event.target.value;
i.setState(i.state);
}
handleRegisterEmailChange(i: Login, event: any) {
i.state.registerForm.email = event.target.value;
if (i.state.registerForm.email == "") {
i.state.registerForm.email = undefined;
}
i.setState(i.state);
}
handleRegisterPasswordChange(i: Login, event: any) {
i.state.registerForm.password = event.target.value;
i.setState(i.state);
}
handleRegisterPasswordVerifyChange(i: Login, event: any) {
i.state.registerForm.password_verify = event.target.value;
i.setState(i.state);
}
handleRegisterShowNsfwChange(i: Login, event: any) {
i.state.registerForm.show_nsfw = event.target.checked;
i.setState(i.state);
}
handleRegisterCaptchaAnswerChange(i: Login, event: any) {
i.state.registerForm.captcha_answer = event.target.value;
i.setState(i.state);
}
handleRegenCaptcha(i: Login) {
i.audio = null;
i.state.captchaPlaying = false;
i.setState(i.state);
WebSocketService.Instance.send(wsClient.getCaptcha());
}
handlePasswordReset(i: Login, event: any) {
event.preventDefault();
let resetForm: PasswordReset = {
@ -421,37 +179,12 @@ export class Login extends Component<any, State> {
WebSocketService.Instance.send(wsClient.passwordReset(resetForm));
}
handleCaptchaPlay(i: Login) {
// This was a bad bug, it should only build the new audio on a new file.
// Replays would stop prematurely if this was rebuilt every time.
if (i.audio == null) {
let base64 = `data:audio/wav;base64,${i.state.captcha.ok.wav}`;
i.audio = new Audio(base64);
}
i.audio.play();
i.state.captchaPlaying = true;
i.setState(i.state);
i.audio.addEventListener("ended", () => {
i.audio.currentTime = 0;
i.state.captchaPlaying = false;
i.setState(i.state);
});
}
captchaPngSrc() {
return `data:image/png;base64,${this.state.captcha.ok.png}`;
}
parseMessage(msg: any) {
let op = wsUserOp(msg);
console.log(msg);
if (msg.error) {
toast(i18n.t(msg.error), "danger");
this.state = this.emptyState;
this.state.registerForm.captcha_answer = undefined;
// Refetch another captcha
WebSocketService.Instance.send(wsClient.getCaptcha());
this.setState(this.state);
@ -469,24 +202,6 @@ export class Login extends Component<any, State> {
);
toast(i18n.t("logged_in"));
this.props.history.push("/");
} else if (op == UserOperation.Register) {
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");
} else if (op == UserOperation.GetCaptcha) {
let data = wsJsonToRes<GetCaptchaResponse>(msg).data;
if (data.ok) {
this.state.captcha = data;
this.state.registerForm.captcha_uuid = data.ok.uuid;
this.setState(this.state);
}
} else if (op == UserOperation.PasswordReset) {
toast(i18n.t("reset_password_mail_sent"));
} else if (op == UserOperation.GetSite) {

View file

@ -0,0 +1,444 @@
import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess";
import {
GetCaptchaResponse,
GetSiteResponse,
LoginResponse,
Register,
SiteView,
UserOperation,
} from "lemmy-js-client";
import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import { Options, passwordStrength } from "check-password-strength";
import { UserService, WebSocketService } from "../../services";
import {
authField,
isBrowser,
joinLemmyUrl,
setIsoData,
toast,
validEmail,
wsClient,
wsJsonToRes,
wsSubscribe,
wsUserOp,
} from "../../utils";
import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon";
import {I18nKeys} from "i18next";
const passwordStrengthOptions: Options<string> = [
{
id: 0,
value: "too_weak",
minDiversity: 0,
minLength: 0,
},
{
id: 1,
value: "weak",
minDiversity: 2,
minLength: 10,
},
{
id: 2,
value: "medium",
minDiversity: 3,
minLength: 12,
},
{
id: 3,
value: "strong",
minDiversity: 4,
minLength: 14,
},
];
interface State {
registerForm: Register;
registerLoading: boolean;
captcha: GetCaptchaResponse;
captchaPlaying: boolean;
site_view: SiteView;
}
export class Signup extends Component<any, State> {
private isoData = setIsoData(this.context);
private subscription: Subscription;
private audio: HTMLAudioElement;
emptyState: State = {
registerForm: {
username: undefined,
password: undefined,
password_verify: undefined,
show_nsfw: false,
captcha_uuid: undefined,
captcha_answer: undefined,
},
registerLoading: false,
captcha: undefined,
captchaPlaying: false,
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);
if (isBrowser()) {
WebSocketService.Instance.send(wsClient.getCaptcha());
}
}
componentWillUnmount() {
if (isBrowser()) {
this.subscription.unsubscribe();
}
}
get documentTitle(): string {
return `${i18n.t("login")} - ${this.state.site_view.site.name}`;
}
get isLemmyMl(): boolean {
return isBrowser() && window.location.hostname == "lemmy.ml";
}
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">{this.registerForm()}</div>
</div>
</div>
);
}
registerForm() {
return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h5>{i18n.t("sign_up")}</h5>
<div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-username">
{i18n.t("username")}
</label>
<div class="col-sm-10">
<input
type="text"
id="register-username"
class="form-control"
value={this.state.registerForm.username}
onInput={linkEvent(this, this.handleRegisterUsernameChange)}
required
minLength={3}
pattern="[a-zA-Z0-9_]+"
title={i18n.t("community_reqs")}
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-email">
{i18n.t("email")}
</label>
<div class="col-sm-10">
<input
type="email"
id="register-email"
class="form-control"
placeholder={i18n.t("optional")}
value={this.state.registerForm.email}
autoComplete="email"
onInput={linkEvent(this, this.handleRegisterEmailChange)}
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>
)}
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-password">
{i18n.t("password")}
</label>
<div class="col-sm-10">
<input
type="password"
id="register-password"
value={this.state.registerForm.password}
autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
minLength={10}
maxLength={60}
class="form-control"
required
/>
{this.state.registerForm.password && (
<div class={this.passwordColorClass}>
{i18n.t(this.passwordStrength as I18nKeys)}
</div>
)}
</div>
</div>
<div class="form-group row">
<label
class="col-sm-2 col-form-label"
htmlFor="register-verify-password"
>
{i18n.t("verify_password")}
</label>
<div class="col-sm-10">
<input
type="password"
id="register-verify-password"
value={this.state.registerForm.password_verify}
autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
maxLength={60}
class="form-control"
required
/>
</div>
</div>
{this.state.captcha && (
<div class="form-group row">
<label class="col-sm-2" htmlFor="register-captcha">
<span class="mr-2">{i18n.t("enter_code")}</span>
<button
type="button"
class="btn btn-secondary"
onClick={linkEvent(this, this.handleRegenCaptcha)}
aria-label={i18n.t("captcha")}
>
<Icon icon="refresh-cw" classes="icon-refresh-cw" />
</button>
</label>
{this.showCaptcha()}
<div class="col-sm-6">
<input
type="text"
class="form-control"
id="register-captcha"
value={this.state.registerForm.captcha_answer}
onInput={linkEvent(
this,
this.handleRegisterCaptchaAnswerChange
)}
required
/>
</div>
</div>
)}
{this.state.site_view.site.enable_nsfw && (
<div class="form-group row">
<div class="col-sm-10">
<div class="form-check">
<input
class="form-check-input"
id="register-show-nsfw"
type="checkbox"
checked={this.state.registerForm.show_nsfw}
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
/>
<label class="form-check-label" htmlFor="register-show-nsfw">
{i18n.t("show_nsfw")}
</label>
</div>
</div>
</div>
)}
{this.isLemmyMl && (
<div class="mt-2 mb-0 alert alert-light" role="alert">
<T i18nKey="lemmy_ml_registration_message">
#<a href={joinLemmyUrl}>#</a>
</T>
</div>
)}
<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")}
</button>
</div>
</div>
</form>
);
}
showCaptcha() {
return (
<div class="col-sm-4">
{this.state.captcha.ok && (
<>
<img
class="rounded-top img-fluid"
src={this.captchaPngSrc()}
style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
alt={i18n.t("captcha")}
/>
{this.state.captcha.ok.wav && (
<button
class="rounded-bottom btn btn-sm btn-secondary btn-block"
style="border-top-right-radius: 0; border-top-left-radius: 0;"
title={i18n.t("play_captcha_audio")}
onClick={linkEvent(this, this.handleCaptchaPlay)}
type="button"
disabled={this.state.captchaPlaying}
>
<Icon icon="play" classes="icon-play" />
</button>
)}
</>
)}
</div>
);
}
get passwordStrength() {
return passwordStrength(
this.state.registerForm.password,
passwordStrengthOptions
).value;
}
get passwordColorClass(): string {
let strength = this.passwordStrength;
if (["weak", "medium"].includes(strength)) {
return "text-warning";
} else if (strength == "strong") {
return "text-success";
} else {
return "text-danger";
}
}
handleRegisterSubmit(i: Signup, event: any) {
event.preventDefault();
i.state.registerLoading = true;
i.setState(i.state);
WebSocketService.Instance.send(wsClient.register(i.state.registerForm));
}
handleRegisterUsernameChange(i: Signup, event: any) {
i.state.registerForm.username = event.target.value;
i.setState(i.state);
}
handleRegisterEmailChange(i: Signup, event: any) {
i.state.registerForm.email = event.target.value;
if (i.state.registerForm.email == "") {
i.state.registerForm.email = undefined;
}
i.setState(i.state);
}
handleRegisterPasswordChange(i: Signup, event: any) {
i.state.registerForm.password = event.target.value;
i.setState(i.state);
}
handleRegisterPasswordVerifyChange(i: Signup, event: any) {
i.state.registerForm.password_verify = event.target.value;
i.setState(i.state);
}
handleRegisterShowNsfwChange(i: Signup, event: any) {
i.state.registerForm.show_nsfw = event.target.checked;
i.setState(i.state);
}
handleRegisterCaptchaAnswerChange(i: Signup, event: any) {
i.state.registerForm.captcha_answer = event.target.value;
i.setState(i.state);
}
handleRegenCaptcha(i: Signup) {
i.audio = null;
i.state.captchaPlaying = false;
i.setState(i.state);
WebSocketService.Instance.send(wsClient.getCaptcha());
}
handleCaptchaPlay(i: Signup) {
// This was a bad bug, it should only build the new audio on a new file.
// Replays would stop prematurely if this was rebuilt every time.
if (i.audio == null) {
let base64 = `data:audio/wav;base64,${i.state.captcha.ok.wav}`;
i.audio = new Audio(base64);
}
i.audio.play();
i.state.captchaPlaying = true;
i.setState(i.state);
i.audio.addEventListener("ended", () => {
i.audio.currentTime = 0;
i.state.captchaPlaying = false;
i.setState(i.state);
});
}
captchaPngSrc() {
return `data:image/png;base64,${this.state.captcha.ok.png}`;
}
parseMessage(msg: any) {
let op = wsUserOp(msg);
console.log(msg);
if (msg.error) {
toast(i18n.t(msg.error), "danger");
this.state = this.emptyState;
this.state.registerForm.captcha_answer = undefined;
// Refetch another captcha
WebSocketService.Instance.send(wsClient.getCaptcha());
this.setState(this.state);
return;
} else {
if (op == UserOperation.Register) {
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");
} else if (op == UserOperation.GetCaptcha) {
let data = wsJsonToRes<GetCaptchaResponse>(msg).data;
if (data.ok) {
this.state.captcha = data;
this.state.registerForm.captcha_uuid = data.ok.uuid;
this.setState(this.state);
}
} else if (op == UserOperation.PasswordReset) {
toast(i18n.t("reset_password_mail_sent"));
} else if (op == UserOperation.GetSite) {
let data = wsJsonToRes<GetSiteResponse>(msg).data;
this.state.site_view = data.site_view;
this.setState(this.state);
}
}
}
}

View file

@ -28,6 +28,7 @@ import {
fetchLimit,
getUsernameFromProps,
mdToHtml,
numToSI,
previewLines,
restoreScrollPosition,
routeSortTypeToEnum,
@ -96,6 +97,7 @@ export class Profile extends Component<any, ProfileState> {
this.state = this.emptyState;
this.handleSortChange = this.handleSortChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
@ -453,11 +455,15 @@ export class Profile extends Component<any, ProfileState> {
<div>
<ul class="list-inline mb-2">
<li className="list-inline-item badge badge-light">
{i18n.t("number_of_posts", { count: pv.counts.post_count })}
{i18n.t("number_of_posts", {
count: pv.counts.post_count,
formattedCount: numToSI(pv.counts.post_count),
})}
</li>
<li className="list-inline-item badge badge-light">
{i18n.t("number_of_comments", {
count: pv.counts.comment_count,
formattedCount: numToSI(pv.counts.comment_count),
})}
</li>
</ul>

View file

@ -227,10 +227,10 @@ export class Settings extends Component<any, SettingsState> {
<h5>{i18n.t("change_password")}</h5>
<form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
<div class="form-group row">
<label class="col-lg-5 col-form-label" htmlFor="user-password">
<label class="col-sm-5 col-form-label" htmlFor="user-password">
{i18n.t("new_password")}
</label>
<div class="col-lg-7">
<div class="col-sm-7">
<input
type="password"
id="user-password"
@ -244,12 +244,12 @@ export class Settings extends Component<any, SettingsState> {
</div>
<div class="form-group row">
<label
class="col-lg-5 col-form-label"
class="col-sm-5 col-form-label"
htmlFor="user-verify-password"
>
{i18n.t("verify_password")}
</label>
<div class="col-lg-7">
<div class="col-sm-7">
<input
type="password"
id="user-verify-password"
@ -262,10 +262,10 @@ export class Settings extends Component<any, SettingsState> {
</div>
</div>
<div class="form-group row">
<label class="col-lg-5 col-form-label" htmlFor="user-old-password">
<label class="col-sm-5 col-form-label" htmlFor="user-old-password">
{i18n.t("old_password")}
</label>
<div class="col-lg-7">
<div class="col-sm-7">
<input
type="password"
id="user-old-password"
@ -417,10 +417,10 @@ export class Settings extends Component<any, SettingsState> {
<h5>{i18n.t("settings")}</h5>
<form onSubmit={linkEvent(this, this.handleSaveSettingsSubmit)}>
<div class="form-group row">
<label class="col-lg-5 col-form-label" htmlFor="display-name">
<label class="col-sm-5 col-form-label" htmlFor="display-name">
{i18n.t("display_name")}
</label>
<div class="col-lg-7">
<div class="col-sm-7">
<input
id="display-name"
type="text"
@ -434,10 +434,10 @@ export class Settings extends Component<any, SettingsState> {
</div>
</div>
<div class="form-group row">
<label class="col-lg-3 col-form-label" htmlFor="user-bio">
<label class="col-sm-3 col-form-label" htmlFor="user-bio">
{i18n.t("bio")}
</label>
<div class="col-lg-9">
<div class="col-sm-9">
<MarkdownTextArea
initialContent={this.state.saveUserSettingsForm.bio}
onContentChange={this.handleBioChange}
@ -447,10 +447,10 @@ export class Settings extends Component<any, SettingsState> {
</div>
</div>
<div class="form-group row">
<label class="col-lg-3 col-form-label" htmlFor="user-email">
<label class="col-sm-3 col-form-label" htmlFor="user-email">
{i18n.t("email")}
</label>
<div class="col-lg-9">
<div class="col-sm-9">
<input
type="email"
id="user-email"
@ -463,12 +463,12 @@ export class Settings extends Component<any, SettingsState> {
</div>
</div>
<div class="form-group row">
<label class="col-lg-5 col-form-label" htmlFor="matrix-user-id">
<label class="col-sm-5 col-form-label" htmlFor="matrix-user-id">
<a href={elementUrl} rel="noopener">
{i18n.t("matrix_user_id")}
</a>
</label>
<div class="col-lg-7">
<div class="col-sm-7">
<input
id="matrix-user-id"
type="text"
@ -480,90 +480,102 @@ export class Settings extends Component<any, SettingsState> {
/>
</div>
</div>
<div class="form-group">
<label>{i18n.t("avatar")}</label>
<ImageUploadForm
uploadTitle={i18n.t("upload_avatar")}
imageSrc={this.state.saveUserSettingsForm.avatar}
onUpload={this.handleAvatarUpload}
onRemove={this.handleAvatarRemove}
rounded
/>
<div class="form-group row">
<label class="col-sm-3">{i18n.t("avatar")}</label>
<div class="col-sm-9">
<ImageUploadForm
uploadTitle={i18n.t("upload_avatar")}
imageSrc={this.state.saveUserSettingsForm.avatar}
onUpload={this.handleAvatarUpload}
onRemove={this.handleAvatarRemove}
rounded
/>
</div>
</div>
<div class="form-group">
<label>{i18n.t("banner")}</label>
<ImageUploadForm
uploadTitle={i18n.t("upload_banner")}
imageSrc={this.state.saveUserSettingsForm.banner}
onUpload={this.handleBannerUpload}
onRemove={this.handleBannerRemove}
/>
<div class="form-group row">
<label class="col-sm-3">{i18n.t("banner")}</label>
<div class="col-sm-9">
<ImageUploadForm
uploadTitle={i18n.t("upload_banner")}
imageSrc={this.state.saveUserSettingsForm.banner}
onUpload={this.handleBannerUpload}
onRemove={this.handleBannerRemove}
/>
</div>
</div>
<div class="form-group">
<label htmlFor="user-language">{i18n.t("language")}</label>
<select
id="user-language"
value={this.state.saveUserSettingsForm.lang}
onChange={linkEvent(this, this.handleLangChange)}
class="ml-2 custom-select w-auto"
>
<option disabled aria-hidden="true">
{i18n.t("language")}
</option>
<option value="browser">{i18n.t("browser_default")}</option>
<option disabled aria-hidden="true">
</option>
{languages.sort().map(lang => (
<option value={lang.code}>
{ISO6391.getNativeName(lang.code) || lang.code}
<div class="form-group row">
<label class="col-sm-3" htmlFor="user-language">
{i18n.t("language")}
</label>
<div class="col-sm-9">
<select
id="user-language"
value={this.state.saveUserSettingsForm.lang}
onChange={linkEvent(this, this.handleLangChange)}
class="custom-select w-auto"
>
<option disabled aria-hidden="true">
{i18n.t("language")}
</option>
))}
</select>
<option value="browser">{i18n.t("browser_default")}</option>
<option disabled aria-hidden="true">
</option>
{languages.sort().map(lang => (
<option value={lang.code}>
{ISO6391.getNativeName(lang.code) || lang.code}
</option>
))}
</select>
</div>
</div>
<div class="form-group">
<label htmlFor="user-theme">{i18n.t("theme")}</label>
<select
id="user-theme"
value={this.state.saveUserSettingsForm.theme}
onChange={linkEvent(this, this.handleThemeChange)}
class="ml-2 custom-select w-auto"
>
<option disabled aria-hidden="true">
{i18n.t("theme")}
</option>
<option value="browser">{i18n.t("browser_default")}</option>
{themes.map(theme => (
<option value={theme}>{theme}</option>
))}
</select>
</div>
<form className="form-group">
<label>
<div class="mr-2">{i18n.t("type")}</div>
<div class="form-group row">
<label class="col-sm-3" htmlFor="user-theme">
{i18n.t("theme")}
</label>
<ListingTypeSelect
type_={
Object.values(ListingType)[
this.state.saveUserSettingsForm.default_listing_type
]
}
showLocal={showLocal(this.isoData)}
onChange={this.handleListingTypeChange}
/>
<div class="col-sm-9">
<select
id="user-theme"
value={this.state.saveUserSettingsForm.theme}
onChange={linkEvent(this, this.handleThemeChange)}
class="custom-select w-auto"
>
<option disabled aria-hidden="true">
{i18n.t("theme")}
</option>
<option value="browser">{i18n.t("browser_default")}</option>
{themes.map(theme => (
<option value={theme}>{theme}</option>
))}
</select>
</div>
</div>
<form className="form-group row">
<label class="col-sm-3">{i18n.t("type")}</label>
<div class="col-sm-9">
<ListingTypeSelect
type_={
Object.values(ListingType)[
this.state.saveUserSettingsForm.default_listing_type
]
}
showLocal={showLocal(this.isoData)}
onChange={this.handleListingTypeChange}
/>
</div>
</form>
<form className="form-group">
<label>
<div class="mr-2">{i18n.t("sort_type")}</div>
</label>
<SortSelect
sort={
Object.values(SortType)[
this.state.saveUserSettingsForm.default_sort_type
]
}
onChange={this.handleSortTypeChange}
/>
<form className="form-group row">
<label class="col-sm-3">{i18n.t("sort_type")}</label>
<div class="col-sm-9">
<SortSelect
sort={
Object.values(SortType)[
this.state.saveUserSettingsForm.default_sort_type
]
}
onChange={this.handleSortTypeChange}
/>
</div>
</form>
{this.state.siteRes.site_view.site.enable_nsfw && (
<div class="form-group">
@ -1023,11 +1035,6 @@ export class Settings extends Component<any, SettingsState> {
i.setState(i.state);
}
handleLogoutClick(i: Settings) {
UserService.Instance.logout();
i.context.router.history.push("/");
}
handleDeleteAccount(i: Settings, event: any) {
event.preventDefault();
i.state.deleteAccountLoading = true;
@ -1107,6 +1114,7 @@ export class Settings extends Component<any, SettingsState> {
});
UserService.Instance.logout();
window.location.href = "/";
location.reload();
} else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data;
this.setState({ personBlocks: updatePersonBlock(data) });

View file

@ -310,18 +310,18 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
)}
{this.props.enableNsfw && (
<div class="form-group row">
<legend class="col-form-label col-sm-2 pt-0">
{i18n.t("nsfw")}
</legend>
<div class="col-sm-10">
<div class="form-check">
<input
class="form-check-input"
class="form-check-input position-static"
id="post-nsfw"
type="checkbox"
checked={this.state.postForm.nsfw}
onChange={linkEvent(this, this.handlePostNsfwChange)}
/>
<label class="form-check-label" htmlFor="post-nsfw">
{i18n.t("nsfw")}
</label>
</div>
</div>
</div>

View file

@ -32,6 +32,7 @@ import {
isVideo,
md,
mdToHtml,
numToSI,
previewLines,
setupTippy,
showScores,
@ -202,16 +203,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
if (isImage(post.url)) {
return (
<div
class="float-right text-body pointer d-inline-block position-relative mb-2"
<a
href={this.getImageSrc()}
class="float-right text-body d-inline-block position-relative mb-2"
data-tippy-content={i18n.t("expand_here")}
onClick={linkEvent(this, this.handleImageExpandClick)}
role="button"
aria-label={i18n.t("expand_here")}
>
{this.imgThumb(this.getImageSrc())}
<Icon icon="image" classes="mini-overlay" />
</div>
</a>
);
} else if (post.thumbnail_url) {
return (
@ -356,7 +357,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
class={`unselectable pointer font-weight-bold text-muted px-1`}
data-tippy-content={this.pointsTippy}
>
{this.state.score}
{numToSI(this.state.score)}
</div>
) : (
<div class="p-1"></div>
@ -418,12 +419,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<Icon icon="minus-square" classes="icon-inline" />
</button>
<div>
<button
<a
href={this.getImageSrc()}
class="btn btn-link d-inline-block"
onClick={linkEvent(this, this.handleImageExpandClick)}
>
<PictrsImage src={this.getImageSrc()} />
</button>
</a>
</div>
</span>
))}
@ -475,12 +477,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
className="text-muted small"
title={i18n.t("number_of_comments", {
count: post_view.counts.comments,
formattedCount: post_view.counts.comments,
})}
to={`/post/${post_view.post.id}?scrollToComments=true`}
>
<Icon icon="message-square" classes="icon-inline mr-1" />
{i18n.t("number_of_comments", {
count: post_view.counts.comments,
formattedCount: numToSI(post_view.counts.comments),
})}
</Link>
</button>
@ -494,7 +498,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
>
<small>
<Icon icon="arrow-down1" classes="icon-inline mr-1" />
<span>{this.state.downvotes}</span>
<span>{numToSI(this.state.downvotes)}</span>
</small>
</button>
)}
@ -532,7 +536,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
aria-label={i18n.t("upvote")}
>
<Icon icon="arrow-up1" classes="icon-inline small mr-2" />
{this.state.upvotes}
{numToSI(this.state.upvotes)}
</button>
) : (
<button
@ -557,7 +561,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
>
<Icon icon="arrow-down1" classes="icon-inline small mr-2" />
{this.state.downvotes !== 0 && (
<span>{this.state.downvotes}</span>
<span>{numToSI(this.state.downvotes)}</span>
)}
</button>
) : (
@ -1539,7 +1543,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
i.setState(i.state);
}
handleImageExpandClick(i: PostListing) {
handleImageExpandClick(i: PostListing, event: any) {
event.preventDefault();
i.state.imageExpanded = !i.state.imageExpanded;
i.setState(i.state);
}
@ -1571,14 +1576,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
get pointsTippy(): string {
let points = i18n.t("number_of_points", {
count: this.state.score,
formattedCount: this.state.score,
});
let upvotes = i18n.t("number_of_upvotes", {
count: this.state.upvotes,
formattedCount: this.state.upvotes,
});
let downvotes = i18n.t("number_of_downvotes", {
count: this.state.downvotes,
formattedCount: this.state.downvotes,
});
return `${points}${upvotes}${downvotes}`;

View file

@ -1,5 +1,5 @@
import { Component } from "inferno";
import { T } from "inferno-i18next";
import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router";
import { PostView } from "lemmy-js-client";
import { i18n } from "../../i18next";

View file

@ -1,5 +1,5 @@
import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next";
import { T } from "inferno-i18next-dess";
import { Prompt } from "inferno-router";
import {
CreatePrivateMessage,

View file

@ -38,6 +38,7 @@ import {
fetchLimit,
fetchUsers,
isBrowser,
numToSI,
personSelectName,
personToChoice,
restoreScrollPosition,
@ -649,6 +650,7 @@ export class Search extends Component<any, SearchState> {
<span>{` -
${i18n.t("number_of_subscribers", {
count: community_view.counts.subscribers,
formattedCount: numToSI(community_view.counts.subscribers),
})}
`}</span>
</>
@ -662,6 +664,7 @@ export class Search extends Component<any, SearchState> {
</span>,
<span>{` - ${i18n.t("number_of_comments", {
count: person_view.counts.comment_count,
formattedCount: numToSI(person_view.counts.comment_count),
})}`}</span>,
];
}

View file

@ -37,9 +37,11 @@ export const httpBaseInternal = `http://${host}`; // Don't use secure here
export const httpBase = `http${secure}://${host}`;
export const wsUri = `ws${secure}://${wsHost}/api/v3/ws`;
export const pictrsUri = `${httpBase}/pictrs/image`;
export const isHttps = secure.endsWith("s");
console.log(`httpbase: ${httpBase}`);
console.log(`wsUri: ${wsUri}`);
console.log(`isHttps: ${isHttps}`);
// This is for html tags, don't include port
const httpExternalUri = `http${secure}://${externalHost.split(":")[0]}`;

View file

@ -8,6 +8,7 @@ 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 { Profile } from "./components/person/profile";
@ -38,6 +39,10 @@ export const routes: IRoutePropsWithFetch[] = [
path: `/login`,
component: Login,
},
{
path: `/signup`,
component: Signup,
},
{
path: `/create_post`,
component: CreatePost,

View file

@ -3,6 +3,7 @@ import IsomorphicCookie from "isomorphic-cookie";
import jwt_decode from "jwt-decode";
import { LoginResponse, MyUserInfo } from "lemmy-js-client";
import { BehaviorSubject, Subject } from "rxjs";
import { isHttps } from "../env";
interface Claims {
sub: number;
@ -31,17 +32,18 @@ export class UserService {
public login(res: LoginResponse) {
let expires = new Date();
expires.setDate(expires.getDate() + 365);
IsomorphicCookie.save("jwt", res.jwt, { expires, secure: false });
IsomorphicCookie.save("jwt", res.jwt, { expires, secure: isHttps });
console.log("jwt cookie set");
this.setClaims(res.jwt);
}
public logout() {
IsomorphicCookie.remove("jwt", { secure: false });
this.claims = undefined;
this.myUserInfo = undefined;
// setTheme();
this.jwtSub.next("");
IsomorphicCookie.remove("jwt"); // TODO is sometimes unreliable for some reason
document.cookie = "jwt=; Max-Age=0; path=/; domain=" + location.host;
console.log("Logged out.");
}

View file

@ -25,6 +25,7 @@ import {
} from "lemmy-js-client";
import markdown_it from "markdown-it";
import markdown_it_container from "markdown-it-container";
import markdown_it_html5_embed from "markdown-it-html5-embed";
import markdown_it_sub from "markdown-it-sub";
import markdown_it_sup from "markdown-it-sup";
import moment from "moment";
@ -91,13 +92,13 @@ export const favIconPngUrl = "/static/assets/icons/apple-touch-icon.png";
// export const defaultFavIcon = `${window.location.protocol}//${window.location.host}${favIconPngUrl}`;
export const repoUrl = "https://github.com/LemmyNet";
export const joinLemmyUrl = "https://join-lemmy.org";
export const supportLemmyUrl = `${joinLemmyUrl}/support`;
export const donateLemmyUrl = `${joinLemmyUrl}/donate`;
export const docsUrl = `${joinLemmyUrl}/docs/en/index.html`;
export const helpGuideUrl = `${joinLemmyUrl}/docs/en/about/guide.html`; // TODO find a way to redirect to the non-en folder
export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
export const archiveUrl = "https://archive.is";
export const elementUrl = "https://element.io/";
export const elementUrl = "https://element.io";
export const postRefetchSeconds: number = 60 * 1000;
export const fetchLimit = 20;
@ -208,6 +209,16 @@ export const md = new markdown_it({
})
.use(markdown_it_sub)
.use(markdown_it_sup)
.use(markdown_it_html5_embed, {
html5embed: {
useImageSyntax: true, // Enables video/audio embed with ![]() syntax (default)
attributes: {
audio: 'controls preload="metadata"',
video:
'width="100%" max-height="100%" controls loop preload="metadata"',
},
},
})
.use(markdown_it_container, "spoiler", {
validate: function (params: any) {
return params.trim().match(/^spoiler\s+(.*)$/);
@ -1431,3 +1442,14 @@ export function initializeSite(site: GetSiteResponse) {
UserService.Instance.myUserInfo = site.my_user;
i18n.changeLanguage(getLanguage());
}
const SHORTNUM_SI_FORMAT = new Intl.NumberFormat("en-US", {
maximumSignificantDigits: 3,
//@ts-ignore
notation: "compact",
compactDisplay: "short",
});
export function numToSI(value: number): string {
return SHORTNUM_SI_FORMAT.format(value);
}

198
yarn.lock
View file

@ -1164,14 +1164,14 @@
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
"@typescript-eslint/parser@^4.28.3":
version "4.29.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.29.2.tgz#1c7744f4c27aeb74610c955d3dce9250e95c370a"
integrity sha512-WQ6BPf+lNuwteUuyk1jD/aHKqMQ9jrdCn7Gxt9vvBnzbpj7aWEf+aZsJ1zvTjx5zFxGCt000lsbD9tQPEL8u6g==
"@typescript-eslint/parser@^4.31.1":
version "4.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.31.1.tgz#8f9a2672033e6f6d33b1c0260eebdc0ddf539064"
integrity sha512-dnVZDB6FhpIby6yVbHkwTKkn2ypjVIfAR9nh+kYsA/ZL0JlTsd22BiDjouotisY3Irmd3OW1qlk9EI5R8GrvRQ==
dependencies:
"@typescript-eslint/scope-manager" "4.29.2"
"@typescript-eslint/types" "4.29.2"
"@typescript-eslint/typescript-estree" "4.29.2"
"@typescript-eslint/scope-manager" "4.31.1"
"@typescript-eslint/types" "4.31.1"
"@typescript-eslint/typescript-estree" "4.31.1"
debug "^4.3.1"
"@typescript-eslint/scope-manager@4.29.2":
@ -1182,11 +1182,24 @@
"@typescript-eslint/types" "4.29.2"
"@typescript-eslint/visitor-keys" "4.29.2"
"@typescript-eslint/scope-manager@4.31.1":
version "4.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.31.1.tgz#0c21e8501f608d6a25c842fcf59541ef4f1ab561"
integrity sha512-N1Uhn6SqNtU2XpFSkD4oA+F0PfKdWHyr4bTX0xTj8NRx1314gBDRL1LUuZd5+L3oP+wo6hCbZpaa1in6SwMcVQ==
dependencies:
"@typescript-eslint/types" "4.31.1"
"@typescript-eslint/visitor-keys" "4.31.1"
"@typescript-eslint/types@4.29.2":
version "4.29.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.2.tgz#fc0489c6b89773f99109fb0aa0aaddff21f52fcd"
integrity sha512-K6ApnEXId+WTGxqnda8z4LhNMa/pZmbTFkDxEBLQAbhLZL50DjeY0VIDCml/0Y3FlcbqXZrABqrcKxq+n0LwzQ==
"@typescript-eslint/types@4.31.1":
version "4.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.31.1.tgz#5f255b695627a13401d2fdba5f7138bc79450d66"
integrity sha512-kixltt51ZJGKENNW88IY5MYqTBA8FR0Md8QdGbJD2pKZ+D5IvxjTYDNtJPDxFBiXmka2aJsITdB1BtO1fsgmsQ==
"@typescript-eslint/typescript-estree@4.29.2":
version "4.29.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.2.tgz#a0ea8b98b274adbb2577100ba545ddf8bf7dc219"
@ -1200,6 +1213,19 @@
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@4.31.1":
version "4.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.1.tgz#4a04d5232cf1031232b7124a9c0310b577a62d17"
integrity sha512-EGHkbsUvjFrvRnusk6yFGqrqMBTue5E5ROnS5puj3laGQPasVUgwhrxfcgkdHNFECHAewpvELE1Gjv0XO3mdWg==
dependencies:
"@typescript-eslint/types" "4.31.1"
"@typescript-eslint/visitor-keys" "4.31.1"
debug "^4.3.1"
globby "^11.0.3"
is-glob "^4.0.1"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/visitor-keys@4.29.2":
version "4.29.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.2.tgz#d2da7341f3519486f50655159f4e5ecdcb2cd1df"
@ -1208,6 +1234,14 @@
"@typescript-eslint/types" "4.29.2"
eslint-visitor-keys "^2.0.0"
"@typescript-eslint/visitor-keys@4.31.1":
version "4.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.1.tgz#f2e7a14c7f20c4ae07d7fc3c5878c4441a1da9cc"
integrity sha512-PCncP8hEqKw6SOJY+3St4LVtoZpPPn+Zlpm7KW5xnviMhdqcsBty4Lsg4J/VECpJjw1CkROaZhH4B8M1OfnXTQ==
dependencies:
"@typescript-eslint/types" "4.31.1"
eslint-visitor-keys "^2.0.0"
"@webassemblyjs/ast@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
@ -2028,6 +2062,11 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
check-password-strength@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/check-password-strength/-/check-password-strength-2.0.3.tgz#fed038b1c457ac11a2999bd96f3185af34e88895"
integrity sha512-UW3YgMUne9QuejgnNWjWwYi4QhWArVj+1OXqDR1NkEQcmMKKO74O3P5ZvXr9JZNbTBfcwlK3yurYCMuJsck83A==
choices.js@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/choices.js/-/choices.js-9.0.1.tgz#745fb29af8670428fdc0bf1cc9dfaa404e9d0510"
@ -2800,6 +2839,11 @@ enquirer@^2.3.5, enquirer@^2.3.6:
dependencies:
ansi-colors "^4.1.1"
entities@~1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
entities@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
@ -3862,10 +3906,10 @@ husky@^7.0.1:
resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.1.tgz#579f4180b5da4520263e8713cc832942b48e1f1c"
integrity sha512-gceRaITVZ+cJH9sNHqx5tFwbzlLCVxtVZcusME8JYQ8Edy5mpGDOqD8QBCdMhpyo9a+JXddnujQ4rpY2Ff9SJA==
i18next@^20.3.3:
version "20.4.0"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.4.0.tgz#6897229a7898e23f3c4885f10315c978b594d3b9"
integrity sha512-89iWWJudmaHJwzIdJ/1eu98GtsJnwBhOUWwlAre70itPMuTE/NTPtgVeaS1CGaB8Q3XrYBGpEqlq4jsScDx9kg==
i18next@^20.6.1:
version "20.6.1"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.6.1.tgz#535e5f6e5baeb685c7d25df70db63bf3cc0aa345"
integrity sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==
dependencies:
"@babel/runtime" "^7.12.0"
@ -4015,6 +4059,13 @@ inferno-clone-vnode@^7.4.2:
dependencies:
inferno "7.4.8"
inferno-create-element@^7.4.10:
version "7.4.10"
resolved "https://registry.yarnpkg.com/inferno-create-element/-/inferno-create-element-7.4.10.tgz#a4d143c98aa141345fd9969b55b56f57336e0329"
integrity sha512-Gvq0FHL7qHofYjItVkpsOJtr8f2Ok1kxftBedbQb1fCUpHIEwiUvJSh+HX83ZunVwE9tisoPc/ddQPYFJ+x72Q==
dependencies:
inferno "7.4.10"
inferno-create-element@^7.4.2:
version "7.4.8"
resolved "https://registry.yarnpkg.com/inferno-create-element/-/inferno-create-element-7.4.8.tgz#77bbf24288645c359cf65b4821a3938c6537eb5e"
@ -4022,13 +4073,6 @@ inferno-create-element@^7.4.2:
dependencies:
inferno "7.4.8"
inferno-create-element@^7.4.9:
version "7.4.9"
resolved "https://registry.yarnpkg.com/inferno-create-element/-/inferno-create-element-7.4.9.tgz#0538b100442163e1c361f2a78664ee3dd5e6f2bb"
integrity sha512-wQ/gnd66pdrlm8uPAjGDlSCF6sX9mQ/mGtq8yYKHBTAmWPdE9P3mVsw5Wg9Iyy5NxRVsJqB9emBXIA8PNNkMCg==
dependencies:
inferno "7.4.9"
inferno-helmet@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/inferno-helmet/-/inferno-helmet-5.2.1.tgz#3717f325760aa14abeae82a78af7213f9055a3dc"
@ -4038,16 +4082,17 @@ inferno-helmet@^5.2.1:
inferno-side-effect "^1.1.5"
object-assign "^4.1.1"
inferno-hydrate@^7.4.9:
version "7.4.9"
resolved "https://registry.yarnpkg.com/inferno-hydrate/-/inferno-hydrate-7.4.9.tgz#ba355f2e17cc273d7adfd957bb19fce4447d8b29"
integrity sha512-QP8zmgTddI4WShmQO9VF+wqC3OXmwaGaZkQQNZsp7ZfprhhYuW6ulSPyckWEB/zGZxxYPdG8ZthEeQZF4NS5Jw==
inferno-hydrate@^7.4.10:
version "7.4.10"
resolved "https://registry.yarnpkg.com/inferno-hydrate/-/inferno-hydrate-7.4.10.tgz#678c2423fa47233d905b79d0597b39e1075da12e"
integrity sha512-bHJo7wd0ZKAmRlzoHqBjGhEgmOYFBh9LL58bIOeOXiuuyXJFUA6tP/vW91sx7j68K9Zq36SMwtbb/QnQ7R4mug==
dependencies:
inferno "7.4.9"
inferno "7.4.10"
"inferno-i18next@github:nimbusec-oss/inferno-i18next#semver:^7.4.2":
version "7.4.2"
resolved "https://codeload.github.com/nimbusec-oss/inferno-i18next/tar.gz/54b9be591ccd62c53799ad23e35f17144a62f909"
inferno-i18next-dess@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/inferno-i18next-dess/-/inferno-i18next-dess-0.0.1.tgz#48ae6bb4c3a617e59ff8dc97b9ed70f2ef762206"
integrity sha512-z/6UnuWFMyBivfR3SI9AmgA0/JvXARqtG/9BQryaLPenlG7iAb4cN2TeSt0mHIgFOo01QHVWZ8dqn7jZRijp2Q==
dependencies:
html-parse-stringify2 "^2.0.1"
inferno "^7.4.2"
@ -4056,33 +4101,33 @@ inferno-hydrate@^7.4.9:
inferno-shared "^7.4.2"
inferno-vnode-flags "^7.4.2"
inferno-router@^7.4.9:
version "7.4.9"
resolved "https://registry.yarnpkg.com/inferno-router/-/inferno-router-7.4.9.tgz#6f3d708f8e01d47f982b41ae18c9312a4c51220b"
integrity sha512-MunV594oIw0lGjcf6HMyPIx/F5bsz2+nz+ao4Jd6cLWy1zUCD8eowqCfZe3Re82wbW8+w7zNp5bCpuV+4IiWNQ==
inferno-router@^7.4.10:
version "7.4.10"
resolved "https://registry.yarnpkg.com/inferno-router/-/inferno-router-7.4.10.tgz#6fb27831b8864204bfab5ed192a2c7280e6420ed"
integrity sha512-XIeqmmPVwkRE445uMLqjimsh/zxtCsjA25Vt18y52zZaBH1R3Me6FoEoPsnZhH3j9zyOfzpHpxEPpUGWKfyZoA==
dependencies:
history "^4.10.1"
hoist-non-inferno-statics "^1.1.3"
inferno "7.4.9"
inferno "7.4.10"
path-to-regexp-es6 "1.7.0"
inferno-server@^7.4.9:
version "7.4.9"
resolved "https://registry.yarnpkg.com/inferno-server/-/inferno-server-7.4.9.tgz#05e73aa14512f492d9eec30698c55f30c39493bb"
integrity sha512-x9kE+Tk34QfRM1OL4+KSWY86t7dLVanRhDWikBs3jsHuFU+bQ9VGuEp4cf4wEBnv0DbPQiH+/XY5aoaQ6S8zFQ==
inferno-server@^7.4.10:
version "7.4.10"
resolved "https://registry.yarnpkg.com/inferno-server/-/inferno-server-7.4.10.tgz#959af7c0946541f29c2258b67a80b7dd11db9df5"
integrity sha512-L9BDXUc6nwmZxo2SPeZl3MtvxiBi5fMmutHe7f6uOVlZoFyylg45CY0KS4+1ySxALxW2fb9ZKJJw/8pBXjmgQA==
dependencies:
inferno "7.4.9"
inferno "7.4.10"
inferno-shared@7.4.10:
version "7.4.10"
resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.4.10.tgz#d4ca2c7fd6b580f86a623e923d080aa1d7259014"
integrity sha512-d7wlcW8NhchfX4vSg+6k9/FwFHAooo81GfWZtnDXtUvZNS4WEMaPH2j1YV6VnN4X3R0850dHRxR7830PdKh4Iw==
inferno-shared@7.4.8, inferno-shared@^7.4.2:
version "7.4.8"
resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.4.8.tgz#2b554a36683b770339008749096d9704846dd337"
integrity sha512-I0jnqsBcQvGJ7hqZF3vEzspQ80evViCe8joP3snWkPXPElk9WBVGLBHX5tHwuFuXv6wW4zeVVA4kBRAs47B+NQ==
inferno-shared@7.4.9:
version "7.4.9"
resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.4.9.tgz#f3cc5e85adadd7471a5e1c72da7df93a0bb98340"
integrity sha512-WNBz7OJ1DiVN+NeHgflwBHqvD589B9xMPkFGTj6mNs1cQCxYIZtslp5cqpkEo5TnA8O3FgEF00LMaLfc/i8fyw==
inferno-side-effect@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/inferno-side-effect/-/inferno-side-effect-1.1.5.tgz#a874c80dbc73602aafc1e0f3f3f1ec216a916271"
@ -4092,15 +4137,24 @@ inferno-side-effect@^1.1.5:
npm "^5.8.0"
shallowequal "^1.0.1"
inferno-vnode-flags@7.4.10:
version "7.4.10"
resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.4.10.tgz#a255931fc1df1e6896b29226d837f28e1c5ac968"
integrity sha512-OzfnqXrJx8Rl3FtyjhdFk7gzuCLMPCbDTiO8Bz0lw6P3ngW9Md5N5LPeg7Jz510PM0NisScQtxxHPwsFQxDIfw==
inferno-vnode-flags@7.4.8, inferno-vnode-flags@^7.4.2:
version "7.4.8"
resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.4.8.tgz#275d70e3c8b2b3f4eb56041cc9b8c832ce1fb26d"
integrity sha512-wOUeO7Aho8VH+s2V2K/53KwS0DtQFgT7TdzPE/s6P26ZIxQj+vt7oTJqzXn+xjRIjnfkTLm2eQ8qfInOWCu1rw==
inferno-vnode-flags@7.4.9:
version "7.4.9"
resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.4.9.tgz#26fe1a40f00de2ebc05b9e7543c8577c674c074b"
integrity sha512-pIGvc1MRSRrvjAOpARRz9OCyGrODKYfhw02ctWTIt3Jtn8dyqsjV0ZIaXOQEjpCi5ii7yfE5xwI+ZRI/g3pviQ==
inferno@7.4.10, inferno@^7.4.10:
version "7.4.10"
resolved "https://registry.yarnpkg.com/inferno/-/inferno-7.4.10.tgz#edfeca0db7dd1790aaf60e6aaf7edae8fe97dc63"
integrity sha512-L/qPVapN/b4WSrQND6fN0LOvhIeCVpGFQRbDplZvovOJoxRRZyE21k92tL/C76hQVFOp2FIgjZ7fjy9AnKPS+A==
dependencies:
inferno-shared "7.4.10"
inferno-vnode-flags "7.4.10"
opencollective-postinstall "^2.0.3"
inferno@7.4.8, inferno@^7.4.2:
version "7.4.8"
@ -4111,15 +4165,6 @@ inferno@7.4.8, inferno@^7.4.2:
inferno-vnode-flags "7.4.8"
opencollective-postinstall "^2.0.3"
inferno@7.4.9, inferno@^7.4.9:
version "7.4.9"
resolved "https://registry.yarnpkg.com/inferno/-/inferno-7.4.9.tgz#3722319b53b7e902ab194ad26fa6e80332d85c47"
integrity sha512-YxUYo3CyFGRkeSne87DacSAV1yXOp6dAu0toaEkwxb4dIIMilxDmJ8ap0EKxr3ZnQpX7EKmGrLBXrkYQVcmfvg==
dependencies:
inferno-shared "7.4.9"
inferno-vnode-flags "7.4.9"
opencollective-postinstall "^2.0.3"
inflight@^1.0.4, inflight@~1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@ -4715,6 +4760,13 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
linkify-it@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf"
integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==
dependencies:
uc.micro "^1.0.1"
linkify-it@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8"
@ -5025,6 +5077,14 @@ markdown-it-container@^3.0.0:
resolved "https://registry.yarnpkg.com/markdown-it-container/-/markdown-it-container-3.0.0.tgz#1d19b06040a020f9a827577bb7dbf67aa5de9a5b"
integrity sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==
markdown-it-html5-embed@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/markdown-it-html5-embed/-/markdown-it-html5-embed-1.0.0.tgz#f36bedca1eb12ce4df2d53b5ec72f62ba5e094b3"
integrity sha512-SPgugO/1+/9sZcgxoxijoTHSUpCUgFCNe1MSuTmDxDkV6NQrVzMclhRMFgE/rcHO+2rhIg3U7Oy80XA/E8ytpg==
dependencies:
markdown-it "^8.4.0"
mimoza "~1.0.0"
markdown-it-sub@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz#375fd6026eae7ddcb012497f6411195ea1e3afe8"
@ -5046,6 +5106,17 @@ markdown-it@^12.1.0:
mdurl "^1.0.1"
uc.micro "^1.0.5"
markdown-it@^8.4.0:
version "8.4.2"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54"
integrity sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==
dependencies:
argparse "^1.0.7"
entities "~1.1.1"
linkify-it "^2.0.0"
mdurl "^1.0.1"
uc.micro "^1.0.5"
mdurl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
@ -5134,6 +5205,11 @@ mime-db@1.49.0, "mime-db@>= 1.43.0 < 2":
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==
mime-db@^1.6.0:
version "1.50.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f"
integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==
mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
version "2.1.32"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5"
@ -5161,6 +5237,13 @@ mimic-fn@^3.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74"
integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==
mimoza@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/mimoza/-/mimoza-1.0.0.tgz#d74aa4fe08932f005e430bdc7bfcfa95fcab4e62"
integrity sha1-10qk/giTLwBeQwvce/z6lfyrTmI=
dependencies:
mime-db "^1.6.0"
min-indent@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
@ -8350,11 +8433,16 @@ write-file-atomic@^2.0.0, write-file-atomic@^2.3.0:
imurmurhash "^0.1.4"
signal-exit "^3.0.2"
ws@^8.1.0, ws@^8.2.0:
ws@^8.1.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.0.tgz#0b738cd484bfc9303421914b11bb4011e07615bb"
integrity sha512-uYhVJ/m9oXwEI04iIVmgLmugh2qrZihkywG9y5FfZV2ATeLIzHf93qs+tUNqlttbQK957/VX3mtwAS+UfIwA4g==
ws@^8.2.2:
version "8.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.2.tgz#ca684330c6dd6076a737250ed81ac1606cb0a63e"
integrity sha512-Q6B6H2oc8QY3llc3cB8kVmQ6pnJWVQbP7Q5algTcIxx7YEpc0oU4NBVHlztA7Ekzfhw2r0rPducMUiCGWKQRzw==
xdg-basedir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"