diff --git a/package.json b/package.json index e5c1fa70..ef07b29d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lemmy-ui", - "version": "0.18.1-rc.2", + "version": "0.18.1-rc.3", "description": "An isomorphic UI for lemmy", "repository": "https://github.com/LemmyNet/lemmy-ui", "license": "AGPL-3.0", @@ -52,6 +52,7 @@ "cross-fetch": "^3.1.5", "css-loader": "^6.7.3", "date-fns": "^2.30.0", + "date-fns-tz": "^2.0.0", "emoji-mart": "^5.4.0", "emoji-short-name": "^2.0.0", "express": "~4.18.2", diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 63c1b471..e5c163b1 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -198,9 +198,9 @@ blockquote { .thumbnail { object-fit: cover; - aspect-ratio: 4/3; - width: 100%; - max-height: 6rem; + aspect-ratio: 1/1; + width: 5rem; + height: 5rem; } .thumbnail svg { diff --git a/src/server/handlers/security-handler.ts b/src/server/handlers/security-handler.ts new file mode 100644 index 00000000..0aed0cdc --- /dev/null +++ b/src/server/handlers/security-handler.ts @@ -0,0 +1,17 @@ +import type { Response } from "express"; + +export default async ({ res }: { res: Response }) => { + res.setHeader("content-type", "text/plain; charset=utf-8"); + + res.send( + `Contact: mailto:security@lemmy.ml + Contact: mailto:admin@` + + process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST + + ` + Contact: mailto:security@` + + process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST + + ` + Expires: 2024-01-01T04:59:00.000Z + ` + ); +}; diff --git a/src/server/index.tsx b/src/server/index.tsx index e9c907f7..76bff380 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -6,6 +6,7 @@ import { COMMIT_HASH } from "../shared/version"; import CatchAllHandler from "./handlers/catch-all-handler"; import ManifestHandler from "./handlers/manifest-handler"; import RobotsHandler from "./handlers/robots-handler"; +import SecurityHandler from "./handlers/security-handler"; import ServiceWorkerHandler from "./handlers/service-worker-handler"; import ThemeHandler from "./handlers/theme-handler"; import ThemesListHandler from "./handlers/themes-list-handler"; @@ -26,6 +27,7 @@ if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) { server.use(setDefaultCsp); } +server.get("/.well-known/security.txt", SecurityHandler); server.get("/robots.txt", RobotsHandler); server.get("/service-worker.js", ServiceWorkerHandler); server.get("/manifest.webmanifest", ManifestHandler); diff --git a/src/shared/components/common/image-upload-form.tsx b/src/shared/components/common/image-upload-form.tsx index e8005ccc..854e7105 100644 --- a/src/shared/components/common/image-upload-form.tsx +++ b/src/shared/components/common/image-upload-form.tsx @@ -84,6 +84,8 @@ export class ImageUploadForm extends Component< if (res.state === "success") { if (res.data.msg === "ok") { i.props.onUpload(res.data.url as string); + } else if (res.data.msg === "too_large") { + toast(I18NextService.i18n.t("upload_too_large"), "danger"); } else { toast(JSON.stringify(res), "danger"); } diff --git a/src/shared/components/common/markdown-textarea.tsx b/src/shared/components/common/markdown-textarea.tsx index f7c4760a..5623ace5 100644 --- a/src/shared/components/common/markdown-textarea.tsx +++ b/src/shared/components/common/markdown-textarea.tsx @@ -443,6 +443,10 @@ export class MarkdownTextArea extends Component< const textarea: any = document.getElementById(i.id); autosize.update(textarea); pictrsDeleteToast(image.name, res.data.delete_url as string); + } else if (res.data.msg === "too_large") { + toast(I18NextService.i18n.t("upload_too_large"), "danger"); + i.setState({ imageUploadStatus: undefined }); + throw JSON.stringify(res.data); } else { throw JSON.stringify(res.data); } diff --git a/src/shared/components/common/moment-time.tsx b/src/shared/components/common/moment-time.tsx index ec97eb49..24bd3c79 100644 --- a/src/shared/components/common/moment-time.tsx +++ b/src/shared/components/common/moment-time.tsx @@ -1,5 +1,5 @@ import { capitalizeFirstLetter, formatPastDate } from "@utils/helpers"; -import format from "date-fns/format"; +import { formatInTimeZone } from "date-fns-tz"; import parseISO from "date-fns/parseISO"; import { Component } from "inferno"; import { I18NextService } from "../../services"; @@ -13,7 +13,9 @@ interface MomentTimeProps { } function formatDate(input: string) { - return format(parseISO(input), "PPPPpppp"); + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + const parsed = parseISO(input + "Z"); + return formatInTimeZone(parsed, tz, "PPPPpppp"); } export class MomentTime extends Component { diff --git a/src/shared/components/home/emojis-form.tsx b/src/shared/components/home/emojis-form.tsx index 149ff032..4108e7a4 100644 --- a/src/shared/components/home/emojis-form.tsx +++ b/src/shared/components/home/emojis-form.tsx @@ -508,6 +508,8 @@ export class EmojiForm extends Component { { form: form, index: index, overrideValue: res.data.url as string }, event ); + } else if (res.data.msg === "too_large") { + toast(I18NextService.i18n.t("upload_too_large"), "danger"); } else { toast(JSON.stringify(res), "danger"); } diff --git a/src/shared/components/post/post-form.tsx b/src/shared/components/post/post-form.tsx index 25a0fcc0..c29d3b1f 100644 --- a/src/shared/components/post/post-form.tsx +++ b/src/shared/components/post/post-form.tsx @@ -187,6 +187,8 @@ function handleImageUpload(i: PostForm, event: any) { imageLoading: false, imageDeleteUrl: res.data.delete_url as string, }); + } else if (res.data.msg === "too_large") { + toast(I18NextService.i18n.t("upload_too_large"), "danger"); } else { toast(JSON.stringify(res), "danger"); } diff --git a/src/shared/components/post/post-listing.tsx b/src/shared/components/post/post-listing.tsx index eb3d0f64..ae6e2f3b 100644 --- a/src/shared/components/post/post-listing.tsx +++ b/src/shared/components/post/post-listing.tsx @@ -333,7 +333,7 @@ export class PostListing extends Component { return ( @@ -981,9 +1024,8 @@ export class PostListing extends Component { get modUnbanFromCommunityButton() { return ( @@ -993,20 +1035,15 @@ export class PostListing extends Component { get addModToCommunityButton() { return ( ); @@ -1015,11 +1052,10 @@ export class PostListing extends Component { get modBanButton() { return ( ); } @@ -1027,14 +1063,13 @@ export class PostListing extends Component { get modUnbanButton() { return ( ); @@ -1043,11 +1078,10 @@ export class PostListing extends Component { get purgePersonButton() { return ( ); } @@ -1055,11 +1089,10 @@ export class PostListing extends Component { get purgePostButton() { return ( ); } @@ -1067,20 +1100,31 @@ export class PostListing extends Component { get toggleAdminButton() { return ( ); } + get transferCommunityButton() { + return ( + + ); + } + get modRemoveButton() { const removed = this.postView.post.removed; return ( @@ -1095,102 +1139,17 @@ export class PostListing extends Component { {this.state.removeLoading ? ( ) : !removed ? ( - I18NextService.i18n.t("remove") + capitalizeFirstLetter(I18NextService.i18n.t("remove_post")) ) : ( - I18NextService.i18n.t("restore") + <> + {capitalizeFirstLetter(I18NextService.i18n.t("restore"))}{" "} + {I18NextService.i18n.t("post")} + )} ); } - /** - * Mod/Admin actions to be taken against the author. - */ - userActionsLine() { - // TODO: make nicer - const post_view = this.postView; - return ( - this.state.showAdvanced && ( -
- {this.canMod_ && ( - <> - {!this.creatorIsMod_ && - (!post_view.creator_banned_from_community - ? this.modBanFromCommunityButton - : this.modUnbanFromCommunityButton)} - {!post_view.creator_banned_from_community && - this.addModToCommunityButton} - - )} - - {/* Community creators and admins can transfer community to another mod */} - {(amCommunityCreator(post_view.creator.id, this.props.moderators) || - this.canAdmin_) && - this.creatorIsMod_ && - (!this.state.showConfirmTransferCommunity ? ( - - ) : ( - <> - - - - - ))} - {/* Admins can ban from all, and appoint other admins */} - {this.canAdmin_ && ( - <> - {!this.creatorIsAdmin_ && ( - <> - {!isBanned(post_view.creator) - ? this.modBanButton - : this.modUnbanButton} - {this.purgePersonButton} - {this.purgePostButton} - - )} - {!isBanned(post_view.creator) && - post_view.creator.local && - this.toggleAdminButton} - - )} -
- ) - ); - } - removeAndBanDialogs() { const post = this.postView; const purgeTypeText = @@ -1218,11 +1177,7 @@ export class PostListing extends Component { value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} /> - )} + {this.state.showConfirmTransferCommunity && ( + <> + + + + + )} {this.state.showBanDialog && (
@@ -1284,11 +1266,7 @@ export class PostListing extends Component { {/* */} {/*
*/}
- )} @@ -1409,7 +1379,6 @@ export class PostListing extends Component { {this.mobileThumbnail()} {this.commentsLine(true)} - {this.userActionsLine()} {this.duplicatesLine()} {this.removeAndBanDialogs()}
@@ -1433,15 +1402,14 @@ export class PostListing extends Component { )}
-
+
{this.thumbnail()}
-
+
{this.postTitleLine()} {this.createdLine()} {this.commentsLine()} {this.duplicatesLine()} - {this.userActionsLine()} {this.removeAndBanDialogs()}
diff --git a/src/shared/components/search.tsx b/src/shared/components/search.tsx index b58580e5..5360066c 100644 --- a/src/shared/components/search.tsx +++ b/src/shared/components/search.tsx @@ -332,9 +332,7 @@ export class Search extends Component { } async componentDidMount() { - if ( - !(this.state.isIsomorphic || this.props.history.location.state?.searched) - ) { + if (!this.state.isIsomorphic) { const promises = [this.fetchCommunities()]; if (this.state.searchText) { promises.push(this.search()); @@ -432,7 +430,15 @@ export class Search extends Component { q: query, auth, }; - resolveObjectResponse = await client.resolveObject(resolveObjectForm); + resolveObjectResponse = await HttpService.silent_client.resolveObject( + resolveObjectForm + ); + + // If we return this object with a state of failed, the catch-all-handler will redirect + // to an error page, so we ignore it by covering up the error with the empty state. + if (resolveObjectResponse.state === "failed") { + resolveObjectResponse = { state: "empty" }; + } } } } @@ -950,7 +956,7 @@ export class Search extends Component { if (auth) { this.setState({ resolveObjectRes: { state: "loading" } }); this.setState({ - resolveObjectRes: await HttpService.client.resolveObject({ + resolveObjectRes: await HttpService.silent_client.resolveObject({ q, auth, }), @@ -1097,10 +1103,6 @@ export class Search extends Component { sort: sort ?? urlSort, }; - this.props.history.push(`/search${getQueryString(queryParams)}`, { - searched: true, - }); - - await this.search(); + this.props.history.push(`/search${getQueryString(queryParams)}`); } } diff --git a/src/shared/services/HttpService.ts b/src/shared/services/HttpService.ts index 361ffbd3..11ec292a 100644 --- a/src/shared/services/HttpService.ts +++ b/src/shared/services/HttpService.ts @@ -1,9 +1,9 @@ import { getHttpBase } from "@utils/env"; import { LemmyHttp } from "lemmy-js-client"; -import { toast } from "../../shared/toast"; +import { toast } from "../toast"; import { I18NextService } from "./I18NextService"; -type EmptyRequestState = { +export type EmptyRequestState = { state: "empty"; }; @@ -45,7 +45,7 @@ export type WrappedLemmyHttp = { class WrappedLemmyHttpClient { #client: LemmyHttp; - constructor(client: LemmyHttp) { + constructor(client: LemmyHttp, silent = false) { this.#client = client; for (const key of Object.getOwnPropertyNames( @@ -61,8 +61,10 @@ class WrappedLemmyHttpClient { state: !(res === undefined || res === null) ? "success" : "empty", }; } catch (error) { - console.error(`API error: ${error}`); - toast(I18NextService.i18n.t(error), "danger"); + if (!silent) { + console.error(`API error: ${error}`); + toast(I18NextService.i18n.t(error), "danger"); + } return { state: "failed", msg: error, @@ -74,16 +76,23 @@ class WrappedLemmyHttpClient { } } -export function wrapClient(client: LemmyHttp) { - return new WrappedLemmyHttpClient(client) as unknown as WrappedLemmyHttp; // unfortunately, this verbose cast is necessary +export function wrapClient(client: LemmyHttp, silent = false) { + // unfortunately, this verbose cast is necessary + return new WrappedLemmyHttpClient( + client, + silent + ) as unknown as WrappedLemmyHttp; } export class HttpService { static #_instance: HttpService; + #silent_client: WrappedLemmyHttp; #client: WrappedLemmyHttp; private constructor() { - this.#client = wrapClient(new LemmyHttp(getHttpBase())); + const lemmyHttp = new LemmyHttp(getHttpBase()); + this.#client = wrapClient(lemmyHttp); + this.#silent_client = wrapClient(lemmyHttp, true); } static get #Instance() { @@ -93,4 +102,8 @@ export class HttpService { public static get client() { return this.#Instance.#client; } + + public static get silent_client() { + return this.#Instance.#silent_client; + } } diff --git a/src/shared/utils/helpers/format-past-date.ts b/src/shared/utils/helpers/format-past-date.ts index 78bc2a2d..5bef4e83 100644 --- a/src/shared/utils/helpers/format-past-date.ts +++ b/src/shared/utils/helpers/format-past-date.ts @@ -2,11 +2,8 @@ import formatDistanceStrict from "date-fns/formatDistanceStrict"; import parseISO from "date-fns/parseISO"; export default function (dateString?: string) { - return formatDistanceStrict( - parseISO(dateString ?? Date.now().toString()), - new Date(), - { - addSuffix: true, - } - ); + const parsed = parseISO((dateString ?? Date.now().toString()) + "Z"); + return formatDistanceStrict(parsed, new Date(), { + addSuffix: true, + }); } diff --git a/webpack.config.js b/webpack.config.js index 0b03f6d9..499d53e3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -101,7 +101,7 @@ const createClientConfig = (_env, mode) => { entry: "./src/client/index.tsx", output: { filename: "js/client.js", - publicPath: `/static/${commitHash}`, + publicPath: `/static/${commitHash}` }, plugins: [ ...base.plugins, @@ -112,7 +112,7 @@ const createClientConfig = (_env, mode) => { "/": `/static/${commitHash}/`, }, cacheId: "lemmy", - include: [/(assets|styles)\/.+\..+|client\.js$/g], + include: [/(assets|styles|js)\/.+\..+$/g], inlineWorkboxRuntime: true, runtimeCaching: [ { diff --git a/yarn.lock b/yarn.lock index 46fcba11..57cde87a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3237,6 +3237,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-fns-tz@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-2.0.0.tgz#1b14c386cb8bc16fc56fe333d4fc34ae1d1099d5" + integrity sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ== + date-fns@^2.30.0: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"