diff --git a/package.json b/package.json index 7b4f4621..d3769f13 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "sortpack": "^2.3.4", "style-loader": "^3.3.2", "terser": "^5.17.3", + "tsconfig-paths-webpack-plugin": "^4.0.1", "typescript": "^5.0.4", "webpack-dev-server": "4.15.0" }, diff --git a/src/client/index.tsx b/src/client/index.tsx index 7b6b6b1c..860c0756 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -1,14 +1,13 @@ import { hydrate } from "inferno-hydrate"; import { Router } from "inferno-router"; import { App } from "../shared/components/app/app"; +import { HistoryService } from "../shared/services/HistoryService"; import { initializeSite } from "../shared/utils"; import "bootstrap/js/dist/collapse"; import "bootstrap/js/dist/dropdown"; -import { HistoryService } from "../shared/services/HistoryService"; -const site = window.isoData.site_res; -initializeSite(site); +initializeSite(window.isoData.site_res); const wrapper = ( @@ -17,6 +16,7 @@ const wrapper = ( ); const root = document.getElementById("root"); + if (root) { hydrate(wrapper, root); } diff --git a/src/shared/components/app/navbar.tsx b/src/shared/components/app/navbar.tsx index 46eede64..0163f603 100644 --- a/src/shared/components/app/navbar.tsx +++ b/src/shared/components/app/navbar.tsx @@ -1,3 +1,6 @@ +import { isBrowser } from "@utils/browser"; +import { poll } from "@utils/helpers"; +import { amAdmin, canCreateCommunity } from "@utils/roles"; import { Component, createRef, linkEvent } from "inferno"; import { NavLink } from "inferno-router"; import { @@ -10,13 +13,9 @@ import { i18n } from "../../i18next"; import { UserService } from "../../services"; import { HttpService, RequestState } from "../../services/HttpService"; import { - amAdmin, - canCreateCommunity, donateLemmyUrl, - isBrowser, myAuth, numToSI, - poll, showAvatars, toast, updateUnreadCountsInterval, diff --git a/src/shared/components/comment/comment-node.tsx b/src/shared/components/comment/comment-node.tsx index 034ceaaf..15c68f75 100644 --- a/src/shared/components/comment/comment-node.tsx +++ b/src/shared/components/comment/comment-node.tsx @@ -1,3 +1,11 @@ +import { + amCommunityCreator, + canAdmin, + canMod, + isAdmin, + isBanned, + isMod, +} from "@utils/roles"; import classNames from "classnames"; import { Component, InfernoNode, linkEvent } from "inferno"; import { Link } from "inferno-router"; @@ -40,16 +48,10 @@ import { } from "../../interfaces"; import { UserService } from "../../services"; import { - amCommunityCreator, - canAdmin, - canMod, colorList, commentTreeMaxDepth, futureDaysToUnixTime, getCommentParentId, - isAdmin, - isBanned, - isMod, mdToHtml, mdToHtmlNoImages, myAuth, diff --git a/src/shared/components/common/markdown-textarea.tsx b/src/shared/components/common/markdown-textarea.tsx index 7992c72b..519a9fda 100644 --- a/src/shared/components/common/markdown-textarea.tsx +++ b/src/shared/components/common/markdown-textarea.tsx @@ -1,3 +1,4 @@ +import { isBrowser } from "@utils/browser"; import autosize from "autosize"; import classNames from "classnames"; import { NoOptionI18nKeys } from "i18next"; @@ -8,7 +9,6 @@ import { HttpService, UserService } from "../../services"; import { concurrentImageUpload, customEmojisLookup, - isBrowser, markdownFieldCharacterLimit, markdownHelpUrl, maxUploadImages, diff --git a/src/shared/components/community/communities.tsx b/src/shared/components/community/communities.tsx index 0126ab11..7cf072ef 100644 --- a/src/shared/components/community/communities.tsx +++ b/src/shared/components/community/communities.tsx @@ -1,3 +1,5 @@ +import { getQueryParams, getQueryString } from "@utils/helpers"; +import type { QueryParams } from "@utils/types"; import { Component, linkEvent } from "inferno"; import { CommunityResponse, @@ -11,12 +13,9 @@ import { InitialFetchRequest } from "../../interfaces"; import { FirstLoadService } from "../../services/FirstLoadService"; import { HttpService, RequestState } from "../../services/HttpService"; import { - QueryParams, RouteDataResponse, editCommunity, getPageFromString, - getQueryParams, - getQueryString, myAuth, myAuthRequired, numToSI, diff --git a/src/shared/components/community/community.tsx b/src/shared/components/community/community.tsx index b833ca9a..e03b6990 100644 --- a/src/shared/components/community/community.tsx +++ b/src/shared/components/community/community.tsx @@ -1,3 +1,5 @@ +import { getQueryParams, getQueryString } from "@utils/helpers"; +import type { QueryParams } from "@utils/types"; import { Component, linkEvent } from "inferno"; import { RouteComponentProps } from "inferno-router/dist/Route"; import { @@ -62,7 +64,6 @@ import { UserService } from "../../services"; import { FirstLoadService } from "../../services/FirstLoadService"; import { HttpService, RequestState } from "../../services/HttpService"; import { - QueryParams, RouteDataResponse, commentsToFlatNodes, communityRSSUrl, @@ -75,8 +76,6 @@ import { getCommentParentId, getDataTypeString, getPageFromString, - getQueryParams, - getQueryString, myAuth, postToCommentSortType, relTags, diff --git a/src/shared/components/community/sidebar.tsx b/src/shared/components/community/sidebar.tsx index d5b54f02..915e789a 100644 --- a/src/shared/components/community/sidebar.tsx +++ b/src/shared/components/community/sidebar.tsx @@ -1,3 +1,4 @@ +import { amAdmin, amMod, amTopMod } from "@utils/roles"; import { Component, InfernoNode, linkEvent } from "inferno"; import { T } from "inferno-i18next-dess"; import { Link } from "inferno-router"; @@ -16,15 +17,7 @@ import { } from "lemmy-js-client"; import { i18n } from "../../i18next"; import { UserService } from "../../services"; -import { - amAdmin, - amMod, - amTopMod, - getUnixTime, - hostname, - mdToHtml, - myAuthRequired, -} from "../../utils"; +import { getUnixTime, hostname, mdToHtml, myAuthRequired } from "../../utils"; import { Badges } from "../common/badges"; import { BannerIconHeader } from "../common/banner-icon-header"; import { Icon, PurgeWarning, Spinner } from "../common/icon"; diff --git a/src/shared/components/home/home.tsx b/src/shared/components/home/home.tsx index 5f726102..765dbf03 100644 --- a/src/shared/components/home/home.tsx +++ b/src/shared/components/home/home.tsx @@ -1,5 +1,8 @@ +import { getQueryParams, getQueryString } from "@utils/helpers"; +import { canCreateCommunity } from "@utils/roles"; +import type { QueryParams } from "@utils/types"; import { NoOptionI18nKeys } from "i18next"; -import { Component, linkEvent, MouseEventHandler } from "inferno"; +import { Component, MouseEventHandler, linkEvent } from "inferno"; import { T } from "inferno-i18next-dess"; import { Link } from "inferno-router"; import { @@ -57,7 +60,7 @@ import { UserService } from "../../services"; import { FirstLoadService } from "../../services/FirstLoadService"; import { HttpService, RequestState } from "../../services/HttpService"; import { - canCreateCommunity, + RouteDataResponse, commentsToFlatNodes, editComment, editPost, @@ -68,16 +71,12 @@ import { getCommentParentId, getDataTypeString, getPageFromString, - getQueryParams, - getQueryString, getRandomFromList, mdToHtml, myAuth, postToCommentSortType, - QueryParams, relTags, restoreScrollPosition, - RouteDataResponse, saveScrollPosition, setIsoData, setupTippy, diff --git a/src/shared/components/home/login.tsx b/src/shared/components/home/login.tsx index 4ea69456..1601750e 100644 --- a/src/shared/components/home/login.tsx +++ b/src/shared/components/home/login.tsx @@ -1,9 +1,10 @@ +import { isBrowser } from "@utils/browser"; import { Component, linkEvent } from "inferno"; import { GetSiteResponse, LoginResponse } from "lemmy-js-client"; import { i18n } from "../../i18next"; import { UserService } from "../../services"; import { HttpService, RequestState } from "../../services/HttpService"; -import { isBrowser, myAuth, setIsoData, toast, validEmail } from "../../utils"; +import { myAuth, setIsoData, toast, validEmail } from "../../utils"; import { HtmlTags } from "../common/html-tags"; import { Spinner } from "../common/icon"; diff --git a/src/shared/components/home/signup.tsx b/src/shared/components/home/signup.tsx index 41813a4f..7d504184 100644 --- a/src/shared/components/home/signup.tsx +++ b/src/shared/components/home/signup.tsx @@ -1,3 +1,4 @@ +import { isBrowser } from "@utils/browser"; import { Options, passwordStrength } from "check-password-strength"; import { NoOptionI18nKeys } from "i18next"; import { Component, linkEvent } from "inferno"; @@ -13,7 +14,6 @@ import { i18n } from "../../i18next"; import { UserService } from "../../services"; import { HttpService, RequestState } from "../../services/HttpService"; import { - isBrowser, joinLemmyUrl, mdToHtml, myAuth, diff --git a/src/shared/components/modlog.tsx b/src/shared/components/modlog.tsx index 4527800f..91da558f 100644 --- a/src/shared/components/modlog.tsx +++ b/src/shared/components/modlog.tsx @@ -1,3 +1,6 @@ +import { debounce, getQueryParams, getQueryString } from "@utils/helpers"; +import { amAdmin, amMod } from "@utils/roles"; +import type { QueryParams } from "@utils/types"; import { NoOptionI18nKeys } from "i18next"; import { Component, linkEvent } from "inferno"; import { T } from "inferno-i18next-dess"; @@ -34,17 +37,11 @@ import { FirstLoadService } from "../services/FirstLoadService"; import { HttpService, RequestState } from "../services/HttpService"; import { Choice, - QueryParams, RouteDataResponse, - amAdmin, - amMod, - debounce, fetchLimit, fetchUsers, getIdFromString, getPageFromString, - getQueryParams, - getQueryString, getUpdatedSearchId, myAuth, personToChoice, diff --git a/src/shared/components/person/profile.tsx b/src/shared/components/person/profile.tsx index 534bff46..6f6ede3e 100644 --- a/src/shared/components/person/profile.tsx +++ b/src/shared/components/person/profile.tsx @@ -1,3 +1,6 @@ +import { getQueryParams, getQueryString } from "@utils/helpers"; +import { canMod, isAdmin, isBanned } from "@utils/roles"; +import type { QueryParams } from "@utils/types"; import classNames from "classnames"; import { NoOptionI18nKeys } from "i18next"; import { Component, linkEvent } from "inferno"; @@ -53,9 +56,7 @@ import { UserService } from "../../services"; import { FirstLoadService } from "../../services/FirstLoadService"; import { HttpService, RequestState } from "../../services/HttpService"; import { - QueryParams, RouteDataResponse, - canMod, capitalizeFirstLetter, editComment, editPost, @@ -66,10 +67,6 @@ import { futureDaysToUnixTime, getCommentParentId, getPageFromString, - getQueryParams, - getQueryString, - isAdmin, - isBanned, mdToHtml, myAuth, myAuthRequired, diff --git a/src/shared/components/person/reports.tsx b/src/shared/components/person/reports.tsx index 00242141..e3d100c7 100644 --- a/src/shared/components/person/reports.tsx +++ b/src/shared/components/person/reports.tsx @@ -1,3 +1,4 @@ +import { amAdmin } from "@utils/roles"; import { Component, linkEvent } from "inferno"; import { CommentReportResponse, @@ -24,7 +25,6 @@ import { FirstLoadService } from "../../services/FirstLoadService"; import { RequestState } from "../../services/HttpService"; import { RouteDataResponse, - amAdmin, editCommentReport, editPostReport, editPrivateMessageReport, diff --git a/src/shared/components/person/settings.tsx b/src/shared/components/person/settings.tsx index b97064e6..5c3fc345 100644 --- a/src/shared/components/person/settings.tsx +++ b/src/shared/components/person/settings.tsx @@ -1,3 +1,4 @@ +import { debounce } from "@utils/helpers"; import { NoOptionI18nKeys } from "i18next"; import { Component, linkEvent } from "inferno"; import { @@ -18,7 +19,6 @@ import { Choice, capitalizeFirstLetter, communityToChoice, - debounce, elementUrl, emDash, fetchCommunities, diff --git a/src/shared/components/post/create-post.tsx b/src/shared/components/post/create-post.tsx index d765a48a..ebdf9995 100644 --- a/src/shared/components/post/create-post.tsx +++ b/src/shared/components/post/create-post.tsx @@ -1,3 +1,5 @@ +import { getQueryParams } from "@utils/helpers"; +import type { QueryParams } from "@utils/types"; import { Component } from "inferno"; import { RouteComponentProps } from "inferno-router/dist/Route"; import { @@ -17,12 +19,10 @@ import { } from "../../services/HttpService"; import { Choice, - QueryParams, RouteDataResponse, enableDownvotes, enableNsfw, getIdFromString, - getQueryParams, myAuth, setIsoData, } from "../../utils"; diff --git a/src/shared/components/post/post-form.tsx b/src/shared/components/post/post-form.tsx index 72f89dde..4b74e07f 100644 --- a/src/shared/components/post/post-form.tsx +++ b/src/shared/components/post/post-form.tsx @@ -1,3 +1,4 @@ +import { debounce } from "@utils/helpers"; import autosize from "autosize"; import { Component, InfernoNode, linkEvent } from "inferno"; import { @@ -18,7 +19,6 @@ import { archiveTodayUrl, capitalizeFirstLetter, communityToChoice, - debounce, fetchCommunities, getIdFromString, ghostArchiveUrl, diff --git a/src/shared/components/post/post-listing.tsx b/src/shared/components/post/post-listing.tsx index 429ccf18..1c585461 100644 --- a/src/shared/components/post/post-listing.tsx +++ b/src/shared/components/post/post-listing.tsx @@ -1,3 +1,14 @@ +import { canShare, share } from "@utils/browser"; +import { + amAdmin, + amCommunityCreator, + amMod, + canAdmin, + canMod, + isAdmin, + isBanned, + isMod, +} from "@utils/roles"; import classNames from "classnames"; import { Component, linkEvent } from "inferno"; import { Link } from "inferno-router"; @@ -28,18 +39,9 @@ import { i18n } from "../../i18next"; import { BanType, PostFormParams, PurgeType, VoteType } from "../../interfaces"; import { UserService } from "../../services"; import { - amAdmin, - amCommunityCreator, - amMod, - canAdmin, - canMod, - canShare, futureDaysToUnixTime, hostname, - isAdmin, - isBanned, isImage, - isMod, isVideo, mdNoImages, mdToHtml, @@ -49,7 +51,6 @@ import { numToSI, relTags, setupTippy, - share, showScores, } from "../../utils"; import { Icon, PurgeWarning, Spinner } from "../common/icon"; diff --git a/src/shared/components/post/post.tsx b/src/shared/components/post/post.tsx index d6c1d3a2..b87cfc8b 100644 --- a/src/shared/components/post/post.tsx +++ b/src/shared/components/post/post.tsx @@ -1,3 +1,5 @@ +import { isBrowser } from "@utils/browser"; +import { debounce } from "@utils/helpers"; import autosize from "autosize"; import { Component, createRef, linkEvent, RefObject } from "inferno"; import { @@ -64,7 +66,6 @@ import { buildCommentsTree, commentsToFlatNodes, commentTreeMaxDepth, - debounce, editComment, editWith, enableDownvotes, @@ -73,7 +74,6 @@ import { getCommentParentId, getDepthFromComment, getIdFromProps, - isBrowser, isImage, myAuth, restoreScrollPosition, diff --git a/src/shared/components/search.tsx b/src/shared/components/search.tsx index e4e16609..f0931b12 100644 --- a/src/shared/components/search.tsx +++ b/src/shared/components/search.tsx @@ -1,3 +1,5 @@ +import { debounce, getQueryParams, getQueryString } from "@utils/helpers"; +import type { QueryParams } from "@utils/types"; import type { NoOptionI18nKeys } from "i18next"; import { Component, linkEvent } from "inferno"; import { @@ -26,12 +28,10 @@ import { FirstLoadService } from "../services/FirstLoadService"; import { HttpService, RequestState } from "../services/HttpService"; import { Choice, - QueryParams, RouteDataResponse, capitalizeFirstLetter, commentsToFlatNodes, communityToChoice, - debounce, enableDownvotes, enableNsfw, fetchCommunities, @@ -39,8 +39,6 @@ import { fetchUsers, getIdFromString, getPageFromString, - getQueryParams, - getQueryString, getUpdatedSearchId, myAuth, numToSI, diff --git a/src/shared/env.ts b/src/shared/env.ts index 576c6c58..287912d1 100644 --- a/src/shared/env.ts +++ b/src/shared/env.ts @@ -1,4 +1,4 @@ -import { isBrowser } from "./utils"; +import { isBrowser } from "@utils/browser"; const testHost = "0.0.0.0:8536"; diff --git a/src/shared/i18next.ts b/src/shared/i18next.ts index 47ca6501..ff5f77f1 100644 --- a/src/shared/i18next.ts +++ b/src/shared/i18next.ts @@ -1,3 +1,4 @@ +import { isBrowser } from "@utils/browser"; import i18next, { i18nTyped, Resource } from "i18next"; import { UserService } from "./services"; import { ar } from "./translations/ar"; @@ -31,7 +32,6 @@ import { sv } from "./translations/sv"; import { vi } from "./translations/vi"; import { zh } from "./translations/zh"; import { zh_Hant } from "./translations/zh_Hant"; -import { isBrowser } from "./utils"; export const languages = [ { resource: ar, code: "ar", name: "العربية" }, diff --git a/src/shared/services/UserService.ts b/src/shared/services/UserService.ts index 57c8aecf..346d833a 100644 --- a/src/shared/services/UserService.ts +++ b/src/shared/services/UserService.ts @@ -1,10 +1,11 @@ // import Cookies from 'js-cookie'; +import { isBrowser } from "@utils/browser"; import IsomorphicCookie from "isomorphic-cookie"; import jwt_decode from "jwt-decode"; import { LoginResponse, MyUserInfo } from "lemmy-js-client"; import { isHttps } from "../env"; import { i18n } from "../i18next"; -import { isAuthPath, isBrowser, toast } from "../utils"; +import { isAuthPath, toast } from "../utils"; interface Claims { sub: number; diff --git a/src/shared/utils.ts b/src/shared/utils.ts index edcb7f61..8b01e5b4 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -1,3 +1,5 @@ +import { isBrowser } from "@utils/browser"; +import { debounce, groupBy } from "@utils/helpers"; import { Picker } from "emoji-mart"; import emojiShortName from "emoji-short-name"; import { @@ -9,7 +11,6 @@ import { CommentReportView, CommentSortType, CommentView, - CommunityModeratorView, CommunityView, CustomEmojiView, GetSiteMetadata, @@ -17,7 +18,6 @@ import { Language, LemmyHttp, MyUserInfo, - Person, PersonMentionView, PersonView, PostReportView, @@ -235,92 +235,6 @@ export function futureDaysToUnixTime(days?: number): number | undefined { : undefined; } -export function canMod( - creator_id: number, - mods?: CommunityModeratorView[], - admins?: PersonView[], - myUserInfo = UserService.Instance.myUserInfo, - onSelf = false -): boolean { - // You can do moderator actions only on the mods added after you. - let adminsThenMods = - admins - ?.map(a => a.person.id) - .concat(mods?.map(m => m.moderator.id) ?? []) ?? []; - - if (myUserInfo) { - const myIndex = adminsThenMods.findIndex( - id => id == myUserInfo.local_user_view.person.id - ); - if (myIndex == -1) { - return false; - } else { - // onSelf +1 on mod actions not for yourself, IE ban, remove, etc - adminsThenMods = adminsThenMods.slice(0, myIndex + (onSelf ? 0 : 1)); - return !adminsThenMods.includes(creator_id); - } - } else { - return false; - } -} - -export function canAdmin( - creatorId: number, - admins?: PersonView[], - myUserInfo = UserService.Instance.myUserInfo, - onSelf = false -): boolean { - return canMod(creatorId, undefined, admins, myUserInfo, onSelf); -} - -export function isMod( - creatorId: number, - mods?: CommunityModeratorView[] -): boolean { - return mods?.map(m => m.moderator.id).includes(creatorId) ?? false; -} - -export function amMod( - mods?: CommunityModeratorView[], - myUserInfo = UserService.Instance.myUserInfo -): boolean { - return myUserInfo ? isMod(myUserInfo.local_user_view.person.id, mods) : false; -} - -export function isAdmin(creatorId: number, admins?: PersonView[]): boolean { - return admins?.map(a => a.person.id).includes(creatorId) ?? false; -} - -export function amAdmin(myUserInfo = UserService.Instance.myUserInfo): boolean { - return myUserInfo?.local_user_view.person.admin ?? false; -} - -export function amCommunityCreator( - creator_id: number, - mods?: CommunityModeratorView[], - myUserInfo = UserService.Instance.myUserInfo -): boolean { - const myId = myUserInfo?.local_user_view.person.id; - // Don't allow mod actions on yourself - return myId == mods?.at(0)?.moderator.id && myId != creator_id; -} - -export function amSiteCreator( - creator_id: number, - admins?: PersonView[], - myUserInfo = UserService.Instance.myUserInfo -): boolean { - const myId = myUserInfo?.local_user_view.person.id; - return myId == admins?.at(0)?.person.id && myId != creator_id; -} - -export function amTopMod( - mods: CommunityModeratorView[], - myUserInfo = UserService.Instance.myUserInfo -): boolean { - return mods.at(0)?.moderator.id == myUserInfo?.local_user_view.person.id; -} - const imageRegex = /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/; const videoRegex = /(http)?s?:?(\/\/[^"']*\.(?:mp4|webm))/; const tldRegex = /([a-z0-9]+\.)*[a-z0-9]+\.[a-z]+/; @@ -371,51 +285,6 @@ export function getDataTypeString(dt: DataType) { return dt === DataType.Post ? "Post" : "Comment"; } -export function debounce( - func: (...e: T) => R, - wait = 1000, - immediate = false -) { - // 'private' variable for instance - // The returned function will be able to reference this due to closure. - // Each call to the returned function will share this common timer. - let timeout: NodeJS.Timeout | null; - - // Calling debounce returns a new anonymous function - return function () { - // reference the context and args for the setTimeout function - const args = arguments; - - // Should the function be called now? If immediate is true - // and not already in a timeout then the answer is: Yes - const callNow = immediate && !timeout; - - // This is the basic debounce behavior where you can call this - // function several times, but it will only execute once - // [before or after imposing a delay]. - // Each time the returned function is called, the timer starts over. - clearTimeout(timeout ?? undefined); - - // Set the new timeout - timeout = setTimeout(function () { - // Inside the timeout function, clear the timeout variable - // which will let the next execution run when in 'immediate' mode - timeout = null; - - // Check if the function already ran with the immediate flag - if (!immediate) { - // Call the original function with apply - // apply lets you define the 'this' object as well as the arguments - // (both captured before setTimeout) - func.apply(this, args); - } - }, wait); - - // Immediate mode and no wait timer? Execute the function.. - if (callNow) func.apply(this, args); - } as (...e: T) => R; -} - export async function fetchThemeList(): Promise { return fetch("/css/themelist").then(res => res.json()); } @@ -1153,10 +1022,6 @@ export function siteBannerCss(banner: string): string { `; } -export function isBrowser() { - return typeof window !== "undefined"; -} - export function setIsoData(context: any): IsoData { // If its the browser, you need to deserialize the data from the window if (isBrowser()) { @@ -1286,21 +1151,6 @@ export function numToSI(value: number): string { return SHORTNUM_SI_FORMAT.format(value); } -export function isBanned(ps: Person): boolean { - const expires = ps.ban_expires; - // Add Z to convert from UTC date - // TODO this check probably isn't necessary anymore - if (expires) { - if (ps.banned && new Date(expires + "Z") > new Date()) { - return true; - } else { - return false; - } - } else { - return ps.banned; - } -} - export function myAuth(): string | undefined { return UserService.Instance.auth(); } @@ -1332,15 +1182,6 @@ export function postToCommentSortType(sort: SortType): CommentSortType { } } -export function canCreateCommunity( - siteRes: GetSiteResponse, - myUserInfo = UserService.Instance.myUserInfo -): boolean { - const adminOnly = siteRes.site_view.local_site.community_creation_admin_only; - // TODO: Make this check if user is logged on as well - return !adminOnly || amAdmin(myUserInfo); -} - export function isPostBlocked( pv: PostView, myUserInfo: MyUserInfo | undefined = UserService.Instance.myUserInfo @@ -1421,64 +1262,12 @@ interface EmojiMartSkin { src: string; } -const groupBy = ( - array: T[], - predicate: (value: T, index: number, array: T[]) => string -) => - array.reduce((acc, value, index, array) => { - (acc[predicate(value, index, array)] ||= []).push(value); - return acc; - }, {} as { [key: string]: T[] }); - -export type QueryParams> = { - [key in keyof T]?: string; -}; - -export function getQueryParams>(processors: { - [K in keyof T]: (param: string) => T[K]; -}): T { - if (isBrowser()) { - const searchParams = new URLSearchParams(window.location.search); - - return Array.from(Object.entries(processors)).reduce( - (acc, [key, process]) => ({ - ...acc, - [key]: process(searchParams.get(key)), - }), - {} as T - ); - } - - return {} as T; -} - -export function getQueryString>( - obj: T -) { - return Object.entries(obj) - .filter(([, val]) => val !== undefined && val !== null) - .reduce( - (acc, [key, val], index) => `${acc}${index > 0 ? "&" : ""}${key}=${val}`, - "?" - ); -} - export function isAuthPath(pathname: string) { return /create_.*|inbox|settings|admin|reports|registration_applications/g.test( pathname ); } -export function canShare() { - return isBrowser() && !!navigator.canShare; -} - -export function share(shareData: ShareData) { - if (isBrowser()) { - navigator.share(shareData); - } -} - export function newVote(voteType: VoteType, myVote?: number): number { if (voteType == VoteType.Upvote) { return myVote == 1 ? 0 : 1; @@ -1490,18 +1279,3 @@ export function newVote(voteType: VoteType, myVote?: number): number { export type RouteDataResponse> = { [K in keyof T]: RequestState; }; - -function sleep(millis: number): Promise { - return new Promise(resolve => setTimeout(resolve, millis)); -} - -/** - * Polls / repeatedly runs a promise, every X milliseconds - */ -export async function poll(promiseFn: any, millis: number) { - if (window.document.visibilityState !== "hidden") { - await promiseFn(); - } - await sleep(millis); - return poll(promiseFn, millis); -} diff --git a/src/shared/utils/browser/can-share.ts b/src/shared/utils/browser/can-share.ts new file mode 100644 index 00000000..77de9838 --- /dev/null +++ b/src/shared/utils/browser/can-share.ts @@ -0,0 +1,5 @@ +import { isBrowser } from "@utils/browser"; + +export default function canShare() { + return isBrowser() && !!navigator.canShare; +} diff --git a/src/shared/utils/browser/index.ts b/src/shared/utils/browser/index.ts new file mode 100644 index 00000000..a7a08a50 --- /dev/null +++ b/src/shared/utils/browser/index.ts @@ -0,0 +1,5 @@ +import canShare from "./can-share"; +import isBrowser from "./is-browser"; +import share from "./share"; + +export { canShare, isBrowser, share }; diff --git a/src/shared/utils/browser/is-browser.ts b/src/shared/utils/browser/is-browser.ts new file mode 100644 index 00000000..cc6ba882 --- /dev/null +++ b/src/shared/utils/browser/is-browser.ts @@ -0,0 +1,3 @@ +export default function isBrowser() { + return typeof window !== "undefined"; +} diff --git a/src/shared/utils/browser/share.ts b/src/shared/utils/browser/share.ts new file mode 100644 index 00000000..98d17712 --- /dev/null +++ b/src/shared/utils/browser/share.ts @@ -0,0 +1,7 @@ +import { isBrowser } from "@utils/browser"; + +export default function share(shareData: ShareData) { + if (isBrowser()) { + navigator.share(shareData); + } +} diff --git a/src/shared/utils/helpers/debounce.ts b/src/shared/utils/helpers/debounce.ts new file mode 100644 index 00000000..7e3b6f03 --- /dev/null +++ b/src/shared/utils/helpers/debounce.ts @@ -0,0 +1,24 @@ +export default function debounce( + func: (...e: T) => R, + wait = 1000, + immediate = false +) { + let timeout: NodeJS.Timeout | null; + + return function () { + const args = arguments; + const callNow = immediate && !timeout; + + clearTimeout(timeout ?? undefined); + + timeout = setTimeout(function () { + timeout = null; + + if (!immediate) { + func.apply(this, args); + } + }, wait); + + if (callNow) func.apply(this, args); + } as (...e: T) => R; +} diff --git a/src/shared/utils/helpers/get-query-params.ts b/src/shared/utils/helpers/get-query-params.ts new file mode 100644 index 00000000..627341e4 --- /dev/null +++ b/src/shared/utils/helpers/get-query-params.ts @@ -0,0 +1,21 @@ +import { isBrowser } from "@utils/browser"; + +export default function getQueryParams< + T extends Record +>(processors: { + [K in keyof T]: (param: string) => T[K]; +}): T { + if (isBrowser()) { + const searchParams = new URLSearchParams(window.location.search); + + return Array.from(Object.entries(processors)).reduce( + (acc, [key, process]) => ({ + ...acc, + [key]: process(searchParams.get(key)), + }), + {} as T + ); + } + + return {} as T; +} diff --git a/src/shared/utils/helpers/get-query-string.ts b/src/shared/utils/helpers/get-query-string.ts new file mode 100644 index 00000000..4b7bdbb5 --- /dev/null +++ b/src/shared/utils/helpers/get-query-string.ts @@ -0,0 +1,10 @@ +export default function getQueryString< + T extends Record +>(obj: T) { + return Object.entries(obj) + .filter(([, val]) => val !== undefined && val !== null) + .reduce( + (acc, [key, val], index) => `${acc}${index > 0 ? "&" : ""}${key}=${val}`, + "?" + ); +} diff --git a/src/shared/utils/helpers/group-by.ts b/src/shared/utils/helpers/group-by.ts new file mode 100644 index 00000000..4dd5d5db --- /dev/null +++ b/src/shared/utils/helpers/group-by.ts @@ -0,0 +1,8 @@ +export const groupBy = ( + array: T[], + predicate: (value: T, index: number, array: T[]) => string +) => + array.reduce((acc, value, index, array) => { + (acc[predicate(value, index, array)] ||= []).push(value); + return acc; + }, {} as { [key: string]: T[] }); diff --git a/src/shared/utils/helpers/index.ts b/src/shared/utils/helpers/index.ts new file mode 100644 index 00000000..663afbf9 --- /dev/null +++ b/src/shared/utils/helpers/index.ts @@ -0,0 +1,8 @@ +import debounce from "./debounce"; +import getQueryParams from "./get-query-params"; +import getQueryString from "./get-query-string"; +import { groupBy } from "./group-by"; +import poll from "./poll"; +import sleep from "./sleep"; + +export { debounce, getQueryParams, getQueryString, groupBy, poll, sleep }; diff --git a/src/shared/utils/helpers/poll.ts b/src/shared/utils/helpers/poll.ts new file mode 100644 index 00000000..8f30e91b --- /dev/null +++ b/src/shared/utils/helpers/poll.ts @@ -0,0 +1,12 @@ +import sleep from "./sleep"; + +/** + * Polls / repeatedly runs a promise, every X milliseconds + */ +export default async function poll(promiseFn: any, millis: number) { + if (window.document.visibilityState !== "hidden") { + await promiseFn(); + } + await sleep(millis); + return poll(promiseFn, millis); +} diff --git a/src/shared/utils/helpers/sleep.ts b/src/shared/utils/helpers/sleep.ts new file mode 100644 index 00000000..6529b522 --- /dev/null +++ b/src/shared/utils/helpers/sleep.ts @@ -0,0 +1,3 @@ +export default function sleep(millis: number): Promise { + return new Promise(resolve => setTimeout(resolve, millis)); +} diff --git a/src/shared/utils/roles/am-admin.ts b/src/shared/utils/roles/am-admin.ts new file mode 100644 index 00000000..69139d37 --- /dev/null +++ b/src/shared/utils/roles/am-admin.ts @@ -0,0 +1,7 @@ +import { UserService } from "../../services"; + +export default function amAdmin( + myUserInfo = UserService.Instance.myUserInfo +): boolean { + return myUserInfo?.local_user_view.person.admin ?? false; +} diff --git a/src/shared/utils/roles/am-community-creator.ts b/src/shared/utils/roles/am-community-creator.ts new file mode 100644 index 00000000..3671ef20 --- /dev/null +++ b/src/shared/utils/roles/am-community-creator.ts @@ -0,0 +1,12 @@ +import { CommunityModeratorView } from "lemmy-js-client"; +import { UserService } from "../../services"; + +export default function amCommunityCreator( + creator_id: number, + mods?: CommunityModeratorView[], + myUserInfo = UserService.Instance.myUserInfo +): boolean { + const myId = myUserInfo?.local_user_view.person.id; + // Don't allow mod actions on yourself + return myId == mods?.at(0)?.moderator.id && myId != creator_id; +} diff --git a/src/shared/utils/roles/am-mod.ts b/src/shared/utils/roles/am-mod.ts new file mode 100644 index 00000000..c0632f78 --- /dev/null +++ b/src/shared/utils/roles/am-mod.ts @@ -0,0 +1,10 @@ +import { isMod } from "@utils/roles"; +import { CommunityModeratorView } from "lemmy-js-client"; +import { UserService } from "../../services"; + +export default function amMod( + mods?: CommunityModeratorView[], + myUserInfo = UserService.Instance.myUserInfo +): boolean { + return myUserInfo ? isMod(myUserInfo.local_user_view.person.id, mods) : false; +} diff --git a/src/shared/utils/roles/am-site-creator.ts b/src/shared/utils/roles/am-site-creator.ts new file mode 100644 index 00000000..9da2840e --- /dev/null +++ b/src/shared/utils/roles/am-site-creator.ts @@ -0,0 +1,11 @@ +import { PersonView } from "lemmy-js-client"; +import { UserService } from "../../services"; + +export default function amSiteCreator( + creator_id: number, + admins?: PersonView[], + myUserInfo = UserService.Instance.myUserInfo +): boolean { + const myId = myUserInfo?.local_user_view.person.id; + return myId == admins?.at(0)?.person.id && myId != creator_id; +} diff --git a/src/shared/utils/roles/am-top-mod.ts b/src/shared/utils/roles/am-top-mod.ts new file mode 100644 index 00000000..9163d7ca --- /dev/null +++ b/src/shared/utils/roles/am-top-mod.ts @@ -0,0 +1,9 @@ +import { CommunityModeratorView } from "lemmy-js-client"; +import { UserService } from "../../services"; + +export default function amTopMod( + mods: CommunityModeratorView[], + myUserInfo = UserService.Instance.myUserInfo +): boolean { + return mods.at(0)?.moderator.id == myUserInfo?.local_user_view.person.id; +} diff --git a/src/shared/utils/roles/can-admin.ts b/src/shared/utils/roles/can-admin.ts new file mode 100644 index 00000000..55bfd1cf --- /dev/null +++ b/src/shared/utils/roles/can-admin.ts @@ -0,0 +1,12 @@ +import { canMod } from "@utils/roles"; +import { PersonView } from "lemmy-js-client"; +import { UserService } from "../../services"; + +export default function canAdmin( + creatorId: number, + admins?: PersonView[], + myUserInfo = UserService.Instance.myUserInfo, + onSelf = false +): boolean { + return canMod(creatorId, undefined, admins, myUserInfo, onSelf); +} diff --git a/src/shared/utils/roles/can-create-community.ts b/src/shared/utils/roles/can-create-community.ts new file mode 100644 index 00000000..1b5cf05c --- /dev/null +++ b/src/shared/utils/roles/can-create-community.ts @@ -0,0 +1,12 @@ +import { amAdmin } from "@utils/roles"; +import { GetSiteResponse } from "lemmy-js-client"; +import { UserService } from "../../services"; + +export default function canCreateCommunity( + siteRes: GetSiteResponse, + myUserInfo = UserService.Instance.myUserInfo +): boolean { + const adminOnly = siteRes.site_view.local_site.community_creation_admin_only; + // TODO: Make this check if user is logged on as well + return !adminOnly || amAdmin(myUserInfo); +} diff --git a/src/shared/utils/roles/can-mod.ts b/src/shared/utils/roles/can-mod.ts new file mode 100644 index 00000000..df639b7f --- /dev/null +++ b/src/shared/utils/roles/can-mod.ts @@ -0,0 +1,31 @@ +import { CommunityModeratorView, PersonView } from "lemmy-js-client"; +import { UserService } from "../../services"; + +export default function canMod( + creator_id: number, + mods?: CommunityModeratorView[], + admins?: PersonView[], + myUserInfo = UserService.Instance.myUserInfo, + onSelf = false +): boolean { + // You can do moderator actions only on the mods added after you. + let adminsThenMods = + admins + ?.map(a => a.person.id) + .concat(mods?.map(m => m.moderator.id) ?? []) ?? []; + + if (myUserInfo) { + const myIndex = adminsThenMods.findIndex( + id => id == myUserInfo.local_user_view.person.id + ); + if (myIndex == -1) { + return false; + } else { + // onSelf +1 on mod actions not for yourself, IE ban, remove, etc + adminsThenMods = adminsThenMods.slice(0, myIndex + (onSelf ? 0 : 1)); + return !adminsThenMods.includes(creator_id); + } + } else { + return false; + } +} diff --git a/src/shared/utils/roles/index.ts b/src/shared/utils/roles/index.ts new file mode 100644 index 00000000..4762637a --- /dev/null +++ b/src/shared/utils/roles/index.ts @@ -0,0 +1,25 @@ +import amAdmin from "./am-admin"; +import amCommunityCreator from "./am-community-creator"; +import amMod from "./am-mod"; +import amSiteCreator from "./am-site-creator"; +import amTopMod from "./am-top-mod"; +import canAdmin from "./can-admin"; +import canCreateCommunity from "./can-create-community"; +import canMod from "./can-mod"; +import isAdmin from "./is-admin"; +import isBanned from "./is-banned"; +import isMod from "./is-mod"; + +export { + amAdmin, + amCommunityCreator, + amMod, + amSiteCreator, + amTopMod, + canAdmin, + canCreateCommunity, + canMod, + isAdmin, + isBanned, + isMod, +}; diff --git a/src/shared/utils/roles/is-admin.ts b/src/shared/utils/roles/is-admin.ts new file mode 100644 index 00000000..bc0332ea --- /dev/null +++ b/src/shared/utils/roles/is-admin.ts @@ -0,0 +1,8 @@ +import { PersonView } from "lemmy-js-client"; + +export default function isAdmin( + creatorId: number, + admins?: PersonView[] +): boolean { + return admins?.map(a => a.person.id).includes(creatorId) ?? false; +} diff --git a/src/shared/utils/roles/is-banned.ts b/src/shared/utils/roles/is-banned.ts new file mode 100644 index 00000000..d71f6f4f --- /dev/null +++ b/src/shared/utils/roles/is-banned.ts @@ -0,0 +1,16 @@ +import { Person } from "lemmy-js-client"; + +export default function isBanned(ps: Person): boolean { + const expires = ps.ban_expires; + // Add Z to convert from UTC date + // TODO this check probably isn't necessary anymore + if (expires) { + if (ps.banned && new Date(expires + "Z") > new Date()) { + return true; + } else { + return false; + } + } else { + return ps.banned; + } +} diff --git a/src/shared/utils/roles/is-mod.ts b/src/shared/utils/roles/is-mod.ts new file mode 100644 index 00000000..018b721e --- /dev/null +++ b/src/shared/utils/roles/is-mod.ts @@ -0,0 +1,8 @@ +import { CommunityModeratorView } from "lemmy-js-client"; + +export default function isMod( + creatorId: number, + mods?: CommunityModeratorView[] +): boolean { + return mods?.map(m => m.moderator.id).includes(creatorId) ?? false; +} diff --git a/src/shared/utils/types/index.ts b/src/shared/utils/types/index.ts new file mode 100644 index 00000000..9b4a1cec --- /dev/null +++ b/src/shared/utils/types/index.ts @@ -0,0 +1,3 @@ +import { QueryParams } from "./query-params"; + +export { QueryParams }; diff --git a/src/shared/utils/types/query-params.ts b/src/shared/utils/types/query-params.ts new file mode 100644 index 00000000..37705bd8 --- /dev/null +++ b/src/shared/utils/types/query-params.ts @@ -0,0 +1,3 @@ +export type QueryParams> = { + [key in keyof T]?: string; +}; diff --git a/tsconfig.json b/tsconfig.json index 3b7d3e41..600d33aa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,13 @@ "noImplicitReturns": true, "experimentalDecorators": true, "strictNullChecks": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "paths": { + "@utils/roles": ["./shared/utils/roles/index"], + "@utils/browser": ["./shared/utils/browser/index"], + "@utils/helpers": ["./shared/utils/helpers/index"], + "@utils/types": ["./shared/utils/types/index"], + } }, "include": [ "src/**/*.ts", diff --git a/webpack.config.js b/webpack.config.js index 67b10a98..fd210f05 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,6 +5,7 @@ const CopyPlugin = require("copy-webpack-plugin"); const RunNodeWebpackPlugin = require("run-node-webpack-plugin"); const merge = require("lodash/merge"); const { ServiceWorkerPlugin } = require("service-worker-webpack"); +const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const banner = ` hash:[contentHash], chunkhash:[chunkhash], name:[name], filebase:[base], query:[query], file:[file] Source code: https://github.com/LemmyNet/lemmy-ui @@ -19,6 +20,7 @@ const base = { hashFunction: "xxhash64", }, resolve: { + plugins: [new TsconfigPathsPlugin()], extensions: [".js", ".jsx", ".ts", ".tsx"], }, performance: { diff --git a/yarn.lock b/yarn.lock index 8ed82982..1e31d7cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2714,7 +2714,7 @@ chalk@^2.0.0, chalk@^2.0.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.0.2: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3578,6 +3578,14 @@ enhanced-resolve@^5.14.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.7.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + entities@^4.2.0, entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" @@ -8686,6 +8694,11 @@ strip-ansi@^7.0.1: dependencies: ansi-regex "^6.0.1" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + strip-comments@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" @@ -8938,6 +8951,24 @@ tributejs@^5.1.3: resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-5.1.3.tgz#980600fc72865be5868893078b4bfde721129eae" integrity sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ== +tsconfig-paths-webpack-plugin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.1.tgz#a24651d0f69668a1abad38d3c2489855c257460d" + integrity sha512-m5//KzLoKmqu2MVix+dgLKq70MnFi8YL8sdzQZ6DblmCdfuq/y3OqvJd5vMndg2KEVCOeNz8Es4WVZhYInteLw== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tsconfig-paths "^4.1.2" + +tsconfig-paths@^4.1.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"