import { UserService } from "../../services";
import { HttpService, RequestState } from "../../services/HttpService";
import {
- amAdmin,
- canCreateCommunity,
donateLemmyUrl,
- isBrowser,
myAuth,
numToSI,
- poll,
showAvatars,
toast,
updateUnreadCountsInterval,
} from "../../utils";
+import { isBrowser } from "../../utils/browser/is-browser";
+import { poll } from "../../utils/helpers/poll";
+import { amAdmin } from "../../utils/roles/am-admin";
+import { canCreateCommunity } from "../../utils/roles/can-create-community";
import { Icon } from "../common/icon";
import { PictrsImage } from "../common/pictrs-image";
)}
<li className="nav-item">
<a
- className="nav-link"
+ className="nav-link d-inline-flex align-items-center d-md-inline-block"
title={i18n.t("support_lemmy")}
href={donateLemmyUrl}
>
<Icon icon="heart" classes="small" />
+ <span className="d-inline ml-1 d-md-none ml-md-0">
+ {i18n.t("support_lemmy")}
+ </span>
</a>
</li>
</ul>
<li id="navSearch" className="nav-item">
<NavLink
to="/search"
- className="nav-link"
+ className="nav-link d-inline-flex align-items-center d-md-inline-block"
title={i18n.t("search")}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
<Icon icon="search" />
+ <span className="d-inline ml-1 d-md-none ml-md-0">
+ {i18n.t("search")}
+ </span>
</NavLink>
</li>
{amAdmin() && (
<li id="navAdmin" className="nav-item">
<NavLink
to="/admin"
- className="nav-link"
+ className="nav-link d-inline-flex align-items-center d-md-inline-block"
title={i18n.t("admin_settings")}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
<Icon icon="settings" />
+ <span className="d-inline ml-1 d-md-none ml-md-0">
+ {i18n.t("admin_settings")}
+ </span>
</NavLink>
</li>
)}
<>
<li id="navMessages" className="nav-item">
<NavLink
- className="nav-link"
+ className="nav-link d-inline-flex align-items-center d-md-inline-block"
to="/inbox"
title={i18n.t("unread_messages", {
count: Number(this.unreadInboxCount),
onMouseUp={linkEvent(this, handleCollapseClick)}
>
<Icon icon="bell" />
+ <span className="badge badge-light d-inline ml-1 d-md-none ml-md-0">
+ {i18n.t("unread_messages", {
+ count: Number(this.unreadInboxCount),
+ formattedCount: numToSI(this.unreadInboxCount),
+ })}
+ </span>
{this.unreadInboxCount > 0 && (
<span className="mx-1 badge badge-light">
{numToSI(this.unreadInboxCount)}
{this.moderatesSomething && (
<li id="navModeration" className="nav-item">
<NavLink
- className="nav-link"
+ className="nav-link d-inline-flex align-items-center d-md-inline-block"
to="/reports"
title={i18n.t("unread_reports", {
count: Number(this.unreadReportCount),
onMouseUp={linkEvent(this, handleCollapseClick)}
>
<Icon icon="shield" />
+ <span className="badge badge-light d-inline ml-1 d-md-none ml-md-0">
+ {i18n.t("unread_reports", {
+ count: Number(this.unreadReportCount),
+ formattedCount: numToSI(this.unreadReportCount),
+ })}
+ </span>
{this.unreadReportCount > 0 && (
<span className="mx-1 badge badge-light">
{numToSI(this.unreadReportCount)}
<li id="navApplications" className="nav-item">
<NavLink
to="/registration_applications"
- className="nav-link"
+ className="nav-link d-inline-flex align-items-center d-md-inline-block"
title={i18n.t("unread_registration_applications", {
count: Number(this.unreadApplicationCount),
formattedCount: numToSI(this.unreadApplicationCount),
onMouseUp={linkEvent(this, handleCollapseClick)}
>
<Icon icon="clipboard" />
+ <span className="badge badge-light d-inline ml-1 d-md-none ml-md-0">
+ {i18n.t("unread_registration_applications", {
+ count: Number(this.unreadApplicationCount),
+ formattedCount: numToSI(this.unreadApplicationCount),
+ })}
+ </span>
{this.unreadApplicationCount > 0 && (
<span className="mx-1 badge badge-light">
{numToSI(this.unreadApplicationCount)}
import autosize from "autosize";
+ import classNames from "classnames";
import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import { Language } from "lemmy-js-client";
import {
concurrentImageUpload,
customEmojisLookup,
- isBrowser,
markdownFieldCharacterLimit,
markdownHelpUrl,
maxUploadImages,
setupTribute,
toast,
} from "../../utils";
+import { isBrowser } from "../../utils/browser/is-browser";
import { EmojiPicker } from "./emoji-picker";
import { Icon, Spinner } from "./icon";
import { LanguageSelect } from "./language-select";
import NavigationPrompt from "./navigation-prompt";
import ProgressBar from "./progress-bar";
-
interface MarkdownTextAreaProps {
initialContent?: string;
initialLanguageId?: number;
}
/>
<div className="form-group row">
- <div className={`col-sm-12`}>
- <textarea
- id={this.id}
- className={`form-control ${this.state.previewMode && "d-none"}`}
- value={this.state.content}
- onInput={linkEvent(this, this.handleContentChange)}
- onPaste={linkEvent(this, this.handleImageUploadPaste)}
- onKeyDown={linkEvent(this, this.handleKeyBinds)}
- required
- disabled={this.isDisabled}
- rows={2}
- maxLength={this.props.maxLength ?? markdownFieldCharacterLimit}
- placeholder={this.props.placeholder}
- />
- {this.state.previewMode && this.state.content && (
- <div
- className="card border-secondary card-body md-div"
- dangerouslySetInnerHTML={mdToHtml(this.state.content)}
- />
- )}
- {this.state.imageUploadStatus &&
- this.state.imageUploadStatus.total > 1 && (
- <ProgressBar
- className="mt-2"
- striped
- animated
- value={this.state.imageUploadStatus.uploaded}
- max={this.state.imageUploadStatus.total}
- text={i18n.t("pictures_uploded_progess", {
- uploaded: this.state.imageUploadStatus.uploaded,
- total: this.state.imageUploadStatus.total,
- })}
+ <div className="col-12">
+ <div className="rounded bg-light border border-light">
+ <div className="d-flex flex-wrap border-bottom border-light">
+ {this.getFormatButton("bold", this.handleInsertBold)}
+ {this.getFormatButton("italic", this.handleInsertItalic)}
+ {this.getFormatButton("link", this.handleInsertLink)}
+ <EmojiPicker
+ onEmojiClick={e => this.handleEmoji(this, e)}
+ disabled={this.isDisabled}
+ ></EmojiPicker>
+ <form className="btn btn-sm text-muted font-weight-bold">
+ <label
+ htmlFor={`file-upload-${this.id}`}
+ className={`mb-0 ${
+ UserService.Instance.myUserInfo && "pointer"
+ }`}
+ data-tippy-content={i18n.t("upload_image")}
+ >
+ {this.state.imageUploadStatus ? (
+ <Spinner />
+ ) : (
+ <Icon icon="image" classes="icon-inline" />
+ )}
+ </label>
+ <input
+ id={`file-upload-${this.id}`}
+ type="file"
+ accept="image/*,video/*"
+ name="file"
+ className="d-none"
+ multiple
+ disabled={
+ !UserService.Instance.myUserInfo || this.isDisabled
+ }
+ onChange={linkEvent(this, this.handleImageUpload)}
+ />
+ </form>
+ {this.getFormatButton("header", this.handleInsertHeader)}
+ {this.getFormatButton(
+ "strikethrough",
+ this.handleInsertStrikethrough
+ )}
+ {this.getFormatButton("quote", this.handleInsertQuote)}
+ {this.getFormatButton("list", this.handleInsertList)}
+ {this.getFormatButton("code", this.handleInsertCode)}
+ {this.getFormatButton("subscript", this.handleInsertSubscript)}
+ {this.getFormatButton(
+ "superscript",
+ this.handleInsertSuperscript
+ )}
+ {this.getFormatButton("spoiler", this.handleInsertSpoiler)}
+ <a
+ href={markdownHelpUrl}
+ className="btn btn-sm text-muted font-weight-bold"
+ title={i18n.t("formatting_help")}
+ rel={relTags}
+ >
+ <Icon icon="help-circle" classes="icon-inline" />
+ </a>
+ </div>
+
+ <div>
+ <textarea
+ id={this.id}
+ className={classNames(
+ "form-control border-0 rounded-top-0 rounded-bottom",
+ {
+ "d-none": this.state.previewMode,
+ }
+ )}
+ value={this.state.content}
+ onInput={linkEvent(this, this.handleContentChange)}
+ onPaste={linkEvent(this, this.handleImageUploadPaste)}
+ onKeyDown={linkEvent(this, this.handleKeyBinds)}
+ required
+ disabled={this.isDisabled}
+ rows={2}
+ maxLength={
+ this.props.maxLength ?? markdownFieldCharacterLimit
+ }
+ placeholder={this.props.placeholder}
/>
- )}
- </div>
- <label className="sr-only" htmlFor={this.id}>
- {i18n.t("body")}
- </label>
- </div>
- <div className="row">
- <div className="col-sm-12 d-flex flex-wrap">
- {this.getFormatButton("bold", this.handleInsertBold)}
- {this.getFormatButton("italic", this.handleInsertItalic)}
- {this.getFormatButton("link", this.handleInsertLink)}
- <EmojiPicker
- onEmojiClick={e => this.handleEmoji(this, e)}
- disabled={this.isDisabled}
- ></EmojiPicker>
- <form className="btn btn-sm text-muted font-weight-bold">
- <label
- htmlFor={`file-upload-${this.id}`}
- className={`mb-0 ${
- UserService.Instance.myUserInfo && "pointer"
- }`}
- data-tippy-content={i18n.t("upload_image")}
- >
- {this.state.imageUploadStatus ? (
- <Spinner />
- ) : (
- <Icon icon="image" classes="icon-inline" />
+ {this.state.previewMode && this.state.content && (
+ <div
+ className="card border-secondary card-body md-div"
+ dangerouslySetInnerHTML={mdToHtml(this.state.content)}
+ />
)}
+ {this.state.imageUploadStatus &&
+ this.state.imageUploadStatus.total > 1 && (
+ <ProgressBar
+ className="mt-2"
+ striped
+ animated
+ value={this.state.imageUploadStatus.uploaded}
+ max={this.state.imageUploadStatus.total}
+ text={i18n.t("pictures_uploded_progess", {
+ uploaded: this.state.imageUploadStatus.uploaded,
+ total: this.state.imageUploadStatus.total,
+ })}
+ />
+ )}
+ </div>
+ <label className="sr-only" htmlFor={this.id}>
+ {i18n.t("body")}
</label>
- <input
- id={`file-upload-${this.id}`}
- type="file"
- accept="image/*,video/*"
- name="file"
- className="d-none"
- multiple
- disabled={!UserService.Instance.myUserInfo || this.isDisabled}
- onChange={linkEvent(this, this.handleImageUpload)}
- />
- </form>
- {this.getFormatButton("header", this.handleInsertHeader)}
- {this.getFormatButton(
- "strikethrough",
- this.handleInsertStrikethrough
- )}
- {this.getFormatButton("quote", this.handleInsertQuote)}
- {this.getFormatButton("list", this.handleInsertList)}
- {this.getFormatButton("code", this.handleInsertCode)}
- {this.getFormatButton("subscript", this.handleInsertSubscript)}
- {this.getFormatButton("superscript", this.handleInsertSuperscript)}
- {this.getFormatButton("spoiler", this.handleInsertSpoiler)}
- <a
- href={markdownHelpUrl}
- className="btn btn-sm text-muted font-weight-bold"
- title={i18n.t("formatting_help")}
- rel={relTags}
- >
- <Icon icon="help-circle" classes="icon-inline" />
- </a>
+ </div>
</div>
- <div className="col-sm-12 d-flex align-items-center flex-wrap">
+ <div className="col-12 d-flex align-items-center flex-wrap mt-2">
{this.props.showLanguage && (
<LanguageSelect
iconVersion
{this.props.buttonTitle && (
<button
type="submit"
- className="btn btn-sm btn-secondary mr-2"
+ className="btn btn-sm btn-secondary ml-2"
disabled={this.isDisabled}
>
{this.state.loading ? (
{this.props.replyType && (
<button
type="button"
- className="btn btn-sm btn-secondary mr-2"
+ className="btn btn-sm btn-secondary ml-2"
onClick={linkEvent(this, this.handleReplyCancel)}
>
{i18n.t("cancel")}
)}
{this.state.content && (
<button
- className={`btn btn-sm btn-secondary mr-2 ${
+ className={`btn btn-sm btn-secondary ml-2 ${
this.state.previewMode && "active"
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
import { FirstLoadService } from "../../services/FirstLoadService";
import { HttpService, RequestState } from "../../services/HttpService";
import {
- QueryParams,
- RouteDataResponse,
editCommunity,
getPageFromString,
- getQueryParams,
- getQueryString,
myAuth,
myAuthRequired,
numToSI,
setIsoData,
showLocal,
} from "../../utils";
+import { getQueryParams } from "../../utils/helpers/get-query-params";
+import { getQueryString } from "../../utils/helpers/get-query-string";
+import type { QueryParams } from "../../utils/types/query-params";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
import { ListingTypeSelect } from "../common/listing-type-select";
const communityLimit = 50;
+ type CommunitiesData = RouteDataResponse<{
+ listCommunitiesResponse: ListCommunitiesResponse;
+ }>;
+
interface CommunitiesState {
listCommunitiesResponse: RequestState<ListCommunitiesResponse>;
siteRes: GetSiteResponse;
}
export class Communities extends Component<any, CommunitiesState> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<CommunitiesData>(this.context);
state: CommunitiesState = {
listCommunitiesResponse: { state: "empty" },
siteRes: this.isoData.site_res,
// Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) {
+ const { listCommunitiesResponse } = this.isoData.routeData;
+
this.state = {
...this.state,
- listCommunitiesResponse: this.isoData.routeData[0],
+ listCommunitiesResponse,
isIsomorphic: true,
};
}
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>
+ >): Promise<CommunitiesData> {
const listCommunitiesForm: ListCommunities = {
type_: getListingTypeFromQuery(listingType),
sort: "TopMonth",
auth: auth,
};
- return [client.listCommunities(listCommunitiesForm)];
+ return {
+ listCommunitiesResponse: await client.listCommunities(
+ listCommunitiesForm
+ ),
+ };
}
getCommunitiesQueryParams() {
import { FirstLoadService } from "../../services/FirstLoadService";
import { HttpService, RequestState } from "../../services/HttpService";
import {
- QueryParams,
- RouteDataResponse,
commentsToFlatNodes,
communityRSSUrl,
editComment,
getCommentParentId,
getDataTypeString,
getPageFromString,
- getQueryParams,
- getQueryString,
myAuth,
postToCommentSortType,
relTags,
updateCommunityBlock,
updatePersonBlock,
} from "../../utils";
+import { getQueryParams } from "../../utils/helpers/get-query-params";
+import { getQueryString } from "../../utils/helpers/get-query-string";
+import type { QueryParams } from "../../utils/types/query-params";
import { CommentNodes } from "../comment/comment-nodes";
import { BannerIconHeader } from "../common/banner-icon-header";
import { DataTypeSelect } from "../common/data-type-select";
import { SiteSidebar } from "../home/site-sidebar";
import { PostListings } from "../post/post-listings";
import { CommunityLink } from "./community-link";
+
+ type CommunityData = RouteDataResponse<{
+ communityRes: GetCommunityResponse;
+ postsRes: GetPostsResponse;
+ commentsRes: GetCommentsResponse;
+ }>;
+
interface State {
communityRes: RequestState<GetCommunityResponse>;
postsRes: RequestState<GetPostsResponse>;
RouteComponentProps<{ name: string }>,
State
> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<CommunityData>(this.context);
state: State = {
communityRes: { state: "empty" },
postsRes: { state: "empty" },
// Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) {
- const [communityRes, postsRes, commentsRes] = this.isoData.routeData;
+ const { communityRes, commentsRes, postsRes } = this.isoData.routeData;
+
this.state = {
...this.state,
+ isIsomorphic: true,
+ commentsRes,
communityRes,
postsRes,
- commentsRes,
- isIsomorphic: true,
};
}
}
saveScrollPosition(this.context);
}
- static fetchInitialData({
+ static async fetchInitialData({
client,
path,
query: { dataType: urlDataType, page: urlPage, sort: urlSort },
auth,
}: 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: RequestState<GetPostsResponse> = { state: "empty" };
+ let commentsResponse: RequestState<GetCommentsResponse> = {
+ state: "empty",
+ };
+
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 = 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 = await client.getComments(getCommentsForm);
}
- return promises;
+ return {
+ communityRes: await client.getCommunity(communityForm),
+ commentsRes: commentsResponse,
+ postsRes: postsResponse,
+ };
}
get documentTitle(): string {
import { i18n } from "../../i18next";
import { UserService } from "../../services";
import {
- amAdmin,
- amMod,
- amTopMod,
getUnixTime,
hostname,
mdToHtml,
myAuthRequired,
- numToSI,
} from "../../utils";
+import { amAdmin } from "../../utils/roles/am-admin";
+import { amMod } from "../../utils/roles/am-mod";
+import { amTopMod } from "../../utils/roles/am-top-mod";
+ import { Badges } from "../common/badges";
import { BannerIconHeader } from "../common/banner-icon-header";
import { Icon, PurgeWarning, Spinner } from "../common/icon";
import { CommunityForm } from "../community/community-form";
<section id="sidebarInfo" className="card border-secondary mb-3">
<div className="card-body">
{this.description()}
- {this.badges()}
+ <Badges
+ communityId={this.props.community_view.community.id}
+ counts={this.props.community_view.counts}
+ />
{this.mods()}
</div>
</section>
);
}
- badges() {
- const community_view = this.props.community_view;
- const counts = community_view.counts;
- return (
- <ul className="my-1 list-inline">
- <li
- className="list-inline-item badge badge-secondary pointer"
- data-tippy-content={i18n.t("active_users_in_the_last_day", {
- count: Number(counts.users_active_day),
- formattedCount: numToSI(counts.users_active_day),
- })}
- >
- {i18n.t("number_of_users", {
- count: Number(counts.users_active_day),
- formattedCount: numToSI(counts.users_active_day),
- })}{" "}
- / {i18n.t("day")}
- </li>
- <li
- className="list-inline-item badge badge-secondary pointer"
- data-tippy-content={i18n.t("active_users_in_the_last_week", {
- count: Number(counts.users_active_week),
- formattedCount: numToSI(counts.users_active_week),
- })}
- >
- {i18n.t("number_of_users", {
- count: Number(counts.users_active_week),
- formattedCount: numToSI(counts.users_active_week),
- })}{" "}
- / {i18n.t("week")}
- </li>
- <li
- className="list-inline-item badge badge-secondary pointer"
- data-tippy-content={i18n.t("active_users_in_the_last_month", {
- count: Number(counts.users_active_month),
- formattedCount: numToSI(counts.users_active_month),
- })}
- >
- {i18n.t("number_of_users", {
- count: Number(counts.users_active_month),
- formattedCount: numToSI(counts.users_active_month),
- })}{" "}
- / {i18n.t("month")}
- </li>
- <li
- className="list-inline-item badge badge-secondary pointer"
- data-tippy-content={i18n.t("active_users_in_the_last_six_months", {
- count: Number(counts.users_active_half_year),
- formattedCount: numToSI(counts.users_active_half_year),
- })}
- >
- {i18n.t("number_of_users", {
- count: Number(counts.users_active_half_year),
- formattedCount: numToSI(counts.users_active_half_year),
- })}{" "}
- / {i18n.t("number_of_months", { count: 6, formattedCount: 6 })}
- </li>
- <li className="list-inline-item badge badge-secondary">
- {i18n.t("number_of_subscribers", {
- count: Number(counts.subscribers),
- formattedCount: numToSI(counts.subscribers),
- })}
- </li>
- <li className="list-inline-item badge badge-secondary">
- {i18n.t("number_of_posts", {
- count: Number(counts.posts),
- formattedCount: numToSI(counts.posts),
- })}
- </li>
- <li className="list-inline-item badge badge-secondary">
- {i18n.t("number_of_comments", {
- count: Number(counts.comments),
- formattedCount: numToSI(counts.comments),
- })}
- </li>
- <li className="list-inline-item">
- <Link
- className="badge badge-primary"
- to={`/modlog/${this.props.community_view.community.id}`}
- >
- {i18n.t("modlog")}
- </Link>
- </li>
- </ul>
- );
- }
-
mods() {
return (
<ul className="list-inline small">
import { FirstLoadService } from "../../services/FirstLoadService";
import { HttpService, RequestState } from "../../services/HttpService";
import {
- canCreateCommunity,
commentsToFlatNodes,
editComment,
editPost,
getCommentParentId,
getDataTypeString,
getPageFromString,
- getQueryParams,
- getQueryString,
getRandomFromList,
mdToHtml,
myAuth,
postToCommentSortType,
- QueryParams,
relTags,
restoreScrollPosition,
+ RouteDataResponse,
saveScrollPosition,
setIsoData,
setupTippy,
trendingFetchLimit,
updatePersonBlock,
} from "../../utils";
+import { getQueryParams } from "../../utils/helpers/get-query-params";
+import { getQueryString } from "../../utils/helpers/get-query-string";
+import { canCreateCommunity } from "../../utils/roles/can-create-community";
+import type { QueryParams } from "../../utils/types/query-params";
import { CommentNodes } from "../comment/comment-nodes";
import { DataTypeSelect } from "../common/data-type-select";
import { HtmlTags } from "../common/html-tags";
page: number;
}
+ type HomeData = RouteDataResponse<{
+ postsRes: GetPostsResponse;
+ commentsRes: GetCommentsResponse;
+ trendingCommunitiesRes: ListCommunitiesResponse;
+ }>;
+
+ function 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} />
+ </>
+ )
+ );
+ }
+
function getDataTypeFromQuery(type?: string): DataType {
return type ? DataType[type] : DataType.Post;
}
);
export class Home extends Component<any, HomeState> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<HomeData>(this.context);
state: HomeState = {
postsRes: { state: "empty" },
commentsRes: { state: "empty" },
// Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) {
- const [postsRes, commentsRes, trendingCommunitiesRes] =
+ const { trendingCommunitiesRes, commentsRes, postsRes } =
this.isoData.routeData;
this.state = {
...this.state,
- postsRes,
- commentsRes,
trendingCommunitiesRes,
+ commentsRes,
+ postsRes,
tagline: getRandomFromList(this.state?.siteRes?.taglines ?? [])
?.content,
isIsomorphic: true,
}
async componentDidMount() {
- if (!this.state.isIsomorphic || !this.isoData.routeData.length) {
+ if (
+ !this.state.isIsomorphic ||
+ !Object.values(this.isoData.routeData).some(
+ res => res.state === "success" || res.state === "failed"
+ )
+ ) {
await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
}
saveScrollPosition(this.context);
}
- static fetchInitialData({
+ static async fetchInitialData({
client,
auth,
query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
- }: 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<RequestState<any>>[] = [];
+ let postsRes: RequestState<GetPostsResponse> = { state: "empty" };
+ let commentsRes: RequestState<GetCommentsResponse> = {
+ state: "empty",
+ };
if (dataType === DataType.Post) {
const getPostsForm: GetPosts = {
auth,
};
- promises.push(client.getPosts(getPostsForm));
- promises.push(Promise.resolve({ state: "empty" }));
+ postsRes = await client.getPosts(getPostsForm);
} else {
const getCommentsForm: GetComments = {
page,
saved_only: false,
auth,
};
- promises.push(Promise.resolve({ state: "empty" }));
- promises.push(client.getComments(getCommentsForm));
+
+ commentsRes = await client.getComments(getCommentsForm);
}
const trendingCommunitiesForm: ListCommunities = {
limit: trendingFetchLimit,
auth,
};
- promises.push(client.listCommunities(trendingCommunitiesForm));
- return promises;
+ return {
+ trendingCommunitiesRes: await client.listCommunities(
+ trendingCommunitiesForm
+ ),
+ commentsRes,
+ postsRes,
+ };
}
get documentTitle(): string {
></div>
)}
<div className="d-block d-md-none">{this.mobileView}</div>
- {this.posts()}
+ {this.posts}
</main>
<aside className="d-none d-md-block col-md-4">
{this.mySidebar}
await this.fetchData();
}
- posts() {
+ get posts() {
const { page } = getHomeQueryParams();
return (
const siteRes = this.state.siteRes;
if (dataType === DataType.Post) {
- switch (this.state.postsRes?.state) {
+ switch (this.state.postsRes.state) {
case "loading":
return (
<h5>
<span className="mr-2">
<SortSelect sort={sort} onChange={this.handleSortChange} />
</span>
- {this.getRss(listingType)}
+ {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({
GetModlog,
GetModlogResponse,
GetPersonDetails,
+ GetPersonDetailsResponse,
ModAddCommunityView,
ModAddView,
ModBanFromCommunityView,
import { HttpService, RequestState } from "../services/HttpService";
import {
Choice,
- QueryParams,
- RouteDataResponse,
- amAdmin,
- amMod,
- debounce,
fetchLimit,
fetchUsers,
getIdFromString,
getPageFromString,
- getQueryParams,
- getQueryString,
getUpdatedSearchId,
myAuth,
personToChoice,
setIsoData,
} from "../utils";
+import { debounce } from "../utils/helpers/debounce";
+import { getQueryParams } from "../utils/helpers/get-query-params";
+import { getQueryString } from "../utils/helpers/get-query-string";
+import { amAdmin } from "../utils/roles/am-admin";
+import { amMod } from "../utils/roles/am-mod";
+import type { QueryParams } from "../utils/types/query-params";
import { HtmlTags } from "./common/html-tags";
import { Icon, Spinner } from "./common/icon";
import { MomentTime } from "./common/moment-time";
| AdminPurgePostView
| AdminPurgeCommentView;
+ type ModlogData = RouteDataResponse<{
+ res: GetModlogResponse;
+ communityRes: GetCommunityResponse;
+ modUserResponse: GetPersonDetailsResponse;
+ userResponse: GetPersonDetailsResponse;
+ }>;
+
interface ModlogType {
id: number;
type_: ModlogActionType;
RouteComponentProps<{ communityId?: string }>,
ModlogState
> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<ModlogData>(this.context);
state: ModlogState = {
res: { state: "empty" },
// Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) {
- const [res, communityRes, filteredModRes, filteredUserRes] =
+ const { res, communityRes, modUserResponse, userResponse } =
this.isoData.routeData;
+
this.state = {
...this.state,
res,
communityRes,
};
- if (filteredModRes.state === "success") {
+ if (modUserResponse.state === "success") {
this.state = {
...this.state,
- modSearchOptions: [personToChoice(filteredModRes.data.person_view)],
+ modSearchOptions: [personToChoice(modUserResponse.data.person_view)],
};
}
- if (filteredUserRes.state === "success") {
+ if (userResponse.state === "success") {
this.state = {
...this.state,
- userSearchOptions: [personToChoice(filteredUserRes.data.person_view)],
+ userSearchOptions: [personToChoice(userResponse.data.person_view)],
};
}
}
}
}
- static fetchInitialData({
+ static async fetchInitialData({
client,
path,
query: { modId: urlModId, page, userId: urlUserId, actionType },
auth,
site,
- }: 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,
};
- promises.push(client.getModlog(modlogForm));
+ let communityResponse: RequestState<GetCommunityResponse> = {
+ state: "empty",
+ };
if (communityId) {
const communityForm: GetCommunity = {
id: communityId,
auth,
};
- promises.push(client.getCommunity(communityForm));
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
+
+ communityResponse = await client.getCommunity(communityForm);
}
+ let modUserResponse: RequestState<GetPersonDetailsResponse> = {
+ state: "empty",
+ };
+
if (modId) {
const getPersonForm: GetPersonDetails = {
person_id: modId,
auth,
};
- promises.push(client.getPersonDetails(getPersonForm));
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
+ modUserResponse = await client.getPersonDetails(getPersonForm);
}
+ let userResponse: RequestState<GetPersonDetailsResponse> = {
+ state: "empty",
+ };
+
if (userId) {
const getPersonForm: GetPersonDetails = {
person_id: userId,
auth,
};
- promises.push(client.getPersonDetails(getPersonForm));
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
+ userResponse = await client.getPersonDetails(getPersonForm);
}
- return promises;
+ return {
+ res: await client.getModlog(modlogForm),
+ communityRes: communityResponse,
+ modUserResponse,
+ userResponse,
+ };
}
}
import { FirstLoadService } from "../../services/FirstLoadService";
import { HttpService, RequestState } from "../../services/HttpService";
import {
- QueryParams,
- RouteDataResponse,
- canMod,
capitalizeFirstLetter,
editComment,
editPost,
futureDaysToUnixTime,
getCommentParentId,
getPageFromString,
- getQueryParams,
- getQueryString,
- isAdmin,
- isBanned,
mdToHtml,
myAuth,
myAuthRequired,
toast,
updatePersonBlock,
} from "../../utils";
+import { getQueryParams } from "../../utils/helpers/get-query-params";
+import { getQueryString } from "../../utils/helpers/get-query-string";
+import { canMod } from "../../utils/roles/can-mod";
+import { isAdmin } from "../../utils/roles/is-admin";
+import { isBanned } from "../../utils/roles/is-banned";
+import type { QueryParams } from "../../utils/types/query-params";
import { BannerIconHeader } from "../common/banner-icon-header";
import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon";
import { PersonDetails } from "./person-details";
import { PersonListing } from "./person-listing";
+ type ProfileData = RouteDataResponse<{
+ personResponse: GetPersonDetailsResponse;
+ }>;
+
interface ProfileState {
personRes: RequestState<GetPersonDetailsResponse>;
personBlocked: boolean;
RouteComponentProps<{ username: string }>,
ProfileState
> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<ProfileData>(this.context);
state: ProfileState = {
personRes: { state: "empty" },
personBlocked: false,
if (FirstLoadService.isFirstLoad) {
this.state = {
...this.state,
- personRes: this.isoData.routeData[0],
+ personRes: this.isoData.routeData.personResponse,
isIsomorphic: true,
};
}
}
}
- static fetchInitialData({
+ static async fetchInitialData({
client,
path,
query: { page, sort, view: urlView },
auth,
- }: 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: await client.getPersonDetails(form),
+ };
}
get documentTitle(): string {
import { FirstLoadService } from "../../services/FirstLoadService";
import { RequestState } from "../../services/HttpService";
import {
- amAdmin,
+ RouteDataResponse,
editCommentReport,
editPostReport,
editPrivateMessageReport,
myAuthRequired,
setIsoData,
} from "../../utils";
+import { amAdmin } from "../../utils/roles/am-admin";
import { CommentReport } from "../comment/comment-report";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
PrivateMessageReport,
}
+ type ReportsData = RouteDataResponse<{
+ commentReportsRes: ListCommentReportsResponse;
+ postReportsRes: ListPostReportsResponse;
+ messageReportsRes: ListPrivateMessageReportsResponse;
+ }>;
+
type ItemType = {
id: number;
type_: MessageEnum;
}
export class Reports extends Component<any, ReportsState> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<ReportsData>(this.context);
state: ReportsState = {
commentReportsRes: { state: "empty" },
postReportsRes: { state: "empty" },
// Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) {
- const [commentReportsRes, postReportsRes, messageReportsRes] =
+ const { commentReportsRes, postReportsRes, messageReportsRes } =
this.isoData.routeData;
+
this.state = {
...this.state,
commentReportsRes,
if (amAdmin()) {
this.state = {
...this.state,
- messageReportsRes,
+ messageReportsRes: messageReportsRes,
};
}
}
await i.refetch();
}
- static fetchInitialData({
+ static async fetchInitialData({
auth,
client,
- }: 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: ReportsData = {
+ commentReportsRes: await client.listCommentReports(commentReportsForm),
+ postReportsRes: await client.listPostReports(postReportsForm),
+ messageReportsRes: { state: "empty" },
+ };
+
+ if (amAdmin()) {
+ const privateMessageReportsForm: ListPrivateMessageReports = {
unresolved_only,
page,
limit,
- auth,
+ auth: auth as string,
};
- promises.push(client.listPostReports(postReportsForm));
- 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.messageReportsRes = await client.listPrivateMessageReports(
+ privateMessageReportsForm
);
}
- return promises;
+ return data;
}
async refetch() {
import {
CreatePost as CreatePostI,
GetCommunity,
+ GetCommunityResponse,
GetSiteResponse,
ListCommunitiesResponse,
} from "lemmy-js-client";
} from "../../services/HttpService";
import {
Choice,
- QueryParams,
+ RouteDataResponse,
enableDownvotes,
enableNsfw,
getIdFromString,
- getQueryParams,
myAuth,
setIsoData,
} from "../../utils";
+import { getQueryParams } from "../../utils/helpers/get-query-params";
+import type { QueryParams } from "../../utils/types/query-params";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
import { PostForm } from "./post-form";
communityId?: number;
}
+ type CreatePostData = RouteDataResponse<{
+ communityResponse: GetCommunityResponse;
+ initialCommunitiesRes: ListCommunitiesResponse;
+ }>;
+
function getCreatePostQueryParams() {
return getQueryParams<CreatePostProps>({
communityId: getIdFromString,
RouteComponentProps<Record<string, never>>,
CreatePostState
> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<CreatePostData>(this.context);
state: CreatePostState = {
siteRes: this.isoData.site_res,
loading: true,
// Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) {
- const [communityRes, listCommunitiesRes] = this.isoData.routeData;
+ const { communityResponse: communityRes, initialCommunitiesRes } =
+ this.isoData.routeData;
+
+ this.state = {
+ ...this.state,
+ loading: false,
+ initialCommunitiesRes,
+ isIsomorphic: true,
+ };
if (communityRes?.state === "success") {
const communityChoice: Choice = {
selectedCommunityChoice: communityChoice,
};
}
-
- this.state = {
- ...this.state,
- loading: false,
- initialCommunitiesRes: listCommunitiesRes,
- isIsomorphic: true,
- };
}
}
}
}
- static fetchInitialData({
+ static async fetchInitialData({
client,
query: { communityId },
auth,
- }: InitialFetchRequest<QueryParams<CreatePostProps>>): Promise<
- RequestState<any>
- >[] {
- const promises: Promise<RequestState<any>>[] = [];
+ }: InitialFetchRequest<
+ QueryParams<CreatePostProps>
+ >): Promise<CreatePostData> {
+ const data: CreatePostData = {
+ initialCommunitiesRes: await fetchCommunitiesForOptions(client),
+ communityResponse: { state: "empty" },
+ };
if (communityId) {
const form: GetCommunity = {
id: getIdFromString(communityId),
};
- 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;
}
}
import { BanType, PostFormParams, PurgeType, VoteType } from "../../interfaces";
import { UserService } from "../../services";
import {
- amAdmin,
- amCommunityCreator,
- amMod,
- canAdmin,
- canMod,
- canShare,
futureDaysToUnixTime,
hostname,
- isAdmin,
- isBanned,
isImage,
- isMod,
isVideo,
mdNoImages,
mdToHtml,
numToSI,
relTags,
setupTippy,
- share,
showScores,
} from "../../utils";
+import { canShare } from "../../utils/browser/can-share";
+import { share } from "../../utils/browser/share";
+import { amAdmin } from "../../utils/roles/am-admin";
+import { amCommunityCreator } from "../../utils/roles/am-community-creator";
+import { amMod } from "../../utils/roles/am-mod";
+import { canAdmin } from "../../utils/roles/can-admin";
+import { canMod } from "../../utils/roles/can-mod";
+import { isAdmin } from "../../utils/roles/is-admin";
+import { isBanned } from "../../utils/roles/is-banned";
+import { isMod } from "../../utils/roles/is-mod";
import { Icon, PurgeWarning, Spinner } from "../common/icon";
import { MomentTime } from "../common/moment-time";
import { PictrsImage } from "../common/pictrs-image";
const post = this.postView.post;
return (
- <div className="d-flex justify-content-start flex-wrap text-muted font-weight-bold mb-1">
+ <div className="d-flex align-items-center justify-content-start flex-wrap text-muted font-weight-bold mb-1">
{this.commentsButton}
{canShare() && (
<button
buildCommentsTree,
commentsToFlatNodes,
commentTreeMaxDepth,
- debounce,
editComment,
editWith,
enableDownvotes,
getCommentParentId,
getDepthFromComment,
getIdFromProps,
- isBrowser,
isImage,
myAuth,
restoreScrollPosition,
+ RouteDataResponse,
saveScrollPosition,
setIsoData,
setupTippy,
updateCommunityBlock,
updatePersonBlock,
} from "../../utils";
+import { isBrowser } from "../../utils/browser/is-browser";
+import { debounce } from "../../utils/helpers/debounce";
import { CommentForm } from "../comment/comment-form";
import { CommentNodes } from "../comment/comment-nodes";
import { HtmlTags } from "../common/html-tags";
const commentsShownInterval = 15;
+ type PostData = RouteDataResponse<{
+ postRes: GetPostResponse;
+ commentsRes: GetCommentsResponse;
+ }>;
+
interface PostState {
postId?: number;
commentId?: number;
}
export class Post extends Component<any, PostState> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<PostData>(this.context);
private commentScrollDebounced: () => void;
state: PostState = {
postRes: { state: "empty" },
// Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) {
- const [postRes, commentsRes] = this.isoData.routeData;
+ const { commentsRes, postRes } = this.isoData.routeData;
this.state = {
...this.state,
}
}
- 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;
commentsForm.parent_id = id;
}
- promises.push(client.getPost(postForm));
- promises.push(client.getComments(commentsForm));
-
- return promises;
+ return {
+ postRes: await client.getPost(postForm),
+ commentsRes: await client.getComments(commentsForm),
+ };
}
componentWillUnmount() {
import { HttpService, RequestState } from "../services/HttpService";
import {
Choice,
- QueryParams,
+ RouteDataResponse,
capitalizeFirstLetter,
commentsToFlatNodes,
communityToChoice,
- debounce,
enableDownvotes,
enableNsfw,
fetchCommunities,
fetchUsers,
getIdFromString,
getPageFromString,
- getQueryParams,
- getQueryString,
getUpdatedSearchId,
myAuth,
numToSI,
setIsoData,
showLocal,
} from "../utils";
+import { debounce } from "../utils/helpers/debounce";
+import { getQueryParams } from "../utils/helpers/get-query-params";
+import { getQueryString } from "../utils/helpers/get-query-string";
+import type { QueryParams } from "../utils/types/query-params";
import { CommentNodes } from "./comment/comment-nodes";
import { HtmlTags } from "./common/html-tags";
import { Spinner } from "./common/icon";
page: number;
}
+ type SearchData = RouteDataResponse<{
+ communityResponse: GetCommunityResponse;
+ listCommunitiesResponse: ListCommunitiesResponse;
+ creatorDetailsResponse: GetPersonDetailsResponse;
+ searchResponse: SearchResponse;
+ resolveObjectResponse: ResolveObjectResponse;
+ }>;
+
type FilterType = "creator" | "community";
interface SearchState {
}
export class Search extends Component<any, SearchState> {
- private isoData = setIsoData(this.context);
+ private isoData = setIsoData<SearchData>(this.context);
+
state: SearchState = {
resolveObjectRes: { state: "empty" },
creatorDetailsRes: { state: "empty" },
// Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) {
- const [
- communityRes,
- communitiesRes,
- creatorDetailsRes,
- searchRes,
- resolveObjectRes,
- ] = this.isoData.routeData;
+ const {
+ communityResponse: communityRes,
+ creatorDetailsResponse: creatorDetailsRes,
+ listCommunitiesResponse: communitiesRes,
+ resolveObjectResponse: resolveObjectRes,
+ searchResponse: searchRes,
+ } = this.isoData.routeData;
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,
+ creatorSearchOptions:
+ creatorDetailsRes?.state === "success"
+ ? [personToChoice(creatorDetailsRes.data.person_view)]
+ : [],
+ creatorDetailsRes,
+ };
+ }
+
+ if (communitiesRes?.state === "success") {
this.state = {
...this.state,
- communitySearchOptions: [
- communityToChoice(communityRes.data.community_view),
- ],
+ communitiesRes,
};
}
- if (q) {
+ if (communityRes?.state === "success") {
this.state = {
...this.state,
- searchRes,
- resolveObjectRes,
+ communityRes,
};
}
+
+ if (q !== "") {
+ this.state = {
+ ...this.state,
+ };
+
+ if (searchRes?.state === "success") {
+ this.state = {
+ ...this.state,
+ searchRes,
+ };
+ }
+
+ if (resolveObjectRes?.state === "success") {
+ this.state = {
+ ...this.state,
+ resolveObjectRes,
+ };
+ }
+ }
}
}
saveScrollPosition(this.context);
}
- static fetchInitialData({
+ static async fetchInitialData({
client,
auth,
query: { communityId, creatorId, q, type, sort, listingType, page },
- }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<
- RequestState<any>
- >[] {
- const promises: Promise<RequestState<any>>[] = [];
-
+ }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<SearchData> {
const community_id = getIdFromString(communityId);
+ let communityResponse: RequestState<GetCommunityResponse> = {
+ state: "empty",
+ };
+ let listCommunitiesResponse: RequestState<ListCommunitiesResponse> = {
+ state: "empty",
+ };
if (community_id) {
const getCommunityForm: GetCommunity = {
id: community_id,
auth,
};
- promises.push(client.getCommunity(getCommunityForm));
- promises.push(Promise.resolve({ state: "empty" }));
+
+ 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 = await client.listCommunities(
+ listCommunitiesForm
+ );
}
const creator_id = getIdFromString(creatorId);
+ let creatorDetailsResponse: RequestState<GetPersonDetailsResponse> = {
+ state: "empty",
+ };
if (creator_id) {
const getCreatorForm: GetPersonDetails = {
person_id: creator_id,
auth,
};
- promises.push(client.getPersonDetails(getCreatorForm));
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
+
+ creatorDetailsResponse = await client.getPersonDetails(getCreatorForm);
}
const query = getSearchQueryFromQuery(q);
+ let searchResponse: RequestState<SearchResponse> = { state: "empty" };
+ let resolveObjectResponse: RequestState<ResolveObjectResponse> = {
+ state: "empty",
+ };
+
if (query) {
const form: SearchForm = {
q: query,
};
if (query !== "") {
- promises.push(client.search(form));
+ searchResponse = await client.search(form);
if (auth) {
const resolveObjectForm: ResolveObject = {
q: query,
auth,
};
- promises.push(client.resolveObject(resolveObjectForm));
+ resolveObjectResponse = await client.resolveObject(resolveObjectForm);
}
- } else {
- promises.push(Promise.resolve({ state: "empty" }));
- promises.push(Promise.resolve({ state: "empty" }));
}
}
- return promises;
+ return {
+ communityResponse,
+ creatorDetailsResponse,
+ listCommunitiesResponse,
+ resolveObjectResponse,
+ searchResponse,
+ };
}
get documentTitle(): string {
minLength={1}
/>
<button type="submit" className="btn btn-secondary mr-2 mb-2">
- {this.state.searchRes.state == "loading" ? (
+ {this.state.searchRes.state === "loading" ? (
<Spinner />
) : (
<span>{i18n.t("search")}</span>
CommentReportView,
CommentSortType,
CommentView,
- CommunityModeratorView,
CommunityView,
CustomEmojiView,
GetSiteMetadata,
Language,
LemmyHttp,
MyUserInfo,
- Person,
PersonMentionView,
PersonView,
PostReportView,
import Toastify from "toastify-js";
import { getHttpBase } from "./env";
import { i18n } from "./i18next";
- import { CommentNodeI, DataType, IsoData, VoteType } from "./interfaces";
+ import {
+ CommentNodeI,
+ DataType,
+ IsoData,
+ RouteData,
+ VoteType,
+ } from "./interfaces";
import { HttpService, UserService } from "./services";
+import { isBrowser } from "./utils/browser/is-browser";
+import { debounce } from "./utils/helpers/debounce";
+import { groupBy } from "./utils/helpers/group-by";
+ import { RequestState } from "./services/HttpService";
let Tribute: any;
if (isBrowser()) {
: undefined;
}
-export function canMod(
- creator_id: number,
- mods?: CommunityModeratorView[],
- admins?: PersonView[],
- myUserInfo = UserService.Instance.myUserInfo,
- onSelf = false
-): boolean {
- // You can do moderator actions only on the mods added after you.
- let adminsThenMods =
- admins
- ?.map(a => a.person.id)
- .concat(mods?.map(m => m.moderator.id) ?? []) ?? [];
-
- if (myUserInfo) {
- const myIndex = adminsThenMods.findIndex(
- id => id == myUserInfo.local_user_view.person.id
- );
- if (myIndex == -1) {
- return false;
- } else {
- // onSelf +1 on mod actions not for yourself, IE ban, remove, etc
- adminsThenMods = adminsThenMods.slice(0, myIndex + (onSelf ? 0 : 1));
- return !adminsThenMods.includes(creator_id);
- }
- } else {
- return false;
- }
-}
-
-export function canAdmin(
- creatorId: number,
- admins?: PersonView[],
- myUserInfo = UserService.Instance.myUserInfo,
- onSelf = false
-): boolean {
- return canMod(creatorId, undefined, admins, myUserInfo, onSelf);
-}
-
-export function isMod(
- creatorId: number,
- mods?: CommunityModeratorView[]
-): boolean {
- return mods?.map(m => m.moderator.id).includes(creatorId) ?? false;
-}
-
-export function amMod(
- mods?: CommunityModeratorView[],
- myUserInfo = UserService.Instance.myUserInfo
-): boolean {
- return myUserInfo ? isMod(myUserInfo.local_user_view.person.id, mods) : false;
-}
-
-export function isAdmin(creatorId: number, admins?: PersonView[]): boolean {
- return admins?.map(a => a.person.id).includes(creatorId) ?? false;
-}
-
-export function amAdmin(myUserInfo = UserService.Instance.myUserInfo): boolean {
- return myUserInfo?.local_user_view.person.admin ?? false;
-}
-
-export function amCommunityCreator(
- creator_id: number,
- mods?: CommunityModeratorView[],
- myUserInfo = UserService.Instance.myUserInfo
-): boolean {
- 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;
-}
-
-export function amSiteCreator(
- creator_id: number,
- admins?: PersonView[],
- myUserInfo = UserService.Instance.myUserInfo
-): boolean {
- const myId = myUserInfo?.local_user_view.person.id;
- return myId == admins?.at(0)?.person.id && myId != creator_id;
-}
-
-export function amTopMod(
- mods: CommunityModeratorView[],
- myUserInfo = UserService.Instance.myUserInfo
-): boolean {
- return mods.at(0)?.moderator.id == myUserInfo?.local_user_view.person.id;
-}
-
const imageRegex = /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/;
const videoRegex = /(http)?s?:?(\/\/[^"']*\.(?:mp4|webm))/;
const tldRegex = /([a-z0-9]+\.)*[a-z0-9]+\.[a-z]+/;
}
export function validURL(str: string) {
- return !!new URL(str);
+ try {
+ new URL(str);
+ return true;
+ } catch {
+ return false;
+ }
}
export function validInstanceTLD(str: string) {
return dt === DataType.Post ? "Post" : "Comment";
}
-export function debounce<T extends any[], R>(
- func: (...e: T) => R,
- wait = 1000,
- immediate = false
-) {
- // 'private' variable for instance
- // The returned function will be able to reference this due to closure.
- // Each call to the returned function will share this common timer.
- let timeout: NodeJS.Timeout | null;
-
- // Calling debounce returns a new anonymous function
- return function () {
- // reference the context and args for the setTimeout function
- const args = arguments;
-
- // Should the function be called now? If immediate is true
- // and not already in a timeout then the answer is: Yes
- const callNow = immediate && !timeout;
-
- // This is the basic debounce behavior where you can call this
- // function several times, but it will only execute once
- // [before or after imposing a delay].
- // Each time the returned function is called, the timer starts over.
- clearTimeout(timeout ?? undefined);
-
- // Set the new timeout
- timeout = setTimeout(function () {
- // Inside the timeout function, clear the timeout variable
- // which will let the next execution run when in 'immediate' mode
- timeout = null;
-
- // Check if the function already ran with the immediate flag
- if (!immediate) {
- // Call the original function with apply
- // apply lets you define the 'this' object as well as the arguments
- // (both captured before setTimeout)
- func.apply(this, args);
- }
- }, wait);
-
- // Immediate mode and no wait timer? Execute the function..
- if (callNow) func.apply(this, args);
- } as (...e: T) => R;
-}
-
export async function fetchThemeList(): Promise<string[]> {
return fetch("/css/themelist").then(res => res.json());
}
`;
}
- export function setIsoData(context: any): IsoData {
-export function isBrowser() {
- return typeof window !== "undefined";
-}
-
+ export function setIsoData<T extends RouteData>(context: any): IsoData<T> {
// If its the browser, you need to deserialize the data from the window
if (isBrowser()) {
return window.isoData;
return SHORTNUM_SI_FORMAT.format(value);
}
-export function isBanned(ps: Person): boolean {
- const expires = ps.ban_expires;
- // Add Z to convert from UTC date
- // TODO this check probably isn't necessary anymore
- if (expires) {
- if (ps.banned && new Date(expires + "Z") > new Date()) {
- return true;
- } else {
- return false;
- }
- } else {
- return ps.banned;
- }
-}
-
export function myAuth(): string | undefined {
return UserService.Instance.auth();
}
}
}
-export function canCreateCommunity(
- siteRes: GetSiteResponse,
- myUserInfo = UserService.Instance.myUserInfo
-): boolean {
- const adminOnly = siteRes.site_view.local_site.community_creation_admin_only;
- // TODO: Make this check if user is logged on as well
- return !adminOnly || amAdmin(myUserInfo);
-}
-
export function isPostBlocked(
pv: PostView,
myUserInfo: MyUserInfo | undefined = UserService.Instance.myUserInfo
src: string;
}
-const groupBy = <T>(
- array: T[],
- predicate: (value: T, index: number, array: T[]) => string
-) =>
- array.reduce((acc, value, index, array) => {
- (acc[predicate(value, index, array)] ||= []).push(value);
- return acc;
- }, {} as { [key: string]: T[] });
-
-export type QueryParams<T extends Record<string, any>> = {
- [key in keyof T]?: string;
-};
-
-export function getQueryParams<T extends Record<string, any>>(processors: {
- [K in keyof T]: (param: string) => T[K];
-}): T {
- if (isBrowser()) {
- const searchParams = new URLSearchParams(window.location.search);
-
- return Array.from(Object.entries(processors)).reduce(
- (acc, [key, process]) => ({
- ...acc,
- [key]: process(searchParams.get(key)),
- }),
- {} as T
- );
- }
-
- return {} as T;
-}
-
-export function getQueryString<T extends Record<string, string | undefined>>(
- obj: T
-) {
- return Object.entries(obj)
- .filter(([, val]) => val !== undefined && val !== null)
- .reduce(
- (acc, [key, val], index) => `${acc}${index > 0 ? "&" : ""}${key}=${val}`,
- "?"
- );
-}
-
export function isAuthPath(pathname: string) {
return /create_.*|inbox|settings|admin|reports|registration_applications/g.test(
pathname
);
}
-export function canShare() {
- return isBrowser() && !!navigator.canShare;
-}
-
-export function share(shareData: ShareData) {
- if (isBrowser()) {
- navigator.share(shareData);
- }
-}
-
export function newVote(voteType: VoteType, myVote?: number): number {
if (voteType == VoteType.Upvote) {
return myVote == 1 ? 0 : 1;
return myVote == -1 ? 0 : -1;
}
}
-
-function sleep(millis: number): Promise<void> {
- return new Promise(resolve => setTimeout(resolve, millis));
-}
-
-/**
- * Polls / repeatedly runs a promise, every X milliseconds
- */
-export async function poll(promiseFn: any, millis: number) {
- if (window.document.visibilityState !== "hidden") {
- await promiseFn();
- }
- await sleep(millis);
- return poll(promiseFn, millis);
-}
+
+ export type RouteDataResponse<T extends Record<string, any>> = {
+ [K in keyof T]: RequestState<T[K]>;
+ };