import 'moment/locale/fi';
import 'moment/locale/ca';
import 'moment/locale/fa';
+import 'moment/locale/pl';
import 'moment/locale/pt-br';
import 'moment/locale/ja';
import 'moment/locale/ka';
import 'moment/locale/hi';
+import 'moment/locale/gl';
+import 'moment/locale/tr';
+import 'moment/locale/hu';
+import 'moment/locale/uk';
+import 'moment/locale/sq';
+import 'moment/locale/km';
+import 'moment/locale/ga';
+import 'moment/locale/sr';
+import 'moment/locale/ko';
import {
UserOperation,
PrivateMessage,
User,
SortType,
- CommentSortType,
ListingType,
- DataType,
SearchType,
WebSocketResponse,
WebSocketJsonResponse,
SearchResponse,
CommentResponse,
PostResponse,
-} from './interfaces';
+} from 'lemmy-js-client';
+
+import { CommentSortType, DataType } from './interfaces';
import { UserService, WebSocketService } from './services';
import Tribute from 'tributejs/src/Tribute.js';
import markdown_it from 'markdown-it';
+import markdown_it_sub from 'markdown-it-sub';
+import markdown_it_sup from 'markdown-it-sup';
import markdownitEmoji from 'markdown-it-emoji/light';
import markdown_it_container from 'markdown-it-container';
-import twemoji from 'twemoji';
import emojiShortName from 'emoji-short-name';
import Toastify from 'toastify-js';
import tippy from 'tippy.js';
-import EmojiButton from '@joeattardi/emoji-button';
+import moment from 'moment';
+export const favIconUrl = '/static/assets/favicon.svg';
+export const favIconPngUrl = '/static/assets/apple-touch-icon.png';
+export const defaultFavIcon = `${window.location.protocol}//${window.location.host}${favIconPngUrl}`;
export const repoUrl = 'https://github.com/LemmyNet/lemmy';
export const helpGuideUrl = '/docs/about_guide.html';
export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
export const archiveUrl = 'https://archive.is';
+export const elementUrl = 'https://element.io/';
export const postRefetchSeconds: number = 60 * 1000;
export const fetchLimit: number = 20;
{ code: 'eo', name: 'Esperanto' },
{ code: 'es', name: 'Español' },
{ code: 'de', name: 'Deutsch' },
+ { code: 'ga', name: 'Gaeilge' },
+ { code: 'gl', name: 'Galego' },
+ { code: 'hu', name: 'Magyar Nyelv' },
{ code: 'ka', name: 'ქართული ენა' },
+ { code: 'ko', name: '한국어' },
+ { code: 'km', name: 'ភាសាខ្មែរ' },
{ code: 'hi', name: 'मानक हिन्दी' },
{ code: 'fa', name: 'فارسی' },
{ code: 'ja', name: '日本語' },
+ { code: 'pl', name: 'Polski' },
{ code: 'pt_BR', name: 'Português Brasileiro' },
{ code: 'zh', name: '中文' },
{ code: 'fi', name: 'Suomi' },
{ code: 'fr', name: 'Français' },
{ code: 'sv', name: 'Svenska' },
+ { code: 'sq', name: 'Shqip' },
+ { code: 'sr_Latn', name: 'srpski' },
+ { code: 'tr', name: 'Türkçe' },
+ { code: 'uk', name: 'Українська Mова' },
{ code: 'ru', name: 'Русский' },
{ code: 'nl', name: 'Nederlands' },
{ code: 'it', name: 'Italiano' },
'vaporwave',
'vaporwave-dark',
'i386',
+ 'litely',
];
-export const emojiPicker = new EmojiButton({
- // Use the emojiShortName from native
- style: 'twemoji',
- theme: 'dark',
- position: 'auto-start',
- // TODO i18n
-});
+const DEFAULT_ALPHABET =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
-export function randomStr() {
- return Math.random()
- .toString(36)
- .replace(/[^a-z]+/g, '')
- .substr(2, 10);
+function getRandomCharFromAlphabet(alphabet: string): string {
+ return alphabet.charAt(Math.floor(Math.random() * alphabet.length));
+}
+
+export function randomStr(
+ idDesiredLength: number = 20,
+ alphabet = DEFAULT_ALPHABET
+): string {
+ /**
+ * Create n-long array and map it to random chars from given alphabet.
+ * Then join individual chars as string
+ */
+ return Array.from({ length: idDesiredLength })
+ .map(() => {
+ return getRandomCharFromAlphabet(alphabet);
+ })
+ .join('');
}
export function wsJsonToRes(msg: WebSocketJsonResponse): WebSocketResponse {
linkify: true,
typographer: true,
})
+ .use(markdown_it_sub)
+ .use(markdown_it_sup)
.use(markdown_it_container, 'spoiler', {
validate: function (params: any) {
return params.trim().match(/^spoiler\s+(.*)$/);
defs: objectFlip(emojiShortName),
});
-md.renderer.rules.emoji = function (token, idx) {
- return twemoji.parse(token[idx].content);
-};
-
export function hotRankComment(comment: Comment): number {
return hotRank(comment.score, comment.published);
}
}
export function routeSortTypeToEnum(sort: string): SortType {
- if (sort == 'new') {
- return SortType.New;
- } else if (sort == 'hot') {
- return SortType.Hot;
- } else if (sort == 'topday') {
- return SortType.TopDay;
- } else if (sort == 'topweek') {
- return SortType.TopWeek;
- } else if (sort == 'topmonth') {
- return SortType.TopMonth;
- } else if (sort == 'topyear') {
- return SortType.TopYear;
- } else if (sort == 'topall') {
- return SortType.TopAll;
- }
+ return SortType[sort];
}
export function routeListingTypeToEnum(type: string): ListingType {
- return ListingType[capitalizeFirstLetter(type)];
+ return ListingType[type];
}
export function routeDataTypeToEnum(type: string): DataType {
};
}
-export function getLanguage(): string {
+export function getLanguage(override?: string): string {
let user = UserService.Instance.user;
- let lang = user && user.lang ? user.lang : 'browser';
+ let lang = override || (user && user.lang ? user.lang : 'browser');
if (lang == 'browser') {
return getBrowserLanguage();
lang = 'ca';
} else if (lang.startsWith('fa')) {
lang = 'fa';
+ } else if (lang.startsWith('pl')) {
+ lang = 'pl';
} else if (lang.startsWith('pt')) {
lang = 'pt-br';
} else if (lang.startsWith('ja')) {
lang = 'el';
} else if (lang.startsWith('eu')) {
lang = 'eu';
+ } else if (lang.startsWith('gl')) {
+ lang = 'gl';
+ } else if (lang.startsWith('tr')) {
+ lang = 'tr';
+ } else if (lang.startsWith('hu')) {
+ lang = 'hu';
+ } else if (lang.startsWith('uk')) {
+ lang = 'uk';
+ } else if (lang.startsWith('sq')) {
+ lang = 'sq';
+ } else if (lang.startsWith('km')) {
+ lang = 'km';
+ } else if (lang.startsWith('ga')) {
+ lang = 'ga';
+ } else if (lang.startsWith('sr')) {
+ lang = 'sr';
+ } else if (lang.startsWith('ko')) {
+ lang = 'ko';
} else {
lang = 'en';
}
return lang;
}
-export function setTheme(theme: string = 'darkly') {
+export function setTheme(theme: string = 'darkly', loggedIn: boolean = false) {
// unload all the other themes
for (var i = 0; i < themes.length; i++) {
let styleSheet = document.getElementById(themes[i]);
}
}
- // Load the theme dynamically
- let cssLoc = `/static/assets/css/themes/${theme}.min.css`;
- loadCss(theme, cssLoc);
- document.getElementById(theme).removeAttribute('disabled');
+ // if the user is not logged in, we load the default themes and let the browser decide
+ if (!loggedIn) {
+ document.getElementById('default-light').removeAttribute('disabled');
+ document.getElementById('default-dark').removeAttribute('disabled');
+ } else {
+ document
+ .getElementById('default-light')
+ .setAttribute('disabled', 'disabled');
+ document
+ .getElementById('default-dark')
+ .setAttribute('disabled', 'disabled');
+
+ // Load the theme dynamically
+ let cssLoc = `/static/assets/css/themes/${theme}.min.css`;
+ loadCss(theme, cssLoc);
+ document.getElementById(theme).removeAttribute('disabled');
+ }
}
export function loadCss(id: string, loc: string) {
return ret;
}
-export function pictshareAvatarThumbnail(src: string): string {
- // sample url: http://localhost:8535/pictshare/gs7xuu.jpg
- let split = src.split('pictshare');
- let out = `${split[0]}pictshare/${canUseWebP() ? 'webp/' : ''}96${split[1]}`;
+export function pictrsAvatarThumbnail(src: string): string {
+ // sample url: http://localhost:8535/pictrs/image/thumbnail256/gs7xuu.jpg
+ let split = src.split('/pictrs/image');
+ let out = `${split[0]}/pictrs/image/${
+ canUseWebP() ? 'webp/' : ''
+ }thumbnail96${split[1]}`;
return out;
}
);
}
+export function isCakeDay(published: string): boolean {
+ // moment(undefined) or moment.utc(undefined) returns the current date/time
+ // moment(null) or moment.utc(null) returns null
+ const userCreationDate = moment.utc(published || null).local();
+ const currentDate = moment(new Date());
+
+ return (
+ userCreationDate.date() === currentDate.date() &&
+ userCreationDate.month() === currentDate.month() &&
+ userCreationDate.year() !== currentDate.year()
+ );
+}
+
// Converts to image thumbnail
-export function pictshareImage(
- hash: string,
- thumbnail: boolean = false
-): string {
- let root = `/pictshare`;
+export function pictrsImage(hash: string, thumbnail: boolean = false): string {
+ let root = `/pictrs/image`;
// Necessary for other servers / domains
- if (hash.includes('pictshare')) {
- let split = hash.split('/pictshare/');
- root = `${split[0]}/pictshare`;
+ if (hash.includes('pictrs')) {
+ let split = hash.split('/pictrs/image/');
+ root = `${split[0]}/pictrs/image`;
hash = split[1];
}
let out = `${root}/${canUseWebP() ? 'webp/' : ''}${
- thumbnail ? '192/' : ''
+ thumbnail ? 'thumbnail256/' : ''
}${hash}`;
return out;
}
-export function isCommentType(item: Comment | PrivateMessage): item is Comment {
- return (item as Comment).community_id !== undefined;
+export function isCommentType(
+ item: Comment | PrivateMessage | Post
+): item is Comment {
+ return (
+ (item as Comment).community_id !== undefined &&
+ (item as Comment).content !== undefined
+ );
+}
+
+export function isPostType(
+ item: Comment | PrivateMessage | Post
+): item is Post {
+ return (item as Post).stickied !== undefined;
}
export function toast(text: string, background: string = 'success') {
}).showToast();
}
-export function messageToastify(
- creator: string,
- avatar: string,
- body: string,
- link: string,
- router: any
+export function pictrsDeleteToast(
+ clickToDeleteText: string,
+ deletePictureText: string,
+ deleteUrl: string
) {
let backgroundColor = `var(--light)`;
+ let toast = Toastify({
+ text: clickToDeleteText,
+ backgroundColor: backgroundColor,
+ gravity: 'top',
+ position: 'right',
+ duration: 10000,
+ onClick: () => {
+ if (toast) {
+ window.location.replace(deleteUrl);
+ alert(deletePictureText);
+ toast.hideToast();
+ }
+ },
+ close: true,
+ }).showToast();
+}
+
+interface NotifyInfo {
+ name: string;
+ icon: string;
+ link: string;
+ body: string;
+}
+
+export function messageToastify(info: NotifyInfo, router: any) {
+ let htmlBody = info.body ? md.render(info.body) : '';
+ let backgroundColor = `var(--light)`;
let toast = Toastify({
- text: `${body}<br />${creator}`,
- avatar: avatar,
+ text: `${htmlBody}<br />${info.name}`,
+ avatar: info.icon,
backgroundColor: backgroundColor,
className: 'text-dark',
close: true,
gravity: 'top',
position: 'right',
- duration: 0,
+ duration: 5000,
onClick: () => {
if (toast) {
toast.hideToast();
- router.history.push(link);
+ router.history.push(info.link);
}
},
}).showToast();
}
+export function notifyPost(post: Post, router: any) {
+ let info: NotifyInfo = {
+ name: post.community_name,
+ icon: post.community_icon ? post.community_icon : defaultFavIcon,
+ link: `/post/${post.id}`,
+ body: post.name,
+ };
+ notify(info, router);
+}
+
+export function notifyComment(comment: Comment, router: any) {
+ let info: NotifyInfo = {
+ name: comment.creator_name,
+ icon: comment.creator_avatar ? comment.creator_avatar : defaultFavIcon,
+ link: `/post/${comment.post_id}/comment/${comment.id}`,
+ body: comment.content,
+ };
+ notify(info, router);
+}
+
+export function notifyPrivateMessage(pm: PrivateMessage, router: any) {
+ let info: NotifyInfo = {
+ name: pm.creator_name,
+ icon: pm.creator_avatar ? pm.creator_avatar : defaultFavIcon,
+ link: `/inbox`,
+ body: pm.content,
+ };
+ notify(info, router);
+}
+
+function notify(info: NotifyInfo, router: any) {
+ messageToastify(info, router);
+
+ if (Notification.permission !== 'granted') Notification.requestPermission();
+ else {
+ var notification = new Notification(info.name, {
+ icon: info.icon,
+ body: info.body,
+ });
+
+ notification.onclick = () => {
+ event.preventDefault();
+ router.history.push(info.link);
+ };
+ }
+}
+
export function setupTribute(): Tribute {
return new Tribute({
+ noMatchTemplate: function () {
+ return '';
+ },
collection: [
// Emojis
{
trigger: ':',
menuItemTemplate: (item: any) => {
let shortName = `:${item.original.key}:`;
- let twemojiIcon = twemoji.parse(item.original.val);
- return `${twemojiIcon} ${shortName}`;
+ return `${item.original.val} ${shortName}`;
},
selectTemplate: (item: any) => {
return `:${item.original.key}:`;
{
trigger: '@',
selectTemplate: (item: any) => {
- return `[/u/${item.original.key}](/u/${item.original.key})`;
+ let link = item.original.local
+ ? `[${item.original.key}](/u/${item.original.name})`
+ : `[${item.original.key}](/user/${item.original.id})`;
+ return link;
},
values: (text: string, cb: any) => {
userSearch(text, (users: any) => cb(users));
// Communities
{
- trigger: '#',
+ trigger: '!',
selectTemplate: (item: any) => {
- return `[/c/${item.original.key}](/c/${item.original.key})`;
+ let link = item.original.local
+ ? `[${item.original.key}](/c/${item.original.name})`
+ : `[${item.original.key}](/community/${item.original.id})`;
+ return link;
},
values: (text: string, cb: any) => {
communitySearch(text, (communities: any) => cb(communities));
if (text) {
let form: SearchForm = {
q: text,
- type_: SearchType[SearchType.Users],
- sort: SortType[SortType.TopAll],
+ type_: SearchType.Users,
+ sort: SortType.TopAll,
page: 1,
limit: mentionDropdownFetchLimit,
};
WebSocketService.Instance.search(form);
- this.userSub = WebSocketService.Instance.subject.subscribe(
+ let userSub = WebSocketService.Instance.subject.subscribe(
msg => {
let res = wsJsonToRes(msg);
if (res.op == UserOperation.Search) {
let data = res.data as SearchResponse;
let users = data.users.map(u => {
- return { key: u.name };
+ return {
+ key: `@${u.name}@${hostname(u.actor_id)}`,
+ name: u.name,
+ local: u.local,
+ id: u.id,
+ };
});
cb(users);
- this.userSub.unsubscribe();
+ userSub.unsubscribe();
}
},
err => console.error(err),
if (text) {
let form: SearchForm = {
q: text,
- type_: SearchType[SearchType.Communities],
- sort: SortType[SortType.TopAll],
+ type_: SearchType.Communities,
+ sort: SortType.TopAll,
page: 1,
limit: mentionDropdownFetchLimit,
};
WebSocketService.Instance.search(form);
- this.communitySub = WebSocketService.Instance.subject.subscribe(
+ let communitySub = WebSocketService.Instance.subject.subscribe(
msg => {
let res = wsJsonToRes(msg);
if (res.op == UserOperation.Search) {
let data = res.data as SearchResponse;
- let communities = data.communities.map(u => {
- return { key: u.name };
+ let communities = data.communities.map(c => {
+ return {
+ key: `!${c.name}@${hostname(c.actor_id)}`,
+ name: c.name,
+ local: c.local,
+ id: c.id,
+ };
});
cb(communities);
- this.communitySub.unsubscribe();
+ communitySub.unsubscribe();
}
},
err => console.error(err),
return props.match.params.listing_type
? routeListingTypeToEnum(props.match.params.listing_type)
: UserService.Instance.user
- ? UserService.Instance.user.default_listing_type
+ ? Object.values(ListingType)[UserService.Instance.user.default_listing_type]
: ListingType.All;
}
return props.match.params.sort
? routeSortTypeToEnum(props.match.params.sort)
: UserService.Instance.user
- ? UserService.Instance.user.default_sort_type
- : SortType.Hot;
+ ? Object.values(SortType)[UserService.Instance.user.default_sort_type]
+ : SortType.Active;
}
export function getPageFromProps(props: any): number {
post.url = data.post.url;
post.name = data.post.name;
post.nsfw = data.post.nsfw;
+ post.deleted = data.post.deleted;
+ post.removed = data.post.removed;
+ post.stickied = data.post.stickied;
+ post.body = data.post.body;
+ post.locked = data.post.locked;
}
}
return CommentSortType.Top;
} else if (sort == SortType.New) {
return CommentSortType.New;
- } else if (sort == SortType.Hot) {
+ } else if (sort == SortType.Hot || sort == SortType.Active) {
return CommentSortType.Hot;
} else {
return CommentSortType.Hot;
+a.removed - +b.removed ||
+a.deleted - +b.deleted ||
(communityType && +b.stickied - +a.stickied) ||
- hotRankPost(b) - hotRankPost(a)
+ b.hot_rank - a.hot_rank
+ );
+ } else if (sort == SortType.Active) {
+ posts.sort(
+ (a, b) =>
+ +a.removed - +b.removed ||
+ +a.deleted - +b.deleted ||
+ (communityType && +b.stickied - +a.stickied) ||
+ b.hot_rank_active - a.hot_rank_active
);
}
}
return `hsla(${Math.random() * 360}, 100%, 50%, 1)`;
}
-export function previewLines(text: string, lines: number = 3): string {
- // Use lines * 2 because markdown requires 2 lines
- return text
- .split('\n')
- .slice(0, lines * 2)
- .join('\n');
+export function previewLines(
+ text: string,
+ maxChars: number = 300,
+ maxLines: number = 1
+): string {
+ return (
+ text
+ .slice(0, maxChars)
+ .split('\n')
+ // Use lines * 2 because markdown requires 2 lines
+ .slice(0, maxLines * 2)
+ .join('\n') + '...'
+ );
+}
+
+export function hostname(url: string): string {
+ let cUrl = new URL(url);
+ return window.location.port
+ ? `${cUrl.hostname}:${cUrl.port}`
+ : `${cUrl.hostname}`;
}
function canUseWebP() {
- var elem = document.createElement('canvas');
+ // TODO pictshare might have a webp conversion bug, try disabling this
+ return false;
- if (!!(elem.getContext && elem.getContext('2d'))) {
- var testString = !(window.mozInnerScreenX == null) ? 'png' : 'webp';
- // was able or not to get WebP representation
- return elem.toDataURL('image/webp').startsWith('data:image/' + testString);
- }
+ // var elem = document.createElement('canvas');
+ // if (!!(elem.getContext && elem.getContext('2d'))) {
+ // var testString = !(window.mozInnerScreenX == null) ? 'png' : 'webp';
+ // // was able or not to get WebP representation
+ // return (
+ // elem.toDataURL('image/webp').startsWith('data:image/' + testString)
+ // );
+ // }
+
+ // // very old browser like IE 8, canvas not supported
+ // return false;
+}
- // very old browser like IE 8, canvas not supported
- return false;
+export function validTitle(title?: string): boolean {
+ // Initial title is null, minimum length is taken care of by textarea's minLength={3}
+ if (title === null || title.length < 3) return true;
+
+ const regex = new RegExp(/.*\S.*/, 'g');
+
+ return regex.test(title);
+}
+
+export function siteBannerCss(banner: string): string {
+ return ` \
+ background-image: linear-gradient( rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.8) ) ,url("${banner}"); \
+ background-attachment: fixed; \
+ background-position: top; \
+ background-repeat: no-repeat; \
+ background-size: 100% cover; \
+
+ width: 100%; \
+ max-height: 100vh; \
+ `;
}