mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-01 10:09:56 +00:00
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:
commit
be6ec3692e
12 changed files with 151 additions and 47 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -1 +1 @@
|
|||
* @dessalines @SleeplessOne1917 @alectrocute
|
||||
* @dessalines @SleeplessOne1917 @alectrocute @jsit
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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"}>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -239,7 +239,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
}
|
||||
|
||||
get img() {
|
||||
return this.imageSrc ? (
|
||||
if (this.imageSrc) {
|
||||
return (
|
||||
<>
|
||||
<div className="offset-sm-3 my-2 d-none d-sm-block">
|
||||
<a href={this.imageSrc} className="d-inline-block">
|
||||
|
@ -255,11 +256,25 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</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) {
|
||||
const post_view = this.postView;
|
||||
return (
|
||||
|
@ -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"
|
||||
<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")}
|
||||
>
|
||||
<source src={url} type="video/mp4" />
|
||||
</video>
|
||||
<div className="thumbnail rounded bg-light d-flex justify-content-center">
|
||||
<Icon icon="play" classes="d-flex align-items-center" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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,
|
||||
// })
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue