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