mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-22 02:41:25 +00:00
Use mixins and decorators for scroll restoration and tippy cleanup (#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 <28871516+SleeplessOne1917@users.noreply.github.com>
This commit is contained in:
parent
b983071e79
commit
e48590b9d6
62 changed files with 571 additions and 143 deletions
7
.babelrc
7
.babelrc
|
@ -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 }]
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
@ -42,6 +43,7 @@ function handleLogOut(i: Navbar) {
|
|||
handleCollapseClick(i);
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||
collapseButtonRef = createRef<HTMLButtonElement>();
|
||||
mobileMenuRef = createRef<HTMLDivElement>();
|
||||
|
|
|
@ -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";
|
||||
|
@ -117,6 +117,7 @@ function handleToggleViewSource(i: CommentNode) {
|
|||
}));
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||
state: CommentNodeState = {
|
||||
showReply: false,
|
||||
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
@ -21,6 +22,7 @@ interface CommentReportState {
|
|||
loading: boolean;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class CommentReport extends Component<
|
||||
CommentReportProps,
|
||||
CommentReportState
|
||||
|
|
|
@ -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;
|
||||
|
@ -34,6 +35,7 @@ async function handleClick(i: ActionButton) {
|
|||
i.setState({ loading: false });
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export default class ActionButton extends Component<
|
||||
ActionButtonProps,
|
||||
ActionButtonState
|
||||
|
|
|
@ -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>;
|
||||
|
@ -76,6 +77,7 @@ type ContentActionDropdownState = {
|
|||
mounted: boolean;
|
||||
} & { [key in DialogType]: boolean };
|
||||
|
||||
@tippyMixin
|
||||
export default class ContentActionDropdown extends Component<
|
||||
ContentActionDropdownProps,
|
||||
ContentActionDropdownState
|
||||
|
|
|
@ -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;
|
||||
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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";
|
||||
|
@ -68,6 +68,7 @@ interface MarkdownTextAreaState {
|
|||
submitted: boolean;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class MarkdownTextArea extends Component<
|
||||
MarkdownTextAreaProps,
|
||||
MarkdownTextAreaState
|
||||
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
@ -55,6 +56,7 @@ function handleToggleShow(i: PasswordInput) {
|
|||
}));
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
class PasswordInput extends Component<PasswordInputProps, PasswordInputState> {
|
||||
state: PasswordInputState = {
|
||||
show: false,
|
||||
|
|
|
@ -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;
|
||||
|
@ -12,7 +13,7 @@ interface UserBadgesProps {
|
|||
classNames?: string;
|
||||
}
|
||||
|
||||
export function getRoleLabelPill({
|
||||
function getRoleLabelPill({
|
||||
label,
|
||||
tooltip,
|
||||
classes,
|
||||
|
@ -34,6 +35,7 @@ export function getRoleLabelPill({
|
|||
);
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class UserBadges extends Component<UserBadgesProps> {
|
||||
render() {
|
||||
return (
|
||||
|
|
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -82,6 +83,7 @@ const handleDownvote = (i: VoteButtons) => {
|
|||
}
|
||||
};
|
||||
|
||||
@tippyMixin
|
||||
export class VoteButtonsCompact extends Component<
|
||||
VoteButtonsProps,
|
||||
VoteButtonsState
|
||||
|
@ -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,
|
||||
|
@ -166,6 +170,7 @@ export class VoteButtonsCompact extends Component<
|
|||
}
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
|
||||
state: VoteButtonsState = {
|
||||
upvoteLoading: false,
|
||||
|
@ -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,
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
getQueryParams,
|
||||
getQueryString,
|
||||
numToSI,
|
||||
resourcesSettled,
|
||||
} from "@utils/helpers";
|
||||
import type { QueryParams } from "@utils/types";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
|
@ -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;
|
||||
|
@ -84,6 +86,7 @@ export type CommunitiesFetchConfig = IRoutePropsWithFetch<
|
|||
CommunitiesProps
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class Communities extends Component<
|
||||
CommunitiesRouteProps,
|
||||
CommunitiesState
|
||||
|
@ -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);
|
||||
|
@ -374,8 +381,6 @@ export class Communities extends Component<
|
|||
page,
|
||||
}),
|
||||
});
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
findAndUpdateCommunity(res: RequestState<CommunityResponse>) {
|
||||
|
|
|
@ -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
|
||||
|
@ -40,6 +41,7 @@ interface CommunityFormState {
|
|||
submitted: boolean;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class CommunityForm extends Component<
|
||||
CommunityFormProps,
|
||||
CommunityFormState
|
||||
|
|
|
@ -14,7 +14,12 @@ import {
|
|||
updateCommunityBlock,
|
||||
updatePersonBlock,
|
||||
} from "@utils/app";
|
||||
import { getQueryParams, getQueryString } from "@utils/helpers";
|
||||
import {
|
||||
getQueryParams,
|
||||
getQueryString,
|
||||
resourcesSettled,
|
||||
} from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import type { QueryParams } from "@utils/types";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
import { Component, RefObject, createRef, linkEvent } from "inferno";
|
||||
|
@ -87,7 +92,7 @@ import {
|
|||
RequestState,
|
||||
wrapClient,
|
||||
} from "../../services/HttpService";
|
||||
import { setupTippy } from "../../tippy";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
import { toast } from "../../toast";
|
||||
import { CommentNodes } from "../comment/comment-nodes";
|
||||
import { BannerIconHeader } from "../common/banner-icon-header";
|
||||
|
@ -172,6 +177,8 @@ export type CommunityFetchConfig = IRoutePropsWithFetch<
|
|||
CommunityProps
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
@tippyMixin
|
||||
export class Community extends Component<CommunityRouteProps, State> {
|
||||
private isoData = setIsoData<CommunityData>(this.context);
|
||||
state: State = {
|
||||
|
@ -184,6 +191,16 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
isIsomorphic: false,
|
||||
};
|
||||
private readonly mainContentRef: RefObject<HTMLElement>;
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([
|
||||
this.state.communityRes,
|
||||
this.props.dataType === DataType.Post
|
||||
? this.state.postsRes
|
||||
: this.state.commentsRes,
|
||||
]);
|
||||
}
|
||||
|
||||
constructor(props: CommunityRouteProps, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
@ -253,8 +270,6 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
if (!this.state.isIsomorphic) {
|
||||
await Promise.all([this.fetchCommunity(), this.fetchData()]);
|
||||
}
|
||||
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
static async fetchInitialData({
|
||||
|
@ -586,17 +601,14 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
|
||||
handlePageNext(nextPage: PaginationCursor) {
|
||||
this.updateUrl({ pageCursor: nextPage });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
handleSortChange(sort: SortType) {
|
||||
this.updateUrl({ sort, pageCursor: undefined });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
handleDataTypeChange(dataType: DataType) {
|
||||
this.updateUrl({ dataType, pageCursor: undefined });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
handleShowSidebarMobile(i: Community) {
|
||||
|
@ -649,8 +661,6 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
}),
|
||||
});
|
||||
}
|
||||
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
async handleDeleteCommunity(form: DeleteCommunity) {
|
||||
|
|
|
@ -7,13 +7,19 @@ import {
|
|||
import { HttpService, I18NextService } from "../../services";
|
||||
import { HtmlTags } from "../common/html-tags";
|
||||
import { CommunityForm } from "./community-form";
|
||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
|
||||
interface CreateCommunityState {
|
||||
siteRes: GetSiteResponse;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class CreateCommunity extends Component<any, CreateCommunityState> {
|
||||
@simpleScrollMixin
|
||||
export class CreateCommunity extends Component<
|
||||
RouteComponentProps<Record<string, never>>,
|
||||
CreateCommunityState
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
state: CreateCommunityState = {
|
||||
siteRes: this.isoData.site_res,
|
||||
|
|
|
@ -25,6 +25,7 @@ import { SubscribeButton } from "../common/subscribe-button";
|
|||
import { CommunityForm } from "../community/community-form";
|
||||
import { CommunityLink } from "../community/community-link";
|
||||
import { PersonListing } from "../person/person-listing";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
|
||||
interface SidebarProps {
|
||||
community_view: CommunityView;
|
||||
|
@ -60,6 +61,7 @@ interface SidebarState {
|
|||
purgeCommunityLoading: boolean;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||
state: SidebarState = {
|
||||
showEdit: false,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { fetchThemeList, setIsoData, showLocal } from "@utils/app";
|
||||
import { capitalizeFirstLetter } from "@utils/helpers";
|
||||
import { capitalizeFirstLetter, resourcesSettled } from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
import classNames from "classnames";
|
||||
import { Component, linkEvent } from "inferno";
|
||||
|
@ -62,6 +63,7 @@ export type AdminSettingsFetchConfig = IRoutePropsWithFetch<
|
|||
Record<string, never>
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class AdminSettings extends Component<
|
||||
AdminSettingsRouteProps,
|
||||
AdminSettingsState
|
||||
|
@ -79,6 +81,10 @@ export class AdminSettings extends Component<
|
|||
isIsomorphic: false,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([this.state.bannedRes, this.state.instancesRes]);
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { pictrsDeleteToast, toast } from "../../toast";
|
|||
import { EmojiMart } from "../common/emoji-mart";
|
||||
import { Icon, Spinner } from "../common/icon";
|
||||
import { Paginator } from "../common/paginator";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
|
||||
interface EmojiFormProps {
|
||||
onEdit(form: EditCustomEmoji): void;
|
||||
|
@ -38,6 +39,7 @@ interface CustomEmojiViewForm {
|
|||
loading: boolean;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private itemsPerPage = 15;
|
||||
|
|
|
@ -17,7 +17,9 @@ import {
|
|||
getQueryParams,
|
||||
getQueryString,
|
||||
getRandomFromList,
|
||||
resourcesSettled,
|
||||
} from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import { canCreateCommunity } from "@utils/roles";
|
||||
import type { QueryParams } from "@utils/types";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
|
@ -87,7 +89,7 @@ import {
|
|||
RequestState,
|
||||
wrapClient,
|
||||
} from "../../services/HttpService";
|
||||
import { setupTippy } from "../../tippy";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
import { toast } from "../../toast";
|
||||
import { CommentNodes } from "../comment/comment-nodes";
|
||||
import { DataTypeSelect } from "../common/data-type-select";
|
||||
|
@ -107,6 +109,7 @@ import {
|
|||
} from "../common/loading-skeleton";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { snapToTop } from "@utils/browser";
|
||||
|
||||
interface HomeState {
|
||||
postsRes: RequestState<GetPostsResponse>;
|
||||
|
@ -116,7 +119,6 @@ interface HomeState {
|
|||
showTrendingMobile: boolean;
|
||||
showSidebarMobile: boolean;
|
||||
subscribedCollapsed: boolean;
|
||||
scrolled: boolean;
|
||||
tagline?: string;
|
||||
siteRes: GetSiteResponse;
|
||||
finished: Map<CommentId, boolean | undefined>;
|
||||
|
@ -253,13 +255,14 @@ export type HomeFetchConfig = IRoutePropsWithFetch<
|
|||
HomeProps
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
@tippyMixin
|
||||
export class Home extends Component<HomeRouteProps, HomeState> {
|
||||
private isoData = setIsoData<HomeData>(this.context);
|
||||
state: HomeState = {
|
||||
postsRes: EMPTY_REQUEST,
|
||||
commentsRes: EMPTY_REQUEST,
|
||||
trendingCommunitiesRes: EMPTY_REQUEST,
|
||||
scrolled: true,
|
||||
siteRes: this.isoData.site_res,
|
||||
showSubscribedMobile: false,
|
||||
showTrendingMobile: false,
|
||||
|
@ -269,6 +272,15 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
|||
isIsomorphic: false,
|
||||
};
|
||||
|
||||
loadingSettled(): boolean {
|
||||
return resourcesSettled([
|
||||
this.state.trendingCommunitiesRes,
|
||||
this.props.dataType === DataType.Post
|
||||
? this.state.postsRes
|
||||
: this.state.commentsRes,
|
||||
]);
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
@ -334,8 +346,6 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
|||
) {
|
||||
await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
|
||||
}
|
||||
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
static async fetchInitialData({
|
||||
|
@ -667,11 +677,6 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
|||
search: getQueryString(queryParams),
|
||||
});
|
||||
|
||||
if (!this.state.scrolled) {
|
||||
this.setState({ scrolled: true });
|
||||
setTimeout(() => window.scrollTo(0, 0), 0);
|
||||
}
|
||||
|
||||
await this.fetchData();
|
||||
}
|
||||
|
||||
|
@ -852,8 +857,6 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
|||
}),
|
||||
});
|
||||
}
|
||||
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
handleShowSubscribedMobile(i: Home) {
|
||||
|
@ -876,27 +879,23 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
|||
this.props.history.back();
|
||||
// A hack to scroll to top
|
||||
setTimeout(() => {
|
||||
window.scrollTo(0, 0);
|
||||
snapToTop();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
handlePageNext(nextPage: PaginationCursor) {
|
||||
this.setState({ scrolled: false });
|
||||
this.updateUrl({ pageCursor: nextPage });
|
||||
}
|
||||
|
||||
handleSortChange(val: SortType) {
|
||||
this.setState({ scrolled: false });
|
||||
this.updateUrl({ sort: val, pageCursor: undefined });
|
||||
}
|
||||
|
||||
handleListingTypeChange(val: ListingType) {
|
||||
this.setState({ scrolled: false });
|
||||
this.updateUrl({ listingType: val, pageCursor: undefined });
|
||||
}
|
||||
|
||||
handleDataTypeChange(val: DataType) {
|
||||
this.setState({ scrolled: false });
|
||||
this.updateUrl({ dataType: val, pageCursor: undefined });
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ import Tabs from "../common/tabs";
|
|||
import { getHttpBaseInternal } from "../../utils/env";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { resourcesSettled } from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
|
||||
type InstancesData = RouteDataResponse<{
|
||||
federatedInstancesResponse: GetFederatedInstancesResponse;
|
||||
|
@ -43,6 +45,7 @@ export type InstancesFetchConfig = IRoutePropsWithFetch<
|
|||
Record<string, never>
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class Instances extends Component<InstancesRouteProps, InstancesState> {
|
||||
private isoData = setIsoData<InstancesData>(this.context);
|
||||
state: InstancesState = {
|
||||
|
@ -51,6 +54,10 @@ export class Instances extends Component<InstancesRouteProps, InstancesState> {
|
|||
isIsomorphic: false,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([this.state.instancesRes]);
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ import { HttpService, I18NextService } from "../../services";
|
|||
import { toast } from "../../toast";
|
||||
import { HtmlTags } from "../common/html-tags";
|
||||
import { Spinner } from "../common/icon";
|
||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
|
||||
interface State {
|
||||
form: {
|
||||
|
@ -15,7 +17,11 @@ interface State {
|
|||
siteRes: GetSiteResponse;
|
||||
}
|
||||
|
||||
export class LoginReset extends Component<any, State> {
|
||||
@simpleScrollMixin
|
||||
export class LoginReset extends Component<
|
||||
RouteComponentProps<Record<string, never>>,
|
||||
State
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
|
||||
state: State = {
|
||||
|
|
|
@ -19,6 +19,7 @@ import TotpModal from "../common/totp-modal";
|
|||
import { UnreadCounterService } from "../../services";
|
||||
import { RouteData } from "../../interfaces";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||
|
||||
interface LoginProps {
|
||||
prev?: string;
|
||||
|
@ -125,6 +126,7 @@ export type LoginFetchConfig = IRoutePropsWithFetch<
|
|||
LoginProps
|
||||
>;
|
||||
|
||||
@simpleScrollMixin
|
||||
export class Login extends Component<LoginRouteProps, State> {
|
||||
private isoData = setIsoData(this.context);
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ import {
|
|||
import { Spinner } from "../common/icon";
|
||||
import PasswordInput from "../common/password-input";
|
||||
import { SiteForm } from "./site-form";
|
||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
|
||||
interface State {
|
||||
form: {
|
||||
|
@ -36,7 +38,11 @@ interface State {
|
|||
siteRes: GetSiteResponse;
|
||||
}
|
||||
|
||||
export class Setup extends Component<any, State> {
|
||||
@simpleScrollMixin
|
||||
export class Setup extends Component<
|
||||
RouteComponentProps<Record<string, never>>,
|
||||
State
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
|
||||
state: State = {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { setIsoData } from "@utils/app";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
import { validEmail } from "@utils/helpers";
|
||||
import { resourcesSettled, validEmail } from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import { Component, linkEvent } from "inferno";
|
||||
import { T } from "inferno-i18next-dess";
|
||||
import {
|
||||
|
@ -24,6 +25,7 @@ import { HtmlTags } from "../common/html-tags";
|
|||
import { Icon, Spinner } from "../common/icon";
|
||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||
import PasswordInput from "../common/password-input";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
|
||||
interface State {
|
||||
registerRes: RequestState<LoginResponse>;
|
||||
|
@ -43,7 +45,11 @@ interface State {
|
|||
siteRes: GetSiteResponse;
|
||||
}
|
||||
|
||||
export class Signup extends Component<any, State> {
|
||||
@scrollMixin
|
||||
export class Signup extends Component<
|
||||
RouteComponentProps<Record<string, never>>,
|
||||
State
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private audio?: HTMLAudioElement;
|
||||
|
||||
|
@ -57,6 +63,13 @@ export class Signup extends Component<any, State> {
|
|||
siteRes: this.isoData.site_res,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return (
|
||||
!this.state.siteRes.site_view.local_site.captcha_enabled ||
|
||||
resourcesSettled([this.state.captchaRes])
|
||||
);
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Badges } from "../common/badges";
|
|||
import { BannerIconHeader } from "../common/banner-icon-header";
|
||||
import { Icon } from "../common/icon";
|
||||
import { PersonListing } from "../person/person-listing";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
|
||||
interface SiteSidebarProps {
|
||||
site: Site;
|
||||
|
@ -20,6 +21,7 @@ interface SiteSidebarState {
|
|||
collapsed: boolean;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
|
||||
state: SiteSidebarState = {
|
||||
collapsed: false,
|
||||
|
|
|
@ -4,6 +4,7 @@ import { EditSite, Tagline } from "lemmy-js-client";
|
|||
import { I18NextService } from "../../services";
|
||||
import { Icon, Spinner } from "../common/icon";
|
||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
|
||||
interface TaglineFormProps {
|
||||
taglines: Array<Tagline>;
|
||||
|
@ -16,6 +17,7 @@ interface TaglineFormState {
|
|||
editingRow?: number;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
||||
state: TaglineFormState = {
|
||||
editingRow: undefined,
|
||||
|
|
145
src/shared/components/mixins/scroll-mixin.ts
Normal file
145
src/shared/components/mixins/scroll-mixin.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { isBrowser, nextUserAction, snapToTop } from "../../utils/browser";
|
||||
import { Component, InfernoNode } from "inferno";
|
||||
import { Location } from "history";
|
||||
|
||||
function restoreScrollPosition(props: { location: Location }) {
|
||||
const key: string = props.location.key;
|
||||
const y = sessionStorage.getItem(`scrollPosition_${key}`);
|
||||
|
||||
if (y !== null) {
|
||||
window.scrollTo({ left: 0, top: Number(y), behavior: "instant" });
|
||||
}
|
||||
}
|
||||
|
||||
function saveScrollPosition(props: { location: Location }) {
|
||||
const key: string = props.location.key;
|
||||
|
||||
const y = window.scrollY;
|
||||
|
||||
sessionStorage.setItem(`scrollPosition_${key}`, y.toString());
|
||||
}
|
||||
|
||||
function dropScrollPosition(props: { location: Location }) {
|
||||
const key: string = props.location.key;
|
||||
sessionStorage.removeItem(`scrollPosition_${key}`);
|
||||
}
|
||||
|
||||
export function scrollMixin<
|
||||
P extends { location: Location },
|
||||
S,
|
||||
Base extends new (
|
||||
...args: any
|
||||
) => Component<P, S> & { loadingSettled(): boolean },
|
||||
>(base: Base, _context?: ClassDecoratorContext<Base>) {
|
||||
return class extends base {
|
||||
private stopUserListener: (() => void) | undefined;
|
||||
private blocked?: string;
|
||||
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
|
||||
if (!isBrowser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.restoreIfLoaded();
|
||||
return super.componentDidMount?.();
|
||||
}
|
||||
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<{ children?: InfernoNode } & P>,
|
||||
prevState: S,
|
||||
snapshot: any,
|
||||
) {
|
||||
this.restoreIfLoaded();
|
||||
return super.componentDidUpdate?.(prevProps, prevState, snapshot);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.saveFinalPosition();
|
||||
return super.componentWillUnmount?.();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(
|
||||
nextProps: Readonly<{ children?: InfernoNode } & P>,
|
||||
nextContext: any,
|
||||
) {
|
||||
// Currently this is hypothetical. Components unmount before route changes.
|
||||
if (this.props.location.key !== nextProps.location.key) {
|
||||
this.saveFinalPosition();
|
||||
this.reset();
|
||||
}
|
||||
return super.componentWillReceiveProps?.(nextProps, nextContext);
|
||||
}
|
||||
|
||||
unloadListeners = () => {
|
||||
// Browsers restore the position after reload, but not after pressing
|
||||
// Enter in the url bar. It's hard to distinguish the two, let the
|
||||
// browser do its thing.
|
||||
window.history.scrollRestoration = "auto";
|
||||
dropScrollPosition(this.props);
|
||||
};
|
||||
|
||||
reset() {
|
||||
this.blocked = undefined;
|
||||
this.stopUserListener?.();
|
||||
// While inferno is rendering no events are dispatched. This only catches
|
||||
// user interactions when network responses are slow/late.
|
||||
this.stopUserListener = nextUserAction(() => {
|
||||
this.preventRestore();
|
||||
});
|
||||
window.removeEventListener("beforeunload", this.unloadListeners);
|
||||
window.addEventListener("beforeunload", this.unloadListeners);
|
||||
}
|
||||
|
||||
savePosition() {
|
||||
saveScrollPosition(this.props);
|
||||
}
|
||||
|
||||
saveFinalPosition() {
|
||||
this.savePosition();
|
||||
snapToTop();
|
||||
window.removeEventListener("beforeunload", this.unloadListeners);
|
||||
}
|
||||
|
||||
preventRestore() {
|
||||
this.blocked = this.props.location.key;
|
||||
this.stopUserListener?.();
|
||||
this.stopUserListener = undefined;
|
||||
}
|
||||
|
||||
restore() {
|
||||
restoreScrollPosition(this.props);
|
||||
this.preventRestore();
|
||||
}
|
||||
|
||||
restoreIfLoaded() {
|
||||
if (!this.isPending() || !this.loadingSettled()) {
|
||||
return;
|
||||
}
|
||||
this.restore();
|
||||
}
|
||||
|
||||
isPending() {
|
||||
return this.blocked !== this.props.location.key;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function simpleScrollMixin<
|
||||
P extends { location: Location },
|
||||
S,
|
||||
Base extends new (...args: any) => Component<P, S>,
|
||||
>(base: Base, _context?: ClassDecoratorContext<Base>) {
|
||||
@scrollMixin
|
||||
class SimpleScrollMixin extends base {
|
||||
loadingSettled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return SimpleScrollMixin;
|
||||
}
|
25
src/shared/components/mixins/tippy-mixin.ts
Normal file
25
src/shared/components/mixins/tippy-mixin.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Component, InfernoNode } from "inferno";
|
||||
import { cleanupTippy } from "../../tippy";
|
||||
|
||||
export function tippyMixin<
|
||||
P,
|
||||
S,
|
||||
Base extends new (...args: any) => Component<P, S>,
|
||||
>(base: Base, _context?: ClassDecoratorContext<Base>) {
|
||||
return class extends base {
|
||||
componentDidUpdate(
|
||||
prevProps: P & { children?: InfernoNode },
|
||||
prevState: S,
|
||||
snapshot: any,
|
||||
) {
|
||||
// For conditional rendering, old tippy instances aren't reused
|
||||
cleanupTippy();
|
||||
return super.componentDidUpdate?.(prevProps, prevState, snapshot);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
cleanupTippy();
|
||||
return super.componentWillUnmount?.();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -11,7 +11,9 @@ import {
|
|||
getPageFromString,
|
||||
getQueryParams,
|
||||
getQueryString,
|
||||
resourcesSettled,
|
||||
} from "@utils/helpers";
|
||||
import { scrollMixin } from "./mixins/scroll-mixin";
|
||||
import { amAdmin, amMod } from "@utils/roles";
|
||||
import type { QueryParams } from "@utils/types";
|
||||
import { Choice, RouteDataResponse } from "@utils/types";
|
||||
|
@ -645,6 +647,7 @@ export type ModlogFetchConfig = IRoutePropsWithFetch<
|
|||
ModlogProps
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class Modlog extends Component<ModlogRouteProps, ModlogState> {
|
||||
private isoData = setIsoData<ModlogData>(this.context);
|
||||
|
||||
|
@ -658,6 +661,10 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
|
|||
isIsomorphic: false,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([this.state.res]);
|
||||
}
|
||||
|
||||
constructor(props: ModlogRouteProps, context: any) {
|
||||
super(props, context);
|
||||
this.handlePageChange = this.handlePageChange.bind(this);
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { Component } from "inferno";
|
||||
import { I18NextService } from "../../services";
|
||||
import { Icon } from "../common/icon";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
|
||||
interface CakeDayProps {
|
||||
creatorName: string;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class CakeDay extends Component<CakeDayProps, any> {
|
||||
render() {
|
||||
return (
|
||||
|
|
|
@ -10,7 +10,12 @@ import {
|
|||
setIsoData,
|
||||
updatePersonBlock,
|
||||
} from "@utils/app";
|
||||
import { capitalizeFirstLetter, randomStr } from "@utils/helpers";
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
randomStr,
|
||||
resourcesSettled,
|
||||
} from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
import classNames from "classnames";
|
||||
import { Component, linkEvent } from "inferno";
|
||||
|
@ -137,6 +142,7 @@ export type InboxFetchConfig = IRoutePropsWithFetch<
|
|||
Record<string, never>
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class Inbox extends Component<InboxRouteProps, InboxState> {
|
||||
private isoData = setIsoData<InboxData>(this.context);
|
||||
state: InboxState = {
|
||||
|
@ -153,6 +159,14 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
|
|||
isIsomorphic: false,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([
|
||||
this.state.repliesRes,
|
||||
this.state.mentionsRes,
|
||||
this.state.messagesRes,
|
||||
]);
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ import { HtmlTags } from "../common/html-tags";
|
|||
import { Spinner } from "../common/icon";
|
||||
import PasswordInput from "../common/password-input";
|
||||
import { toast } from "../../toast";
|
||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
|
||||
interface State {
|
||||
passwordChangeRes: RequestState<SuccessResponse>;
|
||||
|
@ -23,7 +25,11 @@ interface State {
|
|||
siteRes: GetSiteResponse;
|
||||
}
|
||||
|
||||
export class PasswordChange extends Component<any, State> {
|
||||
@simpleScrollMixin
|
||||
export class PasswordChange extends Component<
|
||||
RouteComponentProps<Record<string, never>>,
|
||||
State
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
|
||||
state: State = {
|
||||
|
|
|
@ -41,7 +41,6 @@ import {
|
|||
TransferCommunity,
|
||||
} from "lemmy-js-client";
|
||||
import { CommentViewType, PersonDetailsView } from "../../interfaces";
|
||||
import { setupTippy } from "../../tippy";
|
||||
import { CommentNodes } from "../comment/comment-nodes";
|
||||
import { Paginator } from "../common/paginator";
|
||||
import { PostListing } from "../post/post-listing";
|
||||
|
@ -109,10 +108,6 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
|
|||
this.handlePageChange = this.handlePageChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="person-details">
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
setIsoData,
|
||||
updatePersonBlock,
|
||||
} from "@utils/app";
|
||||
import { restoreScrollPosition, saveScrollPosition } from "@utils/browser";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
futureDaysToUnixTime,
|
||||
|
@ -17,6 +17,7 @@ import {
|
|||
getQueryString,
|
||||
numToSI,
|
||||
randomStr,
|
||||
resourcesSettled,
|
||||
} from "@utils/helpers";
|
||||
import { canMod, isBanned } from "@utils/roles";
|
||||
import type { QueryParams } from "@utils/types";
|
||||
|
@ -82,7 +83,6 @@ import {
|
|||
RequestState,
|
||||
wrapClient,
|
||||
} from "../../services/HttpService";
|
||||
import { setupTippy } from "../../tippy";
|
||||
import { toast } from "../../toast";
|
||||
import { BannerIconHeader } from "../common/banner-icon-header";
|
||||
import { HtmlTags } from "../common/html-tags";
|
||||
|
@ -183,6 +183,7 @@ export type ProfileFetchConfig = IRoutePropsWithFetch<
|
|||
ProfileProps
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||
private isoData = setIsoData<ProfileData>(this.context);
|
||||
state: ProfileState = {
|
||||
|
@ -195,6 +196,10 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
isIsomorphic: false,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([this.state.personRes]);
|
||||
}
|
||||
|
||||
constructor(props: ProfileRouteProps, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
@ -249,11 +254,6 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
if (!this.state.isIsomorphic) {
|
||||
await this.fetchUserData();
|
||||
}
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
saveScrollPosition(this.context);
|
||||
}
|
||||
|
||||
async fetchUserData() {
|
||||
|
@ -271,7 +271,6 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
personRes,
|
||||
personBlocked: isPersonBlocked(personRes),
|
||||
});
|
||||
restoreScrollPosition(this.context);
|
||||
}
|
||||
|
||||
get amCurrentUser() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { editRegistrationApplication, setIsoData } from "@utils/app";
|
||||
import { randomStr } from "@utils/helpers";
|
||||
import { randomStr, resourcesSettled } from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
import classNames from "classnames";
|
||||
import { Component, linkEvent } from "inferno";
|
||||
|
@ -20,7 +21,6 @@ import {
|
|||
RequestState,
|
||||
wrapClient,
|
||||
} from "../../services/HttpService";
|
||||
import { setupTippy } from "../../tippy";
|
||||
import { HtmlTags } from "../common/html-tags";
|
||||
import { Spinner } from "../common/icon";
|
||||
import { Paginator } from "../common/paginator";
|
||||
|
@ -58,6 +58,7 @@ export type RegistrationApplicationsFetchConfig = IRoutePropsWithFetch<
|
|||
Record<string, never>
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class RegistrationApplications extends Component<
|
||||
RegistrationApplicationsRouteProps,
|
||||
RegistrationApplicationsState
|
||||
|
@ -71,6 +72,10 @@ export class RegistrationApplications extends Component<
|
|||
isIsomorphic: false,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([this.state.appsRes]);
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
@ -91,7 +96,6 @@ export class RegistrationApplications extends Component<
|
|||
if (!this.state.isIsomorphic) {
|
||||
await this.refetch();
|
||||
}
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
get documentTitle(): string {
|
||||
|
|
|
@ -4,7 +4,8 @@ import {
|
|||
editPrivateMessageReport,
|
||||
setIsoData,
|
||||
} from "@utils/app";
|
||||
import { randomStr } from "@utils/helpers";
|
||||
import { randomStr, resourcesSettled } from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import { amAdmin } from "@utils/roles";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
import classNames from "classnames";
|
||||
|
@ -103,6 +104,7 @@ export type ReportsFetchConfig = IRoutePropsWithFetch<
|
|||
Record<string, never>
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class Reports extends Component<ReportsRouteProps, ReportsState> {
|
||||
private isoData = setIsoData<ReportsData>(this.context);
|
||||
state: ReportsState = {
|
||||
|
@ -116,6 +118,14 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
|
|||
isIsomorphic: false,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([
|
||||
this.state.commentReportsRes,
|
||||
this.state.postReportsRes,
|
||||
this.state.messageReportsRes,
|
||||
]);
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ import {
|
|||
languages,
|
||||
loadUserLanguage,
|
||||
} from "../../services/I18NextService";
|
||||
import { setupTippy } from "../../tippy";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
import { toast } from "../../toast";
|
||||
import { HtmlTags } from "../common/html-tags";
|
||||
import { Icon, Spinner } from "../common/icon";
|
||||
|
@ -66,10 +66,11 @@ import { PersonListing } from "./person-listing";
|
|||
import { InitialFetchRequest } from "../../interfaces";
|
||||
import TotpModal from "../common/totp-modal";
|
||||
import { LoadingEllipses } from "../common/loading-ellipses";
|
||||
import { refreshTheme, setThemeOverride } from "../../utils/browser";
|
||||
import { refreshTheme, setThemeOverride, snapToTop } from "../../utils/browser";
|
||||
import { getHttpBaseInternal } from "../../utils/env";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||
|
||||
type SettingsData = RouteDataResponse<{
|
||||
instancesRes: GetFederatedInstancesResponse;
|
||||
|
@ -203,6 +204,8 @@ export type SettingsFetchConfig = IRoutePropsWithFetch<
|
|||
Record<string, never>
|
||||
>;
|
||||
|
||||
@simpleScrollMixin
|
||||
@tippyMixin
|
||||
export class Settings extends Component<SettingsRouteProps, SettingsState> {
|
||||
private isoData = setIsoData<SettingsData>(this.context);
|
||||
exportSettingsLink = createRef<HTMLAnchorElement>();
|
||||
|
@ -334,7 +337,6 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
setupTippy();
|
||||
this.setState({ themeList: await fetchThemeList() });
|
||||
|
||||
if (!this.state.isIsomorphic) {
|
||||
|
@ -1578,7 +1580,7 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
|
|||
}
|
||||
|
||||
toast(I18NextService.i18n.t("saved"));
|
||||
window.scrollTo(0, 0);
|
||||
snapToTop();
|
||||
}
|
||||
|
||||
setThemeOverride(undefined);
|
||||
|
@ -1598,7 +1600,7 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
|
|||
old_password,
|
||||
});
|
||||
if (changePasswordRes.state === "success") {
|
||||
window.scrollTo(0, 0);
|
||||
snapToTop();
|
||||
toast(I18NextService.i18n.t("password_changed"));
|
||||
}
|
||||
|
||||
|
|
|
@ -11,13 +11,19 @@ import {
|
|||
import { toast } from "../../toast";
|
||||
import { HtmlTags } from "../common/html-tags";
|
||||
import { Spinner } from "../common/icon";
|
||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
|
||||
interface State {
|
||||
verifyRes: RequestState<SuccessResponse>;
|
||||
siteRes: GetSiteResponse;
|
||||
}
|
||||
|
||||
export class VerifyEmail extends Component<any, State> {
|
||||
@simpleScrollMixin
|
||||
export class VerifyEmail extends Component<
|
||||
RouteComponentProps<Record<string, never>>,
|
||||
State
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
|
||||
state: State = {
|
||||
|
|
|
@ -30,6 +30,7 @@ import { Spinner } from "../common/icon";
|
|||
import { PostForm } from "./post-form";
|
||||
import { getHttpBaseInternal } from "../../utils/env";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||
|
||||
export interface CreatePostProps {
|
||||
communityId?: number;
|
||||
|
@ -70,6 +71,7 @@ export type CreatePostFetchConfig = IRoutePropsWithFetch<
|
|||
CreatePostProps
|
||||
>;
|
||||
|
||||
@simpleScrollMixin
|
||||
export class CreatePost extends Component<
|
||||
CreatePostRouteProps,
|
||||
CreatePostState
|
||||
|
|
|
@ -37,7 +37,6 @@ import {
|
|||
LOADING_REQUEST,
|
||||
RequestState,
|
||||
} from "../../services/HttpService";
|
||||
import { setupTippy } from "../../tippy";
|
||||
import { toast } from "../../toast";
|
||||
import { Icon, Spinner } from "../common/icon";
|
||||
import { LanguageSelect } from "../common/language-select";
|
||||
|
@ -306,7 +305,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
setupTippy();
|
||||
const textarea: any = document.getElementById("post-title");
|
||||
|
||||
if (textarea) {
|
||||
|
|
|
@ -35,7 +35,7 @@ import { relTags } from "../../config";
|
|||
import { VoteContentType } from "../../interfaces";
|
||||
import { mdToHtml, mdToHtmlInline } from "../../markdown";
|
||||
import { I18NextService, UserService } from "../../services";
|
||||
import { setupTippy } from "../../tippy";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
import { Icon } from "../common/icon";
|
||||
import { MomentTime } from "../common/moment-time";
|
||||
import { PictrsImage } from "../common/pictrs-image";
|
||||
|
@ -91,8 +91,10 @@ interface PostListingProps {
|
|||
onAddAdmin(form: AddAdmin): Promise<void>;
|
||||
onTransferCommunity(form: TransferCommunity): Promise<void>;
|
||||
onMarkPostAsRead(form: MarkPostAsRead): void;
|
||||
onScrollIntoCommentsClick?(e: MouseEvent): void;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||
state: PostListingState = {
|
||||
showEdit: false,
|
||||
|
@ -636,6 +638,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
title={title}
|
||||
to={`/post/${pv.post.id}?scrollToComments=true`}
|
||||
data-tippy-content={title}
|
||||
onClick={this.props.onScrollIntoCommentsClick}
|
||||
>
|
||||
<Icon icon="message-square" classes="me-1" inline />
|
||||
{pv.counts.comments}
|
||||
|
@ -982,7 +985,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
handleImageExpandClick(i: PostListing, event: any) {
|
||||
event.preventDefault();
|
||||
i.setState({ imageExpanded: !i.state.imageExpanded });
|
||||
setupTippy();
|
||||
|
||||
if (myAuth() && !i.postView.read) {
|
||||
i.props.onMarkPostAsRead({
|
||||
|
@ -998,7 +1000,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
handleShowBody(i: PostListing) {
|
||||
i.setState({ showBody: !i.state.showBody });
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
get pointsTippy(): string {
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Icon, Spinner } from "../common/icon";
|
|||
import { PersonListing } from "../person/person-listing";
|
||||
import { PostListing } from "./post-listing";
|
||||
import { EMPTY_REQUEST } from "../../services/HttpService";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
|
||||
interface PostReportProps {
|
||||
report: PostReportView;
|
||||
|
@ -16,6 +17,7 @@ interface PostReportState {
|
|||
loading: boolean;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class PostReport extends Component<PostReportProps, PostReportState> {
|
||||
state: PostReportState = {
|
||||
loading: false,
|
||||
|
|
|
@ -13,12 +13,14 @@ import {
|
|||
updateCommunityBlock,
|
||||
updatePersonBlock,
|
||||
} from "@utils/app";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
import {
|
||||
isBrowser,
|
||||
restoreScrollPosition,
|
||||
saveScrollPosition,
|
||||
} from "@utils/browser";
|
||||
import { debounce, getApubName, randomStr } from "@utils/helpers";
|
||||
debounce,
|
||||
getApubName,
|
||||
randomStr,
|
||||
resourcesSettled,
|
||||
} from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import { isImage } from "@utils/media";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
import autosize from "autosize";
|
||||
|
@ -89,7 +91,6 @@ import {
|
|||
RequestState,
|
||||
wrapClient,
|
||||
} from "../../services/HttpService";
|
||||
import { setupTippy } from "../../tippy";
|
||||
import { toast } from "../../toast";
|
||||
import { CommentForm } from "../comment/comment-form";
|
||||
import { CommentNodes } from "../comment/comment-nodes";
|
||||
|
@ -135,6 +136,7 @@ export type PostFetchConfig = IRoutePropsWithFetch<
|
|||
Record<string, never>
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class Post extends Component<PostRouteProps, PostState> {
|
||||
private isoData = setIsoData<PostData>(this.context);
|
||||
private commentScrollDebounced: () => void;
|
||||
|
@ -153,6 +155,10 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
isIsomorphic: false,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([this.state.postRes, this.state.commentsRes]);
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
@ -189,6 +195,8 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
this.handleSavePost = this.handleSavePost.bind(this);
|
||||
this.handlePurgePost = this.handlePurgePost.bind(this);
|
||||
this.handleFeaturePost = this.handleFeaturePost.bind(this);
|
||||
this.handleScrollIntoCommentsClick =
|
||||
this.handleScrollIntoCommentsClick.bind(this);
|
||||
|
||||
this.state = { ...this.state, commentSectionRef: createRef() };
|
||||
|
||||
|
@ -237,10 +245,6 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
commentsRes,
|
||||
});
|
||||
|
||||
setupTippy();
|
||||
|
||||
if (!this.state.commentId) restoreScrollPosition(this.context);
|
||||
|
||||
if (this.checkScrollIntoCommentsParam) {
|
||||
this.scrollIntoCommentSection();
|
||||
}
|
||||
|
@ -284,8 +288,6 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("scroll", this.commentScrollDebounced);
|
||||
|
||||
saveScrollPosition(this.context);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
|
@ -299,16 +301,16 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
document.addEventListener("scroll", this.commentScrollDebounced);
|
||||
}
|
||||
|
||||
async componentDidUpdate(_lastProps: any) {
|
||||
// Necessary if you are on a post and you click another post (same route)
|
||||
if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
|
||||
await this.fetchPost();
|
||||
}
|
||||
handleScrollIntoCommentsClick(e: MouseEvent) {
|
||||
this.scrollIntoCommentSection();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
get checkScrollIntoCommentsParam() {
|
||||
return Boolean(
|
||||
new URLSearchParams(this.props.location.search).get("scrollToComments"),
|
||||
return (
|
||||
Boolean(
|
||||
new URLSearchParams(this.props.location.search).get("scrollToComments"),
|
||||
) && this.props.history.action !== "POP"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -403,6 +405,7 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
onTransferCommunity={this.handleTransferCommunity}
|
||||
onFeaturePost={this.handleFeaturePost}
|
||||
onMarkPostAsRead={() => {}}
|
||||
onScrollIntoCommentsClick={this.handleScrollIntoCommentsClick}
|
||||
/>
|
||||
<div ref={this.state.commentSectionRef} className="mb-2" />
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ import { PrivateMessageForm } from "./private-message-form";
|
|||
import { getHttpBaseInternal } from "../../utils/env";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { resourcesSettled } from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
|
||||
type CreatePrivateMessageData = RouteDataResponse<{
|
||||
recipientDetailsResponse: GetPersonDetailsResponse;
|
||||
|
@ -45,6 +47,7 @@ export type CreatePrivateMessageFetchConfig = IRoutePropsWithFetch<
|
|||
Record<string, never>
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class CreatePrivateMessage extends Component<
|
||||
CreatePrivateMessageRouteProps,
|
||||
CreatePrivateMessageState
|
||||
|
@ -57,6 +60,10 @@ export class CreatePrivateMessage extends Component<
|
|||
isIsomorphic: false,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([this.state.recipientRes]);
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.handlePrivateMessageCreate =
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
} from "lemmy-js-client";
|
||||
import { relTags } from "../../config";
|
||||
import { I18NextService } from "../../services";
|
||||
import { setupTippy } from "../../tippy";
|
||||
import { Icon } from "../common/icon";
|
||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||
import { PersonListing } from "../person/person-listing";
|
||||
|
@ -50,10 +49,6 @@ export class PrivateMessageForm extends Component<
|
|||
this.handleContentChange = this.handleContentChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(
|
||||
nextProps: Readonly<{ children?: InfernoNode } & PrivateMessageFormProps>,
|
||||
): void {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { mdToHtml } from "../../markdown";
|
|||
import { I18NextService } from "../../services";
|
||||
import { Icon, Spinner } from "../common/icon";
|
||||
import { PersonListing } from "../person/person-listing";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
|
||||
interface Props {
|
||||
report: PrivateMessageReportView;
|
||||
|
@ -18,6 +19,7 @@ interface State {
|
|||
loading: boolean;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class PrivateMessageReport extends Component<Props, State> {
|
||||
state: State = {
|
||||
loading: false,
|
||||
|
|
|
@ -15,6 +15,7 @@ import { MomentTime } from "../common/moment-time";
|
|||
import { PersonListing } from "../person/person-listing";
|
||||
import { PrivateMessageForm } from "./private-message-form";
|
||||
import ModActionFormModal from "../common/mod-action-form-modal";
|
||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||
|
||||
interface PrivateMessageState {
|
||||
showReply: boolean;
|
||||
|
@ -35,6 +36,7 @@ interface PrivateMessageProps {
|
|||
onEdit(form: EditPrivateMessage): void;
|
||||
}
|
||||
|
||||
@tippyMixin
|
||||
export class PrivateMessage extends Component<
|
||||
PrivateMessageProps,
|
||||
PrivateMessageState
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { setIsoData } from "@utils/app";
|
||||
import { getQueryParams } from "@utils/helpers";
|
||||
import { getQueryParams, resourcesSettled } from "@utils/helpers";
|
||||
import { scrollMixin } from "./mixins/scroll-mixin";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
import { Component, linkEvent } from "inferno";
|
||||
import {
|
||||
|
@ -97,6 +98,7 @@ export type RemoteFetchFetchConfig = IRoutePropsWithFetch<
|
|||
RemoteFetchProps
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class RemoteFetch extends Component<
|
||||
RemoteFetchRouteProps,
|
||||
RemoteFetchState
|
||||
|
@ -108,6 +110,10 @@ export class RemoteFetch extends Component<
|
|||
followCommunityLoading: false,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([this.state.resolveObjectRes]);
|
||||
}
|
||||
|
||||
constructor(props: RemoteFetchRouteProps, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
setIsoData,
|
||||
showLocal,
|
||||
} from "@utils/app";
|
||||
import { restoreScrollPosition, saveScrollPosition } from "@utils/browser";
|
||||
import { scrollMixin } from "./mixins/scroll-mixin";
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
debounce,
|
||||
|
@ -21,6 +21,7 @@ import {
|
|||
getQueryParams,
|
||||
getQueryString,
|
||||
numToSI,
|
||||
resourcesSettled,
|
||||
} from "@utils/helpers";
|
||||
import type { QueryParams } from "@utils/types";
|
||||
import { Choice, RouteDataResponse } from "@utils/types";
|
||||
|
@ -253,6 +254,7 @@ export type SearchFetchConfig = IRoutePropsWithFetch<
|
|||
SearchProps
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class Search extends Component<SearchRouteProps, SearchState> {
|
||||
private isoData = setIsoData<SearchData>(this.context);
|
||||
searchInput = createRef<HTMLInputElement>();
|
||||
|
@ -268,6 +270,10 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
isIsomorphic: false,
|
||||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([this.state.searchRes]);
|
||||
}
|
||||
|
||||
constructor(props: SearchRouteProps, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
@ -323,7 +329,9 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.searchInput.current?.select();
|
||||
if (this.props.history.action !== "POP") {
|
||||
this.searchInput.current?.select();
|
||||
}
|
||||
|
||||
if (!this.state.isIsomorphic) {
|
||||
this.setState({
|
||||
|
@ -397,10 +405,6 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
saveScrollPosition(this.context);
|
||||
}
|
||||
|
||||
static async fetchInitialData({
|
||||
headers,
|
||||
query: {
|
||||
|
@ -991,8 +995,6 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
limit: fetchLimit,
|
||||
}),
|
||||
});
|
||||
window.scrollTo(0, 0);
|
||||
restoreScrollPosition(this.context);
|
||||
|
||||
if (myAuth()) {
|
||||
this.setState({ resolveObjectRes: LOADING_REQUEST });
|
||||
|
|
|
@ -1,19 +1,55 @@
|
|||
import { isBrowser } from "@utils/browser";
|
||||
import tippy from "tippy.js";
|
||||
import { RefObject } from "inferno";
|
||||
import {
|
||||
DelegateInstance as TippyDelegateInstance,
|
||||
Props as TippyProps,
|
||||
Instance as TippyInstance,
|
||||
delegate as tippyDelegate,
|
||||
} from "tippy.js";
|
||||
|
||||
export let tippyInstance: any;
|
||||
let instance: TippyDelegateInstance<TippyProps> | undefined;
|
||||
const tippySelector = "[data-tippy-content]";
|
||||
const shownInstances: Set<TippyInstance<TippyProps>> = new Set();
|
||||
|
||||
if (isBrowser()) {
|
||||
tippyInstance = tippy("[data-tippy-content]");
|
||||
}
|
||||
const tippyDelegateOptions: Partial<TippyProps> & { target: string } = {
|
||||
delay: [500, 0],
|
||||
// Display on "long press"
|
||||
touch: ["hold", 500],
|
||||
target: tippySelector,
|
||||
onShow(i: TippyInstance<TippyProps>) {
|
||||
shownInstances.add(i);
|
||||
},
|
||||
onHidden(i: TippyInstance<TippyProps>) {
|
||||
shownInstances.delete(i);
|
||||
},
|
||||
};
|
||||
|
||||
export function setupTippy() {
|
||||
if (isBrowser()) {
|
||||
tippyInstance.forEach((e: any) => e.destroy());
|
||||
tippyInstance = tippy("[data-tippy-content]", {
|
||||
delay: [500, 0],
|
||||
// Display on "long press"
|
||||
touch: ["hold", 500],
|
||||
});
|
||||
export function setupTippy(root: RefObject<Element>) {
|
||||
if (!instance && root.current) {
|
||||
instance = tippyDelegate(root.current, tippyDelegateOptions);
|
||||
}
|
||||
}
|
||||
|
||||
let requested = false;
|
||||
export function cleanupTippy() {
|
||||
if (requested) {
|
||||
return;
|
||||
}
|
||||
requested = true;
|
||||
queueMicrotask(() => {
|
||||
requested = false;
|
||||
if (shownInstances.size) {
|
||||
// Avoid randomly closing tooltips.
|
||||
return;
|
||||
}
|
||||
// delegate from tippy.js creates tippy instances when needed, but only
|
||||
// destroys them when the delegate instance is destroyed.
|
||||
const current = instance?.reference ?? null;
|
||||
destroyTippy();
|
||||
setupTippy({ current });
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyTippy() {
|
||||
instance?.destroy();
|
||||
instance = undefined;
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@ import clearAuthCookie from "./clear-auth-cookie";
|
|||
import dataBsTheme from "./data-bs-theme";
|
||||
import isBrowser from "./is-browser";
|
||||
import isDark from "./is-dark";
|
||||
import nextUserAction from "./next-user-action";
|
||||
import platform from "./platform";
|
||||
import refreshTheme from "./refresh-theme";
|
||||
import restoreScrollPosition from "./restore-scroll-position";
|
||||
import saveScrollPosition from "./save-scroll-position";
|
||||
import setAuthCookie from "./set-auth-cookie";
|
||||
import setThemeOverride from "./set-theme-override";
|
||||
import share from "./share";
|
||||
import snapToTop from "./snap-to-top";
|
||||
|
||||
export {
|
||||
canShare,
|
||||
|
@ -17,11 +17,11 @@ export {
|
|||
dataBsTheme,
|
||||
isBrowser,
|
||||
isDark,
|
||||
nextUserAction,
|
||||
platform,
|
||||
refreshTheme,
|
||||
restoreScrollPosition,
|
||||
saveScrollPosition,
|
||||
setAuthCookie,
|
||||
setThemeOverride,
|
||||
share,
|
||||
snapToTop,
|
||||
};
|
||||
|
|
46
src/shared/utils/browser/next-user-action.ts
Normal file
46
src/shared/utils/browser/next-user-action.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
const eventTypes = ["mousedown", "keydown", "touchstart", "touchmove", "wheel"];
|
||||
|
||||
const scrollThreshold = 2;
|
||||
|
||||
type Continue = boolean | void;
|
||||
|
||||
export default function nextUserAction(cb: (e: Event) => Continue) {
|
||||
const eventTarget = window.document.body;
|
||||
|
||||
let cleanup: (() => void) | undefined = () => {
|
||||
cleanup = undefined;
|
||||
eventTypes.forEach(ev => {
|
||||
eventTarget.removeEventListener(ev, listener);
|
||||
});
|
||||
window.removeEventListener("scroll", scrollListener);
|
||||
};
|
||||
|
||||
const listener = (e: Event) => {
|
||||
if (!cb(e)) {
|
||||
cleanup?.();
|
||||
}
|
||||
};
|
||||
eventTypes.forEach(ev => {
|
||||
eventTarget.addEventListener(ev, listener);
|
||||
});
|
||||
|
||||
let remaining = scrollThreshold;
|
||||
const scrollListener = (e: Event) => {
|
||||
// This only has to cover the scrollbars. The problem is that scroll events
|
||||
// are also fired when the document height shrinks below the current bottom
|
||||
// edge of the window.
|
||||
remaining--;
|
||||
if (remaining < 0) {
|
||||
if (!cb(e)) {
|
||||
cleanup?.();
|
||||
} else {
|
||||
remaining = scrollThreshold;
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", scrollListener);
|
||||
|
||||
return () => {
|
||||
cleanup?.();
|
||||
};
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
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,6 +0,0 @@
|
|||
export default function saveScrollPosition(context: any) {
|
||||
const path: string = context.router.route.location.pathname;
|
||||
const y = window.scrollY;
|
||||
|
||||
sessionStorage.setItem(`scrollPosition_${path}`, y.toString());
|
||||
}
|
3
src/shared/utils/browser/snap-to-top.ts
Normal file
3
src/shared/utils/browser/snap-to-top.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function snapToTop() {
|
||||
window.scrollTo({ left: 0, top: 0, behavior: "instant" });
|
||||
}
|
|
@ -17,6 +17,7 @@ import isCakeDay from "./is-cake-day";
|
|||
import numToSI from "./num-to-si";
|
||||
import poll from "./poll";
|
||||
import randomStr from "./random-str";
|
||||
import resourcesSettled from "./resources-settled";
|
||||
import sleep from "./sleep";
|
||||
import validEmail from "./valid-email";
|
||||
import validInstanceTLD from "./valid-instance-tld";
|
||||
|
@ -45,6 +46,7 @@ export {
|
|||
numToSI,
|
||||
poll,
|
||||
randomStr,
|
||||
resourcesSettled,
|
||||
sleep,
|
||||
validEmail,
|
||||
validInstanceTLD,
|
||||
|
|
5
src/shared/utils/helpers/resources-settled.ts
Normal file
5
src/shared/utils/helpers/resources-settled.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { RequestState } from "../../services/HttpService";
|
||||
|
||||
export default function resourcesSettled(resources: RequestState<any>[]) {
|
||||
return resources.every(r => r.state === "success" || r.state === "failed");
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
"skipLibCheck": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"experimentalDecorators": true,
|
||||
"experimentalDecorators": true, // false for non-legacy decorators
|
||||
"strictNullChecks": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
|
|
Loading…
Reference in a new issue