Skip to content

Commit

Permalink
Use mixins and decorators for scroll restoration and tippy cleanup (#…
Browse files Browse the repository at this point in the history
…2415)

* Enable @babel/plugin-proposal-decorators

Dependency already exists

* Use tippy.js delegate addon, cleanup tippy instances from a mixin.

The delegate addon creates tippy instances from mouse and touch events
with a matching `event.target`. This is initially significantly cheaper
than creating all instances at once. The addon keeps all created tippy
instances alive until it is destroyed itself.

`tippyMixin` destroys the addon instance after every render, as long as
all instances are hidden. This drops some tippy instances that may have
to be recreated later (e.g when the mouse moves over the trigger again),
but is otherwise fairly cheap (creates one tippy instance).

* Restore scroll positions when resource loading settles.

The history module generates a random string (`location.key`) for every
browser history entry. The names for saved positions include this key.
The position is saved before a route component unmounts or before the
`location.key` changes.

The `scrollMixin` tires to restore the scroll position after every
change of `location.key`. It only does so after the first render for
which the route components `loadingSettled()` returns true.

Things like `scrollToComments` should only scroll when `history.action`
is not "POP".

* Drop individual scrollTo calls

* Scroll to comments without reloading post

---------

Co-authored-by: SleeplessOne1917 <[email protected]>
  • Loading branch information
matc-pub and SleeplessOne1917 authored Apr 11, 2024
1 parent b983071 commit e48590b
Show file tree
Hide file tree
Showing 62 changed files with 570 additions and 142 deletions.
7 changes: 6 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
["@babel/typescript", { "isTSX": true, "allExtensions": true }]
],
"plugins": [
"@babel/plugin-transform-runtime",
["@babel/plugin-proposal-decorators", { "version": "legacy" }],
[
"@babel/plugin-transform-runtime",
// version defaults to 7.0.0 for which non-legacy decorators produce duplicate code
{ "version": "^7.24.3" }
],
["babel-plugin-inferno", { "imports": true }],
["@babel/plugin-transform-class-properties", { "loose": true }]
]
Expand Down
4 changes: 4 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-unused-vars": [
"error",
{ "argsIgnorePattern": "^_" }
],
"arrow-body-style": 0,
"curly": 0,
"eol-last": 0,
Expand Down
2 changes: 2 additions & 0 deletions src/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ async function startClient() {
verifyDynamicImports(true).then(x => console.log(x));
};

window.history.scrollRestoration = "manual";

initializeSite(window.isoData.site_res);

lazyHighlightjs.enableLazyLoading();
Expand Down
12 changes: 11 additions & 1 deletion src/shared/components/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,25 @@ import { Navbar } from "./navbar";
import "./styles.scss";
import { Theme } from "./theme";
import AnonymousGuard from "../common/anonymous-guard";
import { destroyTippy, setupTippy } from "../../tippy";

export class App extends Component<any, any> {
private isoData: IsoDataOptionalSite = setIsoData(this.context);
private readonly mainContentRef: RefObject<HTMLElement>;
private readonly rootRef = createRef<HTMLDivElement>();
constructor(props: any, context: any) {
super(props, context);
this.mainContentRef = createRef();
}

componentDidMount(): void {
setupTippy(this.rootRef);
}

componentWillUnmount(): void {
destroyTippy();
}

handleJumpToContent(event) {
event.preventDefault();
this.mainContentRef.current?.focus();
Expand All @@ -34,7 +44,7 @@ export class App extends Component<any, any> {
return (
<>
<Provider i18next={I18NextService.i18n}>
<div id="app" className="lemmy-site">
<div id="app" className="lemmy-site" ref={this.rootRef}>
<button
type="button"
className="btn skip-link bg-light position-absolute start-0 z-3"
Expand Down
2 changes: 2 additions & 0 deletions src/shared/components/app/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { toast } from "../../toast";
import { Icon } from "../common/icon";
import { PictrsImage } from "../common/pictrs-image";
import { Subscription } from "rxjs";
import { tippyMixin } from "../mixins/tippy-mixin";

interface NavbarProps {
siteRes?: GetSiteResponse;
Expand Down Expand Up @@ -42,6 +43,7 @@ function handleLogOut(i: Navbar) {
handleCollapseClick(i);
}

@tippyMixin
export class Navbar extends Component<NavbarProps, NavbarState> {
collapseButtonRef = createRef<HTMLButtonElement>();
mobileMenuRef = createRef<HTMLDivElement>();
Expand Down
5 changes: 2 additions & 3 deletions src/shared/components/comment/comment-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
} from "../../interfaces";
import { mdToHtml, mdToHtmlNoImages } from "../../markdown";
import { I18NextService, UserService } from "../../services";
import { setupTippy } from "../../tippy";
import { tippyMixin } from "../mixins/tippy-mixin";
import { Icon, Spinner } from "../common/icon";
import { MomentTime } from "../common/moment-time";
import { UserBadges } from "../common/user-badges";
Expand Down Expand Up @@ -117,6 +117,7 @@ function handleToggleViewSource(i: CommentNode) {
}));
}

@tippyMixin
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
state: CommentNodeState = {
showReply: false,
Expand Down Expand Up @@ -607,12 +608,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {

handleCommentCollapse(i: CommentNode) {
i.setState({ collapsed: !i.state.collapsed });
setupTippy();
}

handleShowAdvanced(i: CommentNode) {
i.setState({ showAdvanced: !i.state.showAdvanced });
setupTippy();
}

async handleSaveComment() {
Expand Down
2 changes: 2 additions & 0 deletions src/shared/components/comment/comment-report.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Icon, Spinner } from "../common/icon";
import { PersonListing } from "../person/person-listing";
import { CommentNode } from "./comment-node";
import { EMPTY_REQUEST } from "../../services/HttpService";
import { tippyMixin } from "../mixins/tippy-mixin";

interface CommentReportProps {
report: CommentReportView;
Expand All @@ -21,6 +22,7 @@ interface CommentReportState {
loading: boolean;
}

@tippyMixin
export class CommentReport extends Component<
CommentReportProps,
CommentReportState
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, linkEvent } from "inferno";
import { Icon, Spinner } from "../icon";
import classNames from "classnames";
import { tippyMixin } from "../../mixins/tippy-mixin";

interface ActionButtonPropsBase {
label: string;
Expand Down Expand Up @@ -34,6 +35,7 @@ async function handleClick(i: ActionButton) {
i.setState({ loading: false });
}

@tippyMixin
export default class ActionButton extends Component<
ActionButtonProps,
ActionButtonState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ViewVotesModal from "../view-votes-modal";
import ModActionFormModal, { BanUpdateForm } from "../mod-action-form-modal";
import { BanType, CommentNodeView, PurgeType } from "../../../interfaces";
import { getApubName, hostname } from "@utils/helpers";
import { tippyMixin } from "../../mixins/tippy-mixin";

interface ContentActionDropdownPropsBase {
onSave: () => Promise<void>;
Expand Down Expand Up @@ -76,6 +77,7 @@ type ContentActionDropdownState = {
mounted: boolean;
} & { [key in DialogType]: boolean };

@tippyMixin
export default class ContentActionDropdown extends Component<
ContentActionDropdownProps,
ContentActionDropdownState
Expand Down
2 changes: 2 additions & 0 deletions src/shared/components/common/emoji-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, linkEvent } from "inferno";
import { I18NextService } from "../../services";
import { EmojiMart } from "./emoji-mart";
import { Icon } from "./icon";
import { tippyMixin } from "../mixins/tippy-mixin";

interface EmojiPickerProps {
onEmojiClick?(val: any): any;
Expand All @@ -16,6 +17,7 @@ function closeEmojiMartOnEsc(i, event): void {
event.key === "Escape" && i.setState({ showPicker: false });
}

@tippyMixin
export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
private emptyState: EmojiPickerState = {
showPicker: false,
Expand Down
12 changes: 6 additions & 6 deletions src/shared/components/common/markdown-textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { numToSI, randomStr } from "@utils/helpers";
import autosize from "autosize";
import classNames from "classnames";
import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import { Component, InfernoNode, linkEvent } from "inferno";
import { Prompt } from "inferno-router";
import { Language } from "lemmy-js-client";
import {
Expand All @@ -15,7 +15,7 @@ import {
} from "../../config";
import { customEmojisLookup, mdToHtml, setupTribute } from "../../markdown";
import { HttpService, I18NextService, UserService } from "../../services";
import { setupTippy } from "../../tippy";
import { tippyMixin } from "../mixins/tippy-mixin";
import { pictrsDeleteToast, toast } from "../../toast";
import { EmojiPicker } from "./emoji-picker";
import { Icon, Spinner } from "./icon";
Expand Down Expand Up @@ -68,6 +68,7 @@ interface MarkdownTextAreaState {
submitted: boolean;
}

@tippyMixin
export class MarkdownTextArea extends Component<
MarkdownTextAreaProps,
MarkdownTextAreaState
Expand Down Expand Up @@ -111,13 +112,12 @@ export class MarkdownTextArea extends Component<
if (this.props.focus) {
textarea.focus();
}

// TODO this is slow for some reason
setupTippy();
}
}

componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
componentWillReceiveProps(
nextProps: MarkdownTextAreaProps & { children?: InfernoNode },
) {
if (nextProps.finished) {
this.setState({
previewMode: false,
Expand Down
2 changes: 2 additions & 0 deletions src/shared/components/common/moment-time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { format, parseISO } from "date-fns";
import { Component } from "inferno";
import { I18NextService } from "../../services";
import { Icon } from "./icon";
import { tippyMixin } from "../mixins/tippy-mixin";

interface MomentTimeProps {
published: string;
Expand All @@ -16,6 +17,7 @@ function formatDate(input: string) {
return format(parsed, "PPPPpppp");
}

@tippyMixin
export class MomentTime extends Component<MomentTimeProps, any> {
constructor(props: any, context: any) {
super(props, context);
Expand Down
2 changes: 2 additions & 0 deletions src/shared/components/common/password-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Component, FormEventHandler, linkEvent } from "inferno";
import { NavLink } from "inferno-router";
import { I18NextService } from "../../services";
import { Icon } from "./icon";
import { tippyMixin } from "../mixins/tippy-mixin";

interface PasswordInputProps {
id: string;
Expand Down Expand Up @@ -55,6 +56,7 @@ function handleToggleShow(i: PasswordInput) {
}));
}

@tippyMixin
class PasswordInput extends Component<PasswordInputProps, PasswordInputState> {
state: PasswordInputState = {
show: false,
Expand Down
4 changes: 3 additions & 1 deletion src/shared/components/common/user-badges.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import classNames from "classnames";
import { Component } from "inferno";
import { I18NextService } from "../../services";
import { tippyMixin } from "../mixins/tippy-mixin";

interface UserBadgesProps {
isBanned?: boolean;
Expand All @@ -12,7 +13,7 @@ interface UserBadgesProps {
classNames?: string;
}

export function getRoleLabelPill({
function getRoleLabelPill({
label,
tooltip,
classes,
Expand All @@ -34,6 +35,7 @@ export function getRoleLabelPill({
);
}

@tippyMixin
export class UserBadges extends Component<UserBadgesProps> {
render() {
return (
Expand Down
13 changes: 10 additions & 3 deletions src/shared/components/common/vote-buttons.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { newVote, showScores } from "@utils/app";
import { numToSI } from "@utils/helpers";
import classNames from "classnames";
import { Component, linkEvent } from "inferno";
import { Component, InfernoNode, linkEvent } from "inferno";
import {
CommentAggregates,
CreateCommentLike,
Expand All @@ -11,6 +11,7 @@ import {
import { VoteContentType, VoteType } from "../../interfaces";
import { I18NextService, UserService } from "../../services";
import { Icon, Spinner } from "../common/icon";
import { tippyMixin } from "../mixins/tippy-mixin";

interface VoteButtonsProps {
voteContentType: VoteContentType;
Expand Down Expand Up @@ -82,6 +83,7 @@ const handleDownvote = (i: VoteButtons) => {
}
};

@tippyMixin
export class VoteButtonsCompact extends Component<
VoteButtonsProps,
VoteButtonsState
Expand All @@ -95,7 +97,9 @@ export class VoteButtonsCompact extends Component<
super(props, context);
}

componentWillReceiveProps(nextProps: VoteButtonsProps) {
componentWillReceiveProps(
nextProps: VoteButtonsProps & { children?: InfernoNode },
) {
if (this.props !== nextProps) {
this.setState({
upvoteLoading: false,
Expand Down Expand Up @@ -166,6 +170,7 @@ export class VoteButtonsCompact extends Component<
}
}

@tippyMixin
export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
state: VoteButtonsState = {
upvoteLoading: false,
Expand All @@ -176,7 +181,9 @@ export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
super(props, context);
}

componentWillReceiveProps(nextProps: VoteButtonsProps) {
componentWillReceiveProps(
nextProps: VoteButtonsProps & { children?: InfernoNode },
) {
if (this.props !== nextProps) {
this.setState({
upvoteLoading: false,
Expand Down
9 changes: 7 additions & 2 deletions src/shared/components/community/communities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getQueryParams,
getQueryString,
numToSI,
resourcesSettled,
} from "@utils/helpers";
import type { QueryParams } from "@utils/types";
import { RouteDataResponse } from "@utils/types";
Expand Down Expand Up @@ -38,6 +39,7 @@ import { SubscribeButton } from "../common/subscribe-button";
import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes";
import { scrollMixin } from "../mixins/scroll-mixin";

type CommunitiesData = RouteDataResponse<{
listCommunitiesResponse: ListCommunitiesResponse;
Expand Down Expand Up @@ -84,6 +86,7 @@ export type CommunitiesFetchConfig = IRoutePropsWithFetch<
CommunitiesProps
>;

@scrollMixin
export class Communities extends Component<
CommunitiesRouteProps,
CommunitiesState
Expand All @@ -96,6 +99,10 @@ export class Communities extends Component<
isIsomorphic: false,
};

loadingSettled() {
return resourcesSettled([this.state.listCommunitiesResponse]);
}

constructor(props: CommunitiesRouteProps, context: any) {
super(props, context);
this.handlePageChange = this.handlePageChange.bind(this);
Expand Down Expand Up @@ -374,8 +381,6 @@ export class Communities extends Component<
page,
}),
});

window.scrollTo(0, 0);
}

findAndUpdateCommunity(res: RequestState<CommunityResponse>) {
Expand Down
2 changes: 2 additions & 0 deletions src/shared/components/community/community-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Icon, Spinner } from "../common/icon";
import { ImageUploadForm } from "../common/image-upload-form";
import { LanguageSelect } from "../common/language-select";
import { MarkdownTextArea } from "../common/markdown-textarea";
import { tippyMixin } from "../mixins/tippy-mixin";

interface CommunityFormProps {
community_view?: CommunityView; // If a community is given, that means this is an edit
Expand Down Expand Up @@ -40,6 +41,7 @@ interface CommunityFormState {
submitted: boolean;
}

@tippyMixin
export class CommunityForm extends Component<
CommunityFormProps,
CommunityFormState
Expand Down
Loading

0 comments on commit e48590b

Please sign in to comment.