diff --git a/.eslintrc.json b/.eslintrc.json index 44dab42..3a60a6b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -18,6 +18,7 @@ "@typescript-eslint/ban-ts-comment": 0, "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-empty-function": 0, "arrow-body-style": 0, "curly": 0, "eol-last": 0, diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml index 6457909..2273a13 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -9,6 +9,19 @@ body: Found a bug? Please fill out the sections below. 👍 Thanks for taking the time to fill out this bug report! For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy) + - type: checkboxes + attributes: + label: Requirements + description: Before you create a bug report please do the following. + options: + - label: Is this a bug report? For questions or discussions use https://lemmy.ml/c/lemmy_support + required: true + - label: Did you check to see if this issue already exists? + required: true + - label: Is this only a single bug? Do not put multiple bugs in one issue. + required: true + - label: Is this a server side (not related to the UI) issue? Use the [Lemmy back end](https://github.com/LemmyNet/lemmy) repo. + required: true - type: textarea id: summary attributes: @@ -22,7 +35,7 @@ body: label: Steps to Reproduce description: | Describe the steps to reproduce the bug. - The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ resolution. + The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ resolution. value: | 1. 2. @@ -45,3 +58,9 @@ body: placeholder: ex. 0.17.4-rc.4 validations: required: true + - type: input + id: lemmy-instance + attributes: + label: Lemmy Instance URL + description: Which Lemmy instance do you use? The address + placeholder: lemmy.ml, lemmy.world, etc diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml index 375d06d..2f6f3fc 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml @@ -7,6 +7,19 @@ body: value: | Have a suggestion about Lemmy's UI? For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy) + - type: checkboxes + attributes: + label: Requirements + description: Before you create a bug report please do the following. + options: + - label: Is this a feature request? For questions or discussions use https://lemmy.ml/c/lemmy_support + required: true + - label: Did you check to see if this issue already exists? + required: true + - label: Is this only a feature request? Do not put multiple feature requests in one issue. + required: true + - label: Is this a server side (not related to the UI) issue? Use the [Lemmy back end](https://github.com/LemmyNet/lemmy) repo. + required: true - type: textarea id: problem attributes: diff --git a/.github/ISSUE_TEMPLATE/QUESTION.yml b/.github/ISSUE_TEMPLATE/QUESTION.yml index 460d9a4..734937e 100644 --- a/.github/ISSUE_TEMPLATE/QUESTION.yml +++ b/.github/ISSUE_TEMPLATE/QUESTION.yml @@ -14,4 +14,4 @@ body: label: Question description: What's the question you have about Lemmy's UI? validations: - required: true \ No newline at end of file + required: true diff --git a/.github/ISSUE_TEMPLATE/hexbear.yml b/.github/ISSUE_TEMPLATE/hexbear.yml index 199b97e..73ef548 100644 --- a/.github/ISSUE_TEMPLATE/hexbear.yml +++ b/.github/ISSUE_TEMPLATE/hexbear.yml @@ -8,4 +8,4 @@ body: label: Question description: What's the question you have about hexbear? validations: - required: true \ No newline at end of file + required: true diff --git a/.woodpecker.yml b/.woodpecker.yml index 8d3c6f1..656903a 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,6 +1,6 @@ pipeline: fetch_git_submodules: - image: node:14-alpine + image: node:alpine commands: - apk add git - git submodule init @@ -8,93 +8,27 @@ pipeline: # - git fetch --tags yarn: - image: node:14-alpine + image: node:alpine commands: - yarn yarn_lint: - image: node:14-alpine + image: node:alpine commands: - yarn lint yarn_build_dev: - image: node:14-alpine + image: node:alpine commands: - yarn build:dev - nightly_build: - image: plugins/docker + publish_release_docker: + image: woodpeckerci/plugin-docker-buildx + secrets: [docker_username, docker_password] settings: - dockerfile: Dockerfile repo: dessalines/lemmy-ui - username: - from_secret: docker_username - password: - from_secret: docker_password - tags: - - dev - when: - event: - - cron - - publish_release_docker_image_amd: - image: plugins/docker - settings: dockerfile: Dockerfile - repo: dessalines/lemmy-ui + platforms: linux/amd64 auto_tag: true - auto_tag_suffix: linux-amd64 - username: - from_secret: docker_username - password: - from_secret: docker_password - when: - event: tag - platform: linux/arm64 - - publish_release_docker_image_arm: - image: plugins/docker - settings: - dockerfile: Dockerfile - repo: dessalines/lemmy-ui - auto_tag: true - auto_tag_suffix: linux-arm64 - username: - from_secret: docker_username - password: - from_secret: docker_password - when: - event: tag - platform: linux/amd64 - - publish_release_docker_manifest: - image: plugins/manifest - settings: - username: - from_secret: docker_username - password: - from_secret: docker_password - target: "dessalines/lemmy-ui:${CI_COMMIT_TAG}" - template: "dessalines/lemmy-ui:${CI_COMMIT_TAG}-OS-ARCH" - platforms: - - linux/amd64 - - linux/arm64 - ignore_missing: true - when: - event: tag - - publish_latest_release_docker_manifest: - image: plugins/manifest - settings: - username: - from_secret: docker_username - password: - from_secret: docker_password - target: "dessalines/lemmy-ui:latest" - template: "dessalines/lemmy-ui:${CI_COMMIT_TAG}-OS-ARCH" - platforms: - - linux/amd64 - - linux/arm64 - ignore_missing: true when: event: tag diff --git a/README.md b/README.md index 6c9ef63..f1917bf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# lemmy-ui +# Lemmy-UI The official web app for [Lemmy](https://github.com/LemmyNet/lemmy), written in inferno. @@ -13,7 +13,6 @@ The following environment variables can be used to configure lemmy-ui: | `LEMMY_UI_HOST` | `string` | `0.0.0.0:1234` | The IP / port that the lemmy-ui isomorphic node server is hosted at. | | `LEMMY_UI_LEMMY_INTERNAL_HOST` | `string` | `0.0.0.0:8536` | The internal IP / port that lemmy is hosted at. Often `lemmy:8536` if using docker. | | `LEMMY_UI_LEMMY_EXTERNAL_HOST` | `string` | `0.0.0.0:8536` | The external IP / port that lemmy is hosted at. Often `DOMAIN.TLD`. | -| `LEMMY_UI_LEMMY_WS_HOST` | `string` | `0.0.0.0:8536` | An alternate location for lemmy's websocket address. Not usually necessary. | | `LEMMY_UI_HTTPS` | `bool` | `false` | Whether to use https. | | `LEMMY_UI_EXTRA_THEMES_FOLDER` | `string` | `./extra_themes` | A location for additional lemmy css themes. | | `LEMMY_UI_DEBUG` | `bool` | `false` | Loads the [Eruda](https://github.com/liriliri/eruda) debugging utility. | diff --git a/deploy.sh b/deploy.sh index c53988d..e919779 100755 --- a/deploy.sh +++ b/deploy.sh @@ -4,6 +4,7 @@ set -e new_tag="$1" # Old deploy +# sudo docker build . --tag dessalines/lemmy-ui:$new_tag --platform=linux/amd64 --push # sudo docker build . --tag dessalines/lemmy-ui:$new_tag --platform=linux/amd64 # sudo docker push dessalines/lemmy-ui:$new_tag diff --git a/lemmy-translations b/lemmy-translations index ddf0d3a..f45ddff 160000 --- a/lemmy-translations +++ b/lemmy-translations @@ -1 +1 @@ -Subproject commit ddf0d3a4dcfba5eddbcdb702db2470b52abb3815 +Subproject commit f45ddff206adb52ab0ac7555bf14978edac5d2f2 diff --git a/package.json b/package.json index 2e09e89..be239e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lemmy-ui", - "version": "0.17.1", + "version": "0.18.0-rc.1", "description": "An isomorphic UI for lemmy", "repository": "https://github.com/LemmyNet/lemmy-ui", "license": "AGPL-3.0", @@ -19,16 +19,9 @@ "themes:watch": "sass --watch src/assets/css/themes/:src/assets/css/themes" }, "lint-staged": { - "*.{ts,tsx,js}": [ - "prettier --write", - "eslint --fix" - ], - "*.{css, scss}": [ - "prettier --write" - ], - "package.json": [ - "sortpack" - ] + "*.{ts,tsx,js}": ["prettier --write", "eslint --fix"], + "*.{css, scss}": ["prettier --write"], + "package.json": ["sortpack"] }, "dependencies": { "@babel/plugin-proposal-decorators": "^7.21.0", @@ -51,6 +44,7 @@ "emoji-mart": "^5.4.0", "emoji-short-name": "^2.0.0", "express": "~4.18.2", + "history": "^5.3.0", "html-to-text": "^9.0.5", "i18next": "^22.4.15", "inferno": "^8.1.1", @@ -62,7 +56,7 @@ "inferno-server": "^8.1.1", "isomorphic-cookie": "^1.2.4", "jwt-decode": "^3.1.2", - "lemmy-js-client": "0.17.2-rc.17", + "lemmy-js-client": "0.17.2-rc.24", "lodash": "^4.17.21", "markdown-it": "^13.0.1", "markdown-it-container": "^3.0.0", @@ -75,7 +69,6 @@ "moment": "^2.29.4", "register-service-worker": "^1.7.2", "run-node-webpack-plugin": "^1.3.0", - "rxjs": "^7.8.1", "sanitize-html": "^2.10.0", "sass": "^1.62.1", "sass-loader": "^13.2.2", @@ -87,8 +80,7 @@ "tributejs": "^5.1.3", "webpack": "5.82.1", "webpack-cli": "^5.1.1", - "webpack-node-externals": "^3.0.0", - "websocket-ts": "^1.1.1" + "webpack-node-externals": "^3.0.0" }, "devDependencies": { "@babel/core": "^7.21.8", diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 1c45341..e1adfc5 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -75,6 +75,11 @@ font-size: 1.2rem; } +.md-div pre { + white-space: pre; + overflow-x: auto; +} + .md-div table { border-collapse: collapse; width: 100%; @@ -213,6 +218,11 @@ blockquote { overflow-y: auto; } +.comments { + list-style: none; + padding: 0; +} + .thumbnail { object-fit: cover; min-height: 60px; diff --git a/src/client/index.tsx b/src/client/index.tsx index 99f1237..7b6b6b1 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -1,18 +1,19 @@ import { hydrate } from "inferno-hydrate"; -import { BrowserRouter } from "inferno-router"; +import { Router } from "inferno-router"; import { App } from "../shared/components/app/app"; 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); const wrapper = ( - + - + ); const root = document.getElementById("root"); diff --git a/src/server/index.tsx b/src/server/index.tsx index 716a936..06dc33a 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -6,19 +6,20 @@ import { Helmet } from "inferno-helmet"; import { matchPath, StaticRouter } from "inferno-router"; import { renderToString } from "inferno-server"; import IsomorphicCookie from "isomorphic-cookie"; -import { GetSite, GetSiteResponse, LemmyHttp, Site } from "lemmy-js-client"; +import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client"; import path from "path"; import process from "process"; import serialize from "serialize-javascript"; import sharp from "sharp"; import { App } from "../shared/components/app/app"; -import { getHttpBase, getHttpBaseInternal } from "../shared/env"; +import { getHttpBaseExternal, getHttpBaseInternal } from "../shared/env"; import { ILemmyConfig, InitialFetchRequest, IsoDataOptionalSite, } from "../shared/interfaces"; import { routes } from "../shared/routes"; +import { RequestState, wrapClient } from "../shared/services/HttpService"; import { ErrorPageData, favIconPngUrl, @@ -64,7 +65,13 @@ Disallow: /search/ server.get("/service-worker.js", async (_req, res) => { res.setHeader("Content-Type", "application/javascript"); - res.sendFile(path.resolve("./dist/service-worker.js")); + res.sendFile( + path.resolve( + `./dist/service-worker${ + process.env.NODE_ENV === "development" ? "-development" : "" + }.js` + ) + ); }); server.get("/robots.txt", async (_req, res) => { @@ -121,7 +128,7 @@ server.get("/*", async (req, res) => { const getSiteForm: GetSite = { auth }; const headers = setForwardedHeaders(req.headers); - const client = new LemmyHttp(getHttpBaseInternal(), headers); + const client = wrapClient(new LemmyHttp(getHttpBaseInternal(), headers)); const { path, url, query } = req; @@ -129,27 +136,30 @@ server.get("/*", async (req, res) => { // This bypasses errors, so that the client can hit the error on its own, // in order to remove the jwt on the browser. Necessary for wrong jwts let site: GetSiteResponse | undefined = undefined; - let routeData: any[] = []; - let errorPageData: ErrorPageData | undefined; - try { - let try_site: any = await client.getSite(getSiteForm); - if (try_site.error == "not_logged_in") { - console.error( - "Incorrect JWT token, skipping auth so frontend can remove jwt cookie" - ); - getSiteForm.auth = undefined; - auth = undefined; - try_site = await client.getSite(getSiteForm); - } + const routeData: RequestState[] = []; + let errorPageData: ErrorPageData | undefined = undefined; + let try_site = await client.getSite(getSiteForm); + if (try_site.state === "failed" && try_site.msg == "not_logged_in") { + console.error( + "Incorrect JWT token, skipping auth so frontend can remove jwt cookie" + ); + getSiteForm.auth = undefined; + auth = undefined; + try_site = await client.getSite(getSiteForm); + } - if (!auth && isAuthPath(path)) { - res.redirect("/login"); - return; - } + if (!auth && isAuthPath(path)) { + return res.redirect("/login"); + } - site = try_site; + if (try_site.state === "success") { + site = try_site.data; initializeSite(site); + if (path !== "/setup" && !site.site_view.local_site.site_setup) { + return res.redirect("/setup"); + } + if (site) { const initialFetchReq: InitialFetchRequest = { client, @@ -160,23 +170,25 @@ server.get("/*", async (req, res) => { }; if (activeRoute?.fetchInitialData) { - routeData = await Promise.all([ - ...activeRoute.fetchInitialData(initialFetchReq), - ]); + routeData.push( + ...(await Promise.all([ + ...activeRoute.fetchInitialData(initialFetchReq), + ])) + ); } } - } catch (error) { - errorPageData = getErrorPageData(error, site); + } else if (try_site.state === "failed") { + errorPageData = getErrorPageData(new Error(try_site.msg), site); } // Redirect to the 404 if there's an API error - if (routeData[0] && routeData[0].error) { - const error = routeData[0].error; + if (routeData[0] && routeData[0].state === "failed") { + const error = routeData[0].msg; console.error(error); if (error === "instance_is_private") { return res.redirect(`/signup`); } else { - errorPageData = getErrorPageData(error, site); + errorPageData = getErrorPageData(new Error(error), site); } } @@ -234,7 +246,7 @@ process.on("SIGINT", () => { process.exit(0); }); -const iconSizes = [72, 96, 128, 144, 152, 192, 384, 512]; +const iconSizes = [72, 96, 144, 192, 512]; const defaultLogoPathDirectory = path.join( process.cwd(), "dist", @@ -242,12 +254,15 @@ const defaultLogoPathDirectory = path.join( "icons" ); -export async function generateManifestBase64(site: Site) { - const url = ( - process.env.NODE_ENV === "development" - ? "http://localhost:1236/" - : getHttpBase() - ).replace(/\/$/g, ""); +export async function generateManifestBase64({ + my_user, + site_view: { + site, + local_site: { community_creation_admin_only }, + }, +}: GetSiteResponse) { + const url = getHttpBaseExternal(); + const icon = site.icon ? await fetchIconPng(site.icon) : null; const manifest = { @@ -281,15 +296,58 @@ export async function generateManifestBase64(site: Site) { }; }) ), + shortcuts: [ + { + name: "Search", + short_name: "Search", + description: "Perform a search.", + url: "/search", + }, + { + name: "Communities", + url: "/communities", + short_name: "Communities", + description: "Browse communities", + }, + ] + .concat( + my_user + ? [ + { + name: "Create Post", + url: "/create_post", + short_name: "Create Post", + description: "Create a post.", + }, + ] + : [] + ) + .concat( + my_user?.local_user_view.person.admin || !community_creation_admin_only + ? [ + { + name: "Create Community", + url: "/create_community", + short_name: "Create Community", + description: "Create a community", + }, + ] + : [] + ), + related_applications: [ + { + platform: "f-droid", + url: "https://f-droid.org/packages/com.jerboa/", + id: "com.jerboa", + }, + ], }; return Buffer.from(JSON.stringify(manifest)).toString("base64"); } async function fetchIconPng(iconUrl: string) { - return await fetch( - iconUrl.replace(/https?:\/\/[^\/]+/g, getHttpBaseInternal()) - ) + return await fetch(iconUrl) .then(res => res.blob()) .then(blob => blob.arrayBuffer()); } @@ -376,9 +434,9 @@ async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) { site && `` } diff --git a/src/shared/components/app/navbar.tsx b/src/shared/components/app/navbar.tsx index 751bf9b..6d310ee 100644 --- a/src/shared/components/app/navbar.tsx +++ b/src/shared/components/app/navbar.tsx @@ -1,35 +1,25 @@ import { Component, createRef, linkEvent } from "inferno"; import { NavLink } from "inferno-router"; import { - CommentResponse, - GetReportCount, GetReportCountResponse, GetSiteResponse, - GetUnreadCount, GetUnreadCountResponse, - GetUnreadRegistrationApplicationCount, GetUnreadRegistrationApplicationCountResponse, - PrivateMessageResponse, - UserOperation, - wsJsonToRes, - wsUserOp, } from "lemmy-js-client"; -import { Subscription } from "rxjs"; import { i18n } from "../../i18next"; -import { UserService, WebSocketService } from "../../services"; +import { UserService } from "../../services"; +import { HttpService, RequestState } from "../../services/HttpService"; import { amAdmin, canCreateCommunity, donateLemmyUrl, isBrowser, myAuth, - notifyComment, - notifyPrivateMessage, numToSI, + poll, showAvatars, toast, - wsClient, - wsSubscribe, + updateUnreadCountsInterval, } from "../../utils"; import { Icon } from "../common/icon"; import { PictrsImage } from "../common/pictrs-image"; @@ -39,14 +29,16 @@ interface NavbarProps { } interface NavbarState { - unreadInboxCount: number; - unreadReportCount: number; - unreadApplicationCount: number; + unreadInboxCountRes: RequestState; + unreadReportCountRes: RequestState; + unreadApplicationCountRes: RequestState; onSiteBanner?(url: string): any; } function handleCollapseClick(i: Navbar) { - i.collapseButtonRef.current?.click(); + if (i.collapseButtonRef.current?.ariaExpanded === "true") { + i.collapseButtonRef.current?.click(); + } } function handleLogOut(i: Navbar) { @@ -55,77 +47,42 @@ function handleLogOut(i: Navbar) { } export class Navbar extends Component { - private wsSub: Subscription; - private userSub: Subscription; - private unreadInboxCountSub: Subscription; - private unreadReportCountSub: Subscription; - private unreadApplicationCountSub: Subscription; state: NavbarState = { - unreadInboxCount: 0, - unreadReportCount: 0, - unreadApplicationCount: 0, + unreadInboxCountRes: { state: "empty" }, + unreadReportCountRes: { state: "empty" }, + unreadApplicationCountRes: { state: "empty" }, }; - subscription: any; collapseButtonRef = createRef(); mobileMenuRef = createRef(); constructor(props: any, context: any) { super(props, context); - this.parseMessage = this.parseMessage.bind(this); - this.subscription = wsSubscribe(this.parseMessage); this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this); } - componentDidMount() { + async componentDidMount() { // Subscribe to jwt changes if (isBrowser()) { // On the first load, check the unreads - const auth = myAuth(false); - if (auth && UserService.Instance.myUserInfo) { - this.requestNotificationPermission(); - WebSocketService.Instance.send( - wsClient.userJoin({ - auth, - }) - ); - - this.fetchUnreads(); - } - + this.requestNotificationPermission(); + this.fetchUnreads(); this.requestNotificationPermission(); - // Subscribe to unread count changes - this.unreadInboxCountSub = - UserService.Instance.unreadInboxCountSub.subscribe(res => { - this.setState({ unreadInboxCount: res }); - }); - // Subscribe to unread report count changes - this.unreadReportCountSub = - UserService.Instance.unreadReportCountSub.subscribe(res => { - this.setState({ unreadReportCount: res }); - }); - // Subscribe to unread application count - this.unreadApplicationCountSub = - UserService.Instance.unreadApplicationCountSub.subscribe(res => { - this.setState({ unreadApplicationCount: res }); - }); - - document.addEventListener("click", this.handleOutsideMenuClick); + document.addEventListener("mouseup", this.handleOutsideMenuClick); } } componentWillUnmount() { - this.wsSub.unsubscribe(); - this.userSub.unsubscribe(); - this.unreadInboxCountSub.unsubscribe(); - this.unreadReportCountSub.unsubscribe(); - this.unreadApplicationCountSub.unsubscribe(); - document.removeEventListener("click", this.handleOutsideMenuClick); + document.removeEventListener("mouseup", this.handleOutsideMenuClick); + } + + render() { + return this.navbar(); } // TODO class active corresponding to current page - render() { + navbar() { const siteView = this.props.siteRes?.site_view; const person = UserService.Instance.myUserInfo?.local_user_view.person; return ( @@ -148,15 +105,15 @@ export class Navbar extends Component { to="/inbox" className="p-1 nav-link border-0" title={i18n.t("unread_messages", { - count: Number(this.state.unreadInboxCount), - formattedCount: numToSI(this.state.unreadInboxCount), + count: Number(this.state.unreadApplicationCountRes.state), + formattedCount: numToSI(this.unreadInboxCount), })} onMouseUp={linkEvent(this, handleCollapseClick)} > - {this.state.unreadInboxCount > 0 && ( + {this.unreadInboxCount > 0 && ( - {numToSI(this.state.unreadInboxCount)} + {numToSI(this.unreadInboxCount)} )} @@ -167,15 +124,15 @@ export class Navbar extends Component { to="/reports" className="p-1 nav-link border-0" title={i18n.t("unread_reports", { - count: Number(this.state.unreadReportCount), - formattedCount: numToSI(this.state.unreadReportCount), + count: Number(this.unreadReportCount), + formattedCount: numToSI(this.unreadReportCount), })} onMouseUp={linkEvent(this, handleCollapseClick)} > - {this.state.unreadReportCount > 0 && ( + {this.unreadReportCount > 0 && ( - {numToSI(this.state.unreadReportCount)} + {numToSI(this.unreadReportCount)} )} @@ -187,15 +144,15 @@ export class Navbar extends Component { to="/registration_applications" className="p-1 nav-link border-0" title={i18n.t("unread_registration_applications", { - count: Number(this.state.unreadApplicationCount), - formattedCount: numToSI(this.state.unreadApplicationCount), + count: Number(this.unreadApplicationCount), + formattedCount: numToSI(this.unreadApplicationCount), })} onMouseUp={linkEvent(this, handleCollapseClick)} > - {this.state.unreadApplicationCount > 0 && ( + {this.unreadApplicationCount > 0 && ( - {numToSI(this.state.unreadApplicationCount)} + {numToSI(this.unreadApplicationCount)} )} @@ -272,20 +229,16 @@ export class Navbar extends Component {
    - {!this.context.router.history.location.pathname.match( - /^\/search/ - ) && ( -
  • - - - -
  • - )} +
  • + + + +
  • {amAdmin() && (
  • { className="nav-link" to="/inbox" title={i18n.t("unread_messages", { - count: Number(this.state.unreadInboxCount), - formattedCount: numToSI(this.state.unreadInboxCount), + count: Number(this.unreadInboxCount), + formattedCount: numToSI(this.unreadInboxCount), })} onMouseUp={linkEvent(this, handleCollapseClick)} > - {this.state.unreadInboxCount > 0 && ( - - {numToSI(this.state.unreadInboxCount)} + {this.unreadInboxCount > 0 && ( + + {numToSI(this.unreadInboxCount)} )} @@ -324,15 +277,15 @@ export class Navbar extends Component { className="nav-link" to="/reports" title={i18n.t("unread_reports", { - count: Number(this.state.unreadReportCount), - formattedCount: numToSI(this.state.unreadReportCount), + count: Number(this.unreadReportCount), + formattedCount: numToSI(this.unreadReportCount), })} onMouseUp={linkEvent(this, handleCollapseClick)} > - {this.state.unreadReportCount > 0 && ( - - {numToSI(this.state.unreadReportCount)} + {this.unreadReportCount > 0 && ( + + {numToSI(this.unreadReportCount)} )} @@ -344,17 +297,15 @@ export class Navbar extends Component { to="/registration_applications" className="nav-link" title={i18n.t("unread_registration_applications", { - count: Number(this.state.unreadApplicationCount), - formattedCount: numToSI( - this.state.unreadApplicationCount - ), + count: Number(this.unreadApplicationCount), + formattedCount: numToSI(this.unreadApplicationCount), })} onMouseUp={linkEvent(this, handleCollapseClick)} > - {this.state.unreadApplicationCount > 0 && ( + {this.unreadApplicationCount > 0 && ( - {numToSI(this.state.unreadApplicationCount)} + {numToSI(this.unreadApplicationCount)} )} @@ -457,101 +408,66 @@ export class Navbar extends Component { return amAdmin() || moderatesS; } - parseMessage(msg: any) { - const op = wsUserOp(msg); - console.log(msg); - if (msg.error) { - if (msg.error == "not_logged_in") { - UserService.Instance.logout(); - } - return; - } else if (msg.reconnect) { - console.log(i18n.t("websocket_reconnected")); - const auth = myAuth(false); - if (UserService.Instance.myUserInfo && auth) { - WebSocketService.Instance.send( - wsClient.userJoin({ - auth, - }) - ); - this.fetchUnreads(); - } - } else if (op == UserOperation.GetUnreadCount) { - const data = wsJsonToRes(msg); - this.setState({ - unreadInboxCount: data.replies + data.mentions + data.private_messages, - }); - this.sendUnreadCount(); - } else if (op == UserOperation.GetReportCount) { - const data = wsJsonToRes(msg); - this.setState({ - unreadReportCount: - data.post_reports + - data.comment_reports + - (data.private_message_reports ?? 0), - }); - this.sendReportUnread(); - } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) { - const data = - wsJsonToRes(msg); - this.setState({ unreadApplicationCount: data.registration_applications }); - this.sendApplicationUnread(); - } else if (op == UserOperation.CreateComment) { - const data = wsJsonToRes(msg); - const mui = UserService.Instance.myUserInfo; - if ( - mui && - data.recipient_ids.includes(mui.local_user_view.local_user.id) - ) { - this.setState({ - unreadInboxCount: this.state.unreadInboxCount + 1, - }); - this.sendUnreadCount(); - notifyComment(data.comment_view, this.context.router); - } - } else if (op == UserOperation.CreatePrivateMessage) { - const data = wsJsonToRes(msg); + fetchUnreads() { + poll(async () => { + if (window.document.visibilityState !== "hidden") { + const auth = myAuth(); + if (auth) { + this.setState({ + unreadInboxCountRes: await HttpService.client.getUnreadCount({ + auth, + }), + }); - if ( - data.private_message_view.recipient.id == - UserService.Instance.myUserInfo?.local_user_view.person.id - ) { - this.setState({ - unreadInboxCount: this.state.unreadInboxCount + 1, - }); - this.sendUnreadCount(); - notifyPrivateMessage(data.private_message_view, this.context.router); + if (this.moderatesSomething) { + this.setState({ + unreadReportCountRes: await HttpService.client.getReportCount({ + auth, + }), + }); + } + + if (amAdmin()) { + this.setState({ + unreadApplicationCountRes: + await HttpService.client.getUnreadRegistrationApplicationCount({ + auth, + }), + }); + } + } } + }, updateUnreadCountsInterval); + } + + get unreadInboxCount(): number { + if (this.state.unreadInboxCountRes.state == "success") { + const data = this.state.unreadInboxCountRes.data; + return data.replies + data.mentions + data.private_messages; + } else { + return 0; } } - fetchUnreads() { - console.log("Fetching inbox unreads..."); + get unreadReportCount(): number { + if (this.state.unreadReportCountRes.state == "success") { + const data = this.state.unreadReportCountRes.data; + return ( + data.post_reports + + data.comment_reports + + (data.private_message_reports ?? 0) + ); + } else { + return 0; + } + } - const auth = myAuth(); - if (auth) { - const unreadForm: GetUnreadCount = { - auth, - }; - WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm)); - - console.log("Fetching reports..."); - - const reportCountForm: GetReportCount = { - auth, - }; - WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm)); - - if (amAdmin()) { - console.log("Fetching applications..."); - - const applicationCountForm: GetUnreadRegistrationApplicationCount = { - auth, - }; - WebSocketService.Instance.send( - wsClient.getUnreadRegistrationApplicationCount(applicationCountForm) - ); - } + get unreadApplicationCount(): number { + if (this.state.unreadApplicationCountRes.state == "success") { + const data = this.state.unreadApplicationCountRes.data; + return data.registration_applications; + } else { + return 0; } } @@ -559,22 +475,6 @@ export class Navbar extends Component { return this.context.router.history.location.pathname; } - sendUnreadCount() { - UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount); - } - - sendReportUnread() { - UserService.Instance.unreadReportCountSub.next( - this.state.unreadReportCount - ); - } - - sendApplicationUnread() { - UserService.Instance.unreadApplicationCountSub.next( - this.state.unreadApplicationCount - ); - } - requestNotificationPermission() { if (UserService.Instance.myUserInfo) { document.addEventListener("DOMContentLoaded", function () { diff --git a/src/shared/components/comment/comment-form.tsx b/src/shared/components/comment/comment-form.tsx index 42ed226..c60cde2 100644 --- a/src/shared/components/comment/comment-form.tsx +++ b/src/shared/components/comment/comment-form.tsx @@ -1,25 +1,11 @@ import { Component } from "inferno"; import { T } from "inferno-i18next-dess"; import { Link } from "inferno-router"; -import { - CommentResponse, - CreateComment, - EditComment, - Language, - UserOperation, - wsJsonToRes, - wsUserOp, -} from "lemmy-js-client"; -import { Subscription } from "rxjs"; +import { CreateComment, EditComment, Language } from "lemmy-js-client"; import { i18n } from "../../i18next"; import { CommentNodeI } from "../../interfaces"; -import { UserService, WebSocketService } from "../../services"; -import { - capitalizeFirstLetter, - myAuth, - wsClient, - wsSubscribe, -} from "../../utils"; +import { UserService } from "../../services"; +import { capitalizeFirstLetter, myAuthRequired } from "../../utils"; import { Icon } from "../common/icon"; import { MarkdownTextArea } from "../common/markdown-textarea"; @@ -28,44 +14,21 @@ interface CommentFormProps { * Can either be the parent, or the editable comment. The right side is a postId. */ node: CommentNodeI | number; + finished?: boolean; edit?: boolean; disabled?: boolean; focus?: boolean; - onReplyCancel?(): any; + onReplyCancel?(): void; allLanguages: Language[]; siteLanguages: number[]; + onUpsertComment(form: EditComment | CreateComment): void; } -interface CommentFormState { - buttonTitle: string; - finished: boolean; - formId?: string; -} - -export class CommentForm extends Component { - private subscription?: Subscription; - state: CommentFormState = { - buttonTitle: - typeof this.props.node === "number" - ? capitalizeFirstLetter(i18n.t("post")) - : this.props.edit - ? capitalizeFirstLetter(i18n.t("save")) - : capitalizeFirstLetter(i18n.t("reply")), - finished: false, - }; - +export class CommentForm extends Component { constructor(props: any, context: any) { super(props, context); this.handleCommentSubmit = this.handleCommentSubmit.bind(this); - this.handleReplyCancel = this.handleReplyCancel.bind(this); - - this.parseMessage = this.parseMessage.bind(this); - this.subscription = wsSubscribe(this.parseMessage); - } - - componentWillUnmount() { - this.subscription?.unsubscribe(); } render() { @@ -82,13 +45,13 @@ export class CommentForm extends Component { { ); } - handleCommentSubmit(msg: { - val: string; - formId: string; - languageId?: number; - }) { - const content = msg.val; - const language_id = msg.languageId; - const node = this.props.node; + get buttonTitle(): string { + return typeof this.props.node === "number" + ? capitalizeFirstLetter(i18n.t("post")) + : this.props.edit + ? capitalizeFirstLetter(i18n.t("save")) + : capitalizeFirstLetter(i18n.t("reply")); + } - this.setState({ formId: msg.formId }); - - const auth = myAuth(); - if (auth) { - if (typeof node === "number") { - const postId = node; - const form: CreateComment = { + handleCommentSubmit(content: string, form_id: string, language_id?: number) { + const { node, onUpsertComment, edit } = this.props; + if (typeof node === "number") { + const post_id = node; + onUpsertComment({ + content, + post_id, + language_id, + form_id, + auth: myAuthRequired(), + }); + } else { + if (edit) { + const comment_id = node.comment_view.comment.id; + onUpsertComment({ content, - form_id: this.state.formId, - post_id: postId, + comment_id, + form_id, language_id, - auth, - }; - WebSocketService.Instance.send(wsClient.createComment(form)); + auth: myAuthRequired(), + }); } else { - if (this.props.edit) { - const form: EditComment = { - content, - form_id: this.state.formId, - comment_id: node.comment_view.comment.id, - language_id, - auth, - }; - WebSocketService.Instance.send(wsClient.editComment(form)); - } else { - const form: CreateComment = { - content, - form_id: this.state.formId, - post_id: node.comment_view.post.id, - parent_id: node.comment_view.comment.id, - language_id, - auth, - }; - WebSocketService.Instance.send(wsClient.createComment(form)); - } - } - } - } - - handleReplyCancel() { - this.props.onReplyCancel?.(); - } - - parseMessage(msg: any) { - const op = wsUserOp(msg); - console.log(msg); - - // Only do the showing and hiding if logged in - if (UserService.Instance.myUserInfo) { - if ( - op == UserOperation.CreateComment || - op == UserOperation.EditComment - ) { - const data = wsJsonToRes(msg); - - // This only finishes this form, if the randomly generated form_id matches the one received - if (this.state.formId && this.state.formId == data.form_id) { - this.setState({ finished: true }); - - // Necessary because it broke tribute for some reason - this.setState({ finished: false }); - } + const post_id = node.comment_view.post.id; + const parent_id = node.comment_view.comment.id; + this.props.onUpsertComment({ + content, + parent_id, + post_id, + form_id, + language_id, + auth: myAuthRequired(), + }); } } } diff --git a/src/shared/components/comment/comment-node.tsx b/src/shared/components/comment/comment-node.tsx index f80cc8b..0380a72 100644 --- a/src/shared/components/comment/comment-node.tsx +++ b/src/shared/components/comment/comment-node.tsx @@ -1,5 +1,5 @@ import classNames from "classnames"; -import { Component, linkEvent } from "inferno"; +import { Component, InfernoNode, linkEvent } from "inferno"; import { Link } from "inferno-router"; import { AddAdmin, @@ -7,13 +7,16 @@ import { BanFromCommunity, BanPerson, BlockPerson, + CommentId, CommentReplyView, CommentView, CommunityModeratorView, + CreateComment, CreateCommentLike, CreateCommentReport, DeleteComment, DistinguishComment, + EditComment, GetComments, Language, MarkCommentReplyAsRead, @@ -33,8 +36,9 @@ import { CommentNodeI, CommentViewType, PurgeType, + VoteType, } from "../../interfaces"; -import { UserService, WebSocketService } from "../../services"; +import { UserService } from "../../services"; import { amCommunityCreator, canAdmin, @@ -49,10 +53,11 @@ import { mdToHtml, mdToHtmlNoImages, myAuth, + myAuthRequired, + newVote, numToSI, setupTippy, showScores, - wsClient, } from "../../utils"; import { Icon, PurgeWarning, Spinner } from "../common/icon"; import { MomentTime } from "../common/moment-time"; @@ -74,7 +79,6 @@ interface CommentNodeState { showPurgeDialog: boolean; purgeReason?: string; purgeType: PurgeType; - purgeLoading: boolean; showConfirmTransferSite: boolean; showConfirmTransferCommunity: boolean; showConfirmAppointAsMod: boolean; @@ -84,12 +88,22 @@ interface CommentNodeState { showAdvanced: boolean; showReportDialog: boolean; reportReason?: string; - my_vote?: number; - score: number; - upvotes: number; - downvotes: number; - readLoading: boolean; + createOrEditCommentLoading: boolean; + upvoteLoading: boolean; + downvoteLoading: boolean; saveLoading: boolean; + readLoading: boolean; + blockPersonLoading: boolean; + deleteLoading: boolean; + removeLoading: boolean; + distinguishLoading: boolean; + banLoading: boolean; + addModLoading: boolean; + addAdminLoading: boolean; + transferCommunityLoading: boolean; + fetchChildrenLoading: boolean; + reportLoading: boolean; + purgeLoading: boolean; } interface CommentNodeProps { @@ -108,6 +122,26 @@ interface CommentNodeProps { allLanguages: Language[]; siteLanguages: number[]; hideImages?: boolean; + finished: Map; + onSaveComment(form: SaveComment): void; + onCommentReplyRead(form: MarkCommentReplyAsRead): void; + onPersonMentionRead(form: MarkPersonMentionAsRead): void; + onCreateComment(form: EditComment | CreateComment): void; + onEditComment(form: EditComment | CreateComment): void; + onCommentVote(form: CreateCommentLike): void; + onBlockPerson(form: BlockPerson): void; + onDeleteComment(form: DeleteComment): void; + onRemoveComment(form: RemoveComment): void; + onDistinguishComment(form: DistinguishComment): void; + onAddModToCommunity(form: AddModToCommunity): void; + onAddAdmin(form: AddAdmin): void; + onBanPersonFromCommunity(form: BanFromCommunity): void; + onBanPerson(form: BanPerson): void; + onTransferCommunity(form: TransferCommunity): void; + onFetchChildren?(form: GetComments): void; + onCommentReport(form: CreateCommentReport): void; + onPurgePerson(form: PurgePerson): void; + onPurgeComment(form: PurgeComment): void; } export class CommentNode extends Component { @@ -119,7 +153,6 @@ export class CommentNode extends Component { removeData: false, banType: BanType.Community, showPurgeDialog: false, - purgeLoading: false, purgeType: PurgeType.Person, collapsed: false, viewSource: false, @@ -129,75 +162,114 @@ export class CommentNode extends Component { showConfirmAppointAsMod: false, showConfirmAppointAsAdmin: false, showReportDialog: false, - my_vote: this.props.node.comment_view.my_vote, - score: this.props.node.comment_view.counts.score, - upvotes: this.props.node.comment_view.counts.upvotes, - downvotes: this.props.node.comment_view.counts.downvotes, - readLoading: false, + createOrEditCommentLoading: false, + upvoteLoading: false, + downvoteLoading: false, saveLoading: false, + readLoading: false, + blockPersonLoading: false, + deleteLoading: false, + removeLoading: false, + distinguishLoading: false, + banLoading: false, + addModLoading: false, + addAdminLoading: false, + transferCommunityLoading: false, + fetchChildrenLoading: false, + reportLoading: false, + purgeLoading: false, }; constructor(props: any, context: any) { super(props, context); this.handleReplyCancel = this.handleReplyCancel.bind(this); - this.handleCommentUpvote = this.handleCommentUpvote.bind(this); - this.handleCommentDownvote = this.handleCommentDownvote.bind(this); } - // TODO see if there's a better way to do this, and all willReceiveProps - componentWillReceiveProps(nextProps: CommentNodeProps) { - const cv = nextProps.node.comment_view; - this.setState({ - my_vote: cv.my_vote, - upvotes: cv.counts.upvotes, - downvotes: cv.counts.downvotes, - score: cv.counts.score, - readLoading: false, - saveLoading: false, - }); + get commentView(): CommentView { + return this.props.node.comment_view; + } + + get commentId(): CommentId { + return this.commentView.comment.id; + } + + componentWillReceiveProps( + nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps> + ): void { + if (this.props != nextProps) { + this.setState({ + showReply: false, + showEdit: false, + showRemoveDialog: false, + showBanDialog: false, + removeData: false, + banType: BanType.Community, + showPurgeDialog: false, + purgeType: PurgeType.Person, + collapsed: false, + viewSource: false, + showAdvanced: false, + showConfirmTransferSite: false, + showConfirmTransferCommunity: false, + showConfirmAppointAsMod: false, + showConfirmAppointAsAdmin: false, + showReportDialog: false, + createOrEditCommentLoading: false, + upvoteLoading: false, + downvoteLoading: false, + saveLoading: false, + readLoading: false, + blockPersonLoading: false, + deleteLoading: false, + removeLoading: false, + distinguishLoading: false, + banLoading: false, + addModLoading: false, + addAdminLoading: false, + transferCommunityLoading: false, + fetchChildrenLoading: false, + reportLoading: false, + purgeLoading: false, + }); + } } render() { const node = this.props.node; - const cv = this.props.node.comment_view; + const cv = this.commentView; const purgeTypeText = this.state.purgeType == PurgeType.Comment ? i18n.t("purge_comment") : `${i18n.t("purge")} ${cv.creator.name}`; - const canMod_ = - canMod(cv.creator.id, this.props.moderators, this.props.admins) && - cv.community.local; - const canModOnSelf = - canMod( - cv.creator.id, - this.props.moderators, - this.props.admins, - UserService.Instance.myUserInfo, - true - ) && cv.community.local; - const canAdmin_ = - canAdmin(cv.creator.id, this.props.admins) && cv.community.local; - const canAdminOnSelf = - canAdmin( - cv.creator.id, - this.props.admins, - UserService.Instance.myUserInfo, - true - ) && cv.community.local; + const canMod_ = canMod( + cv.creator.id, + this.props.moderators, + this.props.admins + ); + const canModOnSelf = canMod( + cv.creator.id, + this.props.moderators, + this.props.admins, + UserService.Instance.myUserInfo, + true + ); + const canAdmin_ = canAdmin(cv.creator.id, this.props.admins); + const canAdminOnSelf = canAdmin( + cv.creator.id, + this.props.admins, + UserService.Instance.myUserInfo, + true + ); const isMod_ = isMod(cv.creator.id, this.props.moderators); - const isAdmin_ = - isAdmin(cv.creator.id, this.props.admins) && cv.community.local; + const isAdmin_ = isAdmin(cv.creator.id, this.props.admins); const amCommunityCreator_ = amCommunityCreator( cv.creator.id, this.props.moderators ); - const borderColor = this.props.node.depth - ? colorList[(this.props.node.depth - 1) % colorList.length] - : colorList[0]; const moreRepliesBorderColor = this.props.node.depth ? colorList[this.props.node.depth % colorList.length] : colorList[0]; @@ -209,28 +281,17 @@ export class CommentNode extends Component { node.comment_view.counts.child_count > 0; return ( -
    +
  • @@ -297,18 +358,24 @@ export class CommentNode extends Component { <> - - {numToSI(this.state.score)} - + {this.state.upvoteLoading ? ( + + ) : ( + + {numToSI(this.commentView.counts.score)} + + )} @@ -327,9 +394,13 @@ export class CommentNode extends Component { edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} + finished={this.props.finished.get( + this.props.node.comment_view.comment.id + )} focus allLanguages={this.props.allLanguages} siteLanguages={this.props.siteLanguages} + onUpsertComment={this.props.onEditComment} /> )} {!this.state.showEdit && !this.state.collapsed && ( @@ -351,7 +422,7 @@ export class CommentNode extends Component { {this.props.markable && ( {this.props.enableDownvotes && ( )} )} {(canModOnSelf || canAdminOnSelf) && ( @@ -546,7 +638,7 @@ export class CommentNode extends Component { className="btn btn-link btn-animate text-muted" onClick={linkEvent( this, - this.handleDistinguishClick + this.handleDistinguishComment )} data-tippy-content={ !cv.comment.distinguished @@ -588,11 +680,15 @@ export class CommentNode extends Component { className="btn btn-link btn-animate text-muted" onClick={linkEvent( this, - this.handleModRemoveSubmit + this.handleRemoveComment )} aria-label={i18n.t("restore")} > - {i18n.t("restore")} + {this.state.removeLoading ? ( + + ) : ( + i18n.t("restore") + )} )} @@ -617,11 +713,15 @@ export class CommentNode extends Component { className="btn btn-link btn-animate text-muted" onClick={linkEvent( this, - this.handleModBanFromCommunitySubmit + this.handleBanPersonFromCommunity )} aria-label={i18n.t("unban")} > - {i18n.t("unban")} + {this.state.banLoading ? ( + + ) : ( + i18n.t("unban") + )} ))} {!cv.creator_banned_from_community && @@ -658,7 +758,11 @@ export class CommentNode extends Component { )} aria-label={i18n.t("yes")} > - {i18n.t("yes")} + {this.state.addModLoading ? ( + + ) : ( + i18n.t("yes") + )} )} @@ -803,7 +915,11 @@ export class CommentNode extends Component { )} aria-label={i18n.t("yes")} > - {i18n.t("yes")} + {this.state.addAdminLoading ? ( + + ) : ( + i18n.t("yes") + )}
    {showMoreChildren && (
    )} @@ -852,7 +976,7 @@ export class CommentNode extends Component { {this.state.showRemoveDialog && (
    )} {this.state.showPurgeDialog && ( -
    +
    + {this.state.collapsed &&
    } +
  • ); } get commentReplyOrMentionRead(): boolean { - const cv = this.props.node.comment_view; + const cv = this.commentView; if (this.isPersonMentionType(cv)) { return cv.person_mention.read; @@ -1044,7 +1200,8 @@ export class CommentNode extends Component { } linkBtn(small = false) { - const cv = this.props.node.comment_view; + const cv = this.commentView; + const classnames = classNames("btn btn-link btn-animate text-muted", { "btn-sm": small, }); @@ -1074,26 +1231,52 @@ export class CommentNode extends Component { ); } - get loadingIcon() { - return ; - } - get myComment(): boolean { return ( UserService.Instance.myUserInfo?.local_user_view.person.id == - this.props.node.comment_view.creator.id + this.commentView.creator.id ); } get isPostCreator(): boolean { - return ( - this.props.node.comment_view.creator.id == - this.props.node.comment_view.post.creator_id - ); + return this.commentView.creator.id == this.commentView.post.creator_id; + } + + get scoreColor() { + if (this.commentView.my_vote == 1) { + return "text-info"; + } else if (this.commentView.my_vote == -1) { + return "text-danger"; + } else { + return "text-muted"; + } + } + + get pointsTippy(): string { + const points = i18n.t("number_of_points", { + count: Number(this.commentView.counts.score), + formattedCount: numToSI(this.commentView.counts.score), + }); + + const upvotes = i18n.t("number_of_upvotes", { + count: Number(this.commentView.counts.upvotes), + formattedCount: numToSI(this.commentView.counts.upvotes), + }); + + const downvotes = i18n.t("number_of_downvotes", { + count: Number(this.commentView.counts.downvotes), + formattedCount: numToSI(this.commentView.counts.downvotes), + }); + + return `${points} • ${upvotes} • ${downvotes}`; + } + + get expandText(): string { + return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse"); } get commentUnlessRemoved(): string { - const comment = this.props.node.comment_view.comment; + const comment = this.commentView.comment; return comment.removed ? `*${i18n.t("removed")}*` : comment.deleted @@ -1109,127 +1292,10 @@ export class CommentNode extends Component { i.setState({ showEdit: true }); } - handleBlockUserClick(i: CommentNode) { - const auth = myAuth(); - if (auth) { - const blockUserForm: BlockPerson = { - person_id: i.props.node.comment_view.creator.id, - block: true, - auth, - }; - WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm)); - } - } - - handleDeleteClick(i: CommentNode) { - const comment = i.props.node.comment_view.comment; - const auth = myAuth(); - if (auth) { - const deleteForm: DeleteComment = { - comment_id: comment.id, - deleted: !comment.deleted, - auth, - }; - WebSocketService.Instance.send(wsClient.deleteComment(deleteForm)); - } - } - - handleSaveCommentClick(i: CommentNode) { - const cv = i.props.node.comment_view; - const save = cv.saved == undefined ? true : !cv.saved; - const auth = myAuth(); - if (auth) { - const form: SaveComment = { - comment_id: cv.comment.id, - save, - auth, - }; - - WebSocketService.Instance.send(wsClient.saveComment(form)); - - i.setState({ saveLoading: true }); - } - } - handleReplyCancel() { this.setState({ showReply: false, showEdit: false }); } - handleCommentUpvote(event: any) { - event.preventDefault(); - const myVote = this.state.my_vote; - const newVote = myVote == 1 ? 0 : 1; - - if (myVote == 1) { - this.setState({ - score: this.state.score - 1, - upvotes: this.state.upvotes - 1, - }); - } else if (myVote == -1) { - this.setState({ - downvotes: this.state.downvotes - 1, - upvotes: this.state.upvotes + 1, - score: this.state.score + 2, - }); - } else { - this.setState({ - score: this.state.score + 1, - upvotes: this.state.upvotes + 1, - }); - } - - this.setState({ my_vote: newVote }); - - const auth = myAuth(); - if (auth) { - const form: CreateCommentLike = { - comment_id: this.props.node.comment_view.comment.id, - score: newVote, - auth, - }; - WebSocketService.Instance.send(wsClient.likeComment(form)); - setupTippy(); - } - } - - handleCommentDownvote(event: any) { - event.preventDefault(); - const myVote = this.state.my_vote; - const newVote = myVote == -1 ? 0 : -1; - - if (myVote == 1) { - this.setState({ - downvotes: this.state.downvotes + 1, - upvotes: this.state.upvotes - 1, - score: this.state.score - 2, - }); - } else if (myVote == -1) { - this.setState({ - downvotes: this.state.downvotes - 1, - score: this.state.score + 1, - }); - } else { - this.setState({ - downvotes: this.state.downvotes + 1, - score: this.state.score - 1, - }); - } - - this.setState({ my_vote: newVote }); - - const auth = myAuth(); - if (auth) { - const form: CreateCommentLike = { - comment_id: this.props.node.comment_view.comment.id, - score: newVote, - auth, - }; - - WebSocketService.Instance.send(wsClient.likeComment(form)); - setupTippy(); - } - } - handleShowReportDialog(i: CommentNode) { i.setState({ showReportDialog: !i.state.showReportDialog }); } @@ -1238,21 +1304,6 @@ export class CommentNode extends Component { i.setState({ reportReason: event.target.value }); } - handleReportSubmit(i: CommentNode) { - const comment = i.props.node.comment_view.comment; - const reason = i.state.reportReason; - const auth = myAuth(); - if (reason && auth) { - const form: CreateCommentReport = { - comment_id: comment.id, - reason, - auth, - }; - WebSocketService.Instance.send(wsClient.createCommentReport(form)); - i.setState({ showReportDialog: false }); - } - } - handleModRemoveShow(i: CommentNode) { i.setState({ showRemoveDialog: !i.state.showRemoveDialog, @@ -1268,36 +1319,6 @@ export class CommentNode extends Component { i.setState({ removeData: event.target.checked }); } - handleModRemoveSubmit(i: CommentNode) { - const comment = i.props.node.comment_view.comment; - const auth = myAuth(); - if (auth) { - const form: RemoveComment = { - comment_id: comment.id, - removed: !comment.removed, - reason: i.state.removeReason, - auth, - }; - WebSocketService.Instance.send(wsClient.removeComment(form)); - - i.setState({ showRemoveDialog: false }); - } - } - - handleDistinguishClick(i: CommentNode) { - const comment = i.props.node.comment_view.comment; - const auth = myAuth(); - if (auth) { - const form: DistinguishComment = { - comment_id: comment.id, - distinguished: !comment.distinguished, - auth, - }; - WebSocketService.Instance.send(wsClient.editComment(form)); - i.setState(i.state); - } - } - isPersonMentionType( item: CommentView | PersonMentionView | CommentReplyView ): item is PersonMentionView { @@ -1310,29 +1331,6 @@ export class CommentNode extends Component { return (item as CommentReplyView).comment_reply?.id !== undefined; } - handleMarkRead(i: CommentNode) { - const auth = myAuth(); - if (auth) { - if (i.isPersonMentionType(i.props.node.comment_view)) { - const form: MarkPersonMentionAsRead = { - person_mention_id: i.props.node.comment_view.person_mention.id, - read: !i.props.node.comment_view.person_mention.read, - auth, - }; - WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form)); - } else if (i.isCommentReplyType(i.props.node.comment_view)) { - const form: MarkCommentReplyAsRead = { - comment_reply_id: i.props.node.comment_view.comment_reply.id, - read: !i.props.node.comment_view.comment_reply.read, - auth, - }; - WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form)); - } - - i.setState({ readLoading: true }); - } - } - handleModBanFromCommunityShow(i: CommentNode) { i.setState({ showBanDialog: true, @@ -1357,57 +1355,6 @@ export class CommentNode extends Component { i.setState({ banExpireDays: event.target.value }); } - handleModBanFromCommunitySubmit(i: CommentNode) { - i.setState({ banType: BanType.Community }); - i.handleModBanBothSubmit(i); - } - - handleModBanSubmit(i: CommentNode) { - i.setState({ banType: BanType.Site }); - i.handleModBanBothSubmit(i); - } - - handleModBanBothSubmit(i: CommentNode) { - const cv = i.props.node.comment_view; - const auth = myAuth(); - if (auth) { - if (i.state.banType == BanType.Community) { - // If its an unban, restore all their data - const ban = !cv.creator_banned_from_community; - if (ban == false) { - i.setState({ removeData: false }); - } - const form: BanFromCommunity = { - person_id: cv.creator.id, - community_id: cv.community.id, - ban, - remove_data: i.state.removeData, - reason: i.state.banReason, - expires: futureDaysToUnixTime(i.state.banExpireDays), - auth, - }; - WebSocketService.Instance.send(wsClient.banFromCommunity(form)); - } else { - // If its an unban, restore all their data - const ban = !cv.creator.banned; - if (ban == false) { - i.setState({ removeData: false }); - } - const form: BanPerson = { - person_id: cv.creator.id, - ban, - remove_data: i.state.removeData, - reason: i.state.banReason, - expires: futureDaysToUnixTime(i.state.banExpireDays), - auth, - }; - WebSocketService.Instance.send(wsClient.banPerson(form)); - } - - i.setState({ showBanDialog: false }); - } - } - handlePurgePersonShow(i: CommentNode) { i.setState({ showPurgeDialog: true, @@ -1428,30 +1375,6 @@ export class CommentNode extends Component { i.setState({ purgeReason: event.target.value }); } - handlePurgeSubmit(i: CommentNode, event: any) { - event.preventDefault(); - const auth = myAuth(); - if (auth) { - if (i.state.purgeType == PurgeType.Person) { - const form: PurgePerson = { - person_id: i.props.node.comment_view.creator.id, - reason: i.state.purgeReason, - auth, - }; - WebSocketService.Instance.send(wsClient.purgePerson(form)); - } else if (i.state.purgeType == PurgeType.Comment) { - const form: PurgeComment = { - comment_id: i.props.node.comment_view.comment.id, - reason: i.state.purgeReason, - auth, - }; - WebSocketService.Instance.send(wsClient.purgeComment(form)); - } - - i.setState({ purgeLoading: true }); - } - } - handleShowConfirmAppointAsMod(i: CommentNode) { i.setState({ showConfirmAppointAsMod: true }); } @@ -1460,21 +1383,6 @@ export class CommentNode extends Component { i.setState({ showConfirmAppointAsMod: false }); } - handleAddModToCommunity(i: CommentNode) { - const cv = i.props.node.comment_view; - const auth = myAuth(); - if (auth) { - const form: AddModToCommunity = { - person_id: cv.creator.id, - community_id: cv.community.id, - added: !isMod(cv.creator.id, i.props.moderators), - auth, - }; - WebSocketService.Instance.send(wsClient.addModToCommunity(form)); - i.setState({ showConfirmAppointAsMod: false }); - } - } - handleShowConfirmAppointAsAdmin(i: CommentNode) { i.setState({ showConfirmAppointAsAdmin: true }); } @@ -1483,20 +1391,6 @@ export class CommentNode extends Component { i.setState({ showConfirmAppointAsAdmin: false }); } - handleAddAdmin(i: CommentNode) { - const auth = myAuth(); - if (auth) { - const creatorId = i.props.node.comment_view.creator.id; - const form: AddAdmin = { - person_id: creatorId, - added: !isAdmin(creatorId, i.props.admins), - auth, - }; - WebSocketService.Instance.send(wsClient.addAdmin(form)); - i.setState({ showConfirmAppointAsAdmin: false }); - } - } - handleShowConfirmTransferCommunity(i: CommentNode) { i.setState({ showConfirmTransferCommunity: true }); } @@ -1505,20 +1399,6 @@ export class CommentNode extends Component { i.setState({ showConfirmTransferCommunity: false }); } - handleTransferCommunity(i: CommentNode) { - const cv = i.props.node.comment_view; - const auth = myAuth(); - if (auth) { - const form: TransferCommunity = { - community_id: cv.community.id, - person_id: cv.creator.id, - auth, - }; - WebSocketService.Instance.send(wsClient.transferCommunity(form)); - i.setState({ showConfirmTransferCommunity: false }); - } - } - handleShowConfirmTransferSite(i: CommentNode) { i.setState({ showConfirmTransferSite: true }); } @@ -1529,7 +1409,7 @@ export class CommentNode extends Component { get isCommentNew(): boolean { const now = moment.utc().subtract(10, "minutes"); - const then = moment.utc(this.props.node.comment_view.comment.published); + const then = moment.utc(this.commentView.comment.published); return now.isBefore(then); } @@ -1547,50 +1427,193 @@ export class CommentNode extends Component { setupTippy(); } + handleSaveComment(i: CommentNode) { + i.setState({ saveLoading: true }); + + i.props.onSaveComment({ + comment_id: i.commentView.comment.id, + save: !i.commentView.saved, + auth: myAuthRequired(), + }); + } + + handleUpvote(i: CommentNode) { + i.setState({ upvoteLoading: true }); + i.props.onCommentVote({ + comment_id: i.commentId, + score: newVote(VoteType.Upvote, i.commentView.my_vote), + auth: myAuthRequired(), + }); + } + + handleDownvote(i: CommentNode) { + i.setState({ downvoteLoading: true }); + i.props.onCommentVote({ + comment_id: i.commentId, + score: newVote(VoteType.Downvote, i.commentView.my_vote), + auth: myAuthRequired(), + }); + } + + handleBlockPerson(i: CommentNode) { + i.setState({ blockPersonLoading: true }); + i.props.onBlockPerson({ + person_id: i.commentView.creator.id, + block: true, + auth: myAuthRequired(), + }); + } + + handleMarkAsRead(i: CommentNode) { + i.setState({ readLoading: true }); + const cv = i.commentView; + if (i.isPersonMentionType(cv)) { + i.props.onPersonMentionRead({ + person_mention_id: cv.person_mention.id, + read: !cv.person_mention.read, + auth: myAuthRequired(), + }); + } else if (i.isCommentReplyType(cv)) { + i.props.onCommentReplyRead({ + comment_reply_id: cv.comment_reply.id, + read: !cv.comment_reply.read, + auth: myAuthRequired(), + }); + } + } + + handleDeleteComment(i: CommentNode) { + i.setState({ deleteLoading: true }); + i.props.onDeleteComment({ + comment_id: i.commentId, + deleted: !i.commentView.comment.deleted, + auth: myAuthRequired(), + }); + } + + handleRemoveComment(i: CommentNode, event: any) { + event.preventDefault(); + i.setState({ removeLoading: true }); + i.props.onRemoveComment({ + comment_id: i.commentId, + removed: !i.commentView.comment.removed, + auth: myAuthRequired(), + }); + } + + handleDistinguishComment(i: CommentNode) { + i.setState({ distinguishLoading: true }); + i.props.onDistinguishComment({ + comment_id: i.commentId, + distinguished: !i.commentView.comment.distinguished, + auth: myAuthRequired(), + }); + } + + handleBanPersonFromCommunity(i: CommentNode) { + i.setState({ banLoading: true }); + i.props.onBanPersonFromCommunity({ + community_id: i.commentView.community.id, + person_id: i.commentView.creator.id, + ban: !i.commentView.creator_banned_from_community, + reason: i.state.banReason, + remove_data: i.state.removeData, + expires: futureDaysToUnixTime(i.state.banExpireDays), + auth: myAuthRequired(), + }); + } + + handleBanPerson(i: CommentNode) { + i.setState({ banLoading: true }); + i.props.onBanPerson({ + person_id: i.commentView.creator.id, + ban: !i.commentView.creator_banned_from_community, + reason: i.state.banReason, + remove_data: i.state.removeData, + expires: futureDaysToUnixTime(i.state.banExpireDays), + auth: myAuthRequired(), + }); + } + + handleModBanBothSubmit(i: CommentNode, event: any) { + event.preventDefault(); + if (i.state.banType == BanType.Community) { + i.handleBanPersonFromCommunity(i); + } else { + i.handleBanPerson(i); + } + } + + handleAddModToCommunity(i: CommentNode) { + i.setState({ addModLoading: true }); + + const added = !isMod(i.commentView.comment.creator_id, i.props.moderators); + i.props.onAddModToCommunity({ + community_id: i.commentView.community.id, + person_id: i.commentView.creator.id, + added, + auth: myAuthRequired(), + }); + } + + handleAddAdmin(i: CommentNode) { + i.setState({ addAdminLoading: true }); + + const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins); + i.props.onAddAdmin({ + person_id: i.commentView.creator.id, + added, + auth: myAuthRequired(), + }); + } + + handleTransferCommunity(i: CommentNode) { + i.setState({ transferCommunityLoading: true }); + i.props.onTransferCommunity({ + community_id: i.commentView.community.id, + person_id: i.commentView.creator.id, + auth: myAuthRequired(), + }); + } + + handleReportComment(i: CommentNode, event: any) { + event.preventDefault(); + i.setState({ reportLoading: true }); + i.props.onCommentReport({ + comment_id: i.commentId, + reason: i.state.reportReason ?? "", + auth: myAuthRequired(), + }); + } + + handlePurgeBothSubmit(i: CommentNode, event: any) { + event.preventDefault(); + i.setState({ purgeLoading: true }); + + if (i.state.purgeType == PurgeType.Person) { + i.props.onPurgePerson({ + person_id: i.commentView.creator.id, + reason: i.state.purgeReason, + auth: myAuthRequired(), + }); + } else { + i.props.onPurgeComment({ + comment_id: i.commentId, + reason: i.state.purgeReason, + auth: myAuthRequired(), + }); + } + } + handleFetchChildren(i: CommentNode) { - const form: GetComments = { - post_id: i.props.node.comment_view.post.id, - parent_id: i.props.node.comment_view.comment.id, + i.setState({ fetchChildrenLoading: true }); + i.props.onFetchChildren?.({ + parent_id: i.commentId, max_depth: commentTreeMaxDepth, limit: 999, // TODO type_: "All", saved_only: false, - auth: myAuth(false), - }; - - WebSocketService.Instance.send(wsClient.getComments(form)); - } - - get scoreColor() { - if (this.state.my_vote == 1) { - return "text-info"; - } else if (this.state.my_vote == -1) { - return "text-danger"; - } else { - return "text-muted"; - } - } - - get pointsTippy(): string { - const points = i18n.t("number_of_points", { - count: Number(this.state.score), - formattedCount: numToSI(this.state.score), + auth: myAuth(), }); - - const upvotes = i18n.t("number_of_upvotes", { - count: Number(this.state.upvotes), - formattedCount: numToSI(this.state.upvotes), - }); - - const downvotes = i18n.t("number_of_downvotes", { - count: Number(this.state.downvotes), - formattedCount: numToSI(this.state.downvotes), - }); - - return `${points} • ${upvotes} • ${downvotes}`; - } - - get expandText(): string { - return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse"); } } diff --git a/src/shared/components/comment/comment-nodes.tsx b/src/shared/components/comment/comment-nodes.tsx index 23f22fe..8c0a236 100644 --- a/src/shared/components/comment/comment-nodes.tsx +++ b/src/shared/components/comment/comment-nodes.tsx @@ -1,6 +1,32 @@ +import classNames from "classnames"; import { Component } from "inferno"; -import { CommunityModeratorView, Language, PersonView } from "lemmy-js-client"; +import { + AddAdmin, + AddModToCommunity, + BanFromCommunity, + BanPerson, + BlockPerson, + CommentId, + CommunityModeratorView, + CreateComment, + CreateCommentLike, + CreateCommentReport, + DeleteComment, + DistinguishComment, + EditComment, + GetComments, + Language, + MarkCommentReplyAsRead, + MarkPersonMentionAsRead, + PersonView, + PurgeComment, + PurgePerson, + RemoveComment, + SaveComment, + TransferCommunity, +} from "lemmy-js-client"; import { CommentNodeI, CommentViewType } from "../../interfaces"; +import { colorList } from "../../utils"; import { CommentNode } from "./comment-node"; interface CommentNodesProps { @@ -20,6 +46,28 @@ interface CommentNodesProps { allLanguages: Language[]; siteLanguages: number[]; hideImages?: boolean; + isChild?: boolean; + depth?: number; + finished: Map; + onSaveComment(form: SaveComment): void; + onCommentReplyRead(form: MarkCommentReplyAsRead): void; + onPersonMentionRead(form: MarkPersonMentionAsRead): void; + onCreateComment(form: EditComment | CreateComment): void; + onEditComment(form: EditComment | CreateComment): void; + onCommentVote(form: CreateCommentLike): void; + onBlockPerson(form: BlockPerson): void; + onDeleteComment(form: DeleteComment): void; + onRemoveComment(form: RemoveComment): void; + onDistinguishComment(form: DistinguishComment): void; + onAddModToCommunity(form: AddModToCommunity): void; + onAddAdmin(form: AddAdmin): void; + onBanPersonFromCommunity(form: BanFromCommunity): void; + onBanPerson(form: BanPerson): void; + onTransferCommunity(form: TransferCommunity): void; + onFetchChildren?(form: GetComments): void; + onCommentReport(form: CreateCommentReport): void; + onPurgePerson(form: PurgePerson): void; + onPurgeComment(form: PurgeComment): void; } export class CommentNodes extends Component { @@ -30,29 +78,61 @@ export class CommentNodes extends Component { render() { const maxComments = this.props.maxCommentsShown ?? this.props.nodes.length; + const borderColor = this.props.depth + ? colorList[this.props.depth % colorList.length] + : colorList[0]; + return ( -
    - {this.props.nodes.slice(0, maxComments).map(node => ( - - ))} -
    + this.props.nodes.length > 0 && ( +
      + {this.props.nodes.slice(0, maxComments).map(node => ( + + ))} +
    + ) ); } } diff --git a/src/shared/components/comment/comment-report.tsx b/src/shared/components/comment/comment-report.tsx index 64c659f..ff00bc5 100644 --- a/src/shared/components/comment/comment-report.tsx +++ b/src/shared/components/comment/comment-report.tsx @@ -1,4 +1,4 @@ -import { Component, linkEvent } from "inferno"; +import { Component, InfernoNode, linkEvent } from "inferno"; import { T } from "inferno-i18next-dess"; import { CommentReportView, @@ -7,21 +7,39 @@ import { } from "lemmy-js-client"; import { i18n } from "../../i18next"; import { CommentNodeI, CommentViewType } from "../../interfaces"; -import { WebSocketService } from "../../services"; -import { myAuth, wsClient } from "../../utils"; -import { Icon } from "../common/icon"; +import { myAuthRequired } from "../../utils"; +import { Icon, Spinner } from "../common/icon"; import { PersonListing } from "../person/person-listing"; import { CommentNode } from "./comment-node"; interface CommentReportProps { report: CommentReportView; + onResolveReport(form: ResolveCommentReport): void; } -export class CommentReport extends Component { +interface CommentReportState { + loading: boolean; +} + +export class CommentReport extends Component< + CommentReportProps, + CommentReportState +> { + state: CommentReportState = { + loading: false, + }; constructor(props: any, context: any) { super(props, context); } + componentWillReceiveProps( + nextProps: Readonly<{ children?: InfernoNode } & CommentReportProps> + ): void { + if (this.props != nextProps) { + this.setState({ loading: false }); + } + } + render() { const r = this.props.report; const comment = r.comment; @@ -62,6 +80,26 @@ export class CommentReport extends Component { allLanguages={[]} siteLanguages={[]} hideImages + // All of these are unused, since its viewonly + finished={new Map()} + onSaveComment={() => {}} + onBlockPerson={() => {}} + onDeleteComment={() => {}} + onRemoveComment={() => {}} + onCommentVote={() => {}} + onCommentReport={() => {}} + onDistinguishComment={() => {}} + onAddModToCommunity={() => {}} + onAddAdmin={() => {}} + onTransferCommunity={() => {}} + onPurgeComment={() => {}} + onPurgePerson={() => {}} + onCommentReplyRead={() => {}} + onPersonMentionRead={() => {}} + onBanPersonFromCommunity={() => {}} + onBanPerson={() => {}} + onCreateComment={() => Promise.resolve({ state: "empty" })} + onEditComment={() => Promise.resolve({ state: "empty" })} />
    {i18n.t("reporter")}: @@ -90,26 +128,27 @@ export class CommentReport extends Component { data-tippy-content={tippyContent} aria-label={tippyContent} > - + {this.state.loading ? ( + + ) : ( + + )}
    ); } handleResolveReport(i: CommentReport) { - const auth = myAuth(); - if (auth) { - const form: ResolveCommentReport = { - report_id: i.props.report.comment_report.id, - resolved: !i.props.report.comment_report.resolved, - auth, - }; - WebSocketService.Instance.send(wsClient.resolveCommentReport(form)); - } + i.setState({ loading: true }); + i.props.onResolveReport({ + report_id: i.props.report.comment_report.id, + resolved: !i.props.report.comment_report.resolved, + auth: myAuthRequired(), + }); } } diff --git a/src/shared/components/common/image-upload-form.tsx b/src/shared/components/common/image-upload-form.tsx index cfb8615..61fdd61 100644 --- a/src/shared/components/common/image-upload-form.tsx +++ b/src/shared/components/common/image-upload-form.tsx @@ -1,7 +1,7 @@ import { Component, linkEvent } from "inferno"; import { i18n } from "../../i18next"; -import { UserService } from "../../services"; -import { randomStr, toast, uploadImage } from "../../utils"; +import { HttpService, UserService } from "../../services"; +import { randomStr, toast } from "../../utils"; import { Icon } from "./icon"; interface ImageUploadFormProps { @@ -73,27 +73,26 @@ export class ImageUploadForm extends Component< handleImageUpload(i: ImageUploadForm, event: any) { event.preventDefault(); - const file = event.target.files[0]; + const image = event.target.files[0] as File; i.setState({ loading: true }); - uploadImage(file) - .then(res => { - console.log("pictrs upload:"); - console.log(res); - if (res.msg === "ok") { - i.setState({ loading: false }); - i.props.onUpload(res.url as string); + HttpService.client.uploadImage({ image }).then(res => { + console.log("pictrs upload:"); + console.log(res); + if (res.state === "success") { + if (res.data.msg === "ok") { + i.props.onUpload(res.data.url as string); } else { - i.setState({ loading: false }); toast(JSON.stringify(res), "danger"); } - }) - .catch(error => { - i.setState({ loading: false }); - console.error(error); - toast(error, "danger"); - }); + } else if (res.state === "failed") { + console.error(res.msg); + toast(res.msg, "danger"); + } + + i.setState({ loading: false }); + }); } handleRemoveImage(i: ImageUploadForm, event: any) { diff --git a/src/shared/components/common/listing-type-select.tsx b/src/shared/components/common/listing-type-select.tsx index abafe37..3e534d3 100644 --- a/src/shared/components/common/listing-type-select.tsx +++ b/src/shared/components/common/listing-type-select.tsx @@ -8,7 +8,7 @@ interface ListingTypeSelectProps { type_: ListingType; showLocal: boolean; showSubscribed: boolean; - onChange?(val: ListingType): any; + onChange(val: ListingType): void; } interface ListingTypeSelectState { @@ -29,11 +29,11 @@ export class ListingTypeSelect extends Component< super(props, context); } - static getDerivedStateFromProps(props: any): ListingTypeSelectProps { + static getDerivedStateFromProps( + props: ListingTypeSelectProps + ): ListingTypeSelectState { return { type_: props.type_, - showLocal: props.showLocal, - showSubscribed: props.showSubscribed, }; } @@ -97,6 +97,6 @@ export class ListingTypeSelect extends Component< } handleTypeChange(i: ListingTypeSelect, event: any) { - i.props.onChange?.(event.target.value); + i.props.onChange(event.target.value); } } diff --git a/src/shared/components/common/markdown-textarea.tsx b/src/shared/components/common/markdown-textarea.tsx index 92b8e2b..9318d3b 100644 --- a/src/shared/components/common/markdown-textarea.tsx +++ b/src/shared/components/common/markdown-textarea.tsx @@ -3,7 +3,7 @@ import { NoOptionI18nKeys } from "i18next"; import { Component, linkEvent } from "inferno"; import { Language } from "lemmy-js-client"; import { i18n } from "../../i18next"; -import { UserService } from "../../services"; +import { HttpService, UserService } from "../../services"; import { concurrentImageUpload, customEmojisLookup, @@ -19,7 +19,6 @@ import { setupTippy, setupTribute, toast, - uploadImage, } from "../../utils"; import { EmojiPicker } from "./emoji-picker"; import { Icon, Spinner } from "./icon"; @@ -39,9 +38,9 @@ interface MarkdownTextAreaProps { finished?: boolean; showLanguage?: boolean; hideNavigationWarnings?: boolean; - onContentChange?(val: string): any; - onReplyCancel?(): any; - onSubmit?(msg: { val?: string; formId: string; languageId?: number }): any; + onContentChange?(val: string): void; + onReplyCancel?(): void; + onSubmit?(content: string, formId: string, languageId?: number): void; allLanguages: Language[]; // TODO should probably be nullable siteLanguages: number[]; // TODO same } @@ -55,8 +54,9 @@ interface MarkdownTextAreaState { content?: string; languageId?: number; previewMode: boolean; - loading: boolean; imageUploadStatus?: ImageUploadStatus; + loading: boolean; + submitted: boolean; } export class MarkdownTextArea extends Component< @@ -72,6 +72,7 @@ export class MarkdownTextArea extends Component< languageId: this.props.initialLanguageId, previewMode: false, loading: false, + submitted: false, }; constructor(props: any, context: any) { @@ -105,17 +106,14 @@ export class MarkdownTextArea extends Component< } } - componentDidUpdate() { - if (!this.props.hideNavigationWarnings && this.state.content) { - window.onbeforeunload = () => true; - } else { - window.onbeforeunload = null; - } - } - componentWillReceiveProps(nextProps: MarkdownTextAreaProps) { if (nextProps.finished) { - this.setState({ previewMode: false, loading: false, content: undefined }); + this.setState({ + previewMode: false, + imageUploadStatus: undefined, + loading: false, + content: undefined, + }); if (this.props.replyType) { this.props.onReplyCancel?.(); } @@ -127,16 +125,23 @@ export class MarkdownTextArea extends Component< } } - componentWillUnmount() { - window.onbeforeunload = null; - } - render() { const languageId = this.state.languageId; + // TODO add these prompts back in at some point + // return ( - +