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 { 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"] || "";
@ -16,6 +17,10 @@ export async function createSsrHtml(
) {
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(
@ -68,7 +73,7 @@ export async function createSsrHtml(
<!-- 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"
@ -85,7 +90,7 @@ export async function createSsrHtml(
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
<!-- Current theme and more -->
${helmet.link.toString()}
${helmet.link.toString() || fallbackTheme}
</head>

View file

@ -22,7 +22,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
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" />
@ -31,7 +31,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
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":

View file

@ -67,6 +67,13 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
<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"}>

View file

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

View file

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

View file

@ -239,25 +239,40 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}
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>
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>
<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>
</>
) : (
<></>
);
);
}
return <></>;
}
imgThumb(src: string) {
@ -325,17 +340,19 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} 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 (

View file

@ -25,4 +25,14 @@ export const fetchLimit = 40;
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";

View file

@ -14,6 +14,7 @@ import markdown_it_sub from "markdown-it-sub";
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;
@ -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() {
const markdownItConfig: MarkdownIt.Options = {
html: false,
@ -88,7 +158,8 @@ export function setupMarkdown() {
.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,
// });
@ -99,6 +170,7 @@ export function setupMarkdown() {
.use(markdown_it_footnote)
.use(markdown_it_html5_embed, html5EmbedConfig)
.use(markdown_it_container, "spoiler", spoilerConfig)
.use(localInstanceLinkParser)
// .use(markdown_it_emoji, {
// defs: emojiDefs,
// })

View file

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

View file

@ -1,5 +1,6 @@
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);
}

View file

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