Halfway done with redux.

This commit is contained in:
Dessalines 2023-12-06 18:11:12 -05:00
parent 4298c3fb91
commit dc2e636f61
37 changed files with 260 additions and 125 deletions

View file

@ -16,7 +16,7 @@ async function startClient() {
await setupDateFns();
const store = setupRedux(windowData);
const store = setupRedux(windowData!);
const wrapper = (
<Provider store={store}>

View file

@ -1,4 +1,4 @@
import { initializeSite, isAuthPath } from "@utils/app";
import { initializeSite, isAuthPath, setupRedux } from "@utils/app";
import { getHttpBaseInternal } from "@utils/env";
import { ErrorPageData } from "@utils/types";
import type { Request, Response } from "express";
@ -21,7 +21,6 @@ import { getErrorPageData } from "../utils/get-error-page-data";
import { setForwardedHeaders } from "../utils/set-forwarded-headers";
import { getJwtCookie } from "../utils/has-jwt-cookie";
import { Provider } from "inferno-redux";
import { configureStore, createSlice } from "@reduxjs/toolkit";
export default async (req: Request, res: Response) => {
try {
@ -110,12 +109,7 @@ export default async (req: Request, res: Response) => {
errorPageData,
};
const slice = createSlice({
name: "isoData",
initialState: { value: isoData },
reducers: {},
});
const store = configureStore({ reducer: slice.reducer });
const store = setupRedux(isoData);
const wrapper = (
<Provider store={store}>

View file

@ -17,6 +17,9 @@ import AnonymousGuard from "../common/anonymous-guard";
import { CodeTheme } from "./code-theme";
export class App extends Component<any, any> {
get isoData(): IsoDataOptionalSite {
return this.context.store.getState().value;
}
private readonly mainContentRef: RefObject<HTMLElement>;
constructor(props: any, context: any) {
super(props, context);
@ -29,8 +32,7 @@ export class App extends Component<any, any> {
}
render() {
const reduxState: IsoDataOptionalSite = this.context.store.getState().value;
const siteRes = reduxState.site_res;
const siteRes = this.isoData.site_res;
const siteView = siteRes?.site_view;
return (
@ -73,11 +75,16 @@ export class App extends Component<any, any> {
<div tabIndex={-1}>
{RouteComponent &&
(isAuthPath(path ?? "") ? (
<AuthGuard {...routeProps}>
<AuthGuard
isLoggedIn={!!siteRes?.my_user}
componentProps={routeProps}
>
<RouteComponent {...routeProps} />
</AuthGuard>
) : isAnonymousPath(path ?? "") ? (
<AnonymousGuard>
<AnonymousGuard
isLoggedIn={!!siteRes?.my_user}
>
<RouteComponent {...routeProps} />
</AnonymousGuard>
) : (

View file

@ -38,7 +38,7 @@ function handleCollapseClick(i: Navbar) {
}
function handleLogOut(i: Navbar) {
HttpService._Instance.logout();
HttpService.logout();
handleCollapseClick(i);
}
@ -110,7 +110,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
>
{siteView?.site.icon &&
showAvatars(this.props.siteRes?.my_user) && (
<PictrsImage src={siteView.site.icon} icon />
<PictrsImage
src={siteView.site.icon}
icon
myUserInfo={this.props.siteRes?.my_user}
/>
)}
{siteView?.site.name}
</NavLink>
@ -379,8 +383,13 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
aria-expanded="false"
data-bs-toggle="dropdown"
>
{showAvatars() && person.avatar && (
<PictrsImage src={person.avatar} icon />
{showAvatars(this.props.siteRes?.my_user) &&
person.avatar && (
<PictrsImage
src={person.avatar}
icon
myUserInfo={this.props.siteRes?.my_user}
/>
)}
{person.display_name ?? person.name}
</button>

View file

@ -1,12 +1,17 @@
import { Component } from "inferno";
import { UserService } from "../../services";
import { Spinner } from "./icon";
interface AnonymousGuardProps {
isLoggedIn: boolean;
}
interface AnonymousGuardState {
hasRedirected: boolean;
}
class AnonymousGuard extends Component<any, AnonymousGuardState> {
class AnonymousGuard extends Component<
AnonymousGuardProps,
AnonymousGuardState
> {
state = {
hasRedirected: false,
} as AnonymousGuardState;
@ -16,7 +21,7 @@ class AnonymousGuard extends Component<any, AnonymousGuardState> {
}
componentDidMount() {
if (UserService.Instance.myUserInfo) {
if (this.props.isLoggedIn) {
this.context.router.history.replace(`/`);
} else {
this.setState({ hasRedirected: true });

View file

@ -1,30 +1,28 @@
import { Component } from "inferno";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { UserService } from "../../services";
import { Spinner } from "./icon";
interface AuthGuardProps {
componentProps: RouteComponentProps<Record<string, string>>;
isLoggedIn: boolean;
}
interface AuthGuardState {
hasRedirected: boolean;
}
class AuthGuard extends Component<
RouteComponentProps<Record<string, string>>,
AuthGuardState
> {
class AuthGuard extends Component<AuthGuardProps, AuthGuardState> {
state = {
hasRedirected: false,
} as AuthGuardState;
constructor(
props: RouteComponentProps<Record<string, string>>,
context: any,
) {
constructor(props: AuthGuardProps, context: any) {
super(props, context);
}
componentDidMount() {
if (!UserService.Instance.myUserInfo) {
const { pathname, search } = this.props.location;
if (!this.props.isLoggedIn) {
const { pathname, search } = this.props.componentProps.location;
this.context.router.history.replace(
`/login?prev=${encodeURIComponent(pathname + search)}`,
);

View file

@ -1,9 +1,11 @@
import { Component } from "inferno";
import { PictrsImage } from "./pictrs-image";
import { MyUserInfo } from "lemmy-js-client";
interface BannerIconHeaderProps {
banner?: string;
icon?: string;
myUserInfo?: MyUserInfo;
}
export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
@ -17,13 +19,21 @@ export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
return (
(banner || icon) && (
<div className="banner-icon-header position-relative mb-2">
{banner && <PictrsImage src={banner} banner alt="" />}
{banner && (
<PictrsImage
src={banner}
banner
alt=""
myUserInfo={this.props.myUserInfo}
/>
)}
{icon && (
<PictrsImage
src={icon}
iconOverlay
pushup={!!this.props.banner}
alt=""
myUserInfo={this.props.myUserInfo}
/>
)}
</div>

View file

@ -1,7 +1,7 @@
import { randomStr } from "@utils/helpers";
import classNames from "classnames";
import { Component, linkEvent } from "inferno";
import { HttpService, I18NextService, UserService } from "../../services";
import { HttpService, I18NextService } from "../../services";
import { toast } from "../../toast";
import { Icon } from "./icon";
@ -11,6 +11,7 @@ interface ImageUploadFormProps {
onUpload(url: string): any;
onRemove(): any;
rounded?: boolean;
isLoggedIn: boolean;
}
interface ImageUploadFormState {
@ -63,7 +64,7 @@ export class ImageUploadForm extends Component<
accept="image/*,video/*"
className="small form-control"
name={this.id}
disabled={!UserService.Instance.myUserInfo}
disabled={!this.props.isLoggedIn}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>

View file

@ -1,14 +1,16 @@
import { randomStr } from "@utils/helpers";
import classNames from "classnames";
import { Component, linkEvent } from "inferno";
import { ListingType } from "lemmy-js-client";
import { I18NextService, UserService } from "../../services";
import { ListingType, MyUserInfo } from "lemmy-js-client";
import { I18NextService } from "../../services";
import { moderatesSomething } from "@utils/roles";
interface ListingTypeSelectProps {
type_: ListingType;
showLocal: boolean;
showSubscribed: boolean;
onChange(val: ListingType): void;
myUserInfo?: MyUserInfo;
}
interface ListingTypeSelectState {
@ -52,15 +54,15 @@ export class ListingTypeSelect extends Component<
value={"Subscribed"}
checked={this.state.type_ === "Subscribed"}
onChange={linkEvent(this, this.handleTypeChange)}
disabled={!UserService.Instance.myUserInfo}
disabled={!this.props.myUserInfo}
/>
<label
htmlFor={`${this.id}-subscribed`}
title={I18NextService.i18n.t("subscribed_description")}
className={classNames("btn btn-outline-secondary", {
active: this.state.type_ === "Subscribed",
disabled: !UserService.Instance.myUserInfo,
pointer: UserService.Instance.myUserInfo,
disabled: !this.props.myUserInfo,
pointer: !!this.props.myUserInfo,
})}
>
{I18NextService.i18n.t("subscribed")}
@ -107,7 +109,7 @@ export class ListingTypeSelect extends Component<
>
{I18NextService.i18n.t("all")}
</label>
{(UserService.Instance.myUserInfo?.moderates.length ?? 0) > 0 && (
{moderatesSomething(this.props.myUserInfo) && (
<>
<input
id={`${this.id}-moderator-view`}

View file

@ -1,7 +1,6 @@
import classNames from "classnames";
import { Component } from "inferno";
import { UserService } from "../../services";
import { MyUserInfo } from "lemmy-js-client";
const iconThumbnailSize = 96;
const thumbnailSize = 256;
@ -16,6 +15,7 @@ interface PictrsImageProps {
iconOverlay?: boolean;
pushup?: boolean;
cardTop?: boolean;
myUserInfo?: MyUserInfo;
}
export class PictrsImage extends Component<PictrsImageProps, any> {
@ -27,9 +27,9 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
const { src, icon, iconOverlay, banner, thumbnail, nsfw, pushup, cardTop } =
this.props;
let user_blur_nsfw = true;
if (UserService.Instance.myUserInfo) {
if (this.props.myUserInfo) {
user_blur_nsfw =
UserService.Instance.myUserInfo?.local_user_view.local_user.blur_nsfw;
this.props.myUserInfo?.local_user_view.local_user.blur_nsfw;
}
const blur_image = nsfw && user_blur_nsfw;

View file

@ -3,12 +3,13 @@ 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 { I18NextService } from "../../services";
import { VERSION } from "../../version";
import { Icon, Spinner } from "./icon";
import { toast } from "../../toast";
interface SubscribeButtonProps {
loggedIn: boolean;
communityView: CommunityView;
onFollow: MouseEventHandler;
onUnFollow: MouseEventHandler;
@ -25,6 +26,7 @@ export function SubscribeButton({
onUnFollow,
loading = false,
isLink = false,
loggedIn,
}: SubscribeButtonProps) {
let i18key: NoOptionI18nKeys;
@ -51,7 +53,7 @@ export function SubscribeButton({
isLink ? "btn-link d-inline-block" : "d-block mb-2 w-100",
);
if (!UserService.Instance.myUserInfo) {
if (!loggedIn) {
return (
<>
<button

View file

@ -186,6 +186,7 @@ export class Communities extends Component<any, CommunitiesState> {
<td className="text-right">
<SubscribeButton
communityView={cv}
loggedIn={!!this.isoData.site_res.my_user}
onFollow={linkEvent(
{
i: this,

View file

@ -6,6 +6,7 @@ import {
CreateCommunity,
EditCommunity,
Language,
MyUserInfo,
} from "lemmy-js-client";
import { I18NextService } from "../../services";
import { Icon, Spinner } from "../common/icon";
@ -15,6 +16,7 @@ import { MarkdownTextArea } from "../common/markdown-textarea";
interface CommunityFormProps {
community_view?: CommunityView; // If a community is given, that means this is an edit
myUserInfo?: MyUserInfo;
allLanguages: Language[];
siteLanguages: number[];
communityLanguages?: number[];
@ -166,6 +168,7 @@ export class CommunityForm extends Component<
imageSrc={this.state.form.icon}
onUpload={this.handleIconUpload}
onRemove={this.handleIconRemove}
isLoggedIn={!!this.props.myUserInfo}
rounded
/>
</div>
@ -180,6 +183,7 @@ export class CommunityForm extends Component<
imageSrc={this.state.form.banner}
onUpload={this.handleBannerUpload}
onRemove={this.handleBannerRemove}
isLoggedIn={!!this.props.myUserInfo}
/>
</div>
</div>

View file

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

View file

@ -507,10 +507,15 @@ export class Community extends Component<
return (
community && (
<div className="mb-2">
<BannerIconHeader banner={community.banner} icon={community.icon} />
<BannerIconHeader
banner={community.banner}
icon={community.icon}
myUserInfo={this.isoData.site_res.my_user}
/>
<h1 className="h4 mb-0 overflow-wrap-anywhere">{community.title}</h1>
<CommunityLink
community={community}
myUserInfo={this.isoData.site_res.my_user}
realLink
useApubName
muted

View file

@ -46,6 +46,7 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
{I18NextService.i18n.t("create_community")}
</h1>
<CommunityForm
myUserInfo={this.isoData.site_res.my_user}
onUpsertCommunity={this.handleCommunityCreate}
enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}

View file

@ -110,6 +110,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
this.sidebar()
) : (
<CommunityForm
myUserInfo={this.props.myUserInfo}
community_view={this.props.community_view}
allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages}
@ -136,6 +137,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{this.communityTitle()}
{this.props.editable && this.adminButtons()}
<SubscribeButton
loggedIn={!!this.props.myUserInfo}
communityView={this.props.community_view}
onFollow={linkEvent(this, this.handleFollowCommunity)}
onUnFollow={linkEvent(this, this.handleUnfollowCommunity)}
@ -180,7 +182,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<div>
<h2 className="h5 mb-0">
{this.props.showIcon && !community.removed && (
<BannerIconHeader icon={community.icon} banner={community.banner} />
<BannerIconHeader
icon={community.icon}
banner={community.banner}
myUserInfo={this.props.myUserInfo}
/>
)}
<span className="me-2">
<CommunityLink community={community} hideAvatar />

View file

@ -1,10 +1,9 @@
import { setIsoData } from "@utils/app";
import { isBrowser, updateDataBsTheme } from "@utils/browser";
import { getQueryParams } from "@utils/helpers";
import { Component, linkEvent } from "inferno";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
import { I18NextService, UserService } from "../../services";
import { I18NextService } from "../../services";
import {
EMPTY_REQUEST,
HttpService,
@ -17,6 +16,9 @@ import { Spinner } from "../common/icon";
import PasswordInput from "../common/password-input";
import TotpModal from "../common/totp-modal";
import { UnreadCounterService } from "../../services";
import { IsoData, IsoDataOptionalSite } from "../../interfaces";
import { updateSite } from "@utils/app/setup-redux";
import { EnhancedStore } from "@reduxjs/toolkit";
interface LoginProps {
prev?: string;
@ -39,15 +41,20 @@ interface State {
show2faModal: boolean;
}
async function handleLoginSuccess(i: Login, loginRes: LoginResponse) {
UserService.Instance.login({
async function handleLoginSuccess(
i: Login,
loginRes: LoginResponse,
store: EnhancedStore<IsoDataOptionalSite>,
) {
HttpService.login({
res: loginRes,
});
const site = await HttpService.client.getSite();
if (site.state === "success") {
// TODO this is the key, you need to update the redux store here
UserService.Instance.myUserInfo = site.data.my_user;
store.dispatch(updateSite(site.data));
// TODO why is this just the theme??
updateDataBsTheme(site.data);
}
@ -86,7 +93,7 @@ async function handleLoginSubmit(i: Login, event: any) {
}
case "success": {
handleLoginSuccess(i, loginRes.data);
handleLoginSuccess(i, loginRes.data, this.context.store);
break;
}
}
@ -111,7 +118,9 @@ export class Login extends Component<
RouteComponentProps<Record<string, never>>,
State
> {
private isoData = setIsoData(this.context);
get isoData(): IsoData {
return this.context.store.getState().value;
}
state: State = {
loginRes: EMPTY_REQUEST,
@ -169,7 +178,7 @@ export class Login extends Component<
const successful = loginRes.state === "success";
if (successful) {
this.setState({ show2faModal: false });
handleLoginSuccess(this, loginRes.data);
handleLoginSuccess(this, loginRes.data, this.context.store);
} else {
toast(I18NextService.i18n.t("incorrect_totp_code"), "danger");
}

View file

@ -1,4 +1,4 @@
import { fetchThemeList, setIsoData } from "@utils/app";
import { fetchThemeList } from "@utils/app";
import { Component, linkEvent } from "inferno";
import { Helmet } from "inferno-helmet";
import {
@ -7,7 +7,7 @@ import {
LoginResponse,
Register,
} from "lemmy-js-client";
import { I18NextService, UserService } from "../../services";
import { I18NextService } from "../../services";
import {
EMPTY_REQUEST,
HttpService,
@ -17,6 +17,7 @@ import {
import { Spinner } from "../common/icon";
import PasswordInput from "../common/password-input";
import { SiteForm } from "./site-form";
import { IsoData } from "../../interfaces";
interface State {
form: {
@ -37,7 +38,9 @@ interface State {
}
export class Setup extends Component<any, State> {
private isoData = setIsoData(this.context);
get isoData(): IsoData {
return this.context.store.getState().value;
}
state: State = {
registerRes: EMPTY_REQUEST,
@ -45,7 +48,7 @@ export class Setup extends Component<any, State> {
form: {
show_nsfw: true,
},
doneRegisteringUser: !!UserService.Instance.myUserInfo,
doneRegisteringUser: !!this.isoData.site_res.my_user,
siteRes: this.isoData.site_res,
};
@ -194,7 +197,7 @@ export class Setup extends Component<any, State> {
if (i.state.registerRes.state === "success") {
const data = i.state.registerRes.data;
UserService.Instance.login({ res: data });
HttpService.login({ res: data });
i.setState({ doneRegisteringUser: true });
}
}

View file

@ -24,6 +24,7 @@ import { Icon, Spinner } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea";
import PasswordInput from "../common/password-input";
import { IsoData } from "../../interfaces";
import { updateSite } from "@utils/app/setup-redux";
interface State {
registerRes: RequestState<LoginResponse>;
@ -400,14 +401,15 @@ export class Signup extends Component<any, State> {
// Only log them in if a jwt was set
if (data.jwt) {
UserService.Instance.login({
HttpService.login({
res: data,
});
const site = await HttpService.client.getSite();
// TODO test this
if (site.state === "success") {
UserService.Instance.myUserInfo = site.data.my_user;
i.context.store.dispatch(updateSite(site.data));
}
i.props.history.replace("/communities");

View file

@ -167,6 +167,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
imageSrc={this.state.siteForm.icon}
onUpload={this.handleIconUpload}
onRemove={this.handleIconRemove}
isLoggedIn={!!this.props.siteRes.my_user}
rounded
/>
</div>
@ -181,6 +182,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
imageSrc={this.state.siteForm.banner}
onUpload={this.handleBannerUpload}
onRemove={this.handleBannerRemove}
isLoggedIn={!!this.props.siteRes.my_user}
/>
</div>
</div>

View file

@ -1,6 +1,6 @@
import classNames from "classnames";
import { Component, linkEvent } from "inferno";
import { PersonView, Site, SiteAggregates } from "lemmy-js-client";
import { MyUserInfo, PersonView, Site, SiteAggregates } from "lemmy-js-client";
import { mdToHtml } from "../../markdown";
import { I18NextService } from "../../services";
import { Badges } from "../common/badges";
@ -14,6 +14,7 @@ interface SiteSidebarProps {
counts?: SiteAggregates;
admins?: PersonView[];
isMobile?: boolean;
myUserInfo?: MyUserInfo;
}
interface SiteSidebarState {
@ -36,7 +37,10 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
<header className="card-header" id="sidebarInfoHeader">
{this.siteName()}
{!this.state.collapsed && (
<BannerIconHeader banner={this.props.site.banner} />
<BannerIconHeader
banner={this.props.site.banner}
myUserInfo={this.props.myUserInfo}
/>
)}
</header>

View file

@ -560,6 +560,7 @@ export class Inbox extends Component<any, InboxState> {
return (
<PrivateMessage
key={i.id}
myUserInfo={this.isoData.site_res.my_user}
private_message_view={i.view as PrivateMessageView}
onDelete={this.handleDeleteMessage}
onMarkRead={this.handleMarkMessageAsRead}
@ -704,6 +705,7 @@ export class Inbox extends Component<any, InboxState> {
<PrivateMessage
key={pmv.private_message.id}
private_message_view={pmv}
myUserInfo={this.isoData.site_res.my_user}
onDelete={this.handleDeleteMessage}
onMarkRead={this.handleMarkMessageAsRead}
onReport={this.handleMessageReport}

View file

@ -1,8 +1,7 @@
import { setIsoData } from "@utils/app";
import { capitalizeFirstLetter } from "@utils/helpers";
import { Component, linkEvent } from "inferno";
import { GetSiteResponse, SuccessResponse } from "lemmy-js-client";
import { HttpService, I18NextService, UserService } from "../../services";
import { HttpService, I18NextService } from "../../services";
import {
EMPTY_REQUEST,
LOADING_REQUEST,
@ -12,6 +11,8 @@ import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
import PasswordInput from "../common/password-input";
import { toast } from "../../toast";
import { IsoData } from "../../interfaces";
import { updateSite } from "@utils/app/setup-redux";
interface State {
passwordChangeRes: RequestState<SuccessResponse>;
@ -24,7 +25,9 @@ interface State {
}
export class PasswordChange extends Component<any, State> {
private isoData = setIsoData(this.context);
get isoData(): IsoData {
return this.context.store.getState().value;
}
state: State = {
passwordChangeRes: EMPTY_REQUEST,
@ -128,9 +131,10 @@ export class PasswordChange extends Component<any, State> {
if (i.state.passwordChangeRes.state === "success") {
toast(I18NextService.i18n.t("password_changed"));
// TODO test this
const site = await HttpService.client.getSite();
if (site.state === "success") {
UserService.Instance.myUserInfo = site.data.my_user;
i.context.store.dispatch(updateSite(site.data));
}
i.props.history.replace("/");

View file

@ -92,6 +92,7 @@ export class PersonListing extends Component<PersonListingProps, any> {
<PictrsImage
src={avatar ?? `${getStaticDir()}/assets/icons/icon-96x96.png`}
icon
myUserInfo={this.props.myUserInfo}
/>
)}
<span>{displayName}</span>

View file

@ -490,6 +490,7 @@ export class Profile extends Component<
<BannerIconHeader
banner={pv.person.banner}
icon={pv.person.avatar}
myUserInfo={this.isoData.site_res.my_user}
/>
)}
<div className="mb-3">

View file

@ -4,7 +4,6 @@ import {
fetchThemeList,
fetchUsers,
instanceToChoice,
myAuth,
personToChoice,
setTheme,
showLocal,
@ -64,6 +63,7 @@ import TotpModal from "../common/totp-modal";
import { LoadingEllipses } from "../common/loading-ellipses";
import { updateDataBsTheme } from "../../utils/browser";
import { getHttpBaseInternal } from "../../utils/env";
import { updateSite } from "@utils/app/setup-redux";
type SettingsData = RouteDataResponse<{
instancesRes: GetFederatedInstancesResponse;
@ -763,6 +763,7 @@ export class Settings extends Component<any, SettingsState> {
imageSrc={this.state.saveUserSettingsForm.avatar}
onUpload={this.handleAvatarUpload}
onRemove={this.handleAvatarRemove}
isLoggedIn={!!this.isoData.site_res.my_user}
rounded
/>
</div>
@ -777,6 +778,7 @@ export class Settings extends Component<any, SettingsState> {
imageSrc={this.state.saveUserSettingsForm.banner}
onUpload={this.handleBannerUpload}
onRemove={this.handleBannerRemove}
isLoggedIn={!!this.isoData.site_res.my_user}
/>
</div>
</div>
@ -1298,14 +1300,12 @@ export class Settings extends Component<any, SettingsState> {
}
async handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
if (myAuth()) {
const res = await HttpService.client.blockCommunity({
community_id: i.communityId,
block: false,
});
i.ctx.communityBlock(res);
}
}
async handleBlockInstance({ value }: Choice) {
if (value !== "0") {
@ -1530,8 +1530,8 @@ export class Settings extends Component<any, SettingsState> {
siteRes: siteRes.data,
});
// TODO need to update this
UserService.Instance.myUserInfo = siteRes.data.my_user;
// TODO need to test this
i.context.store.dispatch(updateSite(siteRes.data));
}
toast(I18NextService.i18n.t("saved"));
@ -1631,8 +1631,8 @@ export class Settings extends Component<any, SettingsState> {
},
} = siteRes.data.my_user!.local_user_view;
// TODO need to update redux
UserService.Instance.myUserInfo = siteRes.data.my_user;
// TODO need to test this
i.context.store.dispatch(updateSite(siteRes.data));
updateDataBsTheme(siteRes.data);
i.setState(prev => ({
@ -1695,7 +1695,7 @@ export class Settings extends Component<any, SettingsState> {
delete_content: false,
});
if (deleteAccountRes.state === "success") {
UserService.Instance.logout();
HttpService.logout();
this.context.router.history.replace("/");
}

View file

@ -1,4 +1,3 @@
import { myAuth } from "@utils/app";
import { canShare, share } from "@utils/browser";
import { getExternalHost, getHttpBase } from "@utils/env";
import {
@ -255,7 +254,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<>
<div className="offset-sm-3 my-2 d-none d-sm-block">
<a href={this.imageSrc} className="d-inline-block">
<PictrsImage src={this.imageSrc} />
<PictrsImage
src={this.imageSrc}
myUserInfo={this.props.myUserInfo}
/>
</a>
</div>
<div className="my-2 d-block d-sm-none">
@ -264,7 +266,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
className="p-0 border-0 bg-transparent d-inline-block"
onClick={linkEvent(this, this.handleImageExpandClick)}
>
<PictrsImage src={this.imageSrc} />
<PictrsImage
src={this.imageSrc}
myUserInfo={this.props.myUserInfo}
/>
</button>
</div>
</>
@ -310,6 +315,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
thumbnail
alt=""
nsfw={pv.post.nsfw || pv.community.nsfw}
myUserInfo={this.props.myUserInfo}
/>
);
}
@ -630,7 +636,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{mobile && !this.props.viewOnly && (
<VoteButtonsCompact
voteContentType={VoteContentType.Post}
loggedIn={!!this.props.myUserInfo}
myUserInfo={this.props.myUserInfo}
id={this.postView.post.id}
onVote={this.props.onPostVote}
enableDownvotes={this.props.enableDownvotes}
@ -1383,7 +1389,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<div className="col flex-grow-0">
<VoteButtons
voteContentType={VoteContentType.Post}
loggedIn={!!this.props.myUserInfo}
myUserInfo={this.props.myUserInfo}
id={this.postView.post.id}
onVote={this.props.onPostVote}
enableDownvotes={this.props.enableDownvotes}
@ -1716,7 +1722,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
i.setState({ imageExpanded: !i.state.imageExpanded });
setupTippy();
if (myAuth() && !i.postView.read) {
if (!!this.props.myUserInfo && !i.postView.read) {
i.props.onMarkPostAsRead({
post_ids: [i.postView.post.id],
read: true,

View file

@ -5,11 +5,12 @@ import {
DeletePrivateMessage,
EditPrivateMessage,
MarkPrivateMessageAsRead,
MyUserInfo,
Person,
PrivateMessageView,
} from "lemmy-js-client";
import { mdToHtml } from "../../markdown";
import { I18NextService, UserService } from "../../services";
import { I18NextService } from "../../services";
import { Icon, Spinner } from "../common/icon";
import { MomentTime } from "../common/moment-time";
import { PersonListing } from "../person/person-listing";
@ -28,6 +29,7 @@ interface PrivateMessageState {
interface PrivateMessageProps {
private_message_view: PrivateMessageView;
myUserInfo?: MyUserInfo;
onDelete(form: DeletePrivateMessage): void;
onMarkRead(form: MarkPrivateMessageAsRead): void;
onReport(form: CreatePrivateMessageReport): void;
@ -57,7 +59,7 @@ export class PrivateMessage extends Component<
get mine(): boolean {
return (
UserService.Instance.myUserInfo?.local_user_view.person.id ===
this.props.myUserInfo?.local_user_view.person.id ===
this.props.private_message_view.creator.id
);
}

View file

@ -151,7 +151,11 @@ export class RemoteFetch extends Component<any, RemoteFetchState> {
<h1>{I18NextService.i18n.t("community_federated")}</h1>
<div className="card mt-5">
{communityView.community.banner && (
<PictrsImage src={communityView.community.banner} cardTop />
<PictrsImage
src={communityView.community.banner}
cardTop
myUserInfo={this.isoData.site_res.my_user}
/>
)}
<div className="card-body">
<h2 className="card-title">
@ -163,6 +167,7 @@ export class RemoteFetch extends Component<any, RemoteFetchState> {
</div>
)}
<SubscribeButton
loggedIn={!!this.isoData.site_res.my_user}
communityView={communityView}
onFollow={linkEvent(this, handleFollow)}
onUnFollow={linkEvent(this, handleUnfollow)}

View file

@ -6,7 +6,6 @@ import {
fetchCommunities,
fetchUsers,
getUpdatedSearchId,
myAuth,
personToChoice,
showLocal,
} from "@utils/app";
@ -979,7 +978,7 @@ export class Search extends Component<any, SearchState> {
window.scrollTo(0, 0);
restoreScrollPosition(this.context);
if (myAuth()) {
if (this.isoData.site_res.my_user) {
this.setState({ resolveObjectRes: LOADING_REQUEST });
this.setState({
resolveObjectRes: await HttpService.silent_client.resolveObject({

View file

@ -104,6 +104,9 @@ export function wrapClient(client: LemmyHttp, silent = false) {
// auth: string;
// }
/**
* An HTTP service, only to be used in the browser client
*/
export class HttpService {
static #_instance: HttpService;
#silent_client: WrappedLemmyHttp;
@ -114,13 +117,13 @@ export class HttpService {
const auth = cookie.parse(document.cookie)[authCookieName];
if (auth) {
HttpService.client.setHeaders({ Authorization: `Bearer ${auth}` });
lemmyHttp.setHeaders({ Authorization: `Bearer ${auth}` });
}
this.#client = wrapClient(lemmyHttp);
this.#silent_client = wrapClient(lemmyHttp, true);
}
public login({
public static login({
res,
showToast = true,
}: {
@ -130,15 +133,17 @@ export class HttpService {
if (isBrowser() && res.jwt) {
showToast && toast(I18NextService.i18n.t("logged_in"));
setAuthCookie(res.jwt);
const headers = { Authorization: `Bearer ${res.jwt}` };
this.#_instance.#client.setHeaders(headers);
this.#_instance.#silent_client.setHeaders(headers);
}
}
public logout() {
public static logout() {
if (isBrowser()) {
clearAuthCookie();
}
this.#client.logout();
this.#_instance.#client.logout();
if (isAuthPath(location.pathname)) {
location.replace("/");

View file

@ -1,6 +1,5 @@
import { isBrowser } from "@utils/browser";
import i18next, { Resource } from "i18next";
import { UserService } from "../services";
import { ar } from "../translations/ar";
import { bg } from "../translations/bg";
import { ca } from "../translations/ca";
@ -32,6 +31,7 @@ import { sv } from "../translations/sv";
import { vi } from "../translations/vi";
import { zh } from "../translations/zh";
import { zh_Hant } from "../translations/zh_Hant";
import { MyUserInfo } from "lemmy-js-client";
export const languages = [
{ resource: ar, code: "ar", name: "العربية" },
@ -74,20 +74,24 @@ function format(value: any, format: any): any {
return format === "uppercase" ? value.toUpperCase() : value;
}
class LanguageDetector {
export class LanguageDetector {
static readonly type = "languageDetector";
private myLanguages: string[];
// TODO What's going on here? test this.
detect() {
return this.myLanguages;
}
setupMyLanguages(myUserInfo?: MyUserInfo): string[] {
const langs: string[] = [];
const myLang =
UserService.Instance.myUserInfo?.local_user_view.local_user
.interface_language ?? "browser";
myUserInfo?.local_user_view.local_user.interface_language ?? "browser";
if (myLang !== "browser") langs.push(myLang);
if (isBrowser()) langs.push(...navigator.languages);
return langs;
}
}

View file

@ -1,10 +1,10 @@
import { HttpService } from "../services";
import { updateUnreadCountsInterval } from "../config";
import { poll } from "@utils/helpers";
import { myAuth } from "@utils/app";
import { amAdmin } from "@utils/roles";
import { amAdmin, moderatesSomething } from "@utils/roles";
import { isBrowser } from "@utils/browser";
import { BehaviorSubject } from "rxjs";
import { MyUserInfo } from "lemmy-js-client";
/**
* Service to poll and keep track of unread messages / notifications.
@ -14,6 +14,8 @@ export class UnreadCounterService {
unreadPrivateMessages = 0;
unreadReplies = 0;
unreadMentions = 0;
myUserInfo?: MyUserInfo = undefined;
public unreadInboxCountSubject: BehaviorSubject<number> =
new BehaviorSubject<number>(0);
@ -36,15 +38,19 @@ export class UnreadCounterService {
}
}
public setMyUserInfo(myUserInfo: MyUserInfo) {
this.myUserInfo = myUserInfo;
}
private get shouldUpdate() {
if (window.document.visibilityState === "hidden") {
return false;
}
if (!myAuth()) {
} else if (!this.myUserInfo) {
return false;
}
} else {
return true;
}
}
public async updateInboxCounts() {
if (this.shouldUpdate) {
@ -61,7 +67,7 @@ export class UnreadCounterService {
}
public async updateReports() {
if (this.shouldUpdate && UserService.Instance.moderatesSomething) {
if (this.shouldUpdate && moderatesSomething(this.myUserInfo)) {
const reportCountRes = await HttpService.client.getReportCount({});
if (reportCountRes.state === "success") {
this.commentReportCount = reportCountRes.data.comment_reports ?? 0;
@ -78,7 +84,7 @@ export class UnreadCounterService {
}
public async updateApplications() {
if (this.shouldUpdate && amAdmin()) {
if (this.shouldUpdate && amAdmin(this.myUserInfo)) {
const unreadApplicationsRes =
await HttpService.client.getUnreadRegistrationApplicationCount();
if (unreadApplicationsRes.state === "success") {

View file

@ -1,12 +1,16 @@
import { GetSiteResponse } from "lemmy-js-client";
import { setupEmojiDataModel, setupMarkdown } from "../../markdown";
import { I18NextService, UserService } from "../../services";
import { I18NextService } from "../../services";
import { updateDataBsTheme } from "@utils/browser";
import { LanguageDetector } from "shared/services/I18NextService";
export default function initializeSite(site?: GetSiteResponse) {
// TODO Should already be in siteRes
UserService.Instance.myUserInfo = site?.my_user;
updateDataBsTheme(site);
// TODO test this
const ld = new LanguageDetector();
ld.setupMyLanguages(site?.my_user);
I18NextService.i18n.changeLanguage();
if (site) {
setupEmojiDataModel(site.custom_emojis ?? []);

View file

@ -1,8 +1,12 @@
import { isBrowser } from "@utils/browser";
import { toast } from "../../../shared/toast";
import { I18NextService } from "../../services";
import cookie from "cookie";
import { authCookieName } from "../../config";
// TODO get rid of this
export default function myAuth(throwErr = false): string | undefined {
const auth = cookie.parse(document.cookie)[authCookieName];
if (auth) {
return auth;
} else {

View file

@ -1,13 +1,33 @@
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { PayloadAction, configureStore, createSlice } from "@reduxjs/toolkit";
import { IsoDataOptionalSite } from "../../../shared/interfaces";
import { GetSiteResponse } from "lemmy-js-client";
// TODO add reducer function here
export default function setupRedux(isoData?: IsoDataOptionalSite) {
const slice = createSlice({
interface isoDataState {
value?: IsoDataOptionalSite;
}
const initialState: isoDataState = {
value: undefined,
};
export const isoDataSlice = createSlice({
name: "isoData",
initialState: { value: isoData },
reducers: {},
initialState,
reducers: {
updateIsoData: (state, action: PayloadAction<IsoDataOptionalSite>) => {
state.value = action.payload;
},
updateSite: (state, action: PayloadAction<GetSiteResponse>) => {
state.value!.site_res = action.payload;
},
},
});
const store = configureStore({ reducer: slice.reducer });
export const { updateIsoData, updateSite } = isoDataSlice.actions;
export default function setupRedux(isoData: IsoDataOptionalSite) {
const store = configureStore({ reducer: isoDataSlice.reducer });
store.dispatch(updateIsoData(isoData));
return store;
}