Moved ChangePassword to its own action.

- Fixes #246
- Changing preferred to display_name. Fixes #243
- Fixing a drone issue
This commit is contained in:
Dessalines 2021-04-08 21:59:34 -04:00
parent 16d81ec090
commit 7ebb96e608
7 changed files with 451 additions and 408 deletions

View file

@ -36,7 +36,7 @@ steps:
dockerfile: Dockerfile
repo: dessalines/lemmy-ui
tags:
- dev-linux-arm64
- dev-linux-amd64
username:
from_secret: docker_username
password:

View file

@ -68,7 +68,7 @@
"eslint-plugin-prettier": "^3.3.1",
"husky": "^6.0.0",
"iso-639-1": "^2.1.9",
"lemmy-js-client": "0.10.3",
"lemmy-js-client": "0.11.0-rc.4",
"lint-staged": "^10.5.4",
"mini-css-extract-plugin": "^1.4.1",
"node-fetch": "^2.6.1",

View file

@ -346,8 +346,8 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
{localUserView.person.avatar && showAvatars() && (
<PictrsImage src={localUserView.person.avatar} icon />
)}
{localUserView.person.preferred_username
? localUserView.person.preferred_username
{localUserView.person.display_name
? localUserView.person.display_name
: localUserView.person.name}
</span>
</Link>

View file

@ -34,11 +34,11 @@ export class PersonListing extends Component<PersonListingProps, any> {
let displayName = this.props.useApubName
? apubName
: person.preferred_username
? person.preferred_username
: person.display_name
? person.display_name
: apubName;
if (this.props.showApubName && !local && person.preferred_username) {
if (this.props.showApubName && !local && person.display_name) {
displayName = `${displayName} (${apubName})`;
}

View file

@ -16,6 +16,7 @@ import {
CommentResponse,
PostResponse,
BanPersonResponse,
ChangePassword,
} from "lemmy-js-client";
import { InitialFetchRequest, PersonDetailsView } from "../interfaces";
import { WebSocketService, UserService } from "../services";
@ -71,8 +72,10 @@ interface PersonState {
sort: SortType;
page: number;
loading: boolean;
userSettingsForm: SaveUserSettings;
userSettingsLoading: boolean;
saveUserSettingsForm: SaveUserSettings;
changePasswordForm: ChangePassword;
saveUserSettingsLoading: boolean;
changePasswordLoading: boolean;
deleteAccountLoading: boolean;
deleteAccountShowConfirm: boolean;
deleteAccountForm: DeleteAccount;
@ -104,7 +107,7 @@ export class Person extends Component<any, PersonState> {
view: Person.getViewFromProps(this.props.match.view),
sort: Person.getSortTypeFromProps(this.props.match.sort),
page: Person.getPageFromProps(this.props.match.page),
userSettingsForm: {
saveUserSettingsForm: {
show_nsfw: null,
theme: null,
default_sort_type: null,
@ -113,10 +116,17 @@ export class Person extends Component<any, PersonState> {
show_avatars: null,
send_notifications_to_email: null,
bio: null,
preferred_username: null,
display_name: null,
auth: authField(false),
},
userSettingsLoading: null,
changePasswordForm: {
new_password: null,
new_password_verify: null,
old_password: null,
auth: authField(false),
},
saveUserSettingsLoading: null,
changePasswordLoading: false,
deleteAccountLoading: null,
deleteAccountShowConfirm: false,
deleteAccountForm: {
@ -408,8 +418,8 @@ export class Person extends Component<any, PersonState> {
<div class="">
<div class="mb-0 d-flex flex-wrap">
<div>
{pv.person.preferred_username && (
<h5 class="mb-0">{pv.person.preferred_username}</h5>
{pv.person.display_name && (
<h5 class="mb-0">{pv.person.display_name}</h5>
)}
<ul class="list-inline mb-2">
<li className="list-inline-item">
@ -502,13 +512,95 @@ export class Person extends Component<any, PersonState> {
<div>
<div class="card border-secondary mb-3">
<div class="card-body">
{this.saveUserSettingsHtmlForm()}
<br />
{this.changePasswordHtmlForm()}
</div>
</div>
</div>
);
}
changePasswordHtmlForm() {
return (
<>
<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">
{i18n.t("new_password")}
</label>
<div class="col-lg-7">
<input
type="password"
id="user-password"
class="form-control"
value={this.state.changePasswordForm.new_password}
autoComplete="new-password"
maxLength={60}
onInput={linkEvent(this, this.handleNewPasswordChange)}
/>
</div>
</div>
<div class="form-group row">
<label
class="col-lg-5 col-form-label"
htmlFor="user-verify-password"
>
{i18n.t("verify_password")}
</label>
<div class="col-lg-7">
<input
type="password"
id="user-verify-password"
class="form-control"
value={this.state.changePasswordForm.new_password_verify}
autoComplete="new-password"
maxLength={60}
onInput={linkEvent(this, this.handleNewPasswordVerifyChange)}
/>
</div>
</div>
<div class="form-group row">
<label class="col-lg-5 col-form-label" htmlFor="user-old-password">
{i18n.t("old_password")}
</label>
<div class="col-lg-7">
<input
type="password"
id="user-old-password"
class="form-control"
value={this.state.changePasswordForm.old_password}
autoComplete="new-password"
maxLength={60}
onInput={linkEvent(this, this.handleOldPasswordChange)}
/>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-block btn-secondary mr-4">
{this.state.changePasswordLoading ? (
<Spinner />
) : (
capitalizeFirstLetter(i18n.t("save"))
)}
</button>
</div>
</form>
</>
);
}
saveUserSettingsHtmlForm() {
return (
<>
<h5>{i18n.t("settings")}</h5>
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
<form onSubmit={linkEvent(this, this.handleSaveUserSettingsSubmit)}>
<div class="form-group">
<label>{i18n.t("avatar")}</label>
<ImageUploadForm
uploadTitle={i18n.t("upload_avatar")}
imageSrc={this.state.userSettingsForm.avatar}
imageSrc={this.state.saveUserSettingsForm.avatar}
onUpload={this.handleAvatarUpload}
onRemove={this.handleAvatarRemove}
rounded
@ -518,7 +610,7 @@ export class Person extends Component<any, PersonState> {
<label>{i18n.t("banner")}</label>
<ImageUploadForm
uploadTitle={i18n.t("upload_banner")}
imageSrc={this.state.userSettingsForm.banner}
imageSrc={this.state.saveUserSettingsForm.banner}
onUpload={this.handleBannerUpload}
onRemove={this.handleBannerRemove}
/>
@ -527,7 +619,7 @@ export class Person extends Component<any, PersonState> {
<label htmlFor="user-language">{i18n.t("language")}</label>
<select
id="user-language"
value={this.state.userSettingsForm.lang}
value={this.state.saveUserSettingsForm.lang}
onChange={linkEvent(this, this.handleUserSettingsLangChange)}
class="ml-2 custom-select w-auto"
>
@ -549,7 +641,7 @@ export class Person extends Component<any, PersonState> {
<label htmlFor="user-theme">{i18n.t("theme")}</label>
<select
id="user-theme"
value={this.state.userSettingsForm.theme}
value={this.state.saveUserSettingsForm.theme}
onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
class="ml-2 custom-select w-auto"
>
@ -569,7 +661,7 @@ export class Person extends Component<any, PersonState> {
<ListingTypeSelect
type_={
Object.values(ListingType)[
this.state.userSettingsForm.default_listing_type
this.state.saveUserSettingsForm.default_listing_type
]
}
showLocal={
@ -585,7 +677,7 @@ export class Person extends Component<any, PersonState> {
<SortSelect
sort={
Object.values(SortType)[
this.state.userSettingsForm.default_sort_type
this.state.saveUserSettingsForm.default_sort_type
]
}
onChange={this.handleUserSettingsSortTypeChange}
@ -601,7 +693,7 @@ export class Person extends Component<any, PersonState> {
type="text"
class="form-control"
placeholder={i18n.t("optional")}
value={this.state.userSettingsForm.preferred_username}
value={this.state.saveUserSettingsForm.display_name}
onInput={linkEvent(
this,
this.handleUserSettingsPreferredUsernameChange
@ -618,7 +710,7 @@ export class Person extends Component<any, PersonState> {
</label>
<div class="col-lg-9">
<MarkdownTextArea
initialContent={this.state.userSettingsForm.bio}
initialContent={this.state.saveUserSettingsForm.bio}
onContentChange={this.handleUserSettingsBioChange}
maxLength={300}
hideNavigationWarnings
@ -635,11 +727,8 @@ export class Person extends Component<any, PersonState> {
id="user-email"
class="form-control"
placeholder={i18n.t("optional")}
value={this.state.userSettingsForm.email}
onInput={linkEvent(
this,
this.handleUserSettingsEmailChange
)}
value={this.state.saveUserSettingsForm.email}
onInput={linkEvent(this, this.handleUserSettingsEmailChange)}
minLength={3}
/>
</div>
@ -656,7 +745,7 @@ export class Person extends Component<any, PersonState> {
type="text"
class="form-control"
placeholder="@user:example.com"
value={this.state.userSettingsForm.matrix_user_id}
value={this.state.saveUserSettingsForm.matrix_user_id}
onInput={linkEvent(
this,
this.handleUserSettingsMatrixUserIdChange
@ -665,69 +754,6 @@ export class Person extends Component<any, PersonState> {
/>
</div>
</div>
<div class="form-group row">
<label class="col-lg-5 col-form-label" htmlFor="user-password">
{i18n.t("new_password")}
</label>
<div class="col-lg-7">
<input
type="password"
id="user-password"
class="form-control"
value={this.state.userSettingsForm.new_password}
autoComplete="new-password"
maxLength={60}
onInput={linkEvent(
this,
this.handleUserSettingsNewPasswordChange
)}
/>
</div>
</div>
<div class="form-group row">
<label
class="col-lg-5 col-form-label"
htmlFor="user-verify-password"
>
{i18n.t("verify_password")}
</label>
<div class="col-lg-7">
<input
type="password"
id="user-verify-password"
class="form-control"
value={this.state.userSettingsForm.new_password_verify}
autoComplete="new-password"
maxLength={60}
onInput={linkEvent(
this,
this.handleUserSettingsNewPasswordVerifyChange
)}
/>
</div>
</div>
<div class="form-group row">
<label
class="col-lg-5 col-form-label"
htmlFor="user-old-password"
>
{i18n.t("old_password")}
</label>
<div class="col-lg-7">
<input
type="password"
id="user-old-password"
class="form-control"
value={this.state.userSettingsForm.old_password}
autoComplete="new-password"
maxLength={60}
onInput={linkEvent(
this,
this.handleUserSettingsOldPasswordChange
)}
/>
</div>
</div>
{this.state.siteRes.site_view.site.enable_nsfw && (
<div class="form-group">
<div class="form-check">
@ -735,7 +761,7 @@ export class Person extends Component<any, PersonState> {
class="form-check-input"
id="user-show-nsfw"
type="checkbox"
checked={this.state.userSettingsForm.show_nsfw}
checked={this.state.saveUserSettingsForm.show_nsfw}
onChange={linkEvent(
this,
this.handleUserSettingsShowNsfwChange
@ -753,7 +779,7 @@ export class Person extends Component<any, PersonState> {
class="form-check-input"
id="user-show-avatars"
type="checkbox"
checked={this.state.userSettingsForm.show_avatars}
checked={this.state.saveUserSettingsForm.show_avatars}
onChange={linkEvent(
this,
this.handleUserSettingsShowAvatarsChange
@ -770,9 +796,9 @@ export class Person extends Component<any, PersonState> {
class="form-check-input"
id="user-send-notifications-to-email"
type="checkbox"
disabled={!this.state.userSettingsForm.email}
disabled={!this.state.saveUserSettingsForm.email}
checked={
this.state.userSettingsForm.send_notifications_to_email
this.state.saveUserSettingsForm.send_notifications_to_email
}
onChange={linkEvent(
this,
@ -789,7 +815,7 @@ export class Person extends Component<any, PersonState> {
</div>
<div class="form-group">
<button type="submit" class="btn btn-block btn-secondary mr-4">
{this.state.userSettingsLoading ? (
{this.state.saveUserSettingsLoading ? (
<Spinner />
) : (
capitalizeFirstLetter(i18n.t("save"))
@ -797,7 +823,7 @@ export class Person extends Component<any, PersonState> {
</button>
</div>
<hr />
<div class="form-group mb-0">
<div class="form-group">
<button
class="btn btn-block btn-danger"
onClick={linkEvent(
@ -847,9 +873,7 @@ export class Person extends Component<any, PersonState> {
)}
</div>
</form>
</div>
</div>
</div>
</>
);
}
@ -928,125 +952,136 @@ export class Person extends Component<any, PersonState> {
}
handleUserSettingsShowNsfwChange(i: Person, event: any) {
i.state.userSettingsForm.show_nsfw = event.target.checked;
i.state.saveUserSettingsForm.show_nsfw = event.target.checked;
i.setState(i.state);
}
handleUserSettingsShowAvatarsChange(i: Person, event: any) {
i.state.userSettingsForm.show_avatars = event.target.checked;
i.state.saveUserSettingsForm.show_avatars = event.target.checked;
UserService.Instance.localUserView.local_user.show_avatars =
event.target.checked; // Just for instant updates
i.setState(i.state);
}
handleUserSettingsSendNotificationsToEmailChange(i: Person, event: any) {
i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
i.state.saveUserSettingsForm.send_notifications_to_email =
event.target.checked;
i.setState(i.state);
}
handleUserSettingsThemeChange(i: Person, event: any) {
i.state.userSettingsForm.theme = event.target.value;
i.state.saveUserSettingsForm.theme = event.target.value;
setTheme(event.target.value, true);
i.setState(i.state);
}
handleUserSettingsLangChange(i: Person, event: any) {
i.state.userSettingsForm.lang = event.target.value;
i18n.changeLanguage(getLanguage(i.state.userSettingsForm.lang));
i.state.saveUserSettingsForm.lang = event.target.value;
i18n.changeLanguage(getLanguage(i.state.saveUserSettingsForm.lang));
i.setState(i.state);
}
handleUserSettingsSortTypeChange(val: SortType) {
this.state.userSettingsForm.default_sort_type = Object.keys(
this.state.saveUserSettingsForm.default_sort_type = Object.keys(
SortType
).indexOf(val);
this.setState(this.state);
}
handleUserSettingsListingTypeChange(val: ListingType) {
this.state.userSettingsForm.default_listing_type = Object.keys(
this.state.saveUserSettingsForm.default_listing_type = Object.keys(
ListingType
).indexOf(val);
this.setState(this.state);
}
handleUserSettingsEmailChange(i: Person, event: any) {
i.state.userSettingsForm.email = event.target.value;
i.state.saveUserSettingsForm.email = event.target.value;
i.setState(i.state);
}
handleUserSettingsBioChange(val: string) {
this.state.userSettingsForm.bio = val;
this.state.saveUserSettingsForm.bio = val;
this.setState(this.state);
}
handleAvatarUpload(url: string) {
this.state.userSettingsForm.avatar = url;
this.state.saveUserSettingsForm.avatar = url;
this.setState(this.state);
}
handleAvatarRemove() {
this.state.userSettingsForm.avatar = "";
this.state.saveUserSettingsForm.avatar = "";
this.setState(this.state);
}
handleBannerUpload(url: string) {
this.state.userSettingsForm.banner = url;
this.state.saveUserSettingsForm.banner = url;
this.setState(this.state);
}
handleBannerRemove() {
this.state.userSettingsForm.banner = "";
this.state.saveUserSettingsForm.banner = "";
this.setState(this.state);
}
handleUserSettingsPreferredUsernameChange(i: Person, event: any) {
i.state.userSettingsForm.preferred_username = event.target.value;
i.state.saveUserSettingsForm.display_name = event.target.value;
i.setState(i.state);
}
handleUserSettingsMatrixUserIdChange(i: Person, event: any) {
i.state.userSettingsForm.matrix_user_id = event.target.value;
i.state.saveUserSettingsForm.matrix_user_id = event.target.value;
if (
i.state.userSettingsForm.matrix_user_id == "" &&
i.state.saveUserSettingsForm.matrix_user_id == "" &&
!UserService.Instance.localUserView.person.matrix_user_id
) {
i.state.userSettingsForm.matrix_user_id = undefined;
i.state.saveUserSettingsForm.matrix_user_id = undefined;
}
i.setState(i.state);
}
handleUserSettingsNewPasswordChange(i: Person, event: any) {
i.state.userSettingsForm.new_password = event.target.value;
if (i.state.userSettingsForm.new_password == "") {
i.state.userSettingsForm.new_password = undefined;
handleNewPasswordChange(i: Person, event: any) {
i.state.changePasswordForm.new_password = event.target.value;
if (i.state.changePasswordForm.new_password == "") {
i.state.changePasswordForm.new_password = undefined;
}
i.setState(i.state);
}
handleUserSettingsNewPasswordVerifyChange(i: Person, event: any) {
i.state.userSettingsForm.new_password_verify = event.target.value;
if (i.state.userSettingsForm.new_password_verify == "") {
i.state.userSettingsForm.new_password_verify = undefined;
handleNewPasswordVerifyChange(i: Person, event: any) {
i.state.changePasswordForm.new_password_verify = event.target.value;
if (i.state.changePasswordForm.new_password_verify == "") {
i.state.changePasswordForm.new_password_verify = undefined;
}
i.setState(i.state);
}
handleUserSettingsOldPasswordChange(i: Person, event: any) {
i.state.userSettingsForm.old_password = event.target.value;
if (i.state.userSettingsForm.old_password == "") {
i.state.userSettingsForm.old_password = undefined;
handleOldPasswordChange(i: Person, event: any) {
i.state.changePasswordForm.old_password = event.target.value;
if (i.state.changePasswordForm.old_password == "") {
i.state.changePasswordForm.old_password = undefined;
}
i.setState(i.state);
}
handleUserSettingsSubmit(i: Person, event: any) {
handleSaveUserSettingsSubmit(i: Person, event: any) {
event.preventDefault();
i.state.userSettingsLoading = true;
i.state.saveUserSettingsLoading = true;
i.setState(i.state);
WebSocketService.Instance.send(
wsClient.saveUserSettings(i.state.userSettingsForm)
wsClient.saveUserSettings(i.state.saveUserSettingsForm)
);
}
handleChangePasswordSubmit(i: Person, event: any) {
event.preventDefault();
i.state.changePasswordLoading = true;
i.setState(i.state);
WebSocketService.Instance.send(
wsClient.changePassword(i.state.changePasswordForm)
);
}
@ -1079,33 +1114,33 @@ export class Person extends Component<any, PersonState> {
setUserInfo() {
if (this.isCurrentUser) {
this.state.userSettingsForm.show_nsfw =
this.state.saveUserSettingsForm.show_nsfw =
UserService.Instance.localUserView.local_user.show_nsfw;
this.state.userSettingsForm.theme = UserService.Instance.localUserView
this.state.saveUserSettingsForm.theme = UserService.Instance.localUserView
.local_user.theme
? UserService.Instance.localUserView.local_user.theme
: "browser";
this.state.userSettingsForm.default_sort_type =
this.state.saveUserSettingsForm.default_sort_type =
UserService.Instance.localUserView.local_user.default_sort_type;
this.state.userSettingsForm.default_listing_type =
this.state.saveUserSettingsForm.default_listing_type =
UserService.Instance.localUserView.local_user.default_listing_type;
this.state.userSettingsForm.lang =
this.state.saveUserSettingsForm.lang =
UserService.Instance.localUserView.local_user.lang;
this.state.userSettingsForm.avatar =
this.state.saveUserSettingsForm.avatar =
UserService.Instance.localUserView.person.avatar;
this.state.userSettingsForm.banner =
this.state.saveUserSettingsForm.banner =
UserService.Instance.localUserView.person.banner;
this.state.userSettingsForm.preferred_username =
UserService.Instance.localUserView.person.preferred_username;
this.state.userSettingsForm.show_avatars =
this.state.saveUserSettingsForm.display_name =
UserService.Instance.localUserView.person.display_name;
this.state.saveUserSettingsForm.show_avatars =
UserService.Instance.localUserView.local_user.show_avatars;
this.state.userSettingsForm.email =
this.state.saveUserSettingsForm.email =
UserService.Instance.localUserView.local_user.email;
this.state.userSettingsForm.bio =
this.state.saveUserSettingsForm.bio =
UserService.Instance.localUserView.person.bio;
this.state.userSettingsForm.send_notifications_to_email =
this.state.saveUserSettingsForm.send_notifications_to_email =
UserService.Instance.localUserView.local_user.send_notifications_to_email;
this.state.userSettingsForm.matrix_user_id =
this.state.saveUserSettingsForm.matrix_user_id =
UserService.Instance.localUserView.person.matrix_user_id;
}
}
@ -1120,7 +1155,8 @@ export class Person extends Component<any, PersonState> {
}
this.setState({
deleteAccountLoading: false,
userSettingsLoading: false,
saveUserSettingsLoading: false,
changePasswordLoading: false,
});
return;
} else if (msg.reconnect) {
@ -1139,14 +1175,21 @@ export class Person extends Component<any, PersonState> {
} else if (op == UserOperation.SaveUserSettings) {
let data = wsJsonToRes<LoginResponse>(msg).data;
UserService.Instance.login(data);
this.state.personRes.person_view.person.bio = this.state.userSettingsForm.bio;
this.state.personRes.person_view.person.preferred_username = this.state.userSettingsForm.preferred_username;
this.state.personRes.person_view.person.banner = this.state.userSettingsForm.banner;
this.state.personRes.person_view.person.avatar = this.state.userSettingsForm.avatar;
this.state.userSettingsLoading = false;
this.state.personRes.person_view.person.bio = this.state.saveUserSettingsForm.bio;
this.state.personRes.person_view.person.display_name = this.state.saveUserSettingsForm.display_name;
this.state.personRes.person_view.person.banner = this.state.saveUserSettingsForm.banner;
this.state.personRes.person_view.person.avatar = this.state.saveUserSettingsForm.avatar;
this.state.saveUserSettingsLoading = false;
this.setState(this.state);
window.scrollTo(0, 0);
} else if (op == UserOperation.ChangePassword) {
let data = wsJsonToRes<LoginResponse>(msg).data;
UserService.Instance.login(data);
this.state.changePasswordLoading = false;
this.setState(this.state);
window.scrollTo(0, 0);
toast(i18n.t("password_changed"));
} else if (op == UserOperation.DeleteAccount) {
this.setState({
deleteAccountLoading: false,

View file

@ -299,7 +299,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<Icon icon="edit" classes="icon-inline" />
</span>
</li>
{!this.amCreator &&
{!this.amTopMod &&
(!this.state.showConfirmLeaveModTeam ? (
<li className="list-inline-item-action">
<span
@ -341,7 +341,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</li>
</>
))}
{this.amCreator && (
{this.amTopMod && (
<li className="list-inline-item-action">
<span
class="pointer"
@ -488,9 +488,9 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
WebSocketService.Instance.send(wsClient.followCommunity(form));
}
private get amCreator(): boolean {
private get amTopMod(): boolean {
return (
this.props.community_view.creator.id ==
this.props.moderators[0].moderator.id ==
UserService.Instance.localUserView.person.id
);
}

View file

@ -5125,10 +5125,10 @@ lcid@^1.0.0:
dependencies:
invert-kv "^1.0.0"
lemmy-js-client@0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.10.3.tgz#815fe3f49b696c010858331aafc3850318b9e15d"
integrity sha512-ay5RQZSfoErLYh+b3aHpjA34dH/8rlNUcZF3L5nN+1cp4UWC4qWl97XjUM52QFyRzHslFPeP8D99fpgVe4USIQ==
lemmy-js-client@0.11.0-rc.4:
version "0.11.0-rc.4"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.11.0-rc.4.tgz#bd5652538309efac686aa10329b3a04fac6f85c2"
integrity sha512-7pCEEWkmaOoxxJ9+QSTVQFwThdjIWFp/mSs4c82TYs6tKAJsmfqzBkvSK9QVpSA2OOeYVWoThklKcrlmNV2d6A==
levn@^0.4.1:
version "0.4.1"