1 import emojiShortName from "emoji-short-name";
3 BlockCommunityResponse,
25 WebSocketJsonResponse,
27 } from "lemmy-js-client";
28 import markdown_it from "markdown-it";
29 import markdown_it_container from "markdown-it-container";
30 import markdown_it_html5_embed from "markdown-it-html5-embed";
31 import markdown_it_sub from "markdown-it-sub";
32 import markdown_it_sup from "markdown-it-sup";
33 import moment from "moment";
34 import "moment/locale/bg";
35 import "moment/locale/bn";
36 import "moment/locale/ca";
37 import "moment/locale/cs";
38 import "moment/locale/cy";
39 import "moment/locale/da";
40 import "moment/locale/de";
41 import "moment/locale/el";
42 import "moment/locale/eo";
43 import "moment/locale/es";
44 import "moment/locale/eu";
45 import "moment/locale/fa";
46 import "moment/locale/fi";
47 import "moment/locale/fr";
48 import "moment/locale/ga";
49 import "moment/locale/gl";
50 import "moment/locale/hi";
51 import "moment/locale/hr";
52 import "moment/locale/hu";
53 import "moment/locale/id";
54 import "moment/locale/it";
55 import "moment/locale/ja";
56 import "moment/locale/ka";
57 import "moment/locale/km";
58 import "moment/locale/ko";
59 import "moment/locale/ml";
60 import "moment/locale/nb";
61 import "moment/locale/nl";
62 import "moment/locale/pl";
63 import "moment/locale/pt-br";
64 import "moment/locale/ru";
65 import "moment/locale/sk";
66 import "moment/locale/sq";
67 import "moment/locale/sr";
68 import "moment/locale/sv";
69 import "moment/locale/tr";
70 import "moment/locale/uk";
71 import "moment/locale/vi";
72 import "moment/locale/zh-cn";
73 import { Subscription } from "rxjs";
74 import { delay, retryWhen, take } from "rxjs/operators";
75 import tippy from "tippy.js";
76 import Toastify from "toastify-js";
77 import { httpBase } from "./env";
78 import { i18n } from "./i18next";
80 CommentNode as CommentNodeI,
84 } from "./interfaces";
85 import { UserService, WebSocketService } from "./services";
89 Tribute = require("tributejs");
92 export const wsClient = new LemmyWebsocket();
94 export const favIconUrl = "/static/assets/icons/favicon.svg";
95 export const favIconPngUrl = "/static/assets/icons/apple-touch-icon.png";
97 // export const defaultFavIcon = `${window.location.protocol}//${window.location.host}${favIconPngUrl}`;
98 export const repoUrl = "https://github.com/LemmyNet";
99 export const joinLemmyUrl = "https://join-lemmy.org";
100 export const donateLemmyUrl = `${joinLemmyUrl}/donate`;
101 export const docsUrl = `${joinLemmyUrl}/docs/en/index.html`;
102 export const helpGuideUrl = `${joinLemmyUrl}/docs/en/about/guide.html`; // TODO find a way to redirect to the non-en folder
103 export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
104 export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
105 export const archiveTodayUrl = "https://archive.today";
106 export const ghostArchiveUrl = "https://ghostarchive.org";
107 export const webArchiveUrl = "https://web.archive.org";
108 export const elementUrl = "https://element.io";
110 export const postRefetchSeconds: number = 60 * 1000;
111 export const fetchLimit = 20;
112 export const mentionDropdownFetchLimit = 10;
114 export const languages = [
163 export const themes = [
179 const DEFAULT_ALPHABET =
180 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
182 function getRandomCharFromAlphabet(alphabet: string): string {
183 return alphabet.charAt(Math.floor(Math.random() * alphabet.length));
186 export function randomStr(
187 idDesiredLength = 20,
188 alphabet = DEFAULT_ALPHABET
191 * Create n-long array and map it to random chars from given alphabet.
192 * Then join individual chars as string
194 return Array.from({ length: idDesiredLength })
196 return getRandomCharFromAlphabet(alphabet);
201 export function wsJsonToRes<ResponseType>(
202 msg: WebSocketJsonResponse<ResponseType>
203 ): WebSocketResponse<ResponseType> {
210 export function wsUserOp(msg: any): UserOperation {
211 let opStr: string = msg.op;
212 return UserOperation[opStr];
215 export const md = new markdown_it({
220 .use(markdown_it_sub)
221 .use(markdown_it_sup)
222 .use(markdown_it_html5_embed, {
224 useImageSyntax: true, // Enables video/audio embed with ![]() syntax (default)
226 audio: 'controls preload="metadata"',
228 'width="100%" max-height="100%" controls loop preload="metadata"',
232 .use(markdown_it_container, "spoiler", {
233 validate: function (params: any) {
234 return params.trim().match(/^spoiler\s+(.*)$/);
237 render: function (tokens: any, idx: any) {
238 var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/);
240 if (tokens[idx].nesting === 1) {
242 return `<details><summary> ${md.utils.escapeHtml(m[1])} </summary>\n`;
245 return "</details>\n";
250 export function hotRankComment(comment_view: CommentView): number {
251 return hotRank(comment_view.counts.score, comment_view.comment.published);
254 export function hotRankActivePost(post_view: PostView): number {
255 return hotRank(post_view.counts.score, post_view.counts.newest_comment_time);
258 export function hotRankPost(post_view: PostView): number {
259 return hotRank(post_view.counts.score, post_view.post.published);
262 export function hotRank(score: number, timeStr: string): number {
263 // Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
264 let date: Date = new Date(timeStr + "Z"); // Add Z to convert from UTC date
265 let now: Date = new Date();
266 let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
269 (10000 * Math.log10(Math.max(1, 3 + score))) /
270 Math.pow(hoursElapsed + 2, 1.8);
272 // console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`);
277 export function mdToHtml(text: string) {
278 return { __html: md.render(text) };
281 export function getUnixTime(text: string): number {
282 return text ? new Date(text).getTime() / 1000 : undefined;
285 export function canMod(
286 myUserInfo: MyUserInfo,
291 // You can do moderator actions only on the mods added after you.
293 let yourIndex = modIds.findIndex(
294 id => id == myUserInfo.local_user_view.person.id
296 if (yourIndex == -1) {
299 // onSelf +1 on mod actions not for yourself, IE ban, remove, etc
300 modIds = modIds.slice(0, yourIndex + (onSelf ? 0 : 1));
301 return !modIds.includes(creator_id);
308 export function isMod(modIds: number[], creator_id: number): boolean {
309 return modIds.includes(creator_id);
312 const imageRegex = new RegExp(
313 /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/
315 const videoRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:mp4))`);
317 export function isImage(url: string) {
318 return imageRegex.test(url);
321 export function isVideo(url: string) {
322 return videoRegex.test(url);
325 export function validURL(str: string) {
326 return !!new URL(str);
329 export function communityRSSUrl(actorId: string, sort: string): string {
330 let url = new URL(actorId);
331 return `${url.origin}/feeds${url.pathname}.xml?sort=${sort}`;
334 export function validEmail(email: string) {
336 /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
337 return re.test(String(email).toLowerCase());
340 export function capitalizeFirstLetter(str: string): string {
341 return str.charAt(0).toUpperCase() + str.slice(1);
344 export function routeSortTypeToEnum(sort: string): SortType {
345 return SortType[sort];
348 export function listingTypeFromNum(type_: number): ListingType {
349 return Object.values(ListingType)[type_];
352 export function sortTypeFromNum(type_: number): SortType {
353 return Object.values(SortType)[type_];
356 export function routeListingTypeToEnum(type: string): ListingType {
357 return ListingType[type];
360 export function routeDataTypeToEnum(type: string): DataType {
361 return DataType[capitalizeFirstLetter(type)];
364 export function routeSearchTypeToEnum(type: string): SearchType {
365 return SearchType[type];
368 export async function getSiteMetadata(url: string) {
369 let form: GetSiteMetadata = {
372 let client = new LemmyHttp(httpBase);
373 return client.getSiteMetadata(form);
376 export function debounce(func: any, wait = 1000, immediate = false) {
377 // 'private' variable for instance
378 // The returned function will be able to reference this due to closure.
379 // Each call to the returned function will share this common timer.
382 // Calling debounce returns a new anonymous function
384 // reference the context and args for the setTimeout function
385 var args = arguments;
387 // Should the function be called now? If immediate is true
388 // and not already in a timeout then the answer is: Yes
389 var callNow = immediate && !timeout;
391 // This is the basic debounce behaviour where you can call this
392 // function several times, but it will only execute once
393 // [before or after imposing a delay].
394 // Each time the returned function is called, the timer starts over.
395 clearTimeout(timeout);
397 // Set the new timeout
398 timeout = setTimeout(function () {
399 // Inside the timeout function, clear the timeout variable
400 // which will let the next execution run when in 'immediate' mode
403 // Check if the function already ran with the immediate flag
405 // Call the original function with apply
406 // apply lets you define the 'this' object as well as the arguments
407 // (both captured before setTimeout)
408 func.apply(this, args);
412 // Immediate mode and no wait timer? Execute the function..
413 if (callNow) func.apply(this, args);
418 export function getLanguage(override?: string): string {
419 let myUserInfo = UserService.Instance.myUserInfo;
422 (myUserInfo?.local_user_view.local_user.lang
423 ? myUserInfo.local_user_view.local_user.lang
426 if (lang == "browser" && isBrowser()) {
427 return getBrowserLanguage();
433 export function getBrowserLanguage(): string {
434 // Intersect lemmy's langs, with the browser langs
435 let langs = languages ? languages.map(l => l.code) : ["en"];
437 // NOTE, mobile browsers seem to be missing this list, so append en
438 let allowedLangs = navigator.languages
440 .filter(v => langs.includes(v));
441 return allowedLangs[0];
444 export function getMomentLanguage(): string {
445 let lang = getLanguage();
446 if (lang.startsWith("zh")) {
448 } else if (lang.startsWith("sv")) {
450 } else if (lang.startsWith("fr")) {
452 } else if (lang.startsWith("de")) {
454 } else if (lang.startsWith("ru")) {
456 } else if (lang.startsWith("es")) {
458 } else if (lang.startsWith("eo")) {
460 } else if (lang.startsWith("nl")) {
462 } else if (lang.startsWith("it")) {
464 } else if (lang.startsWith("fi")) {
466 } else if (lang.startsWith("ca")) {
468 } else if (lang.startsWith("fa")) {
470 } else if (lang.startsWith("pl")) {
472 } else if (lang.startsWith("pt_BR")) {
474 } else if (lang.startsWith("ja")) {
476 } else if (lang.startsWith("ka")) {
478 } else if (lang.startsWith("hi")) {
480 } else if (lang.startsWith("el")) {
482 } else if (lang.startsWith("eu")) {
484 } else if (lang.startsWith("gl")) {
486 } else if (lang.startsWith("tr")) {
488 } else if (lang.startsWith("hu")) {
490 } else if (lang.startsWith("uk")) {
492 } else if (lang.startsWith("sq")) {
494 } else if (lang.startsWith("km")) {
496 } else if (lang.startsWith("ga")) {
498 } else if (lang.startsWith("sr")) {
500 } else if (lang.startsWith("ko")) {
502 } else if (lang.startsWith("da")) {
504 } else if (lang.startsWith("oc")) {
506 } else if (lang.startsWith("hr")) {
508 } else if (lang.startsWith("th")) {
510 } else if (lang.startsWith("bg")) {
512 } else if (lang.startsWith("id")) {
514 } else if (lang.startsWith("nb")) {
516 } else if (lang.startsWith("cy")) {
518 } else if (lang.startsWith("sk")) {
520 } else if (lang.startsWith("vi")) {
522 } else if (lang.startsWith("pt")) {
524 } else if (lang.startsWith("ar")) {
526 } else if (lang.startsWith("bn")) {
528 } else if (lang.startsWith("ml")) {
530 } else if (lang.startsWith("cs")) {
538 export function setTheme(theme: string, forceReload = false) {
542 if (theme === "browser" && !forceReload) {
545 // This is only run on a force reload
546 if (theme == "browser") {
550 // Unload all the other themes
551 for (var i = 0; i < themes.length; i++) {
552 let styleSheet = document.getElementById(themes[i]);
554 styleSheet.setAttribute("disabled", "disabled");
559 .getElementById("default-light")
560 ?.setAttribute("disabled", "disabled");
561 document.getElementById("default-dark")?.setAttribute("disabled", "disabled");
563 // Load the theme dynamically
564 let cssLoc = `/static/assets/css/themes/${theme}.min.css`;
565 loadCss(theme, cssLoc);
566 document.getElementById(theme).removeAttribute("disabled");
569 export function loadCss(id: string, loc: string) {
570 if (!document.getElementById(id)) {
571 var head = document.getElementsByTagName("head")[0];
572 var link = document.createElement("link");
574 link.rel = "stylesheet";
575 link.type = "text/css";
578 head.appendChild(link);
582 export function objectFlip(obj: any) {
584 Object.keys(obj).forEach(key => {
590 export function showAvatars(): boolean {
592 UserService.Instance.myUserInfo?.local_user_view.local_user.show_avatars ||
593 !UserService.Instance.myUserInfo
597 export function showScores(): boolean {
599 UserService.Instance.myUserInfo?.local_user_view.local_user.show_scores ||
600 !UserService.Instance.myUserInfo
604 export function isCakeDay(published: string): boolean {
605 // moment(undefined) or moment.utc(undefined) returns the current date/time
606 // moment(null) or moment.utc(null) returns null
607 const createDate = moment.utc(published || null).local();
608 const currentDate = moment(new Date());
611 createDate.date() === currentDate.date() &&
612 createDate.month() === currentDate.month() &&
613 createDate.year() !== currentDate.year()
617 export function toast(text: string, background = "success") {
619 let backgroundColor = `var(--${background})`;
622 backgroundColor: backgroundColor,
629 export function pictrsDeleteToast(
630 clickToDeleteText: string,
631 deletePictureText: string,
635 let backgroundColor = `var(--light)`;
636 let toast = Toastify({
637 text: clickToDeleteText,
638 backgroundColor: backgroundColor,
644 window.location.replace(deleteUrl);
645 alert(deletePictureText);
654 interface NotifyInfo {
661 export function messageToastify(info: NotifyInfo, router: any) {
663 let htmlBody = info.body ? md.render(info.body) : "";
664 let backgroundColor = `var(--light)`;
666 let toast = Toastify({
667 text: `${htmlBody}<br />${info.name}`,
668 avatar: info.icon ? info.icon : null,
669 backgroundColor: backgroundColor,
670 className: "text-dark",
679 router.history.push(info.link);
686 export function notifyPost(post_view: PostView, router: any) {
687 let info: NotifyInfo = {
688 name: post_view.community.name,
689 icon: post_view.community.icon,
690 link: `/post/${post_view.post.id}`,
691 body: post_view.post.name,
693 notify(info, router);
696 export function notifyComment(comment_view: CommentView, router: any) {
697 let info: NotifyInfo = {
698 name: comment_view.creator.name,
699 icon: comment_view.creator.avatar,
700 link: `/post/${comment_view.post.id}/comment/${comment_view.comment.id}`,
701 body: comment_view.comment.content,
703 notify(info, router);
706 export function notifyPrivateMessage(pmv: PrivateMessageView, router: any) {
707 let info: NotifyInfo = {
708 name: pmv.creator.name,
709 icon: pmv.creator.avatar,
711 body: pmv.private_message.content,
713 notify(info, router);
716 function notify(info: NotifyInfo, router: any) {
717 messageToastify(info, router);
719 // TODO absolute nightmare bug, but notifs are currently broken.
720 // Notification.new will try to do a browser fetch ???
722 // if (Notification.permission !== "granted") Notification.requestPermission();
724 // var notification = new Notification(info.name, {
729 // notification.onclick = (ev: Event): any => {
730 // ev.preventDefault();
731 // router.history.push(info.link);
736 export function setupTribute() {
738 noMatchTemplate: function () {
745 menuItemTemplate: (item: any) => {
746 let shortName = `:${item.original.key}:`;
747 return `${item.original.val} ${shortName}`;
749 selectTemplate: (item: any) => {
750 return `${item.original.val}`;
752 values: Object.entries(emojiShortName).map(e => {
753 return { key: e[1], val: e[0] };
756 autocompleteMode: true,
758 // menuItemLimit: mentionDropdownFetchLimit,
759 menuShowMinLength: 2,
764 selectTemplate: (item: any) => {
765 let it: PersonTribute = item.original;
766 return `[${it.key}](${it.view.person.actor_id})`;
768 values: (text: string, cb: (persons: PersonTribute[]) => any) => {
769 personSearch(text, (persons: PersonTribute[]) => cb(persons));
772 autocompleteMode: true,
774 // menuItemLimit: mentionDropdownFetchLimit,
775 menuShowMinLength: 2,
781 selectTemplate: (item: any) => {
782 let it: CommunityTribute = item.original;
783 return `[${it.key}](${it.view.community.actor_id})`;
785 values: (text: string, cb: any) => {
786 communitySearch(text, (communities: CommunityTribute[]) =>
791 autocompleteMode: true,
793 // menuItemLimit: mentionDropdownFetchLimit,
794 menuShowMinLength: 2,
800 var tippyInstance: any;
802 tippyInstance = tippy("[data-tippy-content]");
805 export function setupTippy() {
807 tippyInstance.forEach((e: any) => e.destroy());
808 tippyInstance = tippy("[data-tippy-content]", {
810 // Display on "long press"
811 touch: ["hold", 500],
816 interface PersonTribute {
818 view: PersonViewSafe;
821 function personSearch(text: string, cb: (persons: PersonTribute[]) => any) {
825 type_: SearchType.Users,
826 sort: SortType.TopAll,
827 listing_type: ListingType.All,
829 limit: mentionDropdownFetchLimit,
830 auth: authField(false),
833 WebSocketService.Instance.send(wsClient.search(form));
835 let personSub = WebSocketService.Instance.subject.subscribe(
837 let res = wsJsonToRes(msg);
838 if (res.op == UserOperation.Search) {
839 let data = res.data as SearchResponse;
840 let persons: PersonTribute[] = data.users.map(pv => {
841 let tribute: PersonTribute = {
842 key: `@${pv.person.name}@${hostname(pv.person.actor_id)}`,
848 personSub.unsubscribe();
851 err => console.error(err),
852 () => console.log("complete")
859 interface CommunityTribute {
864 function communitySearch(
866 cb: (communities: CommunityTribute[]) => any
871 type_: SearchType.Communities,
872 sort: SortType.TopAll,
873 listing_type: ListingType.All,
875 limit: mentionDropdownFetchLimit,
876 auth: authField(false),
879 WebSocketService.Instance.send(wsClient.search(form));
881 let communitySub = WebSocketService.Instance.subject.subscribe(
883 let res = wsJsonToRes(msg);
884 if (res.op == UserOperation.Search) {
885 let data = res.data as SearchResponse;
886 let communities: CommunityTribute[] = data.communities.map(cv => {
887 let tribute: CommunityTribute = {
888 key: `!${cv.community.name}@${hostname(cv.community.actor_id)}`,
894 communitySub.unsubscribe();
897 err => console.error(err),
898 () => console.log("complete")
905 export function getListingTypeFromProps(props: any): ListingType {
906 return props.match.params.listing_type
907 ? routeListingTypeToEnum(props.match.params.listing_type)
908 : UserService.Instance.myUserInfo
909 ? Object.values(ListingType)[
910 UserService.Instance.myUserInfo.local_user_view.local_user
911 .default_listing_type
916 export function getListingTypeFromPropsNoDefault(props: any): ListingType {
917 return props.match.params.listing_type
918 ? routeListingTypeToEnum(props.match.params.listing_type)
922 // TODO might need to add a user setting for this too
923 export function getDataTypeFromProps(props: any): DataType {
924 return props.match.params.data_type
925 ? routeDataTypeToEnum(props.match.params.data_type)
929 export function getSortTypeFromProps(props: any): SortType {
930 return props.match.params.sort
931 ? routeSortTypeToEnum(props.match.params.sort)
932 : UserService.Instance.myUserInfo
933 ? Object.values(SortType)[
934 UserService.Instance.myUserInfo.local_user_view.local_user
940 export function getPageFromProps(props: any): number {
941 return props.match.params.page ? Number(props.match.params.page) : 1;
944 export function getRecipientIdFromProps(props: any): number {
945 return props.match.params.recipient_id
946 ? Number(props.match.params.recipient_id)
950 export function getIdFromProps(props: any): number {
951 return Number(props.match.params.id);
954 export function getCommentIdFromProps(props: any): number {
955 return Number(props.match.params.comment_id);
958 export function getUsernameFromProps(props: any): string {
959 return props.match.params.username;
962 export function editCommentRes(data: CommentView, comments: CommentView[]) {
963 let found = comments.find(c => c.comment.id == data.comment.id);
965 found.comment.content = data.comment.content;
966 found.comment.updated = data.comment.updated;
967 found.comment.removed = data.comment.removed;
968 found.comment.deleted = data.comment.deleted;
969 found.counts.upvotes = data.counts.upvotes;
970 found.counts.downvotes = data.counts.downvotes;
971 found.counts.score = data.counts.score;
975 export function saveCommentRes(data: CommentView, comments: CommentView[]) {
976 let found = comments.find(c => c.comment.id == data.comment.id);
978 found.saved = data.saved;
982 export function updatePersonBlock(
983 data: BlockPersonResponse
984 ): PersonBlockView[] {
986 UserService.Instance.myUserInfo.person_blocks.push({
987 person: UserService.Instance.myUserInfo.local_user_view.person,
988 target: data.person_view.person,
990 toast(`${i18n.t("blocked")} ${data.person_view.person.name}`);
992 UserService.Instance.myUserInfo.person_blocks =
993 UserService.Instance.myUserInfo.person_blocks.filter(
994 i => i.target.id != data.person_view.person.id
996 toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`);
998 return UserService.Instance.myUserInfo.person_blocks;
1001 export function updateCommunityBlock(
1002 data: BlockCommunityResponse
1003 ): CommunityBlockView[] {
1005 UserService.Instance.myUserInfo.community_blocks.push({
1006 person: UserService.Instance.myUserInfo.local_user_view.person,
1007 community: data.community_view.community,
1009 toast(`${i18n.t("blocked")} ${data.community_view.community.name}`);
1011 UserService.Instance.myUserInfo.community_blocks =
1012 UserService.Instance.myUserInfo.community_blocks.filter(
1013 i => i.community.id != data.community_view.community.id
1015 toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`);
1017 return UserService.Instance.myUserInfo.community_blocks;
1020 export function createCommentLikeRes(
1022 comments: CommentView[]
1024 let found = comments.find(c => c.comment.id === data.comment.id);
1026 found.counts.score = data.counts.score;
1027 found.counts.upvotes = data.counts.upvotes;
1028 found.counts.downvotes = data.counts.downvotes;
1029 if (data.my_vote !== null) {
1030 found.my_vote = data.my_vote;
1035 export function createPostLikeFindRes(data: PostView, posts: PostView[]) {
1036 let found = posts.find(p => p.post.id == data.post.id);
1038 createPostLikeRes(data, found);
1042 export function createPostLikeRes(data: PostView, post_view: PostView) {
1044 post_view.counts.score = data.counts.score;
1045 post_view.counts.upvotes = data.counts.upvotes;
1046 post_view.counts.downvotes = data.counts.downvotes;
1047 if (data.my_vote !== null) {
1048 post_view.my_vote = data.my_vote;
1053 export function editPostFindRes(data: PostView, posts: PostView[]) {
1054 let found = posts.find(p => p.post.id == data.post.id);
1056 editPostRes(data, found);
1060 export function editPostRes(data: PostView, post: PostView) {
1062 post.post.url = data.post.url;
1063 post.post.name = data.post.name;
1064 post.post.nsfw = data.post.nsfw;
1065 post.post.deleted = data.post.deleted;
1066 post.post.removed = data.post.removed;
1067 post.post.stickied = data.post.stickied;
1068 post.post.body = data.post.body;
1069 post.post.locked = data.post.locked;
1070 post.saved = data.saved;
1074 export function updatePostReportRes(
1075 data: PostReportView,
1076 reports: PostReportView[]
1078 let found = reports.find(p => p.post.id == data.post.id);
1080 found.post_report = data.post_report;
1084 export function updateCommentReportRes(
1085 data: CommentReportView,
1086 reports: CommentReportView[]
1088 let found = reports.find(c => c.comment.id == data.comment.id);
1090 found.comment_report = data.comment_report;
1094 export function commentsToFlatNodes(comments: CommentView[]): CommentNodeI[] {
1095 let nodes: CommentNodeI[] = [];
1096 for (let comment of comments) {
1097 nodes.push({ comment_view: comment });
1102 function commentSort(tree: CommentNodeI[], sort: CommentSortType) {
1103 // First, put removed and deleted comments at the bottom, then do your other sorts
1104 if (sort == CommentSortType.Top) {
1107 +a.comment_view.comment.removed - +b.comment_view.comment.removed ||
1108 +a.comment_view.comment.deleted - +b.comment_view.comment.deleted ||
1109 b.comment_view.counts.score - a.comment_view.counts.score
1111 } else if (sort == CommentSortType.New) {
1114 +a.comment_view.comment.removed - +b.comment_view.comment.removed ||
1115 +a.comment_view.comment.deleted - +b.comment_view.comment.deleted ||
1116 b.comment_view.comment.published.localeCompare(
1117 a.comment_view.comment.published
1120 } else if (sort == CommentSortType.Old) {
1123 +a.comment_view.comment.removed - +b.comment_view.comment.removed ||
1124 +a.comment_view.comment.deleted - +b.comment_view.comment.deleted ||
1125 a.comment_view.comment.published.localeCompare(
1126 b.comment_view.comment.published
1129 } else if (sort == CommentSortType.Hot) {
1132 +a.comment_view.comment.removed - +b.comment_view.comment.removed ||
1133 +a.comment_view.comment.deleted - +b.comment_view.comment.deleted ||
1134 hotRankComment(b.comment_view) - hotRankComment(a.comment_view)
1138 // Go through the children recursively
1139 for (let node of tree) {
1140 if (node.children) {
1141 commentSort(node.children, sort);
1146 export function commentSortSortType(tree: CommentNodeI[], sort: SortType) {
1147 commentSort(tree, convertCommentSortType(sort));
1150 function convertCommentSortType(sort: SortType): CommentSortType {
1152 sort == SortType.TopAll ||
1153 sort == SortType.TopDay ||
1154 sort == SortType.TopWeek ||
1155 sort == SortType.TopMonth ||
1156 sort == SortType.TopYear
1158 return CommentSortType.Top;
1159 } else if (sort == SortType.New) {
1160 return CommentSortType.New;
1161 } else if (sort == SortType.Hot || sort == SortType.Active) {
1162 return CommentSortType.Hot;
1164 return CommentSortType.Hot;
1168 export function buildCommentsTree(
1169 comments: CommentView[],
1170 commentSortType: CommentSortType
1172 let map = new Map<number, CommentNodeI>();
1173 for (let comment_view of comments) {
1174 let node: CommentNodeI = {
1175 comment_view: comment_view,
1178 map.set(comment_view.comment.id, { ...node });
1180 let tree: CommentNodeI[] = [];
1181 for (let comment_view of comments) {
1182 let child = map.get(comment_view.comment.id);
1183 let parent_id = comment_view.comment.parent_id;
1185 let parent = map.get(parent_id);
1186 // Necessary because blocked comment might not exist
1188 parent.children.push(child);
1197 commentSort(tree, commentSortType);
1202 function setDepth(node: CommentNodeI, i = 0) {
1203 for (let child of node.children) {
1205 setDepth(child, i + 1);
1209 export function insertCommentIntoTree(tree: CommentNodeI[], cv: CommentView) {
1210 // Building a fake node to be used for later
1211 let node: CommentNodeI = {
1217 if (cv.comment.parent_id) {
1218 let parentComment = searchCommentTree(tree, cv.comment.parent_id);
1219 if (parentComment) {
1220 node.depth = parentComment.depth + 1;
1221 parentComment.children.unshift(node);
1228 export function searchCommentTree(
1229 tree: CommentNodeI[],
1232 for (let node of tree) {
1233 if (node.comment_view.comment.id === id) {
1237 for (const child of node.children) {
1238 const res = searchCommentTree([child], id);
1248 export const colorList: string[] = [
1257 function hsl(num: number) {
1258 return `hsla(${num}, 35%, 50%, 1)`;
1261 export function previewLines(
1270 // Use lines * 2 because markdown requires 2 lines
1271 .slice(0, maxLines * 2)
1276 export function hostname(url: string): string {
1277 let cUrl = new URL(url);
1278 return cUrl.port ? `${cUrl.hostname}:${cUrl.port}` : `${cUrl.hostname}`;
1281 export function validTitle(title?: string): boolean {
1282 // Initial title is null, minimum length is taken care of by textarea's minLength={3}
1283 if (title === null || title.length < 3) return true;
1285 const regex = new RegExp(/.*\S.*/, "g");
1287 return regex.test(title);
1290 export function siteBannerCss(banner: string): string {
1292 background-image: linear-gradient( rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.8) ) ,url("${banner}"); \
1293 background-attachment: fixed; \
1294 background-position: top; \
1295 background-repeat: no-repeat; \
1296 background-size: 100% cover; \
1299 max-height: 100vh; \
1303 export function isBrowser() {
1304 return typeof window !== "undefined";
1307 export function setIsoData(context: any): IsoData {
1308 let isoData: IsoData = isBrowser()
1310 : context.router.staticContext;
1314 export function wsSubscribe(parseMessage: any): Subscription {
1316 return WebSocketService.Instance.subject
1317 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
1319 msg => parseMessage(msg),
1320 err => console.error(err),
1321 () => console.log("complete")
1328 export function setOptionalAuth(obj: any, auth = UserService.Instance.auth) {
1334 export function authField(
1336 auth = UserService.Instance.auth
1338 if (auth == null && throwErr) {
1339 toast(i18n.t("not_logged_in"), "danger");
1340 throw "Not logged in";
1346 moment.updateLocale("en", {
1367 export function saveScrollPosition(context: any) {
1368 let path: string = context.router.route.location.pathname;
1369 let y = window.scrollY;
1370 sessionStorage.setItem(`scrollPosition_${path}`, y.toString());
1373 export function restoreScrollPosition(context: any) {
1374 let path: string = context.router.route.location.pathname;
1375 let y = Number(sessionStorage.getItem(`scrollPosition_${path}`));
1376 window.scrollTo(0, y);
1379 export function showLocal(isoData: IsoData): boolean {
1380 return isoData.site_res.federated_instances?.linked.length > 0;
1383 interface ChoicesValue {
1388 export function communityToChoice(cv: CommunityView): ChoicesValue {
1389 let choice: ChoicesValue = {
1390 value: cv.community.id.toString(),
1391 label: communitySelectName(cv),
1396 export function personToChoice(pvs: PersonViewSafe): ChoicesValue {
1397 let choice: ChoicesValue = {
1398 value: pvs.person.id.toString(),
1399 label: personSelectName(pvs),
1404 export async function fetchCommunities(q: string) {
1405 let form: Search = {
1407 type_: SearchType.Communities,
1408 sort: SortType.TopAll,
1409 listing_type: ListingType.All,
1412 auth: authField(false),
1414 let client = new LemmyHttp(httpBase);
1415 return client.search(form);
1418 export async function fetchUsers(q: string) {
1419 let form: Search = {
1421 type_: SearchType.Users,
1422 sort: SortType.TopAll,
1423 listing_type: ListingType.All,
1426 auth: authField(false),
1428 let client = new LemmyHttp(httpBase);
1429 return client.search(form);
1432 export const choicesConfig = {
1434 searchResultLimit: fetchLimit,
1436 containerOuter: "choices",
1437 containerInner: "choices__inner bg-secondary border-0",
1438 input: "form-control",
1439 inputCloned: "choices__input--cloned",
1440 list: "choices__list",
1441 listItems: "choices__list--multiple",
1442 listSingle: "choices__list--single",
1443 listDropdown: "choices__list--dropdown",
1444 item: "choices__item bg-secondary",
1445 itemSelectable: "choices__item--selectable",
1446 itemDisabled: "choices__item--disabled",
1447 itemChoice: "choices__item--choice",
1448 placeholder: "choices__placeholder",
1449 group: "choices__group",
1450 groupHeading: "choices__heading",
1451 button: "choices__button",
1452 activeState: "is-active",
1453 focusState: "is-focused",
1454 openState: "is-open",
1455 disabledState: "is-disabled",
1456 highlightedState: "text-info",
1457 selectedState: "text-info",
1458 flippedState: "is-flipped",
1459 loadingState: "is-loading",
1460 noResults: "has-no-results",
1461 noChoices: "has-no-choices",
1465 export function communitySelectName(cv: CommunityView): string {
1466 return cv.community.local
1467 ? cv.community.title
1468 : `${hostname(cv.community.actor_id)}/${cv.community.title}`;
1471 export function personSelectName(pvs: PersonViewSafe): string {
1472 let pName = pvs.person.display_name || pvs.person.name;
1473 return pvs.person.local ? pName : `${hostname(pvs.person.actor_id)}/${pName}`;
1476 export function initializeSite(site: GetSiteResponse) {
1477 UserService.Instance.myUserInfo = site.my_user;
1478 i18n.changeLanguage(getLanguage());
1481 const SHORTNUM_SI_FORMAT = new Intl.NumberFormat("en-US", {
1482 maximumSignificantDigits: 3,
1484 notation: "compact",
1485 compactDisplay: "short",
1488 export function numToSI(value: number): string {
1489 return SHORTNUM_SI_FORMAT.format(value);