]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/utils.ts
Fix loading indicator on search page (fixes #443) (#606)
[lemmy-ui.git] / src / shared / utils.ts
index 887030fbc0ed19a04d7857d1471aa87fa158540e..df26970d13edca89f4c39e69cd901c5ba473e916 100644 (file)
-import "moment/locale/es";
-import "moment/locale/el";
-import "moment/locale/eu";
-import "moment/locale/eo";
-import "moment/locale/de";
-import "moment/locale/zh-cn";
-import "moment/locale/fr";
-import "moment/locale/sv";
-import "moment/locale/ru";
-import "moment/locale/nl";
-import "moment/locale/it";
-import "moment/locale/fi";
-import "moment/locale/ca";
-import "moment/locale/fa";
-import "moment/locale/pl";
-import "moment/locale/pt-br";
-import "moment/locale/ja";
-import "moment/locale/ka";
-import "moment/locale/hi";
-import "moment/locale/gl";
-import "moment/locale/tr";
-import "moment/locale/hu";
-import "moment/locale/uk";
-import "moment/locale/sq";
-import "moment/locale/km";
-import "moment/locale/ga";
-import "moment/locale/sr";
-import "moment/locale/ko";
-import "moment/locale/da";
-import "moment/locale/hr";
-
+import emojiShortName from "emoji-short-name";
 import {
-  UserOperation,
+  BlockCommunityResponse,
+  BlockPersonResponse,
+  CommentReportView,
   CommentView,
-  UserSafeSettings,
-  SortType,
+  CommunityBlockView,
+  CommunityView,
+  GetSiteMetadata,
+  GetSiteResponse,
+  LemmyHttp,
+  LemmyWebsocket,
   ListingType,
-  SearchType,
-  WebSocketResponse,
-  WebSocketJsonResponse,
-  Search,
-  SearchResponse,
+  MyUserInfo,
+  PersonBlockView,
+  PersonSafe,
+  PersonViewSafe,
+  PostReportView,
   PostView,
   PrivateMessageView,
-  LemmyWebsocket,
-  UserViewSafe,
-  CommunityView,
+  RegistrationApplicationView,
+  Search,
+  SearchResponse,
+  SearchType,
+  SortType,
+  UserOperation,
+  WebSocketJsonResponse,
+  WebSocketResponse,
 } from "lemmy-js-client";
-
+import markdown_it from "markdown-it";
+import markdown_it_container from "markdown-it-container";
+import markdown_it_footnote from "markdown-it-footnote";
+import markdown_it_html5_embed from "markdown-it-html5-embed";
+import markdown_it_sub from "markdown-it-sub";
+import markdown_it_sup from "markdown-it-sup";
+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 { httpBase } from "./env";
+import { i18n, languages } from "./i18next";
 import {
+  CommentNode as CommentNodeI,
   CommentSortType,
   DataType,
   IsoData,
-  CommentNode as CommentNodeI,
 } from "./interfaces";
 import { UserService, WebSocketService } from "./services";
+
 var Tribute: any;
 if (isBrowser()) {
   Tribute = require("tributejs");
 }
-import markdown_it from "markdown-it";
-import markdown_it_sub from "markdown-it-sub";
-import markdown_it_sup from "markdown-it-sup";
-import markdown_it_container from "markdown-it-container";
-import emojiShortName from "emoji-short-name";
-import Toastify from "toastify-js";
-import tippy from "tippy.js";
-import moment from "moment";
-import { Subscription } from "rxjs";
-import { retryWhen, delay, take } from "rxjs/operators";
-import { i18n } from "./i18next";
 
 export const wsClient = new LemmyWebsocket();
 
-export const favIconUrl = "/static/assets/favicon.svg";
-export const favIconPngUrl = "/static/assets/apple-touch-icon.png";
+export const favIconUrl = "/static/assets/icons/favicon.svg";
+export const favIconPngUrl = "/static/assets/icons/apple-touch-icon.png";
 // TODO
 // export const defaultFavIcon = `${window.location.protocol}//${window.location.host}${favIconPngUrl}`;
 export const repoUrl = "https://github.com/LemmyNet";
-export const joinLemmyUrl = "https://join.lemmy.ml";
-export const supportLemmyUrl = "https://join.lemmy.ml/sponsors";
-export const docsUrl = "https://join.lemmy.ml/docs/en/index.html";
-export const helpGuideUrl = "https://join.lemmy.ml/docs/en/about/guide.html"; // TODO find a way to redirect to the non-en folder
-export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
+export const joinLemmyUrl = "https://join-lemmy.org";
+export const donateLemmyUrl = `${joinLemmyUrl}/donate`;
+export const docsUrl = `${joinLemmyUrl}/docs/en/index.html`;
+export const helpGuideUrl = `${joinLemmyUrl}/docs/en/about/guide.html`; // TODO find a way to redirect to the non-en folder
+export const markdownHelpUrl = `${helpGuideUrl}#using-markdown`;
 export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
-export const archiveUrl = "https://archive.is";
-export const elementUrl = "https://element.io/";
+export const archiveTodayUrl = "https://archive.today";
+export const ghostArchiveUrl = "https://ghostarchive.org";
+export const webArchiveUrl = "https://web.archive.org";
+export const elementUrl = "https://element.io";
 
 export const postRefetchSeconds: number = 60 * 1000;
 export const fetchLimit = 20;
 export const mentionDropdownFetchLimit = 10;
 
-export const languages = [
-  { code: "ca", name: "Català" },
-  { code: "en", name: "English" },
-  { code: "el", name: "Ελληνικά" },
-  { code: "eu", name: "Euskara" },
-  { code: "eo", name: "Esperanto" },
-  { code: "es", name: "Español" },
-  { code: "da", name: "Dansk" },
-  { code: "de", name: "Deutsch" },
-  { code: "ga", name: "Gaeilge" },
-  { code: "gl", name: "Galego" },
-  { code: "hr", name: "hrvatski" },
-  { code: "hu", name: "Magyar Nyelv" },
-  { code: "ka", name: "ქართული ენა" },
-  { code: "ko", name: "한국어" },
-  { code: "km", name: "ភាសាខ្មែរ" },
-  { code: "hi", name: "मानक हिन्दी" },
-  { code: "fa", name: "فارسی" },
-  { code: "ja", name: "日本語" },
-  { code: "oc", name: "Occitan" },
-  { code: "pl", name: "Polski" },
-  { code: "pt_BR", name: "Português Brasileiro" },
-  { code: "zh", name: "中文" },
-  { code: "fi", name: "Suomi" },
-  { code: "fr", name: "Français" },
-  { code: "sv", name: "Svenska" },
-  { code: "sq", name: "Shqip" },
-  { code: "sr_Latn", name: "srpski" },
-  { code: "th", name: "ภาษาไทย" },
-  { code: "tr", name: "Türkçe" },
-  { code: "uk", name: "Українська Mова" },
-  { code: "ru", name: "Русский" },
-  { code: "nl", name: "Nederlands" },
-  { code: "it", name: "Italiano" },
-];
-
-export const themes = [
-  "litera",
-  "materia",
-  "minty",
-  "solar",
-  "united",
-  "cyborg",
-  "darkly",
-  "journal",
-  "sketchy",
-  "vaporwave",
-  "vaporwave-dark",
-  "i386",
-  "litely",
-];
+export const relTags = "noopener nofollow";
 
 const DEFAULT_ALPHABET =
   "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@@ -179,12 +114,23 @@ export function wsUserOp(msg: any): UserOperation {
 }
 
 export const md = new markdown_it({
-  html: false,
+  html: true,
   linkify: true,
   typographer: true,
 })
   .use(markdown_it_sub)
   .use(markdown_it_sup)
+  .use(markdown_it_footnote)
+  .use(markdown_it_html5_embed, {
+    html5embed: {
+      useImageSyntax: true, // Enables video/audio embed with ![]() syntax (default)
+      attributes: {
+        audio: 'controls preload="metadata"',
+        video:
+          'width="100%" max-height="100%" controls loop preload="metadata"',
+      },
+    },
+  })
   .use(markdown_it_container, "spoiler", {
     validate: function (params: any) {
       return params.trim().match(/^spoiler\s+(.*)$/);
@@ -238,15 +184,25 @@ export function getUnixTime(text: string): number {
   return text ? new Date(text).getTime() / 1000 : undefined;
 }
 
+export function futureDaysToUnixTime(days: number): number {
+  return days
+    ? Math.trunc(
+        new Date(Date.now() + 1000 * 60 * 60 * 24 * days).getTime() / 1000
+      )
+    : undefined;
+}
+
 export function canMod(
-  user: UserSafeSettings,
+  myUserInfo: MyUserInfo,
   modIds: number[],
   creator_id: number,
   onSelf = false
 ): boolean {
   // You can do moderator actions only on the mods added after you.
-  if (user) {
-    let yourIndex = modIds.findIndex(id => id == user.id);
+  if (myUserInfo) {
+    let yourIndex = modIds.findIndex(
+      id => id == myUserInfo.local_user_view.person.id
+    );
     if (yourIndex == -1) {
       return false;
     } else {
@@ -263,10 +219,8 @@ export function isMod(modIds: number[], creator_id: number): boolean {
   return modIds.includes(creator_id);
 }
 
-const imageRegex = new RegExp(
-  /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/
-);
-const videoRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:mp4))`);
+const imageRegex = /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/;
+const videoRegex = /(http)?s?:?(\/\/[^"']*\.(?:mp4|webm))/;
 
 export function isImage(url: string) {
   return imageRegex.test(url);
@@ -286,7 +240,8 @@ export function communityRSSUrl(actorId: string, sort: string): string {
 }
 
 export function validEmail(email: string) {
-  let re = /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
+  let re =
+    /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
   return re.test(String(email).toLowerCase());
 }
 
@@ -318,10 +273,12 @@ export function routeSearchTypeToEnum(type: string): SearchType {
   return SearchType[type];
 }
 
-export async function getPageTitle(url: string) {
-  let res = await fetch(`/iframely/oembed?url=${url}`).then(res => res.json());
-  let title = await res.title;
-  return title;
+export async function getSiteMetadata(url: string) {
+  let form: GetSiteMetadata = {
+    url,
+  };
+  let client = new LemmyHttp(httpBase);
+  return client.getSiteMetadata(form);
 }
 
 export function debounce(func: any, wait = 1000, immediate = false) {
@@ -365,96 +322,37 @@ export function debounce(func: any, wait = 1000, immediate = false) {
   };
 }
 
-// TODO
-export function getLanguage(override?: string): string {
-  let user = UserService.Instance.user;
-  let lang = override || (user && user.lang ? user.lang : "browser");
+export function getLanguages(override?: string): string[] {
+  let myUserInfo = UserService.Instance.myUserInfo;
+  let lang =
+    override ||
+    (myUserInfo?.local_user_view.local_user.lang
+      ? myUserInfo.local_user_view.local_user.lang
+      : "browser");
 
   if (lang == "browser" && isBrowser()) {
-    return getBrowserLanguage();
+    return getBrowserLanguages();
   } else {
-    return lang;
+    return [lang];
   }
 }
 
-// TODO
-export function getBrowserLanguage(): string {
-  return navigator.language;
-}
-
-export function getMomentLanguage(): string {
-  let lang = getLanguage();
-  if (lang.startsWith("zh")) {
-    lang = "zh-cn";
-  } else if (lang.startsWith("sv")) {
-    lang = "sv";
-  } else if (lang.startsWith("fr")) {
-    lang = "fr";
-  } else if (lang.startsWith("de")) {
-    lang = "de";
-  } else if (lang.startsWith("ru")) {
-    lang = "ru";
-  } else if (lang.startsWith("es")) {
-    lang = "es";
-  } else if (lang.startsWith("eo")) {
-    lang = "eo";
-  } else if (lang.startsWith("nl")) {
-    lang = "nl";
-  } else if (lang.startsWith("it")) {
-    lang = "it";
-  } else if (lang.startsWith("fi")) {
-    lang = "fi";
-  } else if (lang.startsWith("ca")) {
-    lang = "ca";
-  } else if (lang.startsWith("fa")) {
-    lang = "fa";
-  } else if (lang.startsWith("pl")) {
-    lang = "pl";
-  } else if (lang.startsWith("pt")) {
-    lang = "pt-br";
-  } else if (lang.startsWith("ja")) {
-    lang = "ja";
-  } else if (lang.startsWith("ka")) {
-    lang = "ka";
-  } else if (lang.startsWith("hi")) {
-    lang = "hi";
-  } else if (lang.startsWith("el")) {
-    lang = "el";
-  } else if (lang.startsWith("eu")) {
-    lang = "eu";
-  } else if (lang.startsWith("gl")) {
-    lang = "gl";
-  } else if (lang.startsWith("tr")) {
-    lang = "tr";
-  } else if (lang.startsWith("hu")) {
-    lang = "hu";
-  } else if (lang.startsWith("uk")) {
-    lang = "uk";
-  } else if (lang.startsWith("sq")) {
-    lang = "sq";
-  } else if (lang.startsWith("km")) {
-    lang = "km";
-  } else if (lang.startsWith("ga")) {
-    lang = "ga";
-  } else if (lang.startsWith("sr")) {
-    lang = "sr";
-  } else if (lang.startsWith("ko")) {
-    lang = "ko";
-  } else if (lang.startsWith("da")) {
-    lang = "da";
-  } else if (lang.startsWith("oc")) {
-    lang = "oc";
-  } else if (lang.startsWith("hr")) {
-    lang = "hr";
-  } else if (lang.startsWith("th")) {
-    lang = "th";
-  } else {
-    lang = "en";
-  }
-  return lang;
+function getBrowserLanguages(): string[] {
+  // Intersect lemmy's langs, with the browser langs
+  let langs = languages ? languages.map(l => l.code) : ["en"];
+
+  // NOTE, mobile browsers seem to be missing this list, so append en
+  let allowedLangs = navigator.languages
+    .concat("en")
+    .filter(v => langs.includes(v));
+  return allowedLangs;
 }
 
-export function setTheme(theme: string, forceReload = false) {
+export async function fetchThemeList(): Promise<string[]> {
+  return fetch("/css/themelist").then(res => res.json());
+}
+
+export async function setTheme(theme: string, forceReload = false) {
   if (!isBrowser()) {
     return;
   }
@@ -466,9 +364,11 @@ export function setTheme(theme: string, forceReload = false) {
     theme = "darkly";
   }
 
+  let themeList = await fetchThemeList();
+
   // Unload all the other themes
-  for (var i = 0; i < themes.length; i++) {
-    let styleSheet = document.getElementById(themes[i]);
+  for (var i = 0; i < themeList.length; i++) {
+    let styleSheet = document.getElementById(themeList[i]);
     if (styleSheet) {
       styleSheet.setAttribute("disabled", "disabled");
     }
@@ -480,7 +380,8 @@ export function setTheme(theme: string, forceReload = false) {
   document.getElementById("default-dark")?.setAttribute("disabled", "disabled");
 
   // Load the theme dynamically
-  let cssLoc = `/static/assets/css/themes/${theme}.min.css`;
+  let cssLoc = `/css/themes/${theme}.css`;
+
   loadCss(theme, cssLoc);
   document.getElementById(theme).removeAttribute("disabled");
 }
@@ -508,21 +409,28 @@ export function objectFlip(obj: any) {
 
 export function showAvatars(): boolean {
   return (
-    (UserService.Instance.user && UserService.Instance.user.show_avatars) ||
-    !UserService.Instance.user
+    UserService.Instance.myUserInfo?.local_user_view.local_user.show_avatars ||
+    !UserService.Instance.myUserInfo
+  );
+}
+
+export function showScores(): boolean {
+  return (
+    UserService.Instance.myUserInfo?.local_user_view.local_user.show_scores ||
+    !UserService.Instance.myUserInfo
   );
 }
 
 export function isCakeDay(published: string): boolean {
   // moment(undefined) or moment.utc(undefined) returns the current date/time
   // moment(null) or moment.utc(null) returns null
-  const userCreationDate = moment.utc(published || null).local();
+  const createDate = moment.utc(published || null).local();
   const currentDate = moment(new Date());
 
   return (
-    userCreationDate.date() === currentDate.date() &&
-    userCreationDate.month() === currentDate.month() &&
-    userCreationDate.year() !== currentDate.year()
+    createDate.date() === currentDate.date() &&
+    createDate.month() === currentDate.month() &&
+    createDate.year() !== currentDate.year()
   );
 }
 
@@ -584,6 +492,7 @@ export function messageToastify(info: NotifyInfo, router: any) {
       gravity: "top",
       position: "right",
       duration: 5000,
+      escapeMarkup: false,
       onClick: () => {
         if (toast) {
           toast.hideToast();
@@ -627,18 +536,21 @@ export function notifyPrivateMessage(pmv: PrivateMessageView, router: any) {
 function notify(info: NotifyInfo, router: any) {
   messageToastify(info, router);
 
-  if (Notification.permission !== "granted") Notification.requestPermission();
-  else {
-    var notification = new Notification(info.name, {
-      icon: info.icon,
-      body: info.body,
-    });
+  // TODO absolute nightmare bug, but notifs are currently broken.
+  // Notification.new will try to do a browser fetch ???
 
-    notification.onclick = (ev: Event): any => {
-      ev.preventDefault();
-      router.history.push(info.link);
-    };
-  }
+  // if (Notification.permission !== "granted") Notification.requestPermission();
+  // else {
+  //   var notification = new Notification(info.name, {
+  //     icon: info.icon,
+  //     body: info.body,
+  //   });
+
+  //   notification.onclick = (ev: Event): any => {
+  //     ev.preventDefault();
+  //     router.history.push(info.link);
+  //   };
+  // }
 }
 
 export function setupTribute() {
@@ -666,15 +578,15 @@ export function setupTribute() {
         // menuItemLimit: mentionDropdownFetchLimit,
         menuShowMinLength: 2,
       },
-      // Users
+      // Persons
       {
         trigger: "@",
         selectTemplate: (item: any) => {
-          let it: UserTribute = item.original;
-          return `[${it.key}](${it.view.user.actor_id})`;
+          let it: PersonTribute = item.original;
+          return `[${it.key}](${it.view.person.actor_id})`;
         },
-        values: (text: string, cb: (users: UserTribute[]) => any) => {
-          userSearch(text, (users: UserTribute[]) => cb(users));
+        values: (text: string, cb: (persons: PersonTribute[]) => any) => {
+          personSearch(text, (persons: PersonTribute[]) => cb(persons));
         },
         allowSpaces: false,
         autocompleteMode: true,
@@ -721,17 +633,18 @@ export function setupTippy() {
   }
 }
 
-interface UserTribute {
+interface PersonTribute {
   key: string;
-  view: UserViewSafe;
+  view: PersonViewSafe;
 }
 
-function userSearch(text: string, cb: (users: UserTribute[]) => any) {
+function personSearch(text: string, cb: (persons: PersonTribute[]) => any) {
   if (text) {
     let form: Search = {
       q: text,
       type_: SearchType.Users,
       sort: SortType.TopAll,
+      listing_type: ListingType.All,
       page: 1,
       limit: mentionDropdownFetchLimit,
       auth: authField(false),
@@ -739,20 +652,20 @@ function userSearch(text: string, cb: (users: UserTribute[]) => any) {
 
     WebSocketService.Instance.send(wsClient.search(form));
 
-    let userSub = WebSocketService.Instance.subject.subscribe(
+    let personSub = WebSocketService.Instance.subject.subscribe(
       msg => {
         let res = wsJsonToRes(msg);
         if (res.op == UserOperation.Search) {
           let data = res.data as SearchResponse;
-          let users: UserTribute[] = data.users.map(uv => {
-            let tribute: UserTribute = {
-              key: `@${uv.user.name}@${hostname(uv.user.actor_id)}`,
-              view: uv,
+          let persons: PersonTribute[] = data.users.map(pv => {
+            let tribute: PersonTribute = {
+              key: `@${pv.person.name}@${hostname(pv.person.actor_id)}`,
+              view: pv,
             };
             return tribute;
           });
-          cb(users);
-          userSub.unsubscribe();
+          cb(persons);
+          personSub.unsubscribe();
         }
       },
       err => console.error(err),
@@ -777,6 +690,7 @@ function communitySearch(
       q: text,
       type_: SearchType.Communities,
       sort: SortType.TopAll,
+      listing_type: ListingType.All,
       page: 1,
       limit: mentionDropdownFetchLimit,
       auth: authField(false),
@@ -811,8 +725,17 @@ function communitySearch(
 export function getListingTypeFromProps(props: any): ListingType {
   return props.match.params.listing_type
     ? routeListingTypeToEnum(props.match.params.listing_type)
-    : UserService.Instance.user
-    ? Object.values(ListingType)[UserService.Instance.user.default_listing_type]
+    : UserService.Instance.myUserInfo
+    ? Object.values(ListingType)[
+        UserService.Instance.myUserInfo.local_user_view.local_user
+          .default_listing_type
+      ]
+    : ListingType.Local;
+}
+
+export function getListingTypeFromPropsNoDefault(props: any): ListingType {
+  return props.match.params.listing_type
+    ? routeListingTypeToEnum(props.match.params.listing_type)
     : ListingType.Local;
 }
 
@@ -826,8 +749,11 @@ export function getDataTypeFromProps(props: any): DataType {
 export function getSortTypeFromProps(props: any): SortType {
   return props.match.params.sort
     ? routeSortTypeToEnum(props.match.params.sort)
-    : UserService.Instance.user
-    ? Object.values(SortType)[UserService.Instance.user.default_sort_type]
+    : UserService.Instance.myUserInfo
+    ? Object.values(SortType)[
+        UserService.Instance.myUserInfo.local_user_view.local_user
+          .default_sort_type
+      ]
     : SortType.Active;
 }
 
@@ -873,6 +799,44 @@ export function saveCommentRes(data: CommentView, comments: CommentView[]) {
   }
 }
 
+export function updatePersonBlock(
+  data: BlockPersonResponse
+): PersonBlockView[] {
+  if (data.blocked) {
+    UserService.Instance.myUserInfo.person_blocks.push({
+      person: UserService.Instance.myUserInfo.local_user_view.person,
+      target: data.person_view.person,
+    });
+    toast(`${i18n.t("blocked")} ${data.person_view.person.name}`);
+  } else {
+    UserService.Instance.myUserInfo.person_blocks =
+      UserService.Instance.myUserInfo.person_blocks.filter(
+        i => i.target.id != data.person_view.person.id
+      );
+    toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`);
+  }
+  return UserService.Instance.myUserInfo.person_blocks;
+}
+
+export function updateCommunityBlock(
+  data: BlockCommunityResponse
+): CommunityBlockView[] {
+  if (data.blocked) {
+    UserService.Instance.myUserInfo.community_blocks.push({
+      person: UserService.Instance.myUserInfo.local_user_view.person,
+      community: data.community_view.community,
+    });
+    toast(`${i18n.t("blocked")} ${data.community_view.community.name}`);
+  } else {
+    UserService.Instance.myUserInfo.community_blocks =
+      UserService.Instance.myUserInfo.community_blocks.filter(
+        i => i.community.id != data.community_view.community.id
+      );
+    toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`);
+  }
+  return UserService.Instance.myUserInfo.community_blocks;
+}
+
 export function createCommentLikeRes(
   data: CommentView,
   comments: CommentView[]
@@ -927,6 +891,40 @@ export function editPostRes(data: PostView, post: PostView) {
   }
 }
 
+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 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) {
@@ -1016,9 +1014,13 @@ export function buildCommentsTree(
   let tree: CommentNodeI[] = [];
   for (let comment_view of comments) {
     let child = map.get(comment_view.comment.id);
-    if (comment_view.comment.parent_id) {
-      let parent_ = map.get(comment_view.comment.parent_id);
-      parent_.children.push(child);
+    let parent_id = comment_view.comment.parent_id;
+    if (parent_id) {
+      let parent = map.get(parent_id);
+      // Necessary because blocked comment might not exist
+      if (parent) {
+        parent.children.push(child);
+      }
     } else {
       tree.push(child);
     }
@@ -1207,3 +1209,135 @@ export function restoreScrollPosition(context: any) {
   let y = Number(sessionStorage.getItem(`scrollPosition_${path}`));
   window.scrollTo(0, y);
 }
+
+export function showLocal(isoData: IsoData): boolean {
+  return isoData.site_res.federated_instances?.linked.length > 0;
+}
+
+interface ChoicesValue {
+  value: string;
+  label: string;
+}
+
+export function communityToChoice(cv: CommunityView): ChoicesValue {
+  let choice: ChoicesValue = {
+    value: cv.community.id.toString(),
+    label: communitySelectName(cv),
+  };
+  return choice;
+}
+
+export function personToChoice(pvs: PersonViewSafe): ChoicesValue {
+  let choice: ChoicesValue = {
+    value: pvs.person.id.toString(),
+    label: personSelectName(pvs),
+  };
+  return choice;
+}
+
+export async function fetchCommunities(q: string) {
+  let form: Search = {
+    q,
+    type_: SearchType.Communities,
+    sort: SortType.TopAll,
+    listing_type: ListingType.All,
+    page: 1,
+    limit: fetchLimit,
+    auth: authField(false),
+  };
+  let client = new LemmyHttp(httpBase);
+  return client.search(form);
+}
+
+export async function fetchUsers(q: string) {
+  let form: Search = {
+    q,
+    type_: SearchType.Users,
+    sort: SortType.TopAll,
+    listing_type: ListingType.All,
+    page: 1,
+    limit: fetchLimit,
+    auth: authField(false),
+  };
+  let client = new LemmyHttp(httpBase);
+  return client.search(form);
+}
+
+export const choicesConfig = {
+  shouldSort: false,
+  searchResultLimit: fetchLimit,
+  classNames: {
+    containerOuter: "choices",
+    containerInner: "choices__inner bg-secondary border-0",
+    input: "form-control",
+    inputCloned: "choices__input--cloned",
+    list: "choices__list",
+    listItems: "choices__list--multiple",
+    listSingle: "choices__list--single",
+    listDropdown: "choices__list--dropdown",
+    item: "choices__item bg-secondary",
+    itemSelectable: "choices__item--selectable",
+    itemDisabled: "choices__item--disabled",
+    itemChoice: "choices__item--choice",
+    placeholder: "choices__placeholder",
+    group: "choices__group",
+    groupHeading: "choices__heading",
+    button: "choices__button",
+    activeState: "is-active",
+    focusState: "is-focused",
+    openState: "is-open",
+    disabledState: "is-disabled",
+    highlightedState: "text-info",
+    selectedState: "text-info",
+    flippedState: "is-flipped",
+    loadingState: "is-loading",
+    noResults: "has-no-results",
+    noChoices: "has-no-choices",
+  },
+};
+
+export function communitySelectName(cv: CommunityView): string {
+  return cv.community.local
+    ? cv.community.title
+    : `${hostname(cv.community.actor_id)}/${cv.community.title}`;
+}
+
+export function personSelectName(pvs: PersonViewSafe): string {
+  let pName = pvs.person.display_name || pvs.person.name;
+  return pvs.person.local ? pName : `${hostname(pvs.person.actor_id)}/${pName}`;
+}
+
+export function initializeSite(site: GetSiteResponse) {
+  UserService.Instance.myUserInfo = site.my_user;
+  i18n.changeLanguage(getLanguages()[0]);
+}
+
+const SHORTNUM_SI_FORMAT = new Intl.NumberFormat("en-US", {
+  maximumSignificantDigits: 3,
+  //@ts-ignore
+  notation: "compact",
+  compactDisplay: "short",
+});
+
+export function numToSI(value: number): string {
+  return SHORTNUM_SI_FORMAT.format(value);
+}
+
+export function isBanned(ps: PersonSafe): boolean {
+  // Add Z to convert from UTC date
+  if (ps.ban_expires) {
+    if (ps.banned && new Date(ps.ban_expires + "Z") > new Date()) {
+      return true;
+    } else {
+      return false;
+    }
+  } else {
+    return ps.banned;
+  }
+}
+
+export function pushNotNull(array: any[], new_item?: any) {
+  if (new_item) {
+    array.push(...new_item);
+  }
+}