import { matchPath, StaticRouter } from "inferno-router";
import { renderToString } from "inferno-server";
import IsomorphicCookie from "isomorphic-cookie";
- import { GetSite, GetSiteResponse, LemmyHttp, Site } from "lemmy-js-client";
+ import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
import path from "path";
import process from "process";
import serialize from "serialize-javascript";
import sharp from "sharp";
import { App } from "../shared/components/app/app";
- import { getHttpBase, getHttpBaseInternal } from "../shared/env";
+ import { getHttpBaseExternal, getHttpBaseInternal } from "../shared/env";
import {
ILemmyConfig,
InitialFetchRequest,
IsoDataOptionalSite,
} from "../shared/interfaces";
import { routes } from "../shared/routes";
-import { RequestState, wrapClient } from "../shared/services/HttpService";
++import {
++ FailedRequestState,
++ RequestState,
++ wrapClient,
++} from "../shared/services/HttpService";
import {
ErrorPageData,
favIconPngUrl,
server.use(function (_req, res, next) {
res.setHeader(
"Content-Security-Policy",
- `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *`
+ `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src *`
);
next();
});
server.get("/service-worker.js", async (_req, res) => {
res.setHeader("Content-Type", "application/javascript");
- res.sendFile(path.resolve("./dist/service-worker.js"));
+ res.sendFile(
+ path.resolve(
+ `./dist/service-worker${
+ process.env.NODE_ENV === "development" ? "-development" : ""
+ }.js`
+ )
+ );
});
server.get("/robots.txt", async (_req, res) => {
const getSiteForm: GetSite = { auth };
const headers = setForwardedHeaders(req.headers);
- const client = new LemmyHttp(getHttpBaseInternal(), headers);
+ const client = wrapClient(new LemmyHttp(getHttpBaseInternal(), headers));
const { path, url, query } = req;
// This bypasses errors, so that the client can hit the error on its own,
// in order to remove the jwt on the browser. Necessary for wrong jwts
let site: GetSiteResponse | undefined = undefined;
- let routeData: Record<string, any> = {};
- let errorPageData: ErrorPageData | undefined;
- try {
- let try_site: any = await client.getSite(getSiteForm);
- if (try_site.error == "not_logged_in") {
- console.error(
- "Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
- );
- getSiteForm.auth = undefined;
- auth = undefined;
- try_site = await client.getSite(getSiteForm);
- }
- const routeData: RequestState<any>[] = [];
++ let routeData: Record<string, RequestState<any>> = {};
+ let errorPageData: ErrorPageData | undefined = undefined;
+ let try_site = await client.getSite(getSiteForm);
+ if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
+ console.error(
+ "Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
+ );
+ getSiteForm.auth = undefined;
+ auth = undefined;
+ try_site = await client.getSite(getSiteForm);
+ }
- if (!auth && isAuthPath(path)) {
- res.redirect("/login");
- return;
- }
+ if (!auth && isAuthPath(path)) {
+ return res.redirect("/login");
+ }
- site = try_site;
+ if (try_site.state === "success") {
+ site = try_site.data;
initializeSite(site);
+ if (path != "/setup" && !site.site_view.local_site.site_setup) {
+ return res.redirect("/setup");
+ }
+
if (site) {
const initialFetchReq: InitialFetchRequest = {
client,
};
if (activeRoute?.fetchInitialData) {
- routeData.push(
- ...(await Promise.all([
- ...activeRoute.fetchInitialData(initialFetchReq),
- ]))
+ const routeDataKeysAndVals = await Promise.all(
+ Object.entries(activeRoute.fetchInitialData(initialFetchReq)).map(
+ async ([key, val]) => [key, await val]
+ )
);
+
+ routeData = routeDataKeysAndVals.reduce((acc, [key, val]) => {
+ acc[key] = val;
+
+ return acc;
+ }, {});
}
}
- } catch (error) {
- errorPageData = getErrorPageData(error, site);
+ } else if (try_site.state === "failed") {
+ errorPageData = getErrorPageData(new Error(try_site.msg), site);
}
- const error = Object.values(routeData).find(val => val?.error)?.error;
++ const error = Object.values(routeData).find(
++ res => res.state === "failed"
++ ) as FailedRequestState | undefined;
+
// Redirect to the 404 if there's an API error
- if (routeData[0] && routeData[0].state === "failed") {
- const error = routeData[0].msg;
- console.error(error);
- if (error === "instance_is_private") {
+ if (error) {
- console.error(error);
- if (error === "instance_is_private") {
++ console.error(error.msg);
++ if (error.msg === "instance_is_private") {
return res.redirect(`/signup`);
} else {
- errorPageData = getErrorPageData(error, site);
- errorPageData = getErrorPageData(new Error(error), site);
++ errorPageData = getErrorPageData(new Error(error.msg), site);
}
}
function setForwardedHeaders(headers: IncomingHttpHeaders): {
[key: string]: string;
} {
- let out: { [key: string]: string } = {};
+ const out: { [key: string]: string } = {};
if (headers.host) {
out.host = headers.host;
}
- let realIp = headers["x-real-ip"];
+ const realIp = headers["x-real-ip"];
if (realIp) {
out["x-real-ip"] = realIp as string;
}
- let forwardedFor = headers["x-forwarded-for"];
+ const forwardedFor = headers["x-forwarded-for"];
if (forwardedFor) {
out["x-forwarded-for"] = forwardedFor as string;
}
process.exit(0);
});
- const iconSizes = [72, 96, 128, 144, 152, 192, 384, 512];
+ const iconSizes = [72, 96, 144, 192, 512];
const defaultLogoPathDirectory = path.join(
process.cwd(),
"dist",
"icons"
);
- export async function generateManifestBase64(site: Site) {
- const url = (
- process.env.NODE_ENV === "development"
- ? "http://localhost:1236/"
- : getHttpBase()
- ).replace(/\/$/g, "");
+ export async function generateManifestBase64({
+ my_user,
+ site_view: {
+ site,
+ local_site: { community_creation_admin_only },
+ },
+ }: GetSiteResponse) {
+ const url = getHttpBaseExternal();
+
const icon = site.icon ? await fetchIconPng(site.icon) : null;
const manifest = {
};
})
),
+ shortcuts: [
+ {
+ name: "Search",
+ short_name: "Search",
+ description: "Perform a search.",
+ url: "/search",
+ },
+ {
+ name: "Communities",
+ url: "/communities",
+ short_name: "Communities",
+ description: "Browse communities",
+ },
+ ]
+ .concat(
+ my_user
+ ? [
+ {
+ name: "Create Post",
+ url: "/create_post",
+ short_name: "Create Post",
+ description: "Create a post.",
+ },
+ ]
+ : []
+ )
+ .concat(
+ my_user?.local_user_view.person.admin || !community_creation_admin_only
+ ? [
+ {
+ name: "Create Community",
+ url: "/create_community",
+ short_name: "Create Community",
+ description: "Create a community",
+ },
+ ]
+ : []
+ ),
+ related_applications: [
+ {
+ platform: "f-droid",
+ url: "https://f-droid.org/packages/com.jerboa/",
+ id: "com.jerboa",
+ },
+ ],
};
return Buffer.from(JSON.stringify(manifest)).toString("base64");
}
async function fetchIconPng(iconUrl: string) {
- return await fetch(
- iconUrl.replace(/https?:\/\/[^\/]+/g, getHttpBaseInternal())
- )
+ return await fetch(iconUrl)
.then(res => res.blob())
.then(blob => blob.arrayBuffer());
}
.then(buf => buf.toString("base64"))}`
: favIconPngUrl;
- const eruda = (
- <>
- <script src="//cdn.jsdelivr.net/npm/eruda"></script>
- <script>eruda.init();</script>
- </>
- );
-
- const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : "";
+ const erudaStr =
+ process.env["LEMMY_UI_DEBUG"] === "true"
+ ? renderToString(
+ <>
+ <script src="//cdn.jsdelivr.net/npm/eruda"></script>
+ <script>eruda.init();</script>
+ </>
+ )
+ : "";
const helmet = Helmet.renderStatic();
return `
<!DOCTYPE html>
- <html ${helmet.htmlAttributes.toString()} lang="en">
+ <html ${helmet.htmlAttributes.toString()}>
<head>
- <script>window.isoData = ${JSON.stringify(isoData)}</script>
+ <script>window.isoData = ${serialize(isoData)}</script>
<script>window.lemmyConfig = ${serialize(config)}</script>
<!-- A remote debugging utility for mobile -->
site &&
`<link
rel="manifest"
- href={${`data:application/manifest+json;base64,${await generateManifestBase64(
- site.site_view.site
- )}`}}
+ href=${`data:application/manifest+json;base64,${await generateManifestBase64(
+ site
+ )}`}
/>`
}
<link rel="apple-touch-icon" href=${appleTouchIcon} />
import { Component, linkEvent } from "inferno";
import {
CommunityResponse,
- FollowCommunity,
GetSiteResponse,
ListCommunities,
ListCommunitiesResponse,
ListingType,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
- import { Subscription } from "rxjs";
- import { InitialFetchRequest } from "shared/interfaces";
import { i18n } from "../../i18next";
- import { WebSocketService } from "../../services";
+ import { InitialFetchRequest } from "../../interfaces";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
import {
QueryParams,
- WithPromiseKeys,
++ RouteDataResponse,
+ editCommunity,
getPageFromString,
getQueryParams,
getQueryString,
- isBrowser,
myAuth,
+ myAuthRequired,
numToSI,
setIsoData,
showLocal,
- toast,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
const communityLimit = 50;
- interface CommunitiesData {
++type CommunitiesData = RouteDataResponse<{
+ listCommunitiesResponse: ListCommunitiesResponse;
- }
++}>;
+
interface CommunitiesState {
- listCommunitiesResponse?: ListCommunitiesResponse;
- loading: boolean;
+ listCommunitiesResponse: RequestState<ListCommunitiesResponse>;
siteRes: GetSiteResponse;
searchText: string;
+ isIsomorphic: boolean;
}
interface CommunitiesProps {
page: number;
}
- function getCommunitiesQueryParams() {
- return getQueryParams<CommunitiesProps>({
- listingType: getListingTypeFromQuery,
- page: getPageFromString,
- });
- }
-
function getListingTypeFromQuery(listingType?: string): ListingType {
return listingType ? (listingType as ListingType) : "Local";
}
- function toggleSubscribe(community_id: number, follow: boolean) {
- const auth = myAuth();
- if (auth) {
- const form: FollowCommunity = {
- community_id,
- follow,
- auth,
- };
-
- WebSocketService.Instance.send(wsClient.followCommunity(form));
- }
- }
-
- function refetch() {
- const { listingType, page } = getCommunitiesQueryParams();
-
- const listCommunitiesForm: ListCommunities = {
- type_: listingType,
- sort: "TopMonth",
- limit: communityLimit,
- page,
- auth: myAuth(false),
- };
-
- WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
- }
-
export class Communities extends Component<any, CommunitiesState> {
- private subscription?: Subscription;
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<CommunitiesData>(this.context);
state: CommunitiesState = {
- loading: true,
+ listCommunitiesResponse: { state: "empty" },
siteRes: this.isoData.site_res,
searchText: "",
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
this.handlePageChange = this.handlePageChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
-
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
+ if (FirstLoadService.isFirstLoad) {
+ const { listCommunitiesResponse } = this.isoData.routeData;
+
this.state = {
...this.state,
- listCommunitiesResponse: this.isoData.routeData[0],
+ listCommunitiesResponse,
- loading: false,
+ isIsomorphic: true,
};
- } else {
- refetch();
}
}
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.refetch();
}
}
}`;
}
- render() {
- const { listingType, page } = getCommunitiesQueryParams();
-
- return (
- <div className="container-lg">
- <HtmlTags
- title={this.documentTitle}
- path={this.context.router.route.match.url}
- />
- {this.state.loading ? (
+ renderListings() {
+ switch (this.state.listCommunitiesResponse.state) {
+ case "loading":
+ return (
<h5>
<Spinner large />
</h5>
- ) : (
+ );
+ case "success": {
+ const { listingType, page } = this.getCommunitiesQueryParams();
+ return (
<div>
<div className="row">
<div className="col-md-6">
</tr>
</thead>
<tbody>
- {this.state.listCommunitiesResponse?.communities.map(cv => (
- <tr key={cv.community.id}>
- <td>
- <CommunityLink community={cv.community} />
- </td>
- <td className="text-right">
- {numToSI(cv.counts.subscribers)}
- </td>
- <td className="text-right">
- {numToSI(cv.counts.users_active_month)}
- </td>
- <td className="text-right d-none d-lg-table-cell">
- {numToSI(cv.counts.posts)}
- </td>
- <td className="text-right d-none d-lg-table-cell">
- {numToSI(cv.counts.comments)}
- </td>
- <td className="text-right">
- {cv.subscribed == "Subscribed" && (
- <button
- className="btn btn-link d-inline-block"
- onClick={linkEvent(
- cv.community.id,
- this.handleUnsubscribe
- )}
- >
- {i18n.t("unsubscribe")}
- </button>
- )}
- {cv.subscribed === "NotSubscribed" && (
- <button
- className="btn btn-link d-inline-block"
- onClick={linkEvent(
- cv.community.id,
- this.handleSubscribe
- )}
- >
- {i18n.t("subscribe")}
- </button>
- )}
- {cv.subscribed === "Pending" && (
- <div className="text-warning d-inline-block">
- {i18n.t("subscribe_pending")}
- </div>
- )}
- </td>
- </tr>
- ))}
+ {this.state.listCommunitiesResponse.data.communities.map(
+ cv => (
+ <tr key={cv.community.id}>
+ <td>
+ <CommunityLink community={cv.community} />
+ </td>
+ <td className="text-right">
+ {numToSI(cv.counts.subscribers)}
+ </td>
+ <td className="text-right">
+ {numToSI(cv.counts.users_active_month)}
+ </td>
+ <td className="text-right d-none d-lg-table-cell">
+ {numToSI(cv.counts.posts)}
+ </td>
+ <td className="text-right d-none d-lg-table-cell">
+ {numToSI(cv.counts.comments)}
+ </td>
+ <td className="text-right">
+ {cv.subscribed == "Subscribed" && (
+ <button
+ className="btn btn-link d-inline-block"
+ onClick={linkEvent(
+ {
+ i: this,
+ communityId: cv.community.id,
+ follow: false,
+ },
+ this.handleFollow
+ )}
+ >
+ {i18n.t("unsubscribe")}
+ </button>
+ )}
+ {cv.subscribed === "NotSubscribed" && (
+ <button
+ className="btn btn-link d-inline-block"
+ onClick={linkEvent(
+ {
+ i: this,
+ communityId: cv.community.id,
+ follow: true,
+ },
+ this.handleFollow
+ )}
+ >
+ {i18n.t("subscribe")}
+ </button>
+ )}
+ {cv.subscribed === "Pending" && (
+ <div className="text-warning d-inline-block">
+ {i18n.t("subscribe_pending")}
+ </div>
+ )}
+ </td>
+ </tr>
+ )
+ )}
</tbody>
</table>
</div>
<Paginator page={page} onChange={this.handlePageChange} />
</div>
- )}
+ );
+ }
+ }
+ }
+
+ render() {
+ return (
+ <div className="container-lg">
+ <HtmlTags
+ title={this.documentTitle}
+ path={this.context.router.route.match.url}
+ />
+ {this.renderListings()}
</div>
);
}
);
}
- updateUrl({ listingType, page }: Partial<CommunitiesProps>) {
+ async updateUrl({ listingType, page }: Partial<CommunitiesProps>) {
const { listingType: urlListingType, page: urlPage } =
- getCommunitiesQueryParams();
+ this.getCommunitiesQueryParams();
const queryParams: QueryParams<CommunitiesProps> = {
listingType: listingType ?? urlListingType,
this.props.history.push(`/communities${getQueryString(queryParams)}`);
- refetch();
+ await this.refetch();
}
handlePageChange(page: number) {
});
}
- handleUnsubscribe(communityId: number) {
- toggleSubscribe(communityId, false);
- }
-
- handleSubscribe(communityId: number) {
- toggleSubscribe(communityId, true);
- }
-
handleSearchChange(i: Communities, event: any) {
i.setState({ searchText: event.target.value });
}
- handleSearchSubmit(i: Communities) {
+ handleSearchSubmit(i: Communities, event: any) {
+ event.preventDefault();
const searchParamEncoded = encodeURIComponent(i.state.searchText);
i.context.router.history.push(`/search?q=${searchParamEncoded}`);
}
-- static fetchInitialData({
++ static async fetchInitialData({
query: { listingType, page },
client,
auth,
- }: InitialFetchRequest<QueryParams<CommunitiesProps>>): Promise<
- RequestState<any>
- >[] {
+ }: InitialFetchRequest<
+ QueryParams<CommunitiesProps>
- >): WithPromiseKeys<CommunitiesData> {
++ >): Promise<CommunitiesData> {
const listCommunitiesForm: ListCommunities = {
type_: getListingTypeFromQuery(listingType),
sort: "TopMonth",
auth: auth,
};
- return [client.listCommunities(listCommunitiesForm)];
+ return {
- listCommunitiesResponse: client.listCommunities(listCommunitiesForm),
++ listCommunitiesResponse: await client.listCommunities(
++ listCommunitiesForm
++ ),
+ };
}
- parseMessage(msg: any) {
- const op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- } else if (op === UserOperation.ListCommunities) {
- const data = wsJsonToRes<ListCommunitiesResponse>(msg);
- this.setState({ listCommunitiesResponse: data, loading: false });
- window.scrollTo(0, 0);
- } else if (op === UserOperation.FollowCommunity) {
- const {
- community_view: {
- community,
- subscribed,
- counts: { subscribers },
- },
- } = wsJsonToRes<CommunityResponse>(msg);
- const res = this.state.listCommunitiesResponse;
- const found = res?.communities.find(
- ({ community: { id } }) => id == community.id
- );
-
- if (found) {
- found.subscribed = subscribed;
- found.counts.subscribers = subscribers;
- this.setState(this.state);
+ getCommunitiesQueryParams() {
+ return getQueryParams<CommunitiesProps>({
+ listingType: getListingTypeFromQuery,
+ page: getPageFromString,
+ });
+ }
+
+ async handleFollow(data: {
+ i: Communities;
+ communityId: number;
+ follow: boolean;
+ }) {
+ const res = await HttpService.client.followCommunity({
+ community_id: data.communityId,
+ follow: data.follow,
+ auth: myAuthRequired(),
+ });
+ data.i.findAndUpdateCommunity(res);
+ }
+
+ async refetch() {
+ this.setState({ listCommunitiesResponse: { state: "loading" } });
+
+ const { listingType, page } = this.getCommunitiesQueryParams();
+
+ this.setState({
+ listCommunitiesResponse: await HttpService.client.listCommunities({
+ type_: listingType,
+ sort: "TopMonth",
+ limit: communityLimit,
+ page,
+ auth: myAuth(),
+ }),
+ });
+
+ window.scrollTo(0, 0);
+ }
+
+ findAndUpdateCommunity(res: RequestState<CommunityResponse>) {
+ this.setState(s => {
+ if (
+ s.listCommunitiesResponse.state == "success" &&
+ res.state == "success"
+ ) {
+ s.listCommunitiesResponse.data.communities = editCommunity(
+ res.data.community_view,
+ s.listCommunitiesResponse.data.communities
+ );
}
- }
+ return s;
+ });
}
}
import { Component, linkEvent } from "inferno";
import { RouteComponentProps } from "inferno-router/dist/Route";
import {
+ AddAdmin,
+ AddModToCommunity,
AddModToCommunityResponse,
+ BanFromCommunity,
BanFromCommunityResponse,
- BlockCommunityResponse,
- BlockPersonResponse,
+ BanPerson,
+ BanPersonResponse,
+ BlockCommunity,
+ BlockPerson,
+ CommentId,
+ CommentReplyResponse,
CommentResponse,
- CommentView,
CommunityResponse,
+ CreateComment,
+ CreateCommentLike,
+ CreateCommentReport,
+ CreatePostLike,
+ CreatePostReport,
+ DeleteComment,
+ DeleteCommunity,
+ DeletePost,
+ DistinguishComment,
+ EditComment,
+ EditCommunity,
+ EditPost,
+ FeaturePost,
+ FollowCommunity,
GetComments,
GetCommentsResponse,
GetCommunity,
GetCommunityResponse,
GetPosts,
GetPostsResponse,
- PostReportResponse,
+ GetSiteResponse,
+ LockPost,
+ MarkCommentReplyAsRead,
+ MarkPersonMentionAsRead,
PostResponse,
- PostView,
+ PurgeComment,
+ PurgeCommunity,
PurgeItemResponse,
+ PurgePerson,
+ PurgePost,
+ RemoveComment,
+ RemoveCommunity,
+ RemovePost,
+ SaveComment,
+ SavePost,
SortType,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
+ TransferCommunity,
} from "lemmy-js-client";
- import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import {
CommentViewType,
DataType,
InitialFetchRequest,
} from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
import {
QueryParams,
- WithPromiseKeys,
++ RouteDataResponse,
commentsToFlatNodes,
communityRSSUrl,
- createCommentLikeRes,
- createPostLikeFindRes,
- editCommentRes,
- editPostFindRes,
+ editComment,
+ editPost,
+ editWith,
enableDownvotes,
enableNsfw,
fetchLimit,
+ getCommentParentId,
getDataTypeString,
getPageFromString,
getQueryParams,
getQueryString,
- isPostBlocked,
myAuth,
- notifyPost,
- nsfwCheck,
postToCommentSortType,
relTags,
restoreScrollPosition,
- saveCommentRes,
saveScrollPosition,
setIsoData,
setupTippy,
toast,
updateCommunityBlock,
updatePersonBlock,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { CommentNodes } from "../comment/comment-nodes";
import { BannerIconHeader } from "../common/banner-icon-header";
import { PostListings } from "../post/post-listings";
import { CommunityLink } from "./community-link";
- interface CommunityData {
++type CommunityData = RouteDataResponse<{
+ communityResponse: GetCommunityResponse;
+ postsResponse?: GetPostsResponse;
+ commentsResponse?: GetCommentsResponse;
- }
++}>;
+
interface State {
- communityRes?: GetCommunityResponse;
- communityLoading: boolean;
- listingsLoading: boolean;
- posts: PostView[];
- comments: CommentView[];
+ communityRes: RequestState<GetCommunityResponse>;
+ postsRes: RequestState<GetPostsResponse>;
+ commentsRes: RequestState<GetCommentsResponse>;
+ siteRes: GetSiteResponse;
showSidebarMobile: boolean;
+ finished: Map<CommentId, boolean | undefined>;
+ isIsomorphic: boolean;
}
interface CommunityProps {
RouteComponentProps<{ name: string }>,
State
> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<CommunityData>(this.context);
- private subscription?: Subscription;
state: State = {
- communityLoading: true,
- listingsLoading: true,
- posts: [],
- comments: [],
+ communityRes: { state: "empty" },
+ postsRes: { state: "empty" },
+ commentsRes: { state: "empty" },
+ siteRes: this.isoData.site_res,
showSidebarMobile: false,
+ finished: new Map(),
+ isIsomorphic: false,
};
constructor(props: RouteComponentProps<{ name: string }>, context: any) {
this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ // All of the action binds
+ this.handleDeleteCommunity = this.handleDeleteCommunity.bind(this);
+ this.handleEditCommunity = this.handleEditCommunity.bind(this);
+ this.handleFollow = this.handleFollow.bind(this);
+ this.handleRemoveCommunity = this.handleRemoveCommunity.bind(this);
+ this.handleCreateComment = this.handleCreateComment.bind(this);
+ this.handleEditComment = this.handleEditComment.bind(this);
+ this.handleSaveComment = this.handleSaveComment.bind(this);
+ this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
+ this.handleBlockPerson = this.handleBlockPerson.bind(this);
+ this.handleDeleteComment = this.handleDeleteComment.bind(this);
+ this.handleRemoveComment = this.handleRemoveComment.bind(this);
+ this.handleCommentVote = this.handleCommentVote.bind(this);
+ this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+ this.handleAddAdmin = this.handleAddAdmin.bind(this);
+ this.handlePurgePerson = this.handlePurgePerson.bind(this);
+ this.handlePurgeComment = this.handlePurgeComment.bind(this);
+ this.handleCommentReport = this.handleCommentReport.bind(this);
+ this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+ this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+ this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+ this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+ this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+ this.handleBanPerson = this.handleBanPerson.bind(this);
+ this.handlePostVote = this.handlePostVote.bind(this);
+ this.handlePostEdit = this.handlePostEdit.bind(this);
+ this.handlePostReport = this.handlePostReport.bind(this);
+ this.handleLockPost = this.handleLockPost.bind(this);
+ this.handleDeletePost = this.handleDeletePost.bind(this);
+ this.handleRemovePost = this.handleRemovePost.bind(this);
+ this.handleSavePost = this.handleSavePost.bind(this);
+ this.handlePurgePost = this.handlePurgePost.bind(this);
+ this.handleFeaturePost = this.handleFeaturePost.bind(this);
// Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
- const { communityResponse, commentsResponse, postsResponse } =
- this.isoData.routeData;
+ if (FirstLoadService.isFirstLoad) {
- const [communityRes, postsRes, commentsRes] = this.isoData.routeData;
++ const {
++ communityResponse: communityRes,
++ commentsResponse: commentsRes,
++ postsResponse: postsRes,
++ } = this.isoData.routeData;
+
this.state = {
...this.state,
- communityRes: communityResponse,
- communityRes,
- postsRes,
- commentsRes,
+ isIsomorphic: true,
};
- if (postsResponse) {
- this.state = { ...this.state, posts: postsResponse.posts };
+
- if (commentsResponse) {
- this.state = { ...this.state, comments: commentsResponse.comments };
++ if (communityRes.state === "success") {
++ this.state = {
++ ...this.state,
++ communityRes,
++ };
+ }
+
- this.state = {
- ...this.state,
- communityLoading: false,
- listingsLoading: false,
- };
- } else {
- this.fetchCommunity();
- this.fetchData();
++ if (postsRes?.state === "success") {
++ this.state = {
++ ...this.state,
++ postsRes,
++ };
+ }
+
++ if (commentsRes?.state === "success") {
++ this.state = {
++ ...this.state,
++ commentsRes,
++ };
++ }
}
}
- fetchCommunity() {
- const form: GetCommunity = {
- name: this.props.match.params.name,
- auth: myAuth(false),
- };
- WebSocketService.Instance.send(wsClient.getCommunity(form));
+ async fetchCommunity() {
+ this.setState({ communityRes: { state: "loading" } });
+ this.setState({
+ communityRes: await HttpService.client.getCommunity({
+ name: this.props.match.params.name,
+ auth: myAuth(),
+ }),
+ });
}
- componentDidMount() {
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await Promise.all([this.fetchCommunity(), this.fetchData()]);
+ }
+
setupTippy();
}
componentWillUnmount() {
saveScrollPosition(this.context);
- this.subscription?.unsubscribe();
}
-- static fetchInitialData({
++ static async fetchInitialData({
client,
path,
query: { dataType: urlDataType, page: urlPage, sort: urlSort },
auth,
- }: InitialFetchRequest<
- QueryParams<CommunityProps>
- >): WithPromiseKeys<CommunityData> {
+ }: InitialFetchRequest<QueryParams<CommunityProps>>): Promise<
- RequestState<any>
- >[] {
++ Promise<CommunityData>
++ > {
const pathSplit = path.split("/");
- const promises: Promise<RequestState<any>>[] = [];
const communityName = pathSplit[2];
const communityForm: GetCommunity = {
name: communityName,
auth,
};
- promises.push(client.getCommunity(communityForm));
const dataType = getDataTypeFromQuery(urlDataType);
const page = getPageFromString(urlPage);
- let postsResponse: Promise<GetPostsResponse> | undefined = undefined;
- let commentsResponse: Promise<GetCommentsResponse> | undefined = undefined;
++ let postsResponse: RequestState<GetPostsResponse> | undefined = undefined;
++ let commentsResponse: RequestState<GetCommentsResponse> | undefined =
++ undefined;
+
if (dataType === DataType.Post) {
const getPostsForm: GetPosts = {
community_name: communityName,
saved_only: false,
auth,
};
- promises.push(client.getPosts(getPostsForm));
- promises.push(Promise.resolve({ state: "empty" }));
+
- postsResponse = client.getPosts(getPostsForm);
++ postsResponse = await client.getPosts(getPostsForm);
} else {
const getCommentsForm: GetComments = {
community_name: communityName,
saved_only: false,
auth,
};
- promises.push(Promise.resolve({ state: "empty" }));
- promises.push(client.getComments(getCommentsForm));
+
- commentsResponse = client.getComments(getCommentsForm);
++ commentsResponse = await client.getComments(getCommentsForm);
}
- return promises;
+ return {
- communityResponse: client.getCommunity(communityForm),
++ communityResponse: await client.getCommunity(communityForm),
+ commentsResponse,
+ postsResponse,
+ };
}
get documentTitle(): string {
const cRes = this.state.communityRes;
- return cRes
- ? `${cRes.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}`
+ return cRes.state == "success"
+ ? `${cRes.data.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}`
: "";
}
- render() {
- const res = this.state.communityRes;
- const { page } = getCommunityQueryParams();
-
- return (
- <div className="container-lg">
- {this.state.communityLoading ? (
+ renderCommunity() {
+ switch (this.state.communityRes.state) {
+ case "loading":
+ return (
<h5>
<Spinner large />
</h5>
- ) : (
- res && (
- <>
- <HtmlTags
- title={this.documentTitle}
- path={this.context.router.route.match.url}
- description={res.community_view.community.description}
- image={res.community_view.community.icon}
- />
-
- <div className="row">
- <div className="col-12 col-md-8">
- {this.communityInfo}
- <div className="d-block d-md-none">
- <button
- className="btn btn-secondary d-inline-block mb-2 mr-3"
- onClick={linkEvent(this, this.handleShowSidebarMobile)}
- >
- {i18n.t("sidebar")}{" "}
- <Icon
- icon={
- this.state.showSidebarMobile
- ? `minus-square`
- : `plus-square`
- }
- classes="icon-inline"
- />
- </button>
- {this.state.showSidebarMobile && this.sidebar(res)}
- </div>
- {this.selects}
- {this.listings}
- <Paginator page={page} onChange={this.handlePageChange} />
- </div>
- <div className="d-none d-md-block col-md-4">
- {this.sidebar(res)}
+ );
+ case "success": {
+ const res = this.state.communityRes.data;
+ const { page } = getCommunityQueryParams();
+
+ return (
+ <>
+ <HtmlTags
+ title={this.documentTitle}
+ path={this.context.router.route.match.url}
+ description={res.community_view.community.description}
+ image={res.community_view.community.icon}
+ />
+
+ <div className="row">
+ <div className="col-12 col-md-8">
+ {this.communityInfo(res)}
+ <div className="d-block d-md-none">
+ <button
+ className="btn btn-secondary d-inline-block mb-2 mr-3"
+ onClick={linkEvent(this, this.handleShowSidebarMobile)}
+ >
+ {i18n.t("sidebar")}{" "}
+ <Icon
+ icon={
+ this.state.showSidebarMobile
+ ? `minus-square`
+ : `plus-square`
+ }
+ classes="icon-inline"
+ />
+ </button>
+ {this.state.showSidebarMobile && this.sidebar(res)}
</div>
+ {this.selects(res)}
+ {this.listings(res)}
+ <Paginator page={page} onChange={this.handlePageChange} />
</div>
- </>
- )
- )}
- </div>
- );
+ <div className="d-none d-md-block col-md-4">
+ {this.sidebar(res)}
+ </div>
+ </div>
+ </>
+ );
+ }
+ }
+ }
+
+ render() {
+ return <div className="container-lg">{this.renderCommunity()}</div>;
}
- sidebar({
- community_view,
- moderators,
- online,
- discussion_languages,
- site,
- }: GetCommunityResponse) {
+ sidebar(res: GetCommunityResponse) {
const { site_res } = this.isoData;
// For some reason, this returns an empty vec if it matches the site langs
const communityLangs =
- discussion_languages.length === 0
+ res.discussion_languages.length === 0
? site_res.all_languages.map(({ id }) => id)
- : discussion_languages;
+ : res.discussion_languages;
return (
<>
<Sidebar
- community_view={community_view}
- moderators={moderators}
+ community_view={res.community_view}
+ moderators={res.moderators}
admins={site_res.admins}
- online={online}
+ online={res.online}
enableNsfw={enableNsfw(site_res)}
editable
allLanguages={site_res.all_languages}
siteLanguages={site_res.discussion_languages}
communityLanguages={communityLangs}
+ onDeleteCommunity={this.handleDeleteCommunity}
+ onRemoveCommunity={this.handleRemoveCommunity}
+ onLeaveModTeam={this.handleAddModToCommunity}
+ onFollowCommunity={this.handleFollow}
+ onBlockCommunity={this.handleBlockCommunity}
+ onPurgeCommunity={this.handlePurgeCommunity}
+ onEditCommunity={this.handleEditCommunity}
/>
- {!community_view.community.local && site && (
- <SiteSidebar site={site} showLocal={showLocal(this.isoData)} />
+ {!res.community_view.community.local && res.site && (
+ <SiteSidebar site={res.site} showLocal={showLocal(this.isoData)} />
)}
</>
);
}
- get listings() {
+ listings(communityRes: GetCommunityResponse) {
const { dataType } = getCommunityQueryParams();
const { site_res } = this.isoData;
- const { listingsLoading, posts, comments, communityRes } = this.state;
-
- if (listingsLoading) {
- return (
- <h5>
- <Spinner large />
- </h5>
- );
- } else if (dataType === DataType.Post) {
- return (
- <PostListings
- posts={posts}
- removeDuplicates
- enableDownvotes={enableDownvotes(site_res)}
- enableNsfw={enableNsfw(site_res)}
- allLanguages={site_res.all_languages}
- siteLanguages={site_res.discussion_languages}
- />
- );
+
+ if (dataType === DataType.Post) {
+ switch (this.state.postsRes.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success":
+ return (
+ <PostListings
+ posts={this.state.postsRes.data.posts}
+ removeDuplicates
+ enableDownvotes={enableDownvotes(site_res)}
+ enableNsfw={enableNsfw(site_res)}
+ allLanguages={site_res.all_languages}
+ siteLanguages={site_res.discussion_languages}
+ onBlockPerson={this.handleBlockPerson}
+ onPostEdit={this.handlePostEdit}
+ onPostVote={this.handlePostVote}
+ onPostReport={this.handlePostReport}
+ onLockPost={this.handleLockPost}
+ onDeletePost={this.handleDeletePost}
+ onRemovePost={this.handleRemovePost}
+ onSavePost={this.handleSavePost}
+ onPurgePerson={this.handlePurgePerson}
+ onPurgePost={this.handlePurgePost}
+ onBanPerson={this.handleBanPerson}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onFeaturePost={this.handleFeaturePost}
+ />
+ );
+ }
} else {
- return (
- <CommentNodes
- nodes={commentsToFlatNodes(comments)}
- viewType={CommentViewType.Flat}
- noIndent
- showContext
- enableDownvotes={enableDownvotes(site_res)}
- moderators={communityRes?.moderators}
- admins={site_res.admins}
- allLanguages={site_res.all_languages}
- siteLanguages={site_res.discussion_languages}
- />
- );
+ switch (this.state.commentsRes.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success":
+ return (
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.commentsRes.data.comments)}
+ viewType={CommentViewType.Flat}
+ finished={this.state.finished}
+ noIndent
+ showContext
+ enableDownvotes={enableDownvotes(site_res)}
+ moderators={communityRes.moderators}
+ admins={site_res.admins}
+ allLanguages={site_res.all_languages}
+ siteLanguages={site_res.discussion_languages}
+ onSaveComment={this.handleSaveComment}
+ onBlockPerson={this.handleBlockPerson}
+ onDeleteComment={this.handleDeleteComment}
+ onRemoveComment={this.handleRemoveComment}
+ onCommentVote={this.handleCommentVote}
+ onCommentReport={this.handleCommentReport}
+ onDistinguishComment={this.handleDistinguishComment}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onPurgeComment={this.handlePurgeComment}
+ onPurgePerson={this.handlePurgePerson}
+ onCommentReplyRead={this.handleCommentReplyRead}
+ onPersonMentionRead={this.handlePersonMentionRead}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onBanPerson={this.handleBanPerson}
+ onCreateComment={this.handleCreateComment}
+ onEditComment={this.handleEditComment}
+ />
+ );
+ }
}
}
- get communityInfo() {
- const community = this.state.communityRes?.community_view.community;
+ communityInfo(res: GetCommunityResponse) {
+ const community = res.community_view.community;
return (
community && (
);
}
- get selects() {
+ selects(res: GetCommunityResponse) {
// let communityRss = this.state.communityRes.map(r =>
// communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
// );
const { dataType, sort } = getCommunityQueryParams();
- const res = this.state.communityRes;
const communityRss = res
? communityRSSUrl(res.community_view.community.actor_id, sort)
: undefined;
}));
}
- updateUrl({ dataType, page, sort }: Partial<CommunityProps>) {
+ async updateUrl({ dataType, page, sort }: Partial<CommunityProps>) {
const {
dataType: urlDataType,
page: urlPage,
`/c/${this.props.match.params.name}${getQueryString(queryParams)}`
);
- this.setState({
- comments: [],
- posts: [],
- listingsLoading: true,
- });
-
- this.fetchData();
+ await this.fetchData();
}
- fetchData() {
+ async fetchData() {
const { dataType, page, sort } = getCommunityQueryParams();
const { name } = this.props.match.params;
- let req: string;
if (dataType === DataType.Post) {
- const form: GetPosts = {
- page,
- limit: fetchLimit,
- sort,
- type_: "All",
- community_name: name,
- saved_only: false,
- auth: myAuth(false),
- };
- req = wsClient.getPosts(form);
+ this.setState({ postsRes: { state: "loading" } });
+ this.setState({
+ postsRes: await HttpService.client.getPosts({
+ page,
+ limit: fetchLimit,
+ sort,
+ type_: "All",
+ community_name: name,
+ saved_only: false,
+ auth: myAuth(),
+ }),
+ });
} else {
- const form: GetComments = {
- page,
- limit: fetchLimit,
- sort: postToCommentSortType(sort),
- type_: "All",
- community_name: name,
- saved_only: false,
- auth: myAuth(false),
- };
-
- req = wsClient.getComments(form);
+ this.setState({ commentsRes: { state: "loading" } });
+ this.setState({
+ commentsRes: await HttpService.client.getComments({
+ page,
+ limit: fetchLimit,
+ sort: postToCommentSortType(sort),
+ type_: "All",
+ community_name: name,
+ saved_only: false,
+ auth: myAuth(),
+ }),
+ });
}
- WebSocketService.Instance.send(req);
+ restoreScrollPosition(this.context);
+ setupTippy();
}
- parseMessage(msg: any) {
- const { page } = getCommunityQueryParams();
- const op = wsUserOp(msg);
- console.log(msg);
- const res = this.state.communityRes;
-
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- this.context.router.history.push("/");
- } else if (msg.reconnect) {
- if (res) {
- WebSocketService.Instance.send(
- wsClient.communityJoin({
- community_id: res.community_view.community.id,
- })
- );
- }
-
- this.fetchData();
- } else {
- switch (op) {
- case UserOperation.GetCommunity: {
- const data = wsJsonToRes<GetCommunityResponse>(msg);
-
- this.setState({ communityRes: data, communityLoading: false });
- // TODO why is there no auth in this form?
- WebSocketService.Instance.send(
- wsClient.communityJoin({
- community_id: data.community_view.community.id,
- })
- );
+ async handleDeleteCommunity(form: DeleteCommunity) {
+ const deleteCommunityRes = await HttpService.client.deleteCommunity(form);
+ this.updateCommunity(deleteCommunityRes);
+ }
- break;
- }
+ async handleAddModToCommunity(form: AddModToCommunity) {
+ const addModRes = await HttpService.client.addModToCommunity(form);
+ this.updateModerators(addModRes);
+ }
- case UserOperation.EditCommunity:
- case UserOperation.DeleteCommunity:
- case UserOperation.RemoveCommunity: {
- const { community_view, discussion_languages } =
- wsJsonToRes<CommunityResponse>(msg);
+ async handleFollow(form: FollowCommunity) {
+ const followCommunityRes = await HttpService.client.followCommunity(form);
+ this.updateCommunity(followCommunityRes);
- if (res) {
- res.community_view = community_view;
- res.discussion_languages = discussion_languages;
- this.setState(this.state);
- }
+ // Update myUserInfo
+ if (followCommunityRes.state == "success") {
+ const communityId = followCommunityRes.data.community_view.community.id;
+ const mui = UserService.Instance.myUserInfo;
+ if (mui) {
+ mui.follows = mui.follows.filter(i => i.community.id != communityId);
+ }
+ }
+ }
- break;
- }
+ async handlePurgeCommunity(form: PurgeCommunity) {
+ const purgeCommunityRes = await HttpService.client.purgeCommunity(form);
+ this.purgeItem(purgeCommunityRes);
+ }
- case UserOperation.FollowCommunity: {
- const {
- community_view: {
- subscribed,
- counts: { subscribers },
- },
- } = wsJsonToRes<CommunityResponse>(msg);
-
- if (res) {
- res.community_view.subscribed = subscribed;
- res.community_view.counts.subscribers = subscribers;
- this.setState(this.state);
- }
-
- break;
- }
+ async handlePurgePerson(form: PurgePerson) {
+ const purgePersonRes = await HttpService.client.purgePerson(form);
+ this.purgeItem(purgePersonRes);
+ }
- case UserOperation.GetPosts: {
- const { posts } = wsJsonToRes<GetPostsResponse>(msg);
+ async handlePurgeComment(form: PurgeComment) {
+ const purgeCommentRes = await HttpService.client.purgeComment(form);
+ this.purgeItem(purgeCommentRes);
+ }
- this.setState({ posts, listingsLoading: false });
- restoreScrollPosition(this.context);
- setupTippy();
+ async handlePurgePost(form: PurgePost) {
+ const purgeRes = await HttpService.client.purgePost(form);
+ this.purgeItem(purgeRes);
+ }
- break;
- }
+ async handleBlockCommunity(form: BlockCommunity) {
+ const blockCommunityRes = await HttpService.client.blockCommunity(form);
+ if (blockCommunityRes.state == "success") {
+ updateCommunityBlock(blockCommunityRes.data);
+ }
+ }
- case UserOperation.EditPost:
- case UserOperation.DeletePost:
- case UserOperation.RemovePost:
- case UserOperation.LockPost:
- case UserOperation.FeaturePost:
- case UserOperation.SavePost: {
- const { post_view } = wsJsonToRes<PostResponse>(msg);
+ async handleBlockPerson(form: BlockPerson) {
+ const blockPersonRes = await HttpService.client.blockPerson(form);
+ if (blockPersonRes.state == "success") {
+ updatePersonBlock(blockPersonRes.data);
+ }
+ }
- editPostFindRes(post_view, this.state.posts);
- this.setState(this.state);
+ async handleRemoveCommunity(form: RemoveCommunity) {
+ const removeCommunityRes = await HttpService.client.removeCommunity(form);
+ this.updateCommunity(removeCommunityRes);
+ }
- break;
- }
+ async handleEditCommunity(form: EditCommunity) {
+ const res = await HttpService.client.editCommunity(form);
+ this.updateCommunity(res);
- case UserOperation.CreatePost: {
- const { post_view } = wsJsonToRes<PostResponse>(msg);
+ return res;
+ }
- const showPostNotifs =
- UserService.Instance.myUserInfo?.local_user_view.local_user
- .show_new_post_notifs;
+ async handleCreateComment(form: CreateComment) {
+ const createCommentRes = await HttpService.client.createComment(form);
+ this.createAndUpdateComments(createCommentRes);
- // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
- if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
- this.state.posts.unshift(post_view);
- if (showPostNotifs) {
- notifyPost(post_view, this.context.router);
- }
- this.setState(this.state);
- }
+ return createCommentRes;
+ }
- break;
- }
+ async handleEditComment(form: EditComment) {
+ const editCommentRes = await HttpService.client.editComment(form);
+ this.findAndUpdateComment(editCommentRes);
- case UserOperation.CreatePostLike: {
- const { post_view } = wsJsonToRes<PostResponse>(msg);
+ return editCommentRes;
+ }
- createPostLikeFindRes(post_view, this.state.posts);
- this.setState(this.state);
+ async handleDeleteComment(form: DeleteComment) {
+ const deleteCommentRes = await HttpService.client.deleteComment(form);
+ this.findAndUpdateComment(deleteCommentRes);
+ }
- break;
- }
+ async handleDeletePost(form: DeletePost) {
+ const deleteRes = await HttpService.client.deletePost(form);
+ this.findAndUpdatePost(deleteRes);
+ }
- case UserOperation.AddModToCommunity: {
- const { moderators } = wsJsonToRes<AddModToCommunityResponse>(msg);
+ async handleRemovePost(form: RemovePost) {
+ const removeRes = await HttpService.client.removePost(form);
+ this.findAndUpdatePost(removeRes);
+ }
- if (res) {
- res.moderators = moderators;
- this.setState(this.state);
- }
+ async handleRemoveComment(form: RemoveComment) {
+ const removeCommentRes = await HttpService.client.removeComment(form);
+ this.findAndUpdateComment(removeCommentRes);
+ }
- break;
- }
+ async handleSaveComment(form: SaveComment) {
+ const saveCommentRes = await HttpService.client.saveComment(form);
+ this.findAndUpdateComment(saveCommentRes);
+ }
- case UserOperation.BanFromCommunity: {
- const {
- person_view: {
- person: { id: personId },
- },
- banned,
- } = wsJsonToRes<BanFromCommunityResponse>(msg);
+ async handleSavePost(form: SavePost) {
+ const saveRes = await HttpService.client.savePost(form);
+ this.findAndUpdatePost(saveRes);
+ }
- // TODO this might be incorrect
- this.state.posts
- .filter(p => p.creator.id === personId)
- .forEach(p => (p.creator_banned_from_community = banned));
+ async handleFeaturePost(form: FeaturePost) {
+ const featureRes = await HttpService.client.featurePost(form);
+ this.findAndUpdatePost(featureRes);
+ }
- this.setState(this.state);
+ async handleCommentVote(form: CreateCommentLike) {
+ const voteRes = await HttpService.client.likeComment(form);
+ this.findAndUpdateComment(voteRes);
+ }
- break;
- }
+ async handlePostEdit(form: EditPost) {
+ const res = await HttpService.client.editPost(form);
+ this.findAndUpdatePost(res);
+ }
- case UserOperation.GetComments: {
- const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
- this.setState({ comments, listingsLoading: false });
+ async handlePostVote(form: CreatePostLike) {
+ const voteRes = await HttpService.client.likePost(form);
+ this.findAndUpdatePost(voteRes);
+ }
- break;
- }
+ async handleCommentReport(form: CreateCommentReport) {
+ const reportRes = await HttpService.client.createCommentReport(form);
+ if (reportRes.state == "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
- case UserOperation.EditComment:
- case UserOperation.DeleteComment:
- case UserOperation.RemoveComment: {
- const { comment_view } = wsJsonToRes<CommentResponse>(msg);
- editCommentRes(comment_view, this.state.comments);
- this.setState(this.state);
+ async handlePostReport(form: CreatePostReport) {
+ const reportRes = await HttpService.client.createPostReport(form);
+ if (reportRes.state == "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
- break;
- }
+ async handleLockPost(form: LockPost) {
+ const lockRes = await HttpService.client.lockPost(form);
+ this.findAndUpdatePost(lockRes);
+ }
- case UserOperation.CreateComment: {
- const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
+ async handleDistinguishComment(form: DistinguishComment) {
+ const distinguishRes = await HttpService.client.distinguishComment(form);
+ this.findAndUpdateComment(distinguishRes);
+ }
- // Necessary since it might be a user reply
- if (form_id) {
- this.setState(({ comments }) => ({
- comments: [comment_view].concat(comments),
- }));
- }
+ async handleAddAdmin(form: AddAdmin) {
+ const addAdminRes = await HttpService.client.addAdmin(form);
- break;
- }
+ if (addAdminRes.state == "success") {
+ this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+ }
+ }
- case UserOperation.SaveComment: {
- const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+ async handleTransferCommunity(form: TransferCommunity) {
+ const transferCommunityRes = await HttpService.client.transferCommunity(
+ form
+ );
+ toast(i18n.t("transfer_community"));
+ this.updateCommunityFull(transferCommunityRes);
+ }
- saveCommentRes(comment_view, this.state.comments);
- this.setState(this.state);
+ async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+ const readRes = await HttpService.client.markCommentReplyAsRead(form);
+ this.findAndUpdateCommentReply(readRes);
+ }
- break;
- }
+ async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+ // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
+ await HttpService.client.markPersonMentionAsRead(form);
+ }
- case UserOperation.CreateCommentLike: {
- const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+ async handleBanFromCommunity(form: BanFromCommunity) {
+ const banRes = await HttpService.client.banFromCommunity(form);
+ this.updateBanFromCommunity(banRes);
+ }
- createCommentLikeRes(comment_view, this.state.comments);
- this.setState(this.state);
+ async handleBanPerson(form: BanPerson) {
+ const banRes = await HttpService.client.banPerson(form);
+ this.updateBan(banRes);
+ }
- break;
+ updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (s.postsRes.state == "success") {
+ s.postsRes.data.posts
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
}
+ if (s.commentsRes.state == "success") {
+ s.commentsRes.data.comments
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
+ }
+ return s;
+ });
+ }
+ }
- case UserOperation.BlockPerson: {
- const data = wsJsonToRes<BlockPersonResponse>(msg);
- updatePersonBlock(data);
-
- break;
+ updateBan(banRes: RequestState<BanPersonResponse>) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (s.postsRes.state == "success") {
+ s.postsRes.data.posts
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
+ }
+ if (s.commentsRes.state == "success") {
+ s.commentsRes.data.comments
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
}
+ return s;
+ });
+ }
+ }
- case UserOperation.CreatePostReport:
- case UserOperation.CreateCommentReport: {
- const data = wsJsonToRes<PostReportResponse>(msg);
+ updateCommunity(res: RequestState<CommunityResponse>) {
+ this.setState(s => {
+ if (s.communityRes.state == "success" && res.state == "success") {
+ s.communityRes.data.community_view = res.data.community_view;
+ s.communityRes.data.discussion_languages =
+ res.data.discussion_languages;
+ }
+ return s;
+ });
+ }
+
+ updateCommunityFull(res: RequestState<GetCommunityResponse>) {
+ this.setState(s => {
+ if (s.communityRes.state == "success" && res.state == "success") {
+ s.communityRes.data.community_view = res.data.community_view;
+ s.communityRes.data.moderators = res.data.moderators;
+ }
+ return s;
+ });
+ }
- if (data) {
- toast(i18n.t("report_created"));
- }
+ purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
+ if (purgeRes.state == "success") {
+ toast(i18n.t("purge_success"));
+ this.context.router.history.push(`/`);
+ }
+ }
- break;
- }
+ findAndUpdateComment(res: RequestState<CommentResponse>) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments = editComment(
+ res.data.comment_view,
+ s.commentsRes.data.comments
+ );
+ s.finished.set(res.data.comment_view.comment.id, true);
+ }
+ return s;
+ });
+ }
- case UserOperation.PurgeCommunity: {
- const { success } = wsJsonToRes<PurgeItemResponse>(msg);
+ createAndUpdateComments(res: RequestState<CommentResponse>) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments.unshift(res.data.comment_view);
- if (success) {
- toast(i18n.t("purge_success"));
- this.context.router.history.push(`/`);
- }
+ // Set finished for the parent
+ s.finished.set(
+ getCommentParentId(res.data.comment_view.comment) ?? 0,
+ true
+ );
+ }
+ return s;
+ });
+ }
- break;
- }
+ findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments = editWith(
+ res.data.comment_reply_view,
+ s.commentsRes.data.comments
+ );
+ }
+ return s;
+ });
+ }
- case UserOperation.BlockCommunity: {
- const data = wsJsonToRes<BlockCommunityResponse>(msg);
- if (res) {
- res.community_view.blocked = data.blocked;
- this.setState(this.state);
- }
- updateCommunityBlock(data);
+ findAndUpdatePost(res: RequestState<PostResponse>) {
+ this.setState(s => {
+ if (s.postsRes.state == "success" && res.state == "success") {
+ s.postsRes.data.posts = editPost(
+ res.data.post_view,
+ s.postsRes.data.posts
+ );
+ }
+ return s;
+ });
+ }
- break;
- }
+ updateModerators(res: RequestState<AddModToCommunityResponse>) {
+ // Update the moderators
+ this.setState(s => {
+ if (s.communityRes.state == "success" && res.state == "success") {
+ s.communityRes.data.moderators = res.data.moderators;
}
- }
+ return s;
+ });
}
}
- import autosize from "autosize";
import { Component, linkEvent } from "inferno";
import {
BannedPersonsResponse,
+ CreateCustomEmoji,
+ DeleteCustomEmoji,
+ EditCustomEmoji,
+ EditSite,
GetFederatedInstancesResponse,
GetSiteResponse,
PersonView,
- SiteResponse,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
- import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces";
- import { WebSocketService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
import {
- WithPromiseKeys,
++ RouteDataResponse,
capitalizeFirstLetter,
- isBrowser,
- myAuth,
- randomStr,
+ fetchThemeList,
+ myAuthRequired,
+ removeFromEmojiDataModel,
setIsoData,
showLocal,
toast,
- wsClient,
- wsSubscribe,
+ updateEmojiDataModel,
} from "../../utils";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
import { SiteForm } from "./site-form";
import { TaglineForm } from "./tagline-form";
- interface AdminSettingsData {
++type AdminSettingsData = RouteDataResponse<{
+ bannedPersonsResponse: BannedPersonsResponse;
+ federatedInstancesResponse: GetFederatedInstancesResponse;
- }
++}>;
+
interface AdminSettingsState {
siteRes: GetSiteResponse;
- instancesRes?: GetFederatedInstancesResponse;
banned: PersonView[];
- loading: boolean;
- leaveAdminTeamLoading: boolean;
+ currentTab: string;
+ instancesRes: RequestState<GetFederatedInstancesResponse>;
+ bannedRes: RequestState<BannedPersonsResponse>;
+ leaveAdminTeamRes: RequestState<GetSiteResponse>;
+ themeList: string[];
+ isIsomorphic: boolean;
}
export class AdminSettings extends Component<any, AdminSettingsState> {
- private siteConfigTextAreaId = `site-config-${randomStr()}`;
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<AdminSettingsData>(this.context);
- private subscription?: Subscription;
state: AdminSettingsState = {
siteRes: this.isoData.site_res,
banned: [],
- loading: true,
- leaveAdminTeamLoading: false,
+ currentTab: "site",
+ bannedRes: { state: "empty" },
+ instancesRes: { state: "empty" },
+ leaveAdminTeamRes: { state: "empty" },
+ themeList: [],
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
super(props, context);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ this.handleEditSite = this.handleEditSite.bind(this);
+ this.handleEditEmoji = this.handleEditEmoji.bind(this);
+ this.handleDeleteEmoji = this.handleDeleteEmoji.bind(this);
+ this.handleCreateEmoji = this.handleCreateEmoji.bind(this);
// Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
- const { bannedPersonsResponse, federatedInstancesResponse } =
- this.isoData.routeData;
+ if (FirstLoadService.isFirstLoad) {
- const [bannedRes, instancesRes] = this.isoData.routeData;
++ const {
++ bannedPersonsResponse: bannedRes,
++ federatedInstancesResponse: instancesRes,
++ } = this.isoData.routeData;
+
this.state = {
...this.state,
- banned: bannedPersonsResponse.banned,
- instancesRes: federatedInstancesResponse,
- loading: false,
+ bannedRes,
+ instancesRes,
+ isIsomorphic: true,
};
- } else {
- let cAuth = myAuth();
- if (cAuth) {
- WebSocketService.Instance.send(
- wsClient.getBannedPersons({
- auth: cAuth,
- })
- );
- WebSocketService.Instance.send(
- wsClient.getFederatedInstances({ auth: cAuth })
- );
- }
}
}
- async fetchData() {
- this.setState({
- bannedRes: { state: "loading" },
- instancesRes: { state: "loading" },
- themeList: [],
- });
-
- const auth = myAuthRequired();
-
- const [bannedRes, instancesRes, themeList] = await Promise.all([
- HttpService.client.getBannedPersons({ auth }),
- HttpService.client.getFederatedInstances({ auth }),
- fetchThemeList(),
- ]);
-
- this.setState({
- bannedRes,
- instancesRes,
- themeList,
- });
- }
-
-- static fetchInitialData({
++ static async fetchInitialData({
auth,
client,
- }: InitialFetchRequest): WithPromiseKeys<AdminSettingsData> {
- }: InitialFetchRequest): Promise<any>[] {
- const promises: Promise<RequestState<any>>[] = [];
-
- if (auth) {
- promises.push(client.getBannedPersons({ auth }));
- promises.push(client.getFederatedInstances({ auth }));
- } else {
- promises.push(
- Promise.resolve({ state: "empty" }),
- Promise.resolve({ state: "empty" })
- );
- }
-
- return promises;
++ }: InitialFetchRequest): Promise<AdminSettingsData> {
+ return {
- bannedPersonsResponse: client.getBannedPersons({ auth: auth as string }),
- federatedInstancesResponse: client.getFederatedInstances({
++ bannedPersonsResponse: await client.getBannedPersons({
+ auth: auth as string,
- }) as Promise<GetFederatedInstancesResponse>,
++ }),
++ federatedInstancesResponse: await client.getFederatedInstances({
++ auth: auth as string,
++ }),
+ };
}
- componentDidMount() {
- if (isBrowser()) {
- var textarea: any = document.getElementById(this.siteConfigTextAreaId);
- autosize(textarea);
- }
- }
-
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.fetchData();
}
}
}
render() {
+ const federationData =
+ this.state.instancesRes.state === "success"
+ ? this.state.instancesRes.data.federated_instances
+ : undefined;
+
return (
<div className="container-lg">
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
- {this.state.loading ? (
- <h5>
- <Spinner large />
- </h5>
- ) : (
- <Tabs
- tabs={[
- {
- key: "site",
- label: i18n.t("site"),
- getNode: () => (
- <div className="row">
- <div className="col-12 col-md-6">
- <SiteForm
- siteRes={this.state.siteRes}
- instancesRes={this.state.instancesRes}
- showLocal={showLocal(this.isoData)}
- />
- </div>
- <div className="col-12 col-md-6">
- {this.admins()}
- {this.bannedUsers()}
- </div>
- </div>
- ),
- },
- {
- key: "rate_limiting",
- label: "Rate Limiting",
- getNode: () => (
- <RateLimitForm
- localSiteRateLimit={
- this.state.siteRes.site_view.local_site_rate_limit
- }
- applicationQuestion={
- this.state.siteRes.site_view.local_site
- .application_question
- }
- />
- ),
- },
- {
- key: "taglines",
- label: i18n.t("taglines"),
- getNode: () => (
- <div className="row">
- <TaglineForm siteRes={this.state.siteRes} />
+ <Tabs
+ tabs={[
+ {
+ key: "site",
+ label: i18n.t("site"),
+ getNode: () => (
+ <div className="row">
+ <div className="col-12 col-md-6">
+ <SiteForm
+ showLocal={showLocal(this.isoData)}
+ allowedInstances={federationData?.allowed}
+ blockedInstances={federationData?.blocked}
+ onSaveSite={this.handleEditSite}
+ siteRes={this.state.siteRes}
+ themeList={this.state.themeList}
+ />
</div>
- ),
- },
- {
- key: "emojis",
- label: i18n.t("emojis"),
- getNode: () => (
- <div className="row">
- <EmojiForm />
+ <div className="col-12 col-md-6">
+ {this.admins()}
+ {this.bannedUsers()}
</div>
- ),
- },
- ]}
- />
- )}
+ </div>
+ ),
+ },
+ {
+ key: "rate_limiting",
+ label: "Rate Limiting",
+ getNode: () => (
+ <RateLimitForm
+ rateLimits={
+ this.state.siteRes.site_view.local_site_rate_limit
+ }
+ onSaveSite={this.handleEditSite}
+ />
+ ),
+ },
+ {
+ key: "taglines",
+ label: i18n.t("taglines"),
+ getNode: () => (
+ <div className="row">
+ <TaglineForm
+ taglines={this.state.siteRes.taglines}
+ onSaveSite={this.handleEditSite}
+ />
+ </div>
+ ),
+ },
+ {
+ key: "emojis",
+ label: i18n.t("emojis"),
+ getNode: () => (
+ <div className="row">
+ <EmojiForm
+ onCreate={this.handleCreateEmoji}
+ onDelete={this.handleDeleteEmoji}
+ onEdit={this.handleEditEmoji}
+ />
+ </div>
+ ),
+ },
+ ]}
+ />
</div>
);
}
++ async fetchData() {
++ this.setState({
++ bannedRes: { state: "loading" },
++ instancesRes: { state: "loading" },
++ themeList: [],
++ });
++
++ const auth = myAuthRequired();
++
++ const [bannedRes, instancesRes, themeList] = await Promise.all([
++ HttpService.client.getBannedPersons({ auth }),
++ HttpService.client.getFederatedInstances({ auth }),
++ fetchThemeList(),
++ ]);
++
++ this.setState({
++ bannedRes,
++ instancesRes,
++ themeList,
++ });
++ }
++
admins() {
return (
<>
onClick={linkEvent(this, this.handleLeaveAdminTeam)}
className="btn btn-danger mb-2"
>
- {this.state.leaveAdminTeamLoading ? (
+ {this.state.leaveAdminTeamRes.state == "loading" ? (
<Spinner />
) : (
i18n.t("leave_admin_team")
}
bannedUsers() {
- return (
- <>
- <h5>{i18n.t("banned_users")}</h5>
- <ul className="list-unstyled">
- {this.state.banned.map(banned => (
- <li key={banned.person.id} className="list-inline-item">
- <PersonListing person={banned.person} />
- </li>
- ))}
- </ul>
- </>
- );
+ switch (this.state.bannedRes.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const bans = this.state.bannedRes.data.banned;
+ return (
+ <>
+ <h5>{i18n.t("banned_users")}</h5>
+ <ul className="list-unstyled">
+ {bans.map(banned => (
+ <li key={banned.person.id} className="list-inline-item">
+ <PersonListing person={banned.person} />
+ </li>
+ ))}
+ </ul>
+ </>
+ );
+ }
+ }
}
- handleLeaveAdminTeam(i: AdminSettings) {
- let auth = myAuth();
- if (auth) {
- i.setState({ leaveAdminTeamLoading: true });
- WebSocketService.Instance.send(wsClient.leaveAdmin({ auth }));
+ async handleEditSite(form: EditSite) {
+ const editRes = await HttpService.client.editSite(form);
+
+ if (editRes.state === "success") {
+ this.setState(s => {
+ s.siteRes.site_view = editRes.data.site_view;
+ // TODO: Where to get taglines from?
+ s.siteRes.taglines = editRes.data.taglines;
+ return s;
+ });
+ toast(i18n.t("site_saved"));
}
+
+ return editRes;
}
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- this.context.router.history.push("/");
- this.setState({ loading: false });
- return;
- } else if (op == UserOperation.EditSite) {
- let data = wsJsonToRes<SiteResponse>(msg);
- this.setState(s => ((s.siteRes.site_view = data.site_view), s));
- toast(i18n.t("site_saved"));
- } else if (op == UserOperation.GetBannedPersons) {
- let data = wsJsonToRes<BannedPersonsResponse>(msg);
- this.setState({ banned: data.banned, loading: false });
- } else if (op == UserOperation.LeaveAdmin) {
- let data = wsJsonToRes<GetSiteResponse>(msg);
- this.setState(s => ((s.siteRes.site_view = data.site_view), s));
- this.setState({ leaveAdminTeamLoading: false });
+ handleSwitchTab(i: { ctx: AdminSettings; tab: string }) {
+ i.ctx.setState({ currentTab: i.tab });
+ }
+
+ async handleLeaveAdminTeam(i: AdminSettings) {
+ i.setState({ leaveAdminTeamRes: { state: "loading" } });
+ this.setState({
+ leaveAdminTeamRes: await HttpService.client.leaveAdmin({
+ auth: myAuthRequired(),
+ }),
+ });
+
+ if (this.state.leaveAdminTeamRes.state === "success") {
toast(i18n.t("left_admin_team"));
- this.context.router.history.push("/");
- } else if (op == UserOperation.GetFederatedInstances) {
- let data = wsJsonToRes<GetFederatedInstancesResponse>(msg);
- this.setState({ instancesRes: data });
+ this.context.router.history.replace("/");
+ }
+ }
+
+ async handleEditEmoji(form: EditCustomEmoji) {
+ const res = await HttpService.client.editCustomEmoji(form);
+ if (res.state === "success") {
+ updateEmojiDataModel(res.data.custom_emoji);
+ }
+ }
+
+ async handleDeleteEmoji(form: DeleteCustomEmoji) {
+ const res = await HttpService.client.deleteCustomEmoji(form);
+ if (res.state === "success") {
+ removeFromEmojiDataModel(res.data.id);
+ }
+ }
+
+ async handleCreateEmoji(form: CreateCustomEmoji) {
+ const res = await HttpService.client.createCustomEmoji(form);
+ if (res.state === "success") {
+ updateEmojiDataModel(res.data.custom_emoji);
}
}
}
import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router";
import {
- AddAdminResponse,
+ AddAdmin,
+ AddModToCommunity,
+ BanFromCommunity,
+ BanFromCommunityResponse,
+ BanPerson,
BanPersonResponse,
- BlockPersonResponse,
- CommentReportResponse,
+ BlockPerson,
+ CommentId,
+ CommentReplyResponse,
CommentResponse,
- CommentView,
- CommunityView,
+ CreateComment,
+ CreateCommentLike,
+ CreateCommentReport,
+ CreatePostLike,
+ CreatePostReport,
+ DeleteComment,
+ DeletePost,
+ DistinguishComment,
+ EditComment,
+ EditPost,
+ FeaturePost,
GetComments,
GetCommentsResponse,
GetPosts,
ListCommunities,
ListCommunitiesResponse,
ListingType,
- PostReportResponse,
+ LockPost,
+ MarkCommentReplyAsRead,
+ MarkPersonMentionAsRead,
PostResponse,
- PostView,
+ PurgeComment,
PurgeItemResponse,
- SiteResponse,
+ PurgePerson,
+ PurgePost,
+ RemoveComment,
+ RemovePost,
+ SaveComment,
+ SavePost,
SortType,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
+ TransferCommunity,
} from "lemmy-js-client";
- import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import {
CommentViewType,
DataType,
InitialFetchRequest,
} from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
import {
canCreateCommunity,
commentsToFlatNodes,
- createCommentLikeRes,
- createPostLikeFindRes,
- editCommentRes,
- editPostFindRes,
+ editComment,
+ editPost,
+ editWith,
enableDownvotes,
enableNsfw,
fetchLimit,
+ getCommentParentId,
getDataTypeString,
getPageFromString,
getQueryParams,
getQueryString,
getRandomFromList,
- isBrowser,
- isPostBlocked,
mdToHtml,
myAuth,
- notifyPost,
- nsfwCheck,
postToCommentSortType,
QueryParams,
relTags,
restoreScrollPosition,
- saveCommentRes,
++ RouteDataResponse,
saveScrollPosition,
setIsoData,
setupTippy,
toast,
trendingFetchLimit,
updatePersonBlock,
- WithPromiseKeys,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { CommentNodes } from "../comment/comment-nodes";
import { DataTypeSelect } from "../common/data-type-select";
import { SiteSidebar } from "./site-sidebar";
interface HomeState {
- trendingCommunities: CommunityView[];
- siteRes: GetSiteResponse;
- posts: PostView[];
- comments: CommentView[];
+ postsRes: RequestState<GetPostsResponse>;
+ commentsRes: RequestState<GetCommentsResponse>;
+ trendingCommunitiesRes: RequestState<ListCommunitiesResponse>;
showSubscribedMobile: boolean;
showTrendingMobile: boolean;
showSidebarMobile: boolean;
subscribedCollapsed: boolean;
- loading: boolean;
tagline?: string;
+ siteRes: GetSiteResponse;
+ finished: Map<CommentId, boolean | undefined>;
+ isIsomorphic: boolean;
}
interface HomeProps {
page: number;
}
- interface HomeData {
++type HomeData = RouteDataResponse<{
+ postsResponse?: GetPostsResponse;
+ commentsResponse?: GetCommentsResponse;
+ trendingResponse: ListCommunitiesResponse;
- }
++}>;
+
function getDataTypeFromQuery(type?: string): DataType {
return type ? DataType[type] : DataType.Post;
}
UserService.Instance.myUserInfo?.local_user_view?.local_user
?.default_listing_type;
- return type ? (type as ListingType) : myListingType ?? "Local";
+ return (type ? (type as ListingType) : myListingType) ?? "Local";
}
function getSortTypeFromQuery(type?: string): SortType {
UserService.Instance.myUserInfo?.local_user_view?.local_user
?.default_sort_type;
- return type ? (type as SortType) : mySortType ?? "Active";
+ return (type ? (type as SortType) : mySortType) ?? "Active";
}
const getHomeQueryParams = () =>
dataType: getDataTypeFromQuery,
});
- function fetchTrendingCommunities() {
- const listCommunitiesForm: ListCommunities = {
- type_: "Local",
- sort: "Hot",
- limit: trendingFetchLimit,
- auth: myAuth(false),
- };
- WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
- }
-
- function fetchData() {
- const auth = myAuth(false);
- const { dataType, page, listingType, sort } = getHomeQueryParams();
- let req: string;
-
- if (dataType === DataType.Post) {
- const getPostsForm: GetPosts = {
- page,
- limit: fetchLimit,
- sort,
- saved_only: false,
- type_: listingType,
- auth,
- };
-
- req = wsClient.getPosts(getPostsForm);
- } else {
- const getCommentsForm: GetComments = {
- page,
- limit: fetchLimit,
- sort: postToCommentSortType(sort),
- saved_only: false,
- type_: listingType,
- auth,
- };
-
- req = wsClient.getComments(getCommentsForm);
- }
-
- WebSocketService.Instance.send(req);
- }
-
const MobileButton = ({
textKey,
show,
</Link>
);
- function getRss(listingType: ListingType) {
- const { sort } = getHomeQueryParams();
- const auth = myAuth(false);
-
- let rss: string | undefined = undefined;
-
- switch (listingType) {
- case "All": {
- rss = `/feeds/all.xml?sort=${sort}`;
- break;
- }
- case "Local": {
- rss = `/feeds/local.xml?sort=${sort}`;
- break;
- }
- case "Subscribed": {
- rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
- break;
- }
- }
-
- return (
- rss && (
- <>
- <a href={rss} rel={relTags} title="RSS">
- <Icon icon="rss" classes="text-muted small" />
- </a>
- <link rel="alternate" type="application/atom+xml" href={rss} />
- </>
- )
- );
- }
-
export class Home extends Component<any, HomeState> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<HomeData>(this.context);
- private subscription?: Subscription;
state: HomeState = {
- trendingCommunities: [],
+ postsRes: { state: "empty" },
+ commentsRes: { state: "empty" },
+ trendingCommunitiesRes: { state: "empty" },
siteRes: this.isoData.site_res,
showSubscribedMobile: false,
showTrendingMobile: false,
showSidebarMobile: false,
subscribedCollapsed: false,
- loading: true,
- posts: [],
- comments: [],
+ finished: new Map(),
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ this.handleCreateComment = this.handleCreateComment.bind(this);
+ this.handleEditComment = this.handleEditComment.bind(this);
+ this.handleSaveComment = this.handleSaveComment.bind(this);
+ this.handleBlockPerson = this.handleBlockPerson.bind(this);
+ this.handleDeleteComment = this.handleDeleteComment.bind(this);
+ this.handleRemoveComment = this.handleRemoveComment.bind(this);
+ this.handleCommentVote = this.handleCommentVote.bind(this);
+ this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+ this.handleAddAdmin = this.handleAddAdmin.bind(this);
+ this.handlePurgePerson = this.handlePurgePerson.bind(this);
+ this.handlePurgeComment = this.handlePurgeComment.bind(this);
+ this.handleCommentReport = this.handleCommentReport.bind(this);
+ this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+ this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+ this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+ this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+ this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+ this.handleBanPerson = this.handleBanPerson.bind(this);
+ this.handlePostEdit = this.handlePostEdit.bind(this);
+ this.handlePostVote = this.handlePostVote.bind(this);
+ this.handlePostReport = this.handlePostReport.bind(this);
+ this.handleLockPost = this.handleLockPost.bind(this);
+ this.handleDeletePost = this.handleDeletePost.bind(this);
+ this.handleRemovePost = this.handleRemovePost.bind(this);
+ this.handleSavePost = this.handleSavePost.bind(this);
+ this.handlePurgePost = this.handlePurgePost.bind(this);
+ this.handleFeaturePost = this.handleFeaturePost.bind(this);
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
- const { trendingResponse, commentsResponse, postsResponse } =
- this.isoData.routeData;
+ if (FirstLoadService.isFirstLoad) {
- const [postsRes, commentsRes, trendingCommunitiesRes] =
- this.isoData.routeData;
++ const {
++ trendingResponse: trendingCommunitiesRes,
++ commentsResponse: commentsRes,
++ postsResponse: postsRes,
++ } = this.isoData.routeData;
- if (postsResponse) {
- this.state = { ...this.state, posts: postsResponse.posts };
- }
+ this.state = {
+ ...this.state,
- postsRes,
- commentsRes,
+ trendingCommunitiesRes,
+ tagline: getRandomFromList(this.state?.siteRes?.taglines ?? [])
+ ?.content,
+ isIsomorphic: true,
+ };
+
- if (commentsResponse) {
- this.state = { ...this.state, comments: commentsResponse.comments };
++ if (commentsRes?.state === "success") {
++ this.state = {
++ ...this.state,
++ commentsRes,
++ };
+ }
+
- if (isBrowser()) {
- WebSocketService.Instance.send(
- wsClient.communityJoin({ community_id: 0 })
- );
++ if (postsRes?.state === "success") {
++ this.state = {
++ ...this.state,
++ postsRes,
++ };
+ }
- const taglines = this.state?.siteRes?.taglines ?? [];
- this.state = {
- ...this.state,
- trendingCommunities: trendingResponse?.communities ?? [],
- loading: false,
- tagline: getRandomFromList(taglines)?.content,
- };
- } else {
- fetchTrendingCommunities();
- fetchData();
}
}
- componentDidMount() {
- // This means it hasn't been set up yet
- if (!this.state.siteRes.site_view.local_site.site_setup) {
- this.context.router.history.push("/setup");
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
}
setupTippy();
}
componentWillUnmount() {
saveScrollPosition(this.context);
- this.subscription?.unsubscribe();
}
-- static fetchInitialData({
++ static async fetchInitialData({
client,
auth,
query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
- }: InitialFetchRequest<QueryParams<HomeProps>>): WithPromiseKeys<HomeData> {
- }: InitialFetchRequest<QueryParams<HomeProps>>): Promise<
- RequestState<any>
- >[] {
++ }: InitialFetchRequest<QueryParams<HomeProps>>): Promise<HomeData> {
const dataType = getDataTypeFromQuery(urlDataType);
// TODO figure out auth default_listingType, default_sort_type
const page = urlPage ? Number(urlPage) : 1;
- const promises: Promise<any>[] = [];
-
- let postsResponse: Promise<GetPostsResponse> | undefined = undefined;
- let commentsResponse: Promise<GetCommentsResponse> | undefined = undefined;
- const promises: Promise<RequestState<any>>[] = [];
++ let postsResponse: RequestState<GetPostsResponse> | undefined = undefined;
++ let commentsResponse: RequestState<GetCommentsResponse> | undefined =
++ undefined;
if (dataType === DataType.Post) {
const getPostsForm: GetPosts = {
auth,
};
- postsResponse = client.getPosts(getPostsForm);
- promises.push(client.getPosts(getPostsForm));
- promises.push(Promise.resolve({ state: "empty" }));
++ postsResponse = await client.getPosts(getPostsForm);
} else {
const getCommentsForm: GetComments = {
page,
saved_only: false,
auth,
};
- promises.push(Promise.resolve({ state: "empty" }));
- promises.push(client.getComments(getCommentsForm));
+
- commentsResponse = client.getComments(getCommentsForm);
++ commentsResponse = await client.getComments(getCommentsForm);
}
const trendingCommunitiesForm: ListCommunities = {
limit: trendingFetchLimit,
auth,
};
-- promises.push(client.listCommunities(trendingCommunitiesForm));
- return promises;
+ return {
- trendingResponse: client.listCommunities(trendingCommunitiesForm),
++ trendingResponse: await client.listCommunities(trendingCommunitiesForm),
+ commentsResponse,
+ postsResponse,
+ };
}
get documentTitle(): string {
admins,
online,
},
- loading,
} = this.state;
return (
<div>
- {!loading && (
- <div>
- <div className="card border-secondary mb-3">
- <div className="card-body">
- {this.trendingCommunities()}
- {canCreateCommunity(this.state.siteRes) && (
- <LinkButton
- path="/create_community"
- translationKey="create_a_community"
- />
- )}
+ <div>
+ <div className="card border-secondary mb-3">
+ <div className="card-body">
+ {this.trendingCommunities()}
+ {canCreateCommunity(this.state.siteRes) && (
<LinkButton
- path="/communities"
- translationKey="explore_communities"
+ path="/create_community"
+ translationKey="create_a_community"
/>
- </div>
+ )}
+ <LinkButton
+ path="/communities"
+ translationKey="explore_communities"
+ />
</div>
- <SiteSidebar
- site={site}
- admins={admins}
- counts={counts}
- online={online}
- showLocal={showLocal(this.isoData)}
- />
- {this.hasFollows && (
- <div className="card border-secondary mb-3">
- <div className="card-body">{this.subscribedCommunities}</div>
- </div>
- )}
</div>
- )}
+ <SiteSidebar
+ site={site}
+ admins={admins}
+ counts={counts}
+ online={online}
+ showLocal={showLocal(this.isoData)}
+ />
+ {this.hasFollows && (
+ <div className="card border-secondary mb-3">
+ <div className="card-body">{this.subscribedCommunities}</div>
+ </div>
+ )}
+ </div>
</div>
);
}
trendingCommunities(isMobile = false) {
- return (
- <div className={!isMobile ? "mb-2" : ""}>
- <h5>
- <T i18nKey="trending_communities">
- #
- <Link className="text-body" to="/communities">
- #
- </Link>
- </T>
- </h5>
- <ul className="list-inline mb-0">
- {this.state.trendingCommunities.map(cv => (
- <li
- key={cv.community.id}
- className="list-inline-item d-inline-block"
- >
- <CommunityLink community={cv.community} />
- </li>
- ))}
- </ul>
- </div>
- );
+ switch (this.state.trendingCommunitiesRes.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const trending = this.state.trendingCommunitiesRes.data.communities;
+ return (
+ <div className={!isMobile ? "mb-2" : ""}>
+ <h5>
+ <T i18nKey="trending_communities">
+ #
+ <Link className="text-body" to="/communities">
+ #
+ </Link>
+ </T>
+ </h5>
+ <ul className="list-inline mb-0">
+ {trending.map(cv => (
+ <li
+ key={cv.community.id}
+ className="list-inline-item d-inline-block"
+ >
+ <CommunityLink community={cv.community} />
+ </li>
+ ))}
+ </ul>
+ </div>
+ );
+ }
+ }
}
get subscribedCommunities() {
);
}
- updateUrl({ dataType, listingType, page, sort }: Partial<HomeProps>) {
+ async updateUrl({ dataType, listingType, page, sort }: Partial<HomeProps>) {
const {
dataType: urlDataType,
listingType: urlListingType,
search: getQueryString(queryParams),
});
- this.setState({
- loading: true,
- posts: [],
- comments: [],
- });
-
- fetchData();
+ await this.fetchData();
}
posts() {
return (
<div className="main-content-wrapper">
- {this.state.loading ? (
- <h5>
- <Spinner large />
- </h5>
- ) : (
- <div>
- {this.selects()}
- {this.listings}
- <Paginator page={page} onChange={this.handlePageChange} />
- </div>
- )}
+ <div>
+ {this.selects}
+ {this.listings}
+ <Paginator page={page} onChange={this.handlePageChange} />
+ </div>
</div>
);
}
get listings() {
const { dataType } = getHomeQueryParams();
- const { siteRes, posts, comments } = this.state;
-
- return dataType === DataType.Post ? (
- <PostListings
- posts={posts}
- showCommunity
- removeDuplicates
- enableDownvotes={enableDownvotes(siteRes)}
- enableNsfw={enableNsfw(siteRes)}
- allLanguages={siteRes.all_languages}
- siteLanguages={siteRes.discussion_languages}
- />
- ) : (
- <CommentNodes
- nodes={commentsToFlatNodes(comments)}
- viewType={CommentViewType.Flat}
- noIndent
- showCommunity
- showContext
- enableDownvotes={enableDownvotes(siteRes)}
- allLanguages={siteRes.all_languages}
- siteLanguages={siteRes.discussion_languages}
- />
- );
+ const siteRes = this.state.siteRes;
+
+ if (dataType === DataType.Post) {
+ switch (this.state.postsRes.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const posts = this.state.postsRes.data.posts;
+ return (
+ <PostListings
+ posts={posts}
+ showCommunity
+ removeDuplicates
+ enableDownvotes={enableDownvotes(siteRes)}
+ enableNsfw={enableNsfw(siteRes)}
+ allLanguages={siteRes.all_languages}
+ siteLanguages={siteRes.discussion_languages}
+ onBlockPerson={this.handleBlockPerson}
+ onPostEdit={this.handlePostEdit}
+ onPostVote={this.handlePostVote}
+ onPostReport={this.handlePostReport}
+ onLockPost={this.handleLockPost}
+ onDeletePost={this.handleDeletePost}
+ onRemovePost={this.handleRemovePost}
+ onSavePost={this.handleSavePost}
+ onPurgePerson={this.handlePurgePerson}
+ onPurgePost={this.handlePurgePost}
+ onBanPerson={this.handleBanPerson}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onFeaturePost={this.handleFeaturePost}
+ />
+ );
+ }
+ }
+ } else {
+ switch (this.state.commentsRes.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const comments = this.state.commentsRes.data.comments;
+ return (
+ <CommentNodes
+ nodes={commentsToFlatNodes(comments)}
+ viewType={CommentViewType.Flat}
+ finished={this.state.finished}
+ noIndent
+ showCommunity
+ showContext
+ enableDownvotes={enableDownvotes(siteRes)}
+ allLanguages={siteRes.all_languages}
+ siteLanguages={siteRes.discussion_languages}
+ onSaveComment={this.handleSaveComment}
+ onBlockPerson={this.handleBlockPerson}
+ onDeleteComment={this.handleDeleteComment}
+ onRemoveComment={this.handleRemoveComment}
+ onCommentVote={this.handleCommentVote}
+ onCommentReport={this.handleCommentReport}
+ onDistinguishComment={this.handleDistinguishComment}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onPurgeComment={this.handlePurgeComment}
+ onPurgePerson={this.handlePurgePerson}
+ onCommentReplyRead={this.handleCommentReplyRead}
+ onPersonMentionRead={this.handlePersonMentionRead}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onBanPerson={this.handleBanPerson}
+ onCreateComment={this.handleCreateComment}
+ onEditComment={this.handleEditComment}
+ />
+ );
+ }
+ }
+ }
}
- selects() {
+ get selects() {
const { listingType, dataType, sort } = getHomeQueryParams();
return (
<span className="mr-2">
<SortSelect sort={sort} onChange={this.handleSortChange} />
</span>
- {getRss(listingType)}
+ {this.getRss(listingType)}
</div>
);
}
+ getRss(listingType: ListingType) {
+ const { sort } = getHomeQueryParams();
+ const auth = myAuth();
+
+ let rss: string | undefined = undefined;
+
+ switch (listingType) {
+ case "All": {
+ rss = `/feeds/all.xml?sort=${sort}`;
+ break;
+ }
+ case "Local": {
+ rss = `/feeds/local.xml?sort=${sort}`;
+ break;
+ }
+ case "Subscribed": {
+ rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
+ break;
+ }
+ }
+
+ return (
+ rss && (
+ <>
+ <a href={rss} rel={relTags} title="RSS">
+ <Icon icon="rss" classes="text-muted small" />
+ </a>
+ <link rel="alternate" type="application/atom+xml" href={rss} />
+ </>
+ )
+ );
+ }
+
+ async fetchTrendingCommunities() {
+ this.setState({ trendingCommunitiesRes: { state: "loading" } });
+ this.setState({
+ trendingCommunitiesRes: await HttpService.client.listCommunities({
+ type_: "Local",
+ sort: "Hot",
+ limit: trendingFetchLimit,
+ auth: myAuth(),
+ }),
+ });
+ }
+
+ async fetchData() {
+ const auth = myAuth();
+ const { dataType, page, listingType, sort } = getHomeQueryParams();
+
+ if (dataType === DataType.Post) {
+ this.setState({ postsRes: { state: "loading" } });
+ this.setState({
+ postsRes: await HttpService.client.getPosts({
+ page,
+ limit: fetchLimit,
+ sort,
+ saved_only: false,
+ type_: listingType,
+ auth,
+ }),
+ });
+ } else {
+ this.setState({ commentsRes: { state: "loading" } });
+ this.setState({
+ commentsRes: await HttpService.client.getComments({
+ page,
+ limit: fetchLimit,
+ sort: postToCommentSortType(sort),
+ saved_only: false,
+ type_: listingType,
+ auth,
+ }),
+ });
+ }
+
+ restoreScrollPosition(this.context);
+ setupTippy();
+ }
+
handleShowSubscribedMobile(i: Home) {
i.setState({ showSubscribedMobile: !i.state.showSubscribedMobile });
}
window.scrollTo(0, 0);
}
- parseMessage(msg: any) {
- const op = wsUserOp(msg);
- console.log(msg);
+ async handleAddModToCommunity(form: AddModToCommunity) {
+ // TODO not sure what to do here
+ await HttpService.client.addModToCommunity(form);
+ }
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- } else if (msg.reconnect) {
- WebSocketService.Instance.send(
- wsClient.communityJoin({ community_id: 0 })
- );
- fetchData();
- } else {
- switch (op) {
- case UserOperation.ListCommunities: {
- const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
- this.setState({ trendingCommunities: communities });
+ async handlePurgePerson(form: PurgePerson) {
+ const purgePersonRes = await HttpService.client.purgePerson(form);
+ this.purgeItem(purgePersonRes);
+ }
- break;
- }
+ async handlePurgeComment(form: PurgeComment) {
+ const purgeCommentRes = await HttpService.client.purgeComment(form);
+ this.purgeItem(purgeCommentRes);
+ }
- case UserOperation.EditSite: {
- const { site_view } = wsJsonToRes<SiteResponse>(msg);
- this.setState(s => ((s.siteRes.site_view = site_view), s));
- toast(i18n.t("site_saved"));
+ async handlePurgePost(form: PurgePost) {
+ const purgeRes = await HttpService.client.purgePost(form);
+ this.purgeItem(purgeRes);
+ }
- break;
- }
+ async handleBlockPerson(form: BlockPerson) {
+ const blockPersonRes = await HttpService.client.blockPerson(form);
+ if (blockPersonRes.state == "success") {
+ updatePersonBlock(blockPersonRes.data);
+ }
+ }
- case UserOperation.GetPosts: {
- const { posts } = wsJsonToRes<GetPostsResponse>(msg);
- this.setState({ posts, loading: false });
- WebSocketService.Instance.send(
- wsClient.communityJoin({ community_id: 0 })
- );
- restoreScrollPosition(this.context);
- setupTippy();
+ async handleCreateComment(form: CreateComment) {
+ const createCommentRes = await HttpService.client.createComment(form);
+ this.createAndUpdateComments(createCommentRes);
- break;
- }
+ return createCommentRes;
+ }
- case UserOperation.CreatePost: {
- const { page, listingType } = getHomeQueryParams();
- const { post_view } = wsJsonToRes<PostResponse>(msg);
-
- // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
- if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
- const mui = UserService.Instance.myUserInfo;
- const showPostNotifs =
- mui?.local_user_view.local_user.show_new_post_notifs;
- let shouldAddPost: boolean;
-
- switch (listingType) {
- case "Subscribed": {
- // If you're on subscribed, only push it if you're subscribed.
- shouldAddPost = !!mui?.follows.some(
- ({ community: { id } }) => id === post_view.community.id
- );
- break;
- }
- case "Local": {
- // If you're on the local view, only push it if its local
- shouldAddPost = post_view.post.local;
- break;
- }
- default: {
- shouldAddPost = true;
- break;
- }
- }
-
- if (shouldAddPost) {
- this.setState(({ posts }) => ({
- posts: [post_view].concat(posts),
- }));
- if (showPostNotifs) {
- notifyPost(post_view, this.context.router);
- }
- }
- }
-
- break;
- }
+ async handleEditComment(form: EditComment) {
+ const editCommentRes = await HttpService.client.editComment(form);
+ this.findAndUpdateComment(editCommentRes);
- case UserOperation.EditPost:
- case UserOperation.DeletePost:
- case UserOperation.RemovePost:
- case UserOperation.LockPost:
- case UserOperation.FeaturePost:
- case UserOperation.SavePost: {
- const { post_view } = wsJsonToRes<PostResponse>(msg);
- editPostFindRes(post_view, this.state.posts);
- this.setState(this.state);
-
- break;
- }
+ return editCommentRes;
+ }
- case UserOperation.CreatePostLike: {
- const { post_view } = wsJsonToRes<PostResponse>(msg);
- createPostLikeFindRes(post_view, this.state.posts);
- this.setState(this.state);
+ async handleDeleteComment(form: DeleteComment) {
+ const deleteCommentRes = await HttpService.client.deleteComment(form);
+ this.findAndUpdateComment(deleteCommentRes);
+ }
- break;
- }
+ async handleDeletePost(form: DeletePost) {
+ const deleteRes = await HttpService.client.deletePost(form);
+ this.findAndUpdatePost(deleteRes);
+ }
- case UserOperation.AddAdmin: {
- const { admins } = wsJsonToRes<AddAdminResponse>(msg);
- this.setState(s => ((s.siteRes.admins = admins), s));
+ async handleRemovePost(form: RemovePost) {
+ const removeRes = await HttpService.client.removePost(form);
+ this.findAndUpdatePost(removeRes);
+ }
- break;
- }
+ async handleRemoveComment(form: RemoveComment) {
+ const removeCommentRes = await HttpService.client.removeComment(form);
+ this.findAndUpdateComment(removeCommentRes);
+ }
- case UserOperation.BanPerson: {
- const {
- banned,
- person_view: {
- person: { id },
- },
- } = wsJsonToRes<BanPersonResponse>(msg);
+ async handleSaveComment(form: SaveComment) {
+ const saveCommentRes = await HttpService.client.saveComment(form);
+ this.findAndUpdateComment(saveCommentRes);
+ }
- this.state.posts
- .filter(p => p.creator.id == id)
- .forEach(p => (p.creator.banned = banned));
- this.setState(this.state);
+ async handleSavePost(form: SavePost) {
+ const saveRes = await HttpService.client.savePost(form);
+ this.findAndUpdatePost(saveRes);
+ }
- break;
- }
+ async handleFeaturePost(form: FeaturePost) {
+ const featureRes = await HttpService.client.featurePost(form);
+ this.findAndUpdatePost(featureRes);
+ }
- case UserOperation.GetComments: {
- const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
- this.setState({ comments, loading: false });
+ async handleCommentVote(form: CreateCommentLike) {
+ const voteRes = await HttpService.client.likeComment(form);
+ this.findAndUpdateComment(voteRes);
+ }
- break;
- }
+ async handlePostEdit(form: EditPost) {
+ const res = await HttpService.client.editPost(form);
+ this.findAndUpdatePost(res);
+ }
- case UserOperation.EditComment:
- case UserOperation.DeleteComment:
- case UserOperation.RemoveComment: {
- const { comment_view } = wsJsonToRes<CommentResponse>(msg);
- editCommentRes(comment_view, this.state.comments);
- this.setState(this.state);
+ async handlePostVote(form: CreatePostLike) {
+ const voteRes = await HttpService.client.likePost(form);
+ this.findAndUpdatePost(voteRes);
+ }
- break;
- }
+ async handleCommentReport(form: CreateCommentReport) {
+ const reportRes = await HttpService.client.createCommentReport(form);
+ if (reportRes.state == "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
- case UserOperation.CreateComment: {
- const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
-
- // Necessary since it might be a user reply
- if (form_id) {
- const { listingType } = getHomeQueryParams();
-
- // If you're on subscribed, only push it if you're subscribed.
- const shouldAddComment =
- listingType === "Subscribed"
- ? UserService.Instance.myUserInfo?.follows.some(
- ({ community: { id } }) => id === comment_view.community.id
- )
- : true;
-
- if (shouldAddComment) {
- this.setState(({ comments }) => ({
- comments: [comment_view].concat(comments),
- }));
- }
- }
-
- break;
- }
+ async handlePostReport(form: CreatePostReport) {
+ const reportRes = await HttpService.client.createPostReport(form);
+ if (reportRes.state == "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
- case UserOperation.SaveComment: {
- const { comment_view } = wsJsonToRes<CommentResponse>(msg);
- saveCommentRes(comment_view, this.state.comments);
- this.setState(this.state);
+ async handleLockPost(form: LockPost) {
+ const lockRes = await HttpService.client.lockPost(form);
+ this.findAndUpdatePost(lockRes);
+ }
- break;
- }
+ async handleDistinguishComment(form: DistinguishComment) {
+ const distinguishRes = await HttpService.client.distinguishComment(form);
+ this.findAndUpdateComment(distinguishRes);
+ }
- case UserOperation.CreateCommentLike: {
- const { comment_view } = wsJsonToRes<CommentResponse>(msg);
- createCommentLikeRes(comment_view, this.state.comments);
- this.setState(this.state);
+ async handleAddAdmin(form: AddAdmin) {
+ const addAdminRes = await HttpService.client.addAdmin(form);
- break;
- }
+ if (addAdminRes.state == "success") {
+ this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+ }
+ }
- case UserOperation.BlockPerson: {
- const data = wsJsonToRes<BlockPersonResponse>(msg);
- updatePersonBlock(data);
+ async handleTransferCommunity(form: TransferCommunity) {
+ await HttpService.client.transferCommunity(form);
+ toast(i18n.t("transfer_community"));
+ }
- break;
- }
+ async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+ const readRes = await HttpService.client.markCommentReplyAsRead(form);
+ this.findAndUpdateCommentReply(readRes);
+ }
- case UserOperation.CreatePostReport: {
- const data = wsJsonToRes<PostReportResponse>(msg);
- if (data) {
- toast(i18n.t("report_created"));
- }
+ async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+ // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
+ await HttpService.client.markPersonMentionAsRead(form);
+ }
- break;
- }
+ async handleBanFromCommunity(form: BanFromCommunity) {
+ const banRes = await HttpService.client.banFromCommunity(form);
+ this.updateBanFromCommunity(banRes);
+ }
- case UserOperation.CreateCommentReport: {
- const data = wsJsonToRes<CommentReportResponse>(msg);
- if (data) {
- toast(i18n.t("report_created"));
- }
+ async handleBanPerson(form: BanPerson) {
+ const banRes = await HttpService.client.banPerson(form);
+ this.updateBan(banRes);
+ }
- break;
+ updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (s.postsRes.state == "success") {
+ s.postsRes.data.posts
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
+ }
+ if (s.commentsRes.state == "success") {
+ s.commentsRes.data.comments
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
}
+ return s;
+ });
+ }
+ }
- case UserOperation.PurgePerson:
- case UserOperation.PurgePost:
- case UserOperation.PurgeComment:
- case UserOperation.PurgeCommunity: {
- const data = wsJsonToRes<PurgeItemResponse>(msg);
- if (data.success) {
- toast(i18n.t("purge_success"));
- this.context.router.history.push(`/`);
- }
-
- break;
+ updateBan(banRes: RequestState<BanPersonResponse>) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (s.postsRes.state == "success") {
+ s.postsRes.data.posts
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
}
- }
+ if (s.commentsRes.state == "success") {
+ s.commentsRes.data.comments
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
+ }
+ return s;
+ });
}
}
+
+ purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
+ if (purgeRes.state == "success") {
+ toast(i18n.t("purge_success"));
+ this.context.router.history.push(`/`);
+ }
+ }
+
+ findAndUpdateComment(res: RequestState<CommentResponse>) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments = editComment(
+ res.data.comment_view,
+ s.commentsRes.data.comments
+ );
+ s.finished.set(res.data.comment_view.comment.id, true);
+ }
+ return s;
+ });
+ }
+
+ createAndUpdateComments(res: RequestState<CommentResponse>) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments.unshift(res.data.comment_view);
+
+ // Set finished for the parent
+ s.finished.set(
+ getCommentParentId(res.data.comment_view.comment) ?? 0,
+ true
+ );
+ }
+ return s;
+ });
+ }
+
+ findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments = editWith(
+ res.data.comment_reply_view,
+ s.commentsRes.data.comments
+ );
+ }
+ return s;
+ });
+ }
+
+ findAndUpdatePost(res: RequestState<PostResponse>) {
+ this.setState(s => {
+ if (s.postsRes.state == "success" && res.state == "success") {
+ s.postsRes.data.posts = editPost(
+ res.data.post_view,
+ s.postsRes.data.posts
+ );
+ }
+ return s;
+ });
+ }
}
GetFederatedInstancesResponse,
GetSiteResponse,
Instance,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
- import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces";
- import { WebSocketService } from "../../services";
- import {
- WithPromiseKeys,
- isBrowser,
- relTags,
- setIsoData,
- toast,
- wsClient,
- wsSubscribe,
- } from "../../utils";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
-import { relTags, setIsoData } from "../../utils";
++import { RouteDataResponse, relTags, setIsoData } from "../../utils";
import { HtmlTags } from "../common/html-tags";
+ import { Spinner } from "../common/icon";
- interface InstancesData {
++type InstancesData = RouteDataResponse<{
+ federatedInstancesResponse: GetFederatedInstancesResponse;
- }
++}>;
+
interface InstancesState {
+ instancesRes: RequestState<GetFederatedInstancesResponse>;
siteRes: GetSiteResponse;
- instancesRes?: GetFederatedInstancesResponse;
- loading: boolean;
+ isIsomorphic: boolean;
}
export class Instances extends Component<any, InstancesState> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<InstancesData>(this.context);
state: InstancesState = {
+ instancesRes: { state: "empty" },
siteRes: this.isoData.site_res,
- loading: true,
+ isIsomorphic: false,
};
- private subscription?: Subscription;
constructor(props: any, context: any) {
super(props, context);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
-
// Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
+ if (FirstLoadService.isFirstLoad) {
this.state = {
...this.state,
- instancesRes: this.isoData.routeData[0],
+ instancesRes: this.isoData.routeData.federatedInstancesResponse,
- loading: false,
+ isIsomorphic: true,
};
- } else {
- WebSocketService.Instance.send(wsClient.getFederatedInstances({}));
}
}
- static fetchInitialData({
- client,
- }: InitialFetchRequest): WithPromiseKeys<InstancesData> {
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.fetchInstances();
+ }
+ }
+
+ async fetchInstances() {
+ this.setState({
+ instancesRes: { state: "loading" },
+ });
+
+ this.setState({
+ instancesRes: await HttpService.client.getFederatedInstances({}),
+ });
+ }
+
- static fetchInitialData(
++ static async fetchInitialData(
+ req: InitialFetchRequest
- ): Promise<RequestState<any>>[] {
- return [req.client.getFederatedInstances({})];
++ ): Promise<InstancesData> {
+ return {
- federatedInstancesResponse: client.getFederatedInstances(
- {}
- ) as Promise<GetFederatedInstancesResponse>,
++ federatedInstancesResponse: await req.client.getFederatedInstances({}),
+ };
}
get documentTitle(): string {
return `${i18n.t("instances")} - ${this.state.siteRes.site_view.site.name}`;
}
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
+ renderInstances() {
+ switch (this.state.instancesRes.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const instances = this.state.instancesRes.data.federated_instances;
+ return instances ? (
+ <div className="row">
+ <div className="col-md-6">
+ <h5>{i18n.t("linked_instances")}</h5>
+ {this.itemList(instances.linked)}
+ </div>
+ {instances.allowed && instances.allowed.length > 0 && (
+ <div className="col-md-6">
+ <h5>{i18n.t("allowed_instances")}</h5>
+ {this.itemList(instances.allowed)}
+ </div>
+ )}
+ {instances.blocked && instances.blocked.length > 0 && (
+ <div className="col-md-6">
+ <h5>{i18n.t("blocked_instances")}</h5>
+ {this.itemList(instances.blocked)}
+ </div>
+ )}
+ </div>
+ ) : (
+ <></>
+ );
+ }
}
}
render() {
- let federated_instances = this.state.instancesRes?.federated_instances;
- return federated_instances ? (
+ return (
<div className="container-lg">
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
- <div className="row">
- <div className="col-md-6">
- <h5>{i18n.t("linked_instances")}</h5>
- {this.itemList(federated_instances.linked)}
- </div>
- {federated_instances.allowed &&
- federated_instances.allowed.length > 0 && (
- <div className="col-md-6">
- <h5>{i18n.t("allowed_instances")}</h5>
- {this.itemList(federated_instances.allowed)}
- </div>
- )}
- {federated_instances.blocked &&
- federated_instances.blocked.length > 0 && (
- <div className="col-md-6">
- <h5>{i18n.t("blocked_instances")}</h5>
- {this.itemList(federated_instances.blocked)}
- </div>
- )}
- </div>
+ {this.renderInstances()}
</div>
- ) : (
- <></>
);
}
<div>{i18n.t("none_found")}</div>
);
}
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- this.context.router.history.push("/");
- this.setState({ loading: false });
- return;
- } else if (op == UserOperation.GetFederatedInstances) {
- let data = wsJsonToRes<GetFederatedInstancesResponse>(msg);
- this.setState({ loading: false, instancesRes: data });
- }
- }
}
AdminPurgeCommunityView,
AdminPurgePersonView,
AdminPurgePostView,
- CommunityModeratorView,
GetCommunity,
GetCommunityResponse,
GetModlog,
GetModlogResponse,
GetPersonDetails,
+ GetPersonDetailsResponse,
ModAddCommunityView,
ModAddView,
ModBanFromCommunityView,
ModTransferCommunityView,
ModlogActionType,
Person,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
import moment from "moment";
- import { Subscription } from "rxjs";
import { i18n } from "../i18next";
import { InitialFetchRequest } from "../interfaces";
- import { WebSocketService } from "../services";
+ import { FirstLoadService } from "../services/FirstLoadService";
+ import { HttpService, RequestState } from "../services/HttpService";
import {
Choice,
QueryParams,
- WithPromiseKeys,
++ RouteDataResponse,
amAdmin,
amMod,
debounce,
getQueryParams,
getQueryString,
getUpdatedSearchId,
- isBrowser,
myAuth,
personToChoice,
setIsoData,
- toast,
- wsClient,
- wsSubscribe,
} from "../utils";
import { HtmlTags } from "./common/html-tags";
import { Icon, Spinner } from "./common/icon";
| AdminPurgePostView
| AdminPurgeCommentView;
- interface ModlogData {
++type ModlogData = RouteDataResponse<{
+ modlogResponse: GetModlogResponse;
+ communityResponse?: GetCommunityResponse;
+ modUserResponse?: GetPersonDetailsResponse;
+ userResponse?: GetPersonDetailsResponse;
- }
++}>;
+
interface ModlogType {
id: number;
type_: ModlogActionType;
});
interface ModlogState {
- res?: GetModlogResponse;
- communityMods?: CommunityModeratorView[];
- communityName?: string;
- loadingModlog: boolean;
+ res: RequestState<GetModlogResponse>;
+ communityRes: RequestState<GetCommunityResponse>;
loadingModSearch: boolean;
loadingUserSearch: boolean;
modSearchOptions: Choice[];
if (text.length > 0) {
newOptions.push(
- ...(await fetchUsers(text)).users
+ ...(await fetchUsers(text))
.slice(0, Number(fetchLimit))
.map<Choice>(personToChoice)
);
RouteComponentProps<{ communityId?: string }>,
ModlogState
> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<ModlogData>(this.context);
- private subscription?: Subscription;
state: ModlogState = {
- loadingModlog: true,
+ res: { state: "empty" },
+ communityRes: { state: "empty" },
loadingModSearch: false,
loadingUserSearch: false,
userSearchOptions: [],
this.handleUserChange = this.handleUserChange.bind(this);
this.handleModChange = this.handleModChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
-
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
+ if (FirstLoadService.isFirstLoad) {
- const [res, communityRes, filteredModRes, filteredUserRes] =
- this.isoData.routeData;
+ const {
- modlogResponse,
- communityResponse,
++ modlogResponse: res,
++ communityResponse: communityRes,
+ modUserResponse,
+ userResponse,
+ } = this.isoData.routeData;
+
this.state = {
...this.state,
- res: modlogResponse,
+ res,
- communityRes,
};
- // Getting the moderators
- this.state = {
- ...this.state,
- communityMods: communityResponse?.moderators,
- };
-
- if (modUserResponse) {
- if (filteredModRes.state === "success") {
++ if (communityRes?.state === "success") {
this.state = {
...this.state,
- modSearchOptions: [personToChoice(modUserResponse.person_view)],
- modSearchOptions: [personToChoice(filteredModRes.data.person_view)],
++ communityRes,
};
}
- if (userResponse) {
- if (filteredUserRes.state === "success") {
++ if (modUserResponse?.state === "success") {
this.state = {
...this.state,
- userSearchOptions: [personToChoice(userResponse.person_view)],
- userSearchOptions: [personToChoice(filteredUserRes.data.person_view)],
++ modSearchOptions: [personToChoice(modUserResponse.data.person_view)],
+ };
+ }
+
- this.state = { ...this.state, loadingModlog: false };
- } else {
- this.refetch();
- }
- }
-
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
++ if (userResponse?.state === "success") {
++ this.state = {
++ ...this.state,
++ userSearchOptions: [personToChoice(userResponse.data.person_view)],
+ };
+ }
}
}
get combined() {
const res = this.state.res;
- const combined = res ? buildCombined(res) : [];
+ const combined = res.state == "success" ? buildCombined(res.data) : [];
return (
<tbody>
}
get amAdminOrMod(): boolean {
- return amAdmin() || amMod(this.state.communityMods);
+ const amMod_ =
+ this.state.communityRes.state == "success" &&
+ amMod(this.state.communityRes.data.moderators);
+ return amAdmin() || amMod_;
}
modOrAdminText(person?: Person): string {
render() {
const {
- communityName,
- loadingModlog,
loadingModSearch,
loadingUserSearch,
userSearchOptions,
modSearchOptions,
} = this.state;
- const { actionType, page, modId, userId } = getModlogQueryParams();
+ const { actionType, modId, userId } = getModlogQueryParams();
return (
<div className="container-lg">
#<strong>#</strong>#
</T>
</div>
- <h5>
- {communityName && (
- <Link className="text-body" to={`/c/${communityName}`}>
- /c/{communityName}{" "}
+ {this.state.communityRes.state === "success" && (
+ <h5>
+ <Link
+ className="text-body"
+ to={`/c/${this.state.communityRes.data.community_view.community.name}`}
+ >
+ /c/{this.state.communityRes.data.community_view.community.name}{" "}
</Link>
- )}
- <span>{i18n.t("modlog")}</span>
- </h5>
+ <span>{i18n.t("modlog")}</span>
+ </h5>
+ )}
<div className="form-row">
<select
value={actionType}
/>
)}
</div>
- <div className="table-responsive">
- {loadingModlog ? (
- <h5>
- <Spinner large />
- </h5>
- ) : (
- <table id="modlog_table" className="table table-sm table-hover">
- <thead className="pointer">
- <tr>
- <th> {i18n.t("time")}</th>
- <th>{i18n.t("mod")}</th>
- <th>{i18n.t("action")}</th>
- </tr>
- </thead>
- {this.combined}
- </table>
- )}
- <Paginator page={page} onChange={this.handlePageChange} />
- </div>
+ {this.renderModlogTable()}
</div>
</div>
);
}
+ renderModlogTable() {
+ switch (this.state.res.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const page = getModlogQueryParams().page;
+ return (
+ <div className="table-responsive">
+ <table id="modlog_table" className="table table-sm table-hover">
+ <thead className="pointer">
+ <tr>
+ <th> {i18n.t("time")}</th>
+ <th>{i18n.t("mod")}</th>
+ <th>{i18n.t("action")}</th>
+ </tr>
+ </thead>
+ {this.combined}
+ </table>
+ <Paginator page={page} onChange={this.handlePageChange} />
+ </div>
+ );
+ }
+ }
+ }
+
handleFilterActionChange(i: Modlog, event: any) {
i.updateUrl({
actionType: event.target.value as ModlogActionType,
});
});
- updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
+ async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
const {
page: urlPage,
actionType: urlActionType,
)}`
);
- this.setState({
- loadingModlog: true,
- res: undefined,
- });
-
- this.refetch();
+ await this.refetch();
}
- refetch() {
- const auth = myAuth(false);
+ async refetch() {
+ const auth = myAuth();
const { actionType, page, modId, userId } = getModlogQueryParams();
const { communityId: urlCommunityId } = this.props.match.params;
const communityId = getIdFromString(urlCommunityId);
- const modlogForm: GetModlog = {
- community_id: communityId,
- page,
- limit: fetchLimit,
- type_: actionType,
- other_person_id: userId ?? undefined,
- mod_person_id: !this.isoData.site_res.site_view.local_site
- .hide_modlog_mod_names
- ? modId ?? undefined
- : undefined,
- auth,
- };
-
- WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
-
- if (communityId) {
- const communityForm: GetCommunity = {
- id: communityId,
+ this.setState({ res: { state: "loading" } });
+ this.setState({
+ res: await HttpService.client.getModlog({
+ community_id: communityId,
+ page,
+ limit: fetchLimit,
+ type_: actionType,
+ other_person_id: userId ?? undefined,
+ mod_person_id: !this.isoData.site_res.site_view.local_site
+ .hide_modlog_mod_names
+ ? modId ?? undefined
+ : undefined,
auth,
- };
+ }),
+ });
- WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
+ if (communityId) {
+ this.setState({ communityRes: { state: "loading" } });
+ this.setState({
+ communityRes: await HttpService.client.getCommunity({
+ id: communityId,
+ auth,
+ }),
+ });
}
}
-- static fetchInitialData({
++ static async fetchInitialData({
client,
path,
query: { modId: urlModId, page, userId: urlUserId, actionType },
auth,
site,
- }: InitialFetchRequest<
- QueryParams<ModlogProps>
- >): WithPromiseKeys<ModlogData> {
- }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<
- RequestState<any>
- >[] {
++ }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> {
const pathSplit = path.split("/");
- const promises: Promise<RequestState<any>>[] = [];
const communityId = getIdFromString(pathSplit[2]);
const modId = !site.site_view.local_site.hide_modlog_mod_names
? getIdFromString(urlModId)
auth,
};
- let communityResponse: Promise<GetCommunityResponse> | undefined =
- promises.push(client.getModlog(modlogForm));
++ let communityResponse: RequestState<GetCommunityResponse> | undefined =
+ undefined;
if (communityId) {
const communityForm: GetCommunity = {
id: communityId,
auth,
};
- promises.push(client.getCommunity(communityForm));
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
+
- communityResponse = client.getCommunity(communityForm);
++ communityResponse = await client.getCommunity(communityForm);
}
- let modUserResponse: Promise<GetPersonDetailsResponse> | undefined =
++ let modUserResponse: RequestState<GetPersonDetailsResponse> | undefined =
+ undefined;
+
if (modId) {
const getPersonForm: GetPersonDetails = {
person_id: modId,
auth,
};
- modUserResponse = client.getPersonDetails(getPersonForm);
- promises.push(client.getPersonDetails(getPersonForm));
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
++ modUserResponse = await client.getPersonDetails(getPersonForm);
}
- let userResponse: Promise<GetPersonDetailsResponse> | undefined = undefined;
++ let userResponse: RequestState<GetPersonDetailsResponse> | undefined =
++ undefined;
+
if (userId) {
const getPersonForm: GetPersonDetails = {
person_id: userId,
auth,
};
- userResponse = client.getPersonDetails(getPersonForm);
- promises.push(client.getPersonDetails(getPersonForm));
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
++ userResponse = await client.getPersonDetails(getPersonForm);
}
- return promises;
+ return {
- modlogResponse: client.getModlog(modlogForm),
++ modlogResponse: await client.getModlog(modlogForm),
+ communityResponse,
+ modUserResponse,
+ userResponse,
+ };
}
-
- parseMessage(msg: any) {
- const op = wsUserOp(msg);
- console.log(msg);
-
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- } else {
- switch (op) {
- case UserOperation.GetModlog: {
- const res = wsJsonToRes<GetModlogResponse>(msg);
- window.scrollTo(0, 0);
- this.setState({ res, loadingModlog: false });
-
- break;
- }
-
- case UserOperation.GetCommunity: {
- const {
- moderators,
- community_view: {
- community: { name },
- },
- } = wsJsonToRes<GetCommunityResponse>(msg);
- this.setState({
- communityMods: moderators,
- communityName: name,
- });
-
- break;
- }
- }
- }
- }
}
import { Component, linkEvent } from "inferno";
import {
- BlockPersonResponse,
+ AddAdmin,
+ AddModToCommunity,
+ BanFromCommunity,
+ BanFromCommunityResponse,
+ BanPerson,
+ BanPersonResponse,
+ BlockPerson,
+ CommentId,
CommentReplyResponse,
CommentReplyView,
CommentReportResponse,
CommentResponse,
CommentSortType,
CommentView,
- GetPersonMentions,
+ CreateComment,
+ CreateCommentLike,
+ CreateCommentReport,
+ CreatePrivateMessage,
+ CreatePrivateMessageReport,
+ DeleteComment,
+ DeletePrivateMessage,
+ DistinguishComment,
+ EditComment,
+ EditPrivateMessage,
- GetPersonMentions,
GetPersonMentionsResponse,
-- GetPrivateMessages,
-- GetReplies,
GetRepliesResponse,
GetSiteResponse,
+ MarkCommentReplyAsRead,
+ MarkPersonMentionAsRead,
+ MarkPrivateMessageAsRead,
PersonMentionResponse,
PersonMentionView,
- PostReportResponse,
PrivateMessageReportResponse,
PrivateMessageResponse,
PrivateMessageView,
PrivateMessagesResponse,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
+ PurgeComment,
+ PurgeItemResponse,
+ PurgePerson,
+ PurgePost,
+ RemoveComment,
+ SaveComment,
+ TransferCommunity,
} from "lemmy-js-client";
- import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import { CommentViewType, InitialFetchRequest } from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
import {
- WithPromiseKeys,
++ RouteDataResponse,
commentsToFlatNodes,
- createCommentLikeRes,
- editCommentRes,
+ editCommentReply,
+ editMention,
+ editPrivateMessage,
+ editWith,
enableDownvotes,
fetchLimit,
- isBrowser,
+ getCommentParentId,
myAuth,
+ myAuthRequired,
relTags,
- saveCommentRes,
setIsoData,
- setupTippy,
toast,
updatePersonBlock,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { CommentNodes } from "../comment/comment-nodes";
import { CommentSortSelect } from "../common/comment-sort-select";
Mention,
Message,
}
- interface InboxData {
+
- }
++type InboxData = RouteDataResponse<{
+ repliesResponse: GetRepliesResponse;
+ personMentionsResponse: GetPersonMentionsResponse;
+ privateMessagesResponse: PrivateMessagesResponse;
++}>;
+
type ReplyType = {
id: number;
type_: ReplyEnum;
interface InboxState {
unreadOrAll: UnreadOrAll;
messageType: MessageType;
- replies: CommentReplyView[];
- mentions: PersonMentionView[];
- messages: PrivateMessageView[];
- combined: ReplyType[];
+ repliesRes: RequestState<GetRepliesResponse>;
+ mentionsRes: RequestState<GetPersonMentionsResponse>;
+ messagesRes: RequestState<PrivateMessagesResponse>;
+ markAllAsReadRes: RequestState<GetRepliesResponse>;
sort: CommentSortType;
page: number;
siteRes: GetSiteResponse;
- loading: boolean;
+ finished: Map<CommentId, boolean | undefined>;
+ isIsomorphic: boolean;
}
export class Inbox extends Component<any, InboxState> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<InboxData>(this.context);
- private subscription?: Subscription;
state: InboxState = {
unreadOrAll: UnreadOrAll.Unread,
messageType: MessageType.All,
- replies: [],
- mentions: [],
- messages: [],
- combined: [],
sort: "New",
page: 1,
siteRes: this.isoData.site_res,
- loading: true,
+ repliesRes: { state: "empty" },
+ mentionsRes: { state: "empty" },
+ messagesRes: { state: "empty" },
+ markAllAsReadRes: { state: "empty" },
+ finished: new Map(),
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
this.handleSortChange = this.handleSortChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ this.handleCreateComment = this.handleCreateComment.bind(this);
+ this.handleEditComment = this.handleEditComment.bind(this);
+ this.handleSaveComment = this.handleSaveComment.bind(this);
+ this.handleBlockPerson = this.handleBlockPerson.bind(this);
+ this.handleDeleteComment = this.handleDeleteComment.bind(this);
+ this.handleRemoveComment = this.handleRemoveComment.bind(this);
+ this.handleCommentVote = this.handleCommentVote.bind(this);
+ this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+ this.handleAddAdmin = this.handleAddAdmin.bind(this);
+ this.handlePurgePerson = this.handlePurgePerson.bind(this);
+ this.handlePurgeComment = this.handlePurgeComment.bind(this);
+ this.handleCommentReport = this.handleCommentReport.bind(this);
+ this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+ this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+ this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+ this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+ this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+ this.handleBanPerson = this.handleBanPerson.bind(this);
+
+ this.handleDeleteMessage = this.handleDeleteMessage.bind(this);
+ this.handleMarkMessageAsRead = this.handleMarkMessageAsRead.bind(this);
+ this.handleMessageReport = this.handleMessageReport.bind(this);
+ this.handleCreateMessage = this.handleCreateMessage.bind(this);
+ this.handleEditMessage = this.handleEditMessage.bind(this);
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
+ if (FirstLoadService.isFirstLoad) {
- const [repliesRes, mentionsRes, messagesRes] = this.isoData.routeData;
+ const {
- personMentionsResponse,
- privateMessagesResponse,
- repliesResponse,
++ personMentionsResponse: mentionsRes,
++ privateMessagesResponse: messagesRes,
++ repliesResponse: repliesRes,
+ } = this.isoData.routeData;
this.state = {
...this.state,
- replies: repliesResponse.replies ?? [],
- mentions: personMentionsResponse.mentions ?? [],
- messages: privateMessagesResponse.private_messages ?? [],
- loading: false,
+ repliesRes,
+ mentionsRes,
+ messagesRes,
+ isIsomorphic: true,
};
- this.state = { ...this.state, combined: this.buildCombined() };
- } else {
- this.refetch();
}
}
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.refetch();
}
}
get documentTitle(): string {
- let mui = UserService.Instance.myUserInfo;
+ const mui = UserService.Instance.myUserInfo;
return mui
? `@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
this.state.siteRes.site_view.site.name
: "";
}
+ get hasUnreads(): boolean {
+ if (this.state.unreadOrAll == UnreadOrAll.Unread) {
+ const { repliesRes, mentionsRes, messagesRes } = this.state;
+ const replyCount =
+ repliesRes.state == "success" ? repliesRes.data.replies.length : 0;
+ const mentionCount =
+ mentionsRes.state == "success" ? mentionsRes.data.mentions.length : 0;
+ const messageCount =
+ messagesRes.state == "success"
+ ? messagesRes.data.private_messages.length
+ : 0;
+
+ return replyCount + mentionCount + messageCount > 0;
+ } else {
+ return false;
+ }
+ }
+
render() {
- let auth = myAuth();
- let inboxRss = auth ? `/feeds/inbox/${auth}.xml` : undefined;
+ const auth = myAuth();
+ const inboxRss = auth ? `/feeds/inbox/${auth}.xml` : undefined;
return (
<div className="container-lg">
- {this.state.loading ? (
- <h5>
- <Spinner large />
- </h5>
- ) : (
- <div className="row">
- <div className="col-12">
- <HtmlTags
- title={this.documentTitle}
- path={this.context.router.route.match.url}
- />
- <h5 className="mb-2">
- {i18n.t("inbox")}
- {inboxRss && (
- <small>
- <a href={inboxRss} title="RSS" rel={relTags}>
- <Icon icon="rss" classes="ml-2 text-muted small" />
- </a>
- <link
- rel="alternate"
- type="application/atom+xml"
- href={inboxRss}
- />
- </small>
+ <div className="row">
+ <div className="col-12">
+ <HtmlTags
+ title={this.documentTitle}
+ path={this.context.router.route.match.url}
+ />
+ <h5 className="mb-2">
+ {i18n.t("inbox")}
+ {inboxRss && (
+ <small>
+ <a href={inboxRss} title="RSS" rel={relTags}>
+ <Icon icon="rss" classes="ml-2 text-muted small" />
+ </a>
+ <link
+ rel="alternate"
+ type="application/atom+xml"
+ href={inboxRss}
+ />
+ </small>
+ )}
+ </h5>
+ {this.hasUnreads && (
+ <button
+ className="btn btn-secondary mb-2"
+ onClick={linkEvent(this, this.handleMarkAllAsRead)}
+ >
+ {this.state.markAllAsReadRes.state == "loading" ? (
+ <Spinner />
+ ) : (
+ i18n.t("mark_all_as_read")
)}
- </h5>
- {this.state.replies.length +
- this.state.mentions.length +
- this.state.messages.length >
- 0 &&
- this.state.unreadOrAll == UnreadOrAll.Unread && (
- <button
- className="btn btn-secondary mb-2"
- onClick={linkEvent(this, this.markAllAsRead)}
- >
- {i18n.t("mark_all_as_read")}
- </button>
- )}
- {this.selects()}
- {this.state.messageType == MessageType.All && this.all()}
- {this.state.messageType == MessageType.Replies && this.replies()}
- {this.state.messageType == MessageType.Mentions &&
- this.mentions()}
- {this.state.messageType == MessageType.Messages &&
- this.messages()}
- <Paginator
- page={this.state.page}
- onChange={this.handlePageChange}
- />
- </div>
+ </button>
+ )}
+ {this.selects()}
+ {this.section}
+ <Paginator
+ page={this.state.page}
+ onChange={this.handlePageChange}
+ />
</div>
- )}
+ </div>
</div>
);
}
+ get section() {
+ switch (this.state.messageType) {
+ case MessageType.All: {
+ return this.all();
+ }
+ case MessageType.Replies: {
+ return this.replies();
+ }
+ case MessageType.Mentions: {
+ return this.mentions();
+ }
+ case MessageType.Messages: {
+ return this.messages();
+ }
+ default: {
+ return null;
+ }
+ }
+ }
+
unreadOrAllRadios() {
return (
<div className="btn-group btn-group-toggle flex-wrap mb-2">
}
buildCombined(): ReplyType[] {
- let replies: ReplyType[] = this.state.replies.map(r =>
- this.replyToReplyType(r)
- );
- let mentions: ReplyType[] = this.state.mentions.map(r =>
- this.mentionToReplyType(r)
- );
- let messages: ReplyType[] = this.state.messages.map(r =>
- this.messageToReplyType(r)
- );
+ const replies: ReplyType[] =
+ this.state.repliesRes.state == "success"
+ ? this.state.repliesRes.data.replies.map(this.replyToReplyType)
+ : [];
+ const mentions: ReplyType[] =
+ this.state.mentionsRes.state == "success"
+ ? this.state.mentionsRes.data.mentions.map(this.mentionToReplyType)
+ : [];
+ const messages: ReplyType[] =
+ this.state.messagesRes.state == "success"
+ ? this.state.messagesRes.data.private_messages.map(
+ this.messageToReplyType
+ )
+ : [];
return [...replies, ...mentions, ...messages].sort((a, b) =>
b.published.localeCompare(a.published)
{ comment_view: i.view as CommentView, children: [], depth: 0 },
]}
viewType={CommentViewType.Flat}
+ finished={this.state.finished}
noIndent
markable
showCommunity
enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
+ onSaveComment={this.handleSaveComment}
+ onBlockPerson={this.handleBlockPerson}
+ onDeleteComment={this.handleDeleteComment}
+ onRemoveComment={this.handleRemoveComment}
+ onCommentVote={this.handleCommentVote}
+ onCommentReport={this.handleCommentReport}
+ onDistinguishComment={this.handleDistinguishComment}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onPurgeComment={this.handlePurgeComment}
+ onPurgePerson={this.handlePurgePerson}
+ onCommentReplyRead={this.handleCommentReplyRead}
+ onPersonMentionRead={this.handlePersonMentionRead}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onBanPerson={this.handleBanPerson}
+ onCreateComment={this.handleCreateComment}
+ onEditComment={this.handleEditComment}
/>
);
case ReplyEnum.Mention:
depth: 0,
},
]}
+ finished={this.state.finished}
viewType={CommentViewType.Flat}
noIndent
markable
enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
+ onSaveComment={this.handleSaveComment}
+ onBlockPerson={this.handleBlockPerson}
+ onDeleteComment={this.handleDeleteComment}
+ onRemoveComment={this.handleRemoveComment}
+ onCommentVote={this.handleCommentVote}
+ onCommentReport={this.handleCommentReport}
+ onDistinguishComment={this.handleDistinguishComment}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onPurgeComment={this.handlePurgeComment}
+ onPurgePerson={this.handlePurgePerson}
+ onCommentReplyRead={this.handleCommentReplyRead}
+ onPersonMentionRead={this.handlePersonMentionRead}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onBanPerson={this.handleBanPerson}
+ onCreateComment={this.handleCreateComment}
+ onEditComment={this.handleEditComment}
/>
);
case ReplyEnum.Message:
<PrivateMessage
key={i.id}
private_message_view={i.view as PrivateMessageView}
+ onDelete={this.handleDeleteMessage}
+ onMarkRead={this.handleMarkMessageAsRead}
+ onReport={this.handleMessageReport}
+ onCreate={this.handleCreateMessage}
+ onEdit={this.handleEditMessage}
/>
);
default:
}
all() {
- return <div>{this.state.combined.map(i => this.renderReplyType(i))}</div>;
+ if (
+ this.state.repliesRes.state == "loading" ||
+ this.state.mentionsRes.state == "loading" ||
+ this.state.messagesRes.state == "loading"
+ ) {
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ } else {
+ return (
+ <div>{this.buildCombined().map(r => this.renderReplyType(r))}</div>
+ );
+ }
}
replies() {
- return (
- <div>
- <CommentNodes
- nodes={commentsToFlatNodes(this.state.replies)}
- viewType={CommentViewType.Flat}
- noIndent
- markable
- showCommunity
- showContext
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
- />
- </div>
- );
+ switch (this.state.repliesRes.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const replies = this.state.repliesRes.data.replies;
+ return (
+ <div>
+ <CommentNodes
+ nodes={commentsToFlatNodes(replies)}
+ viewType={CommentViewType.Flat}
+ finished={this.state.finished}
+ noIndent
+ markable
+ showCommunity
+ showContext
+ enableDownvotes={enableDownvotes(this.state.siteRes)}
+ allLanguages={this.state.siteRes.all_languages}
+ siteLanguages={this.state.siteRes.discussion_languages}
+ onSaveComment={this.handleSaveComment}
+ onBlockPerson={this.handleBlockPerson}
+ onDeleteComment={this.handleDeleteComment}
+ onRemoveComment={this.handleRemoveComment}
+ onCommentVote={this.handleCommentVote}
+ onCommentReport={this.handleCommentReport}
+ onDistinguishComment={this.handleDistinguishComment}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onPurgeComment={this.handlePurgeComment}
+ onPurgePerson={this.handlePurgePerson}
+ onCommentReplyRead={this.handleCommentReplyRead}
+ onPersonMentionRead={this.handlePersonMentionRead}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onBanPerson={this.handleBanPerson}
+ onCreateComment={this.handleCreateComment}
+ onEditComment={this.handleEditComment}
+ />
+ </div>
+ );
+ }
+ }
}
mentions() {
- return (
- <div>
- {this.state.mentions.map(umv => (
- <CommentNodes
- key={umv.person_mention.id}
- nodes={[{ comment_view: umv, children: [], depth: 0 }]}
- viewType={CommentViewType.Flat}
- noIndent
- markable
- showCommunity
- showContext
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
- />
- ))}
- </div>
- );
+ switch (this.state.mentionsRes.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const mentions = this.state.mentionsRes.data.mentions;
+ return (
+ <div>
+ {mentions.map(umv => (
+ <CommentNodes
+ key={umv.person_mention.id}
+ nodes={[{ comment_view: umv, children: [], depth: 0 }]}
+ viewType={CommentViewType.Flat}
+ finished={this.state.finished}
+ noIndent
+ markable
+ showCommunity
+ showContext
+ enableDownvotes={enableDownvotes(this.state.siteRes)}
+ allLanguages={this.state.siteRes.all_languages}
+ siteLanguages={this.state.siteRes.discussion_languages}
+ onSaveComment={this.handleSaveComment}
+ onBlockPerson={this.handleBlockPerson}
+ onDeleteComment={this.handleDeleteComment}
+ onRemoveComment={this.handleRemoveComment}
+ onCommentVote={this.handleCommentVote}
+ onCommentReport={this.handleCommentReport}
+ onDistinguishComment={this.handleDistinguishComment}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onPurgeComment={this.handlePurgeComment}
+ onPurgePerson={this.handlePurgePerson}
+ onCommentReplyRead={this.handleCommentReplyRead}
+ onPersonMentionRead={this.handlePersonMentionRead}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onBanPerson={this.handleBanPerson}
+ onCreateComment={this.handleCreateComment}
+ onEditComment={this.handleEditComment}
+ />
+ ))}
+ </div>
+ );
+ }
+ }
}
messages() {
- return (
- <div>
- {this.state.messages.map(pmv => (
- <PrivateMessage
- key={pmv.private_message.id}
- private_message_view={pmv}
- />
- ))}
- </div>
- );
+ switch (this.state.messagesRes.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const messages = this.state.messagesRes.data.private_messages;
+ return (
+ <div>
+ {messages.map(pmv => (
+ <PrivateMessage
+ key={pmv.private_message.id}
+ private_message_view={pmv}
+ onDelete={this.handleDeleteMessage}
+ onMarkRead={this.handleMarkMessageAsRead}
+ onReport={this.handleMessageReport}
+ onCreate={this.handleCreateMessage}
+ onEdit={this.handleEditMessage}
+ />
+ ))}
+ </div>
+ );
+ }
+ }
}
- handlePageChange(page: number) {
+ async handlePageChange(page: number) {
this.setState({ page });
- this.refetch();
+ await this.refetch();
}
- handleUnreadOrAllChange(i: Inbox, event: any) {
+ async handleUnreadOrAllChange(i: Inbox, event: any) {
i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
- i.refetch();
+ await i.refetch();
}
- handleMessageTypeChange(i: Inbox, event: any) {
+ async handleMessageTypeChange(i: Inbox, event: any) {
i.setState({ messageType: Number(event.target.value), page: 1 });
- i.refetch();
+ await i.refetch();
}
-- static fetchInitialData({
- auth,
++ static async fetchInitialData({
client,
- }: InitialFetchRequest): WithPromiseKeys<InboxData> {
+ auth,
- }: InitialFetchRequest): Promise<any>[] {
- const promises: Promise<RequestState<any>>[] = [];
-
++ }: InitialFetchRequest): Promise<InboxData> {
const sort: CommentSortType = "New";
- // It can be /u/me, or /username/1
- const repliesForm: GetReplies = {
- sort,
- unread_only: true,
- page: 1,
- limit: fetchLimit,
- auth: auth as string,
- };
- if (auth) {
- // It can be /u/me, or /username/1
- const repliesForm: GetReplies = {
- sort,
- unread_only: true,
- page: 1,
- limit: fetchLimit,
- auth,
- };
- promises.push(client.getReplies(repliesForm));
--
- const personMentionsForm: GetPersonMentions = {
- sort,
- unread_only: true,
- page: 1,
- limit: fetchLimit,
- auth: auth as string,
- };
- const personMentionsForm: GetPersonMentions = {
- sort,
- unread_only: true,
- page: 1,
- limit: fetchLimit,
- auth,
- };
- promises.push(client.getPersonMentions(personMentionsForm));
--
- const privateMessagesForm: GetPrivateMessages = {
- unread_only: true,
- page: 1,
- limit: fetchLimit,
- auth: auth as string,
- };
- const privateMessagesForm: GetPrivateMessages = {
- unread_only: true,
- page: 1,
- limit: fetchLimit,
- auth,
- };
- promises.push(client.getPrivateMessages(privateMessagesForm));
- } else {
- promises.push(
- Promise.resolve({ state: "empty" }),
- Promise.resolve({ state: "empty" }),
- Promise.resolve({ state: "empty" })
- );
- }
--
- return promises;
+ return {
- privateMessagesResponse: client.getPrivateMessages(privateMessagesForm),
- personMentionsResponse: client.getPersonMentions(personMentionsForm),
- repliesResponse: client.getReplies(repliesForm),
++ personMentionsResponse: auth
++ ? await client.getPersonMentions({
++ sort,
++ unread_only: true,
++ page: 1,
++ limit: fetchLimit,
++ auth,
++ })
++ : { state: "empty" },
++ privateMessagesResponse: auth
++ ? await client.getPrivateMessages({
++ unread_only: true,
++ page: 1,
++ limit: fetchLimit,
++ auth,
++ })
++ : { state: "empty" },
++ repliesResponse: auth
++ ? await client.getReplies({
++ sort,
++ unread_only: true,
++ page: 1,
++ limit: fetchLimit,
++ auth,
++ })
++ : { state: "empty" },
+ };
}
- refetch() {
- const { sort, page, unreadOrAll } = this.state;
- const unread_only = unreadOrAll === UnreadOrAll.Unread;
+ async refetch() {
+ const sort = this.state.sort;
+ const unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
+ const page = this.state.page;
const limit = fetchLimit;
- const auth = myAuth();
+ const auth = myAuthRequired();
- if (auth) {
- const repliesForm: GetReplies = {
+ this.setState({ repliesRes: { state: "loading" } });
+ this.setState({
+ repliesRes: await HttpService.client.getReplies({
sort,
unread_only,
page,
limit,
auth,
- };
- WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
+ }),
+ });
- const personMentionsForm: GetPersonMentions = {
+ this.setState({ mentionsRes: { state: "loading" } });
+ this.setState({
+ mentionsRes: await HttpService.client.getPersonMentions({
sort,
unread_only,
page,
limit,
auth,
- };
- WebSocketService.Instance.send(
- wsClient.getPersonMentions(personMentionsForm)
- );
+ }),
+ });
- const privateMessagesForm: GetPrivateMessages = {
+ this.setState({ messagesRes: { state: "loading" } });
+ this.setState({
+ messagesRes: await HttpService.client.getPrivateMessages({
unread_only,
page,
limit,
auth,
- };
- WebSocketService.Instance.send(
- wsClient.getPrivateMessages(privateMessagesForm)
- );
- }
+ }),
+ });
}
- handleSortChange(val: CommentSortType) {
+ async handleSortChange(val: CommentSortType) {
this.setState({ sort: val, page: 1 });
- this.refetch();
+ await this.refetch();
}
- markAllAsRead(i: Inbox) {
- let auth = myAuth();
- if (auth) {
- WebSocketService.Instance.send(
- wsClient.markAllAsRead({
- auth,
- })
- );
- i.setState({ replies: [], mentions: [], messages: [] });
- i.setState({ combined: i.buildCombined() });
- UserService.Instance.unreadInboxCountSub.next(0);
- window.scrollTo(0, 0);
- i.setState(i.state);
+ async handleMarkAllAsRead(i: Inbox) {
+ i.setState({ markAllAsReadRes: { state: "loading" } });
+
+ i.setState({
+ markAllAsReadRes: await HttpService.client.markAllAsRead({
+ auth: myAuthRequired(),
+ }),
+ });
+
+ if (i.state.markAllAsReadRes.state == "success") {
+ i.setState({
+ repliesRes: { state: "empty" },
+ mentionsRes: { state: "empty" },
+ messagesRes: { state: "empty" },
+ });
}
}
- sendUnreadCount(read: boolean) {
- let urcs = UserService.Instance.unreadInboxCountSub;
- if (read) {
- urcs.next(urcs.getValue() - 1);
- } else {
- urcs.next(urcs.getValue() + 1);
+ async handleAddModToCommunity(form: AddModToCommunity) {
+ // TODO not sure what to do here
+ HttpService.client.addModToCommunity(form);
+ }
+
+ async handlePurgePerson(form: PurgePerson) {
+ const purgePersonRes = await HttpService.client.purgePerson(form);
+ this.purgeItem(purgePersonRes);
+ }
+
+ async handlePurgeComment(form: PurgeComment) {
+ const purgeCommentRes = await HttpService.client.purgeComment(form);
+ this.purgeItem(purgeCommentRes);
+ }
+
+ async handlePurgePost(form: PurgePost) {
+ const purgeRes = await HttpService.client.purgePost(form);
+ this.purgeItem(purgeRes);
+ }
+
+ async handleBlockPerson(form: BlockPerson) {
+ const blockPersonRes = await HttpService.client.blockPerson(form);
+ if (blockPersonRes.state == "success") {
+ updatePersonBlock(blockPersonRes.data);
}
}
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- return;
- } else if (msg.reconnect) {
- this.refetch();
- } else if (op == UserOperation.GetReplies) {
- let data = wsJsonToRes<GetRepliesResponse>(msg);
- this.setState({ replies: data.replies });
- this.setState({ combined: this.buildCombined(), loading: false });
- window.scrollTo(0, 0);
- setupTippy();
- } else if (op == UserOperation.GetPersonMentions) {
- let data = wsJsonToRes<GetPersonMentionsResponse>(msg);
- this.setState({ mentions: data.mentions });
- this.setState({ combined: this.buildCombined() });
- window.scrollTo(0, 0);
- setupTippy();
- } else if (op == UserOperation.GetPrivateMessages) {
- let data = wsJsonToRes<PrivateMessagesResponse>(msg);
- this.setState({ messages: data.private_messages });
- this.setState({ combined: this.buildCombined() });
- window.scrollTo(0, 0);
- setupTippy();
- } else if (op == UserOperation.EditPrivateMessage) {
- let data = wsJsonToRes<PrivateMessageResponse>(msg);
- let found = this.state.messages.find(
- m =>
- m.private_message.id === data.private_message_view.private_message.id
- );
- if (found) {
- let combinedView = this.state.combined.find(
- i => i.id == data.private_message_view.private_message.id
- )?.view as PrivateMessageView | undefined;
- if (combinedView) {
- found.private_message.content = combinedView.private_message.content =
- data.private_message_view.private_message.content;
- found.private_message.updated = combinedView.private_message.updated =
- data.private_message_view.private_message.updated;
- }
+ async handleCreateComment(form: CreateComment) {
+ const res = await HttpService.client.createComment(form);
+
+ if (res.state === "success") {
+ toast(i18n.t("reply_sent"));
+ this.findAndUpdateComment(res);
+ }
+
+ return res;
+ }
+
+ async handleEditComment(form: EditComment) {
+ const res = await HttpService.client.editComment(form);
+
+ if (res.state === "success") {
+ toast(i18n.t("edit"));
+ this.findAndUpdateComment(res);
+ } else if (res.state === "failed") {
+ toast(res.msg, "danger");
+ }
+
+ return res;
+ }
+
+ async handleDeleteComment(form: DeleteComment) {
+ const res = await HttpService.client.deleteComment(form);
+ if (res.state == "success") {
+ toast(i18n.t("deleted"));
+ this.findAndUpdateComment(res);
+ }
+ }
+
+ async handleRemoveComment(form: RemoveComment) {
+ const res = await HttpService.client.removeComment(form);
+ if (res.state == "success") {
+ toast(i18n.t("remove_comment"));
+ this.findAndUpdateComment(res);
+ }
+ }
+
+ async handleSaveComment(form: SaveComment) {
+ const res = await HttpService.client.saveComment(form);
+ this.findAndUpdateComment(res);
+ }
+
+ async handleCommentVote(form: CreateCommentLike) {
+ const res = await HttpService.client.likeComment(form);
+ this.findAndUpdateComment(res);
+ }
+
+ async handleCommentReport(form: CreateCommentReport) {
+ const reportRes = await HttpService.client.createCommentReport(form);
+ this.reportToast(reportRes);
+ }
+
+ async handleDistinguishComment(form: DistinguishComment) {
+ const res = await HttpService.client.distinguishComment(form);
+ this.findAndUpdateComment(res);
+ }
+
+ async handleAddAdmin(form: AddAdmin) {
+ const addAdminRes = await HttpService.client.addAdmin(form);
+
+ if (addAdminRes.state === "success") {
+ this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+ }
+ }
+
+ async handleTransferCommunity(form: TransferCommunity) {
+ await HttpService.client.transferCommunity(form);
+ toast(i18n.t("transfer_community"));
+ }
+
+ async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+ const res = await HttpService.client.markCommentReplyAsRead(form);
+ this.findAndUpdateCommentReply(res);
+ }
+
+ async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+ const res = await HttpService.client.markPersonMentionAsRead(form);
+ this.findAndUpdateMention(res);
+ }
+
+ async handleBanFromCommunity(form: BanFromCommunity) {
+ const banRes = await HttpService.client.banFromCommunity(form);
+ this.updateBanFromCommunity(banRes);
+ }
+
+ async handleBanPerson(form: BanPerson) {
+ const banRes = await HttpService.client.banPerson(form);
+ this.updateBan(banRes);
+ }
+
+ async handleDeleteMessage(form: DeletePrivateMessage) {
+ const res = await HttpService.client.deletePrivateMessage(form);
+ this.findAndUpdateMessage(res);
+ }
+
+ async handleEditMessage(form: EditPrivateMessage) {
+ const res = await HttpService.client.editPrivateMessage(form);
+ this.findAndUpdateMessage(res);
+ }
+
+ async handleMarkMessageAsRead(form: MarkPrivateMessageAsRead) {
+ const res = await HttpService.client.markPrivateMessageAsRead(form);
+ this.findAndUpdateMessage(res);
+ }
+
+ async handleMessageReport(form: CreatePrivateMessageReport) {
+ const res = await HttpService.client.createPrivateMessageReport(form);
+ this.reportToast(res);
+ }
+
+ async handleCreateMessage(form: CreatePrivateMessage) {
+ const res = await HttpService.client.createPrivateMessage(form);
+ this.setState(s => {
+ if (s.messagesRes.state == "success" && res.state == "success") {
+ s.messagesRes.data.private_messages.unshift(
+ res.data.private_message_view
+ );
}
- this.setState(this.state);
- } else if (op == UserOperation.DeletePrivateMessage) {
- let data = wsJsonToRes<PrivateMessageResponse>(msg);
- let found = this.state.messages.find(
- m =>
- m.private_message.id === data.private_message_view.private_message.id
- );
- if (found) {
- let combinedView = this.state.combined.find(
- i => i.id == data.private_message_view.private_message.id
- )?.view as PrivateMessageView | undefined;
- if (combinedView) {
- found.private_message.deleted = combinedView.private_message.deleted =
- data.private_message_view.private_message.deleted;
- found.private_message.updated = combinedView.private_message.updated =
- data.private_message_view.private_message.updated;
- }
+
+ return s;
+ });
+ }
+
+ findAndUpdateMessage(res: RequestState<PrivateMessageResponse>) {
+ this.setState(s => {
+ if (s.messagesRes.state === "success" && res.state === "success") {
+ s.messagesRes.data.private_messages = editPrivateMessage(
+ res.data.private_message_view,
+ s.messagesRes.data.private_messages
+ );
}
- this.setState(this.state);
- } else if (op == UserOperation.MarkPrivateMessageAsRead) {
- let data = wsJsonToRes<PrivateMessageResponse>(msg);
- let found = this.state.messages.find(
- m =>
- m.private_message.id === data.private_message_view.private_message.id
- );
+ return s;
+ });
+ }
- if (found) {
- let combinedView = this.state.combined.find(
- i =>
- i.id == data.private_message_view.private_message.id &&
- i.type_ == ReplyEnum.Message
- )?.view as PrivateMessageView | undefined;
- if (combinedView) {
- found.private_message.updated = combinedView.private_message.updated =
- data.private_message_view.private_message.updated;
-
- // If youre in the unread view, just remove it from the list
- if (
- this.state.unreadOrAll == UnreadOrAll.Unread &&
- data.private_message_view.private_message.read
- ) {
- this.setState({
- messages: this.state.messages.filter(
- r =>
- r.private_message.id !==
- data.private_message_view.private_message.id
- ),
- });
- this.setState({
- combined: this.state.combined.filter(
- r => r.id !== data.private_message_view.private_message.id
- ),
- });
- } else {
- found.private_message.read = combinedView.private_message.read =
- data.private_message_view.private_message.read;
- }
+ updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (s.repliesRes.state == "success") {
+ s.repliesRes.data.replies
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
}
- }
- this.sendUnreadCount(data.private_message_view.private_message.read);
- this.setState(this.state);
- } else if (op == UserOperation.MarkAllAsRead) {
- // Moved to be instant
- } else if (
- op == UserOperation.EditComment ||
- op == UserOperation.DeleteComment ||
- op == UserOperation.RemoveComment
- ) {
- let data = wsJsonToRes<CommentResponse>(msg);
- editCommentRes(data.comment_view, this.state.replies);
- this.setState(this.state);
- } else if (op == UserOperation.MarkCommentReplyAsRead) {
- let data = wsJsonToRes<CommentReplyResponse>(msg);
-
- let found = this.state.replies.find(
- c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
- );
+ if (s.mentionsRes.state == "success") {
+ s.mentionsRes.data.mentions
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
+ }
+ return s;
+ });
+ }
+ }
- if (found) {
- let combinedView = this.state.combined.find(
- i =>
- i.id == data.comment_reply_view.comment_reply.id &&
- i.type_ == ReplyEnum.Reply
- )?.view as CommentReplyView | undefined;
- if (combinedView) {
- found.comment.content = combinedView.comment.content =
- data.comment_reply_view.comment.content;
- found.comment.updated = combinedView.comment.updated =
- data.comment_reply_view.comment.updated;
- found.comment.removed = combinedView.comment.removed =
- data.comment_reply_view.comment.removed;
- found.comment.deleted = combinedView.comment.deleted =
- data.comment_reply_view.comment.deleted;
- found.counts.upvotes = combinedView.counts.upvotes =
- data.comment_reply_view.counts.upvotes;
- found.counts.downvotes = combinedView.counts.downvotes =
- data.comment_reply_view.counts.downvotes;
- found.counts.score = combinedView.counts.score =
- data.comment_reply_view.counts.score;
-
- // If youre in the unread view, just remove it from the list
- if (
- this.state.unreadOrAll == UnreadOrAll.Unread &&
- data.comment_reply_view.comment_reply.read
- ) {
- this.setState({
- replies: this.state.replies.filter(
- r =>
- r.comment_reply.id !==
- data.comment_reply_view.comment_reply.id
- ),
- });
- this.setState({
- combined: this.state.combined.filter(
- r => r.id !== data.comment_reply_view.comment_reply.id
- ),
- });
- } else {
- found.comment_reply.read = combinedView.comment_reply.read =
- data.comment_reply_view.comment_reply.read;
- }
+ updateBan(banRes: RequestState<BanPersonResponse>) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (s.repliesRes.state == "success") {
+ s.repliesRes.data.replies
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
}
- }
- this.sendUnreadCount(data.comment_reply_view.comment_reply.read);
- this.setState(this.state);
- } else if (op == UserOperation.MarkPersonMentionAsRead) {
- let data = wsJsonToRes<PersonMentionResponse>(msg);
-
- // TODO this might not be correct, it might need to use the comment id
- let found = this.state.mentions.find(
- c => c.person_mention.id == data.person_mention_view.person_mention.id
- );
+ if (s.mentionsRes.state == "success") {
+ s.mentionsRes.data.mentions
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
+ }
+ return s;
+ });
+ }
+ }
+
+ purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
+ if (purgeRes.state == "success") {
+ toast(i18n.t("purge_success"));
+ this.context.router.history.push(`/`);
+ }
+ }
+
+ reportToast(
+ res: RequestState<PrivateMessageReportResponse | CommentReportResponse>
+ ) {
+ if (res.state == "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
- if (found) {
- let combinedView = this.state.combined.find(
- i =>
- i.id == data.person_mention_view.person_mention.id &&
- i.type_ == ReplyEnum.Mention
- )?.view as PersonMentionView | undefined;
- if (combinedView) {
- found.comment.content = combinedView.comment.content =
- data.person_mention_view.comment.content;
- found.comment.updated = combinedView.comment.updated =
- data.person_mention_view.comment.updated;
- found.comment.removed = combinedView.comment.removed =
- data.person_mention_view.comment.removed;
- found.comment.deleted = combinedView.comment.deleted =
- data.person_mention_view.comment.deleted;
- found.counts.upvotes = combinedView.counts.upvotes =
- data.person_mention_view.counts.upvotes;
- found.counts.downvotes = combinedView.counts.downvotes =
- data.person_mention_view.counts.downvotes;
- found.counts.score = combinedView.counts.score =
- data.person_mention_view.counts.score;
-
- // If youre in the unread view, just remove it from the list
- if (
- this.state.unreadOrAll == UnreadOrAll.Unread &&
- data.person_mention_view.person_mention.read
- ) {
- this.setState({
- mentions: this.state.mentions.filter(
- r =>
- r.person_mention.id !==
- data.person_mention_view.person_mention.id
- ),
- });
- this.setState({
- combined: this.state.combined.filter(
- r => r.id !== data.person_mention_view.person_mention.id
- ),
- });
- } else {
- // TODO test to make sure these mentions are getting marked as read
- found.person_mention.read = combinedView.person_mention.read =
- data.person_mention_view.person_mention.read;
- }
+ // A weird case, since you have only replies and mentions, not comment responses
+ findAndUpdateComment(res: RequestState<CommentResponse>) {
+ if (res.state == "success") {
+ this.setState(s => {
+ if (s.repliesRes.state == "success") {
+ s.repliesRes.data.replies = editWith(
+ res.data.comment_view,
+ s.repliesRes.data.replies
+ );
}
- }
- this.sendUnreadCount(data.person_mention_view.person_mention.read);
- this.setState(this.state);
- } else if (op == UserOperation.CreatePrivateMessage) {
- let data = wsJsonToRes<PrivateMessageResponse>(msg);
- let mui = UserService.Instance.myUserInfo;
- if (
- data.private_message_view.recipient.id == mui?.local_user_view.person.id
- ) {
- this.state.messages.unshift(data.private_message_view);
- this.state.combined.unshift(
- this.messageToReplyType(data.private_message_view)
+ if (s.mentionsRes.state == "success") {
+ s.mentionsRes.data.mentions = editWith(
+ res.data.comment_view,
+ s.mentionsRes.data.mentions
+ );
+ }
+ // Set finished for the parent
+ s.finished.set(
+ getCommentParentId(res.data.comment_view.comment) ?? 0,
+ true
);
- this.setState(this.state);
- }
- } else if (op == UserOperation.SaveComment) {
- let data = wsJsonToRes<CommentResponse>(msg);
- saveCommentRes(data.comment_view, this.state.replies);
- this.setState(this.state);
- setupTippy();
- } else if (op == UserOperation.CreateCommentLike) {
- let data = wsJsonToRes<CommentResponse>(msg);
- createCommentLikeRes(data.comment_view, this.state.replies);
- this.setState(this.state);
- } else if (op == UserOperation.BlockPerson) {
- let data = wsJsonToRes<BlockPersonResponse>(msg);
- updatePersonBlock(data);
- } else if (op == UserOperation.CreatePostReport) {
- let data = wsJsonToRes<PostReportResponse>(msg);
- if (data) {
- toast(i18n.t("report_created"));
- }
- } else if (op == UserOperation.CreateCommentReport) {
- let data = wsJsonToRes<CommentReportResponse>(msg);
- if (data) {
- toast(i18n.t("report_created"));
- }
- } else if (op == UserOperation.CreatePrivateMessageReport) {
- let data = wsJsonToRes<PrivateMessageReportResponse>(msg);
- if (data) {
- toast(i18n.t("report_created"));
- }
+ return s;
+ });
}
}
- isMention(view: any): view is PersonMentionView {
- return (view as PersonMentionView).person_mention !== undefined;
+ findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
+ this.setState(s => {
+ if (s.repliesRes.state == "success" && res.state == "success") {
+ s.repliesRes.data.replies = editCommentReply(
+ res.data.comment_reply_view,
+ s.repliesRes.data.replies
+ );
+ }
+ return s;
+ });
}
- isReply(view: any): view is CommentReplyView {
- return (view as CommentReplyView).comment_reply !== undefined;
+ findAndUpdateMention(res: RequestState<PersonMentionResponse>) {
+ this.setState(s => {
+ if (s.mentionsRes.state == "success" && res.state == "success") {
+ s.mentionsRes.data.mentions = editMention(
+ res.data.person_mention_view,
+ s.mentionsRes.data.mentions
+ );
+ }
+ return s;
+ });
}
}
import { Link } from "inferno-router";
import { RouteComponentProps } from "inferno-router/dist/Route";
import {
- AddAdminResponse,
+ AddAdmin,
+ AddModToCommunity,
+ BanFromCommunity,
+ BanFromCommunityResponse,
BanPerson,
BanPersonResponse,
BlockPerson,
- BlockPersonResponse,
+ CommentId,
+ CommentReplyResponse,
CommentResponse,
Community,
CommunityModeratorView,
+ CreateComment,
+ CreateCommentLike,
+ CreateCommentReport,
+ CreatePostLike,
+ CreatePostReport,
+ DeleteComment,
+ DeletePost,
+ DistinguishComment,
+ EditComment,
+ EditPost,
+ FeaturePost,
GetPersonDetails,
GetPersonDetailsResponse,
GetSiteResponse,
+ LockPost,
+ MarkCommentReplyAsRead,
+ MarkPersonMentionAsRead,
+ PersonView,
PostResponse,
+ PurgeComment,
PurgeItemResponse,
+ PurgePerson,
+ PurgePost,
+ RemoveComment,
+ RemovePost,
+ SaveComment,
+ SavePost,
SortType,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
+ TransferCommunity,
} from "lemmy-js-client";
import moment from "moment";
- import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
import {
QueryParams,
- WithPromiseKeys,
++ RouteDataResponse,
canMod,
capitalizeFirstLetter,
- createCommentLikeRes,
- createPostLikeFindRes,
- editCommentRes,
- editPostFindRes,
+ editComment,
+ editPost,
+ editWith,
enableDownvotes,
enableNsfw,
fetchLimit,
futureDaysToUnixTime,
+ getCommentParentId,
getPageFromString,
getQueryParams,
getQueryString,
isBanned,
mdToHtml,
myAuth,
+ myAuthRequired,
numToSI,
relTags,
restoreScrollPosition,
- saveCommentRes,
saveScrollPosition,
setIsoData,
setupTippy,
toast,
updatePersonBlock,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { BannerIconHeader } from "../common/banner-icon-header";
import { HtmlTags } from "../common/html-tags";
import { PersonDetails } from "./person-details";
import { PersonListing } from "./person-listing";
- interface ProfileData {
++type ProfileData = RouteDataResponse<{
+ personResponse: GetPersonDetailsResponse;
- }
++}>;
+
interface ProfileState {
- personRes?: GetPersonDetailsResponse;
- loading: boolean;
+ personRes: RequestState<GetPersonDetailsResponse>;
personBlocked: boolean;
banReason?: string;
banExpireDays?: number;
showBanDialog: boolean;
removeData: boolean;
siteRes: GetSiteResponse;
+ finished: Map<CommentId, boolean | undefined>;
+ isIsomorphic: boolean;
}
interface ProfileProps {
: PersonDetailsView.Overview;
}
- function toggleBlockPerson(recipientId: number, block: boolean) {
- const auth = myAuth();
-
- if (auth) {
- const blockUserForm: BlockPerson = {
- person_id: recipientId,
- block,
- auth,
- };
-
- WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
- }
- }
-
- const handleUnblockPerson = (personId: number) =>
- toggleBlockPerson(personId, false);
-
- const handleBlockPerson = (personId: number) =>
- toggleBlockPerson(personId, true);
-
const getCommunitiesListing = (
translationKey: NoOptionI18nKeys,
communityViews?: { community: Community }[]
RouteComponentProps<{ username: string }>,
ProfileState
> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<ProfileData>(this.context);
- private subscription?: Subscription;
state: ProfileState = {
- loading: true,
+ personRes: { state: "empty" },
personBlocked: false,
siteRes: this.isoData.site_res,
showBanDialog: false,
removeData: false,
+ finished: new Map(),
+ isIsomorphic: false,
};
constructor(props: RouteComponentProps<{ username: string }>, context: any) {
this.handleSortChange = this.handleSortChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ this.handleBlockPerson = this.handleBlockPerson.bind(this);
+ this.handleUnblockPerson = this.handleUnblockPerson.bind(this);
+
+ this.handleCreateComment = this.handleCreateComment.bind(this);
+ this.handleEditComment = this.handleEditComment.bind(this);
+ this.handleSaveComment = this.handleSaveComment.bind(this);
+ this.handleBlockPersonAlt = this.handleBlockPersonAlt.bind(this);
+ this.handleDeleteComment = this.handleDeleteComment.bind(this);
+ this.handleRemoveComment = this.handleRemoveComment.bind(this);
+ this.handleCommentVote = this.handleCommentVote.bind(this);
+ this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+ this.handleAddAdmin = this.handleAddAdmin.bind(this);
+ this.handlePurgePerson = this.handlePurgePerson.bind(this);
+ this.handlePurgeComment = this.handlePurgeComment.bind(this);
+ this.handleCommentReport = this.handleCommentReport.bind(this);
+ this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+ this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+ this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+ this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+ this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+ this.handleBanPerson = this.handleBanPerson.bind(this);
+ this.handlePostVote = this.handlePostVote.bind(this);
+ this.handlePostEdit = this.handlePostEdit.bind(this);
+ this.handlePostReport = this.handlePostReport.bind(this);
+ this.handleLockPost = this.handleLockPost.bind(this);
+ this.handleDeletePost = this.handleDeletePost.bind(this);
+ this.handleRemovePost = this.handleRemovePost.bind(this);
+ this.handleSavePost = this.handleSavePost.bind(this);
+ this.handlePurgePost = this.handlePurgePost.bind(this);
+ this.handleFeaturePost = this.handleFeaturePost.bind(this);
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
+ if (FirstLoadService.isFirstLoad) {
this.state = {
...this.state,
- personRes: this.isoData.routeData[0],
+ personRes: this.isoData.routeData.personResponse,
- loading: false,
+ isIsomorphic: true,
};
- } else {
- this.fetchUserData();
}
}
- fetchUserData() {
- const { page, sort, view } = getProfileQueryParams();
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.fetchUserData();
+ }
+ setupTippy();
+ }
- const form: GetPersonDetails = {
- username: this.props.match.params.username,
- sort,
- saved_only: view === PersonDetailsView.Saved,
- page,
- limit: fetchLimit,
- auth: myAuth(false),
- };
+ componentWillUnmount() {
+ saveScrollPosition(this.context);
+ }
+
+ async fetchUserData() {
+ const { page, sort, view } = getProfileQueryParams();
- WebSocketService.Instance.send(wsClient.getPersonDetails(form));
+ this.setState({ personRes: { state: "empty" } });
+ this.setState({
+ personRes: await HttpService.client.getPersonDetails({
+ username: this.props.match.params.username,
+ sort,
+ saved_only: view === PersonDetailsView.Saved,
+ page,
+ limit: fetchLimit,
+ auth: myAuth(),
+ }),
+ });
+ restoreScrollPosition(this.context);
+ this.setPersonBlock();
}
get amCurrentUser() {
- return (
- UserService.Instance.myUserInfo?.local_user_view.person.id ===
- this.state.personRes?.person_view.person.id
- );
+ if (this.state.personRes.state === "success") {
+ return (
+ UserService.Instance.myUserInfo?.local_user_view.person.id ===
+ this.state.personRes.data.person_view.person.id
+ );
+ } else {
+ return false;
+ }
}
setPersonBlock() {
const mui = UserService.Instance.myUserInfo;
const res = this.state.personRes;
- if (mui && res) {
+ if (mui && res.state === "success") {
this.setState({
personBlocked: mui.person_blocks.some(
- ({ target: { id } }) => id === res.person_view.person.id
+ ({ target: { id } }) => id === res.data.person_view.person.id
),
});
}
}
-- static fetchInitialData({
++ static async fetchInitialData({
client,
path,
query: { page, sort, view: urlView },
auth,
- }: InitialFetchRequest<
- QueryParams<ProfileProps>
- >): WithPromiseKeys<ProfileData> {
- }: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<
- RequestState<any>
- >[] {
++ }: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<ProfileData> {
const pathSplit = path.split("/");
const username = pathSplit[2];
auth,
};
- return [client.getPersonDetails(form)];
+ return {
- personResponse: client.getPersonDetails(form),
++ personResponse: await client.getPersonDetails(form),
+ };
}
- componentDidMount() {
- this.setPersonBlock();
- setupTippy();
- }
-
- componentWillUnmount() {
- this.subscription?.unsubscribe();
- saveScrollPosition(this.context);
- }
-
get documentTitle(): string {
+ const siteName = this.state.siteRes.site_view.site.name;
const res = this.state.personRes;
- return res
- ? `@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`
- : "";
+ return res.state == "success"
+ ? `@${res.data.person_view.person.name} - ${siteName}`
+ : siteName;
}
- render() {
- const { personRes, loading, siteRes } = this.state;
- const { page, sort, view } = getProfileQueryParams();
-
- return (
- <div className="container-lg">
- {loading ? (
+ renderPersonRes() {
+ switch (this.state.personRes.state) {
+ case "loading":
+ return (
<h5>
<Spinner large />
</h5>
- ) : (
- personRes && (
- <div className="row">
- <div className="col-12 col-md-8">
- <HtmlTags
- title={this.documentTitle}
- path={this.context.router.route.match.url}
- description={personRes.person_view.person.bio}
- image={personRes.person_view.person.avatar}
- />
-
- {this.userInfo}
-
- <hr />
-
- {this.selects}
-
- <PersonDetails
- personRes={personRes}
- admins={siteRes.admins}
- sort={sort}
- page={page}
- limit={fetchLimit}
- enableDownvotes={enableDownvotes(siteRes)}
- enableNsfw={enableNsfw(siteRes)}
- view={view}
- onPageChange={this.handlePageChange}
- allLanguages={siteRes.all_languages}
- siteLanguages={siteRes.discussion_languages}
- />
- </div>
+ );
+ case "success": {
+ const siteRes = this.state.siteRes;
+ const personRes = this.state.personRes.data;
+ const { page, sort, view } = getProfileQueryParams();
+
+ return (
+ <div className="row">
+ <div className="col-12 col-md-8">
+ <HtmlTags
+ title={this.documentTitle}
+ path={this.context.router.route.match.url}
+ description={personRes.person_view.person.bio}
+ image={personRes.person_view.person.avatar}
+ />
+
+ {this.userInfo(personRes.person_view)}
+
+ <hr />
+
+ {this.selects}
+
+ <PersonDetails
+ personRes={personRes}
+ admins={siteRes.admins}
+ sort={sort}
+ page={page}
+ limit={fetchLimit}
+ finished={this.state.finished}
+ enableDownvotes={enableDownvotes(siteRes)}
+ enableNsfw={enableNsfw(siteRes)}
+ view={view}
+ onPageChange={this.handlePageChange}
+ allLanguages={siteRes.all_languages}
+ siteLanguages={siteRes.discussion_languages}
+ // TODO all the forms here
+ onSaveComment={this.handleSaveComment}
+ onBlockPerson={this.handleBlockPersonAlt}
+ onDeleteComment={this.handleDeleteComment}
+ onRemoveComment={this.handleRemoveComment}
+ onCommentVote={this.handleCommentVote}
+ onCommentReport={this.handleCommentReport}
+ onDistinguishComment={this.handleDistinguishComment}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onPurgeComment={this.handlePurgeComment}
+ onPurgePerson={this.handlePurgePerson}
+ onCommentReplyRead={this.handleCommentReplyRead}
+ onPersonMentionRead={this.handlePersonMentionRead}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onBanPerson={this.handleBanPerson}
+ onCreateComment={this.handleCreateComment}
+ onEditComment={this.handleEditComment}
+ onPostEdit={this.handlePostEdit}
+ onPostVote={this.handlePostVote}
+ onPostReport={this.handlePostReport}
+ onLockPost={this.handleLockPost}
+ onDeletePost={this.handleDeletePost}
+ onRemovePost={this.handleRemovePost}
+ onSavePost={this.handleSavePost}
+ onPurgePost={this.handlePurgePost}
+ onFeaturePost={this.handleFeaturePost}
+ />
+ </div>
- <div className="col-12 col-md-4">
- <Moderates moderates={personRes.moderates} />
- {this.amCurrentUser && <Follows />}
- </div>
+ <div className="col-12 col-md-4">
+ <Moderates moderates={personRes.moderates} />
+ {this.amCurrentUser && <Follows />}
</div>
- )
- )}
- </div>
- );
+ </div>
+ );
+ }
+ }
+ }
+
+ render() {
+ return <div className="container-lg">{this.renderPersonRes()}</div>;
}
get viewRadios() {
{this.getRadio(PersonDetailsView.Overview)}
{this.getRadio(PersonDetailsView.Comments)}
{this.getRadio(PersonDetailsView.Posts)}
- {this.getRadio(PersonDetailsView.Saved)}
+ {this.amCurrentUser && this.getRadio(PersonDetailsView.Saved)}
</div>
);
}
);
}
- get userInfo() {
- const pv = this.state.personRes?.person_view;
+ userInfo(pv: PersonView) {
const {
personBlocked,
siteRes: { admins },
)}
</ul>
</div>
- {this.banDialog}
+ {this.banDialog(pv)}
<div className="flex-grow-1 unselectable pointer mx-2"></div>
{!this.amCurrentUser && UserService.Instance.myUserInfo && (
<>
className={
"d-flex align-self-start btn btn-secondary mr-2"
}
- onClick={linkEvent(pv.person.id, handleUnblockPerson)}
+ onClick={linkEvent(
+ pv.person.id,
+ this.handleUnblockPerson
+ )}
>
{i18n.t("unblock_user")}
</button>
className={
"d-flex align-self-start btn btn-secondary mr-2"
}
- onClick={linkEvent(pv.person.id, handleBlockPerson)}
+ onClick={linkEvent(
+ pv.person.id,
+ this.handleBlockPerson
+ )}
>
{i18n.t("block_user")}
</button>
);
}
- get banDialog() {
- const pv = this.state.personRes?.person_view;
+ banDialog(pv: PersonView) {
const { showBanDialog } = this.state;
return (
- pv && (
- <>
- {showBanDialog && (
- <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
- <div className="form-group row col-12">
- <label className="col-form-label" htmlFor="profile-ban-reason">
- {i18n.t("reason")}
- </label>
- <input
- type="text"
- id="profile-ban-reason"
- className="form-control mr-2"
- placeholder={i18n.t("reason")}
- value={this.state.banReason}
- onInput={linkEvent(this, this.handleModBanReasonChange)}
- />
- <label className="col-form-label" htmlFor={`mod-ban-expires`}>
- {i18n.t("expires")}
- </label>
+ showBanDialog && (
+ <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
+ <div className="form-group row col-12">
+ <label className="col-form-label" htmlFor="profile-ban-reason">
+ {i18n.t("reason")}
+ </label>
+ <input
+ type="text"
+ id="profile-ban-reason"
+ className="form-control mr-2"
+ placeholder={i18n.t("reason")}
+ value={this.state.banReason}
+ onInput={linkEvent(this, this.handleModBanReasonChange)}
+ />
+ <label className="col-form-label" htmlFor={`mod-ban-expires`}>
+ {i18n.t("expires")}
+ </label>
+ <input
+ type="number"
+ id={`mod-ban-expires`}
+ className="form-control mr-2"
+ placeholder={i18n.t("number_of_days")}
+ value={this.state.banExpireDays}
+ onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
+ />
+ <div className="form-group">
+ <div className="form-check">
<input
- type="number"
- id={`mod-ban-expires`}
- className="form-control mr-2"
- placeholder={i18n.t("number_of_days")}
- value={this.state.banExpireDays}
- onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
+ className="form-check-input"
+ id="mod-ban-remove-data"
+ type="checkbox"
+ checked={this.state.removeData}
+ onChange={linkEvent(this, this.handleModRemoveDataChange)}
/>
- <div className="form-group">
- <div className="form-check">
- <input
- className="form-check-input"
- id="mod-ban-remove-data"
- type="checkbox"
- checked={this.state.removeData}
- onChange={linkEvent(this, this.handleModRemoveDataChange)}
- />
- <label
- className="form-check-label"
- htmlFor="mod-ban-remove-data"
- title={i18n.t("remove_content_more")}
- >
- {i18n.t("remove_content")}
- </label>
- </div>
- </div>
- </div>
- {/* TODO hold off on expires until later */}
- {/* <div class="form-group row"> */}
- {/* <label class="col-form-label">Expires</label> */}
- {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
- {/* </div> */}
- <div className="form-group row">
- <button
- type="reset"
- className="btn btn-secondary mr-2"
- aria-label={i18n.t("cancel")}
- onClick={linkEvent(this, this.handleModBanSubmitCancel)}
- >
- {i18n.t("cancel")}
- </button>
- <button
- type="submit"
- className="btn btn-secondary"
- aria-label={i18n.t("ban")}
+ <label
+ className="form-check-label"
+ htmlFor="mod-ban-remove-data"
+ title={i18n.t("remove_content_more")}
>
- {i18n.t("ban")} {pv.person.name}
- </button>
+ {i18n.t("remove_content")}
+ </label>
</div>
- </form>
- )}
- </>
+ </div>
+ </div>
+ {/* TODO hold off on expires until later */}
+ {/* <div class="form-group row"> */}
+ {/* <label class="col-form-label">Expires</label> */}
+ {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
+ {/* </div> */}
+ <div className="form-group row">
+ <button
+ type="reset"
+ className="btn btn-secondary mr-2"
+ aria-label={i18n.t("cancel")}
+ onClick={linkEvent(this, this.handleModBanSubmitCancel)}
+ >
+ {i18n.t("cancel")}
+ </button>
+ <button
+ type="submit"
+ className="btn btn-secondary"
+ aria-label={i18n.t("ban")}
+ >
+ {i18n.t("ban")} {pv.person.name}
+ </button>
+ </div>
+ </form>
)
);
}
- updateUrl({ page, sort, view }: Partial<ProfileProps>) {
+ async updateUrl({ page, sort, view }: Partial<ProfileProps>) {
const {
page: urlPage,
sort: urlSort,
const { username } = this.props.match.params;
this.props.history.push(`/u/${username}${getQueryString(queryParams)}`);
-
- this.setState({ loading: true });
- this.fetchUserData();
+ await this.fetchUserData();
}
handlePageChange(page: number) {
i.setState({ removeData: event.target.checked });
}
- handleModBanSubmitCancel(i: Profile, event?: any) {
- event.preventDefault();
+ handleModBanSubmitCancel(i: Profile) {
i.setState({ showBanDialog: false });
}
- handleModBanSubmit(i: Profile, event?: any) {
- if (event) event.preventDefault();
- const { personRes, removeData, banReason, banExpireDays } = i.state;
+ async handleModBanSubmit(i: Profile, event: any) {
+ event.preventDefault();
+ const { removeData, banReason, banExpireDays } = i.state;
- const person = personRes?.person_view.person;
- const auth = myAuth();
+ const personRes = i.state.personRes;
- if (person && auth) {
+ if (personRes.state == "success") {
+ const person = personRes.data.person_view.person;
const ban = !person.banned;
// If its an unban, restore all their data
i.setState({ removeData: false });
}
- const form: BanPerson = {
+ const res = await HttpService.client.banPerson({
person_id: person.id,
ban,
remove_data: removeData,
reason: banReason,
expires: futureDaysToUnixTime(banExpireDays),
- auth,
- };
- WebSocketService.Instance.send(wsClient.banPerson(form));
-
+ auth: myAuthRequired(),
+ });
+ // TODO
+ this.updateBan(res);
i.setState({ showBanDialog: false });
}
}
- parseMessage(msg: any) {
- const op = wsUserOp(msg);
- console.log(msg);
+ async toggleBlockPerson(recipientId: number, block: boolean) {
+ const res = await HttpService.client.blockPerson({
+ person_id: recipientId,
+ block,
+ auth: myAuthRequired(),
+ });
+ if (res.state == "success") {
+ updatePersonBlock(res.data);
+ }
+ }
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
+ handleUnblockPerson(personId: number) {
+ this.toggleBlockPerson(personId, false);
+ }
- if (msg.error === "couldnt_find_that_username_or_email") {
- this.context.router.history.push("/");
- }
- } else if (msg.reconnect) {
- this.fetchUserData();
- } else {
- switch (op) {
- case UserOperation.GetPersonDetails: {
- // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
- // and set the parent state if it is not set or differs
- // TODO this might need to get abstracted
- const data = wsJsonToRes<GetPersonDetailsResponse>(msg);
- this.setState({ personRes: data, loading: false });
- this.setPersonBlock();
- restoreScrollPosition(this.context);
-
- break;
- }
+ handleBlockPerson(personId: number) {
+ this.toggleBlockPerson(personId, true);
+ }
- case UserOperation.AddAdmin: {
- const { admins } = wsJsonToRes<AddAdminResponse>(msg);
- this.setState(s => ((s.siteRes.admins = admins), s));
+ async handleAddModToCommunity(form: AddModToCommunity) {
+ // TODO not sure what to do here
+ await HttpService.client.addModToCommunity(form);
+ }
- break;
- }
+ async handlePurgePerson(form: PurgePerson) {
+ const purgePersonRes = await HttpService.client.purgePerson(form);
+ this.purgeItem(purgePersonRes);
+ }
- case UserOperation.CreateCommentLike: {
- const { comment_view } = wsJsonToRes<CommentResponse>(msg);
- createCommentLikeRes(comment_view, this.state.personRes?.comments);
- this.setState(this.state);
+ async handlePurgeComment(form: PurgeComment) {
+ const purgeCommentRes = await HttpService.client.purgeComment(form);
+ this.purgeItem(purgeCommentRes);
+ }
- break;
- }
+ async handlePurgePost(form: PurgePost) {
+ const purgeRes = await HttpService.client.purgePost(form);
+ this.purgeItem(purgeRes);
+ }
+
+ async handleBlockPersonAlt(form: BlockPerson) {
+ const blockPersonRes = await HttpService.client.blockPerson(form);
+ if (blockPersonRes.state === "success") {
+ updatePersonBlock(blockPersonRes.data);
+ }
+ }
- case UserOperation.EditComment:
- case UserOperation.DeleteComment:
- case UserOperation.RemoveComment: {
- const { comment_view } = wsJsonToRes<CommentResponse>(msg);
- editCommentRes(comment_view, this.state.personRes?.comments);
- this.setState(this.state);
+ async handleCreateComment(form: CreateComment) {
+ const createCommentRes = await HttpService.client.createComment(form);
+ this.createAndUpdateComments(createCommentRes);
- break;
- }
+ return createCommentRes;
+ }
- case UserOperation.CreateComment: {
- const {
- comment_view: {
- creator: { id },
- },
- } = wsJsonToRes<CommentResponse>(msg);
- const mui = UserService.Instance.myUserInfo;
+ async handleEditComment(form: EditComment) {
+ const editCommentRes = await HttpService.client.editComment(form);
+ this.findAndUpdateComment(editCommentRes);
- if (id === mui?.local_user_view.person.id) {
- toast(i18n.t("reply_sent"));
- }
+ return editCommentRes;
+ }
- break;
- }
+ async handleDeleteComment(form: DeleteComment) {
+ const deleteCommentRes = await HttpService.client.deleteComment(form);
+ this.findAndUpdateComment(deleteCommentRes);
+ }
- case UserOperation.SaveComment: {
- const { comment_view } = wsJsonToRes<CommentResponse>(msg);
- saveCommentRes(comment_view, this.state.personRes?.comments);
- this.setState(this.state);
+ async handleDeletePost(form: DeletePost) {
+ const deleteRes = await HttpService.client.deletePost(form);
+ this.findAndUpdatePost(deleteRes);
+ }
- break;
- }
+ async handleRemovePost(form: RemovePost) {
+ const removeRes = await HttpService.client.removePost(form);
+ this.findAndUpdatePost(removeRes);
+ }
- case UserOperation.EditPost:
- case UserOperation.DeletePost:
- case UserOperation.RemovePost:
- case UserOperation.LockPost:
- case UserOperation.FeaturePost:
- case UserOperation.SavePost: {
- const { post_view } = wsJsonToRes<PostResponse>(msg);
- editPostFindRes(post_view, this.state.personRes?.posts);
- this.setState(this.state);
-
- break;
- }
+ async handleRemoveComment(form: RemoveComment) {
+ const removeCommentRes = await HttpService.client.removeComment(form);
+ this.findAndUpdateComment(removeCommentRes);
+ }
+
+ async handleSaveComment(form: SaveComment) {
+ const saveCommentRes = await HttpService.client.saveComment(form);
+ this.findAndUpdateComment(saveCommentRes);
+ }
+
+ async handleSavePost(form: SavePost) {
+ const saveRes = await HttpService.client.savePost(form);
+ this.findAndUpdatePost(saveRes);
+ }
+
+ async handleFeaturePost(form: FeaturePost) {
+ const featureRes = await HttpService.client.featurePost(form);
+ this.findAndUpdatePost(featureRes);
+ }
+
+ async handleCommentVote(form: CreateCommentLike) {
+ const voteRes = await HttpService.client.likeComment(form);
+ this.findAndUpdateComment(voteRes);
+ }
+
+ async handlePostVote(form: CreatePostLike) {
+ const voteRes = await HttpService.client.likePost(form);
+ this.findAndUpdatePost(voteRes);
+ }
+
+ async handlePostEdit(form: EditPost) {
+ const res = await HttpService.client.editPost(form);
+ this.findAndUpdatePost(res);
+ }
+
+ async handleCommentReport(form: CreateCommentReport) {
+ const reportRes = await HttpService.client.createCommentReport(form);
+ if (reportRes.state === "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
- case UserOperation.CreatePostLike: {
- const { post_view } = wsJsonToRes<PostResponse>(msg);
- createPostLikeFindRes(post_view, this.state.personRes?.posts);
- this.setState(this.state);
+ async handlePostReport(form: CreatePostReport) {
+ const reportRes = await HttpService.client.createPostReport(form);
+ if (reportRes.state === "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
+
+ async handleLockPost(form: LockPost) {
+ const lockRes = await HttpService.client.lockPost(form);
+ this.findAndUpdatePost(lockRes);
+ }
+
+ async handleDistinguishComment(form: DistinguishComment) {
+ const distinguishRes = await HttpService.client.distinguishComment(form);
+ this.findAndUpdateComment(distinguishRes);
+ }
- break;
+ async handleAddAdmin(form: AddAdmin) {
+ const addAdminRes = await HttpService.client.addAdmin(form);
+
+ if (addAdminRes.state == "success") {
+ this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+ }
+ }
+
+ async handleTransferCommunity(form: TransferCommunity) {
+ await HttpService.client.transferCommunity(form);
+ toast(i18n.t("transfer_community"));
+ }
+
+ async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+ const readRes = await HttpService.client.markCommentReplyAsRead(form);
+ this.findAndUpdateCommentReply(readRes);
+ }
+
+ async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+ // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
+ await HttpService.client.markPersonMentionAsRead(form);
+ }
+
+ async handleBanFromCommunity(form: BanFromCommunity) {
+ const banRes = await HttpService.client.banFromCommunity(form);
+ this.updateBanFromCommunity(banRes);
+ }
+
+ async handleBanPerson(form: BanPerson) {
+ const banRes = await HttpService.client.banPerson(form);
+ this.updateBan(banRes);
+ }
+
+ updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
+ // Maybe not necessary
+ if (banRes.state === "success") {
+ this.setState(s => {
+ if (s.personRes.state == "success") {
+ s.personRes.data.posts
+ .filter(c => c.creator.id === banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
+
+ s.personRes.data.comments
+ .filter(c => c.creator.id === banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
}
+ return s;
+ });
+ }
+ }
- case UserOperation.BanPerson: {
- const data = wsJsonToRes<BanPersonResponse>(msg);
- const res = this.state.personRes;
- res?.comments
- .filter(c => c.creator.id === data.person_view.person.id)
- .forEach(c => (c.creator.banned = data.banned));
- res?.posts
- .filter(c => c.creator.id === data.person_view.person.id)
- .forEach(c => (c.creator.banned = data.banned));
- const pv = res?.person_view;
-
- if (pv?.person.id === data.person_view.person.id) {
- pv.person.banned = data.banned;
- }
- this.setState(this.state);
-
- break;
+ updateBan(banRes: RequestState<BanPersonResponse>) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (s.personRes.state == "success") {
+ s.personRes.data.posts
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
+ s.personRes.data.comments
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
}
+ return s;
+ });
+ }
+ }
- case UserOperation.BlockPerson: {
- const data = wsJsonToRes<BlockPersonResponse>(msg);
- updatePersonBlock(data);
- this.setPersonBlock();
+ purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
+ if (purgeRes.state == "success") {
+ toast(i18n.t("purge_success"));
+ this.context.router.history.push(`/`);
+ }
+ }
- break;
- }
+ findAndUpdateComment(res: RequestState<CommentResponse>) {
+ this.setState(s => {
+ if (s.personRes.state == "success" && res.state == "success") {
+ s.personRes.data.comments = editComment(
+ res.data.comment_view,
+ s.personRes.data.comments
+ );
+ s.finished.set(res.data.comment_view.comment.id, true);
+ }
+ return s;
+ });
+ }
- case UserOperation.PurgePerson:
- case UserOperation.PurgePost:
- case UserOperation.PurgeComment:
- case UserOperation.PurgeCommunity: {
- const { success } = wsJsonToRes<PurgeItemResponse>(msg);
+ createAndUpdateComments(res: RequestState<CommentResponse>) {
+ this.setState(s => {
+ if (s.personRes.state == "success" && res.state == "success") {
+ s.personRes.data.comments.unshift(res.data.comment_view);
+ // Set finished for the parent
+ s.finished.set(
+ getCommentParentId(res.data.comment_view.comment) ?? 0,
+ true
+ );
+ }
+ return s;
+ });
+ }
- if (success) {
- toast(i18n.t("purge_success"));
- this.context.router.history.push(`/`);
- }
- }
+ findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
+ this.setState(s => {
+ if (s.personRes.state == "success" && res.state == "success") {
+ s.personRes.data.comments = editWith(
+ res.data.comment_reply_view,
+ s.personRes.data.comments
+ );
}
- }
+ return s;
+ });
+ }
+
+ findAndUpdatePost(res: RequestState<PostResponse>) {
+ this.setState(s => {
+ if (s.personRes.state == "success" && res.state == "success") {
+ s.personRes.data.posts = editPost(
+ res.data.post_view,
+ s.personRes.data.posts
+ );
+ }
+ return s;
+ });
}
}
import { Component, linkEvent } from "inferno";
import {
+ ApproveRegistrationApplication,
GetSiteResponse,
-- ListRegistrationApplications,
ListRegistrationApplicationsResponse,
- RegistrationApplicationResponse,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
+ RegistrationApplicationView,
} from "lemmy-js-client";
- import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
import {
- WithPromiseKeys,
++ RouteDataResponse,
+ editRegistrationApplication,
fetchLimit,
- isBrowser,
- myAuth,
+ myAuthRequired,
setIsoData,
setupTippy,
- toast,
- updateRegistrationApplicationRes,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
All,
}
- interface RegistrationApplicationsData {
++type RegistrationApplicationsData = RouteDataResponse<{
+ listRegistrationApplicationsResponse: ListRegistrationApplicationsResponse;
- }
++}>;
+
interface RegistrationApplicationsState {
- listRegistrationApplicationsResponse?: ListRegistrationApplicationsResponse;
+ appsRes: RequestState<ListRegistrationApplicationsResponse>;
siteRes: GetSiteResponse;
unreadOrAll: UnreadOrAll;
page: number;
- loading: boolean;
+ isIsomorphic: boolean;
}
export class RegistrationApplications extends Component<
any,
RegistrationApplicationsState
> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<RegistrationApplicationsData>(this.context);
- private subscription?: Subscription;
state: RegistrationApplicationsState = {
+ appsRes: { state: "empty" },
siteRes: this.isoData.site_res,
unreadOrAll: UnreadOrAll.Unread,
page: 1,
- loading: true,
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
super(props, context);
this.handlePageChange = this.handlePageChange.bind(this);
-
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ this.handleApproveApplication = this.handleApproveApplication.bind(this);
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
+ if (FirstLoadService.isFirstLoad) {
this.state = {
...this.state,
- listRegistrationApplicationsResponse:
- this.isoData.routeData.listRegistrationApplicationsResponse,
- loading: false,
- appsRes: this.isoData.routeData[0],
++ appsRes: this.isoData.routeData.listRegistrationApplicationsResponse,
+ isIsomorphic: true,
};
- } else {
- this.refetch();
}
}
- componentDidMount() {
- setupTippy();
- }
-
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.refetch();
}
+ setupTippy();
}
get documentTitle(): string {
- let mui = UserService.Instance.myUserInfo;
+ const mui = UserService.Instance.myUserInfo;
return mui
? `@${mui.local_user_view.person.name} ${i18n.t(
"registration_applications"
: "";
}
- render() {
- return (
- <div className="container-lg">
- {this.state.loading ? (
+ renderApps() {
+ switch (this.state.appsRes.state) {
+ case "loading":
+ return (
<h5>
<Spinner large />
</h5>
- ) : (
+ );
+ case "success": {
+ const apps = this.state.appsRes.data.registration_applications;
+ return (
<div className="row">
<div className="col-12">
<HtmlTags
/>
<h5 className="mb-2">{i18n.t("registration_applications")}</h5>
{this.selects()}
- {this.applicationList()}
+ {this.applicationList(apps)}
<Paginator
page={this.state.page}
onChange={this.handlePageChange}
/>
</div>
</div>
- )}
- </div>
- );
+ );
+ }
+ }
+ }
+
+ render() {
+ return <div className="container-lg">{this.renderApps()}</div>;
}
unreadOrAllRadios() {
);
}
- applicationList() {
- let res = this.state.listRegistrationApplicationsResponse;
+ applicationList(apps: RegistrationApplicationView[]) {
return (
- res && (
- <div>
- {res.registration_applications.map(ra => (
- <>
- <hr />
- <RegistrationApplication
- key={ra.registration_application.id}
- application={ra}
- />
- </>
- ))}
- </div>
- )
+ <div>
+ {apps.map(ra => (
+ <>
+ <hr />
+ <RegistrationApplication
+ key={ra.registration_application.id}
+ application={ra}
+ onApproveApplication={this.handleApproveApplication}
+ />
+ </>
+ ))}
+ </div>
);
}
this.refetch();
}
-- static fetchInitialData({
++ static async fetchInitialData({
auth,
client,
- }: InitialFetchRequest): WithPromiseKeys<RegistrationApplicationsData> {
- const form: ListRegistrationApplications = {
- unread_only: true,
- page: 1,
- limit: fetchLimit,
- auth: auth as string,
- };
- }: InitialFetchRequest): Promise<any>[] {
- const promises: Promise<RequestState<any>>[] = [];
-
- if (auth) {
- const form: ListRegistrationApplications = {
- unread_only: true,
- page: 1,
- limit: fetchLimit,
- auth,
- };
- promises.push(client.listRegistrationApplications(form));
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
- }
--
- return promises;
++ }: InitialFetchRequest): Promise<RegistrationApplicationsData> {
+ return {
- listRegistrationApplicationsResponse:
- client.listRegistrationApplications(form),
++ listRegistrationApplicationsResponse: auth
++ ? await client.listRegistrationApplications({
++ unread_only: true,
++ page: 1,
++ limit: fetchLimit,
++ auth: auth as string,
++ })
++ : { state: "empty" },
+ };
}
- refetch() {
- let unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
- let auth = myAuth();
- if (auth) {
- let form: ListRegistrationApplications = {
+ async refetch() {
+ const unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
+ this.setState({
+ appsRes: { state: "loading" },
+ });
+ this.setState({
+ appsRes: await HttpService.client.listRegistrationApplications({
unread_only: unread_only,
page: this.state.page,
limit: fetchLimit,
- auth,
- };
- WebSocketService.Instance.send(
- wsClient.listRegistrationApplications(form)
- );
- }
+ auth: myAuthRequired(),
+ }),
+ });
}
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- return;
- } else if (msg.reconnect) {
- this.refetch();
- } else if (op == UserOperation.ListRegistrationApplications) {
- let data = wsJsonToRes<ListRegistrationApplicationsResponse>(msg);
- this.setState({
- listRegistrationApplicationsResponse: data,
- loading: false,
- });
- window.scrollTo(0, 0);
- } else if (op == UserOperation.ApproveRegistrationApplication) {
- let data = wsJsonToRes<RegistrationApplicationResponse>(msg);
- updateRegistrationApplicationRes(
- data.registration_application,
- this.state.listRegistrationApplicationsResponse
- ?.registration_applications
- );
- let uacs = UserService.Instance.unreadApplicationCountSub;
- // Minor bug, where if the application switches from deny to approve, the count will still go down
- uacs.next(uacs.getValue() - 1);
- this.setState(this.state);
- }
+ async handleApproveApplication(form: ApproveRegistrationApplication) {
+ const approveRes = await HttpService.client.approveRegistrationApplication(
+ form
+ );
+ this.setState(s => {
+ if (s.appsRes.state == "success" && approveRes.state == "success") {
+ s.appsRes.data.registration_applications = editRegistrationApplication(
+ approveRes.data.registration_application,
+ s.appsRes.data.registration_applications
+ );
+ }
+ return s;
+ });
}
}
PostReportView,
PrivateMessageReportResponse,
PrivateMessageReportView,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
+ ResolveCommentReport,
+ ResolvePostReport,
+ ResolvePrivateMessageReport,
} from "lemmy-js-client";
- import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { HttpService, UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { RequestState } from "../../services/HttpService";
import {
- WithPromiseKeys,
++ RouteDataResponse,
amAdmin,
+ editCommentReport,
+ editPostReport,
+ editPrivateMessageReport,
fetchLimit,
- isBrowser,
- myAuth,
+ myAuthRequired,
setIsoData,
- setupTippy,
- toast,
- updateCommentReportRes,
- updatePostReportRes,
- updatePrivateMessageReportRes,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { CommentReport } from "../comment/comment-report";
import { HtmlTags } from "../common/html-tags";
PrivateMessageReport,
}
- interface ReportsData {
++type ReportsData = RouteDataResponse<{
+ commentReportsResponse: ListCommentReportsResponse;
+ postReportsResponse: ListPostReportsResponse;
+ privateMessageReportsResponse?: ListPrivateMessageReportsResponse;
- }
++}>;
+
type ItemType = {
id: number;
type_: MessageEnum;
};
interface ReportsState {
- listCommentReportsResponse?: ListCommentReportsResponse;
- listPostReportsResponse?: ListPostReportsResponse;
- listPrivateMessageReportsResponse?: ListPrivateMessageReportsResponse;
+ commentReportsRes: RequestState<ListCommentReportsResponse>;
+ postReportsRes: RequestState<ListPostReportsResponse>;
+ messageReportsRes: RequestState<ListPrivateMessageReportsResponse>;
unreadOrAll: UnreadOrAll;
messageType: MessageType;
- combined: ItemType[];
siteRes: GetSiteResponse;
page: number;
- loading: boolean;
+ isIsomorphic: boolean;
}
export class Reports extends Component<any, ReportsState> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<ReportsData>(this.context);
- private subscription?: Subscription;
state: ReportsState = {
+ commentReportsRes: { state: "empty" },
+ postReportsRes: { state: "empty" },
+ messageReportsRes: { state: "empty" },
unreadOrAll: UnreadOrAll.Unread,
messageType: MessageType.All,
- combined: [],
page: 1,
siteRes: this.isoData.site_res,
- loading: true,
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
super(props, context);
this.handlePageChange = this.handlePageChange.bind(this);
-
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ this.handleResolveCommentReport =
+ this.handleResolveCommentReport.bind(this);
+ this.handleResolvePostReport = this.handleResolvePostReport.bind(this);
+ this.handleResolvePrivateMessageReport =
+ this.handleResolvePrivateMessageReport.bind(this);
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
+ if (FirstLoadService.isFirstLoad) {
- const [commentReportsRes, postReportsRes, messageReportsRes] =
- this.isoData.routeData;
+ const {
- commentReportsResponse,
- postReportsResponse,
- privateMessageReportsResponse,
++ commentReportsResponse: commentReportsRes,
++ postReportsResponse: postReportsRes,
++ privateMessageReportsResponse: messageReportsRes,
+ } = this.isoData.routeData;
+
this.state = {
...this.state,
- listCommentReportsResponse: commentReportsResponse,
- listPostReportsResponse: postReportsResponse,
- listPrivateMessageReportsResponse: privateMessageReportsResponse,
+ commentReportsRes,
+ postReportsRes,
+ isIsomorphic: true,
};
- this.state = {
- ...this.state,
- combined: this.buildCombined(),
- loading: false,
- };
- } else {
- this.refetch();
+ if (amAdmin()) {
+ this.state = {
+ ...this.state,
- messageReportsRes,
++ messageReportsRes: messageReportsRes ?? { state: "empty" },
+ };
+ }
}
}
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.refetch();
}
}
get documentTitle(): string {
- let mui = UserService.Instance.myUserInfo;
+ const mui = UserService.Instance.myUserInfo;
return mui
? `@${mui.local_user_view.person.name} ${i18n.t("reports")} - ${
this.state.siteRes.site_view.site.name
render() {
return (
<div className="container-lg">
- {this.state.loading ? (
- <h5>
- <Spinner large />
- </h5>
- ) : (
- <div className="row">
- <div className="col-12">
- <HtmlTags
- title={this.documentTitle}
- path={this.context.router.route.match.url}
- />
- <h5 className="mb-2">{i18n.t("reports")}</h5>
- {this.selects()}
- {this.state.messageType == MessageType.All && this.all()}
- {this.state.messageType == MessageType.CommentReport &&
- this.commentReports()}
- {this.state.messageType == MessageType.PostReport &&
- this.postReports()}
- {this.state.messageType == MessageType.PrivateMessageReport &&
- this.privateMessageReports()}
- <Paginator
- page={this.state.page}
- onChange={this.handlePageChange}
- />
- </div>
+ <div className="row">
+ <div className="col-12">
+ <HtmlTags
+ title={this.documentTitle}
+ path={this.context.router.route.match.url}
+ />
+ <h5 className="mb-2">{i18n.t("reports")}</h5>
+ {this.selects()}
+ {this.section}
+ <Paginator
+ page={this.state.page}
+ onChange={this.handlePageChange}
+ />
</div>
- )}
+ </div>
</div>
);
}
+ get section() {
+ switch (this.state.messageType) {
+ case MessageType.All: {
+ return this.all();
+ }
+ case MessageType.CommentReport: {
+ return this.commentReports();
+ }
+ case MessageType.PostReport: {
+ return this.postReports();
+ }
+ case MessageType.PrivateMessageReport: {
+ return this.privateMessageReports();
+ }
+
+ default: {
+ return null;
+ }
+ }
+ }
+
unreadOrAllRadios() {
return (
<div className="btn-group btn-group-toggle flex-wrap mb-2">
};
}
- buildCombined(): ItemType[] {
- // let comments: ItemType[] = this.state.listCommentReportsResponse
- // .map(r => r.comment_reports)
- // .unwrapOr([])
- // .map(r => this.commentReportToItemType(r));
- let comments =
- this.state.listCommentReportsResponse?.comment_reports.map(
- this.commentReportToItemType
- ) ?? [];
- let posts =
- this.state.listPostReportsResponse?.post_reports.map(
- this.postReportToItemType
- ) ?? [];
- let privateMessages =
- this.state.listPrivateMessageReportsResponse?.private_message_reports.map(
- this.privateMessageReportToItemType
- ) ?? [];
+ get buildCombined(): ItemType[] {
+ const commentRes = this.state.commentReportsRes;
+ const comments =
+ commentRes.state == "success"
+ ? commentRes.data.comment_reports.map(this.commentReportToItemType)
+ : [];
+
+ const postRes = this.state.postReportsRes;
+ const posts =
+ postRes.state == "success"
+ ? postRes.data.post_reports.map(this.postReportToItemType)
+ : [];
+ const pmRes = this.state.messageReportsRes;
+ const privateMessages =
+ pmRes.state == "success"
+ ? pmRes.data.private_message_reports.map(
+ this.privateMessageReportToItemType
+ )
+ : [];
return [...comments, ...posts, ...privateMessages].sort((a, b) =>
b.published.localeCompare(a.published)
switch (i.type_) {
case MessageEnum.CommentReport:
return (
- <CommentReport key={i.id} report={i.view as CommentReportView} />
+ <CommentReport
+ key={i.id}
+ report={i.view as CommentReportView}
+ onResolveReport={this.handleResolveCommentReport}
+ />
);
case MessageEnum.PostReport:
- return <PostReport key={i.id} report={i.view as PostReportView} />;
+ return (
+ <PostReport
+ key={i.id}
+ report={i.view as PostReportView}
+ onResolveReport={this.handleResolvePostReport}
+ />
+ );
case MessageEnum.PrivateMessageReport:
return (
<PrivateMessageReport
key={i.id}
report={i.view as PrivateMessageReportView}
+ onResolveReport={this.handleResolvePrivateMessageReport}
/>
);
default:
all() {
return (
<div>
- {this.state.combined.map(i => (
+ {this.buildCombined.map(i => (
<>
<hr />
{this.renderItemType(i)}
}
commentReports() {
- let reports = this.state.listCommentReportsResponse?.comment_reports;
- return (
- reports && (
- <div>
- {reports.map(cr => (
- <>
- <hr />
- <CommentReport key={cr.comment_report.id} report={cr} />
- </>
- ))}
- </div>
- )
- );
+ const res = this.state.commentReportsRes;
+ switch (res.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const reports = res.data.comment_reports;
+ return (
+ <div>
+ {reports.map(cr => (
+ <>
+ <hr />
+ <CommentReport
+ key={cr.comment_report.id}
+ report={cr}
+ onResolveReport={this.handleResolveCommentReport}
+ />
+ </>
+ ))}
+ </div>
+ );
+ }
+ }
}
postReports() {
- let reports = this.state.listPostReportsResponse?.post_reports;
- return (
- reports && (
- <div>
- {reports.map(pr => (
- <>
- <hr />
- <PostReport key={pr.post_report.id} report={pr} />
- </>
- ))}
- </div>
- )
- );
+ const res = this.state.postReportsRes;
+ switch (res.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const reports = res.data.post_reports;
+ return (
+ <div>
+ {reports.map(pr => (
+ <>
+ <hr />
+ <PostReport
+ key={pr.post_report.id}
+ report={pr}
+ onResolveReport={this.handleResolvePostReport}
+ />
+ </>
+ ))}
+ </div>
+ );
+ }
+ }
}
privateMessageReports() {
- let reports =
- this.state.listPrivateMessageReportsResponse?.private_message_reports;
- return (
- reports && (
- <div>
- {reports.map(pmr => (
- <>
- <hr />
- <PrivateMessageReport
- key={pmr.private_message_report.id}
- report={pmr}
- />
- </>
- ))}
- </div>
- )
- );
+ const res = this.state.messageReportsRes;
+ switch (res.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const reports = res.data.private_message_reports;
+ return (
+ <div>
+ {reports.map(pmr => (
+ <>
+ <hr />
+ <PrivateMessageReport
+ key={pmr.private_message_report.id}
+ report={pmr}
+ onResolveReport={this.handleResolvePrivateMessageReport}
+ />
+ </>
+ ))}
+ </div>
+ );
+ }
+ }
}
- handlePageChange(page: number) {
+ async handlePageChange(page: number) {
this.setState({ page });
- this.refetch();
+ await this.refetch();
}
- handleUnreadOrAllChange(i: Reports, event: any) {
+ async handleUnreadOrAllChange(i: Reports, event: any) {
i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
- i.refetch();
+ await i.refetch();
}
- handleMessageTypeChange(i: Reports, event: any) {
+ async handleMessageTypeChange(i: Reports, event: any) {
i.setState({ messageType: Number(event.target.value), page: 1 });
- i.refetch();
+ await i.refetch();
}
-- static fetchInitialData({
++ static async fetchInitialData({
auth,
client,
- }: InitialFetchRequest): WithPromiseKeys<ReportsData> {
- }: InitialFetchRequest): Promise<any>[] {
- const promises: Promise<RequestState<any>>[] = [];
-
++ }: InitialFetchRequest): Promise<ReportsData> {
const unresolved_only = true;
const page = 1;
const limit = fetchLimit;
- if (auth) {
- const commentReportsForm: ListCommentReports = {
- unresolved_only,
- page,
- limit,
- auth,
- };
- promises.push(client.listCommentReports(commentReportsForm));
+ const commentReportsForm: ListCommentReports = {
+ unresolved_only,
+ page,
+ limit,
+ auth: auth as string,
+ };
- const postReportsForm: ListPostReports = {
+ const postReportsForm: ListPostReports = {
+ unresolved_only,
+ page,
+ limit,
+ auth: auth as string,
+ };
+
- const data: WithPromiseKeys<ReportsData> = {
- commentReportsResponse: client.listCommentReports(commentReportsForm),
- postReportsResponse: client.listPostReports(postReportsForm),
++ const data: ReportsData = {
++ commentReportsResponse: await client.listCommentReports(
++ commentReportsForm
++ ),
++ postReportsResponse: await client.listPostReports(postReportsForm),
+ };
+
+ if (amAdmin()) {
+ const privateMessageReportsForm: ListPrivateMessageReports = {
unresolved_only,
page,
limit,
- auth,
+ auth: auth as string,
};
- promises.push(client.listPostReports(postReportsForm));
- data.privateMessageReportsResponse = client.listPrivateMessageReports(
- privateMessageReportsForm
- if (amAdmin()) {
- const privateMessageReportsForm: ListPrivateMessageReports = {
- unresolved_only,
- page,
- limit,
- auth,
- };
- promises.push(
- client.listPrivateMessageReports(privateMessageReportsForm)
- );
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
- }
- } else {
- promises.push(
- Promise.resolve({ state: "empty" }),
- Promise.resolve({ state: "empty" }),
- Promise.resolve({ state: "empty" })
-- );
++ data.privateMessageReportsResponse =
++ await client.listPrivateMessageReports(privateMessageReportsForm);
}
- return promises;
+ return data;
}
- refetch() {
- const unresolved_only = this.state.unreadOrAll === UnreadOrAll.Unread;
+ async refetch() {
+ const unresolved_only = this.state.unreadOrAll == UnreadOrAll.Unread;
const page = this.state.page;
const limit = fetchLimit;
- const auth = myAuth();
+ const auth = myAuthRequired();
+
+ this.setState({
+ commentReportsRes: { state: "loading" },
+ postReportsRes: { state: "loading" },
+ messageReportsRes: { state: "loading" },
+ });
+
+ const form:
+ | ListCommentReports
+ | ListPostReports
+ | ListPrivateMessageReports = {
+ unresolved_only,
+ page,
+ limit,
+ auth,
+ };
- if (auth) {
- const commentReportsForm: ListCommentReports = {
- unresolved_only,
- page,
- limit,
- auth,
- };
+ this.setState({
+ commentReportsRes: await HttpService.client.listCommentReports(form),
+ postReportsRes: await HttpService.client.listPostReports(form),
+ });
- WebSocketService.Instance.send(
- wsClient.listCommentReports(commentReportsForm)
- );
+ if (amAdmin()) {
+ this.setState({
+ messageReportsRes: await HttpService.client.listPrivateMessageReports(
+ form
+ ),
+ });
+ }
+ }
- const postReportsForm: ListPostReports = {
- unresolved_only,
- page,
- limit,
- auth,
- };
+ async handleResolveCommentReport(form: ResolveCommentReport) {
+ const res = await HttpService.client.resolveCommentReport(form);
+ this.findAndUpdateCommentReport(res);
+ }
- WebSocketService.Instance.send(wsClient.listPostReports(postReportsForm));
+ async handleResolvePostReport(form: ResolvePostReport) {
+ const res = await HttpService.client.resolvePostReport(form);
+ this.findAndUpdatePostReport(res);
+ }
- if (amAdmin()) {
- const privateMessageReportsForm: ListPrivateMessageReports = {
- unresolved_only,
- page,
- limit,
- auth,
- };
- WebSocketService.Instance.send(
- wsClient.listPrivateMessageReports(privateMessageReportsForm)
+ async handleResolvePrivateMessageReport(form: ResolvePrivateMessageReport) {
+ const res = await HttpService.client.resolvePrivateMessageReport(form);
+ this.findAndUpdatePrivateMessageReport(res);
+ }
+
+ findAndUpdateCommentReport(res: RequestState<CommentReportResponse>) {
+ this.setState(s => {
+ if (s.commentReportsRes.state == "success" && res.state == "success") {
+ s.commentReportsRes.data.comment_reports = editCommentReport(
+ res.data.comment_report_view,
+ s.commentReportsRes.data.comment_reports
);
}
- }
+ return s;
+ });
}
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- return;
- } else if (msg.reconnect) {
- this.refetch();
- } else if (op == UserOperation.ListCommentReports) {
- let data = wsJsonToRes<ListCommentReportsResponse>(msg);
- this.setState({ listCommentReportsResponse: data });
- this.setState({ combined: this.buildCombined(), loading: false });
- // this.sendUnreadCount();
- window.scrollTo(0, 0);
- setupTippy();
- } else if (op == UserOperation.ListPostReports) {
- let data = wsJsonToRes<ListPostReportsResponse>(msg);
- this.setState({ listPostReportsResponse: data });
- this.setState({ combined: this.buildCombined(), loading: false });
- // this.sendUnreadCount();
- window.scrollTo(0, 0);
- setupTippy();
- } else if (op == UserOperation.ListPrivateMessageReports) {
- let data = wsJsonToRes<ListPrivateMessageReportsResponse>(msg);
- this.setState({ listPrivateMessageReportsResponse: data });
- this.setState({ combined: this.buildCombined(), loading: false });
- // this.sendUnreadCount();
- window.scrollTo(0, 0);
- setupTippy();
- } else if (op == UserOperation.ResolvePostReport) {
- let data = wsJsonToRes<PostReportResponse>(msg);
- updatePostReportRes(
- data.post_report_view,
- this.state.listPostReportsResponse?.post_reports
- );
- let urcs = UserService.Instance.unreadReportCountSub;
- if (data.post_report_view.post_report.resolved) {
- urcs.next(urcs.getValue() - 1);
- } else {
- urcs.next(urcs.getValue() + 1);
- }
- this.setState(this.state);
- } else if (op == UserOperation.ResolveCommentReport) {
- let data = wsJsonToRes<CommentReportResponse>(msg);
- updateCommentReportRes(
- data.comment_report_view,
- this.state.listCommentReportsResponse?.comment_reports
- );
- let urcs = UserService.Instance.unreadReportCountSub;
- if (data.comment_report_view.comment_report.resolved) {
- urcs.next(urcs.getValue() - 1);
- } else {
- urcs.next(urcs.getValue() + 1);
+ findAndUpdatePostReport(res: RequestState<PostReportResponse>) {
+ this.setState(s => {
+ if (s.postReportsRes.state == "success" && res.state == "success") {
+ s.postReportsRes.data.post_reports = editPostReport(
+ res.data.post_report_view,
+ s.postReportsRes.data.post_reports
+ );
}
- this.setState(this.state);
- } else if (op == UserOperation.ResolvePrivateMessageReport) {
- let data = wsJsonToRes<PrivateMessageReportResponse>(msg);
- updatePrivateMessageReportRes(
- data.private_message_report_view,
- this.state.listPrivateMessageReportsResponse?.private_message_reports
- );
- let urcs = UserService.Instance.unreadReportCountSub;
- if (data.private_message_report_view.private_message_report.resolved) {
- urcs.next(urcs.getValue() - 1);
- } else {
- urcs.next(urcs.getValue() + 1);
+ return s;
+ });
+ }
+
+ findAndUpdatePrivateMessageReport(
+ res: RequestState<PrivateMessageReportResponse>
+ ) {
+ this.setState(s => {
+ if (s.messageReportsRes.state == "success" && res.state == "success") {
+ s.messageReportsRes.data.private_message_reports =
+ editPrivateMessageReport(
+ res.data.private_message_report_view,
+ s.messageReportsRes.data.private_message_reports
+ );
}
- this.setState(this.state);
- }
+ return s;
+ });
}
}
import { Component } from "inferno";
import { RouteComponentProps } from "inferno-router/dist/Route";
import {
+ CreatePost as CreatePostI,
GetCommunity,
+ GetCommunityResponse,
GetSiteResponse,
- PostView,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
+ ListCommunitiesResponse,
} from "lemmy-js-client";
- import { Subscription } from "rxjs";
- import { InitialFetchRequest, PostFormParams } from "shared/interfaces";
import { i18n } from "../../i18next";
- import { WebSocketService } from "../../services";
+ import { InitialFetchRequest, PostFormParams } from "../../interfaces";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import {
+ HttpService,
+ RequestState,
+ WrappedLemmyHttp,
+ } from "../../services/HttpService";
import {
Choice,
QueryParams,
- WithPromiseKeys,
++ RouteDataResponse,
enableDownvotes,
enableNsfw,
getIdFromString,
getQueryParams,
- isBrowser,
myAuth,
setIsoData,
- toast,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
communityId?: number;
}
- interface CreatePostData {
++type CreatePostData = RouteDataResponse<{
+ communityResponse?: GetCommunityResponse;
- }
++ initialCommunitiesRes: ListCommunitiesResponse;
++}>;
+
function getCreatePostQueryParams() {
return getQueryParams<CreatePostProps>({
communityId: getIdFromString,
});
}
+ function fetchCommunitiesForOptions(client: WrappedLemmyHttp) {
+ return client.listCommunities({ limit: 30, sort: "TopMonth", type_: "All" });
+ }
+
interface CreatePostState {
siteRes: GetSiteResponse;
loading: boolean;
selectedCommunityChoice?: Choice;
+ initialCommunitiesRes: RequestState<ListCommunitiesResponse>;
+ isIsomorphic: boolean;
}
export class CreatePost extends Component<
RouteComponentProps<Record<string, never>>,
CreatePostState
> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<CreatePostData>(this.context);
- private subscription?: Subscription;
state: CreatePostState = {
siteRes: this.isoData.site_res,
loading: true,
+ initialCommunitiesRes: { state: "empty" },
+ isIsomorphic: false,
};
constructor(props: RouteComponentProps<Record<string, never>>, context: any) {
this.handleSelectedCommunityChange =
this.handleSelectedCommunityChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
-
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
- const { communityResponse } = this.isoData.routeData;
+ if (FirstLoadService.isFirstLoad) {
- const [communityRes, listCommunitiesRes] = this.isoData.routeData;
++ const { communityResponse: communityRes, initialCommunitiesRes } =
++ this.isoData.routeData;
- if (communityResponse) {
+ if (communityRes?.state === "success") {
const communityChoice: Choice = {
- label: communityResponse.community_view.community.title,
- value: communityResponse.community_view.community.id.toString(),
+ label: communityRes.data.community_view.community.title,
+ value: communityRes.data.community_view.community.id.toString(),
};
this.state = {
this.state = {
...this.state,
loading: false,
- initialCommunitiesRes: listCommunitiesRes,
++ initialCommunitiesRes,
+ isIsomorphic: true,
};
- } else {
- this.fetchCommunity();
}
}
- fetchCommunity() {
+ async fetchCommunity() {
const { communityId } = getCreatePostQueryParams();
- const auth = myAuth(false);
+ const auth = myAuth();
if (communityId) {
- const form: GetCommunity = {
+ const res = await HttpService.client.getCommunity({
id: communityId,
auth,
- };
-
- WebSocketService.Instance.send(wsClient.getCommunity(form));
+ });
+ if (res.state === "success") {
+ this.setState({
+ selectedCommunityChoice: {
+ label: res.data.community_view.community.name,
+ value: res.data.community_view.community.id.toString(),
+ },
+ loading: false,
+ });
+ }
}
}
- componentDidMount(): void {
- const { communityId } = getCreatePostQueryParams();
+ async componentDidMount() {
+ // TODO test this
+ if (!this.state.isIsomorphic) {
+ const { communityId } = getCreatePostQueryParams();
+
+ const initialCommunitiesRes = await fetchCommunitiesForOptions(
+ HttpService.client
+ );
- if (communityId?.toString() !== this.state.selectedCommunityChoice?.value) {
- this.fetchCommunity();
- } else if (!communityId) {
this.setState({
- selectedCommunityChoice: undefined,
- loading: false,
+ initialCommunitiesRes,
});
- }
- }
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
+ if (
+ communityId?.toString() !== this.state.selectedCommunityChoice?.value
+ ) {
+ await this.fetchCommunity();
+ } else if (!communityId) {
+ this.setState({
+ selectedCommunityChoice: undefined,
+ loading: false,
+ });
+ }
}
}
siteLanguages={this.state.siteRes.discussion_languages}
selectedCommunityChoice={selectedCommunityChoice}
onSelectCommunity={this.handleSelectedCommunityChange}
+ initialCommunities={
+ this.state.initialCommunitiesRes.state === "success"
+ ? this.state.initialCommunitiesRes.data.communities
+ : []
+ }
/>
</div>
</div>
);
}
- updateUrl({ communityId }: Partial<CreatePostProps>) {
+ async updateUrl({ communityId }: Partial<CreatePostProps>) {
const { communityId: urlCommunityId } = getCreatePostQueryParams();
const locationState = this.props.history.location.state as
history.replaceState(locationState, "", url);
- this.fetchCommunity();
+ await this.fetchCommunity();
}
handleSelectedCommunityChange(choice: Choice) {
});
}
- handlePostCreate(post_view: PostView) {
- this.props.history.replace(`/post/${post_view.post.id}`);
+ async handlePostCreate(form: CreatePostI) {
+ const res = await HttpService.client.createPost(form);
+
+ if (res.state === "success") {
+ const postId = res.data.post_view.post.id;
+ this.props.history.replace(`/post/${postId}`);
+ }
}
-- static fetchInitialData({
++ static async fetchInitialData({
client,
query: { communityId },
auth,
- }: InitialFetchRequest<QueryParams<CreatePostProps>>): Promise<
- RequestState<any>
- >[] {
- const promises: Promise<RequestState<any>>[] = [];
+ }: InitialFetchRequest<
+ QueryParams<CreatePostProps>
- >): WithPromiseKeys<CreatePostData> {
- const data: WithPromiseKeys<CreatePostData> = {};
++ >): Promise<CreatePostData> {
++ const data: CreatePostData = {
++ initialCommunitiesRes: await fetchCommunitiesForOptions(client),
++ };
if (communityId) {
const form: GetCommunity = {
id: getIdFromString(communityId),
};
- data.communityResponse = client.getCommunity(form);
- promises.push(client.getCommunity(form));
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
++ data.communityResponse = await client.getCommunity(form);
}
- promises.push(fetchCommunitiesForOptions(client));
-
- return promises;
+ return data;
}
-
- parseMessage(msg: any) {
- const op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- return;
- }
-
- if (op === UserOperation.GetCommunity) {
- const {
- community_view: {
- community: { title, id },
- },
- } = wsJsonToRes<GetCommunityResponse>(msg);
-
- this.setState({
- selectedCommunityChoice: { label: title, value: id.toString() },
- loading: false,
- });
- }
- }
}
import autosize from "autosize";
import { Component, createRef, linkEvent, RefObject } from "inferno";
import {
- AddAdminResponse,
+ AddAdmin,
+ AddModToCommunity,
AddModToCommunityResponse,
+ BanFromCommunity,
BanFromCommunityResponse,
+ BanPerson,
BanPersonResponse,
- BlockPersonResponse,
- CommentReportResponse,
+ BlockCommunity,
+ BlockPerson,
+ CommentId,
+ CommentReplyResponse,
CommentResponse,
CommentSortType,
CommunityResponse,
+ CreateComment,
+ CreateCommentLike,
+ CreateCommentReport,
+ CreatePostLike,
+ CreatePostReport,
+ DeleteComment,
+ DeleteCommunity,
+ DeletePost,
+ DistinguishComment,
+ EditComment,
+ EditCommunity,
+ EditPost,
+ FeaturePost,
+ FollowCommunity,
GetComments,
GetCommentsResponse,
GetCommunityResponse,
GetPost,
GetPostResponse,
GetSiteResponse,
- PostReportResponse,
+ LockPost,
+ MarkCommentReplyAsRead,
+ MarkPersonMentionAsRead,
PostResponse,
- PostView,
+ PurgeComment,
+ PurgeCommunity,
PurgeItemResponse,
- Search,
- SearchResponse,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
+ PurgePerson,
+ PurgePost,
+ RemoveComment,
+ RemoveCommunity,
+ RemovePost,
+ SaveComment,
+ SavePost,
+ TransferCommunity,
} from "lemmy-js-client";
- import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import {
CommentNodeI,
CommentViewType,
InitialFetchRequest,
} from "../../interfaces";
- import { UserService, WebSocketService } from "../../services";
+ import { UserService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
import {
buildCommentsTree,
commentsToFlatNodes,
commentTreeMaxDepth,
- createCommentLikeRes,
- createPostLikeRes,
debounce,
- editCommentRes,
+ editComment,
+ editWith,
enableDownvotes,
enableNsfw,
getCommentIdFromProps,
getCommentParentId,
getDepthFromComment,
getIdFromProps,
- insertCommentIntoTree,
isBrowser,
isImage,
myAuth,
restoreScrollPosition,
- saveCommentRes,
++ RouteDataResponse,
saveScrollPosition,
setIsoData,
setupTippy,
toast,
- trendingFetchLimit,
+ updateCommunityBlock,
updatePersonBlock,
- WithPromiseKeys,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { CommentForm } from "../comment/comment-form";
import { CommentNodes } from "../comment/comment-nodes";
const commentsShownInterval = 15;
- interface PostData {
- postResponse: GetPostResponse;
- commentsResponse: GetCommentsResponse;
- }
++type PostData = RouteDataResponse<{
++ postRes: GetPostResponse;
++ commentsRes: GetCommentsResponse;
++}>;
+
interface PostState {
postId?: number;
commentId?: number;
- postRes?: GetPostResponse;
- commentsRes?: GetCommentsResponse;
- commentTree: CommentNodeI[];
+ postRes: RequestState<GetPostResponse>;
+ commentsRes: RequestState<GetCommentsResponse>;
commentSort: CommentSortType;
commentViewType: CommentViewType;
scrolled?: boolean;
- loading: boolean;
- crossPosts?: PostView[];
siteRes: GetSiteResponse;
commentSectionRef?: RefObject<HTMLDivElement>;
showSidebarMobile: boolean;
maxCommentsShown: number;
+ finished: Map<CommentId, boolean | undefined>;
+ isIsomorphic: boolean;
}
export class Post extends Component<any, PostState> {
- private subscription?: Subscription;
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<PostData>(this.context);
private commentScrollDebounced: () => void;
state: PostState = {
+ postRes: { state: "empty" },
+ commentsRes: { state: "empty" },
postId: getIdFromProps(this.props),
commentId: getCommentIdFromProps(this.props),
- commentTree: [],
commentSort: "Hot",
commentViewType: CommentViewType.Tree,
scrolled: false,
- loading: true,
siteRes: this.isoData.site_res,
showSidebarMobile: false,
maxCommentsShown: commentsShownInterval,
+ finished: new Map(),
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
super(props, context);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ this.handleDeleteCommunityClick =
+ this.handleDeleteCommunityClick.bind(this);
+ this.handleEditCommunity = this.handleEditCommunity.bind(this);
+ this.handleFollow = this.handleFollow.bind(this);
+ this.handleModRemoveCommunity = this.handleModRemoveCommunity.bind(this);
+ this.handleCreateComment = this.handleCreateComment.bind(this);
+ this.handleEditComment = this.handleEditComment.bind(this);
+ this.handleSaveComment = this.handleSaveComment.bind(this);
+ this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
+ this.handleBlockPerson = this.handleBlockPerson.bind(this);
+ this.handleDeleteComment = this.handleDeleteComment.bind(this);
+ this.handleRemoveComment = this.handleRemoveComment.bind(this);
+ this.handleCommentVote = this.handleCommentVote.bind(this);
+ this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+ this.handleAddAdmin = this.handleAddAdmin.bind(this);
+ this.handlePurgePerson = this.handlePurgePerson.bind(this);
+ this.handlePurgeComment = this.handlePurgeComment.bind(this);
+ this.handleCommentReport = this.handleCommentReport.bind(this);
+ this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+ this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+ this.handleFetchChildren = this.handleFetchChildren.bind(this);
+ this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+ this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+ this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+ this.handleBanPerson = this.handleBanPerson.bind(this);
+ this.handlePostEdit = this.handlePostEdit.bind(this);
+ this.handlePostVote = this.handlePostVote.bind(this);
+ this.handlePostReport = this.handlePostReport.bind(this);
+ this.handleLockPost = this.handleLockPost.bind(this);
+ this.handleDeletePost = this.handleDeletePost.bind(this);
+ this.handleRemovePost = this.handleRemovePost.bind(this);
+ this.handleSavePost = this.handleSavePost.bind(this);
+ this.handlePurgePost = this.handlePurgePost.bind(this);
+ this.handleFeaturePost = this.handleFeaturePost.bind(this);
this.state = { ...this.state, commentSectionRef: createRef() };
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
- const { commentsResponse, postResponse } = this.isoData.routeData;
+ if (FirstLoadService.isFirstLoad) {
- const [postRes, commentsRes] = this.isoData.routeData;
++ const { commentsRes, postRes } = this.isoData.routeData;
this.state = {
...this.state,
- postRes: postResponse,
- commentsRes: commentsResponse,
+ postRes,
+ commentsRes,
+ isIsomorphic: true,
};
- if (this.state.commentsRes) {
- this.state = {
- ...this.state,
- commentTree: buildCommentsTree(
- this.state.commentsRes.comments,
- !!this.state.commentId
- ),
- };
- }
-
- this.state = { ...this.state, loading: false };
-
if (isBrowser()) {
- if (this.state.postRes) {
- WebSocketService.Instance.send(
- wsClient.communityJoin({
- community_id: this.state.postRes.community_view.community.id,
- })
- );
- }
-
- if (this.state.postId) {
- WebSocketService.Instance.send(
- wsClient.postJoin({ post_id: this.state.postId })
- );
- }
-
- this.fetchCrossPosts();
-
if (this.checkScrollIntoCommentsParam) {
this.scrollIntoCommentSection();
}
}
- } else {
- this.fetchPost();
}
}
- fetchPost() {
- let auth = myAuth(false);
- let postForm: GetPost = {
- id: this.state.postId,
- comment_id: this.state.commentId,
- auth,
- };
- WebSocketService.Instance.send(wsClient.getPost(postForm));
+ async fetchPost() {
+ this.setState({
+ postRes: { state: "loading" },
+ commentsRes: { state: "loading" },
+ });
- let commentsForm: GetComments = {
- post_id: this.state.postId,
- parent_id: this.state.commentId,
- max_depth: commentTreeMaxDepth,
- sort: this.state.commentSort,
- type_: "All",
- saved_only: false,
- auth,
- };
- WebSocketService.Instance.send(wsClient.getComments(commentsForm));
- }
-
- fetchCrossPosts() {
- let q = this.state.postRes?.post_view.post.url;
- if (q) {
- let form: Search = {
- q,
- type_: "Url",
- sort: "TopAll",
- listing_type: "All",
- page: 1,
- limit: trendingFetchLimit,
- auth: myAuth(false),
- };
- WebSocketService.Instance.send(wsClient.search(form));
+ const auth = myAuth();
+
+ this.setState({
+ postRes: await HttpService.client.getPost({
+ id: this.state.postId,
+ comment_id: this.state.commentId,
+ auth,
+ }),
+ commentsRes: await HttpService.client.getComments({
+ post_id: this.state.postId,
+ parent_id: this.state.commentId,
+ max_depth: commentTreeMaxDepth,
+ sort: this.state.commentSort,
+ type_: "All",
+ saved_only: false,
+ auth,
+ }),
+ });
+
+ setupTippy();
+
+ if (!this.state.commentId) restoreScrollPosition(this.context);
+
+ if (this.checkScrollIntoCommentsParam) {
+ this.scrollIntoCommentSection();
}
}
- static fetchInitialData(req: InitialFetchRequest): WithPromiseKeys<PostData> {
- const pathSplit = req.path.split("/");
- static fetchInitialData({
- auth,
++ static async fetchInitialData({
+ client,
+ path,
- }: InitialFetchRequest): Promise<any>[] {
++ auth,
++ }: InitialFetchRequest): Promise<PostData> {
+ const pathSplit = path.split("/");
- const promises: Promise<RequestState<any>>[] = [];
const pathType = pathSplit.at(1);
const id = pathSplit.at(2) ? Number(pathSplit.at(2)) : undefined;
- const auth = req.auth;
const postForm: GetPost = {
auth,
commentsForm.parent_id = id;
}
- promises.push(client.getPost(postForm));
- promises.push(client.getComments(commentsForm));
-
- return promises;
+ return {
- postResponse: req.client.getPost(postForm),
- commentsResponse: req.client.getComments(commentsForm),
++ postRes: await client.getPost(postForm),
++ commentsRes: await client.getComments(commentsForm),
+ };
}
componentWillUnmount() {
- this.subscription?.unsubscribe();
document.removeEventListener("scroll", this.commentScrollDebounced);
saveScrollPosition(this.context);
}
- componentDidMount() {
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.fetchPost();
+ }
+
autosize(document.querySelectorAll("textarea"));
this.commentScrollDebounced = debounce(this.trackCommentsBoxScrolling, 100);
document.addEventListener("scroll", this.commentScrollDebounced);
}
- componentDidUpdate(_lastProps: any) {
+ async componentDidUpdate(_lastProps: any) {
// Necessary if you are on a post and you click another post (same route)
if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
- // TODO Couldnt get a refresh working. This does for now.
- location.reload();
-
- // let currentId = this.props.match.params.id;
- // WebSocketService.Instance.getPost(currentId);
- // this.context.refresh();
- // this.context.router.history.push(_lastProps.location.pathname);
+ await this.fetchPost();
}
}
trackCommentsBoxScrolling = () => {
const wrappedElement = document.getElementsByClassName("comments")[0];
if (wrappedElement && this.isBottom(wrappedElement)) {
- this.setState({
- maxCommentsShown: this.state.maxCommentsShown + commentsShownInterval,
- });
+ const commentCount =
+ this.state.commentsRes.state == "success"
+ ? this.state.commentsRes.data.comments.length
+ : 0;
+
+ if (this.state.maxCommentsShown < commentCount) {
+ this.setState({
+ maxCommentsShown: this.state.maxCommentsShown + commentsShownInterval,
+ });
+ }
}
};
get documentTitle(): string {
- let name_ = this.state.postRes?.post_view.post.name;
- let siteName = this.state.siteRes.site_view.site.name;
- return name_ ? `${name_} - ${siteName}` : "";
+ const siteName = this.state.siteRes.site_view.site.name;
+ return this.state.postRes.state == "success"
+ ? `${this.state.postRes.data.post_view.post.name} - ${siteName}`
+ : siteName;
}
get imageTag(): string | undefined {
- let post = this.state.postRes?.post_view.post;
- let thumbnail = post?.thumbnail_url;
- let url = post?.url;
- return thumbnail || (url && isImage(url) ? url : undefined);
+ if (this.state.postRes.state == "success") {
+ const post = this.state.postRes.data.post_view.post;
+ const thumbnail = post.thumbnail_url;
+ const url = post.url;
+ return thumbnail || (url && isImage(url) ? url : undefined);
+ } else return undefined;
}
- render() {
- let res = this.state.postRes;
- let description = res?.post_view.post.body;
- return (
- <div className="container-lg">
- {this.state.loading ? (
+ renderPostRes() {
+ switch (this.state.postRes.state) {
+ case "loading":
+ return (
<h5>
<Spinner large />
</h5>
- ) : (
- res && (
- <div className="row">
- <div className="col-12 col-md-8 mb-3">
- <HtmlTags
- title={this.documentTitle}
- path={this.context.router.route.match.url}
- image={this.imageTag}
- description={description}
- />
- <PostListing
- post_view={res.post_view}
- duplicates={this.state.crossPosts}
- showBody
- showCommunity
- moderators={res.moderators}
- admins={this.state.siteRes.admins}
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- enableNsfw={enableNsfw(this.state.siteRes)}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
- />
- <div ref={this.state.commentSectionRef} className="mb-2" />
- <CommentForm
- node={res.post_view.post.id}
- disabled={res.post_view.post.locked}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
- />
- <div className="d-block d-md-none">
- <button
- className="btn btn-secondary d-inline-block mb-2 mr-3"
- onClick={linkEvent(this, this.handleShowSidebarMobile)}
- >
- {i18n.t("sidebar")}{" "}
- <Icon
- icon={
- this.state.showSidebarMobile
- ? `minus-square`
- : `plus-square`
- }
- classes="icon-inline"
- />
- </button>
- {this.state.showSidebarMobile && this.sidebar()}
- </div>
- {this.sortRadios()}
- {this.state.commentViewType == CommentViewType.Tree &&
- this.commentsTree()}
- {this.state.commentViewType == CommentViewType.Flat &&
- this.commentsFlat()}
+ );
+ case "success": {
+ const res = this.state.postRes.data;
+ return (
+ <div className="row">
+ <div className="col-12 col-md-8 mb-3">
+ <HtmlTags
+ title={this.documentTitle}
+ path={this.context.router.route.match.url}
+ image={this.imageTag}
+ description={res.post_view.post.body}
+ />
+ <PostListing
+ post_view={res.post_view}
+ crossPosts={res.cross_posts}
+ showBody
+ showCommunity
+ moderators={res.moderators}
+ admins={this.state.siteRes.admins}
+ enableDownvotes={enableDownvotes(this.state.siteRes)}
+ enableNsfw={enableNsfw(this.state.siteRes)}
+ allLanguages={this.state.siteRes.all_languages}
+ siteLanguages={this.state.siteRes.discussion_languages}
+ onBlockPerson={this.handleBlockPerson}
+ onPostEdit={this.handlePostEdit}
+ onPostVote={this.handlePostVote}
+ onPostReport={this.handlePostReport}
+ onLockPost={this.handleLockPost}
+ onDeletePost={this.handleDeletePost}
+ onRemovePost={this.handleRemovePost}
+ onSavePost={this.handleSavePost}
+ onPurgePerson={this.handlePurgePerson}
+ onPurgePost={this.handlePurgePost}
+ onBanPerson={this.handleBanPerson}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onFeaturePost={this.handleFeaturePost}
+ />
+ <div ref={this.state.commentSectionRef} className="mb-2" />
+ <CommentForm
+ node={res.post_view.post.id}
+ disabled={res.post_view.post.locked}
+ allLanguages={this.state.siteRes.all_languages}
+ siteLanguages={this.state.siteRes.discussion_languages}
+ onUpsertComment={this.handleCreateComment}
+ finished={this.state.finished.get(0)}
+ />
+ <div className="d-block d-md-none">
+ <button
+ className="btn btn-secondary d-inline-block mb-2 mr-3"
+ onClick={linkEvent(this, this.handleShowSidebarMobile)}
+ >
+ {i18n.t("sidebar")}{" "}
+ <Icon
+ icon={
+ this.state.showSidebarMobile
+ ? `minus-square`
+ : `plus-square`
+ }
+ classes="icon-inline"
+ />
+ </button>
+ {this.state.showSidebarMobile && this.sidebar()}
</div>
- <div className="d-none d-md-block col-md-4">{this.sidebar()}</div>
+ {this.sortRadios()}
+ {this.state.commentViewType == CommentViewType.Tree &&
+ this.commentsTree()}
+ {this.state.commentViewType == CommentViewType.Flat &&
+ this.commentsFlat()}
</div>
- )
- )}
- </div>
- );
+ <div className="d-none d-md-block col-md-4">{this.sidebar()}</div>
+ </div>
+ );
+ }
+ }
+ }
+
+ render() {
+ return <div className="container-lg">{this.renderPostRes()}</div>;
}
sortRadios() {
commentsFlat() {
// These are already sorted by new
- let commentsRes = this.state.commentsRes;
- let postRes = this.state.postRes;
- return (
- commentsRes &&
- postRes && (
+ const commentsRes = this.state.commentsRes;
+ const postRes = this.state.postRes;
+
+ if (commentsRes.state == "success" && postRes.state == "success") {
+ return (
<div>
<CommentNodes
- nodes={commentsToFlatNodes(commentsRes.comments)}
+ nodes={commentsToFlatNodes(commentsRes.data.comments)}
viewType={this.state.commentViewType}
maxCommentsShown={this.state.maxCommentsShown}
noIndent
- locked={postRes.post_view.post.locked}
- moderators={postRes.moderators}
+ locked={postRes.data.post_view.post.locked}
+ moderators={postRes.data.moderators}
admins={this.state.siteRes.admins}
enableDownvotes={enableDownvotes(this.state.siteRes)}
showContext
+ finished={this.state.finished}
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
+ onSaveComment={this.handleSaveComment}
+ onBlockPerson={this.handleBlockPerson}
+ onDeleteComment={this.handleDeleteComment}
+ onRemoveComment={this.handleRemoveComment}
+ onCommentVote={this.handleCommentVote}
+ onCommentReport={this.handleCommentReport}
+ onDistinguishComment={this.handleDistinguishComment}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onFetchChildren={this.handleFetchChildren}
+ onPurgeComment={this.handlePurgeComment}
+ onPurgePerson={this.handlePurgePerson}
+ onCommentReplyRead={this.handleCommentReplyRead}
+ onPersonMentionRead={this.handlePersonMentionRead}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onBanPerson={this.handleBanPerson}
+ onCreateComment={this.handleCreateComment}
+ onEditComment={this.handleEditComment}
/>
</div>
- )
- );
+ );
+ }
}
sidebar() {
- let res = this.state.postRes;
- return (
- res && (
+ const res = this.state.postRes;
+ if (res.state === "success") {
+ return (
<div className="mb-3">
<Sidebar
- community_view={res.community_view}
- moderators={res.moderators}
+ community_view={res.data.community_view}
+ moderators={res.data.moderators}
admins={this.state.siteRes.admins}
- online={res.online}
+ online={res.data.online}
enableNsfw={enableNsfw(this.state.siteRes)}
showIcon
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
+ onDeleteCommunity={this.handleDeleteCommunityClick}
+ onLeaveModTeam={this.handleAddModToCommunity}
+ onFollowCommunity={this.handleFollow}
+ onRemoveCommunity={this.handleModRemoveCommunity}
+ onPurgeCommunity={this.handlePurgeCommunity}
+ onBlockCommunity={this.handleBlockCommunity}
+ onEditCommunity={this.handleEditCommunity}
/>
</div>
- )
- );
- }
-
- handleCommentSortChange(i: Post, event: any) {
- i.setState({
- commentSort: event.target.value as CommentSortType,
- commentViewType: CommentViewType.Tree,
- commentsRes: undefined,
- postRes: undefined,
- });
- i.fetchPost();
- }
-
- handleCommentViewTypeChange(i: Post, event: any) {
- let comments = i.state.commentsRes?.comments;
- if (comments) {
- i.setState({
- commentViewType: Number(event.target.value),
- commentSort: "New",
- commentTree: buildCommentsTree(comments, !!i.state.commentId),
- });
- }
- }
-
- handleShowSidebarMobile(i: Post) {
- i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
- }
-
- handleViewPost(i: Post) {
- let id = i.state.postRes?.post_view.post.id;
- if (id) {
- i.context.router.history.push(`/post/${id}`);
- }
- }
-
- handleViewContext(i: Post) {
- let parentId = getCommentParentId(
- i.state.commentsRes?.comments?.at(0)?.comment
- );
- if (parentId) {
- i.context.router.history.push(`/comment/${parentId}`);
+ );
}
}
commentsTree() {
- let res = this.state.postRes;
- let firstComment = this.state.commentTree.at(0)?.comment_view.comment;
- let depth = getDepthFromComment(firstComment);
- let showContextButton = depth ? depth > 0 : false;
+ const res = this.state.postRes;
+ const firstComment = this.commentTree().at(0)?.comment_view.comment;
+ const depth = getDepthFromComment(firstComment);
+ const showContextButton = depth ? depth > 0 : false;
return (
- res && (
+ res.state == "success" && (
<div>
{!!this.state.commentId && (
<>
</>
)}
<CommentNodes
- nodes={this.state.commentTree}
+ nodes={this.commentTree()}
viewType={this.state.commentViewType}
maxCommentsShown={this.state.maxCommentsShown}
- locked={res.post_view.post.locked}
- moderators={res.moderators}
+ locked={res.data.post_view.post.locked}
+ moderators={res.data.moderators}
admins={this.state.siteRes.admins}
enableDownvotes={enableDownvotes(this.state.siteRes)}
+ finished={this.state.finished}
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
+ onSaveComment={this.handleSaveComment}
+ onBlockPerson={this.handleBlockPerson}
+ onDeleteComment={this.handleDeleteComment}
+ onRemoveComment={this.handleRemoveComment}
+ onCommentVote={this.handleCommentVote}
+ onCommentReport={this.handleCommentReport}
+ onDistinguishComment={this.handleDistinguishComment}
+ onAddModToCommunity={this.handleAddModToCommunity}
+ onAddAdmin={this.handleAddAdmin}
+ onTransferCommunity={this.handleTransferCommunity}
+ onFetchChildren={this.handleFetchChildren}
+ onPurgeComment={this.handlePurgeComment}
+ onPurgePerson={this.handlePurgePerson}
+ onCommentReplyRead={this.handleCommentReplyRead}
+ onPersonMentionRead={this.handlePersonMentionRead}
+ onBanPersonFromCommunity={this.handleBanFromCommunity}
+ onBanPerson={this.handleBanPerson}
+ onCreateComment={this.handleCreateComment}
+ onEditComment={this.handleEditComment}
/>
</div>
)
);
}
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- return;
- } else if (msg.reconnect) {
- let post_id = this.state.postRes?.post_view.post.id;
- if (post_id) {
- WebSocketService.Instance.send(wsClient.postJoin({ post_id }));
- WebSocketService.Instance.send(
- wsClient.getPost({
- id: post_id,
- auth: myAuth(false),
- })
- );
- }
- } else if (op == UserOperation.GetPost) {
- let data = wsJsonToRes<GetPostResponse>(msg);
- this.setState({ postRes: data });
-
- // join the rooms
- WebSocketService.Instance.send(
- wsClient.postJoin({ post_id: data.post_view.post.id })
- );
- WebSocketService.Instance.send(
- wsClient.communityJoin({
- community_id: data.community_view.community.id,
- })
+ commentTree(): CommentNodeI[] {
+ if (this.state.commentsRes.state == "success") {
+ return buildCommentsTree(
+ this.state.commentsRes.data.comments,
+ !!this.state.commentId
);
+ } else {
+ return [];
+ }
+ }
+
+ async handleCommentSortChange(i: Post, event: any) {
+ i.setState({
+ commentSort: event.target.value as CommentSortType,
+ commentViewType: CommentViewType.Tree,
+ commentsRes: { state: "loading" },
+ postRes: { state: "loading" },
+ });
+ await i.fetchPost();
+ }
+
+ handleCommentViewTypeChange(i: Post, event: any) {
+ i.setState({
+ commentViewType: Number(event.target.value),
+ commentSort: "New",
+ });
+ }
- // Get cross-posts
- // TODO move this into initial fetch and refetch
- this.fetchCrossPosts();
- setupTippy();
- if (!this.state.commentId) restoreScrollPosition(this.context);
+ handleShowSidebarMobile(i: Post) {
+ i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
+ }
+
+ handleViewPost(i: Post) {
+ if (i.state.postRes.state == "success") {
+ const id = i.state.postRes.data.post_view.post.id;
+ i.context.router.history.push(`/post/${id}`);
+ }
+ }
- if (this.checkScrollIntoCommentsParam) {
- this.scrollIntoCommentSection();
+ handleViewContext(i: Post) {
+ if (i.state.commentsRes.state == "success") {
+ const parentId = getCommentParentId(
+ i.state.commentsRes.data.comments.at(0)?.comment
+ );
+ if (parentId) {
+ i.context.router.history.push(`/comment/${parentId}`);
}
- } else if (op == UserOperation.GetComments) {
- let data = wsJsonToRes<GetCommentsResponse>(msg);
- // This section sets the comments res
- let comments = this.state.commentsRes?.comments;
- if (comments) {
- // You might need to append here, since this could be building more comments from a tree fetch
- // Remove the first comment, since it is the parent
- let newComments = data.comments;
- newComments.shift();
- comments.push(...newComments);
- } else {
- this.setState({ commentsRes: data });
+ }
+ }
+
+ async handleDeleteCommunityClick(form: DeleteCommunity) {
+ const deleteCommunityRes = await HttpService.client.deleteCommunity(form);
+ this.updateCommunity(deleteCommunityRes);
+ }
+
+ async handleAddModToCommunity(form: AddModToCommunity) {
+ const addModRes = await HttpService.client.addModToCommunity(form);
+ this.updateModerators(addModRes);
+ }
+
+ async handleFollow(form: FollowCommunity) {
+ const followCommunityRes = await HttpService.client.followCommunity(form);
+ this.updateCommunity(followCommunityRes);
+
+ // Update myUserInfo
+ if (followCommunityRes.state === "success") {
+ const communityId = followCommunityRes.data.community_view.community.id;
+ const mui = UserService.Instance.myUserInfo;
+ if (mui) {
+ mui.follows = mui.follows.filter(i => i.community.id != communityId);
}
+ }
+ }
- let cComments = this.state.commentsRes?.comments ?? [];
- this.setState({
- commentTree: buildCommentsTree(cComments, !!this.state.commentId),
- loading: false,
- });
- } else if (op == UserOperation.CreateComment) {
- let data = wsJsonToRes<CommentResponse>(msg);
+ async handlePurgeCommunity(form: PurgeCommunity) {
+ const purgeCommunityRes = await HttpService.client.purgeCommunity(form);
+ this.purgeItem(purgeCommunityRes);
+ }
- // Don't get comments from the post room, if the creator is blocked
- let creatorBlocked = UserService.Instance.myUserInfo?.person_blocks
- .map(pb => pb.target.id)
- .includes(data.comment_view.creator.id);
+ async handlePurgePerson(form: PurgePerson) {
+ const purgePersonRes = await HttpService.client.purgePerson(form);
+ this.purgeItem(purgePersonRes);
+ }
+
+ async handlePurgeComment(form: PurgeComment) {
+ const purgeCommentRes = await HttpService.client.purgeComment(form);
+ this.purgeItem(purgeCommentRes);
+ }
+
+ async handlePurgePost(form: PurgePost) {
+ const purgeRes = await HttpService.client.purgePost(form);
+ this.purgeItem(purgeRes);
+ }
- // Necessary since it might be a user reply, which has the recipients, to avoid double
- let postRes = this.state.postRes;
- let commentsRes = this.state.commentsRes;
+ async handleBlockCommunity(form: BlockCommunity) {
+ const blockCommunityRes = await HttpService.client.blockCommunity(form);
+ // TODO Probably isn't necessary
+ this.setState(s => {
if (
- data.recipient_ids.length == 0 &&
- !creatorBlocked &&
- postRes &&
- data.comment_view.post.id == postRes.post_view.post.id &&
- commentsRes
+ s.postRes.state == "success" &&
+ blockCommunityRes.state == "success"
) {
- commentsRes.comments.unshift(data.comment_view);
- insertCommentIntoTree(
- this.state.commentTree,
- data.comment_view,
- !!this.state.commentId
- );
- postRes.post_view.counts.comments++;
-
- this.setState(this.state);
- setupTippy();
+ s.postRes.data.community_view = blockCommunityRes.data.community_view;
}
- } else if (
- op == UserOperation.EditComment ||
- op == UserOperation.DeleteComment ||
- op == UserOperation.RemoveComment
- ) {
- let data = wsJsonToRes<CommentResponse>(msg);
- editCommentRes(data.comment_view, this.state.commentsRes?.comments);
- this.setState(this.state);
- setupTippy();
- } else if (op == UserOperation.SaveComment) {
- let data = wsJsonToRes<CommentResponse>(msg);
- saveCommentRes(data.comment_view, this.state.commentsRes?.comments);
- this.setState(this.state);
- setupTippy();
- } else if (op == UserOperation.CreateCommentLike) {
- let data = wsJsonToRes<CommentResponse>(msg);
- createCommentLikeRes(data.comment_view, this.state.commentsRes?.comments);
- this.setState(this.state);
- } else if (op == UserOperation.CreatePostLike) {
- let data = wsJsonToRes<PostResponse>(msg);
- createPostLikeRes(data.post_view, this.state.postRes?.post_view);
- this.setState(this.state);
- } else if (
- op == UserOperation.EditPost ||
- op == UserOperation.DeletePost ||
- op == UserOperation.RemovePost ||
- op == UserOperation.LockPost ||
- op == UserOperation.FeaturePost ||
- op == UserOperation.SavePost
- ) {
- let data = wsJsonToRes<PostResponse>(msg);
- let res = this.state.postRes;
- if (res) {
- res.post_view = data.post_view;
- this.setState(this.state);
- setupTippy();
- }
- } else if (
- op == UserOperation.EditCommunity ||
- op == UserOperation.DeleteCommunity ||
- op == UserOperation.RemoveCommunity ||
- op == UserOperation.FollowCommunity
+ return s;
+ });
+
+ if (blockCommunityRes.state == "success") {
+ updateCommunityBlock(blockCommunityRes.data);
+ }
+ }
+
+ async handleBlockPerson(form: BlockPerson) {
+ const blockPersonRes = await HttpService.client.blockPerson(form);
+ if (blockPersonRes.state == "success") {
+ updatePersonBlock(blockPersonRes.data);
+ }
+ }
+
+ async handleModRemoveCommunity(form: RemoveCommunity) {
+ const removeCommunityRes = await HttpService.client.removeCommunity(form);
+ this.updateCommunity(removeCommunityRes);
+ }
+
+ async handleEditCommunity(form: EditCommunity) {
+ const res = await HttpService.client.editCommunity(form);
+ this.updateCommunity(res);
+
+ return res;
+ }
+
+ async handleCreateComment(form: CreateComment) {
+ const createCommentRes = await HttpService.client.createComment(form);
+ this.createAndUpdateComments(createCommentRes);
+
+ return createCommentRes;
+ }
+
+ async handleEditComment(form: EditComment) {
+ const editCommentRes = await HttpService.client.editComment(form);
+ this.findAndUpdateComment(editCommentRes);
+
+ return editCommentRes;
+ }
+
+ async handleDeleteComment(form: DeleteComment) {
+ const deleteCommentRes = await HttpService.client.deleteComment(form);
+ this.findAndUpdateComment(deleteCommentRes);
+ }
+
+ async handleDeletePost(form: DeletePost) {
+ const deleteRes = await HttpService.client.deletePost(form);
+ this.updatePost(deleteRes);
+ }
+
+ async handleRemovePost(form: RemovePost) {
+ const removeRes = await HttpService.client.removePost(form);
+ this.updatePost(removeRes);
+ }
+
+ async handleRemoveComment(form: RemoveComment) {
+ const removeCommentRes = await HttpService.client.removeComment(form);
+ this.findAndUpdateComment(removeCommentRes);
+ }
+
+ async handleSaveComment(form: SaveComment) {
+ const saveCommentRes = await HttpService.client.saveComment(form);
+ this.findAndUpdateComment(saveCommentRes);
+ }
+
+ async handleSavePost(form: SavePost) {
+ const saveRes = await HttpService.client.savePost(form);
+ this.updatePost(saveRes);
+ }
+
+ async handleFeaturePost(form: FeaturePost) {
+ const featureRes = await HttpService.client.featurePost(form);
+ this.updatePost(featureRes);
+ }
+
+ async handleCommentVote(form: CreateCommentLike) {
+ const voteRes = await HttpService.client.likeComment(form);
+ this.findAndUpdateComment(voteRes);
+ }
+
+ async handlePostVote(form: CreatePostLike) {
+ const voteRes = await HttpService.client.likePost(form);
+ this.updatePost(voteRes);
+ }
+
+ async handlePostEdit(form: EditPost) {
+ const res = await HttpService.client.editPost(form);
+ this.updatePost(res);
+ }
+
+ async handleCommentReport(form: CreateCommentReport) {
+ const reportRes = await HttpService.client.createCommentReport(form);
+ if (reportRes.state == "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
+
+ async handlePostReport(form: CreatePostReport) {
+ const reportRes = await HttpService.client.createPostReport(form);
+ if (reportRes.state == "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
+
+ async handleLockPost(form: LockPost) {
+ const lockRes = await HttpService.client.lockPost(form);
+ this.updatePost(lockRes);
+ }
+
+ async handleDistinguishComment(form: DistinguishComment) {
+ const distinguishRes = await HttpService.client.distinguishComment(form);
+ this.findAndUpdateComment(distinguishRes);
+ }
+
+ async handleAddAdmin(form: AddAdmin) {
+ const addAdminRes = await HttpService.client.addAdmin(form);
+
+ if (addAdminRes.state === "success") {
+ this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+ }
+ }
+
+ async handleTransferCommunity(form: TransferCommunity) {
+ const transferCommunityRes = await HttpService.client.transferCommunity(
+ form
+ );
+ this.updateCommunityFull(transferCommunityRes);
+ }
+
+ async handleFetchChildren(form: GetComments) {
+ const moreCommentsRes = await HttpService.client.getComments(form);
+ if (
+ this.state.commentsRes.state == "success" &&
+ moreCommentsRes.state == "success"
) {
- let data = wsJsonToRes<CommunityResponse>(msg);
- let res = this.state.postRes;
- if (res) {
- res.community_view = data.community_view;
- res.post_view.community = data.community_view.community;
- this.setState(this.state);
- }
- } else if (op == UserOperation.BanFromCommunity) {
- let data = wsJsonToRes<BanFromCommunityResponse>(msg);
+ const newComments = moreCommentsRes.data.comments;
+ // Remove the first comment, since it is the parent
+ newComments.shift();
+ const newRes = this.state.commentsRes;
+ newRes.data.comments.push(...newComments);
+ this.setState({ commentsRes: newRes });
+ }
+ }
+
+ async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+ const readRes = await HttpService.client.markCommentReplyAsRead(form);
+ this.findAndUpdateCommentReply(readRes);
+ }
- let res = this.state.postRes;
- if (res) {
- if (res.post_view.creator.id == data.person_view.person.id) {
- res.post_view.creator_banned_from_community = data.banned;
+ async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+ // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
+ await HttpService.client.markPersonMentionAsRead(form);
+ }
+
+ async handleBanFromCommunity(form: BanFromCommunity) {
+ const banRes = await HttpService.client.banFromCommunity(form);
+ this.updateBan(banRes);
+ }
+
+ async handleBanPerson(form: BanPerson) {
+ const banRes = await HttpService.client.banPerson(form);
+ this.updateBan(banRes);
+ }
+
+ updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (
+ s.postRes.state == "success" &&
+ s.postRes.data.post_view.creator.id ==
+ banRes.data.person_view.person.id
+ ) {
+ s.postRes.data.post_view.creator_banned_from_community =
+ banRes.data.banned;
+ }
+ if (s.commentsRes.state == "success") {
+ s.commentsRes.data.comments
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
+ }
+ return s;
+ });
+ }
+ }
+
+ updateBan(banRes: RequestState<BanPersonResponse>) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (
+ s.postRes.state == "success" &&
+ s.postRes.data.post_view.creator.id ==
+ banRes.data.person_view.person.id
+ ) {
+ s.postRes.data.post_view.creator.banned = banRes.data.banned;
}
+ if (s.commentsRes.state == "success") {
+ s.commentsRes.data.comments
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
+ }
+ return s;
+ });
+ }
+ }
+
+ updateCommunity(communityRes: RequestState<CommunityResponse>) {
+ this.setState(s => {
+ if (s.postRes.state == "success" && communityRes.state == "success") {
+ s.postRes.data.community_view = communityRes.data.community_view;
}
+ return s;
+ });
+ }
- this.state.commentsRes?.comments
- .filter(c => c.creator.id == data.person_view.person.id)
- .forEach(c => (c.creator_banned_from_community = data.banned));
- this.setState(this.state);
- } else if (op == UserOperation.AddModToCommunity) {
- let data = wsJsonToRes<AddModToCommunityResponse>(msg);
- let res = this.state.postRes;
- if (res) {
- res.moderators = data.moderators;
- this.setState(this.state);
+ updateCommunityFull(res: RequestState<GetCommunityResponse>) {
+ this.setState(s => {
+ if (s.postRes.state == "success" && res.state == "success") {
+ s.postRes.data.community_view = res.data.community_view;
+ s.postRes.data.moderators = res.data.moderators;
}
- } else if (op == UserOperation.BanPerson) {
- let data = wsJsonToRes<BanPersonResponse>(msg);
- this.state.commentsRes?.comments
- .filter(c => c.creator.id == data.person_view.person.id)
- .forEach(c => (c.creator.banned = data.banned));
-
- let res = this.state.postRes;
- if (res) {
- if (res.post_view.creator.id == data.person_view.person.id) {
- res.post_view.creator.banned = data.banned;
- }
+ return s;
+ });
+ }
+
+ updatePost(post: RequestState<PostResponse>) {
+ this.setState(s => {
+ if (s.postRes.state == "success" && post.state == "success") {
+ s.postRes.data.post_view = post.data.post_view;
}
- this.setState(this.state);
- } else if (op == UserOperation.AddAdmin) {
- let data = wsJsonToRes<AddAdminResponse>(msg);
- this.setState(s => ((s.siteRes.admins = data.admins), s));
- } else if (op == UserOperation.Search) {
- let data = wsJsonToRes<SearchResponse>(msg);
- let xPosts = data.posts.filter(
- p => p.post.ap_id != this.state.postRes?.post_view.post.ap_id
- );
- this.setState({ crossPosts: xPosts.length > 0 ? xPosts : undefined });
- } else if (op == UserOperation.LeaveAdmin) {
- let data = wsJsonToRes<GetSiteResponse>(msg);
- this.setState({ siteRes: data });
- } else if (op == UserOperation.TransferCommunity) {
- let data = wsJsonToRes<GetCommunityResponse>(msg);
- let res = this.state.postRes;
- if (res) {
- res.community_view = data.community_view;
- res.post_view.community = data.community_view.community;
- res.moderators = data.moderators;
- this.setState(this.state);
+ return s;
+ });
+ }
+
+ purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
+ if (purgeRes.state == "success") {
+ toast(i18n.t("purge_success"));
+ this.context.router.history.push(`/`);
+ }
+ }
+
+ createAndUpdateComments(res: RequestState<CommentResponse>) {
+ this.setState(s => {
+ if (s.commentsRes.state === "success" && res.state === "success") {
+ s.commentsRes.data.comments.unshift(res.data.comment_view);
+
+ // Set finished for the parent
+ s.finished.set(
+ getCommentParentId(res.data.comment_view.comment) ?? 0,
+ true
+ );
}
- } else if (op == UserOperation.BlockPerson) {
- let data = wsJsonToRes<BlockPersonResponse>(msg);
- updatePersonBlock(data);
- } else if (op == UserOperation.CreatePostReport) {
- let data = wsJsonToRes<PostReportResponse>(msg);
- if (data) {
- toast(i18n.t("report_created"));
+ return s;
+ });
+ }
+
+ findAndUpdateComment(res: RequestState<CommentResponse>) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments = editComment(
+ res.data.comment_view,
+ s.commentsRes.data.comments
+ );
+ s.finished.set(res.data.comment_view.comment.id, true);
}
- } else if (op == UserOperation.CreateCommentReport) {
- let data = wsJsonToRes<CommentReportResponse>(msg);
- if (data) {
- toast(i18n.t("report_created"));
+ return s;
+ });
+ }
+
+ findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments = editWith(
+ res.data.comment_reply_view,
+ s.commentsRes.data.comments
+ );
}
- } else if (
- op == UserOperation.PurgePerson ||
- op == UserOperation.PurgePost ||
- op == UserOperation.PurgeComment ||
- op == UserOperation.PurgeCommunity
- ) {
- let data = wsJsonToRes<PurgeItemResponse>(msg);
- if (data.success) {
- toast(i18n.t("purge_success"));
- this.context.router.history.push(`/`);
+ return s;
+ });
+ }
+
+ updateModerators(res: RequestState<AddModToCommunityResponse>) {
+ // Update the moderators
+ this.setState(s => {
+ if (s.postRes.state == "success" && res.state == "success") {
+ s.postRes.data.moderators = res.data.moderators;
}
- }
+ return s;
+ });
}
}
import { Component } from "inferno";
import {
+ CreatePrivateMessage as CreatePrivateMessageI,
GetPersonDetails,
GetPersonDetailsResponse,
GetSiteResponse,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
- import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces";
- import { WebSocketService } from "../../services";
+ import { FirstLoadService } from "../../services/FirstLoadService";
+ import { HttpService, RequestState } from "../../services/HttpService";
import {
- WithPromiseKeys,
++ RouteDataResponse,
getRecipientIdFromProps,
- isBrowser,
myAuth,
setIsoData,
toast,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
import { PrivateMessageForm } from "./private-message-form";
- interface CreatePrivateMessageData {
++type CreatePrivateMessageData = RouteDataResponse<{
+ recipientDetailsResponse: GetPersonDetailsResponse;
- }
++}>;
+
interface CreatePrivateMessageState {
siteRes: GetSiteResponse;
- recipientDetailsRes?: GetPersonDetailsResponse;
- recipient_id: number;
- loading: boolean;
+ recipientRes: RequestState<GetPersonDetailsResponse>;
+ recipientId: number;
+ isIsomorphic: boolean;
}
export class CreatePrivateMessage extends Component<
any,
CreatePrivateMessageState
> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<CreatePrivateMessageData>(this.context);
- private subscription?: Subscription;
state: CreatePrivateMessageState = {
siteRes: this.isoData.site_res,
- recipient_id: getRecipientIdFromProps(this.props),
- loading: true,
+ recipientRes: { state: "empty" },
+ recipientId: getRecipientIdFromProps(this.props),
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
this.handlePrivateMessageCreate =
this.handlePrivateMessageCreate.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
-
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
+ if (FirstLoadService.isFirstLoad) {
this.state = {
...this.state,
- recipientDetailsRes: this.isoData.routeData.recipientDetailsResponse,
- loading: false,
- recipientRes: this.isoData.routeData[0],
++ recipientRes: this.isoData.routeData.recipientDetailsResponse,
+ isIsomorphic: true,
};
- } else {
- this.fetchPersonDetails();
}
}
- fetchPersonDetails() {
- let form: GetPersonDetails = {
- person_id: this.state.recipient_id,
- sort: "New",
- saved_only: false,
- auth: myAuth(false),
- };
- WebSocketService.Instance.send(wsClient.getPersonDetails(form));
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.fetchPersonDetails();
+ }
}
- static fetchInitialData(
- req: InitialFetchRequest
- ): WithPromiseKeys<CreatePrivateMessageData> {
- const person_id = Number(req.path.split("/").pop());
++ static async fetchInitialData({
++ client,
++ path,
++ auth,
++ }: InitialFetchRequest): Promise<CreatePrivateMessageData> {
++ const person_id = Number(path.split("/").pop());
+
+ const form: GetPersonDetails = {
+ person_id,
+ sort: "New",
+ saved_only: false,
- auth: req.auth,
++ auth,
+ };
+
+ return {
- recipientDetailsResponse: req.client.getPersonDetails(form),
++ recipientDetailsResponse: await client.getPersonDetails(form),
+ };
+ }
+
+ async fetchPersonDetails() {
+ this.setState({
+ recipientRes: { state: "loading" },
+ });
+
+ this.setState({
+ recipientRes: await HttpService.client.getPersonDetails({
+ person_id: this.state.recipientId,
+ sort: "New",
+ saved_only: false,
+ auth: myAuth(),
+ }),
+ });
+ }
+
- static fetchInitialData(
- req: InitialFetchRequest
- ): Promise<RequestState<any>>[] {
- const person_id = Number(req.path.split("/").pop());
- const form: GetPersonDetails = {
- person_id,
- sort: "New",
- saved_only: false,
- auth: req.auth,
- };
- return [req.client.getPersonDetails(form)];
- }
-
get documentTitle(): string {
- let name_ = this.state.recipientDetailsRes?.person_view.person.name;
- return name_ ? `${i18n.t("create_private_message")} - ${name_}` : "";
+ if (this.state.recipientRes.state == "success") {
+ const name_ = this.state.recipientRes.data.person_view.person.name;
+ return `${i18n.t("create_private_message")} - ${name_}`;
+ } else {
+ return "";
+ }
}
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
+ renderRecipientRes() {
+ switch (this.state.recipientRes.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const res = this.state.recipientRes.data;
+ return (
+ <div className="row">
+ <div className="col-12 col-lg-6 offset-lg-3 mb-4">
+ <h5>{i18n.t("create_private_message")}</h5>
+ <PrivateMessageForm
+ onCreate={this.handlePrivateMessageCreate}
+ recipient={res.person_view.person}
+ />
+ </div>
+ </div>
+ );
+ }
}
}
render() {
- let res = this.state.recipientDetailsRes;
return (
<div className="container-lg">
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
- {this.state.loading ? (
- <h5>
- <Spinner large />
- </h5>
- ) : (
- res && (
- <div className="row">
- <div className="col-12 col-lg-6 offset-lg-3 mb-4">
- <h5>{i18n.t("create_private_message")}</h5>
- <PrivateMessageForm
- onCreate={this.handlePrivateMessageCreate}
- recipient={res.person_view.person}
- />
- </div>
- </div>
- )
- )}
+ {this.renderRecipientRes()}
</div>
);
}
- handlePrivateMessageCreate() {
- toast(i18n.t("message_sent"));
+ async handlePrivateMessageCreate(form: CreatePrivateMessageI) {
+ const res = await HttpService.client.createPrivateMessage(form);
- // Navigate to the front
- this.context.router.history.push("/");
- }
+ if (res.state == "success") {
+ toast(i18n.t("message_sent"));
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- this.setState({ loading: false });
- return;
- } else if (op == UserOperation.GetPersonDetails) {
- let data = wsJsonToRes<GetPersonDetailsResponse>(msg);
- this.setState({ recipientDetailsRes: data, loading: false });
+ // Navigate to the front
+ this.context.router.history.push("/");
}
}
}
import type { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import {
- CommentResponse,
CommentView,
CommunityView,
GetCommunity,
ListCommunitiesResponse,
ListingType,
PersonView,
- PostResponse,
PostView,
ResolveObject,
ResolveObjectResponse,
SearchResponse,
SearchType,
SortType,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
- import { Subscription } from "rxjs";
import { i18n } from "../i18next";
import { CommentViewType, InitialFetchRequest } from "../interfaces";
- import { WebSocketService } from "../services";
+ import { FirstLoadService } from "../services/FirstLoadService";
+ import { HttpService, RequestState } from "../services/HttpService";
import {
Choice,
QueryParams,
- WithPromiseKeys,
++ RouteDataResponse,
capitalizeFirstLetter,
commentsToFlatNodes,
communityToChoice,
- createCommentLikeRes,
- createPostLikeFindRes,
debounce,
enableDownvotes,
enableNsfw,
saveScrollPosition,
setIsoData,
showLocal,
- toast,
- wsClient,
- wsSubscribe,
} from "../utils";
import { CommentNodes } from "./comment/comment-nodes";
import { HtmlTags } from "./common/html-tags";
page: number;
}
- interface SearchData {
++type SearchData = RouteDataResponse<{
+ communityResponse?: GetCommunityResponse;
+ listCommunitiesResponse?: ListCommunitiesResponse;
+ creatorDetailsResponse?: GetPersonDetailsResponse;
+ searchResponse?: SearchResponse;
+ resolveObjectResponse?: ResolveObjectResponse;
- }
++}>;
+
type FilterType = "creator" | "community";
interface SearchState {
- searchResponse?: SearchResponse;
- communities: CommunityView[];
- creatorDetails?: GetPersonDetailsResponse;
- searchLoading: boolean;
- searchCommunitiesLoading: boolean;
- searchCreatorLoading: boolean;
+ searchRes: RequestState<SearchResponse>;
+ resolveObjectRes: RequestState<ResolveObjectResponse>;
+ creatorDetailsRes: RequestState<GetPersonDetailsResponse>;
+ communitiesRes: RequestState<ListCommunitiesResponse>;
+ communityRes: RequestState<GetCommunityResponse>;
siteRes: GetSiteResponse;
searchText?: string;
- resolveObjectResponse?: ResolveObjectResponse;
communitySearchOptions: Choice[];
creatorSearchOptions: Choice[];
+ searchCreatorLoading: boolean;
+ searchCommunitiesLoading: boolean;
+ isIsomorphic: boolean;
}
interface Combined {
}
export class Search extends Component<any, SearchState> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<SearchData>(this.context);
- private subscription?: Subscription;
++
state: SearchState = {
- searchLoading: false,
+ resolveObjectRes: { state: "empty" },
+ creatorDetailsRes: { state: "empty" },
+ communitiesRes: { state: "empty" },
+ communityRes: { state: "empty" },
siteRes: this.isoData.site_res,
- communities: [],
- searchCommunitiesLoading: false,
- searchCreatorLoading: false,
creatorSearchOptions: [],
communitySearchOptions: [],
+ searchRes: { state: "empty" },
+ searchCreatorLoading: false,
+ searchCommunitiesLoading: false,
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
this.handleCommunityFilterChange.bind(this);
this.handleCreatorFilterChange = this.handleCreatorFilterChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
-
const { q } = getSearchQueryParams();
this.state = {
};
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
+ if (FirstLoadService.isFirstLoad) {
- const [
- communityRes,
- communitiesRes,
- creatorDetailsRes,
- searchRes,
- resolveObjectRes,
- ] = this.isoData.routeData;
+ const {
- communityResponse,
- creatorDetailsResponse,
- listCommunitiesResponse,
- resolveObjectResponse,
- searchResponse,
++ communityResponse: communityRes,
++ creatorDetailsResponse: creatorDetailsRes,
++ listCommunitiesResponse: communitiesRes,
++ resolveObjectResponse: resolveObjectRes,
++ searchResponse: searchRes,
+ } = this.isoData.routeData;
- // This can be single or multiple communities given
- if (listCommunitiesResponse) {
+ this.state = {
+ ...this.state,
- communitiesRes,
- communityRes,
- creatorDetailsRes,
- creatorSearchOptions:
- creatorDetailsRes.state == "success"
- ? [personToChoice(creatorDetailsRes.data.person_view)]
- : [],
+ isIsomorphic: true,
+ };
+
- if (communityRes.state === "success") {
++ if (creatorDetailsRes?.state === "success") {
+ this.state = {
+ ...this.state,
- communities: listCommunitiesResponse.communities,
++ creatorSearchOptions:
++ creatorDetailsRes?.state === "success"
++ ? [personToChoice(creatorDetailsRes.data.person_view)]
++ : [],
++ creatorDetailsRes,
+ };
+ }
- if (communityResponse) {
++
++ if (communitiesRes?.state === "success") {
this.state = {
...this.state,
- communities: [communityResponse.community_view],
-- communitySearchOptions: [
- communityToChoice(communityResponse.community_view),
- communityToChoice(communityRes.data.community_view),
-- ],
++ communitiesRes,
};
}
- this.state = {
- ...this.state,
- creatorDetails: creatorDetailsResponse,
- creatorSearchOptions: creatorDetailsResponse
- ? [personToChoice(creatorDetailsResponse.person_view)]
- : [],
- };
- if (q) {
++ if (communityRes?.state === "success") {
+ this.state = {
+ ...this.state,
- searchRes,
- resolveObjectRes,
++ communityRes,
+ };
+ }
+
+ if (q !== "") {
+ this.state = {
+ ...this.state,
- searchResponse,
- resolveObjectResponse,
- searchLoading: false,
+ };
- } else {
- this.search();
- }
- } else {
- const listCommunitiesForm: ListCommunities = {
- type_: defaultListingType,
- sort: defaultSortType,
- limit: fetchLimit,
- auth: myAuth(false),
- };
+
- WebSocketService.Instance.send(
- wsClient.listCommunities(listCommunitiesForm)
- );
++ if (searchRes?.state === "success") {
++ this.state = {
++ ...this.state,
++ searchRes,
++ };
++ }
+
- if (q) {
- this.search();
++ if (resolveObjectRes?.state === "success") {
++ this.state = {
++ ...this.state,
++ resolveObjectRes,
++ };
++ }
+ }
}
}
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ const promises = [this.fetchCommunities()];
+ if (this.state.searchText) {
+ promises.push(this.search());
+ }
+
+ await Promise.all(promises);
+ }
+ }
+
+ async fetchCommunities() {
+ this.setState({ communitiesRes: { state: "loading" } });
+ this.setState({
+ communitiesRes: await HttpService.client.listCommunities({
+ type_: defaultListingType,
+ sort: defaultSortType,
+ limit: fetchLimit,
+ auth: myAuth(),
+ }),
+ });
+ }
+
componentWillUnmount() {
- this.subscription?.unsubscribe();
saveScrollPosition(this.context);
}
-- static fetchInitialData({
++ static async fetchInitialData({
client,
auth,
query: { communityId, creatorId, q, type, sort, listingType, page },
- }: InitialFetchRequest<
- QueryParams<SearchProps>
- >): WithPromiseKeys<SearchData> {
- }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<
- RequestState<any>
- >[] {
- const promises: Promise<RequestState<any>>[] = [];
-
++ }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<SearchData> {
const community_id = getIdFromString(communityId);
- let communityResponse: Promise<GetCommunityResponse> | undefined =
- undefined;
- let listCommunitiesResponse: Promise<ListCommunitiesResponse> | undefined =
++ let communityResponse: RequestState<GetCommunityResponse> | undefined =
+ undefined;
++ let listCommunitiesResponse:
++ | RequestState<ListCommunitiesResponse>
++ | undefined = undefined;
if (community_id) {
const getCommunityForm: GetCommunity = {
id: community_id,
auth,
};
- promises.push(client.getCommunity(getCommunityForm));
- promises.push(Promise.resolve({ state: "empty" }));
+
- communityResponse = client.getCommunity(getCommunityForm);
++ communityResponse = await client.getCommunity(getCommunityForm);
} else {
const listCommunitiesForm: ListCommunities = {
type_: defaultListingType,
limit: fetchLimit,
auth,
};
- promises.push(Promise.resolve({ state: "empty" }));
- promises.push(client.listCommunities(listCommunitiesForm));
+
- listCommunitiesResponse = client.listCommunities(listCommunitiesForm);
++ listCommunitiesResponse = await client.listCommunities(
++ listCommunitiesForm
++ );
}
const creator_id = getIdFromString(creatorId);
- let creatorDetailsResponse: Promise<GetPersonDetailsResponse> | undefined =
- undefined;
++ let creatorDetailsResponse:
++ | RequestState<GetPersonDetailsResponse>
++ | undefined = undefined;
if (creator_id) {
const getCreatorForm: GetPersonDetails = {
person_id: creator_id,
auth,
};
- promises.push(client.getPersonDetails(getCreatorForm));
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
+
- creatorDetailsResponse = client.getPersonDetails(getCreatorForm);
++ creatorDetailsResponse = await client.getPersonDetails(getCreatorForm);
}
const query = getSearchQueryFromQuery(q);
- let searchResponse: Promise<SearchResponse> | undefined = undefined;
- let resolveObjectResponse:
- | Promise<ResolveObjectResponse | undefined>
- | undefined = undefined;
++ let searchResponse: RequestState<SearchResponse> | undefined = undefined;
++ let resolveObjectResponse: RequestState<ResolveObjectResponse> | undefined =
++ undefined;
+
if (query) {
const form: SearchForm = {
q: query,
};
if (query !== "") {
- searchResponse = client.search(form);
- promises.push(client.search(form));
++ searchResponse = await client.search(form);
if (auth) {
const resolveObjectForm: ResolveObject = {
q: query,
auth,
};
- resolveObjectResponse = client
- promises.push(client.resolveObject(resolveObjectForm));
++ resolveObjectResponse = await client
+ .resolveObject(resolveObjectForm)
+ .catch(() => undefined);
}
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
- promises.push(Promise.resolve({ state: "empty" }));
}
}
- return promises;
+ return {
+ communityResponse,
+ creatorDetailsResponse,
+ listCommunitiesResponse,
+ resolveObjectResponse,
+ searchResponse,
+ };
}
get documentTitle(): string {
{this.selects}
{this.searchForm}
{this.displayResults(type)}
- {this.resultsCount === 0 && !this.state.searchLoading && (
- <span>{i18n.t("no_results")}</span>
- )}
+ {this.resultsCount === 0 &&
+ this.state.searchRes.state === "success" && (
+ <span>{i18n.t("no_results")}</span>
+ )}
<Paginator page={page} onChange={this.handlePageChange} />
</div>
);
minLength={1}
/>
<button type="submit" className="btn btn-secondary mr-2 mb-2">
- {this.state.searchLoading ? (
- {this.state.searchRes.state == "loading" ? (
++ {this.state.searchRes.state === "loading" ? (
<Spinner />
) : (
<span>{i18n.t("search")}</span>
creatorSearchOptions,
searchCommunitiesLoading,
searchCreatorLoading,
+ communitiesRes,
} = this.state;
+ const hasCommunities =
+ communitiesRes.state == "success" &&
+ communitiesRes.data.communities.length > 0;
+
return (
<div className="mb-2">
<select
/>
</span>
<div className="form-row">
- {this.state.communities.length > 0 && (
+ {hasCommunities && (
<Filter
filterType="community"
onChange={this.handleCommunityFilterChange}
onSearch={this.handleCommunitySearch}
options={communitySearchOptions}
- loading={searchCommunitiesLoading}
value={communityId}
+ loading={searchCommunitiesLoading}
/>
)}
<Filter
onChange={this.handleCreatorFilterChange}
onSearch={this.handleCreatorSearch}
options={creatorSearchOptions}
- loading={searchCreatorLoading}
value={creatorId}
+ loading={searchCreatorLoading}
/>
</div>
</div>
buildCombined(): Combined[] {
const combined: Combined[] = [];
- const { resolveObjectResponse, searchResponse } = this.state;
+ const {
+ resolveObjectRes: resolveObjectResponse,
+ searchRes: searchResponse,
+ } = this.state;
// Push the possible resolve / federated objects first
- if (resolveObjectResponse) {
- const { comment, post, community, person } = resolveObjectResponse;
+ if (resolveObjectResponse.state == "success") {
+ const { comment, post, community, person } = resolveObjectResponse.data;
if (comment) {
combined.push(commentViewToCombined(comment));
}
// Push the search results
- if (searchResponse) {
- const { comments, posts, communities, users } = searchResponse;
+ if (searchResponse.state === "success") {
+ const { comments, posts, communities, users } = searchResponse.data;
combined.push(
...[
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
viewOnly
+ // All of these are unused, since its view only
+ onPostEdit={() => {}}
+ onPostVote={() => {}}
+ onPostReport={() => {}}
+ onBlockPerson={() => {}}
+ onLockPost={() => {}}
+ onDeletePost={() => {}}
+ onRemovePost={() => {}}
+ onSavePost={() => {}}
+ onFeaturePost={() => {}}
+ onPurgePerson={() => {}}
+ onPurgePost={() => {}}
+ onBanPersonFromCommunity={() => {}}
+ onBanPerson={() => {}}
+ onAddModToCommunity={() => {}}
+ onAddAdmin={() => {}}
+ onTransferCommunity={() => {}}
/>
)}
{i.type_ === "comments" && (
enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages}
+ // All of these are unused, since its viewonly
+ finished={new Map()}
+ onSaveComment={() => {}}
+ onBlockPerson={() => {}}
+ onDeleteComment={() => {}}
+ onRemoveComment={() => {}}
+ onCommentVote={() => {}}
+ onCommentReport={() => {}}
+ onDistinguishComment={() => {}}
+ onAddModToCommunity={() => {}}
+ onAddAdmin={() => {}}
+ onTransferCommunity={() => {}}
+ onPurgeComment={() => {}}
+ onPurgePerson={() => {}}
+ onCommentReplyRead={() => {}}
+ onPersonMentionRead={() => {}}
+ onBanPersonFromCommunity={() => {}}
+ onBanPerson={() => {}}
+ onCreateComment={() => Promise.resolve({ state: "empty" })}
+ onEditComment={() => Promise.resolve({ state: "empty" })}
/>
)}
{i.type_ === "communities" && (
}
get comments() {
- const { searchResponse, resolveObjectResponse, siteRes } = this.state;
- const comments = searchResponse?.comments ?? [];
-
- if (resolveObjectResponse?.comment) {
- comments.unshift(resolveObjectResponse?.comment);
+ const {
+ searchRes: searchResponse,
+ resolveObjectRes: resolveObjectResponse,
+ siteRes,
+ } = this.state;
+ const comments =
+ searchResponse.state === "success" ? searchResponse.data.comments : [];
+
+ if (
+ resolveObjectResponse.state === "success" &&
+ resolveObjectResponse.data.comment
+ ) {
+ comments.unshift(resolveObjectResponse.data.comment);
}
return (
enableDownvotes={enableDownvotes(siteRes)}
allLanguages={siteRes.all_languages}
siteLanguages={siteRes.discussion_languages}
+ // All of these are unused, since its viewonly
+ finished={new Map()}
+ onSaveComment={() => {}}
+ onBlockPerson={() => {}}
+ onDeleteComment={() => {}}
+ onRemoveComment={() => {}}
+ onCommentVote={() => {}}
+ onCommentReport={() => {}}
+ onDistinguishComment={() => {}}
+ onAddModToCommunity={() => {}}
+ onAddAdmin={() => {}}
+ onTransferCommunity={() => {}}
+ onPurgeComment={() => {}}
+ onPurgePerson={() => {}}
+ onCommentReplyRead={() => {}}
+ onPersonMentionRead={() => {}}
+ onBanPersonFromCommunity={() => {}}
+ onBanPerson={() => {}}
+ onCreateComment={() => Promise.resolve({ state: "empty" })}
+ onEditComment={() => Promise.resolve({ state: "empty" })}
/>
);
}
get posts() {
- const { searchResponse, resolveObjectResponse, siteRes } = this.state;
- const posts = searchResponse?.posts ?? [];
-
- if (resolveObjectResponse?.post) {
- posts.unshift(resolveObjectResponse.post);
+ const {
+ searchRes: searchResponse,
+ resolveObjectRes: resolveObjectResponse,
+ siteRes,
+ } = this.state;
+ const posts =
+ searchResponse.state === "success" ? searchResponse.data.posts : [];
+
+ if (
+ resolveObjectResponse.state === "success" &&
+ resolveObjectResponse.data.post
+ ) {
+ posts.unshift(resolveObjectResponse.data.post);
}
return (
allLanguages={siteRes.all_languages}
siteLanguages={siteRes.discussion_languages}
viewOnly
+ // All of these are unused, since its view only
+ onPostEdit={() => {}}
+ onPostVote={() => {}}
+ onPostReport={() => {}}
+ onBlockPerson={() => {}}
+ onLockPost={() => {}}
+ onDeletePost={() => {}}
+ onRemovePost={() => {}}
+ onSavePost={() => {}}
+ onFeaturePost={() => {}}
+ onPurgePerson={() => {}}
+ onPurgePost={() => {}}
+ onBanPersonFromCommunity={() => {}}
+ onBanPerson={() => {}}
+ onAddModToCommunity={() => {}}
+ onAddAdmin={() => {}}
+ onTransferCommunity={() => {}}
/>
</div>
</div>
}
get communities() {
- const { searchResponse, resolveObjectResponse } = this.state;
- const communities = searchResponse?.communities ?? [];
-
- if (resolveObjectResponse?.community) {
- communities.unshift(resolveObjectResponse.community);
+ const {
+ searchRes: searchResponse,
+ resolveObjectRes: resolveObjectResponse,
+ } = this.state;
+ const communities =
+ searchResponse.state === "success" ? searchResponse.data.communities : [];
+
+ if (
+ resolveObjectResponse.state === "success" &&
+ resolveObjectResponse.data.community
+ ) {
+ communities.unshift(resolveObjectResponse.data.community);
}
return (
}
get users() {
- const { searchResponse, resolveObjectResponse } = this.state;
- const users = searchResponse?.users ?? [];
-
- if (resolveObjectResponse?.person) {
- users.unshift(resolveObjectResponse.person);
+ const {
+ searchRes: searchResponse,
+ resolveObjectRes: resolveObjectResponse,
+ } = this.state;
+ const users =
+ searchResponse.state === "success" ? searchResponse.data.users : [];
+
+ if (
+ resolveObjectResponse.state === "success" &&
+ resolveObjectResponse.data.person
+ ) {
+ users.unshift(resolveObjectResponse.data.person);
}
return (
}
get resultsCount(): number {
- const { searchResponse: r, resolveObjectResponse: resolveRes } = this.state;
-
- const searchCount = r
- ? r.posts.length +
- r.comments.length +
- r.communities.length +
- r.users.length
- : 0;
-
- const resObjCount = resolveRes
- ? resolveRes.post ||
- resolveRes.person ||
- resolveRes.community ||
- resolveRes.comment
- ? 1
- : 0
- : 0;
+ const { searchRes: r, resolveObjectRes: resolveRes } = this.state;
+
+ const searchCount =
+ r.state === "success"
+ ? r.data.posts.length +
+ r.data.comments.length +
+ r.data.communities.length +
+ r.data.users.length
+ : 0;
+
+ const resObjCount =
+ resolveRes.state === "success"
+ ? resolveRes.data.post ||
+ resolveRes.data.person ||
+ resolveRes.data.community ||
+ resolveRes.data.comment
+ ? 1
+ : 0
+ : 0;
return resObjCount + searchCount;
}
- search() {
- const auth = myAuth(false);
+ async search() {
+ const auth = myAuth();
const { searchText: q } = this.state;
const { communityId, creatorId, type, sort, listingType, page } =
getSearchQueryParams();
- if (q && q !== "") {
- const form: SearchForm = {
- q,
- community_id: communityId ?? undefined,
- creator_id: creatorId ?? undefined,
- type_: type,
- sort,
- listing_type: listingType,
- page,
- limit: fetchLimit,
- auth,
- };
-
- if (auth) {
- const resolveObjectForm: ResolveObject = {
+ if (q) {
+ this.setState({ searchRes: { state: "loading" } });
+ this.setState({
+ searchRes: await HttpService.client.search({
q,
+ community_id: communityId ?? undefined,
+ creator_id: creatorId ?? undefined,
+ type_: type,
+ sort,
+ listing_type: listingType,
+ page,
+ limit: fetchLimit,
auth,
- };
- WebSocketService.Instance.send(
- wsClient.resolveObject(resolveObjectForm)
- );
- }
-
- this.setState({
- searchResponse: undefined,
- resolveObjectResponse: undefined,
- searchLoading: true,
+ }),
});
+ window.scrollTo(0, 0);
+ restoreScrollPosition(this.context);
- WebSocketService.Instance.send(wsClient.search(form));
+ if (auth) {
+ this.setState({ resolveObjectRes: { state: "loading" } });
+ this.setState({
+ resolveObjectRes: await HttpService.client.resolveObject({
+ q,
+ auth,
+ }),
+ });
+ }
}
}
handleCreatorSearch = debounce(async (text: string) => {
const { creatorId } = getSearchQueryParams();
const { creatorSearchOptions } = this.state;
- this.setState({
- searchCreatorLoading: true,
- });
-
const newOptions: Choice[] = [];
+ this.setState({ searchCreatorLoading: true });
+
const selectedChoice = creatorSearchOptions.find(
choice => getIdFromString(choice.value) === creatorId
);
}
if (text.length > 0) {
- newOptions.push(...(await fetchUsers(text)).users.map(personToChoice));
+ newOptions.push(...(await fetchUsers(text)).map(personToChoice));
}
this.setState({
}
if (text.length > 0) {
- newOptions.push(
- ...(await fetchCommunities(text)).communities.map(communityToChoice)
- );
+ newOptions.push(...(await fetchCommunities(text)).map(communityToChoice));
}
this.setState({
i.setState({ searchText: event.target.value });
}
- updateUrl({
+ async updateUrl({
q,
type,
listingType,
this.props.history.push(`/search${getQueryString(queryParams)}`);
- this.search();
- }
-
- parseMessage(msg: any) {
- console.log(msg);
- const op = wsUserOp(msg);
- if (msg.error) {
- if (msg.error === "couldnt_find_object") {
- this.setState({
- resolveObjectResponse: {},
- });
- this.checkFinishedLoading();
- } else {
- toast(i18n.t(msg.error), "danger");
- }
- } else {
- switch (op) {
- case UserOperation.Search: {
- const searchResponse = wsJsonToRes<SearchResponse>(msg);
- this.setState({ searchResponse });
- window.scrollTo(0, 0);
- this.checkFinishedLoading();
- restoreScrollPosition(this.context);
-
- break;
- }
-
- case UserOperation.CreateCommentLike: {
- const { comment_view } = wsJsonToRes<CommentResponse>(msg);
- createCommentLikeRes(
- comment_view,
- this.state.searchResponse?.comments
- );
-
- break;
- }
-
- case UserOperation.CreatePostLike: {
- const { post_view } = wsJsonToRes<PostResponse>(msg);
- createPostLikeFindRes(post_view, this.state.searchResponse?.posts);
-
- break;
- }
-
- case UserOperation.ListCommunities: {
- const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
- this.setState({ communities });
-
- break;
- }
-
- case UserOperation.ResolveObject: {
- const resolveObjectResponse = wsJsonToRes<ResolveObjectResponse>(msg);
- this.setState({ resolveObjectResponse });
- this.checkFinishedLoading();
-
- break;
- }
- }
- }
- }
-
- checkFinishedLoading() {
- if (this.state.searchResponse || this.state.resolveObjectResponse) {
- this.setState({ searchLoading: false });
- }
+ await this.search();
}
}
- import { CommentView, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
+ import { CommentView, GetSiteResponse } from "lemmy-js-client";
import type { ParsedQs } from "qs";
+ import { RequestState, WrappedLemmyHttp } from "./services/HttpService";
import { ErrorPageData } from "./utils";
/**
* This contains serialized data, it needs to be deserialized before use.
*/
- export interface IsoData<T extends object = any> {
-export interface IsoData {
++export interface IsoData<T extends Record<string, RequestState<any>> = any> {
path: string;
- routeData: RequestState<any>[];
+ routeData: T;
site_res: GetSiteResponse;
errorPageData?: ErrorPageData;
}
- export type IsoDataOptionalSite<T extends object = any> = Partial<IsoData<T>> &
-export type IsoDataOptionalSite = Partial<IsoData> &
- Pick<IsoData, Exclude<keyof IsoData, "site_res">>;
++export type IsoDataOptionalSite<
++ T extends Record<string, RequestState<any>> = any
++> = Partial<IsoData<T>> &
+ Pick<IsoData<T>, Exclude<keyof IsoData<T>, "site_res">>;
export interface ILemmyConfig {
wsHost?: string;
export interface InitialFetchRequest<T extends ParsedQs = ParsedQs> {
auth?: string;
- client: LemmyHttp;
+ client: WrappedLemmyHttp;
path: string;
query: T;
site: GetSiteResponse;
Comment,
}
+ export enum VoteType {
+ Upvote,
+ Downvote,
+ }
+
export interface CommentNodeI {
comment_view: CommentView;
children: Array<CommentNodeI>;
import { CreatePrivateMessage } from "./components/private_message/create-private-message";
import { Search } from "./components/search";
import { InitialFetchRequest } from "./interfaces";
- import { WithPromiseKeys } from "./utils";
+ import { RequestState } from "./services/HttpService";
- interface IRoutePropsWithFetch<T extends object> extends IRouteProps {
-interface IRoutePropsWithFetch extends IRouteProps {
++interface IRoutePropsWithFetch<T extends Record<string, RequestState<any>>>
++ extends IRouteProps {
// TODO Make sure this one is good.
- fetchInitialData?(req: InitialFetchRequest): WithPromiseKeys<T>;
- fetchInitialData?(req: InitialFetchRequest): Promise<RequestState<any>>[];
++ fetchInitialData?(req: InitialFetchRequest): T;
}
-export const routes: IRoutePropsWithFetch[] = [
+export const routes: IRoutePropsWithFetch<Record<string, any>>[] = [
{
path: `/`,
component: Home,
--- /dev/null
-type FailedRequestState = {
+ import { LemmyHttp } from "lemmy-js-client";
+ import { getHttpBase } from "../../shared/env";
+ import { i18n } from "../../shared/i18next";
+ import { toast } from "../../shared/utils";
+
+ type EmptyRequestState = {
+ state: "empty";
+ };
+
+ type LoadingRequestState = {
+ state: "loading";
+ };
+
- state: "success",
++export type FailedRequestState = {
+ state: "failed";
+ msg: string;
+ };
+
+ type SuccessRequestState<T> = {
+ state: "success";
+ data: T;
+ };
+
+ /**
+ * Shows the state of an API request.
+ *
+ * Can be empty, loading, failed, or success
+ */
+ export type RequestState<T> =
+ | EmptyRequestState
+ | LoadingRequestState
+ | FailedRequestState
+ | SuccessRequestState<T>;
+
+ export type WrappedLemmyHttp = {
+ [K in keyof LemmyHttp]: LemmyHttp[K] extends (...args: any[]) => any
+ ? ReturnType<LemmyHttp[K]> extends Promise<infer U>
+ ? (...args: Parameters<LemmyHttp[K]>) => Promise<RequestState<U>>
+ : (
+ ...args: Parameters<LemmyHttp[K]>
+ ) => Promise<RequestState<LemmyHttp[K]>>
+ : LemmyHttp[K];
+ };
+
+ class WrappedLemmyHttpClient {
+ #client: LemmyHttp;
+
+ constructor(client: LemmyHttp) {
+ this.#client = client;
+
+ for (const key of Object.getOwnPropertyNames(
+ Object.getPrototypeOf(this.#client)
+ )) {
+ if (key !== "constructor") {
+ WrappedLemmyHttpClient.prototype[key] = async (...args) => {
+ try {
+ const res = await this.#client[key](...args);
+
+ return {
+ data: res,
++ state: !(res === undefined || res === null) ? "success" : "empty",
+ };
+ } catch (error) {
+ console.error(`API error: ${error}`);
+ toast(i18n.t(error), "danger");
+ return {
+ state: "failed",
+ msg: error,
+ };
+ }
+ };
+ }
+ }
+ }
+ }
+
+ export function wrapClient(client: LemmyHttp) {
+ return new WrappedLemmyHttpClient(client) as unknown as WrappedLemmyHttp; // unfortunately, this verbose cast is necessary
+ }
+
+ export class HttpService {
+ static #_instance: HttpService;
+ #client: WrappedLemmyHttp;
+
+ private constructor() {
+ this.#client = wrapClient(new LemmyHttp(getHttpBase()));
+ }
+
+ static get #Instance() {
+ return this.#_instance ?? (this.#_instance = new this());
+ }
+
+ public static get client() {
+ return this.#Instance.#client;
+ }
+ }
import {
BlockCommunityResponse,
BlockPersonResponse,
+ CommentAggregates,
Comment as CommentI,
+ CommentReplyView,
CommentReportView,
CommentSortType,
CommentView,
GetSiteResponse,
Language,
LemmyHttp,
- LemmyWebsocket,
MyUserInfo,
Person,
+ PersonMentionView,
PersonView,
PostReportView,
PostView,
PrivateMessageView,
RegistrationApplicationView,
Search,
+ SearchType,
SortType,
- UploadImageResponse,
} from "lemmy-js-client";
import { default as MarkdownIt } from "markdown-it";
import markdown_it_container from "markdown-it-container";
import Renderer from "markdown-it/lib/renderer";
import Token from "markdown-it/lib/token";
import moment from "moment";
- import { Subscription } from "rxjs";
- import { delay, retryWhen, take } from "rxjs/operators";
import tippy from "tippy.js";
import Toastify from "toastify-js";
import { getHttpBase } from "./env";
import { i18n, languages } from "./i18next";
- import { CommentNodeI, DataType, IsoData } from "./interfaces";
- import { UserService, WebSocketService } from "./services";
+ import { CommentNodeI, DataType, IsoData, VoteType } from "./interfaces";
+ import { HttpService, UserService } from "./services";
++import { RequestState } from "./services/HttpService";
let Tribute: any;
if (isBrowser()) {
Tribute = require("tributejs");
}
- export const wsClient = new LemmyWebsocket();
-
export const favIconUrl = "/static/assets/icons/favicon.svg";
export const favIconPngUrl = "/static/assets/icons/apple-touch-icon.png";
// TODO
export const docsUrl = `${joinLemmyUrl}/docs/en/index.html`;
export const helpGuideUrl = `${joinLemmyUrl}/docs/en/users/01-getting-started.html`; // TODO find a way to redirect to the non-en folder
export const markdownHelpUrl = `${joinLemmyUrl}/docs/en/users/02-media.html`;
- export const sortingHelpUrl = `${helpGuideUrl}/docs/en/users/03-votes-and-ranking.html`;
+ export const sortingHelpUrl = `${joinLemmyUrl}/docs/en/users/03-votes-and-ranking.html`;
export const archiveTodayUrl = "https://archive.today";
export const ghostArchiveUrl = "https://ghostarchive.org";
export const webArchiveUrl = "https://web.archive.org";
adminMatrixIds?: string[];
}
- let customEmojis: EmojiMartCategory[] = [];
+ const customEmojis: EmojiMartCategory[] = [];
export let customEmojisLookup: Map<string, CustomEmojiView> = new Map<
string,
CustomEmojiView
export function hotRank(score: number, timeStr: string): number {
// Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
- let date: Date = new Date(timeStr + "Z"); // Add Z to convert from UTC date
- let now: Date = new Date();
- let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
+ const date: Date = new Date(timeStr + "Z"); // Add Z to convert from UTC date
+ const now: Date = new Date();
+ const hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
- let rank =
+ const rank =
(10000 * Math.log10(Math.max(1, 3 + Number(score)))) /
Math.pow(hoursElapsed + 2, 1.8);
.concat(mods?.map(m => m.moderator.id) ?? []) ?? [];
if (myUserInfo) {
- let myIndex = adminsThenMods.findIndex(
+ const myIndex = adminsThenMods.findIndex(
id => id == myUserInfo.local_user_view.person.id
);
if (myIndex == -1) {
mods?: CommunityModeratorView[],
myUserInfo = UserService.Instance.myUserInfo
): boolean {
- let myId = myUserInfo?.local_user_view.person.id;
+ const myId = myUserInfo?.local_user_view.person.id;
// Don't allow mod actions on yourself
return myId == mods?.at(0)?.moderator.id && myId != creator_id;
}
admins?: PersonView[],
myUserInfo = UserService.Instance.myUserInfo
): boolean {
- let myId = myUserInfo?.local_user_view.person.id;
+ const myId = myUserInfo?.local_user_view.person.id;
return myId == admins?.at(0)?.person.id && myId != creator_id;
}
}
export function communityRSSUrl(actorId: string, sort: string): string {
- let url = new URL(actorId);
+ const url = new URL(actorId);
return `${url.origin}/feeds${url.pathname}.xml?sort=${sort}`;
}
export function validEmail(email: string) {
- let re =
+ const re =
/^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
}
export async function getSiteMetadata(url: string) {
- let form: GetSiteMetadata = { url };
- let client = new LemmyHttp(getHttpBase());
+ const form: GetSiteMetadata = { url };
+ const client = new LemmyHttp(getHttpBase());
return client.getSiteMetadata(form);
}
override?: string,
myUserInfo = UserService.Instance.myUserInfo
): string[] {
- let myLang = myUserInfo?.local_user_view.local_user.interface_language;
- let lang = override || myLang || "browser";
+ const myLang = myUserInfo?.local_user_view.local_user.interface_language;
+ const lang = override || myLang || "browser";
if (lang == "browser" && isBrowser()) {
return getBrowserLanguages();
function getBrowserLanguages(): string[] {
// Intersect lemmy's langs, with the browser langs
- let langs = languages ? languages.map(l => l.code) : ["en"];
+ const langs = languages ? languages.map(l => l.code) : ["en"];
// NOTE, mobile browsers seem to be missing this list, so append en
- let allowedLangs = navigator.languages
+ const allowedLangs = navigator.languages
.concat("en")
.filter(v => langs.includes(v));
return allowedLangs;
theme = "darkly";
}
- let themeList = await fetchThemeList();
+ const themeList = await fetchThemeList();
// Unload all the other themes
for (var i = 0; i < themeList.length; i++) {
- let styleSheet = document.getElementById(themeList[i]);
+ const styleSheet = document.getElementById(themeList[i]);
if (styleSheet) {
styleSheet.setAttribute("disabled", "disabled");
}
document.getElementById("default-dark")?.setAttribute("disabled", "disabled");
// Load the theme dynamically
- let cssLoc = `/css/themes/${theme}.css`;
+ const cssLoc = `/css/themes/${theme}.css`;
loadCss(theme, cssLoc);
document.getElementById(theme)?.removeAttribute("disabled");
}
}
- interface NotifyInfo {
- name: string;
- icon?: string;
- link: string;
- body?: string;
- }
-
- export function messageToastify(info: NotifyInfo, router: any) {
- if (isBrowser()) {
- let htmlBody = info.body ? md.render(info.body) : "";
- let backgroundColor = `var(--light)`;
-
- let toast = Toastify({
- text: `${htmlBody}<br />${info.name}`,
- avatar: info.icon,
- backgroundColor: backgroundColor,
- className: "text-dark",
- close: true,
- gravity: "top",
- position: "right",
- duration: 5000,
- escapeMarkup: false,
- onClick: () => {
- if (toast) {
- toast.hideToast();
- router.history.push(info.link);
- }
- },
- });
- toast.showToast();
- }
- }
-
- export function notifyPost(post_view: PostView, router: any) {
- let info: NotifyInfo = {
- name: post_view.community.name,
- icon: post_view.community.icon,
- link: `/post/${post_view.post.id}`,
- body: post_view.post.name,
- };
- notify(info, router);
- }
-
- export function notifyComment(comment_view: CommentView, router: any) {
- let info: NotifyInfo = {
- name: comment_view.creator.name,
- icon: comment_view.creator.avatar,
- link: `/comment/${comment_view.comment.id}`,
- body: comment_view.comment.content,
- };
- notify(info, router);
- }
-
- export function notifyPrivateMessage(pmv: PrivateMessageView, router: any) {
- let info: NotifyInfo = {
- name: pmv.creator.name,
- icon: pmv.creator.avatar,
- link: `/inbox`,
- body: pmv.private_message.content,
- };
- notify(info, router);
- }
-
- function notify(info: NotifyInfo, router: any) {
- messageToastify(info, router);
-
- if (Notification.permission !== "granted") Notification.requestPermission();
- else {
- var notification = new Notification(info.name, {
- ...{ body: info.body },
- ...(info.icon && { icon: info.icon }),
- });
-
- notification.onclick = (ev: Event): any => {
- ev.preventDefault();
- router.history.push(info.link);
- };
- }
- }
-
export function setupTribute() {
return new Tribute({
noMatchTemplate: function () {
{
trigger: ":",
menuItemTemplate: (item: any) => {
- let shortName = `:${item.original.key}:`;
+ const shortName = `:${item.original.key}:`;
return `${item.original.val} ${shortName}`;
},
selectTemplate: (item: any) => {
- let customEmoji = customEmojisLookup.get(
+ const customEmoji = customEmojisLookup.get(
item.original.key
)?.custom_emoji;
if (customEmoji == undefined) return `${item.original.val}`;
{
trigger: "@",
selectTemplate: (item: any) => {
- let it: PersonTribute = item.original;
+ const it: PersonTribute = item.original;
return `[${it.key}](${it.view.person.actor_id})`;
},
values: debounce(async (text: string, cb: any) => {
{
trigger: "!",
selectTemplate: (item: any) => {
- let it: CommunityTribute = item.original;
+ const it: CommunityTribute = item.original;
return `[${it.key}](${it.view.community.actor_id})`;
},
values: debounce(async (text: string, cb: any) => {
}
function setupEmojiDataModel(custom_emoji_views: CustomEmojiView[]) {
- let groupedEmojis = groupBy(custom_emoji_views, x => x.custom_emoji.category);
+ const groupedEmojis = groupBy(
+ custom_emoji_views,
+ x => x.custom_emoji.category
+ );
for (const [category, emojis] of Object.entries(groupedEmojis)) {
customEmojis.push({
id: category,
keywords: custom_emoji_view.keywords.map(x => x.keyword),
skins: [{ src: custom_emoji_view.custom_emoji.image_url }],
};
- let categoryIndex = customEmojis.findIndex(
+ const categoryIndex = customEmojis.findIndex(
x => x.id == custom_emoji_view.custom_emoji.category
);
if (categoryIndex == -1) {
emojis: [emoji],
});
} else {
- let emojiIndex = customEmojis[categoryIndex].emojis.findIndex(
+ const emojiIndex = customEmojis[categoryIndex].emojis.findIndex(
x => x.id == custom_emoji_view.custom_emoji.shortcode
);
if (emojiIndex == -1) {
export function removeFromEmojiDataModel(id: number) {
let view: CustomEmojiView | undefined;
- for (let item of customEmojisLookup.values()) {
+ for (const item of customEmojisLookup.values()) {
if (item.custom_emoji.id === id) {
view = item;
break;
}
async function personSearch(text: string): Promise<PersonTribute[]> {
- let users = (await fetchUsers(text)).users;
- let persons: PersonTribute[] = users.map(pv => {
- let tribute: PersonTribute = {
- key: `@${pv.person.name}@${hostname(pv.person.actor_id)}`,
- view: pv,
- };
- return tribute;
- });
- return persons;
+ const usersResponse = await fetchUsers(text);
+
+ return usersResponse.map(pv => ({
+ key: `@${pv.person.name}@${hostname(pv.person.actor_id)}`,
+ view: pv,
+ }));
}
interface CommunityTribute {
}
async function communitySearch(text: string): Promise<CommunityTribute[]> {
- let comms = (await fetchCommunities(text)).communities;
- let communities: CommunityTribute[] = comms.map(cv => {
- let tribute: CommunityTribute = {
- key: `!${cv.community.name}@${hostname(cv.community.actor_id)}`,
- view: cv,
- };
- return tribute;
- });
- return communities;
+ const communitiesResponse = await fetchCommunities(text);
+
+ return communitiesResponse.map(cv => ({
+ key: `!${cv.community.name}@${hostname(cv.community.actor_id)}`,
+ view: cv,
+ }));
}
export function getRecipientIdFromProps(props: any): number {
}
export function getIdFromProps(props: any): number | undefined {
- let id = props.match.params.post_id;
+ const id = props.match.params.post_id;
return id ? Number(id) : undefined;
}
export function getCommentIdFromProps(props: any): number | undefined {
- let id = props.match.params.comment_id;
+ const id = props.match.params.comment_id;
return id ? Number(id) : undefined;
}
- export function editCommentRes(data: CommentView, comments?: CommentView[]) {
- let found = comments?.find(c => c.comment.id == data.comment.id);
- if (found) {
- found.comment.content = data.comment.content;
- found.comment.distinguished = data.comment.distinguished;
- found.comment.updated = data.comment.updated;
- found.comment.removed = data.comment.removed;
- found.comment.deleted = data.comment.deleted;
- found.counts.upvotes = data.counts.upvotes;
- found.counts.downvotes = data.counts.downvotes;
- found.counts.score = data.counts.score;
- }
+ type ImmutableListKey =
+ | "comment"
+ | "comment_reply"
+ | "person_mention"
+ | "community"
+ | "private_message"
+ | "post"
+ | "post_report"
+ | "comment_report"
+ | "private_message_report"
+ | "registration_application";
+
+ function editListImmutable<
+ T extends { [key in F]: { id: number } },
+ F extends ImmutableListKey
+ >(fieldName: F, data: T, list: T[]): T[] {
+ return [
+ ...list.map(c => (c[fieldName].id === data[fieldName].id ? data : c)),
+ ];
+ }
+
+ export function editComment(
+ data: CommentView,
+ comments: CommentView[]
+ ): CommentView[] {
+ return editListImmutable("comment", data, comments);
}
- export function saveCommentRes(data: CommentView, comments?: CommentView[]) {
- let found = comments?.find(c => c.comment.id == data.comment.id);
- if (found) {
- found.saved = data.saved;
- }
+ export function editCommentReply(
+ data: CommentReplyView,
+ replies: CommentReplyView[]
+ ): CommentReplyView[] {
+ return editListImmutable("comment_reply", data, replies);
+ }
+
+ interface WithComment {
+ comment: CommentI;
+ counts: CommentAggregates;
+ my_vote?: number;
+ saved: boolean;
+ }
+
+ export function editMention(
+ data: PersonMentionView,
+ comments: PersonMentionView[]
+ ): PersonMentionView[] {
+ return editListImmutable("person_mention", data, comments);
+ }
+
+ export function editCommunity(
+ data: CommunityView,
+ communities: CommunityView[]
+ ): CommunityView[] {
+ return editListImmutable("community", data, communities);
+ }
+
+ export function editPrivateMessage(
+ data: PrivateMessageView,
+ messages: PrivateMessageView[]
+ ): PrivateMessageView[] {
+ return editListImmutable("private_message", data, messages);
+ }
+
+ export function editPost(data: PostView, posts: PostView[]): PostView[] {
+ return editListImmutable("post", data, posts);
+ }
+
+ export function editPostReport(
+ data: PostReportView,
+ reports: PostReportView[]
+ ) {
+ return editListImmutable("post_report", data, reports);
+ }
+
+ export function editCommentReport(
+ data: CommentReportView,
+ reports: CommentReportView[]
+ ): CommentReportView[] {
+ return editListImmutable("comment_report", data, reports);
+ }
+
+ export function editPrivateMessageReport(
+ data: PrivateMessageReportView,
+ reports: PrivateMessageReportView[]
+ ): PrivateMessageReportView[] {
+ return editListImmutable("private_message_report", data, reports);
+ }
+
+ export function editRegistrationApplication(
+ data: RegistrationApplicationView,
+ apps: RegistrationApplicationView[]
+ ): RegistrationApplicationView[] {
+ return editListImmutable("registration_application", data, apps);
+ }
+
+ export function editWith<D extends WithComment, L extends WithComment>(
+ { comment, counts, saved, my_vote }: D,
+ list: L[]
+ ) {
+ return [
+ ...list.map(c =>
+ c.comment.id === comment.id
+ ? { ...c, comment, counts, saved, my_vote }
+ : c
+ ),
+ ];
}
export function updatePersonBlock(
data: BlockPersonResponse,
myUserInfo: MyUserInfo | undefined = UserService.Instance.myUserInfo
) {
- let mui = myUserInfo;
- if (mui) {
+ if (myUserInfo) {
if (data.blocked) {
- mui.person_blocks.push({
- person: mui.local_user_view.person,
+ myUserInfo.person_blocks.push({
+ person: myUserInfo.local_user_view.person,
target: data.person_view.person,
});
toast(`${i18n.t("blocked")} ${data.person_view.person.name}`);
} else {
- mui.person_blocks = mui.person_blocks.filter(
- i => i.target.id != data.person_view.person.id
+ myUserInfo.person_blocks = myUserInfo.person_blocks.filter(
+ i => i.target.id !== data.person_view.person.id
);
toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`);
}
data: BlockCommunityResponse,
myUserInfo: MyUserInfo | undefined = UserService.Instance.myUserInfo
) {
- let mui = myUserInfo;
- if (mui) {
+ if (myUserInfo) {
if (data.blocked) {
- mui.community_blocks.push({
- person: mui.local_user_view.person,
+ myUserInfo.community_blocks.push({
+ person: myUserInfo.local_user_view.person,
community: data.community_view.community,
});
toast(`${i18n.t("blocked")} ${data.community_view.community.name}`);
} else {
- mui.community_blocks = mui.community_blocks.filter(
- i => i.community.id != data.community_view.community.id
+ myUserInfo.community_blocks = myUserInfo.community_blocks.filter(
+ i => i.community.id !== data.community_view.community.id
);
toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`);
}
}
}
- export function createCommentLikeRes(
- data: CommentView,
- comments?: CommentView[]
- ) {
- let found = comments?.find(c => c.comment.id === data.comment.id);
- if (found) {
- found.counts.score = data.counts.score;
- found.counts.upvotes = data.counts.upvotes;
- found.counts.downvotes = data.counts.downvotes;
- if (data.my_vote !== null) {
- found.my_vote = data.my_vote;
- }
- }
- }
-
- export function createPostLikeFindRes(data: PostView, posts?: PostView[]) {
- let found = posts?.find(p => p.post.id == data.post.id);
- if (found) {
- createPostLikeRes(data, found);
- }
- }
-
- export function createPostLikeRes(data: PostView, post_view?: PostView) {
- if (post_view) {
- post_view.counts.score = data.counts.score;
- post_view.counts.upvotes = data.counts.upvotes;
- post_view.counts.downvotes = data.counts.downvotes;
- if (data.my_vote !== null) {
- post_view.my_vote = data.my_vote;
- }
- }
- }
-
- export function editPostFindRes(data: PostView, posts?: PostView[]) {
- let found = posts?.find(p => p.post.id == data.post.id);
- if (found) {
- editPostRes(data, found);
- }
- }
-
- export function editPostRes(data: PostView, post: PostView) {
- if (post) {
- post.post.url = data.post.url;
- post.post.name = data.post.name;
- post.post.nsfw = data.post.nsfw;
- post.post.deleted = data.post.deleted;
- post.post.removed = data.post.removed;
- post.post.featured_community = data.post.featured_community;
- post.post.featured_local = data.post.featured_local;
- post.post.body = data.post.body;
- post.post.locked = data.post.locked;
- post.saved = data.saved;
- }
- }
-
- // TODO possible to make these generic?
- export function updatePostReportRes(
- data: PostReportView,
- reports?: PostReportView[]
- ) {
- let found = reports?.find(p => p.post_report.id == data.post_report.id);
- if (found) {
- found.post_report = data.post_report;
- }
- }
-
- export function updateCommentReportRes(
- data: CommentReportView,
- reports?: CommentReportView[]
- ) {
- let found = reports?.find(c => c.comment_report.id == data.comment_report.id);
- if (found) {
- found.comment_report = data.comment_report;
- }
- }
-
- export function updatePrivateMessageReportRes(
- data: PrivateMessageReportView,
- reports?: PrivateMessageReportView[]
- ) {
- let found = reports?.find(
- c => c.private_message_report.id == data.private_message_report.id
- );
- if (found) {
- found.private_message_report = data.private_message_report;
- }
- }
-
- export function updateRegistrationApplicationRes(
- data: RegistrationApplicationView,
- applications?: RegistrationApplicationView[]
- ) {
- let found = applications?.find(
- ra => ra.registration_application.id == data.registration_application.id
- );
- if (found) {
- found.registration_application = data.registration_application;
- found.admin = data.admin;
- found.creator_local_user = data.creator_local_user;
- }
- }
-
export function commentsToFlatNodes(comments: CommentView[]): CommentNodeI[] {
- let nodes: CommentNodeI[] = [];
- for (let comment of comments) {
+ const nodes: CommentNodeI[] = [];
+ for (const comment of comments) {
nodes.push({ comment_view: comment, children: [], depth: 0 });
}
return nodes;
comments: CommentView[],
parentComment: boolean
): CommentNodeI[] {
- let map = new Map<number, CommentNodeI>();
- let depthOffset = !parentComment
+ const map = new Map<number, CommentNodeI>();
+ const depthOffset = !parentComment
? 0
: getDepthFromComment(comments[0].comment) ?? 0;
- for (let comment_view of comments) {
- let depthI = getDepthFromComment(comment_view.comment) ?? 0;
- let depth = depthI ? depthI - depthOffset : 0;
- let node: CommentNodeI = {
+ for (const comment_view of comments) {
+ const depthI = getDepthFromComment(comment_view.comment) ?? 0;
+ const depth = depthI ? depthI - depthOffset : 0;
+ const node: CommentNodeI = {
comment_view,
children: [],
depth,
map.set(comment_view.comment.id, { ...node });
}
- let tree: CommentNodeI[] = [];
+ const tree: CommentNodeI[] = [];
// if its a parent comment fetch, then push the first comment to the top node.
if (parentComment) {
- let cNode = map.get(comments[0].comment.id);
+ const cNode = map.get(comments[0].comment.id);
if (cNode) {
tree.push(cNode);
}
}
- for (let comment_view of comments) {
- let child = map.get(comment_view.comment.id);
+ for (const comment_view of comments) {
+ const child = map.get(comment_view.comment.id);
if (child) {
- let parent_id = getCommentParentId(comment_view.comment);
+ const parent_id = getCommentParentId(comment_view.comment);
if (parent_id) {
- let parent = map.get(parent_id);
+ const parent = map.get(parent_id);
// Necessary because blocked comment might not exist
if (parent) {
parent.children.push(child);
}
export function getCommentParentId(comment?: CommentI): number | undefined {
- let split = comment?.path.split(".");
+ const split = comment?.path.split(".");
// remove the 0
split?.shift();
}
export function getDepthFromComment(comment?: CommentI): number | undefined {
- let len = comment?.path.split(".").length;
+ const len = comment?.path.split(".").length;
return len ? len - 2 : undefined;
}
+ // TODO make immutable
export function insertCommentIntoTree(
tree: CommentNodeI[],
cv: CommentView,
parentComment: boolean
) {
// Building a fake node to be used for later
- let node: CommentNodeI = {
+ const node: CommentNodeI = {
comment_view: cv,
children: [],
depth: 0,
};
- let parentId = getCommentParentId(cv.comment);
+ const parentId = getCommentParentId(cv.comment);
if (parentId) {
- let parent_comment = searchCommentTree(tree, parentId);
+ const parent_comment = searchCommentTree(tree, parentId);
if (parent_comment) {
node.depth = parent_comment.depth + 1;
parent_comment.children.unshift(node);
tree: CommentNodeI[],
id: number
): CommentNodeI | undefined {
- for (let node of tree) {
+ for (const node of tree) {
if (node.comment_view.comment.id === id) {
return node;
}
for (const child of node.children) {
- let res = searchCommentTree([child], id);
+ const res = searchCommentTree([child], id);
if (res) {
return res;
}
export function hostname(url: string): string {
- let cUrl = new URL(url);
+ const cUrl = new URL(url);
return cUrl.port ? `${cUrl.hostname}:${cUrl.port}` : `${cUrl.hostname}`;
}
return typeof window !== "undefined";
}
- export function setIsoData<T extends object>(context: any): IsoData<T> {
-export function setIsoData(context: any): IsoData {
++export function setIsoData<T extends Record<string, RequestState<any>>>(
++ context: any
++): IsoData<T> {
// If its the browser, you need to deserialize the data from the window
if (isBrowser()) {
return window.isoData;
} else return context.router.staticContext;
}
- export function wsSubscribe(parseMessage: any): Subscription | undefined {
- if (isBrowser()) {
- return WebSocketService.Instance.subject
- .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
- .subscribe(
- msg => parseMessage(msg),
- err => console.error(err),
- () => console.log("complete")
- );
- } else {
- return undefined;
- }
- }
-
moment.updateLocale("en", {
relativeTime: {
future: "in %s",
});
export function saveScrollPosition(context: any) {
- let path: string = context.router.route.location.pathname;
- let y = window.scrollY;
+ const path: string = context.router.route.location.pathname;
+ const y = window.scrollY;
sessionStorage.setItem(`scrollPosition_${path}`, y.toString());
}
export function restoreScrollPosition(context: any) {
- let path: string = context.router.route.location.pathname;
- let y = Number(sessionStorage.getItem(`scrollPosition_${path}`));
+ const path: string = context.router.route.location.pathname;
+ const y = Number(sessionStorage.getItem(`scrollPosition_${path}`));
window.scrollTo(0, y);
}
};
}
- export async function fetchCommunities(q: string) {
- let form: Search = {
+ function fetchSearchResults(q: string, type_: SearchType) {
+ const form: Search = {
q,
- type_: "Communities",
+ type_,
sort: "TopAll",
listing_type: "All",
page: 1,
limit: fetchLimit,
- auth: myAuth(false),
+ auth: myAuth(),
};
- let client = new LemmyHttp(getHttpBase());
- return client.search(form);
+
+ return HttpService.client.search(form);
+ }
+
+ export async function fetchCommunities(q: string) {
+ const res = await fetchSearchResults(q, "Communities");
+
+ return res.state === "success" ? res.data.communities : [];
}
export async function fetchUsers(q: string) {
- let form: Search = {
- q,
- type_: "Users",
- sort: "TopAll",
- listing_type: "All",
- page: 1,
- limit: fetchLimit,
- auth: myAuth(false),
- };
- let client = new LemmyHttp(getHttpBase());
- return client.search(form);
+ const res = await fetchSearchResults(q, "Users");
+
+ return res.state === "success" ? res.data.users : [];
}
export function communitySelectName(cv: CommunityView): string {
UserService.Instance.myUserInfo = site?.my_user;
i18n.changeLanguage(getLanguages()[0]);
if (site) {
- setupEmojiDataModel(site.custom_emojis);
+ setupEmojiDataModel(site.custom_emojis ?? []);
}
setupMarkdown();
}
}
export function isBanned(ps: Person): boolean {
- let expires = ps.ban_expires;
+ const expires = ps.ban_expires;
// Add Z to convert from UTC date
// TODO this check probably isn't necessary anymore
if (expires) {
}
}
- export function myAuth(throwErr = true): string | undefined {
- return UserService.Instance.auth(throwErr);
+ export function myAuth(): string | undefined {
+ return UserService.Instance.auth();
+ }
+
+ export function myAuthRequired(): string {
+ return UserService.Instance.auth(true) ?? "";
}
export function enableDownvotes(siteRes: GetSiteResponse): boolean {
pv: PostView,
myUserInfo = UserService.Instance.myUserInfo
): boolean {
- let nsfw = pv.post.nsfw || pv.community.nsfw;
- let myShowNsfw = myUserInfo?.local_user_view.local_user.show_nsfw ?? false;
+ const nsfw = pv.post.nsfw || pv.community.nsfw;
+ const myShowNsfw = myUserInfo?.local_user_view.local_user.show_nsfw ?? false;
return !nsfw || (nsfw && myShowNsfw);
}
showSite?: boolean,
myUserInfo = UserService.Instance.myUserInfo
): Language[] {
- let allLangIds = allLanguages.map(l => l.id);
+ const allLangIds = allLanguages.map(l => l.id);
let myLangs = myUserInfo?.discussion_languages ?? allLangIds;
myLangs = myLangs.length == 0 ? allLangIds : myLangs;
- let siteLangs = siteLanguages.length == 0 ? allLangIds : siteLanguages;
+ const siteLangs = siteLanguages.length == 0 ? allLangIds : siteLanguages;
if (showAll) {
return allLanguages;
}
}
- export function uploadImage(image: File): Promise<UploadImageResponse> {
- const client = new LemmyHttp(getHttpBase());
-
- return client.uploadImage({ image });
- }
-
interface EmojiMartCategory {
id: string;
name: string;
[key in keyof T]?: string;
};
- export type WithPromiseKeys<T extends object> = {
- [K in keyof T]: Promise<T[K]>;
- };
-
export function getQueryParams<T extends Record<string, any>>(processors: {
[K in keyof T]: (param: string) => T[K];
}): T {
}
export function isAuthPath(pathname: string) {
- return /create_.*|inbox|settings|setup|admin|reports|registration_applications/g.test(
+ return /create_.*|inbox|settings|admin|reports|registration_applications/g.test(
pathname
);
}
navigator.share(shareData);
}
}
+
+ export function newVote(voteType: VoteType, myVote?: number): number {
+ if (voteType == VoteType.Upvote) {
+ return myVote == 1 ? 0 : 1;
+ } else {
+ return myVote == -1 ? 0 : -1;
+ }
+ }
++
++export type RouteDataResponse<T extends Record<string, any>> = {
++ [K in keyof T]: RequestState<Exclude<T[K], undefined>>;
++};