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