diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml index 64579090..2273a138 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 375d06d3..2f6f3fc1 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 460d9a44..734937e9 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 199b97e9..73ef5482 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/package.json b/package.json index fd7cf4ad..2298d9e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lemmy-ui", - "version": "0.18.0-beta.6", + "version": "0.18.0-rc.1", "description": "An isomorphic UI for lemmy", "repository": "https://github.com/LemmyNet/lemmy-ui", "license": "AGPL-3.0", diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 1c45341d..e1adfc53 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/server/index.tsx b/src/server/index.tsx index 43024076..06dc33a4 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -156,7 +156,7 @@ server.get("/*", async (req, res) => { site = try_site.data; initializeSite(site); - if (path != "/setup" && !site.site_view.local_site.site_setup) { + if (path !== "/setup" && !site.site_view.local_site.site_setup) { return res.redirect("/setup"); } diff --git a/src/shared/components/app/navbar.tsx b/src/shared/components/app/navbar.tsx index bdbac9ff..6d310eef 100644 --- a/src/shared/components/app/navbar.tsx +++ b/src/shared/components/app/navbar.tsx @@ -16,8 +16,10 @@ import { isBrowser, myAuth, numToSI, + poll, showAvatars, toast, + updateUnreadCountsInterval, } from "../../utils"; import { Icon } from "../common/icon"; import { PictrsImage } from "../common/pictrs-image"; @@ -64,7 +66,7 @@ export class Navbar extends Component { if (isBrowser()) { // On the first load, check the unreads this.requestNotificationPermission(); - await this.fetchUnreads(); + this.fetchUnreads(); this.requestNotificationPermission(); document.addEventListener("mouseup", this.handleOutsideMenuClick); @@ -406,35 +408,36 @@ export class Navbar extends Component { return amAdmin() || moderatesS; } - async fetchUnreads() { - const auth = myAuth(); - if (auth) { - this.setState({ unreadInboxCountRes: { state: "loading" } }); - this.setState({ - unreadInboxCountRes: await HttpService.client.getUnreadCount({ - auth, - }), - }); - - if (this.moderatesSomething) { - this.setState({ unreadReportCountRes: { state: "loading" } }); - this.setState({ - unreadReportCountRes: await HttpService.client.getReportCount({ - auth, - }), - }); - } - - if (amAdmin()) { - this.setState({ unreadApplicationCountRes: { state: "loading" } }); - this.setState({ - unreadApplicationCountRes: - await HttpService.client.getUnreadRegistrationApplicationCount({ + fetchUnreads() { + poll(async () => { + if (window.document.visibilityState !== "hidden") { + const auth = myAuth(); + if (auth) { + this.setState({ + unreadInboxCountRes: await HttpService.client.getUnreadCount({ auth, }), - }); + }); + + 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 { diff --git a/src/shared/components/comment/comment-node.tsx b/src/shared/components/comment/comment-node.tsx index 8559f38b..0380a726 100644 --- a/src/shared/components/comment/comment-node.tsx +++ b/src/shared/components/comment/comment-node.tsx @@ -270,9 +270,6 @@ export class CommentNode extends Component { 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]; @@ -284,26 +281,17 @@ export class CommentNode extends Component { node.comment_view.counts.child_count > 0; return ( -
+
  • @@ -959,9 +947,9 @@ export class CommentNode extends Component {
    {showMoreChildren && (
    + {this.state.collapsed &&
    } +
  • ); } @@ -1211,6 +1201,7 @@ export class CommentNode extends Component { linkBtn(small = false) { const cv = this.commentView; + const classnames = classNames("btn btn-link btn-animate text-muted", { "btn-sm": small, }); diff --git a/src/shared/components/comment/comment-nodes.tsx b/src/shared/components/comment/comment-nodes.tsx index 3f9b48ef..8c0a236e 100644 --- a/src/shared/components/comment/comment-nodes.tsx +++ b/src/shared/components/comment/comment-nodes.tsx @@ -1,3 +1,4 @@ +import classNames from "classnames"; import { Component } from "inferno"; import { AddAdmin, @@ -25,6 +26,7 @@ import { TransferCommunity, } from "lemmy-js-client"; import { CommentNodeI, CommentViewType } from "../../interfaces"; +import { colorList } from "../../utils"; import { CommentNode } from "./comment-node"; interface CommentNodesProps { @@ -44,6 +46,8 @@ interface CommentNodesProps { allLanguages: Language[]; siteLanguages: number[]; hideImages?: boolean; + isChild?: boolean; + depth?: number; finished: Map; onSaveComment(form: SaveComment): void; onCommentReplyRead(form: MarkCommentReplyAsRead): void; @@ -74,49 +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/home/login.tsx b/src/shared/components/home/login.tsx index 87ef234e..94915542 100644 --- a/src/shared/components/home/login.tsx +++ b/src/shared/components/home/login.tsx @@ -195,7 +195,7 @@ export class Login extends Component { } handleLoginUsernameChange(i: Login, event: any) { - i.state.form.username_or_email = event.target.value; + i.state.form.username_or_email = event.target.value.trim(); i.setState(i.state); } diff --git a/src/shared/components/home/setup.tsx b/src/shared/components/home/setup.tsx index 581c1c56..14350a58 100644 --- a/src/shared/components/home/setup.tsx +++ b/src/shared/components/home/setup.tsx @@ -221,7 +221,7 @@ export class Setup extends Component { } handleRegisterUsernameChange(i: Setup, event: any) { - i.state.form.username = event.target.value; + i.state.form.username = event.target.value.trim(); i.setState(i.state); } diff --git a/src/shared/components/home/signup.tsx b/src/shared/components/home/signup.tsx index 3efeac62..16a3cc6d 100644 --- a/src/shared/components/home/signup.tsx +++ b/src/shared/components/home/signup.tsx @@ -496,7 +496,7 @@ export class Signup extends Component { } handleRegisterUsernameChange(i: Signup, event: any) { - i.state.form.username = event.target.value; + i.state.form.username = event.target.value.trim(); i.setState(i.state); } diff --git a/src/shared/components/post/post-form.tsx b/src/shared/components/post/post-form.tsx index 3ce96bb0..4640922d 100644 --- a/src/shared/components/post/post-form.tsx +++ b/src/shared/components/post/post-form.tsx @@ -25,7 +25,6 @@ import { isImage, myAuth, myAuthRequired, - pictrsDeleteToast, relTags, setupTippy, toast, @@ -73,6 +72,7 @@ interface PostFormState { suggestedPostsRes: RequestState; metadataRes: RequestState; imageLoading: boolean; + imageDeleteUrl: string; communitySearchLoading: boolean; communitySearchOptions: Choice[]; previewMode: boolean; @@ -86,6 +86,7 @@ export class PostForm extends Component { form: {}, loading: false, imageLoading: false, + imageDeleteUrl: "", communitySearchLoading: false, previewMode: false, communitySearchOptions: [], @@ -269,6 +270,17 @@ export class PostForm extends Component { {url && isImage(url) && ( )} + {this.state.imageDeleteUrl && ( + + )} {this.props.crossPosts && this.props.crossPosts.length > 0 && ( <>
    @@ -553,7 +565,15 @@ export class PostForm extends Component { } handlePostUrlChange(i: PostForm, event: any) { - i.setState(s => ((s.form.url = event.target.value), s)); + const url = event.target.value; + + i.setState({ + form: { + url, + }, + imageDeleteUrl: "", + }); + i.fetchPageTitle(); } @@ -644,18 +664,35 @@ export class PostForm extends Component { if (res.state === "success") { if (res.data.msg === "ok") { i.state.form.url = res.data.url; - pictrsDeleteToast(file.name, res.data.delete_url as string); - i.setState({ imageLoading: false }); + i.setState({ + imageLoading: false, + imageDeleteUrl: res.data.delete_url as string, + }); } else { toast(JSON.stringify(res), "danger"); } } else if (res.state === "failed") { console.error(res.msg); toast(res.msg, "danger"); + i.setState({ imageLoading: false }); } }); } + handleImageDelete(i: PostForm) { + const { imageDeleteUrl } = i.state; + + fetch(imageDeleteUrl); + + i.setState({ + imageDeleteUrl: "", + imageLoading: false, + form: { + url: "", + }, + }); + } + handleCommunitySearch = debounce(async (text: string) => { const { selectedCommunityChoice } = this.props; this.setState({ communitySearchLoading: true }); diff --git a/src/shared/components/post/post-listing.tsx b/src/shared/components/post/post-listing.tsx index f1f06c58..d5fc785c 100644 --- a/src/shared/components/post/post-listing.tsx +++ b/src/shared/components/post/post-listing.tsx @@ -835,6 +835,8 @@ export class PostListing extends Component { search: "", }} title={i18n.t("cross_post")} + data-tippy-content={i18n.t("cross_post")} + aria-label={i18n.t("cross_post")} > diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 46e8601b..4a3b298a 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -75,6 +75,7 @@ export const commentTreeMaxDepth = 8; export const markdownFieldCharacterLimit = 50000; export const maxUploadImages = 20; export const concurrentImageUpload = 4; +export const updateUnreadCountsInterval = 30000; export const relTags = "noopener nofollow"; @@ -1127,7 +1128,7 @@ export const colorList: string[] = [ ]; function hsl(num: number) { - return `hsla(${num}, 35%, 50%, 1)`; + return `hsla(${num}, 35%, 50%, 0.5)`; } export function hostname(url: string): string { @@ -1490,3 +1491,18 @@ export function newVote(voteType: VoteType, myVote?: number): number { return myVote == -1 ? 0 : -1; } } + +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); +}