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