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