-* @dessalines @SleeplessOne1917 @alectrocute
+* @dessalines @SleeplessOne1917 @alectrocute @jsit
import sharp from "sharp";
import { favIconPngUrl, favIconUrl } from "../../shared/config";
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
+import { buildThemeList } from "./build-themes-list";
import { fetchIconPng } from "./fetch-icon-png";
const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || "";
) {
const site = isoData.site_res;
+ const fallbackTheme = `<link rel="stylesheet" type="text/css" href="/css/themes/${
+ (await buildThemeList())[0]
+ }.css" />`;
+
if (!appleTouchIcon) {
appleTouchIcon = site?.site_view.site.icon
? `data:image/png;base64,${sharp(
<!-- Required meta tags -->
<meta name="Description" content="Lemmy">
<meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link
id="favicon"
rel="shortcut icon"
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
<!-- Current theme and more -->
- ${helmet.link.toString()}
+ ${helmet.link.toString() || fallbackTheme}
</head>
render() {
return (
- <picture className="pictrs-image d-inline-block overflow-hidden">
+ <picture>
<source srcSet={this.src("webp")} type="image/webp" />
<source srcSet={this.props.src} />
<source srcSet={this.src("jpg")} type="image/jpeg" />
alt={this.alt()}
title={this.alt()}
loading="lazy"
- className={classNames({
+ className={classNames("overflow-hidden pictrs-image", {
"img-fluid": !this.props.icon && !this.props.iconOverlay,
banner: this.props.banner,
"thumbnail rounded":
<option disabled aria-hidden="true">
─────
</option>
+ <option value={"TopHour"}>{I18NextService.i18n.t("top_hour")}</option>
+ <option value={"TopSixHour"}>
+ {I18NextService.i18n.t("top_six_hours")}
+ </option>
+ <option value={"TopTwelveHour"}>
+ {I18NextService.i18n.t("top_twelve_hours")}
+ </option>
<option value={"TopDay"}>{I18NextService.i18n.t("top_day")}</option>
<option value={"TopWeek"}>{I18NextService.i18n.t("top_week")}</option>
<option value={"TopMonth"}>
updateCommunityBlock,
updatePersonBlock,
} from "@utils/app";
-import { restoreScrollPosition, saveScrollPosition } from "@utils/browser";
import {
getPageFromString,
getQueryParams,
setupTippy();
}
- componentWillUnmount() {
- saveScrollPosition(this.context);
- }
-
static async fetchInitialData({
client,
path,
});
}
- restoreScrollPosition(this.context);
setupTippy();
}
showLocal,
updatePersonBlock,
} from "@utils/app";
-import { restoreScrollPosition, saveScrollPosition } from "@utils/browser";
import {
getPageFromString,
getQueryParams,
setupTippy();
}
- componentWillUnmount() {
- saveScrollPosition(this.context);
- }
-
static async fetchInitialData({
client,
auth,
});
}
- restoreScrollPosition(this.context);
setupTippy();
}
}
get img() {
- return this.imageSrc ? (
- <>
- <div className="offset-sm-3 my-2 d-none d-sm-block">
- <a href={this.imageSrc} className="d-inline-block">
- <PictrsImage src={this.imageSrc} />
- </a>
- </div>
- <div className="my-2 d-block d-sm-none">
- <a
- className="d-inline-block"
- onClick={linkEvent(this, this.handleImageExpandClick)}
- >
- <PictrsImage src={this.imageSrc} />
- </a>
+ if (this.imageSrc) {
+ return (
+ <>
+ <div className="offset-sm-3 my-2 d-none d-sm-block">
+ <a href={this.imageSrc} className="d-inline-block">
+ <PictrsImage src={this.imageSrc} />
+ </a>
+ </div>
+ <div className="my-2 d-block d-sm-none">
+ <a
+ className="d-inline-block"
+ onClick={linkEvent(this, this.handleImageExpandClick)}
+ >
+ <PictrsImage src={this.imageSrc} />
+ </a>
+ </div>
+ </>
+ );
+ }
+
+ const { post } = this.postView;
+ const { url } = post;
+
+ if (url && isVideo(url)) {
+ return (
+ <div className="embed-responsive mt-3">
+ <video muted controls className="embed-responsive-item col-12">
+ <source src={url} type="video/mp4" />
+ </video>
</div>
- </>
- ) : (
- <></>
- );
+ );
+ }
+
+ return <></>;
}
imgThumb(src: string) {
} else if (url) {
if (!this.props.hideImage && isVideo(url)) {
return (
- <div className="embed-responsive embed-responsive-16by9">
- <video
- playsInline
- muted
- loop
- controls
- className="embed-responsive-item"
- >
- <source src={url} type="video/mp4" />
- </video>
- </div>
+ <a
+ className="text-body"
+ href={url}
+ title={url}
+ rel={relTags}
+ data-tippy-content={I18NextService.i18n.t("expand_here")}
+ onClick={linkEvent(this, this.handleImageExpandClick)}
+ aria-label={I18NextService.i18n.t("expand_here")}
+ >
+ <div className="thumbnail rounded bg-light d-flex justify-content-center">
+ <Icon icon="play" classes="d-flex align-items-center" />
+ </div>
+ </a>
);
} else {
return (
export const relTags = "noopener nofollow";
export const emDash = "\u2014";
+/**
+ * Accepted formats:
+ * !community@server.com
+ * /c/community@server.com
+ * /m/community@server.com
+ * /u/username@server.com
+ */
+export const instanceLinkRegex =
+ /(\/[cmu]\/|!)[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
+
export const testHost = "0.0.0.0:8536";
import markdown_it_sup from "markdown-it-sup";
import Renderer from "markdown-it/lib/renderer";
import Token from "markdown-it/lib/token";
+import { instanceLinkRegex } from "./config";
export let Tribute: any;
},
};
+function localInstanceLinkParser(md: MarkdownIt) {
+ md.core.ruler.push("replace-text", state => {
+ for (let i = 0; i < state.tokens.length; i++) {
+ if (state.tokens[i].type !== "inline") {
+ continue;
+ }
+ const inlineTokens: Token[] = state.tokens[i].children || [];
+ for (let j = inlineTokens.length - 1; j >= 0; j--) {
+ if (
+ inlineTokens[j].type === "text" &&
+ new RegExp(instanceLinkRegex).test(inlineTokens[j].content)
+ ) {
+ const text = inlineTokens[j].content;
+ const matches = Array.from(text.matchAll(instanceLinkRegex));
+
+ let lastIndex = 0;
+ const newTokens: Token[] = [];
+
+ let linkClass = "community-link";
+
+ for (const match of matches) {
+ // If there is plain text before the match, add it as a separate token
+ if (match.index !== undefined && match.index > lastIndex) {
+ const textToken = new state.Token("text", "", 0);
+ textToken.content = text.slice(lastIndex, match.index);
+ newTokens.push(textToken);
+ }
+
+ let href;
+ if (match[0].startsWith("!")) {
+ href = "/c/" + match[0].substring(1);
+ } else if (match[0].startsWith("/m/")) {
+ href = "/c/" + match[0].substring(3);
+ } else {
+ href = match[0];
+ if (match[0].startsWith("/u/")) {
+ linkClass = "user-link";
+ }
+ }
+
+ const linkOpenToken = new state.Token("link_open", "a", 1);
+ linkOpenToken.attrs = [
+ ["href", href],
+ ["class", linkClass],
+ ];
+ const textToken = new state.Token("text", "", 0);
+ textToken.content = match[0];
+ const linkCloseToken = new state.Token("link_close", "a", -1);
+
+ newTokens.push(linkOpenToken, textToken, linkCloseToken);
+
+ lastIndex =
+ (match.index !== undefined ? match.index : 0) + match[0].length;
+ }
+
+ // If there is plain text after the last match, add it as a separate token
+ if (lastIndex < text.length) {
+ const textToken = new state.Token("text", "", 0);
+ textToken.content = text.slice(lastIndex);
+ newTokens.push(textToken);
+ }
+
+ inlineTokens.splice(j, 1, ...newTokens);
+ }
+ }
+ }
+ });
+}
+
export function setupMarkdown() {
const markdownItConfig: MarkdownIt.Options = {
html: false,
.use(markdown_it_sup)
.use(markdown_it_footnote)
.use(markdown_it_html5_embed, html5EmbedConfig)
- .use(markdown_it_container, "spoiler", spoilerConfig);
+ .use(markdown_it_container, "spoiler", spoilerConfig)
+ .use(localInstanceLinkParser);
// .use(markdown_it_emoji, {
// defs: emojiDefs,
// });
.use(markdown_it_footnote)
.use(markdown_it_html5_embed, html5EmbedConfig)
.use(markdown_it_container, "spoiler", spoilerConfig)
+ .use(localInstanceLinkParser)
// .use(markdown_it_emoji, {
// defs: emojiDefs,
// })
): CommentSortType {
switch (sort) {
case "TopAll":
+ case "TopHour":
+ case "TopSixHour":
+ case "TopTwelveHour":
case "TopDay":
case "TopWeek":
case "TopMonth":
export default function restoreScrollPosition(context: any) {
const path: string = context.router.route.location.pathname;
const y = Number(sessionStorage.getItem(`scrollPosition_${path}`));
+
window.scrollTo(0, y);
}
export default function saveScrollPosition(context: any) {
const path: string = context.router.route.location.pathname;
const y = window.scrollY;
+
sessionStorage.setItem(`scrollPosition_${path}`, y.toString());
}