]> Untitled Git - lemmy.git/blob - ui/src/utils.ts
Merge remote-tracking branch 'nutomic/http-api' into dessalines-http-api
[lemmy.git] / ui / src / utils.ts
1 import 'moment/locale/es';
2 import 'moment/locale/eo';
3 import 'moment/locale/de';
4 import 'moment/locale/zh-cn';
5 import 'moment/locale/fr';
6 import 'moment/locale/sv';
7 import 'moment/locale/ru';
8 import 'moment/locale/nl';
9 import 'moment/locale/it';
10
11 import {
12   UserOperation,
13   Comment,
14   User,
15   SortType,
16   ListingType,
17   SearchType,
18   WebSocketResponse,
19   WebSocketJsonResponse,
20 } from './interfaces';
21 import { UserService } from './services/UserService';
22 import markdown_it from 'markdown-it';
23 import markdownitEmoji from 'markdown-it-emoji/light';
24 import markdown_it_container from 'markdown-it-container';
25 import * as twemoji from 'twemoji';
26 import * as emojiShortName from 'emoji-short-name';
27
28 export const repoUrl = 'https://github.com/dessalines/lemmy';
29 export const markdownHelpUrl = 'https://commonmark.org/help/';
30 export const archiveUrl = 'https://archive.is';
31
32 export const postRefetchSeconds: number = 60 * 1000;
33 export const fetchLimit: number = 20;
34 export const mentionDropdownFetchLimit = 6;
35
36 export function randomStr() {
37   return Math.random()
38     .toString(36)
39     .replace(/[^a-z]+/g, '')
40     .substr(2, 10);
41 }
42
43 export function wsJsonToRes(msg: WebSocketJsonResponse): WebSocketResponse {
44   let opStr: string = msg.op;
45   return {
46     op: UserOperation[opStr],
47     data: msg.data,
48   };
49 }
50
51 export const md = new markdown_it({
52   html: false,
53   linkify: true,
54   typographer: true,
55 })
56   .use(markdown_it_container, 'spoiler', {
57     validate: function(params: any) {
58       return params.trim().match(/^spoiler\s+(.*)$/);
59     },
60
61     render: function(tokens: any, idx: any) {
62       var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/);
63
64       if (tokens[idx].nesting === 1) {
65         // opening tag
66         return `<details><summary> ${md.utils.escapeHtml(m[1])} </summary>\n`;
67       } else {
68         // closing tag
69         return '</details>\n';
70       }
71     },
72   })
73   .use(markdownitEmoji, {
74     defs: objectFlip(emojiShortName),
75   });
76
77 md.renderer.rules.emoji = function(token, idx) {
78   return twemoji.parse(token[idx].content);
79 };
80
81 export function hotRank(comment: Comment): number {
82   // Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
83
84   let date: Date = new Date(comment.published + 'Z'); // Add Z to convert from UTC date
85   let now: Date = new Date();
86   let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
87
88   let rank =
89     (10000 * Math.log10(Math.max(1, 3 + comment.score))) /
90     Math.pow(hoursElapsed + 2, 1.8);
91
92   // console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`);
93
94   return rank;
95 }
96
97 export function mdToHtml(text: string) {
98   return { __html: md.render(text) };
99 }
100
101 export function getUnixTime(text: string): number {
102   return text ? new Date(text).getTime() / 1000 : undefined;
103 }
104
105 export function addTypeInfo<T>(
106   arr: Array<T>,
107   name: string
108 ): Array<{ type_: string; data: T }> {
109   return arr.map(e => {
110     return { type_: name, data: e };
111   });
112 }
113
114 export function canMod(
115   user: User,
116   modIds: Array<number>,
117   creator_id: number,
118   onSelf: boolean = false
119 ): boolean {
120   // You can do moderator actions only on the mods added after you.
121   if (user) {
122     let yourIndex = modIds.findIndex(id => id == user.id);
123     if (yourIndex == -1) {
124       return false;
125     } else {
126       // onSelf +1 on mod actions not for yourself, IE ban, remove, etc
127       modIds = modIds.slice(0, yourIndex + (onSelf ? 0 : 1));
128       return !modIds.includes(creator_id);
129     }
130   } else {
131     return false;
132   }
133 }
134
135 export function isMod(modIds: Array<number>, creator_id: number): boolean {
136   return modIds.includes(creator_id);
137 }
138
139 var imageRegex = new RegExp(
140   `(http)?s?:?(\/\/[^"']*\.(?:png|jpg|jpeg|gif|png|svg))`
141 );
142 var videoRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:mp4))`);
143
144 export function isImage(url: string) {
145   return imageRegex.test(url);
146 }
147
148 export function isVideo(url: string) {
149   return videoRegex.test(url);
150 }
151
152 export function validURL(str: string) {
153   try {
154     return !!new URL(str);
155   } catch {
156     return false;
157   }
158 }
159
160 export function validEmail(email: string) {
161   let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
162   return re.test(String(email).toLowerCase());
163 }
164
165 export function capitalizeFirstLetter(str: string): string {
166   return str.charAt(0).toUpperCase() + str.slice(1);
167 }
168
169 export function routeSortTypeToEnum(sort: string): SortType {
170   if (sort == 'new') {
171     return SortType.New;
172   } else if (sort == 'hot') {
173     return SortType.Hot;
174   } else if (sort == 'topday') {
175     return SortType.TopDay;
176   } else if (sort == 'topweek') {
177     return SortType.TopWeek;
178   } else if (sort == 'topmonth') {
179     return SortType.TopMonth;
180   } else if (sort == 'topyear') {
181     return SortType.TopYear;
182   } else if (sort == 'topall') {
183     return SortType.TopAll;
184   }
185 }
186
187 export function routeListingTypeToEnum(type: string): ListingType {
188   return ListingType[capitalizeFirstLetter(type)];
189 }
190
191 export function routeSearchTypeToEnum(type: string): SearchType {
192   return SearchType[capitalizeFirstLetter(type)];
193 }
194
195 export async function getPageTitle(url: string) {
196   let res = await fetch(`https://textance.herokuapp.com/title/${url}`);
197   let data = await res.text();
198   return data;
199 }
200
201 export function debounce(
202   func: any,
203   wait: number = 1000,
204   immediate: boolean = false
205 ) {
206   // 'private' variable for instance
207   // The returned function will be able to reference this due to closure.
208   // Each call to the returned function will share this common timer.
209   let timeout: number;
210
211   // Calling debounce returns a new anonymous function
212   return function() {
213     // reference the context and args for the setTimeout function
214     var context = this,
215       args = arguments;
216
217     // Should the function be called now? If immediate is true
218     //   and not already in a timeout then the answer is: Yes
219     var callNow = immediate && !timeout;
220
221     // This is the basic debounce behaviour where you can call this
222     //   function several times, but it will only execute once
223     //   [before or after imposing a delay].
224     //   Each time the returned function is called, the timer starts over.
225     clearTimeout(timeout);
226
227     // Set the new timeout
228     timeout = setTimeout(function() {
229       // Inside the timeout function, clear the timeout variable
230       // which will let the next execution run when in 'immediate' mode
231       timeout = null;
232
233       // Check if the function already ran with the immediate flag
234       if (!immediate) {
235         // Call the original function with apply
236         // apply lets you define the 'this' object as well as the arguments
237         //    (both captured before setTimeout)
238         func.apply(context, args);
239       }
240     }, wait);
241
242     // Immediate mode and no wait timer? Execute the function..
243     if (callNow) func.apply(context, args);
244   };
245 }
246
247 export const languages = [
248   { code: 'en', name: 'English' },
249   { code: 'eo', name: 'Esperanto' },
250   { code: 'es', name: 'Español' },
251   { code: 'de', name: 'Deutsch' },
252   { code: 'zh', name: '中文' },
253   { code: 'fr', name: 'Français' },
254   { code: 'sv', name: 'Svenska' },
255   { code: 'ru', name: 'Русский' },
256   { code: 'nl', name: 'Nederlands' },
257   { code: 'it', name: 'Italiano' },
258 ];
259
260 export function getLanguage(): string {
261   let user = UserService.Instance.user;
262   let lang = user && user.lang ? user.lang : 'browser';
263
264   if (lang == 'browser') {
265     return getBrowserLanguage();
266   } else {
267     return lang;
268   }
269 }
270
271 export function getBrowserLanguage(): string {
272   return navigator.language;
273 }
274
275 export function getMomentLanguage(): string {
276   let lang = getLanguage();
277   if (lang.startsWith('zh')) {
278     lang = 'zh-cn';
279   } else if (lang.startsWith('sv')) {
280     lang = 'sv';
281   } else if (lang.startsWith('fr')) {
282     lang = 'fr';
283   } else if (lang.startsWith('de')) {
284     lang = 'de';
285   } else if (lang.startsWith('ru')) {
286     lang = 'ru';
287   } else if (lang.startsWith('es')) {
288     lang = 'es';
289   } else if (lang.startsWith('eo')) {
290     lang = 'eo';
291   } else if (lang.startsWith('nl')) {
292     lang = 'nl';
293   } else if (lang.startsWith('it')) {
294     lang = 'it';
295   } else {
296     lang = 'en';
297   }
298   return lang;
299 }
300
301 export const themes = [
302   'litera',
303   'minty',
304   'solar',
305   'united',
306   'cyborg',
307   'darkly',
308   'journal',
309   'sketchy',
310   'vaporwave',
311   'vaporwave-dark',
312 ];
313
314 export function setTheme(theme: string = 'darkly') {
315   // unload all the other themes
316   for (var i = 0; i < themes.length; i++) {
317     let styleSheet = document.getElementById(themes[i]);
318     if (styleSheet) {
319       styleSheet.setAttribute('disabled', 'disabled');
320     }
321   }
322
323   // Load the theme dynamically
324   if (!document.getElementById(theme)) {
325     var head = document.getElementsByTagName('head')[0];
326     var link = document.createElement('link');
327     link.id = theme;
328     link.rel = 'stylesheet';
329     link.type = 'text/css';
330     link.href = `/static/assets/css/themes/${theme}.min.css`;
331     link.media = 'all';
332     head.appendChild(link);
333   }
334   document.getElementById(theme).removeAttribute('disabled');
335 }
336
337 export function objectFlip(obj: any) {
338   const ret = {};
339   Object.keys(obj).forEach(key => {
340     ret[obj[key]] = key;
341   });
342   return ret;
343 }
344
345 export function pictshareAvatarThumbnail(src: string): string {
346   // sample url: http://localhost:8535/pictshare/gs7xuu.jpg
347   let split = src.split('pictshare');
348   let out = `${split[0]}pictshare/96x96${split[1]}`;
349   return out;
350 }
351
352 export function showAvatars(): boolean {
353   return (
354     (UserService.Instance.user && UserService.Instance.user.show_avatars) ||
355     !UserService.Instance.user
356   );
357 }
358
359 /// Converts to image thumbnail (only supports pictshare currently)
360 export function imageThumbnailer(url: string): string {
361   let split = url.split('pictshare');
362   if (split.length > 1) {
363     let out = `${split[0]}pictshare/140x140${split[1]}`;
364     return out;
365   } else {
366     return url;
367   }
368 }