]> Untitled Git - lemmy-ui.git/commitdiff
Merge branch 'main' into route-data-refactor
authorabias <abias1122@gmail.com>
Fri, 16 Jun 2023 22:17:17 +0000 (18:17 -0400)
committerabias <abias1122@gmail.com>
Fri, 16 Jun 2023 22:17:17 +0000 (18:17 -0400)
1  2 
package.json
src/shared/components/community/community.tsx
src/shared/components/home/admin-settings.tsx
src/shared/components/home/home.tsx
src/shared/components/post/create-post.tsx
src/shared/components/post/post.tsx
src/shared/utils.ts

diff --combined package.json
index df264eb03ace26314e24d12bc89bcc6f3b5655cd,b7c48c79b070276ac94f7e6c5d536b9abaf393c1..20e5645d7a6429f3f8ff2526f1ef5577c76afca5
      "start": "yarn build:dev --watch"
    },
    "lint-staged": {
 -    "*.{ts,tsx,js}": ["prettier --write", "eslint --fix"],
 -    "*.{css, scss}": ["prettier --write"],
 -    "package.json": ["sortpack"]
 +    "*.{ts,tsx,js}": [
 +      "prettier --write",
 +      "eslint --fix"
 +    ],
 +    "*.{css, scss}": [
 +      "prettier --write"
 +    ],
 +    "package.json": [
 +      "sortpack"
 +    ]
    },
    "dependencies": {
      "@babel/plugin-proposal-decorators": "^7.21.0",
@@@ -61,7 -54,7 +61,7 @@@
      "inferno-server": "^8.1.1",
      "isomorphic-cookie": "^1.2.4",
      "jwt-decode": "^3.1.2",
-     "lemmy-js-client": "0.17.2-rc.24",
+     "lemmy-js-client": "0.18.0-rc.1",
      "lodash": "^4.17.21",
      "markdown-it": "^13.0.1",
      "markdown-it-container": "^3.0.0",
index 6b4eecff64bc19082b232e2571d7ae31eb2713bd,f2d7ad729a8e492b2944205314ae6e47e88c8dca..6f3c9112f782231e9af853b199414b2174541a10
@@@ -63,7 -63,6 +63,7 @@@ import { FirstLoadService } from "../..
  import { HttpService, RequestState } from "../../services/HttpService";
  import {
    QueryParams,
 +  RouteDataResponse,
    commentsToFlatNodes,
    communityRSSUrl,
    editComment,
@@@ -101,12 -100,6 +101,12 @@@ import { SiteSidebar } from "../home/si
  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>;
@@@ -147,7 -140,7 +147,7 @@@ export class Community extends Componen
    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 {
            community_view={res.community_view}
            moderators={res.moderators}
            admins={site_res.admins}
-           online={res.online}
            enableNsfw={enableNsfw(site_res)}
            editable
            allLanguages={site_res.all_languages}
      const blockCommunityRes = await HttpService.client.blockCommunity(form);
      if (blockCommunityRes.state == "success") {
        updateCommunityBlock(blockCommunityRes.data);
+       this.setState(s => {
+         if (s.communityRes.state == "success") {
+           s.communityRes.data.community_view.blocked =
+             blockCommunityRes.data.blocked;
+         }
+       });
      }
    }
  
index 91ba727f2bae7292b83124d750d190a15e2acca8,302e96bdf85e4a3798ee9af1e5c28e8c2305450d..ad7ac931aead7fbcfb5298535f5eba9f7798d737
@@@ -14,7 -14,6 +14,7 @@@ import { InitialFetchRequest } from "..
  import { FirstLoadService } from "../../services/FirstLoadService";
  import { HttpService, RequestState } from "../../services/HttpService";
  import {
 +  RouteDataResponse,
    capitalizeFirstLetter,
    fetchThemeList,
    myAuthRequired,
@@@ -33,11 -32,6 +33,11 @@@ import RateLimitForm from "./rate-limit
  import { SiteForm } from "./site-form";
  import { TaglineForm } from "./tagline-form";
  
 +type AdminSettingsData = RouteDataResponse<{
 +  bannedRes: BannedPersonsResponse;
 +  instancesRes: GetFederatedInstancesResponse;
 +}>;
 +
  interface AdminSettingsState {
    siteRes: GetSiteResponse;
    banned: PersonView[];
    instancesRes: RequestState<GetFederatedInstancesResponse>;
    bannedRes: RequestState<BannedPersonsResponse>;
    leaveAdminTeamRes: RequestState<GetSiteResponse>;
+   emojiLoading: boolean;
+   loading: boolean;
    themeList: string[];
    isIsomorphic: boolean;
  }
  
  export class AdminSettings extends Component<any, AdminSettingsState> {
 -  private isoData = setIsoData(this.context);
 +  private isoData = setIsoData<AdminSettingsData>(this.context);
    state: AdminSettingsState = {
      siteRes: this.isoData.site_res,
      banned: [],
@@@ -58,6 -54,8 +60,8 @@@
      bannedRes: { state: "empty" },
      instancesRes: { state: "empty" },
      leaveAdminTeamRes: { state: "empty" },
+     emojiLoading: false,
+     loading: false,
      themeList: [],
      isIsomorphic: false,
    };
@@@ -72,8 -70,7 +76,8 @@@
  
      // Only fetch the data if coming from another route
      if (FirstLoadService.isFirstLoad) {
 -      const [bannedRes, instancesRes] = this.isoData.routeData;
 +      const { bannedRes, instancesRes } = this.isoData.routeData;
 +
        this.state = {
          ...this.state,
          bannedRes,
      }
    }
  
 -  async fetchData() {
 -    this.setState({
 -      bannedRes: { state: "loading" },
 -      instancesRes: { state: "loading" },
 -      themeList: [],
 -      loading: true,
 -    });
 -
 -    const auth = myAuthRequired();
 -
 -    const [bannedRes, instancesRes, themeList] = await Promise.all([
 -      HttpService.client.getBannedPersons({ auth }),
 -      HttpService.client.getFederatedInstances({ auth }),
 -      fetchThemeList(),
 -    ]);
 -
 -    this.setState({
 -      bannedRes,
 -      instancesRes,
 -      themeList,
 -      loading: false,
 -    });
 -  }
 -
 -  static fetchInitialData({
 +  static async fetchInitialData({
      auth,
      client,
 -  }: 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 {
 +      bannedRes: await client.getBannedPersons({
 +        auth: auth as string,
 +      }),
 +      instancesRes: await client.getFederatedInstances({
 +        auth: auth as string,
 +      }),
 +    };
    }
  
    async componentDidMount() {
                        onSaveSite={this.handleEditSite}
                        siteRes={this.state.siteRes}
                        themeList={this.state.themeList}
+                       loading={this.state.loading}
                      />
                    </div>
                    <div className="col-12 col-md-6">
                      this.state.siteRes.site_view.local_site_rate_limit
                    }
                    onSaveSite={this.handleEditSite}
+                   loading={this.state.loading}
                  />
                ),
              },
                    <TaglineForm
                      taglines={this.state.siteRes.taglines}
                      onSaveSite={this.handleEditSite}
+                     loading={this.state.loading}
                    />
                  </div>
                ),
                      onCreate={this.handleCreateEmoji}
                      onDelete={this.handleDeleteEmoji}
                      onEdit={this.handleEditEmoji}
+                     loading={this.state.emojiLoading}
                    />
                  </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 (
        <>
    }
  
    async handleEditSite(form: EditSite) {
+     this.setState({ loading: true });
      const editRes = await HttpService.client.editSite(form);
  
      if (editRes.state === "success") {
        toast(i18n.t("site_saved"));
      }
  
+     this.setState({ loading: false });
      return editRes;
    }
  
    }
  
    async handleEditEmoji(form: EditCustomEmoji) {
+     this.setState({ emojiLoading: true });
      const res = await HttpService.client.editCustomEmoji(form);
      if (res.state === "success") {
        updateEmojiDataModel(res.data.custom_emoji);
      }
+     this.setState({ emojiLoading: false });
    }
  
    async handleDeleteEmoji(form: DeleteCustomEmoji) {
+     this.setState({ emojiLoading: true });
      const res = await HttpService.client.deleteCustomEmoji(form);
      if (res.state === "success") {
        removeFromEmojiDataModel(res.data.id);
      }
+     this.setState({ emojiLoading: false });
    }
  
    async handleCreateEmoji(form: CreateCustomEmoji) {
+     this.setState({ emojiLoading: true });
      const res = await HttpService.client.createCustomEmoji(form);
      if (res.state === "success") {
        updateEmojiDataModel(res.data.custom_emoji);
      }
+     this.setState({ emojiLoading: false });
    }
  }
index ed379b846af0374628291939b74d76a565c119f1,215075d8fbf3d87da9937bb18b0fa473d56d96ba..67b02e460660dcf0e4db0d72156aca70fb4a4993
@@@ -77,7 -77,6 +77,7 @@@ import 
    QueryParams,
    relTags,
    restoreScrollPosition,
 +  RouteDataResponse,
    saveScrollPosition,
    setIsoData,
    setupTippy,
@@@ -118,45 -117,6 +118,45 @@@ interface HomeProps 
    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;
  }
@@@ -216,7 -176,7 +216,7 @@@ const LinkButton = (
  );
  
  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}
        siteRes: {
          site_view: { counts, site },
          admins,
-         online,
        },
        showSubscribedMobile,
        showTrendingMobile,
                site={site}
                admins={admins}
                counts={counts}
-               online={online}
                showLocal={showLocal(this.isoData)}
              />
            )}
        siteRes: {
          site_view: { counts, site },
          admins,
-         online,
        },
      } = this.state;
  
              site={site}
              admins={admins}
              counts={counts}
-             online={online}
              showLocal={showLocal(this.isoData)}
            />
            {this.hasFollows && (
      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({
index 05279c35d0d7a47249e3534d943d52e1ecb7b472,c7597917740486cd58eae285e54232887737b893..046bc2de5e2c4dd0eeaccf15ae2388ffbde6c55c
@@@ -3,7 -3,6 +3,7 @@@ import { RouteComponentProps } from "in
  import {
    CreatePost as CreatePostI,
    GetCommunity,
 +  GetCommunityResponse,
    GetSiteResponse,
    ListCommunitiesResponse,
  } from "lemmy-js-client";
@@@ -18,7 -17,6 +18,7 @@@ import 
  import {
    Choice,
    QueryParams,
 +  RouteDataResponse,
    enableDownvotes,
    enableNsfw,
    getIdFromString,
@@@ -34,11 -32,6 +34,11 @@@ export interface CreatePostProps 
    communityId?: number;
  }
  
 +type CreatePostData = RouteDataResponse<{
 +  communityResponse: GetCommunityResponse;
 +  initialCommunitiesRes: ListCommunitiesResponse;
 +}>;
 +
  function getCreatePostQueryParams() {
    return getQueryParams<CreatePostProps>({
      communityId: getIdFromString,
@@@ -61,7 -54,7 +61,7 @@@ export class CreatePost extends Compone
    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,
 -      };
      }
    }
  
      if (res.state === "success") {
        const postId = res.data.post_view.post.id;
        this.props.history.replace(`/post/${postId}`);
+     } else {
+       this.setState({
+         loading: false,
+       });
      }
    }
  
 -  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;
    }
  }
index 501c06dcb675011132bf4a05181e72ef0e766483,b602f309285cab92ffcdaa40ed7029c371fd916f..7eff1b4a4496d2e7115be1851df6e8de4aac59db
@@@ -77,7 -77,6 +77,7 @@@ import 
    isImage,
    myAuth,
    restoreScrollPosition,
 +  RouteDataResponse,
    saveScrollPosition,
    setIsoData,
    setupTippy,
@@@ -94,11 -93,6 +94,11 @@@ import { PostListing } from "./post-lis
  
  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() {
              community_view={res.data.community_view}
              moderators={res.data.moderators}
              admins={this.state.siteRes.admins}
-             online={res.data.online}
              enableNsfw={enableNsfw(this.state.siteRes)}
              showIcon
              allLanguages={this.state.siteRes.all_languages}
  
    async handleBlockCommunity(form: BlockCommunity) {
      const blockCommunityRes = await HttpService.client.blockCommunity(form);
-     // TODO Probably isn't necessary
-     this.setState(s => {
-       if (
-         s.postRes.state == "success" &&
-         blockCommunityRes.state == "success"
-       ) {
-         s.postRes.data.community_view = blockCommunityRes.data.community_view;
-       }
-       return s;
-     });
      if (blockCommunityRes.state == "success") {
        updateCommunityBlock(blockCommunityRes.data);
+       this.setState(s => {
+         if (s.postRes.state == "success") {
+           s.postRes.data.community_view.blocked =
+             blockCommunityRes.data.blocked;
+         }
+       });
      }
    }
  
diff --combined src/shared/utils.ts
index 97d05fca56933ffe9f9cef648c1f86d630a8eaba,df7673a47b6a1e8e860a41b764bf70c58745b464..7007a214ffdeb67a48338ae3b6ed2b5dae29ee38
@@@ -42,16 -42,9 +42,16 @@@ import moment from "moment"
  import tippy from "tippy.js";
  import Toastify from "toastify-js";
  import { getHttpBase } from "./env";
- import { i18n, languages } from "./i18next";
+ 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 { RequestState } from "./services/HttpService";
  
  let Tribute: any;
  if (isBrowser()) {
@@@ -323,6 -316,7 +323,7 @@@ export function amTopMod
  
  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 isImage(url: string) {
    return imageRegex.test(url);
@@@ -336,6 -330,10 +337,10 @@@ export function validURL(str: string) 
    return !!new URL(str);
  }
  
+ export function validInstanceTLD(str: string) {
+   return tldRegex.test(str);
+ }
  export function communityRSSUrl(actorId: string, sort: string): string {
    const url = new URL(actorId);
    return `${url.origin}/feeds${url.pathname}.xml?sort=${sort}`;
@@@ -406,31 -404,6 +411,6 @@@ export function debounce<T extends any[
    } as (...e: T) => R;
  }
  
- export function getLanguages(
-   override?: string,
-   myUserInfo = UserService.Instance.myUserInfo
- ): string[] {
-   const myLang = myUserInfo?.local_user_view.local_user.interface_language;
-   const lang = override || myLang || "browser";
-   if (lang == "browser" && isBrowser()) {
-     return getBrowserLanguages();
-   } else {
-     return [lang];
-   }
- }
- function getBrowserLanguages(): string[] {
-   // Intersect lemmy's langs, with the browser langs
-   const langs = languages ? languages.map(l => l.code) : ["en"];
-   // NOTE, mobile browsers seem to be missing this list, so append en
-   const allowedLangs = navigator.languages
-     .concat("en")
-     .filter(v => langs.includes(v));
-   return allowedLangs;
- }
  export async function fetchThemeList(): Promise<string[]> {
    return fetch("/css/themelist").then(res => res.json());
  }
@@@ -1172,7 -1145,7 +1152,7 @@@ export function isBrowser() 
    return typeof window !== "undefined";
  }
  
 -export function setIsoData(context: any): IsoData {
 +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;
@@@ -1283,7 -1256,7 +1263,7 @@@ export function personSelectName(
  
  export function initializeSite(site?: GetSiteResponse) {
    UserService.Instance.myUserInfo = site?.my_user;
-   i18n.changeLanguage(getLanguages()[0]);
+   i18n.changeLanguage();
    if (site) {
      setupEmojiDataModel(site.custom_emojis ?? []);
    }
@@@ -1502,10 -1475,6 +1482,10 @@@ export function newVote(voteType: VoteT
    }
  }
  
 +export type RouteDataResponse<T extends Record<string, any>> = {
 +  [K in keyof T]: RequestState<T[K]>;
 +};
 +
  function sleep(millis: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, millis));
  }