Merge branch 'main' into fix/add-aria-describedby-lang-warn

This commit is contained in:
SleeplessOne1917 2023-06-23 21:34:43 +00:00 committed by GitHub
commit dac0fa50ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 24544 additions and 120 deletions

View file

@ -0,0 +1 @@
@import "variables.darkly";

View file

@ -0,0 +1 @@
@import "variables.litely";

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,59 @@
@import "variables.darkly-compact";
/*
GENERAL
*/
// Desktop Breakpoint
$container-max-widths: (
lg: 1920px,
);
// Reduce hr height
hr.my-3 {
margin-top: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
/*
POST-LISTING
*/
.post-listing {
line-height: 1;
.post-title h5 {
margin: 0;
}
.post-title + p {
padding-top: 0.125rem !important;
padding-bottom: 0.125rem !important;
}
.community-link {
padding-left: 0.125rem;
}
.person-listing {
padding-right: 0.125rem;
}
ul.list-inline {
&.mt-2 {
margin-top: 0.125rem !important;
}
&.mb-1 {
margin-bottom: 0.125rem !important;
}
}
.btn-sm {
--bs-btn-padding-y: 0;
}
.img-icon {
display: none;
}
}
@import "../../../../node_modules/bootstrap/scss/bootstrap";

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,59 @@
@import "variables.litely-compact";
/*
GENERAL
*/
// Desktop Breakpoint
$container-max-widths: (
lg: 1920px,
);
// Reduce hr height
hr.my-3 {
margin-top: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
/*
POST-LISTING
*/
.post-listing {
line-height: 1;
.post-title h5 {
margin: 0;
}
.post-title + p {
padding-top: 0.125rem !important;
padding-bottom: 0.125rem !important;
}
.community-link {
padding-left: 0.125rem;
}
.person-listing {
padding-right: 0.125rem;
}
ul.list-inline {
&.mt-2 {
margin-top: 0.125rem !important;
}
&.mb-1 {
margin-bottom: 0.125rem !important;
}
}
.btn-sm {
--bs-btn-padding-y: 0;
}
.img-icon {
display: none;
}
}
@import "../../../../node_modules/bootstrap/scss/bootstrap";

View file

@ -1,4 +1,4 @@
import { initializeSite } from "@utils/app";
import { initializeSite, setupDateFns } from "@utils/app";
import { hydrate } from "inferno-hydrate";
import { Router } from "inferno-router";
import { App } from "../shared/components/app/app";
@ -7,16 +7,22 @@ import { HistoryService } from "../shared/services";
import "bootstrap/js/dist/collapse";
import "bootstrap/js/dist/dropdown";
initializeSite(window.isoData.site_res);
async function startClient() {
initializeSite(window.isoData.site_res);
const wrapper = (
<Router history={HistoryService.history}>
<App />
</Router>
);
await setupDateFns();
const root = document.getElementById("root");
const wrapper = (
<Router history={HistoryService.history}>
<App />
</Router>
);
if (root) {
hydrate(wrapper, root);
const root = document.getElementById("root");
if (root) {
hydrate(wrapper, root);
}
}
startClient();

View file

@ -1,3 +1,4 @@
import { setupDateFns } from "@utils/app";
import express from "express";
import path from "path";
import process from "process";
@ -31,6 +32,7 @@ server.get("/css/themelist", ThemesListHandler);
server.get("/*", CatchAllHandler);
server.listen(Number(port), hostname, () => {
setupDateFns();
console.log(`http://${hostname}:${port}`);
});

View file

@ -7,8 +7,10 @@ const extraThemesFolder =
const themes: ReadonlyArray<string> = [
"darkly",
"darkly-red",
"darkly-compact",
"litely",
"litely-red",
"litely-compact",
];
export async function buildThemeList(): Promise<ReadonlyArray<string>> {

View file

@ -4,7 +4,7 @@ import { Provider } from "inferno-i18next-dess";
import { Route, Switch } from "inferno-router";
import { IsoDataOptionalSite } from "../../interfaces";
import { routes } from "../../routes";
import { I18NextService } from "../../services";
import { FirstLoadService, I18NextService } from "../../services";
import AuthGuard from "../common/auth-guard";
import ErrorGuard from "../common/error-guard";
import { ErrorPage } from "./error-page";
@ -45,27 +45,35 @@ export class App extends Component<any, any> {
<Navbar siteRes={siteRes} />
<div className="mt-4 p-0 fl-1">
<Switch>
{routes.map(({ path, component: RouteComponent }) => (
<Route
key={path}
path={path}
exact
component={routeProps => (
<ErrorGuard>
<main tabIndex={-1} ref={this.mainContentRef}>
{RouteComponent &&
(isAuthPath(path ?? "") ? (
<AuthGuard>
<RouteComponent {...routeProps} />
</AuthGuard>
) : (
<RouteComponent {...routeProps} />
))}
</main>
</ErrorGuard>
)}
/>
))}
{routes.map(
({ path, component: RouteComponent, fetchInitialData }) => (
<Route
key={path}
path={path}
exact
component={routeProps => {
if (!fetchInitialData) {
FirstLoadService.falsify();
}
return (
<ErrorGuard>
<main tabIndex={-1} ref={this.mainContentRef}>
{RouteComponent &&
(isAuthPath(path ?? "") ? (
<AuthGuard>
<RouteComponent {...routeProps} />
</AuthGuard>
) : (
<RouteComponent {...routeProps} />
))}
</main>
</ErrorGuard>
);
}}
/>
)
)}
<Route component={ErrorPage} />
</Switch>
</div>

View file

@ -16,6 +16,9 @@ import {
isMod,
} from "@utils/roles";
import classNames from "classnames";
import isBefore from "date-fns/isBefore";
import parseISO from "date-fns/parseISO";
import subMinutes from "date-fns/subMinutes";
import { Component, InfernoNode, linkEvent } from "inferno";
import { Link } from "inferno-router";
import {
@ -46,7 +49,6 @@ import {
SaveComment,
TransferCommunity,
} from "lemmy-js-client";
import moment from "moment";
import { commentTreeMaxDepth } from "../../config";
import {
BanType,
@ -1451,9 +1453,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
get isCommentNew(): boolean {
const now = moment.utc().subtract(10, "minutes");
const then = moment.utc(this.commentView.comment.published);
return now.isBefore(then);
const now = subMinutes(new Date(), 10);
const then = parseISO(this.commentView.comment.published);
return isBefore(now, then);
}
handleCommentCollapse(i: CommentNode) {

View file

@ -1,6 +1,7 @@
import { capitalizeFirstLetter } from "@utils/helpers";
import { capitalizeFirstLetter, formatPastDate } from "@utils/helpers";
import format from "date-fns/format";
import parseISO from "date-fns/parseISO";
import { Component } from "inferno";
import moment from "moment";
import { I18NextService } from "../../services";
import { Icon } from "./icon";
@ -11,22 +12,24 @@ interface MomentTimeProps {
ignoreUpdated?: boolean;
}
function formatDate(input: string) {
return format(parseISO(input), "PPPPpppp");
}
export class MomentTime extends Component<MomentTimeProps, any> {
constructor(props: any, context: any) {
super(props, context);
moment.locale([...I18NextService.i18n.languages]);
}
createdAndModifiedTimes() {
const updated = this.props.updated;
let line = `${capitalizeFirstLetter(
I18NextService.i18n.t("created")
)}: ${this.format(this.props.published)}`;
)}: ${formatDate(this.props.published)}`;
if (updated) {
line += `\n\n\n${capitalizeFirstLetter(
I18NextService.i18n.t("modified")
)} ${this.format(updated)}`;
)} ${formatDate(updated)}`;
}
return line;
}
@ -39,7 +42,7 @@ export class MomentTime extends Component<MomentTimeProps, any> {
className="moment-time font-italics pointer unselectable"
>
<Icon icon="edit-2" classes="icon-inline me-1" />
{moment.utc(this.props.updated).fromNow(!this.props.showAgo)}
{formatPastDate(this.props.updated)}
</span>
);
} else {
@ -47,15 +50,11 @@ export class MomentTime extends Component<MomentTimeProps, any> {
return (
<span
className="moment-time pointer unselectable"
data-tippy-content={this.format(published)}
data-tippy-content={formatDate(published)}
>
{moment.utc(published).fromNow(!this.props.showAgo)}
{formatPastDate(published)}
</span>
);
}
}
format(input: string): string {
return moment.utc(input).local().format("LLLL");
}
}

View file

@ -78,7 +78,12 @@ import {
InitialFetchRequest,
} from "../../interfaces";
import { mdToHtml } from "../../markdown";
import { FirstLoadService, I18NextService, UserService } from "../../services";
import {
FirstLoadService,
HomeCacheService,
I18NextService,
UserService,
} from "../../services";
import { HttpService, RequestState } from "../../services/HttpService";
import { setupTippy } from "../../tippy";
import { toast } from "../../toast";
@ -101,6 +106,7 @@ interface HomeState {
showTrendingMobile: boolean;
showSidebarMobile: boolean;
subscribedCollapsed: boolean;
scrolled: boolean;
tagline?: string;
siteRes: GetSiteResponse;
finished: Map<CommentId, boolean | undefined>;
@ -217,6 +223,7 @@ export class Home extends Component<any, HomeState> {
postsRes: { state: "empty" },
commentsRes: { state: "empty" },
trendingCommunitiesRes: { state: "empty" },
scrolled: true,
siteRes: this.isoData.site_res,
showSubscribedMobile: false,
showTrendingMobile: false,
@ -276,9 +283,15 @@ export class Home extends Component<any, HomeState> {
?.content,
isIsomorphic: true,
};
HomeCacheService.postsRes = postsRes;
}
}
componentWillUnmount() {
HomeCacheService.activate();
}
async componentDidMount() {
if (
!this.state.isIsomorphic ||
@ -620,6 +633,11 @@ export class Home extends Component<any, HomeState> {
search: getQueryString(queryParams),
});
if (!this.state.scrolled) {
this.setState({ scrolled: true });
setTimeout(() => window.scrollTo(0, 0), 0);
}
await this.fetchData();
}
@ -643,6 +661,8 @@ export class Home extends Component<any, HomeState> {
if (dataType === DataType.Post) {
switch (this.state.postsRes?.state) {
case "empty":
return <div style="min-height: 20000px;"></div>;
case "loading":
return (
<h5>
@ -770,17 +790,30 @@ export class Home extends Component<any, HomeState> {
const { dataType, page, listingType, sort } = getHomeQueryParams();
if (dataType === DataType.Post) {
this.setState({ postsRes: { state: "loading" } });
this.setState({
postsRes: await HttpService.client.getPosts({
page,
limit: fetchLimit,
sort,
saved_only: false,
type_: listingType,
auth,
}),
});
if (HomeCacheService.active) {
const { postsRes, scrollY } = HomeCacheService;
HomeCacheService.deactivate();
this.setState({ postsRes });
window.scrollTo({
left: 0,
top: scrollY,
behavior: "instant",
});
} else {
this.setState({ postsRes: { state: "loading" } });
this.setState({
postsRes: await HttpService.client.getPosts({
page,
limit: fetchLimit,
sort,
saved_only: false,
type_: listingType,
auth,
}),
});
HomeCacheService.postsRes = this.state.postsRes;
}
} else {
this.setState({ commentsRes: { state: "loading" } });
this.setState({
@ -815,23 +848,23 @@ export class Home extends Component<any, HomeState> {
}
handlePageChange(page: number) {
this.setState({ scrolled: false });
this.updateUrl({ page });
window.scrollTo(0, 0);
}
handleSortChange(val: SortType) {
this.setState({ scrolled: false });
this.updateUrl({ sort: val, page: 1 });
window.scrollTo(0, 0);
}
handleListingTypeChange(val: ListingType) {
this.setState({ scrolled: false });
this.updateUrl({ listingType: val, page: 1 });
window.scrollTo(0, 0);
}
handleDataTypeChange(val: DataType) {
this.setState({ scrolled: false });
this.updateUrl({ dataType: val, page: 1 });
window.scrollTo(0, 0);
}
async handleAddModToCommunity(form: AddModToCommunity) {

View file

@ -7,6 +7,7 @@ import {
} from "@utils/app";
import {
debounce,
formatPastDate,
getIdFromString,
getPageFromString,
getQueryParams,
@ -44,7 +45,6 @@ import {
ModlogActionType,
Person,
} from "lemmy-js-client";
import moment from "moment";
import { fetchLimit } from "../config";
import { InitialFetchRequest } from "../interfaces";
import { FirstLoadService, I18NextService } from "../services";
@ -371,7 +371,7 @@ function renderModlogType({ type_, view }: ModlogType) {
)}
{expires && (
<span>
<div>expires: {moment.utc(expires).fromNow()}</div>
<div>expires: {formatPastDate(expires)}</div>
</span>
)}
</>
@ -403,7 +403,7 @@ function renderModlogType({ type_, view }: ModlogType) {
)}
{expires && (
<span>
<div>expires: {moment.utc(expires).fromNow()}</div>
<div>expires: {formatPastDate(expires)}</div>
</span>
)}
</>
@ -467,7 +467,7 @@ function renderModlogType({ type_, view }: ModlogType) {
)}
{expires && (
<span>
<div>expires: {moment.utc(expires).fromNow()}</div>
<div>expires: {formatPastDate(expires)}</div>
</span>
)}
</>

View file

@ -23,6 +23,8 @@ import { canMod, isAdmin, isBanned } from "@utils/roles";
import type { QueryParams } from "@utils/types";
import { RouteDataResponse } from "@utils/types";
import classNames from "classnames";
import format from "date-fns/format";
import parseISO from "date-fns/parseISO";
import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router";
@ -70,7 +72,6 @@ import {
SortType,
TransferCommunity,
} from "lemmy-js-client";
import moment from "moment";
import { fetchLimit, relTags } from "../../config";
import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
import { mdToHtml } from "../../markdown";
@ -613,10 +614,7 @@ export class Profile extends Component<
<Icon icon="cake" />
<span className="ms-2">
{I18NextService.i18n.t("cake_day_title")}{" "}
{moment
.utc(pv.person.published)
.local()
.format("MMM DD, YYYY")}
{format(parseISO(pv.person.published), "PPP")}
</span>
</div>
{!UserService.Instance.myUserInfo && (

View file

@ -1177,7 +1177,6 @@ export class Settings extends Component<any, SettingsState> {
});
if (saveRes.state === "success") {
UserService.Instance.login(saveRes.data);
location.reload();
toast(I18NextService.i18n.t("saved"));
window.scrollTo(0, 0);
}

View file

@ -1,3 +1,5 @@
import { isBrowser } from "@utils/browser";
export class FirstLoadService {
#isFirstLoad: boolean;
static #instance: FirstLoadService;
@ -15,11 +17,19 @@ export class FirstLoadService {
return isFirst;
}
falsify() {
this.#isFirstLoad = false;
}
static get #Instance() {
return this.#instance ?? (this.#instance = new this());
}
static get isFirstLoad() {
return this.#Instance.isFirstLoad;
return !isBrowser() || this.#Instance.isFirstLoad;
}
static falsify() {
this.#Instance.falsify();
}
}

View file

@ -0,0 +1,60 @@
import { GetPostsResponse } from "lemmy-js-client";
import { RequestState } from "./HttpService.js";
/**
* Service to cache home post listings and restore home state when user uses the browser back buttons.
*/
export class HomeCacheService {
static #_instance: HomeCacheService;
historyIdx = 0;
scrollY = 0;
posts: RequestState<GetPostsResponse> = { state: "empty" };
get active() {
return (
this.historyIdx === window.history.state.idx + 1 &&
this.posts.state === "success"
);
}
deactivate() {
this.historyIdx = 0;
}
activate() {
this.scrollY = window.scrollY;
this.historyIdx = window.history.state.idx;
}
static get #Instance() {
return this.#_instance ?? (this.#_instance = new this());
}
public static get scrollY() {
return this.#Instance.scrollY;
}
public static get historyIdx() {
return this.#Instance.historyIdx;
}
public static set postsRes(posts: RequestState<GetPostsResponse>) {
this.#Instance.posts = posts;
}
public static get postsRes() {
return this.#Instance.posts;
}
public static get active() {
return this.#Instance.active;
}
public static deactivate() {
this.#Instance.deactivate();
}
public static activate() {
this.#Instance.activate();
}
}

View file

@ -1,5 +1,6 @@
export { FirstLoadService } from "./FirstLoadService";
export { HistoryService } from "./HistoryService";
export { HomeCacheService } from "./HomeCacheService";
export { HttpService } from "./HttpService";
export { I18NextService } from "./I18NextService";
export { UserService } from "./UserService";

View file

@ -46,6 +46,7 @@ import searchCommentTree from "./search-comment-tree";
import selectableLanguages from "./selectable-languages";
import setIsoData from "./set-iso-data";
import setTheme from "./set-theme";
import setupDateFns from "./setup-date-fns";
import showAvatars from "./show-avatars";
import showLocal from "./show-local";
import showScores from "./show-scores";
@ -102,6 +103,7 @@ export {
selectableLanguages,
setIsoData,
setTheme,
setupDateFns,
showAvatars,
showLocal,
showScores,

View file

@ -0,0 +1,19 @@
import setDefaultOptions from "date-fns/setDefaultOptions";
import { I18NextService } from "../../services";
export default async function () {
let lang = I18NextService.i18n.language;
if (lang === "en") {
lang = "en-US";
}
const locale = (
await import(
/* webpackExclude: /\.js\.flow$/ */
`date-fns/locale/${lang}`
)
).default;
setDefaultOptions({
locale,
});
}

View file

@ -0,0 +1,12 @@
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,
}
);
}

View file

@ -1,6 +1,7 @@
import capitalizeFirstLetter from "./capitalize-first-letter";
import debounce from "./debounce";
import editListImmutable from "./edit-list-immutable";
import formatPastDate from "./format-past-date";
import futureDaysToUnixTime from "./future-days-to-unix-time";
import getIdFromString from "./get-id-from-string";
import getPageFromString from "./get-page-from-string";
@ -26,6 +27,7 @@ export {
capitalizeFirstLetter,
debounce,
editListImmutable,
formatPastDate,
futureDaysToUnixTime,
getIdFromString,
getPageFromString,

View file

@ -1,33 +1,13 @@
import moment from "moment";
moment.updateLocale("en", {
relativeTime: {
future: "in %s",
past: "%s ago",
s: "<1m",
ss: "%ds",
m: "1m",
mm: "%dm",
h: "1h",
hh: "%dh",
d: "1d",
dd: "%dd",
w: "1w",
ww: "%dw",
M: "1M",
MM: "%dM",
y: "1Y",
yy: "%dY",
},
});
import getDayOfYear from "date-fns/getDayOfYear";
import getYear from "date-fns/getYear";
import parseISO from "date-fns/parseISO";
export default function isCakeDay(published: string): boolean {
const createDate = moment.utc(published).local();
const currentDate = moment(new Date());
const createDate = parseISO(published);
const currentDate = new Date();
return (
createDate.date() === currentDate.date() &&
createDate.month() === currentDate.month() &&
createDate.year() !== currentDate.year()
getDayOfYear(createDate) === getDayOfYear(currentDate) &&
getYear(createDate) !== getYear(currentDate)
);
}