mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-21 22:27:11 +00:00
Remote follow (#1875)
* Redirect to page user was trying to access on login * Make modal * Make modal look better * Forgot to include in merge * Get rid of modal * Add external interaction page * Tweak success page * Add loading screen for remote fetch and refactor loading ellipses * Add error state for remote fetch page * Add card to federation success page * Bring back remote fetch modal * Add autofocus to remote fetch modal input * Redirect for remote fetch * Remove dummy data * Remove duplicate functions * Update translations * Update translations * Fix linting error * Fix linting errors * feat: Add toasts for remote follow error conditions
This commit is contained in:
parent
65e669035d
commit
d9fe7d1488
|
@ -1 +1 @@
|
||||||
Subproject commit a0f95fc29b7501156b6d8bbb504b1e787b5769e7
|
Subproject commit de9de2c53bee034d3824ecaa9a2104f8f341332e
|
|
@ -6,6 +6,7 @@ import { UserService } from "../shared/services";
|
||||||
|
|
||||||
import "bootstrap/js/dist/collapse";
|
import "bootstrap/js/dist/collapse";
|
||||||
import "bootstrap/js/dist/dropdown";
|
import "bootstrap/js/dist/dropdown";
|
||||||
|
import "bootstrap/js/dist/modal";
|
||||||
|
|
||||||
async function startClient() {
|
async function startClient() {
|
||||||
initializeSite(window.isoData.site_res);
|
initializeSite(window.isoData.site_res);
|
||||||
|
|
|
@ -58,7 +58,7 @@ export default async (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!auth && isAuthPath(path)) {
|
if (!auth && isAuthPath(path)) {
|
||||||
return res.redirect("/login");
|
return res.redirect(`/login?prev=${encodeURIComponent(url)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (try_site.state === "success") {
|
if (try_site.state === "success") {
|
||||||
|
|
|
@ -75,7 +75,7 @@ export class App extends Component<AppProps, any> {
|
||||||
<div tabIndex={-1}>
|
<div tabIndex={-1}>
|
||||||
{RouteComponent &&
|
{RouteComponent &&
|
||||||
(isAuthPath(path ?? "") ? (
|
(isAuthPath(path ?? "") ? (
|
||||||
<AuthGuard>
|
<AuthGuard {...routeProps}>
|
||||||
<RouteComponent {...routeProps} />
|
<RouteComponent {...routeProps} />
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,12 +1,40 @@
|
||||||
import { InfernoNode } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { Redirect } from "inferno-router";
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
import { UserService } from "../../services";
|
import { UserService } from "../../services";
|
||||||
|
import { Spinner } from "./icon";
|
||||||
|
|
||||||
function AuthGuard(props: { children?: InfernoNode }) {
|
interface AuthGuardState {
|
||||||
if (!UserService.Instance.myUserInfo) {
|
hasRedirected: boolean;
|
||||||
return <Redirect to="/login" />;
|
}
|
||||||
} else {
|
|
||||||
return props.children;
|
class AuthGuard extends Component<
|
||||||
|
RouteComponentProps<Record<string, string>>,
|
||||||
|
AuthGuardState
|
||||||
|
> {
|
||||||
|
state = {
|
||||||
|
hasRedirected: false,
|
||||||
|
} as AuthGuardState;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
props: RouteComponentProps<Record<string, string>>,
|
||||||
|
context: any,
|
||||||
|
) {
|
||||||
|
super(props, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!UserService.Instance.myUserInfo) {
|
||||||
|
const { pathname, search } = this.props.location;
|
||||||
|
this.context.router.history.replace(
|
||||||
|
`/login?prev=${encodeURIComponent(pathname + search)}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.setState({ hasRedirected: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.state.hasRedirected ? this.props.children : <Spinner />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
34
src/shared/components/common/loading-ellipses.tsx
Normal file
34
src/shared/components/common/loading-ellipses.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { Component } from "inferno";
|
||||||
|
|
||||||
|
interface LoadingEllipsesState {
|
||||||
|
ellipses: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoadingEllipses extends Component<any, LoadingEllipsesState> {
|
||||||
|
state: LoadingEllipsesState = {
|
||||||
|
ellipses: "...",
|
||||||
|
};
|
||||||
|
#interval?: NodeJS.Timer;
|
||||||
|
|
||||||
|
constructor(props: any, context: any) {
|
||||||
|
super(props, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.state.ellipses;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.#interval = setInterval(this.#updateEllipses, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearInterval(this.#interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateEllipses = () => {
|
||||||
|
this.setState(({ ellipses }) => ({
|
||||||
|
ellipses: ellipses.length === 3 ? "" : ellipses + ".",
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ interface PictrsImageProps {
|
||||||
nsfw?: boolean;
|
nsfw?: boolean;
|
||||||
iconOverlay?: boolean;
|
iconOverlay?: boolean;
|
||||||
pushup?: boolean;
|
pushup?: boolean;
|
||||||
|
cardTop?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PictrsImage extends Component<PictrsImageProps, any> {
|
export class PictrsImage extends Component<PictrsImageProps, any> {
|
||||||
|
@ -23,37 +24,40 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { src, icon, iconOverlay, banner, thumbnail, nsfw, pushup, cardTop } =
|
||||||
|
this.props;
|
||||||
let user_blur_nsfw = true;
|
let user_blur_nsfw = true;
|
||||||
if (UserService.Instance.myUserInfo) {
|
if (UserService.Instance.myUserInfo) {
|
||||||
user_blur_nsfw =
|
user_blur_nsfw =
|
||||||
UserService.Instance.myUserInfo?.local_user_view.local_user.blur_nsfw;
|
UserService.Instance.myUserInfo?.local_user_view.local_user.blur_nsfw;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blur_image = this.props.nsfw && user_blur_nsfw;
|
const blur_image = nsfw && user_blur_nsfw;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<picture>
|
<picture>
|
||||||
<source srcSet={this.src("webp")} type="image/webp" />
|
<source srcSet={this.src("webp")} type="image/webp" />
|
||||||
<source srcSet={this.props.src} />
|
<source srcSet={src} />
|
||||||
<source srcSet={this.src("jpg")} type="image/jpeg" />
|
<source srcSet={this.src("jpg")} type="image/jpeg" />
|
||||||
<img
|
<img
|
||||||
src={this.props.src}
|
src={src}
|
||||||
alt={this.alt()}
|
alt={this.alt()}
|
||||||
title={this.alt()}
|
title={this.alt()}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
className={classNames("overflow-hidden pictrs-image", {
|
className={classNames("overflow-hidden pictrs-image", {
|
||||||
"img-fluid": !this.props.icon && !this.props.iconOverlay,
|
"img-fluid": !(icon || iconOverlay),
|
||||||
banner: this.props.banner,
|
banner,
|
||||||
"thumbnail rounded object-fit-cover":
|
"thumbnail rounded object-fit-cover":
|
||||||
this.props.thumbnail && !this.props.icon && !this.props.banner,
|
thumbnail && !(icon || banner),
|
||||||
"img-expanded slight-radius":
|
"img-expanded slight-radius": !(thumbnail || icon),
|
||||||
!this.props.thumbnail && !this.props.icon,
|
"img-blur": thumbnail && nsfw,
|
||||||
"img-blur-icon": this.props.icon && blur_image,
|
"object-fit-cover img-icon me-1": icon,
|
||||||
"img-blur-thumb": this.props.thumbnail && blur_image,
|
"img-blur-icon": icon && blur_image,
|
||||||
"object-fit-cover img-icon me-1": this.props.icon,
|
"img-blur-thumb": thumbnail && blur_image,
|
||||||
"ms-2 mb-0 rounded-circle object-fit-cover avatar-overlay":
|
"ms-2 mb-0 rounded-circle object-fit-cover avatar-overlay":
|
||||||
this.props.iconOverlay,
|
iconOverlay,
|
||||||
"avatar-pushup": this.props.pushup,
|
"avatar-pushup": pushup,
|
||||||
|
"card-img-top": cardTop,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</picture>
|
</picture>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
} from "inferno";
|
} from "inferno";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
import { Icon, Spinner } from "./icon";
|
import { Icon, Spinner } from "./icon";
|
||||||
|
import { LoadingEllipses } from "./loading-ellipses";
|
||||||
|
|
||||||
interface SearchableSelectProps {
|
interface SearchableSelectProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -22,7 +23,6 @@ interface SearchableSelectProps {
|
||||||
interface SearchableSelectState {
|
interface SearchableSelectState {
|
||||||
selectedIndex: number;
|
selectedIndex: number;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
loadingEllipses: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch(i: SearchableSelect, e: ChangeEvent<HTMLInputElement>) {
|
function handleSearch(i: SearchableSelect, e: ChangeEvent<HTMLInputElement>) {
|
||||||
|
@ -70,12 +70,10 @@ export class SearchableSelect extends Component<
|
||||||
> {
|
> {
|
||||||
searchInputRef: RefObject<HTMLInputElement> = createRef();
|
searchInputRef: RefObject<HTMLInputElement> = createRef();
|
||||||
toggleButtonRef: RefObject<HTMLButtonElement> = createRef();
|
toggleButtonRef: RefObject<HTMLButtonElement> = createRef();
|
||||||
private loadingEllipsesInterval?: NodeJS.Timer = undefined;
|
|
||||||
|
|
||||||
state: SearchableSelectState = {
|
state: SearchableSelectState = {
|
||||||
selectedIndex: 0,
|
selectedIndex: 0,
|
||||||
searchText: "",
|
searchText: "",
|
||||||
loadingEllipses: "...",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: SearchableSelectProps, context: any) {
|
constructor(props: SearchableSelectProps, context: any) {
|
||||||
|
@ -99,7 +97,7 @@ export class SearchableSelect extends Component<
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { id, options, onSearch, loading } = this.props;
|
const { id, options, onSearch, loading } = this.props;
|
||||||
const { searchText, selectedIndex, loadingEllipses } = this.state;
|
const { searchText, selectedIndex } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="searchable-select dropdown col-12 col-sm-auto flex-grow-1">
|
<div className="searchable-select dropdown col-12 col-sm-auto flex-grow-1">
|
||||||
|
@ -116,9 +114,14 @@ export class SearchableSelect extends Component<
|
||||||
onClick={linkEvent(this, focusSearch)}
|
onClick={linkEvent(this, focusSearch)}
|
||||||
ref={this.toggleButtonRef}
|
ref={this.toggleButtonRef}
|
||||||
>
|
>
|
||||||
{loading
|
{loading ? (
|
||||||
? `${I18NextService.i18n.t("loading")}${loadingEllipses}`
|
<>
|
||||||
: options[selectedIndex].label}
|
{I18NextService.i18n.t("loading")}
|
||||||
|
<LoadingEllipses />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
options[selectedIndex].label
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
<div className="modlog-choices-font-size dropdown-menu w-100 p-2">
|
<div className="modlog-choices-font-size dropdown-menu w-100 p-2">
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
|
@ -180,24 +183,4 @@ export class SearchableSelect extends Component<
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
const { loading } = this.props;
|
|
||||||
if (loading && !this.loadingEllipsesInterval) {
|
|
||||||
this.loadingEllipsesInterval = setInterval(() => {
|
|
||||||
this.setState(({ loadingEllipses }) => ({
|
|
||||||
loadingEllipses:
|
|
||||||
loadingEllipses.length === 3 ? "" : loadingEllipses + ".",
|
|
||||||
}));
|
|
||||||
}, 750);
|
|
||||||
} else if (!loading && this.loadingEllipsesInterval) {
|
|
||||||
clearInterval(this.loadingEllipsesInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.loadingEllipsesInterval) {
|
|
||||||
clearInterval(this.loadingEllipsesInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
227
src/shared/components/common/subscribe-button.tsx
Normal file
227
src/shared/components/common/subscribe-button.tsx
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
import { validInstanceTLD } from "@utils/helpers";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { NoOptionI18nKeys } from "i18next";
|
||||||
|
import { Component, MouseEventHandler, linkEvent } from "inferno";
|
||||||
|
import { CommunityView } from "lemmy-js-client";
|
||||||
|
import { I18NextService, UserService } from "../../services";
|
||||||
|
import { VERSION } from "../../version";
|
||||||
|
import { Icon, Spinner } from "./icon";
|
||||||
|
import { toast } from "../../toast";
|
||||||
|
|
||||||
|
interface SubscribeButtonProps {
|
||||||
|
communityView: CommunityView;
|
||||||
|
onFollow: MouseEventHandler;
|
||||||
|
onUnFollow: MouseEventHandler;
|
||||||
|
loading?: boolean;
|
||||||
|
isLink?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SubscribeButton({
|
||||||
|
communityView: {
|
||||||
|
subscribed,
|
||||||
|
community: { actor_id },
|
||||||
|
},
|
||||||
|
onFollow,
|
||||||
|
onUnFollow,
|
||||||
|
loading = false,
|
||||||
|
isLink = false,
|
||||||
|
}: SubscribeButtonProps) {
|
||||||
|
let i18key: NoOptionI18nKeys;
|
||||||
|
|
||||||
|
switch (subscribed) {
|
||||||
|
case "NotSubscribed": {
|
||||||
|
i18key = "subscribe";
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Subscribed": {
|
||||||
|
i18key = "joined";
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
i18key = "subscribe_pending";
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonClass = classNames(
|
||||||
|
"btn",
|
||||||
|
isLink ? "btn-link d-inline-block" : "d-block mb-2 w-100",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!UserService.Instance.myUserInfo) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames(buttonClass, {
|
||||||
|
"btn-secondary": !isLink,
|
||||||
|
})}
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#remoteFetchModal"
|
||||||
|
>
|
||||||
|
{I18NextService.i18n.t("subscribe")}
|
||||||
|
</button>
|
||||||
|
<RemoteFetchModal communityActorId={actor_id} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames(buttonClass, {
|
||||||
|
[`btn-${subscribed === "Pending" ? "warning" : "secondary"}`]: !isLink,
|
||||||
|
})}
|
||||||
|
onClick={subscribed === "NotSubscribed" ? onFollow : onUnFollow}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{subscribed === "Subscribed" && (
|
||||||
|
<Icon icon="check" classes="icon-inline me-1" />
|
||||||
|
)}
|
||||||
|
{I18NextService.i18n.t(i18key)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemoteFetchModalProps {
|
||||||
|
communityActorId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemoteFetchModalState {
|
||||||
|
instanceText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput(i: RemoteFetchModal, event: any) {
|
||||||
|
i.setState({ instanceText: event.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusInput() {
|
||||||
|
document.getElementById("remoteFetchInstance")?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitRemoteFollow(
|
||||||
|
{ state: { instanceText }, props: { communityActorId } }: RemoteFetchModal,
|
||||||
|
event: Event,
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
instanceText = instanceText.trim();
|
||||||
|
|
||||||
|
if (!validInstanceTLD(instanceText)) {
|
||||||
|
toast(
|
||||||
|
I18NextService.i18n.t("remote_follow_invalid_instance", {
|
||||||
|
instance: instanceText,
|
||||||
|
}),
|
||||||
|
"danger",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocolRegex = /^https?:\/\//;
|
||||||
|
if (instanceText.replace(protocolRegex, "") === window.location.host) {
|
||||||
|
toast(I18NextService.i18n.t("remote_follow_local_instance"), "danger");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!protocolRegex.test(instanceText)) {
|
||||||
|
instanceText = `http${VERSION !== "dev" ? "s" : ""}://${instanceText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = `${instanceText}/activitypub/externalInteraction?uri=${encodeURIComponent(
|
||||||
|
communityActorId,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteFetchModal extends Component<
|
||||||
|
RemoteFetchModalProps,
|
||||||
|
RemoteFetchModalState
|
||||||
|
> {
|
||||||
|
state: RemoteFetchModalState = {
|
||||||
|
instanceText: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: any, context: any) {
|
||||||
|
super(props, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document
|
||||||
|
.getElementById("remoteFetchModal")
|
||||||
|
?.addEventListener("shown.bs.modal", focusInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
document
|
||||||
|
.getElementById("remoteFetchModal")
|
||||||
|
?.removeEventListener("shown.bs.modal", focusInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="modal fade"
|
||||||
|
id="remoteFetchModal"
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-hidden
|
||||||
|
aria-labelledby="#remoteFetchModalTitle"
|
||||||
|
>
|
||||||
|
<div className="modal-dialog modal-fullscreen-sm-down">
|
||||||
|
<div className="modal-content">
|
||||||
|
<header className="modal-header">
|
||||||
|
<h3 className="modal-title" id="remoteFetchModalTitle">
|
||||||
|
{I18NextService.i18n.t("remote_follow_modal_title")}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
<form
|
||||||
|
id="remote-fetch-form"
|
||||||
|
className="modal-body d-flex flex-column justify-content-center"
|
||||||
|
onSubmit={linkEvent(this, submitRemoteFollow)}
|
||||||
|
>
|
||||||
|
<label className="form-label" htmlFor="remoteFetchInstance">
|
||||||
|
{I18NextService.i18n.t("remote_follow_prompt")}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="remoteFetchInstance"
|
||||||
|
className="form-control"
|
||||||
|
name="instance"
|
||||||
|
value={this.state.instanceText}
|
||||||
|
onInput={linkEvent(this, handleInput)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<footer className="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-danger"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
>
|
||||||
|
{I18NextService.i18n.t("cancel")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success"
|
||||||
|
form="remote-fetch-form"
|
||||||
|
>
|
||||||
|
{I18NextService.i18n.t("fetch_community")}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import { SortSelect } from "../common/sort-select";
|
||||||
import { CommunityLink } from "./community-link";
|
import { CommunityLink } from "./community-link";
|
||||||
|
|
||||||
import { communityLimit } from "../../config";
|
import { communityLimit } from "../../config";
|
||||||
|
import { SubscribeButton } from "../common/subscribe-button";
|
||||||
|
|
||||||
type CommunitiesData = RouteDataResponse<{
|
type CommunitiesData = RouteDataResponse<{
|
||||||
listCommunitiesResponse: ListCommunitiesResponse;
|
listCommunitiesResponse: ListCommunitiesResponse;
|
||||||
|
@ -173,41 +174,26 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
{numToSI(cv.counts.comments)}
|
{numToSI(cv.counts.comments)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-right">
|
<td className="text-right">
|
||||||
{cv.subscribed === "Subscribed" && (
|
<SubscribeButton
|
||||||
<button
|
communityView={cv}
|
||||||
className="btn btn-link d-inline-block"
|
onFollow={linkEvent(
|
||||||
onClick={linkEvent(
|
{
|
||||||
{
|
i: this,
|
||||||
i: this,
|
communityId: cv.community.id,
|
||||||
communityId: cv.community.id,
|
follow: false,
|
||||||
follow: false,
|
},
|
||||||
},
|
this.handleFollow,
|
||||||
this.handleFollow,
|
)}
|
||||||
)}
|
onUnFollow={linkEvent(
|
||||||
>
|
{
|
||||||
{I18NextService.i18n.t("unsubscribe")}
|
i: this,
|
||||||
</button>
|
communityId: cv.community.id,
|
||||||
)}
|
follow: true,
|
||||||
{cv.subscribed === "NotSubscribed" && (
|
},
|
||||||
<button
|
this.handleFollow,
|
||||||
className="btn btn-link d-inline-block"
|
)}
|
||||||
onClick={linkEvent(
|
isLink
|
||||||
{
|
/>
|
||||||
i: this,
|
|
||||||
communityId: cv.community.id,
|
|
||||||
follow: true,
|
|
||||||
},
|
|
||||||
this.handleFollow,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("subscribe")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{cv.subscribed === "Pending" && (
|
|
||||||
<div className="text-warning d-inline-block">
|
|
||||||
{I18NextService.i18n.t("subscribe_pending")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
),
|
),
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { I18NextService, UserService } from "../../services";
|
||||||
import { Badges } from "../common/badges";
|
import { Badges } from "../common/badges";
|
||||||
import { BannerIconHeader } from "../common/banner-icon-header";
|
import { BannerIconHeader } from "../common/banner-icon-header";
|
||||||
import { Icon, PurgeWarning, Spinner } from "../common/icon";
|
import { Icon, PurgeWarning, Spinner } from "../common/icon";
|
||||||
|
import { SubscribeButton } from "../common/subscribe-button";
|
||||||
import { CommunityForm } from "../community/community-form";
|
import { CommunityForm } from "../community/community-form";
|
||||||
import { CommunityLink } from "../community/community-link";
|
import { CommunityLink } from "../community/community-link";
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
|
@ -122,7 +123,9 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
|
|
||||||
sidebar() {
|
sidebar() {
|
||||||
const myUSerInfo = UserService.Instance.myUserInfo;
|
const myUSerInfo = UserService.Instance.myUserInfo;
|
||||||
const { name, actor_id } = this.props.community_view.community;
|
const {
|
||||||
|
community: { name, actor_id },
|
||||||
|
} = this.props.community_view;
|
||||||
return (
|
return (
|
||||||
<aside className="mb-3">
|
<aside className="mb-3">
|
||||||
<div id="sidebarContainer">
|
<div id="sidebarContainer">
|
||||||
|
@ -130,7 +133,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{this.communityTitle()}
|
{this.communityTitle()}
|
||||||
{this.props.editable && this.adminButtons()}
|
{this.props.editable && this.adminButtons()}
|
||||||
{myUSerInfo && this.subscribe()}
|
<SubscribeButton
|
||||||
|
communityView={this.props.community_view}
|
||||||
|
onFollow={linkEvent(this, this.handleFollowCommunity)}
|
||||||
|
onUnFollow={linkEvent(this, this.handleUnfollowCommunity)}
|
||||||
|
loading={this.state.followCommunityLoading}
|
||||||
|
/>
|
||||||
{this.canPost && this.createPost()}
|
{this.canPost && this.createPost()}
|
||||||
{myUSerInfo && this.blockCommunity()}
|
{myUSerInfo && this.blockCommunity()}
|
||||||
{!myUSerInfo && (
|
{!myUSerInfo && (
|
||||||
|
@ -229,58 +237,6 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe() {
|
|
||||||
const community_view = this.props.community_view;
|
|
||||||
|
|
||||||
if (community_view.subscribed === "NotSubscribed") {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="btn btn-secondary d-block mb-2 w-100"
|
|
||||||
onClick={linkEvent(this, this.handleFollowCommunity)}
|
|
||||||
>
|
|
||||||
{this.state.followCommunityLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
I18NextService.i18n.t("subscribe")
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (community_view.subscribed === "Subscribed") {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="btn btn-secondary d-block mb-2 w-100"
|
|
||||||
onClick={linkEvent(this, this.handleUnfollowCommunity)}
|
|
||||||
>
|
|
||||||
{this.state.followCommunityLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon icon="check" classes="icon-inline me-1" />
|
|
||||||
{I18NextService.i18n.t("joined")}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (community_view.subscribed === "Pending") {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="btn btn-warning d-block mb-2 w-100"
|
|
||||||
onClick={linkEvent(this, this.handleUnfollowCommunity)}
|
|
||||||
>
|
|
||||||
{this.state.followCommunityLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
I18NextService.i18n.t("subscribe_pending")
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blockCommunity() {
|
blockCommunity() {
|
||||||
const { subscribed, blocked } = this.props.community_view;
|
const { subscribed, blocked } = this.props.community_view;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { setIsoData } from "@utils/app";
|
import { setIsoData } from "@utils/app";
|
||||||
import { isBrowser } from "@utils/browser";
|
import { isBrowser } from "@utils/browser";
|
||||||
|
import { getQueryParams } from "@utils/helpers";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
|
import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
|
||||||
import { I18NextService, UserService } from "../../services";
|
import { I18NextService, UserService } from "../../services";
|
||||||
import { HttpService, RequestState } from "../../services/HttpService";
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
|
@ -9,6 +11,17 @@ import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
import PasswordInput from "../common/password-input";
|
import PasswordInput from "../common/password-input";
|
||||||
|
|
||||||
|
interface LoginProps {
|
||||||
|
prev?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLoginQueryParams = () =>
|
||||||
|
getQueryParams<LoginProps>({
|
||||||
|
prev(param) {
|
||||||
|
return param ? decodeURIComponent(param) : undefined;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
loginRes: RequestState<LoginResponse>;
|
loginRes: RequestState<LoginResponse>;
|
||||||
form: {
|
form: {
|
||||||
|
@ -20,7 +33,73 @@ interface State {
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Login extends Component<any, State> {
|
async function handleLoginSubmit(i: Login, event: any) {
|
||||||
|
event.preventDefault();
|
||||||
|
const { password, totp_2fa_token, username_or_email } = i.state.form;
|
||||||
|
|
||||||
|
if (username_or_email && password) {
|
||||||
|
i.setState({ loginRes: { state: "loading" } });
|
||||||
|
|
||||||
|
const loginRes = await HttpService.client.login({
|
||||||
|
username_or_email,
|
||||||
|
password,
|
||||||
|
totp_2fa_token,
|
||||||
|
});
|
||||||
|
switch (loginRes.state) {
|
||||||
|
case "failed": {
|
||||||
|
if (loginRes.msg === "missing_totp_token") {
|
||||||
|
i.setState({ showTotp: true });
|
||||||
|
toast(I18NextService.i18n.t("enter_two_factor_code"), "info");
|
||||||
|
} else {
|
||||||
|
toast(I18NextService.i18n.t(loginRes.msg), "danger");
|
||||||
|
}
|
||||||
|
|
||||||
|
i.setState({ loginRes: { state: "failed", msg: loginRes.msg } });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "success": {
|
||||||
|
UserService.Instance.login({
|
||||||
|
res: loginRes.data,
|
||||||
|
});
|
||||||
|
const site = await HttpService.client.getSite();
|
||||||
|
|
||||||
|
if (site.state === "success") {
|
||||||
|
UserService.Instance.myUserInfo = site.data.my_user;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { prev } = getLoginQueryParams();
|
||||||
|
|
||||||
|
prev
|
||||||
|
? i.props.history.replace(prev)
|
||||||
|
: i.props.history.action === "PUSH"
|
||||||
|
? i.props.history.back()
|
||||||
|
: i.props.history.replace("/");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLoginUsernameChange(i: Login, event: any) {
|
||||||
|
i.setState(
|
||||||
|
prevState => (prevState.form.username_or_email = event.target.value.trim()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLoginTotpChange(i: Login, event: any) {
|
||||||
|
i.setState(prevState => (prevState.form.totp_2fa_token = event.target.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLoginPasswordChange(i: Login, event: any) {
|
||||||
|
i.setState(prevState => (prevState.form.password = event.target.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Login extends Component<
|
||||||
|
RouteComponentProps<Record<string, never>>,
|
||||||
|
State
|
||||||
|
> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
|
@ -68,7 +147,7 @@ export class Login extends Component<any, State> {
|
||||||
loginForm() {
|
loginForm() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
|
<form onSubmit={linkEvent(this, handleLoginSubmit)}>
|
||||||
<h1 className="h4 mb-4">{I18NextService.i18n.t("login")}</h1>
|
<h1 className="h4 mb-4">{I18NextService.i18n.t("login")}</h1>
|
||||||
<div className="mb-3 row">
|
<div className="mb-3 row">
|
||||||
<label
|
<label
|
||||||
|
@ -83,7 +162,7 @@ export class Login extends Component<any, State> {
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="login-email-or-username"
|
id="login-email-or-username"
|
||||||
value={this.state.form.username_or_email}
|
value={this.state.form.username_or_email}
|
||||||
onInput={linkEvent(this, this.handleLoginUsernameChange)}
|
onInput={linkEvent(this, handleLoginUsernameChange)}
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
required
|
required
|
||||||
minLength={3}
|
minLength={3}
|
||||||
|
@ -94,7 +173,7 @@ export class Login extends Component<any, State> {
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
id="login-password"
|
id="login-password"
|
||||||
value={this.state.form.password}
|
value={this.state.form.password}
|
||||||
onInput={linkEvent(this, this.handleLoginPasswordChange)}
|
onInput={linkEvent(this, handleLoginPasswordChange)}
|
||||||
label={I18NextService.i18n.t("password")}
|
label={I18NextService.i18n.t("password")}
|
||||||
showForgotLink
|
showForgotLink
|
||||||
/>
|
/>
|
||||||
|
@ -116,7 +195,7 @@ export class Login extends Component<any, State> {
|
||||||
pattern="[0-9]*"
|
pattern="[0-9]*"
|
||||||
autoComplete="one-time-code"
|
autoComplete="one-time-code"
|
||||||
value={this.state.form.totp_2fa_token}
|
value={this.state.form.totp_2fa_token}
|
||||||
onInput={linkEvent(this, this.handleLoginTotpChange)}
|
onInput={linkEvent(this, handleLoginTotpChange)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -136,64 +215,4 @@ export class Login extends Component<any, State> {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLoginSubmit(i: Login, event: any) {
|
|
||||||
event.preventDefault();
|
|
||||||
const { password, totp_2fa_token, username_or_email } = i.state.form;
|
|
||||||
|
|
||||||
if (username_or_email && password) {
|
|
||||||
i.setState({ loginRes: { state: "loading" } });
|
|
||||||
|
|
||||||
const loginRes = await HttpService.client.login({
|
|
||||||
username_or_email,
|
|
||||||
password,
|
|
||||||
totp_2fa_token,
|
|
||||||
});
|
|
||||||
switch (loginRes.state) {
|
|
||||||
case "failed": {
|
|
||||||
if (loginRes.msg === "missing_totp_token") {
|
|
||||||
i.setState({ showTotp: true });
|
|
||||||
toast(I18NextService.i18n.t("enter_two_factor_code"), "info");
|
|
||||||
} else {
|
|
||||||
toast(I18NextService.i18n.t(loginRes.msg), "danger");
|
|
||||||
}
|
|
||||||
|
|
||||||
i.setState({ loginRes: { state: "failed", msg: loginRes.msg } });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "success": {
|
|
||||||
UserService.Instance.login({
|
|
||||||
res: loginRes.data,
|
|
||||||
});
|
|
||||||
const site = await HttpService.client.getSite();
|
|
||||||
|
|
||||||
if (site.state === "success") {
|
|
||||||
UserService.Instance.myUserInfo = site.data.my_user;
|
|
||||||
}
|
|
||||||
|
|
||||||
i.props.history.action === "PUSH"
|
|
||||||
? i.props.history.back()
|
|
||||||
: i.props.history.replace("/");
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLoginUsernameChange(i: Login, event: any) {
|
|
||||||
i.state.form.username_or_email = event.target.value.trim();
|
|
||||||
i.setState(i.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLoginTotpChange(i: Login, event: any) {
|
|
||||||
i.state.form.totp_2fa_token = event.target.value;
|
|
||||||
i.setState(i.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLoginPasswordChange(i: Login, event: any) {
|
|
||||||
i.state.form.password = event.target.value;
|
|
||||||
i.setState(i.state);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
221
src/shared/components/remote-fetch.tsx
Normal file
221
src/shared/components/remote-fetch.tsx
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
import { setIsoData } from "@utils/app";
|
||||||
|
import { getQueryParams } from "@utils/helpers";
|
||||||
|
import { QueryParams, RouteDataResponse } from "@utils/types";
|
||||||
|
import { Component, linkEvent } from "inferno";
|
||||||
|
import { CommunityView, ResolveObjectResponse } from "lemmy-js-client";
|
||||||
|
import { InitialFetchRequest } from "../interfaces";
|
||||||
|
import { FirstLoadService, HttpService, I18NextService } from "../services";
|
||||||
|
import { RequestState } from "../services/HttpService";
|
||||||
|
import { HtmlTags } from "./common/html-tags";
|
||||||
|
import { Spinner } from "./common/icon";
|
||||||
|
import { LoadingEllipses } from "./common/loading-ellipses";
|
||||||
|
import { PictrsImage } from "./common/pictrs-image";
|
||||||
|
import { SubscribeButton } from "./common/subscribe-button";
|
||||||
|
import { CommunityLink } from "./community/community-link";
|
||||||
|
|
||||||
|
interface RemoteFetchProps {
|
||||||
|
uri?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoteFetchData = RouteDataResponse<{
|
||||||
|
resolveObjectRes: ResolveObjectResponse;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
interface RemoteFetchState {
|
||||||
|
resolveObjectRes: RequestState<ResolveObjectResponse>;
|
||||||
|
isIsomorphic: boolean;
|
||||||
|
followCommunityLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUriFromQuery = (uri?: string): string | undefined =>
|
||||||
|
uri ? decodeURIComponent(uri) : undefined;
|
||||||
|
|
||||||
|
const getRemoteFetchQueryParams = () =>
|
||||||
|
getQueryParams<RemoteFetchProps>({
|
||||||
|
uri: getUriFromQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
function uriToQuery(uri: string) {
|
||||||
|
const match = decodeURIComponent(uri).match(/https?:\/\/(.+)\/c\/(.+)/);
|
||||||
|
|
||||||
|
return match ? `!${match[2]}@${match[1]}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleToggleFollow(i: RemoteFetch, follow: boolean) {
|
||||||
|
const { resolveObjectRes } = i.state;
|
||||||
|
if (resolveObjectRes.state === "success" && resolveObjectRes.data.community) {
|
||||||
|
i.setState({
|
||||||
|
followCommunityLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const communityRes = await HttpService.client.followCommunity({
|
||||||
|
community_id: resolveObjectRes.data.community.community.id,
|
||||||
|
follow,
|
||||||
|
});
|
||||||
|
|
||||||
|
i.setState(prev => {
|
||||||
|
if (
|
||||||
|
communityRes.state === "success" &&
|
||||||
|
prev.resolveObjectRes.state === "success" &&
|
||||||
|
prev.resolveObjectRes.data.community
|
||||||
|
) {
|
||||||
|
prev.resolveObjectRes.data.community = communityRes.data.community_view;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
followCommunityLoading: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFollow = (i: RemoteFetch) => handleToggleFollow(i, true);
|
||||||
|
const handleUnfollow = (i: RemoteFetch) => handleToggleFollow(i, false);
|
||||||
|
|
||||||
|
export class RemoteFetch extends Component<any, RemoteFetchState> {
|
||||||
|
private isoData = setIsoData<RemoteFetchData>(this.context);
|
||||||
|
state: RemoteFetchState = {
|
||||||
|
resolveObjectRes: { state: "empty" },
|
||||||
|
isIsomorphic: false,
|
||||||
|
followCommunityLoading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: any, context: any) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
if (FirstLoadService.isFirstLoad) {
|
||||||
|
const { resolveObjectRes } = this.isoData.routeData;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
...this.state,
|
||||||
|
isIsomorphic: true,
|
||||||
|
resolveObjectRes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
if (!this.state.isIsomorphic) {
|
||||||
|
const { uri } = getRemoteFetchQueryParams();
|
||||||
|
|
||||||
|
if (uri) {
|
||||||
|
this.setState({ resolveObjectRes: { state: "loading" } });
|
||||||
|
this.setState({
|
||||||
|
resolveObjectRes: await HttpService.client.resolveObject({
|
||||||
|
q: uriToQuery(uri),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="remote-fetch container-lg">
|
||||||
|
<HtmlTags
|
||||||
|
title={this.documentTitle}
|
||||||
|
path={this.context.router.route.match.url}
|
||||||
|
/>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-12 col-lg-6 offset-lg-3 text-center">
|
||||||
|
{this.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get content() {
|
||||||
|
const res = this.state.resolveObjectRes;
|
||||||
|
|
||||||
|
const { uri } = getRemoteFetchQueryParams();
|
||||||
|
const remoteCommunityName = uri ? uriToQuery(uri) : "remote community";
|
||||||
|
|
||||||
|
switch (res.state) {
|
||||||
|
case "success": {
|
||||||
|
const communityView = res.data.community as CommunityView;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>{I18NextService.i18n.t("community_federated")}</h1>
|
||||||
|
<div className="card mt-5">
|
||||||
|
{communityView.community.banner && (
|
||||||
|
<PictrsImage src={communityView.community.banner} cardTop />
|
||||||
|
)}
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">
|
||||||
|
<CommunityLink community={communityView.community} />
|
||||||
|
</h2>
|
||||||
|
{communityView.community.description && (
|
||||||
|
<div className="card-text mb-3 preview-lines">
|
||||||
|
{communityView.community.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<SubscribeButton
|
||||||
|
communityView={communityView}
|
||||||
|
onFollow={linkEvent(this, handleFollow)}
|
||||||
|
onUnFollow={linkEvent(this, handleUnfollow)}
|
||||||
|
loading={this.state.followCommunityLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "loading": {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>
|
||||||
|
{I18NextService.i18n.t("fetching_community", {
|
||||||
|
community: remoteCommunityName,
|
||||||
|
})}
|
||||||
|
<LoadingEllipses />
|
||||||
|
</h1>
|
||||||
|
<h5>
|
||||||
|
<Spinner large />
|
||||||
|
</h5>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>
|
||||||
|
{I18NextService.i18n.t("could_not_fetch_community", {
|
||||||
|
community: remoteCommunityName,
|
||||||
|
})}
|
||||||
|
</h1>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
const { uri } = getRemoteFetchQueryParams();
|
||||||
|
const name = this.isoData.site_res.site_view.site.name;
|
||||||
|
return `${I18NextService.i18n.t("remote_follow")} - ${
|
||||||
|
uri ? `${uri} - ` : ""
|
||||||
|
}${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fetchInitialData({
|
||||||
|
auth,
|
||||||
|
client,
|
||||||
|
query: { uri },
|
||||||
|
}: InitialFetchRequest<
|
||||||
|
QueryParams<RemoteFetchProps>
|
||||||
|
>): Promise<RemoteFetchData> {
|
||||||
|
const data: RemoteFetchData = { resolveObjectRes: { state: "empty" } };
|
||||||
|
|
||||||
|
if (uri && auth) {
|
||||||
|
data.resolveObjectRes = await client.resolveObject({
|
||||||
|
q: uriToQuery(uri),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ export interface InitialFetchRequest<T extends ParsedQs = ParsedQs> {
|
||||||
path: string;
|
path: string;
|
||||||
query: T;
|
query: T;
|
||||||
site: GetSiteResponse;
|
site: GetSiteResponse;
|
||||||
|
auth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PostFormParams {
|
export interface PostFormParams {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { VerifyEmail } from "./components/person/verify-email";
|
||||||
import { CreatePost } from "./components/post/create-post";
|
import { CreatePost } from "./components/post/create-post";
|
||||||
import { Post } from "./components/post/post";
|
import { Post } from "./components/post/post";
|
||||||
import { CreatePrivateMessage } from "./components/private_message/create-private-message";
|
import { CreatePrivateMessage } from "./components/private_message/create-private-message";
|
||||||
|
import { RemoteFetch } from "./components/remote-fetch";
|
||||||
import { Search } from "./components/search";
|
import { Search } from "./components/search";
|
||||||
import { InitialFetchRequest, RouteData } from "./interfaces";
|
import { InitialFetchRequest, RouteData } from "./interfaces";
|
||||||
|
|
||||||
|
@ -140,4 +141,9 @@ export const routes: IRoutePropsWithFetch<Record<string, any>>[] = [
|
||||||
fetchInitialData: Instances.fetchInitialData,
|
fetchInitialData: Instances.fetchInitialData,
|
||||||
},
|
},
|
||||||
{ path: `/legal`, component: Legal },
|
{ path: `/legal`, component: Legal },
|
||||||
|
{
|
||||||
|
path: "/activitypub/externalInteraction",
|
||||||
|
component: RemoteFetch,
|
||||||
|
fetchInitialData: RemoteFetch.fetchInitialData,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export default function isAuthPath(pathname: string) {
|
export default function isAuthPath(pathname: string) {
|
||||||
return /^\/(create_.*?|inbox|settings|admin|reports|registration_applications)\b/g.test(
|
return /^\/(create_.*?|inbox|settings|admin|reports|registration_applications|activitypub.*?)\b/g.test(
|
||||||
pathname,
|
pathname,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,5 +4,5 @@ export default function isAdmin(
|
||||||
creatorId: number,
|
creatorId: number,
|
||||||
admins?: PersonView[],
|
admins?: PersonView[],
|
||||||
): boolean {
|
): boolean {
|
||||||
return admins?.map(a => a.person.id).includes(creatorId) ?? false;
|
return admins?.some(({ person: { id } }) => id === creatorId) ?? false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export const VERSION = "unknown version";
|
export const VERSION = "unknown version" as string;
|
||||||
|
|
|
@ -148,7 +148,7 @@ module.exports = (env, argv) => {
|
||||||
|
|
||||||
const RunNodeWebpackPlugin = require("run-node-webpack-plugin");
|
const RunNodeWebpackPlugin = require("run-node-webpack-plugin");
|
||||||
serverConfig.plugins.push(
|
serverConfig.plugins.push(
|
||||||
new RunNodeWebpackPlugin({ runOnlyInWatchMode: true })
|
new RunNodeWebpackPlugin({ runOnlyInWatchMode: true }),
|
||||||
);
|
);
|
||||||
} else if (mode === "none") {
|
} else if (mode === "none") {
|
||||||
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
|
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
|
||||||
|
|
Loading…
Reference in a new issue