]> Untitled Git - lemmy-ui.git/blob - src/shared/utils.ts
Somewhat working webpack. Sponsors and communities pages done.
[lemmy-ui.git] / src / shared / utils.ts
1 import 'moment/locale/es';
2 import 'moment/locale/el';
3 import 'moment/locale/eu';
4 import 'moment/locale/eo';
5 import 'moment/locale/de';
6 import 'moment/locale/zh-cn';
7 import 'moment/locale/fr';
8 import 'moment/locale/sv';
9 import 'moment/locale/ru';
10 import 'moment/locale/nl';
11 import 'moment/locale/it';
12 import 'moment/locale/fi';
13 import 'moment/locale/ca';
14 import 'moment/locale/fa';
15 import 'moment/locale/pl';
16 import 'moment/locale/pt-br';
17 import 'moment/locale/ja';
18 import 'moment/locale/ka';
19 import 'moment/locale/hi';
20 import 'moment/locale/gl';
21 import 'moment/locale/tr';
22 import 'moment/locale/hu';
23 import 'moment/locale/uk';
24 import 'moment/locale/sq';
25 import 'moment/locale/km';
26 import 'moment/locale/ga';
27 import 'moment/locale/sr';
28
29 import {
30   UserOperation,
31   Comment,
32   CommentNode as CommentNodeI,
33   Post,
34   PrivateMessage,
35   User,
36   SortType,
37   ListingType,
38   SearchType,
39   WebSocketResponse,
40   WebSocketJsonResponse,
41   SearchForm,
42   SearchResponse,
43   CommentResponse,
44   PostResponse,
45   LemmyHttp,
46 } from 'lemmy-js-client';
47
48 import { httpUri } from './env';
49
50 import { CommentSortType, DataType } from './interfaces';
51 import { UserService, WebSocketService } from './services';
52
53 // import Tribute from 'tributejs';
54 import markdown_it from 'markdown-it';
55 import markdown_it_sub from 'markdown-it-sub';
56 import markdown_it_sup from 'markdown-it-sup';
57 import markdownitEmoji from 'markdown-it-emoji/light';
58 import markdown_it_container from 'markdown-it-container';
59 import emojiShortName from 'emoji-short-name';
60 import Toastify from 'toastify-js';
61 import tippy from 'tippy.js';
62 import moment from 'moment';
63
64 export const favIconUrl = '/static/assets/favicon.svg';
65 export const favIconPngUrl = '/static/assets/apple-touch-icon.png';
66 // TODO
67 // export const defaultFavIcon = `${window.location.protocol}//${window.location.host}${favIconPngUrl}`;
68 export const defaultFavIcon = 'test';
69 export const repoUrl = 'https://github.com/LemmyNet/lemmy';
70 export const helpGuideUrl = '/docs/about_guide.html';
71 export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
72 export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
73 export const archiveUrl = 'https://archive.is';
74 export const elementUrl = 'https://element.io/';
75
76 export const postRefetchSeconds: number = 60 * 1000;
77 export const fetchLimit: number = 20;
78 export const mentionDropdownFetchLimit = 10;
79
80 export const lemmyHttp = new LemmyHttp(httpUri);
81
82 export const languages = [
83   { code: 'ca', name: 'Català' },
84   { code: 'en', name: 'English' },
85   { code: 'el', name: 'Ελληνικά' },
86   { code: 'eu', name: 'Euskara' },
87   { code: 'eo', name: 'Esperanto' },
88   { code: 'es', name: 'Español' },
89   { code: 'de', name: 'Deutsch' },
90   { code: 'ga', name: 'Gaeilge' },
91   { code: 'gl', name: 'Galego' },
92   { code: 'hu', name: 'Magyar Nyelv' },
93   { code: 'ka', name: 'ქართული ენა' },
94   { code: 'km', name: 'ភាសាខ្មែរ' },
95   { code: 'hi', name: 'मानक हिन्दी' },
96   { code: 'fa', name: 'فارسی' },
97   { code: 'ja', name: '日本語' },
98   { code: 'pl', name: 'Polski' },
99   { code: 'pt_BR', name: 'Português Brasileiro' },
100   { code: 'zh', name: '中文' },
101   { code: 'fi', name: 'Suomi' },
102   { code: 'fr', name: 'Français' },
103   { code: 'sv', name: 'Svenska' },
104   { code: 'sq', name: 'Shqip' },
105   { code: 'sr_Latn', name: 'srpski' },
106   { code: 'tr', name: 'Türkçe' },
107   { code: 'uk', name: 'Українська Mова' },
108   { code: 'ru', name: 'Русский' },
109   { code: 'nl', name: 'Nederlands' },
110   { code: 'it', name: 'Italiano' },
111 ];
112
113 export const themes = [
114   'litera',
115   'materia',
116   'minty',
117   'solar',
118   'united',
119   'cyborg',
120   'darkly',
121   'journal',
122   'sketchy',
123   'vaporwave',
124   'vaporwave-dark',
125   'i386',
126   'litely',
127 ];
128
129 const DEFAULT_ALPHABET =
130   'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
131
132 function getRandomCharFromAlphabet(alphabet: string): string {
133   return alphabet.charAt(Math.floor(Math.random() * alphabet.length));
134 }
135
136 export function randomStr(
137   idDesiredLength: number = 20,
138   alphabet = DEFAULT_ALPHABET
139 ): string {
140   /**
141    * Create n-long array and map it to random chars from given alphabet.
142    * Then join individual chars as string
143    */
144   return Array.from({ length: idDesiredLength })
145     .map(() => {
146       return getRandomCharFromAlphabet(alphabet);
147     })
148     .join('');
149 }
150
151 export function wsJsonToRes(msg: WebSocketJsonResponse): WebSocketResponse {
152   let opStr: string = msg.op;
153   return {
154     op: UserOperation[opStr],
155     data: msg.data,
156   };
157 }
158
159 export const md = new markdown_it({
160   html: false,
161   linkify: true,
162   typographer: true,
163 })
164   .use(markdown_it_sub)
165   .use(markdown_it_sup)
166   .use(markdown_it_container, 'spoiler', {
167     validate: function (params: any) {
168       return params.trim().match(/^spoiler\s+(.*)$/);
169     },
170
171     render: function (tokens: any, idx: any) {
172       var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/);
173
174       if (tokens[idx].nesting === 1) {
175         // opening tag
176         return `<details><summary> ${md.utils.escapeHtml(m[1])} </summary>\n`;
177       } else {
178         // closing tag
179         return '</details>\n';
180       }
181     },
182   })
183   .use(markdownitEmoji, {
184     defs: objectFlip(emojiShortName),
185   });
186
187 export function hotRankComment(comment: Comment): number {
188   return hotRank(comment.score, comment.published);
189 }
190
191 export function hotRankPost(post: Post): number {
192   return hotRank(post.score, post.newest_activity_time);
193 }
194
195 export function hotRank(score: number, timeStr: string): number {
196   // Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
197   let date: Date = new Date(timeStr + 'Z'); // Add Z to convert from UTC date
198   let now: Date = new Date();
199   let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
200
201   let rank =
202     (10000 * Math.log10(Math.max(1, 3 + score))) /
203     Math.pow(hoursElapsed + 2, 1.8);
204
205   // console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`);
206
207   return rank;
208 }
209
210 export function mdToHtml(text: string) {
211   return { __html: md.render(text) };
212 }
213
214 export function getUnixTime(text: string): number {
215   return text ? new Date(text).getTime() / 1000 : undefined;
216 }
217
218 export function addTypeInfo<T>(
219   arr: T[],
220   name: string
221 ): { type_: string; data: T }[] {
222   return arr.map(e => {
223     return { type_: name, data: e };
224   });
225 }
226
227 export function canMod(
228   user: User,
229   modIds: number[],
230   creator_id: number,
231   onSelf: boolean = false
232 ): boolean {
233   // You can do moderator actions only on the mods added after you.
234   if (user) {
235     let yourIndex = modIds.findIndex(id => id == user.id);
236     if (yourIndex == -1) {
237       return false;
238     } else {
239       // onSelf +1 on mod actions not for yourself, IE ban, remove, etc
240       modIds = modIds.slice(0, yourIndex + (onSelf ? 0 : 1));
241       return !modIds.includes(creator_id);
242     }
243   } else {
244     return false;
245   }
246 }
247
248 export function isMod(modIds: number[], creator_id: number): boolean {
249   return modIds.includes(creator_id);
250 }
251
252 const imageRegex = new RegExp(
253   /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/
254 );
255 const videoRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:mp4))`);
256
257 export function isImage(url: string) {
258   return imageRegex.test(url);
259 }
260
261 export function isVideo(url: string) {
262   return videoRegex.test(url);
263 }
264
265 // TODO this broke
266 export function validURL(str: string) {
267   // try {
268   return !!new URL(str);
269   // } catch {
270   // return false;
271   // }
272 }
273
274 export function validEmail(email: string) {
275   let re = /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
276   return re.test(String(email).toLowerCase());
277 }
278
279 export function capitalizeFirstLetter(str: string): string {
280   return str.charAt(0).toUpperCase() + str.slice(1);
281 }
282
283 export function routeSortTypeToEnum(sort: string): SortType {
284   return SortType[sort];
285 }
286
287 export function routeListingTypeToEnum(type: string): ListingType {
288   return ListingType[type];
289 }
290
291 export function routeDataTypeToEnum(type: string): DataType {
292   return DataType[capitalizeFirstLetter(type)];
293 }
294
295 export function routeSearchTypeToEnum(type: string): SearchType {
296   return SearchType[capitalizeFirstLetter(type)];
297 }
298
299 export async function getPageTitle(url: string) {
300   let res = await fetch(`/iframely/oembed?url=${url}`).then(res => res.json());
301   let title = await res.title;
302   return title;
303 }
304
305 export function debounce(
306   func: any,
307   wait: number = 1000,
308   immediate: boolean = false
309 ) {
310   // 'private' variable for instance
311   // The returned function will be able to reference this due to closure.
312   // Each call to the returned function will share this common timer.
313   let timeout: any;
314
315   // Calling debounce returns a new anonymous function
316   return function () {
317     // reference the context and args for the setTimeout function
318     var context = this,
319       args = arguments;
320
321     // Should the function be called now? If immediate is true
322     //   and not already in a timeout then the answer is: Yes
323     var callNow = immediate && !timeout;
324
325     // This is the basic debounce behaviour where you can call this
326     //   function several times, but it will only execute once
327     //   [before or after imposing a delay].
328     //   Each time the returned function is called, the timer starts over.
329     clearTimeout(timeout);
330
331     // Set the new timeout
332     timeout = setTimeout(function () {
333       // Inside the timeout function, clear the timeout variable
334       // which will let the next execution run when in 'immediate' mode
335       timeout = null;
336
337       // Check if the function already ran with the immediate flag
338       if (!immediate) {
339         // Call the original function with apply
340         // apply lets you define the 'this' object as well as the arguments
341         //    (both captured before setTimeout)
342         func.apply(context, args);
343       }
344     }, wait);
345
346     // Immediate mode and no wait timer? Execute the function..
347     if (callNow) func.apply(context, args);
348   };
349 }
350
351 // TODO
352 export function getLanguage(override?: string): string {
353   return 'en';
354   // let user = UserService.Instance.user;
355   // let lang = override || (user && user.lang ? user.lang : 'browser');
356
357   // if (lang == 'browser') {
358   //   return getBrowserLanguage();
359   // } else {
360   //   return lang;
361   // }
362 }
363
364 // TODO
365 export function getBrowserLanguage(): string {
366   return navigator.language;
367 }
368
369 export function getMomentLanguage(): string {
370   let lang = getLanguage();
371   if (lang.startsWith('zh')) {
372     lang = 'zh-cn';
373   } else if (lang.startsWith('sv')) {
374     lang = 'sv';
375   } else if (lang.startsWith('fr')) {
376     lang = 'fr';
377   } else if (lang.startsWith('de')) {
378     lang = 'de';
379   } else if (lang.startsWith('ru')) {
380     lang = 'ru';
381   } else if (lang.startsWith('es')) {
382     lang = 'es';
383   } else if (lang.startsWith('eo')) {
384     lang = 'eo';
385   } else if (lang.startsWith('nl')) {
386     lang = 'nl';
387   } else if (lang.startsWith('it')) {
388     lang = 'it';
389   } else if (lang.startsWith('fi')) {
390     lang = 'fi';
391   } else if (lang.startsWith('ca')) {
392     lang = 'ca';
393   } else if (lang.startsWith('fa')) {
394     lang = 'fa';
395   } else if (lang.startsWith('pl')) {
396     lang = 'pl';
397   } else if (lang.startsWith('pt')) {
398     lang = 'pt-br';
399   } else if (lang.startsWith('ja')) {
400     lang = 'ja';
401   } else if (lang.startsWith('ka')) {
402     lang = 'ka';
403   } else if (lang.startsWith('hi')) {
404     lang = 'hi';
405   } else if (lang.startsWith('el')) {
406     lang = 'el';
407   } else if (lang.startsWith('eu')) {
408     lang = 'eu';
409   } else if (lang.startsWith('gl')) {
410     lang = 'gl';
411   } else if (lang.startsWith('tr')) {
412     lang = 'tr';
413   } else if (lang.startsWith('hu')) {
414     lang = 'hu';
415   } else if (lang.startsWith('uk')) {
416     lang = 'uk';
417   } else if (lang.startsWith('sq')) {
418     lang = 'sq';
419   } else if (lang.startsWith('km')) {
420     lang = 'km';
421   } else if (lang.startsWith('ga')) {
422     lang = 'ga';
423   } else if (lang.startsWith('sr')) {
424     lang = 'sr';
425   } else {
426     lang = 'en';
427   }
428   return lang;
429 }
430
431 // TODO
432 export function setTheme(theme: string = 'darkly', loggedIn: boolean = false) {
433   // unload all the other themes
434   // for (var i = 0; i < themes.length; i++) {
435   //   let styleSheet = document.getElementById(themes[i]);
436   //   if (styleSheet) {
437   //     styleSheet.setAttribute('disabled', 'disabled');
438   //   }
439   // }
440   // // if the user is not logged in, we load the default themes and let the browser decide
441   // if (!loggedIn) {
442   //   document.getElementById('default-light').removeAttribute('disabled');
443   //   document.getElementById('default-dark').removeAttribute('disabled');
444   // } else {
445   //   document
446   //     .getElementById('default-light')
447   //     .setAttribute('disabled', 'disabled');
448   //   document
449   //     .getElementById('default-dark')
450   //     .setAttribute('disabled', 'disabled');
451   //   // Load the theme dynamically
452   //   let cssLoc = `/static/assets/css/themes/${theme}.min.css`;
453   //   loadCss(theme, cssLoc);
454   //   document.getElementById(theme).removeAttribute('disabled');
455   // }
456 }
457
458 // export function loadCss(id: string, loc: string) {
459 //   if (!document.getElementById(id)) {
460 //     var head = document.getElementsByTagName('head')[0];
461 //     var link = document.createElement('link');
462 //     link.id = id;
463 //     link.rel = 'stylesheet';
464 //     link.type = 'text/css';
465 //     link.href = loc;
466 //     link.media = 'all';
467 //     head.appendChild(link);
468 //   }
469 // }
470
471 export function objectFlip(obj: any) {
472   const ret = {};
473   Object.keys(obj).forEach(key => {
474     ret[obj[key]] = key;
475   });
476   return ret;
477 }
478
479 export function pictrsAvatarThumbnail(src: string): string {
480   // sample url: http://localhost:8535/pictrs/image/thumbnail256/gs7xuu.jpg
481   let split = src.split('/pictrs/image');
482   let out = `${split[0]}/pictrs/image/${
483     canUseWebP() ? 'webp/' : ''
484   }thumbnail96${split[1]}`;
485   return out;
486 }
487
488 export function showAvatars(): boolean {
489   return (
490     (UserService.Instance.user && UserService.Instance.user.show_avatars) ||
491     !UserService.Instance.user
492   );
493 }
494
495 export function isCakeDay(published: string): boolean {
496   // moment(undefined) or moment.utc(undefined) returns the current date/time
497   // moment(null) or moment.utc(null) returns null
498   const userCreationDate = moment.utc(published || null).local();
499   const currentDate = moment(new Date());
500
501   return (
502     userCreationDate.date() === currentDate.date() &&
503     userCreationDate.month() === currentDate.month() &&
504     userCreationDate.year() !== currentDate.year()
505   );
506 }
507
508 // Converts to image thumbnail
509 export function pictrsImage(hash: string, thumbnail: boolean = false): string {
510   let root = `/pictrs/image`;
511
512   // Necessary for other servers / domains
513   if (hash.includes('pictrs')) {
514     let split = hash.split('/pictrs/image/');
515     root = `${split[0]}/pictrs/image`;
516     hash = split[1];
517   }
518
519   let out = `${root}/${canUseWebP() ? 'webp/' : ''}${
520     thumbnail ? 'thumbnail256/' : ''
521   }${hash}`;
522   return out;
523 }
524
525 export function isCommentType(
526   item: Comment | PrivateMessage | Post
527 ): item is Comment {
528   return (
529     (item as Comment).community_id !== undefined &&
530     (item as Comment).content !== undefined
531   );
532 }
533
534 export function isPostType(
535   item: Comment | PrivateMessage | Post
536 ): item is Post {
537   return (item as Post).stickied !== undefined;
538 }
539
540 export function toast(text: string, background: string = 'success') {
541   let backgroundColor = `var(--${background})`;
542   Toastify({
543     text: text,
544     backgroundColor: backgroundColor,
545     gravity: 'bottom',
546     position: 'left',
547   }).showToast();
548 }
549
550 export function pictrsDeleteToast(
551   clickToDeleteText: string,
552   deletePictureText: string,
553   deleteUrl: string
554 ) {
555   let backgroundColor = `var(--light)`;
556   let toast = Toastify({
557     text: clickToDeleteText,
558     backgroundColor: backgroundColor,
559     gravity: 'top',
560     position: 'right',
561     duration: 10000,
562     onClick: () => {
563       if (toast) {
564         window.location.replace(deleteUrl);
565         alert(deletePictureText);
566         toast.hideToast();
567       }
568     },
569     close: true,
570   }).showToast();
571 }
572
573 interface NotifyInfo {
574   name: string;
575   icon: string;
576   link: string;
577   body: string;
578 }
579
580 export function messageToastify(info: NotifyInfo, router: any) {
581   let htmlBody = info.body ? md.render(info.body) : '';
582   let backgroundColor = `var(--light)`;
583
584   let toast = Toastify({
585     text: `${htmlBody}<br />${info.name}`,
586     avatar: info.icon,
587     backgroundColor: backgroundColor,
588     className: 'text-dark',
589     close: true,
590     gravity: 'top',
591     position: 'right',
592     duration: 5000,
593     onClick: () => {
594       if (toast) {
595         toast.hideToast();
596         router.history.push(info.link);
597       }
598     },
599   }).showToast();
600 }
601
602 export function notifyPost(post: Post, router: any) {
603   let info: NotifyInfo = {
604     name: post.community_name,
605     icon: post.community_icon ? post.community_icon : defaultFavIcon,
606     link: `/post/${post.id}`,
607     body: post.name,
608   };
609   notify(info, router);
610 }
611
612 export function notifyComment(comment: Comment, router: any) {
613   let info: NotifyInfo = {
614     name: comment.creator_name,
615     icon: comment.creator_avatar ? comment.creator_avatar : defaultFavIcon,
616     link: `/post/${comment.post_id}/comment/${comment.id}`,
617     body: comment.content,
618   };
619   notify(info, router);
620 }
621
622 export function notifyPrivateMessage(pm: PrivateMessage, router: any) {
623   let info: NotifyInfo = {
624     name: pm.creator_name,
625     icon: pm.creator_avatar ? pm.creator_avatar : defaultFavIcon,
626     link: `/inbox`,
627     body: pm.content,
628   };
629   notify(info, router);
630 }
631
632 function notify(info: NotifyInfo, router: any) {
633   messageToastify(info, router);
634
635   if (Notification.permission !== 'granted') Notification.requestPermission();
636   else {
637     var notification = new Notification(info.name, {
638       icon: info.icon,
639       body: info.body,
640     });
641
642     notification.onclick = () => {
643       event.preventDefault();
644       router.history.push(info.link);
645     };
646   }
647 }
648
649 // export function setupTribute(): Tribute<{}> {
650 //   return new Tribute({
651 //     noMatchTemplate: function () {
652 //       return '';
653 //     },
654 //     collection: [
655 //       // Emojis
656 //       {
657 //         trigger: ':',
658 //         menuItemTemplate: (item: any) => {
659 //           let shortName = `:${item.original.key}:`;
660 //           return `${item.original.val} ${shortName}`;
661 //         },
662 //         selectTemplate: (item: any) => {
663 //           return `:${item.original.key}:`;
664 //         },
665 //         values: Object.entries(emojiShortName).map(e => {
666 //           return { key: e[1], val: e[0] };
667 //         }),
668 //         allowSpaces: false,
669 //         autocompleteMode: true,
670 //         // TODO
671 //         // menuItemLimit: mentionDropdownFetchLimit,
672 //         menuShowMinLength: 2,
673 //       },
674 //       // Users
675 //       {
676 //         trigger: '@',
677 //         selectTemplate: (item: any) => {
678 //           let link = item.original.local
679 //             ? `[${item.original.key}](/u/${item.original.name})`
680 //             : `[${item.original.key}](/user/${item.original.id})`;
681 //           return link;
682 //         },
683 //         values: (text: string, cb: any) => {
684 //           userSearch(text, (users: any) => cb(users));
685 //         },
686 //         allowSpaces: false,
687 //         autocompleteMode: true,
688 //         // TODO
689 //         // menuItemLimit: mentionDropdownFetchLimit,
690 //         menuShowMinLength: 2,
691 //       },
692
693 //       // Communities
694 //       {
695 //         trigger: '!',
696 //         selectTemplate: (item: any) => {
697 //           let link = item.original.local
698 //             ? `[${item.original.key}](/c/${item.original.name})`
699 //             : `[${item.original.key}](/community/${item.original.id})`;
700 //           return link;
701 //         },
702 //         values: (text: string, cb: any) => {
703 //           communitySearch(text, (communities: any) => cb(communities));
704 //         },
705 //         allowSpaces: false,
706 //         autocompleteMode: true,
707 //         // TODO
708 //         // menuItemLimit: mentionDropdownFetchLimit,
709 //         menuShowMinLength: 2,
710 //       },
711 //     ],
712 //   });
713 // }
714
715 // TODO
716 // let tippyInstance = tippy('[data-tippy-content]');
717
718 export function setupTippy() {
719   // tippyInstance.forEach(e => e.destroy());
720   // tippyInstance = tippy('[data-tippy-content]', {
721   //   delay: [500, 0],
722   //   // Display on "long press"
723   //   touch: ['hold', 500],
724   // });
725 }
726
727 function userSearch(text: string, cb: any) {
728   if (text) {
729     let form: SearchForm = {
730       q: text,
731       type_: SearchType.Users,
732       sort: SortType.TopAll,
733       page: 1,
734       limit: mentionDropdownFetchLimit,
735     };
736
737     WebSocketService.Instance.search(form);
738
739     let userSub = WebSocketService.Instance.subject.subscribe(
740       msg => {
741         let res = wsJsonToRes(msg);
742         if (res.op == UserOperation.Search) {
743           let data = res.data as SearchResponse;
744           let users = data.users.map(u => {
745             return {
746               key: `@${u.name}@${hostname(u.actor_id)}`,
747               name: u.name,
748               local: u.local,
749               id: u.id,
750             };
751           });
752           cb(users);
753           userSub.unsubscribe();
754         }
755       },
756       err => console.error(err),
757       () => console.log('complete')
758     );
759   } else {
760     cb([]);
761   }
762 }
763
764 function communitySearch(text: string, cb: any) {
765   if (text) {
766     let form: SearchForm = {
767       q: text,
768       type_: SearchType.Communities,
769       sort: SortType.TopAll,
770       page: 1,
771       limit: mentionDropdownFetchLimit,
772     };
773
774     WebSocketService.Instance.search(form);
775
776     let communitySub = WebSocketService.Instance.subject.subscribe(
777       msg => {
778         let res = wsJsonToRes(msg);
779         if (res.op == UserOperation.Search) {
780           let data = res.data as SearchResponse;
781           let communities = data.communities.map(c => {
782             return {
783               key: `!${c.name}@${hostname(c.actor_id)}`,
784               name: c.name,
785               local: c.local,
786               id: c.id,
787             };
788           });
789           cb(communities);
790           communitySub.unsubscribe();
791         }
792       },
793       err => console.error(err),
794       () => console.log('complete')
795     );
796   } else {
797     cb([]);
798   }
799 }
800
801 export function getListingTypeFromProps(props: any): ListingType {
802   // TODO
803   return ListingType.All;
804   // return props.match.params.listing_type
805   //   ? routeListingTypeToEnum(props.match.params.listing_type)
806   //   : UserService.Instance.user
807   //   ? Object.values(ListingType)[UserService.Instance.user.default_listing_type]
808   //   : ListingType.All;
809 }
810
811 // TODO might need to add a user setting for this too
812 export function getDataTypeFromProps(props: any): DataType {
813   // TODO
814   return DataType.Post;
815   // return props.match.params.data_type
816   //   ? routeDataTypeToEnum(props.match.params.data_type)
817   //   : DataType.Post;
818 }
819
820 export function getSortTypeFromProps(props: any): SortType {
821   // TODO
822   return SortType.Active;
823   // return props.match.params.sort
824   //   ? routeSortTypeToEnum(props.match.params.sort)
825   //   : UserService.Instance.user
826   //   ? Object.values(SortType)[UserService.Instance.user.default_sort_type]
827   //   : SortType.Active;
828 }
829
830 export function getPageFromProps(props: any): number {
831   // TODO
832   return 1;
833   // return props.match.params.page ? Number(props.match.params.page) : 1;
834 }
835
836 export function editCommentRes(data: CommentResponse, comments: Comment[]) {
837   let found = comments.find(c => c.id == data.comment.id);
838   if (found) {
839     found.content = data.comment.content;
840     found.updated = data.comment.updated;
841     found.removed = data.comment.removed;
842     found.deleted = data.comment.deleted;
843     found.upvotes = data.comment.upvotes;
844     found.downvotes = data.comment.downvotes;
845     found.score = data.comment.score;
846   }
847 }
848
849 export function saveCommentRes(data: CommentResponse, comments: Comment[]) {
850   let found = comments.find(c => c.id == data.comment.id);
851   if (found) {
852     found.saved = data.comment.saved;
853   }
854 }
855
856 export function createCommentLikeRes(
857   data: CommentResponse,
858   comments: Comment[]
859 ) {
860   let found: Comment = comments.find(c => c.id === data.comment.id);
861   if (found) {
862     found.score = data.comment.score;
863     found.upvotes = data.comment.upvotes;
864     found.downvotes = data.comment.downvotes;
865     if (data.comment.my_vote !== null) {
866       found.my_vote = data.comment.my_vote;
867     }
868   }
869 }
870
871 export function createPostLikeFindRes(data: PostResponse, posts: Post[]) {
872   let found = posts.find(c => c.id == data.post.id);
873   if (found) {
874     createPostLikeRes(data, found);
875   }
876 }
877
878 export function createPostLikeRes(data: PostResponse, post: Post) {
879   if (post) {
880     post.score = data.post.score;
881     post.upvotes = data.post.upvotes;
882     post.downvotes = data.post.downvotes;
883     if (data.post.my_vote !== null) {
884       post.my_vote = data.post.my_vote;
885     }
886   }
887 }
888
889 export function editPostFindRes(data: PostResponse, posts: Post[]) {
890   let found = posts.find(c => c.id == data.post.id);
891   if (found) {
892     editPostRes(data, found);
893   }
894 }
895
896 export function editPostRes(data: PostResponse, post: Post) {
897   if (post) {
898     post.url = data.post.url;
899     post.name = data.post.name;
900     post.nsfw = data.post.nsfw;
901     post.deleted = data.post.deleted;
902     post.removed = data.post.removed;
903     post.stickied = data.post.stickied;
904     post.body = data.post.body;
905     post.locked = data.post.locked;
906   }
907 }
908
909 export function commentsToFlatNodes(comments: Comment[]): CommentNodeI[] {
910   let nodes: CommentNodeI[] = [];
911   for (let comment of comments) {
912     nodes.push({ comment: comment });
913   }
914   return nodes;
915 }
916
917 export function commentSort(tree: CommentNodeI[], sort: CommentSortType) {
918   // First, put removed and deleted comments at the bottom, then do your other sorts
919   if (sort == CommentSortType.Top) {
920     tree.sort(
921       (a, b) =>
922         +a.comment.removed - +b.comment.removed ||
923         +a.comment.deleted - +b.comment.deleted ||
924         b.comment.score - a.comment.score
925     );
926   } else if (sort == CommentSortType.New) {
927     tree.sort(
928       (a, b) =>
929         +a.comment.removed - +b.comment.removed ||
930         +a.comment.deleted - +b.comment.deleted ||
931         b.comment.published.localeCompare(a.comment.published)
932     );
933   } else if (sort == CommentSortType.Old) {
934     tree.sort(
935       (a, b) =>
936         +a.comment.removed - +b.comment.removed ||
937         +a.comment.deleted - +b.comment.deleted ||
938         a.comment.published.localeCompare(b.comment.published)
939     );
940   } else if (sort == CommentSortType.Hot) {
941     tree.sort(
942       (a, b) =>
943         +a.comment.removed - +b.comment.removed ||
944         +a.comment.deleted - +b.comment.deleted ||
945         hotRankComment(b.comment) - hotRankComment(a.comment)
946     );
947   }
948
949   // Go through the children recursively
950   for (let node of tree) {
951     if (node.children) {
952       commentSort(node.children, sort);
953     }
954   }
955 }
956
957 export function commentSortSortType(tree: CommentNodeI[], sort: SortType) {
958   commentSort(tree, convertCommentSortType(sort));
959 }
960
961 function convertCommentSortType(sort: SortType): CommentSortType {
962   if (
963     sort == SortType.TopAll ||
964     sort == SortType.TopDay ||
965     sort == SortType.TopWeek ||
966     sort == SortType.TopMonth ||
967     sort == SortType.TopYear
968   ) {
969     return CommentSortType.Top;
970   } else if (sort == SortType.New) {
971     return CommentSortType.New;
972   } else if (sort == SortType.Hot || sort == SortType.Active) {
973     return CommentSortType.Hot;
974   } else {
975     return CommentSortType.Hot;
976   }
977 }
978
979 export function postSort(
980   posts: Post[],
981   sort: SortType,
982   communityType: boolean
983 ) {
984   // First, put removed and deleted comments at the bottom, then do your other sorts
985   if (
986     sort == SortType.TopAll ||
987     sort == SortType.TopDay ||
988     sort == SortType.TopWeek ||
989     sort == SortType.TopMonth ||
990     sort == SortType.TopYear
991   ) {
992     posts.sort(
993       (a, b) =>
994         +a.removed - +b.removed ||
995         +a.deleted - +b.deleted ||
996         (communityType && +b.stickied - +a.stickied) ||
997         b.score - a.score
998     );
999   } else if (sort == SortType.New) {
1000     posts.sort(
1001       (a, b) =>
1002         +a.removed - +b.removed ||
1003         +a.deleted - +b.deleted ||
1004         (communityType && +b.stickied - +a.stickied) ||
1005         b.published.localeCompare(a.published)
1006     );
1007   } else if (sort == SortType.Hot) {
1008     posts.sort(
1009       (a, b) =>
1010         +a.removed - +b.removed ||
1011         +a.deleted - +b.deleted ||
1012         (communityType && +b.stickied - +a.stickied) ||
1013         b.hot_rank - a.hot_rank
1014     );
1015   } else if (sort == SortType.Active) {
1016     posts.sort(
1017       (a, b) =>
1018         +a.removed - +b.removed ||
1019         +a.deleted - +b.deleted ||
1020         (communityType && +b.stickied - +a.stickied) ||
1021         b.hot_rank_active - a.hot_rank_active
1022     );
1023   }
1024 }
1025
1026 export const colorList: string[] = [
1027   hsl(0),
1028   hsl(100),
1029   hsl(150),
1030   hsl(200),
1031   hsl(250),
1032   hsl(300),
1033 ];
1034
1035 function hsl(num: number) {
1036   return `hsla(${num}, 35%, 50%, 1)`;
1037 }
1038
1039 function randomHsl() {
1040   return `hsla(${Math.random() * 360}, 100%, 50%, 1)`;
1041 }
1042
1043 export function previewLines(
1044   text: string,
1045   maxChars: number = 300,
1046   maxLines: number = 1
1047 ): string {
1048   return (
1049     text
1050       .slice(0, maxChars)
1051       .split('\n')
1052       // Use lines * 2 because markdown requires 2 lines
1053       .slice(0, maxLines * 2)
1054       .join('\n') + '...'
1055   );
1056 }
1057
1058 export function hostname(url: string): string {
1059   let cUrl = new URL(url);
1060   // TODO
1061   return `${cUrl.hostname}:${cUrl.port}`;
1062   // return window.location.port
1063   //   ? `${cUrl.hostname}:${cUrl.port}`
1064   //   : `${cUrl.hostname}`;
1065 }
1066
1067 function canUseWebP() {
1068   // TODO pictshare might have a webp conversion bug, try disabling this
1069   return false;
1070
1071   // var elem = document.createElement('canvas');
1072   // if (!!(elem.getContext && elem.getContext('2d'))) {
1073   //   var testString = !(window.mozInnerScreenX == null) ? 'png' : 'webp';
1074   //   // was able or not to get WebP representation
1075   //   return (
1076   //     elem.toDataURL('image/webp').startsWith('data:image/' + testString)
1077   //   );
1078   // }
1079
1080   // // very old browser like IE 8, canvas not supported
1081   // return false;
1082 }
1083
1084 export function validTitle(title?: string): boolean {
1085   // Initial title is null, minimum length is taken care of by textarea's minLength={3}
1086   if (title === null || title.length < 3) return true;
1087
1088   const regex = new RegExp(/.*\S.*/, 'g');
1089
1090   return regex.test(title);
1091 }
1092
1093 export function siteBannerCss(banner: string): string {
1094   return ` \
1095     background-image: linear-gradient( rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.8) ) ,url("${banner}"); \
1096     background-attachment: fixed; \
1097     background-position: top; \
1098     background-repeat: no-repeat; \
1099     background-size: 100% cover; \
1100
1101     width: 100%; \
1102     max-height: 100vh; \
1103     `;
1104 }
1105
1106 export function isBrowser() {
1107   return typeof window !== 'undefined';
1108 }
1109
1110 export function setAuth(obj: any, auth: string) {
1111   if (auth) {
1112     obj.auth = auth;
1113   }
1114 }