Merge remote-tracking branch 'origin/main' into feat/vote-components

* origin/main: (26 commits)
  Adding jsit to codeowners.
  Cleanup, only check for /u/ if /c/ and /m/ checks fail
  Rename function to be more generic, since it parses users
  Typescript linter fixes
  bandaid fix our video embeds
  Remove pipe from community link regex
  Add missing classes
  Use shorter regex in community link parser
  Move regex pattern to config
  Update community link markdown parsing
  Fix avatar alignment issue (#1475)
  Omit user-scalable to use default
  Update getHttpBase dependency reference
  Enable users to zoom on mobile
  rethink it a bit
  rethink it a bit
  add fallback style tag
  Add community link class
  prettier
  Add local community link parser plugin for Markdown-It
  ...
This commit is contained in:
Jay Sitter 2023-06-22 16:56:08 -04:00
commit be6ec3692e
12 changed files with 151 additions and 47 deletions

2
.github/CODEOWNERS vendored
View file

@ -1 +1 @@
* @dessalines @SleeplessOne1917 @alectrocute * @dessalines @SleeplessOne1917 @alectrocute @jsit

View file

@ -4,6 +4,7 @@ import serialize from "serialize-javascript";
import sharp from "sharp"; import sharp from "sharp";
import { favIconPngUrl, favIconUrl } from "../../shared/config"; import { favIconPngUrl, favIconUrl } from "../../shared/config";
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces"; import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
import { buildThemeList } from "./build-themes-list";
import { fetchIconPng } from "./fetch-icon-png"; import { fetchIconPng } from "./fetch-icon-png";
const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || ""; const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || "";
@ -16,6 +17,10 @@ export async function createSsrHtml(
) { ) {
const site = isoData.site_res; const site = isoData.site_res;
const fallbackTheme = `<link rel="stylesheet" type="text/css" href="/css/themes/${
(await buildThemeList())[0]
}.css" />`;
if (!appleTouchIcon) { if (!appleTouchIcon) {
appleTouchIcon = site?.site_view.site.icon appleTouchIcon = site?.site_view.site.icon
? `data:image/png;base64,${sharp( ? `data:image/png;base64,${sharp(
@ -68,7 +73,7 @@ export async function createSsrHtml(
<!-- Required meta tags --> <!-- Required meta tags -->
<meta name="Description" content="Lemmy"> <meta name="Description" content="Lemmy">
<meta charset="utf-8"> <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 <link
id="favicon" id="favicon"
rel="shortcut icon" rel="shortcut icon"
@ -85,7 +90,7 @@ export async function createSsrHtml(
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" /> <link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
<!-- Current theme and more --> <!-- Current theme and more -->
${helmet.link.toString()} ${helmet.link.toString() || fallbackTheme}
</head> </head>

View file

@ -22,7 +22,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
render() { render() {
return ( return (
<picture className="pictrs-image d-inline-block overflow-hidden"> <picture>
<source srcSet={this.src("webp")} type="image/webp" /> <source srcSet={this.src("webp")} type="image/webp" />
<source srcSet={this.props.src} /> <source srcSet={this.props.src} />
<source srcSet={this.src("jpg")} type="image/jpeg" /> <source srcSet={this.src("jpg")} type="image/jpeg" />
@ -31,7 +31,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
alt={this.alt()} alt={this.alt()}
title={this.alt()} title={this.alt()}
loading="lazy" loading="lazy"
className={classNames({ className={classNames("overflow-hidden pictrs-image", {
"img-fluid": !this.props.icon && !this.props.iconOverlay, "img-fluid": !this.props.icon && !this.props.iconOverlay,
banner: this.props.banner, banner: this.props.banner,
"thumbnail rounded": "thumbnail rounded":

View file

@ -67,6 +67,13 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
<option disabled aria-hidden="true"> <option disabled aria-hidden="true">
</option> </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={"TopDay"}>{I18NextService.i18n.t("top_day")}</option>
<option value={"TopWeek"}>{I18NextService.i18n.t("top_week")}</option> <option value={"TopWeek"}>{I18NextService.i18n.t("top_week")}</option>
<option value={"TopMonth"}> <option value={"TopMonth"}>

View file

@ -15,7 +15,6 @@ import {
updateCommunityBlock, updateCommunityBlock,
updatePersonBlock, updatePersonBlock,
} from "@utils/app"; } from "@utils/app";
import { restoreScrollPosition, saveScrollPosition } from "@utils/browser";
import { import {
getPageFromString, getPageFromString,
getQueryParams, getQueryParams,
@ -229,10 +228,6 @@ export class Community extends Component<
setupTippy(); setupTippy();
} }
componentWillUnmount() {
saveScrollPosition(this.context);
}
static async fetchInitialData({ static async fetchInitialData({
client, client,
path, path,
@ -609,7 +604,6 @@ export class Community extends Component<
}); });
} }
restoreScrollPosition(this.context);
setupTippy(); setupTippy();
} }

View file

@ -13,7 +13,6 @@ import {
showLocal, showLocal,
updatePersonBlock, updatePersonBlock,
} from "@utils/app"; } from "@utils/app";
import { restoreScrollPosition, saveScrollPosition } from "@utils/browser";
import { import {
getPageFromString, getPageFromString,
getQueryParams, getQueryParams,
@ -293,10 +292,6 @@ export class Home extends Component<any, HomeState> {
setupTippy(); setupTippy();
} }
componentWillUnmount() {
saveScrollPosition(this.context);
}
static async fetchInitialData({ static async fetchInitialData({
client, client,
auth, auth,
@ -800,7 +795,6 @@ export class Home extends Component<any, HomeState> {
}); });
} }
restoreScrollPosition(this.context);
setupTippy(); setupTippy();
} }

View file

@ -239,7 +239,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
get img() { get img() {
return this.imageSrc ? ( if (this.imageSrc) {
return (
<> <>
<div className="offset-sm-3 my-2 d-none d-sm-block"> <div className="offset-sm-3 my-2 d-none d-sm-block">
<a href={this.imageSrc} className="d-inline-block"> <a href={this.imageSrc} className="d-inline-block">
@ -255,11 +256,25 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</a> </a>
</div> </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) { imgThumb(src: string) {
const post_view = this.postView; const post_view = this.postView;
return ( return (
@ -325,17 +340,19 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} else if (url) { } else if (url) {
if (!this.props.hideImage && isVideo(url)) { if (!this.props.hideImage && isVideo(url)) {
return ( return (
<div className="embed-responsive embed-responsive-16by9"> <a
<video className="text-body"
playsInline href={url}
muted title={url}
loop rel={relTags}
controls data-tippy-content={I18NextService.i18n.t("expand_here")}
className="embed-responsive-item" onClick={linkEvent(this, this.handleImageExpandClick)}
aria-label={I18NextService.i18n.t("expand_here")}
> >
<source src={url} type="video/mp4" /> <div className="thumbnail rounded bg-light d-flex justify-content-center">
</video> <Icon icon="play" classes="d-flex align-items-center" />
</div> </div>
</a>
); );
} else { } else {
return ( return (

View file

@ -25,4 +25,14 @@ export const fetchLimit = 40;
export const relTags = "noopener nofollow"; export const relTags = "noopener nofollow";
export const emDash = "\u2014"; 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"; export const testHost = "0.0.0.0:8536";

View file

@ -14,6 +14,7 @@ import markdown_it_sub from "markdown-it-sub";
import markdown_it_sup from "markdown-it-sup"; import markdown_it_sup from "markdown-it-sup";
import Renderer from "markdown-it/lib/renderer"; import Renderer from "markdown-it/lib/renderer";
import Token from "markdown-it/lib/token"; import Token from "markdown-it/lib/token";
import { instanceLinkRegex } from "./config";
export let Tribute: any; export let Tribute: any;
@ -72,6 +73,75 @@ const html5EmbedConfig = {
}, },
}; };
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() { export function setupMarkdown() {
const markdownItConfig: MarkdownIt.Options = { const markdownItConfig: MarkdownIt.Options = {
html: false, html: false,
@ -88,7 +158,8 @@ export function setupMarkdown() {
.use(markdown_it_sup) .use(markdown_it_sup)
.use(markdown_it_footnote) .use(markdown_it_footnote)
.use(markdown_it_html5_embed, html5EmbedConfig) .use(markdown_it_html5_embed, html5EmbedConfig)
.use(markdown_it_container, "spoiler", spoilerConfig); .use(markdown_it_container, "spoiler", spoilerConfig)
.use(localInstanceLinkParser);
// .use(markdown_it_emoji, { // .use(markdown_it_emoji, {
// defs: emojiDefs, // defs: emojiDefs,
// }); // });
@ -99,6 +170,7 @@ export function setupMarkdown() {
.use(markdown_it_footnote) .use(markdown_it_footnote)
.use(markdown_it_html5_embed, html5EmbedConfig) .use(markdown_it_html5_embed, html5EmbedConfig)
.use(markdown_it_container, "spoiler", spoilerConfig) .use(markdown_it_container, "spoiler", spoilerConfig)
.use(localInstanceLinkParser)
// .use(markdown_it_emoji, { // .use(markdown_it_emoji, {
// defs: emojiDefs, // defs: emojiDefs,
// }) // })

View file

@ -5,6 +5,9 @@ export default function convertCommentSortType(
): CommentSortType { ): CommentSortType {
switch (sort) { switch (sort) {
case "TopAll": case "TopAll":
case "TopHour":
case "TopSixHour":
case "TopTwelveHour":
case "TopDay": case "TopDay":
case "TopWeek": case "TopWeek":
case "TopMonth": case "TopMonth":

View file

@ -1,5 +1,6 @@
export default function restoreScrollPosition(context: any) { export default function restoreScrollPosition(context: any) {
const path: string = context.router.route.location.pathname; const path: string = context.router.route.location.pathname;
const y = Number(sessionStorage.getItem(`scrollPosition_${path}`)); const y = Number(sessionStorage.getItem(`scrollPosition_${path}`));
window.scrollTo(0, y); window.scrollTo(0, y);
} }

View file

@ -1,5 +1,6 @@
export default function saveScrollPosition(context: any) { export default function saveScrollPosition(context: any) {
const path: string = context.router.route.location.pathname; const path: string = context.router.route.location.pathname;
const y = window.scrollY; const y = window.scrollY;
sessionStorage.setItem(`scrollPosition_${path}`, y.toString()); sessionStorage.setItem(`scrollPosition_${path}`, y.toString());
} }