diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml index 64579090..ae2d4e51 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: false - 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..3c75050a 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: false - 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/Dockerfile b/Dockerfile index 3d6d6212..2b36581d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:alpine as builder +FROM node:20.2-alpine as builder RUN apk update && apk add curl yarn python3 build-base gcc wget git --no-cache RUN curl -sf https://gobinaries.com/tj/node-prune | sh diff --git a/dev.dockerfile b/dev.dockerfile index 0e925c0a..3bfc10da 100644 --- a/dev.dockerfile +++ b/dev.dockerfile @@ -1,4 +1,4 @@ -FROM node:alpine as builder +FROM node:20.2-alpine as builder RUN apk update && apk add curl yarn python3 build-base gcc wget git --no-cache WORKDIR /usr/src/app diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 1c45341d..82f8433e 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -75,28 +75,9 @@ font-size: 1.2rem; } -.md-div table { - border-collapse: collapse; - width: 100%; - margin-bottom: 1rem; - border: 1px solid var(--dark); -} - -.md-div table th, -.md-div table td { - padding: 0.3rem; - vertical-align: top; - border-top: 1px solid var(--dark); - border: 1px solid var(--dark); -} - -.md-div table thead th { - vertical-align: bottom; - border-bottom: 2px solid var(--dark); -} - -.md-div table tbody + tbody { - border-top: 2px solid var(--dark); +.md-div pre { + white-space: pre; + overflow-x: auto; } .vote-bar { @@ -213,6 +194,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..3a12ad7e 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"); } @@ -421,7 +421,7 @@ async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) { - + { 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/common/html-tags.tsx b/src/shared/components/common/html-tags.tsx index 0e6cb2d0..f32b0fc0 100644 --- a/src/shared/components/common/html-tags.tsx +++ b/src/shared/components/common/html-tags.tsx @@ -2,7 +2,8 @@ import { htmlToText } from "html-to-text"; import { Component } from "inferno"; import { Helmet } from "inferno-helmet"; import { httpExternalPath } from "../../env"; -import { getLanguages, md } from "../../utils"; +import { i18n } from "../../i18next"; +import { md } from "../../utils"; interface HtmlTagsProps { title: string; @@ -17,11 +18,10 @@ export class HtmlTags extends Component { const url = httpExternalPath(this.props.path); const desc = this.props.description; const image = this.props.image; - const lang = getLanguages()[0]; return ( - + {["title", "og:title", "twitter:title"].map(t => ( diff --git a/src/shared/components/common/markdown-textarea.tsx b/src/shared/components/common/markdown-textarea.tsx index 9318d3bb..a4459ac0 100644 --- a/src/shared/components/common/markdown-textarea.tsx +++ b/src/shared/components/common/markdown-textarea.tsx @@ -184,53 +184,6 @@ export class MarkdownTextArea extends Component<
    - {this.props.buttonTitle && ( - - )} - {this.props.replyType && ( - - )} - {this.state.content && ( - - )} - {/* A flex expander */} -
    - - {this.props.showLanguage && ( - - )} {this.getFormatButton("bold", this.handleInsertBold)} {this.getFormatButton("italic", this.handleInsertItalic)} {this.getFormatButton("link", this.handleInsertLink)} @@ -283,6 +236,57 @@ export class MarkdownTextArea extends Component<
    + +
    + {this.props.showLanguage && ( + + )} + + {/* A flex expander */} +
    + + {this.props.buttonTitle && ( + + )} + {this.props.replyType && ( + + )} + {this.state.content && ( + + )} +
    ); diff --git a/src/shared/components/common/moment-time.tsx b/src/shared/components/common/moment-time.tsx index 10714f5b..30c1682c 100644 --- a/src/shared/components/common/moment-time.tsx +++ b/src/shared/components/common/moment-time.tsx @@ -1,7 +1,7 @@ import { Component } from "inferno"; import moment from "moment"; import { i18n } from "../../i18next"; -import { capitalizeFirstLetter, getLanguages } from "../../utils"; +import { capitalizeFirstLetter } from "../../utils"; import { Icon } from "./icon"; interface MomentTimeProps { @@ -15,9 +15,7 @@ export class MomentTime extends Component { constructor(props: any, context: any) { super(props, context); - const lang = getLanguages(); - - moment.locale(lang); + moment.locale([...i18n.languages]); } createdAndModifiedTimes() { diff --git a/src/shared/components/community/community-form.tsx b/src/shared/components/community/community-form.tsx index f317c983..4eed4645 100644 --- a/src/shared/components/community/community-form.tsx +++ b/src/shared/components/community/community-form.tsx @@ -21,6 +21,7 @@ interface CommunityFormProps { onCancel?(): any; onUpsertCommunity(form: CreateCommunity | EditCommunity): void; enableNsfw?: boolean; + loading?: boolean; } interface CommunityFormState { @@ -34,7 +35,6 @@ interface CommunityFormState { posting_restricted_to_mods?: boolean; discussion_languages?: number[]; }; - loading: boolean; submitted: boolean; } @@ -46,7 +46,6 @@ export class CommunityForm extends Component< state: CommunityFormState = { form: {}, - loading: false, submitted: false, }; @@ -80,7 +79,6 @@ export class CommunityForm extends Component< posting_restricted_to_mods: cv.community.posting_restricted_to_mods, discussion_languages: this.props.communityLanguages, }, - loading: false, }; } } @@ -90,7 +88,7 @@ export class CommunityForm extends Component<
    - {this.state.loading ? ( + {this.props.loading ? ( ) : this.props.community_view ? ( capitalizeFirstLetter(i18n.t("save")) @@ -270,7 +268,7 @@ export class CommunityForm extends Component< handleCreateCommunitySubmit(i: CommunityForm, event: any) { event.preventDefault(); - i.setState({ loading: true, submitted: true }); + i.setState({ submitted: true }); const cForm = i.state.form; const auth = myAuthRequired(); diff --git a/src/shared/components/community/create-community.tsx b/src/shared/components/community/create-community.tsx index f75c4fbb..ff31b839 100644 --- a/src/shared/components/community/create-community.tsx +++ b/src/shared/components/community/create-community.tsx @@ -11,12 +11,14 @@ import { CommunityForm } from "./community-form"; interface CreateCommunityState { siteRes: GetSiteResponse; + loading: boolean; } export class CreateCommunity extends Component { private isoData = setIsoData(this.context); state: CreateCommunityState = { siteRes: this.isoData.site_res, + loading: false, }; constructor(props: any, context: any) { super(props, context); @@ -45,6 +47,7 @@ export class CreateCommunity extends Component { allLanguages={this.state.siteRes.all_languages} siteLanguages={this.state.siteRes.discussion_languages} communityLanguages={this.state.siteRes.discussion_languages} + loading={this.state.loading} /> @@ -53,10 +56,15 @@ export class CreateCommunity extends Component { } async handleCommunityCreate(form: CreateCommunityI) { + this.setState({ loading: true }); + const res = await HttpService.client.createCommunity(form); + if (res.state === "success") { const name = res.data.community_view.community.name; this.props.history.replace(`/c/${name}`); + } else { + this.setState({ loading: false }); } } } diff --git a/src/shared/components/community/sidebar.tsx b/src/shared/components/community/sidebar.tsx index a5c620f3..63378a18 100644 --- a/src/shared/components/community/sidebar.tsx +++ b/src/shared/components/community/sidebar.tsx @@ -1,4 +1,5 @@ import { Component, InfernoNode, linkEvent } from "inferno"; +import { T } from "inferno-i18next-dess"; import { Link } from "inferno-router"; import { AddModToCommunity, @@ -144,10 +145,15 @@ export class Sidebar extends Component { {myUSerInfo && this.blockCommunity()} {!myUSerInfo && (
    - {i18n.t("community_not_logged_in_alert", { - community: name, - instance: hostname(actor_id), - })} + + ### +
    )} diff --git a/src/shared/components/home/admin-settings.tsx b/src/shared/components/home/admin-settings.tsx index 9b7256d0..302e96bd 100644 --- a/src/shared/components/home/admin-settings.tsx +++ b/src/shared/components/home/admin-settings.tsx @@ -39,6 +39,8 @@ interface AdminSettingsState { instancesRes: RequestState; bannedRes: RequestState; leaveAdminTeamRes: RequestState; + emojiLoading: boolean; + loading: boolean; themeList: string[]; isIsomorphic: boolean; } @@ -52,6 +54,8 @@ export class AdminSettings extends Component { bannedRes: { state: "empty" }, instancesRes: { state: "empty" }, leaveAdminTeamRes: { state: "empty" }, + emojiLoading: false, + loading: false, themeList: [], isIsomorphic: false, }; @@ -81,6 +85,7 @@ export class AdminSettings extends Component { bannedRes: { state: "loading" }, instancesRes: { state: "loading" }, themeList: [], + loading: true, }); const auth = myAuthRequired(); @@ -95,6 +100,7 @@ export class AdminSettings extends Component { bannedRes, instancesRes, themeList, + loading: false, }); } @@ -156,6 +162,7 @@ export class AdminSettings extends Component { onSaveSite={this.handleEditSite} siteRes={this.state.siteRes} themeList={this.state.themeList} + loading={this.state.loading} />
    @@ -174,6 +181,7 @@ export class AdminSettings extends Component { this.state.siteRes.site_view.local_site_rate_limit } onSaveSite={this.handleEditSite} + loading={this.state.loading} /> ), }, @@ -185,6 +193,7 @@ export class AdminSettings extends Component {
    ), @@ -198,6 +207,7 @@ export class AdminSettings extends Component { onCreate={this.handleCreateEmoji} onDelete={this.handleDeleteEmoji} onEdit={this.handleEditEmoji} + loading={this.state.emojiLoading} /> ), @@ -266,6 +276,8 @@ export class AdminSettings extends Component { } async handleEditSite(form: EditSite) { + this.setState({ loading: true }); + const editRes = await HttpService.client.editSite(form); if (editRes.state === "success") { @@ -278,6 +290,8 @@ export class AdminSettings extends Component { toast(i18n.t("site_saved")); } + this.setState({ loading: false }); + return editRes; } @@ -300,23 +314,35 @@ export class AdminSettings extends Component { } async handleEditEmoji(form: EditCustomEmoji) { + this.setState({ emojiLoading: true }); + const res = await HttpService.client.editCustomEmoji(form); if (res.state === "success") { updateEmojiDataModel(res.data.custom_emoji); } + + this.setState({ emojiLoading: false }); } async handleDeleteEmoji(form: DeleteCustomEmoji) { + this.setState({ emojiLoading: true }); + const res = await HttpService.client.deleteCustomEmoji(form); if (res.state === "success") { removeFromEmojiDataModel(res.data.id); } + + this.setState({ emojiLoading: false }); } async handleCreateEmoji(form: CreateCustomEmoji) { + this.setState({ emojiLoading: true }); + const res = await HttpService.client.createCustomEmoji(form); if (res.state === "success") { updateEmojiDataModel(res.data.custom_emoji); } + + this.setState({ emojiLoading: false }); } } diff --git a/src/shared/components/home/emojis-form.tsx b/src/shared/components/home/emojis-form.tsx index 171b7c99..f77f5125 100644 --- a/src/shared/components/home/emojis-form.tsx +++ b/src/shared/components/home/emojis-form.tsx @@ -23,12 +23,12 @@ interface EmojiFormProps { onEdit(form: EditCustomEmoji): void; onCreate(form: CreateCustomEmoji): void; onDelete(form: DeleteCustomEmoji): void; + loading: boolean; } interface EmojiFormState { siteRes: GetSiteResponse; customEmojis: CustomEmojiViewForm[]; - loading: boolean; page: number; } @@ -47,7 +47,6 @@ export class EmojiForm extends Component { private isoData = setIsoData(this.context); private itemsPerPage = 15; private emptyState: EmojiFormState = { - loading: false, siteRes: this.isoData.site_res, customEmojis: this.isoData.site_res.custom_emojis.map((x, index) => ({ id: x.custom_emoji.id, @@ -223,7 +222,7 @@ export class EmojiForm extends Component { data-tippy-content={i18n.t("save")} aria-label={i18n.t("save")} disabled={ - this.state.loading || + this.props.loading || !this.canEdit(cv) || !cv.changed } @@ -243,7 +242,7 @@ export class EmojiForm extends Component { )} data-tippy-content={i18n.t("delete")} aria-label={i18n.t("delete")} - disabled={this.state.loading} + disabled={this.props.loading} title={i18n.t("delete")} > { } async componentDidMount() { - if (!this.state.isIsomorphic) { + if (!this.state.isIsomorphic || !this.isoData.routeData.length) { await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]); } + setupTippy(); } @@ -456,7 +457,7 @@ export class Home extends Component { } trendingCommunities(isMobile = false) { - switch (this.state.trendingCommunitiesRes.state) { + switch (this.state.trendingCommunitiesRes?.state) { case "loading": return (
    @@ -573,7 +574,7 @@ export class Home extends Component { const siteRes = this.state.siteRes; if (dataType === DataType.Post) { - switch (this.state.postsRes.state) { + switch (this.state.postsRes?.state) { case "loading": return (
    diff --git a/src/shared/components/home/login.tsx b/src/shared/components/home/login.tsx index 87ef234e..381c13bb 100644 --- a/src/shared/components/home/login.tsx +++ b/src/shared/components/home/login.tsx @@ -186,7 +186,9 @@ export class Login extends Component { UserService.Instance.myUserInfo = site.data.my_user; } - i.props.history.replace("/"); + i.props.history.action === "PUSH" + ? i.props.history.back() + : i.props.history.replace("/"); break; } @@ -195,7 +197,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/rate-limit-form.tsx b/src/shared/components/home/rate-limit-form.tsx index 74ed18e3..0ce01260 100644 --- a/src/shared/components/home/rate-limit-form.tsx +++ b/src/shared/components/home/rate-limit-form.tsx @@ -24,6 +24,7 @@ interface RateLimitsProps { interface RateLimitFormProps { rateLimits: LocalSiteRateLimit; onSaveSite(form: EditSite): void; + loading: boolean; } interface RateLimitFormState { @@ -41,7 +42,6 @@ interface RateLimitFormState { register?: number; register_per_second?: number; }; - loading: boolean; } function RateLimits({ @@ -117,7 +117,6 @@ function submitRateLimitForm(i: RateLimitsForm, event: any) { } ); - i.setState({ loading: true }); i.props.onSaveSite(form); } @@ -126,7 +125,6 @@ export default class RateLimitsForm extends Component< RateLimitFormState > { state: RateLimitFormState = { - loading: false, form: this.props.rateLimits, }; constructor(props: RateLimitFormProps, context: any) { @@ -164,9 +162,9 @@ export default class RateLimitsForm extends Component< + )} {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..60e188a3 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")} > @@ -1380,9 +1382,12 @@ export class PostListing extends Component { } showMobilePreview() { - const body = this.postView.post.body; + const { body, id } = this.postView.post; + return !this.showBody && body ? ( -
    {body}
    + +
    {body}
    + ) : ( <> ); diff --git a/src/shared/components/post/post-listings.tsx b/src/shared/components/post/post-listings.tsx index 098a015d..00370f03 100644 --- a/src/shared/components/post/post-listings.tsx +++ b/src/shared/components/post/post-listings.tsx @@ -68,7 +68,7 @@ export class PostListings extends Component { return (
    {this.posts.length > 0 ? ( - this.posts.map(post_view => ( + this.posts.map((post_view, idx) => ( <> { onAddAdmin={this.props.onAddAdmin} onTransferCommunity={this.props.onTransferCommunity} /> -
    + {idx + 1 !== this.posts.length && ( +
    + )} )) ) : ( diff --git a/src/shared/i18next.ts b/src/shared/i18next.ts index eaedbbf8..47ca6501 100644 --- a/src/shared/i18next.ts +++ b/src/shared/i18next.ts @@ -1,4 +1,5 @@ import i18next, { i18nTyped, Resource } from "i18next"; +import { UserService } from "./services"; import { ar } from "./translations/ar"; import { bg } from "./translations/bg"; import { ca } from "./translations/ca"; @@ -30,7 +31,7 @@ import { sv } from "./translations/sv"; import { vi } from "./translations/vi"; import { zh } from "./translations/zh"; import { zh_Hant } from "./translations/zh_Hant"; -import { getLanguages } from "./utils"; +import { isBrowser } from "./utils"; export const languages = [ { resource: ar, code: "ar", name: "العربية" }, @@ -73,12 +74,31 @@ function format(value: any, format: any): any { return format === "uppercase" ? value.toUpperCase() : value; } -i18next.init({ +class LanguageDetector { + static readonly type = "languageDetector"; + + detect() { + const langs: string[] = []; + + const myLang = + UserService.Instance.myUserInfo?.local_user_view.local_user + .interface_language ?? "browser"; + + if (myLang !== "browser") langs.push(myLang); + + if (isBrowser()) langs.push(...navigator.languages); + + return langs; + } +} + +i18next.use(LanguageDetector).init({ debug: false, compatibilityJSON: "v3", + supportedLngs: languages.map(l => l.code), + nonExplicitSupportedLngs: true, // load: 'languageOnly', // initImmediate: false, - lng: getLanguages()[0], fallbackLng: "en", resources, interpolation: { format }, diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 46e8601b..df7673a4 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -42,7 +42,7 @@ import moment from "moment"; import tippy from "tippy.js"; import Toastify from "toastify-js"; import { getHttpBase } from "./env"; -import { i18n, languages } from "./i18next"; +import { i18n } from "./i18next"; import { CommentNodeI, DataType, IsoData, VoteType } from "./interfaces"; import { HttpService, UserService } from "./services"; @@ -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"; @@ -315,6 +316,7 @@ export function amTopMod( 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]+/; export function isImage(url: string) { return imageRegex.test(url); @@ -328,6 +330,10 @@ export function validURL(str: string) { return !!new URL(str); } +export function validInstanceTLD(str: string) { + return tldRegex.test(str); +} + export function communityRSSUrl(actorId: string, sort: string): string { const url = new URL(actorId); return `${url.origin}/feeds${url.pathname}.xml?sort=${sort}`; @@ -398,31 +404,6 @@ export function debounce( } as (...e: T) => R; } -export function getLanguages( - override?: string, - myUserInfo = UserService.Instance.myUserInfo -): string[] { - const myLang = myUserInfo?.local_user_view.local_user.interface_language; - const lang = override || myLang || "browser"; - - if (lang == "browser" && isBrowser()) { - return getBrowserLanguages(); - } else { - return [lang]; - } -} - -function getBrowserLanguages(): string[] { - // Intersect lemmy's langs, with the browser langs - const langs = languages ? languages.map(l => l.code) : ["en"]; - - // NOTE, mobile browsers seem to be missing this list, so append en - const allowedLangs = navigator.languages - .concat("en") - .filter(v => langs.includes(v)); - return allowedLangs; -} - export async function fetchThemeList(): Promise { return fetch("/css/themelist").then(res => res.json()); } @@ -738,7 +719,7 @@ function setupMarkdown() { defs: emojiDefs, }) .disable("image"); - var defaultRenderer = md.renderer.rules.image; + const defaultRenderer = md.renderer.rules.image; md.renderer.rules.image = function ( tokens: Token[], idx: number, @@ -757,6 +738,9 @@ function setupMarkdown() { const alt_text = item.content; return `${alt_text}`; }; + md.renderer.rules.table_open = function () { + return ''; + }; } export function getEmojiMart( @@ -1127,7 +1111,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 { @@ -1272,7 +1256,7 @@ export function personSelectName({ export function initializeSite(site?: GetSiteResponse) { UserService.Instance.myUserInfo = site?.my_user; - i18n.changeLanguage(getLanguages()[0]); + i18n.changeLanguage(); if (site) { setupEmojiDataModel(site.custom_emojis ?? []); } @@ -1490,3 +1474,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); +}