mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-25 05:41:13 +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
5
.babelrc
5
.babelrc
|
@ -13,7 +13,12 @@
|
||||||
["@babel/typescript", { "isTSX": true, "allExtensions": true }]
|
["@babel/typescript", { "isTSX": true, "allExtensions": true }]
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
["@babel/plugin-proposal-decorators", { "version": "legacy" }],
|
||||||
|
[
|
||||||
"@babel/plugin-transform-runtime",
|
"@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-inferno", { "imports": true }],
|
||||||
["@babel/plugin-transform-class-properties", { "loose": true }]
|
["@babel/plugin-transform-class-properties", { "loose": true }]
|
||||||
]
|
]
|
||||||
|
|
|
@ -21,6 +21,10 @@
|
||||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||||
"@typescript-eslint/no-empty-function": 0,
|
"@typescript-eslint/no-empty-function": 0,
|
||||||
"@typescript-eslint/no-non-null-assertion": 0,
|
"@typescript-eslint/no-non-null-assertion": 0,
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ "argsIgnorePattern": "^_" }
|
||||||
|
],
|
||||||
"arrow-body-style": 0,
|
"arrow-body-style": 0,
|
||||||
"curly": 0,
|
"curly": 0,
|
||||||
"eol-last": 0,
|
"eol-last": 0,
|
||||||
|
|
|
@ -16,6 +16,8 @@ async function startClient() {
|
||||||
verifyDynamicImports(true).then(x => console.log(x));
|
verifyDynamicImports(true).then(x => console.log(x));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.history.scrollRestoration = "manual";
|
||||||
|
|
||||||
initializeSite(window.isoData.site_res);
|
initializeSite(window.isoData.site_res);
|
||||||
|
|
||||||
lazyHighlightjs.enableLazyLoading();
|
lazyHighlightjs.enableLazyLoading();
|
||||||
|
|
|
@ -13,15 +13,25 @@ import { Navbar } from "./navbar";
|
||||||
import "./styles.scss";
|
import "./styles.scss";
|
||||||
import { Theme } from "./theme";
|
import { Theme } from "./theme";
|
||||||
import AnonymousGuard from "../common/anonymous-guard";
|
import AnonymousGuard from "../common/anonymous-guard";
|
||||||
|
import { destroyTippy, setupTippy } from "../../tippy";
|
||||||
|
|
||||||
export class App extends Component<any, any> {
|
export class App extends Component<any, any> {
|
||||||
private isoData: IsoDataOptionalSite = setIsoData(this.context);
|
private isoData: IsoDataOptionalSite = setIsoData(this.context);
|
||||||
private readonly mainContentRef: RefObject<HTMLElement>;
|
private readonly mainContentRef: RefObject<HTMLElement>;
|
||||||
|
private readonly rootRef = createRef<HTMLDivElement>();
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.mainContentRef = createRef();
|
this.mainContentRef = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
setupTippy(this.rootRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
destroyTippy();
|
||||||
|
}
|
||||||
|
|
||||||
handleJumpToContent(event) {
|
handleJumpToContent(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.mainContentRef.current?.focus();
|
this.mainContentRef.current?.focus();
|
||||||
|
@ -34,7 +44,7 @@ export class App extends Component<any, any> {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Provider i18next={I18NextService.i18n}>
|
<Provider i18next={I18NextService.i18n}>
|
||||||
<div id="app" className="lemmy-site">
|
<div id="app" className="lemmy-site" ref={this.rootRef}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn skip-link bg-light position-absolute start-0 z-3"
|
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 { Icon } from "../common/icon";
|
||||||
import { PictrsImage } from "../common/pictrs-image";
|
import { PictrsImage } from "../common/pictrs-image";
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from "rxjs";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
siteRes?: GetSiteResponse;
|
siteRes?: GetSiteResponse;
|
||||||
|
@ -42,6 +43,7 @@ function handleLogOut(i: Navbar) {
|
||||||
handleCollapseClick(i);
|
handleCollapseClick(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class Navbar extends Component<NavbarProps, NavbarState> {
|
export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
collapseButtonRef = createRef<HTMLButtonElement>();
|
collapseButtonRef = createRef<HTMLButtonElement>();
|
||||||
mobileMenuRef = createRef<HTMLDivElement>();
|
mobileMenuRef = createRef<HTMLDivElement>();
|
||||||
|
|
|
@ -42,7 +42,7 @@ import {
|
||||||
} from "../../interfaces";
|
} from "../../interfaces";
|
||||||
import { mdToHtml, mdToHtmlNoImages } from "../../markdown";
|
import { mdToHtml, mdToHtmlNoImages } from "../../markdown";
|
||||||
import { I18NextService, UserService } from "../../services";
|
import { I18NextService, UserService } from "../../services";
|
||||||
import { setupTippy } from "../../tippy";
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { MomentTime } from "../common/moment-time";
|
import { MomentTime } from "../common/moment-time";
|
||||||
import { UserBadges } from "../common/user-badges";
|
import { UserBadges } from "../common/user-badges";
|
||||||
|
@ -117,6 +117,7 @@ function handleToggleViewSource(i: CommentNode) {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
state: CommentNodeState = {
|
state: CommentNodeState = {
|
||||||
showReply: false,
|
showReply: false,
|
||||||
|
@ -607,12 +608,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
|
|
||||||
handleCommentCollapse(i: CommentNode) {
|
handleCommentCollapse(i: CommentNode) {
|
||||||
i.setState({ collapsed: !i.state.collapsed });
|
i.setState({ collapsed: !i.state.collapsed });
|
||||||
setupTippy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowAdvanced(i: CommentNode) {
|
handleShowAdvanced(i: CommentNode) {
|
||||||
i.setState({ showAdvanced: !i.state.showAdvanced });
|
i.setState({ showAdvanced: !i.state.showAdvanced });
|
||||||
setupTippy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSaveComment() {
|
async handleSaveComment() {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { Icon, Spinner } from "../common/icon";
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
import { CommentNode } from "./comment-node";
|
import { CommentNode } from "./comment-node";
|
||||||
import { EMPTY_REQUEST } from "../../services/HttpService";
|
import { EMPTY_REQUEST } from "../../services/HttpService";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface CommentReportProps {
|
interface CommentReportProps {
|
||||||
report: CommentReportView;
|
report: CommentReportView;
|
||||||
|
@ -21,6 +22,7 @@ interface CommentReportState {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class CommentReport extends Component<
|
export class CommentReport extends Component<
|
||||||
CommentReportProps,
|
CommentReportProps,
|
||||||
CommentReportState
|
CommentReportState
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import { Icon, Spinner } from "../icon";
|
import { Icon, Spinner } from "../icon";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { tippyMixin } from "../../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface ActionButtonPropsBase {
|
interface ActionButtonPropsBase {
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -34,6 +35,7 @@ async function handleClick(i: ActionButton) {
|
||||||
i.setState({ loading: false });
|
i.setState({ loading: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export default class ActionButton extends Component<
|
export default class ActionButton extends Component<
|
||||||
ActionButtonProps,
|
ActionButtonProps,
|
||||||
ActionButtonState
|
ActionButtonState
|
||||||
|
|
|
@ -20,6 +20,7 @@ import ViewVotesModal from "../view-votes-modal";
|
||||||
import ModActionFormModal, { BanUpdateForm } from "../mod-action-form-modal";
|
import ModActionFormModal, { BanUpdateForm } from "../mod-action-form-modal";
|
||||||
import { BanType, CommentNodeView, PurgeType } from "../../../interfaces";
|
import { BanType, CommentNodeView, PurgeType } from "../../../interfaces";
|
||||||
import { getApubName, hostname } from "@utils/helpers";
|
import { getApubName, hostname } from "@utils/helpers";
|
||||||
|
import { tippyMixin } from "../../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface ContentActionDropdownPropsBase {
|
interface ContentActionDropdownPropsBase {
|
||||||
onSave: () => Promise<void>;
|
onSave: () => Promise<void>;
|
||||||
|
@ -76,6 +77,7 @@ type ContentActionDropdownState = {
|
||||||
mounted: boolean;
|
mounted: boolean;
|
||||||
} & { [key in DialogType]: boolean };
|
} & { [key in DialogType]: boolean };
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export default class ContentActionDropdown extends Component<
|
export default class ContentActionDropdown extends Component<
|
||||||
ContentActionDropdownProps,
|
ContentActionDropdownProps,
|
||||||
ContentActionDropdownState
|
ContentActionDropdownState
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Component, linkEvent } from "inferno";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
import { EmojiMart } from "./emoji-mart";
|
import { EmojiMart } from "./emoji-mart";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface EmojiPickerProps {
|
interface EmojiPickerProps {
|
||||||
onEmojiClick?(val: any): any;
|
onEmojiClick?(val: any): any;
|
||||||
|
@ -16,6 +17,7 @@ function closeEmojiMartOnEsc(i, event): void {
|
||||||
event.key === "Escape" && i.setState({ showPicker: false });
|
event.key === "Escape" && i.setState({ showPicker: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
|
export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
|
||||||
private emptyState: EmojiPickerState = {
|
private emptyState: EmojiPickerState = {
|
||||||
showPicker: false,
|
showPicker: false,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { numToSI, randomStr } from "@utils/helpers";
|
||||||
import autosize from "autosize";
|
import autosize from "autosize";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { NoOptionI18nKeys } from "i18next";
|
import { NoOptionI18nKeys } from "i18next";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||||
import { Prompt } from "inferno-router";
|
import { Prompt } from "inferno-router";
|
||||||
import { Language } from "lemmy-js-client";
|
import { Language } from "lemmy-js-client";
|
||||||
import {
|
import {
|
||||||
|
@ -15,7 +15,7 @@ import {
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { customEmojisLookup, mdToHtml, setupTribute } from "../../markdown";
|
import { customEmojisLookup, mdToHtml, setupTribute } from "../../markdown";
|
||||||
import { HttpService, I18NextService, UserService } from "../../services";
|
import { HttpService, I18NextService, UserService } from "../../services";
|
||||||
import { setupTippy } from "../../tippy";
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
import { pictrsDeleteToast, toast } from "../../toast";
|
import { pictrsDeleteToast, toast } from "../../toast";
|
||||||
import { EmojiPicker } from "./emoji-picker";
|
import { EmojiPicker } from "./emoji-picker";
|
||||||
import { Icon, Spinner } from "./icon";
|
import { Icon, Spinner } from "./icon";
|
||||||
|
@ -68,6 +68,7 @@ interface MarkdownTextAreaState {
|
||||||
submitted: boolean;
|
submitted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class MarkdownTextArea extends Component<
|
export class MarkdownTextArea extends Component<
|
||||||
MarkdownTextAreaProps,
|
MarkdownTextAreaProps,
|
||||||
MarkdownTextAreaState
|
MarkdownTextAreaState
|
||||||
|
@ -111,13 +112,12 @@ export class MarkdownTextArea extends Component<
|
||||||
if (this.props.focus) {
|
if (this.props.focus) {
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO this is slow for some reason
|
|
||||||
setupTippy();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
|
componentWillReceiveProps(
|
||||||
|
nextProps: MarkdownTextAreaProps & { children?: InfernoNode },
|
||||||
|
) {
|
||||||
if (nextProps.finished) {
|
if (nextProps.finished) {
|
||||||
this.setState({
|
this.setState({
|
||||||
previewMode: false,
|
previewMode: false,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { format, parseISO } from "date-fns";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface MomentTimeProps {
|
interface MomentTimeProps {
|
||||||
published: string;
|
published: string;
|
||||||
|
@ -16,6 +17,7 @@ function formatDate(input: string) {
|
||||||
return format(parsed, "PPPPpppp");
|
return format(parsed, "PPPPpppp");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class MomentTime extends Component<MomentTimeProps, any> {
|
export class MomentTime extends Component<MomentTimeProps, any> {
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Component, FormEventHandler, linkEvent } from "inferno";
|
||||||
import { NavLink } from "inferno-router";
|
import { NavLink } from "inferno-router";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface PasswordInputProps {
|
interface PasswordInputProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -55,6 +56,7 @@ function handleToggleShow(i: PasswordInput) {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
class PasswordInput extends Component<PasswordInputProps, PasswordInputState> {
|
class PasswordInput extends Component<PasswordInputProps, PasswordInputState> {
|
||||||
state: PasswordInputState = {
|
state: PasswordInputState = {
|
||||||
show: false,
|
show: false,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface UserBadgesProps {
|
interface UserBadgesProps {
|
||||||
isBanned?: boolean;
|
isBanned?: boolean;
|
||||||
|
@ -12,7 +13,7 @@ interface UserBadgesProps {
|
||||||
classNames?: string;
|
classNames?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRoleLabelPill({
|
function getRoleLabelPill({
|
||||||
label,
|
label,
|
||||||
tooltip,
|
tooltip,
|
||||||
classes,
|
classes,
|
||||||
|
@ -34,6 +35,7 @@ export function getRoleLabelPill({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class UserBadges extends Component<UserBadgesProps> {
|
export class UserBadges extends Component<UserBadgesProps> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { newVote, showScores } from "@utils/app";
|
import { newVote, showScores } from "@utils/app";
|
||||||
import { numToSI } from "@utils/helpers";
|
import { numToSI } from "@utils/helpers";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||||
import {
|
import {
|
||||||
CommentAggregates,
|
CommentAggregates,
|
||||||
CreateCommentLike,
|
CreateCommentLike,
|
||||||
|
@ -11,6 +11,7 @@ import {
|
||||||
import { VoteContentType, VoteType } from "../../interfaces";
|
import { VoteContentType, VoteType } from "../../interfaces";
|
||||||
import { I18NextService, UserService } from "../../services";
|
import { I18NextService, UserService } from "../../services";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface VoteButtonsProps {
|
interface VoteButtonsProps {
|
||||||
voteContentType: VoteContentType;
|
voteContentType: VoteContentType;
|
||||||
|
@ -82,6 +83,7 @@ const handleDownvote = (i: VoteButtons) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class VoteButtonsCompact extends Component<
|
export class VoteButtonsCompact extends Component<
|
||||||
VoteButtonsProps,
|
VoteButtonsProps,
|
||||||
VoteButtonsState
|
VoteButtonsState
|
||||||
|
@ -95,7 +97,9 @@ export class VoteButtonsCompact extends Component<
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps: VoteButtonsProps) {
|
componentWillReceiveProps(
|
||||||
|
nextProps: VoteButtonsProps & { children?: InfernoNode },
|
||||||
|
) {
|
||||||
if (this.props !== nextProps) {
|
if (this.props !== nextProps) {
|
||||||
this.setState({
|
this.setState({
|
||||||
upvoteLoading: false,
|
upvoteLoading: false,
|
||||||
|
@ -166,6 +170,7 @@ export class VoteButtonsCompact extends Component<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
|
export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
|
||||||
state: VoteButtonsState = {
|
state: VoteButtonsState = {
|
||||||
upvoteLoading: false,
|
upvoteLoading: false,
|
||||||
|
@ -176,7 +181,9 @@ export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps: VoteButtonsProps) {
|
componentWillReceiveProps(
|
||||||
|
nextProps: VoteButtonsProps & { children?: InfernoNode },
|
||||||
|
) {
|
||||||
if (this.props !== nextProps) {
|
if (this.props !== nextProps) {
|
||||||
this.setState({
|
this.setState({
|
||||||
upvoteLoading: false,
|
upvoteLoading: false,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
getQueryParams,
|
getQueryParams,
|
||||||
getQueryString,
|
getQueryString,
|
||||||
numToSI,
|
numToSI,
|
||||||
|
resourcesSettled,
|
||||||
} from "@utils/helpers";
|
} from "@utils/helpers";
|
||||||
import type { QueryParams } from "@utils/types";
|
import type { QueryParams } from "@utils/types";
|
||||||
import { RouteDataResponse } from "@utils/types";
|
import { RouteDataResponse } from "@utils/types";
|
||||||
|
@ -38,6 +39,7 @@ import { SubscribeButton } from "../common/subscribe-button";
|
||||||
import { getHttpBaseInternal } from "../../utils/env";
|
import { getHttpBaseInternal } from "../../utils/env";
|
||||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
import { IRoutePropsWithFetch } from "../../routes";
|
import { IRoutePropsWithFetch } from "../../routes";
|
||||||
|
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||||
|
|
||||||
type CommunitiesData = RouteDataResponse<{
|
type CommunitiesData = RouteDataResponse<{
|
||||||
listCommunitiesResponse: ListCommunitiesResponse;
|
listCommunitiesResponse: ListCommunitiesResponse;
|
||||||
|
@ -84,6 +86,7 @@ export type CommunitiesFetchConfig = IRoutePropsWithFetch<
|
||||||
CommunitiesProps
|
CommunitiesProps
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
export class Communities extends Component<
|
export class Communities extends Component<
|
||||||
CommunitiesRouteProps,
|
CommunitiesRouteProps,
|
||||||
CommunitiesState
|
CommunitiesState
|
||||||
|
@ -96,6 +99,10 @@ export class Communities extends Component<
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadingSettled() {
|
||||||
|
return resourcesSettled([this.state.listCommunitiesResponse]);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: CommunitiesRouteProps, context: any) {
|
constructor(props: CommunitiesRouteProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.handlePageChange = this.handlePageChange.bind(this);
|
this.handlePageChange = this.handlePageChange.bind(this);
|
||||||
|
@ -374,8 +381,6 @@ export class Communities extends Component<
|
||||||
page,
|
page,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findAndUpdateCommunity(res: RequestState<CommunityResponse>) {
|
findAndUpdateCommunity(res: RequestState<CommunityResponse>) {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { Icon, Spinner } from "../common/icon";
|
||||||
import { ImageUploadForm } from "../common/image-upload-form";
|
import { ImageUploadForm } from "../common/image-upload-form";
|
||||||
import { LanguageSelect } from "../common/language-select";
|
import { LanguageSelect } from "../common/language-select";
|
||||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface CommunityFormProps {
|
interface CommunityFormProps {
|
||||||
community_view?: CommunityView; // If a community is given, that means this is an edit
|
community_view?: CommunityView; // If a community is given, that means this is an edit
|
||||||
|
@ -40,6 +41,7 @@ interface CommunityFormState {
|
||||||
submitted: boolean;
|
submitted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class CommunityForm extends Component<
|
export class CommunityForm extends Component<
|
||||||
CommunityFormProps,
|
CommunityFormProps,
|
||||||
CommunityFormState
|
CommunityFormState
|
||||||
|
|
|
@ -14,7 +14,12 @@ import {
|
||||||
updateCommunityBlock,
|
updateCommunityBlock,
|
||||||
updatePersonBlock,
|
updatePersonBlock,
|
||||||
} from "@utils/app";
|
} 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 type { QueryParams } from "@utils/types";
|
||||||
import { RouteDataResponse } from "@utils/types";
|
import { RouteDataResponse } from "@utils/types";
|
||||||
import { Component, RefObject, createRef, linkEvent } from "inferno";
|
import { Component, RefObject, createRef, linkEvent } from "inferno";
|
||||||
|
@ -87,7 +92,7 @@ import {
|
||||||
RequestState,
|
RequestState,
|
||||||
wrapClient,
|
wrapClient,
|
||||||
} from "../../services/HttpService";
|
} from "../../services/HttpService";
|
||||||
import { setupTippy } from "../../tippy";
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
import { toast } from "../../toast";
|
import { toast } from "../../toast";
|
||||||
import { CommentNodes } from "../comment/comment-nodes";
|
import { CommentNodes } from "../comment/comment-nodes";
|
||||||
import { BannerIconHeader } from "../common/banner-icon-header";
|
import { BannerIconHeader } from "../common/banner-icon-header";
|
||||||
|
@ -172,6 +177,8 @@ export type CommunityFetchConfig = IRoutePropsWithFetch<
|
||||||
CommunityProps
|
CommunityProps
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
|
@tippyMixin
|
||||||
export class Community extends Component<CommunityRouteProps, State> {
|
export class Community extends Component<CommunityRouteProps, State> {
|
||||||
private isoData = setIsoData<CommunityData>(this.context);
|
private isoData = setIsoData<CommunityData>(this.context);
|
||||||
state: State = {
|
state: State = {
|
||||||
|
@ -184,6 +191,16 @@ export class Community extends Component<CommunityRouteProps, State> {
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
private readonly mainContentRef: RefObject<HTMLElement>;
|
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) {
|
constructor(props: CommunityRouteProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
@ -253,8 +270,6 @@ export class Community extends Component<CommunityRouteProps, State> {
|
||||||
if (!this.state.isIsomorphic) {
|
if (!this.state.isIsomorphic) {
|
||||||
await Promise.all([this.fetchCommunity(), this.fetchData()]);
|
await Promise.all([this.fetchCommunity(), this.fetchData()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupTippy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetchInitialData({
|
static async fetchInitialData({
|
||||||
|
@ -586,17 +601,14 @@ export class Community extends Component<CommunityRouteProps, State> {
|
||||||
|
|
||||||
handlePageNext(nextPage: PaginationCursor) {
|
handlePageNext(nextPage: PaginationCursor) {
|
||||||
this.updateUrl({ pageCursor: nextPage });
|
this.updateUrl({ pageCursor: nextPage });
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(sort: SortType) {
|
handleSortChange(sort: SortType) {
|
||||||
this.updateUrl({ sort, pageCursor: undefined });
|
this.updateUrl({ sort, pageCursor: undefined });
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDataTypeChange(dataType: DataType) {
|
handleDataTypeChange(dataType: DataType) {
|
||||||
this.updateUrl({ dataType, pageCursor: undefined });
|
this.updateUrl({ dataType, pageCursor: undefined });
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowSidebarMobile(i: Community) {
|
handleShowSidebarMobile(i: Community) {
|
||||||
|
@ -649,8 +661,6 @@ export class Community extends Component<CommunityRouteProps, State> {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupTippy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDeleteCommunity(form: DeleteCommunity) {
|
async handleDeleteCommunity(form: DeleteCommunity) {
|
||||||
|
|
|
@ -7,13 +7,19 @@ import {
|
||||||
import { HttpService, I18NextService } from "../../services";
|
import { HttpService, I18NextService } from "../../services";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { CommunityForm } from "./community-form";
|
import { CommunityForm } from "./community-form";
|
||||||
|
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||||
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
|
|
||||||
interface CreateCommunityState {
|
interface CreateCommunityState {
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
loading: boolean;
|
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);
|
private isoData = setIsoData(this.context);
|
||||||
state: CreateCommunityState = {
|
state: CreateCommunityState = {
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { SubscribeButton } from "../common/subscribe-button";
|
||||||
import { CommunityForm } from "../community/community-form";
|
import { CommunityForm } from "../community/community-form";
|
||||||
import { CommunityLink } from "../community/community-link";
|
import { CommunityLink } from "../community/community-link";
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
community_view: CommunityView;
|
community_view: CommunityView;
|
||||||
|
@ -60,6 +61,7 @@ interface SidebarState {
|
||||||
purgeCommunityLoading: boolean;
|
purgeCommunityLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class Sidebar extends Component<SidebarProps, SidebarState> {
|
export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
state: SidebarState = {
|
state: SidebarState = {
|
||||||
showEdit: false,
|
showEdit: false,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { fetchThemeList, setIsoData, showLocal } from "@utils/app";
|
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 { RouteDataResponse } from "@utils/types";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
|
@ -62,6 +63,7 @@ export type AdminSettingsFetchConfig = IRoutePropsWithFetch<
|
||||||
Record<string, never>
|
Record<string, never>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
export class AdminSettings extends Component<
|
export class AdminSettings extends Component<
|
||||||
AdminSettingsRouteProps,
|
AdminSettingsRouteProps,
|
||||||
AdminSettingsState
|
AdminSettingsState
|
||||||
|
@ -79,6 +81,10 @@ export class AdminSettings extends Component<
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadingSettled() {
|
||||||
|
return resourcesSettled([this.state.bannedRes, this.state.instancesRes]);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { pictrsDeleteToast, toast } from "../../toast";
|
||||||
import { EmojiMart } from "../common/emoji-mart";
|
import { EmojiMart } from "../common/emoji-mart";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { Paginator } from "../common/paginator";
|
import { Paginator } from "../common/paginator";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface EmojiFormProps {
|
interface EmojiFormProps {
|
||||||
onEdit(form: EditCustomEmoji): void;
|
onEdit(form: EditCustomEmoji): void;
|
||||||
|
@ -38,6 +39,7 @@ interface CustomEmojiViewForm {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private itemsPerPage = 15;
|
private itemsPerPage = 15;
|
||||||
|
|
|
@ -17,7 +17,9 @@ import {
|
||||||
getQueryParams,
|
getQueryParams,
|
||||||
getQueryString,
|
getQueryString,
|
||||||
getRandomFromList,
|
getRandomFromList,
|
||||||
|
resourcesSettled,
|
||||||
} from "@utils/helpers";
|
} from "@utils/helpers";
|
||||||
|
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||||
import { canCreateCommunity } from "@utils/roles";
|
import { canCreateCommunity } from "@utils/roles";
|
||||||
import type { QueryParams } from "@utils/types";
|
import type { QueryParams } from "@utils/types";
|
||||||
import { RouteDataResponse } from "@utils/types";
|
import { RouteDataResponse } from "@utils/types";
|
||||||
|
@ -87,7 +89,7 @@ import {
|
||||||
RequestState,
|
RequestState,
|
||||||
wrapClient,
|
wrapClient,
|
||||||
} from "../../services/HttpService";
|
} from "../../services/HttpService";
|
||||||
import { setupTippy } from "../../tippy";
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
import { toast } from "../../toast";
|
import { toast } from "../../toast";
|
||||||
import { CommentNodes } from "../comment/comment-nodes";
|
import { CommentNodes } from "../comment/comment-nodes";
|
||||||
import { DataTypeSelect } from "../common/data-type-select";
|
import { DataTypeSelect } from "../common/data-type-select";
|
||||||
|
@ -107,6 +109,7 @@ import {
|
||||||
} from "../common/loading-skeleton";
|
} from "../common/loading-skeleton";
|
||||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
import { IRoutePropsWithFetch } from "../../routes";
|
import { IRoutePropsWithFetch } from "../../routes";
|
||||||
|
import { snapToTop } from "@utils/browser";
|
||||||
|
|
||||||
interface HomeState {
|
interface HomeState {
|
||||||
postsRes: RequestState<GetPostsResponse>;
|
postsRes: RequestState<GetPostsResponse>;
|
||||||
|
@ -116,7 +119,6 @@ interface HomeState {
|
||||||
showTrendingMobile: boolean;
|
showTrendingMobile: boolean;
|
||||||
showSidebarMobile: boolean;
|
showSidebarMobile: boolean;
|
||||||
subscribedCollapsed: boolean;
|
subscribedCollapsed: boolean;
|
||||||
scrolled: boolean;
|
|
||||||
tagline?: string;
|
tagline?: string;
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
finished: Map<CommentId, boolean | undefined>;
|
finished: Map<CommentId, boolean | undefined>;
|
||||||
|
@ -253,13 +255,14 @@ export type HomeFetchConfig = IRoutePropsWithFetch<
|
||||||
HomeProps
|
HomeProps
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
|
@tippyMixin
|
||||||
export class Home extends Component<HomeRouteProps, HomeState> {
|
export class Home extends Component<HomeRouteProps, HomeState> {
|
||||||
private isoData = setIsoData<HomeData>(this.context);
|
private isoData = setIsoData<HomeData>(this.context);
|
||||||
state: HomeState = {
|
state: HomeState = {
|
||||||
postsRes: EMPTY_REQUEST,
|
postsRes: EMPTY_REQUEST,
|
||||||
commentsRes: EMPTY_REQUEST,
|
commentsRes: EMPTY_REQUEST,
|
||||||
trendingCommunitiesRes: EMPTY_REQUEST,
|
trendingCommunitiesRes: EMPTY_REQUEST,
|
||||||
scrolled: true,
|
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
showSubscribedMobile: false,
|
showSubscribedMobile: false,
|
||||||
showTrendingMobile: false,
|
showTrendingMobile: false,
|
||||||
|
@ -269,6 +272,15 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
||||||
isIsomorphic: false,
|
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) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
@ -334,8 +346,6 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
||||||
) {
|
) {
|
||||||
await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
|
await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupTippy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetchInitialData({
|
static async fetchInitialData({
|
||||||
|
@ -667,11 +677,6 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
||||||
search: getQueryString(queryParams),
|
search: getQueryString(queryParams),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.state.scrolled) {
|
|
||||||
this.setState({ scrolled: true });
|
|
||||||
setTimeout(() => window.scrollTo(0, 0), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.fetchData();
|
await this.fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -852,8 +857,6 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupTippy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowSubscribedMobile(i: Home) {
|
handleShowSubscribedMobile(i: Home) {
|
||||||
|
@ -876,27 +879,23 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
||||||
this.props.history.back();
|
this.props.history.back();
|
||||||
// A hack to scroll to top
|
// A hack to scroll to top
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.scrollTo(0, 0);
|
snapToTop();
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePageNext(nextPage: PaginationCursor) {
|
handlePageNext(nextPage: PaginationCursor) {
|
||||||
this.setState({ scrolled: false });
|
|
||||||
this.updateUrl({ pageCursor: nextPage });
|
this.updateUrl({ pageCursor: nextPage });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(val: SortType) {
|
handleSortChange(val: SortType) {
|
||||||
this.setState({ scrolled: false });
|
|
||||||
this.updateUrl({ sort: val, pageCursor: undefined });
|
this.updateUrl({ sort: val, pageCursor: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleListingTypeChange(val: ListingType) {
|
handleListingTypeChange(val: ListingType) {
|
||||||
this.setState({ scrolled: false });
|
|
||||||
this.updateUrl({ listingType: val, pageCursor: undefined });
|
this.updateUrl({ listingType: val, pageCursor: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDataTypeChange(val: DataType) {
|
handleDataTypeChange(val: DataType) {
|
||||||
this.setState({ scrolled: false });
|
|
||||||
this.updateUrl({ dataType: val, pageCursor: undefined });
|
this.updateUrl({ dataType: val, pageCursor: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,8 @@ import Tabs from "../common/tabs";
|
||||||
import { getHttpBaseInternal } from "../../utils/env";
|
import { getHttpBaseInternal } from "../../utils/env";
|
||||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
import { IRoutePropsWithFetch } from "../../routes";
|
import { IRoutePropsWithFetch } from "../../routes";
|
||||||
|
import { resourcesSettled } from "@utils/helpers";
|
||||||
|
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||||
|
|
||||||
type InstancesData = RouteDataResponse<{
|
type InstancesData = RouteDataResponse<{
|
||||||
federatedInstancesResponse: GetFederatedInstancesResponse;
|
federatedInstancesResponse: GetFederatedInstancesResponse;
|
||||||
|
@ -43,6 +45,7 @@ export type InstancesFetchConfig = IRoutePropsWithFetch<
|
||||||
Record<string, never>
|
Record<string, never>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
export class Instances extends Component<InstancesRouteProps, InstancesState> {
|
export class Instances extends Component<InstancesRouteProps, InstancesState> {
|
||||||
private isoData = setIsoData<InstancesData>(this.context);
|
private isoData = setIsoData<InstancesData>(this.context);
|
||||||
state: InstancesState = {
|
state: InstancesState = {
|
||||||
|
@ -51,6 +54,10 @@ export class Instances extends Component<InstancesRouteProps, InstancesState> {
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadingSettled() {
|
||||||
|
return resourcesSettled([this.state.instancesRes]);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ import { HttpService, I18NextService } from "../../services";
|
||||||
import { toast } from "../../toast";
|
import { toast } from "../../toast";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
|
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||||
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
form: {
|
form: {
|
||||||
|
@ -15,7 +17,11 @@ interface State {
|
||||||
siteRes: GetSiteResponse;
|
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);
|
private isoData = setIsoData(this.context);
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import TotpModal from "../common/totp-modal";
|
||||||
import { UnreadCounterService } from "../../services";
|
import { UnreadCounterService } from "../../services";
|
||||||
import { RouteData } from "../../interfaces";
|
import { RouteData } from "../../interfaces";
|
||||||
import { IRoutePropsWithFetch } from "../../routes";
|
import { IRoutePropsWithFetch } from "../../routes";
|
||||||
|
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
prev?: string;
|
prev?: string;
|
||||||
|
@ -125,6 +126,7 @@ export type LoginFetchConfig = IRoutePropsWithFetch<
|
||||||
LoginProps
|
LoginProps
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@simpleScrollMixin
|
||||||
export class Login extends Component<LoginRouteProps, State> {
|
export class Login extends Component<LoginRouteProps, State> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ import {
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
import PasswordInput from "../common/password-input";
|
import PasswordInput from "../common/password-input";
|
||||||
import { SiteForm } from "./site-form";
|
import { SiteForm } from "./site-form";
|
||||||
|
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||||
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
form: {
|
form: {
|
||||||
|
@ -36,7 +38,11 @@ interface State {
|
||||||
siteRes: GetSiteResponse;
|
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);
|
private isoData = setIsoData(this.context);
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { setIsoData } from "@utils/app";
|
import { setIsoData } from "@utils/app";
|
||||||
import { isBrowser } from "@utils/browser";
|
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 { Component, linkEvent } from "inferno";
|
||||||
import { T } from "inferno-i18next-dess";
|
import { T } from "inferno-i18next-dess";
|
||||||
import {
|
import {
|
||||||
|
@ -24,6 +25,7 @@ import { HtmlTags } from "../common/html-tags";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||||
import PasswordInput from "../common/password-input";
|
import PasswordInput from "../common/password-input";
|
||||||
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
registerRes: RequestState<LoginResponse>;
|
registerRes: RequestState<LoginResponse>;
|
||||||
|
@ -43,7 +45,11 @@ interface State {
|
||||||
siteRes: GetSiteResponse;
|
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 isoData = setIsoData(this.context);
|
||||||
private audio?: HTMLAudioElement;
|
private audio?: HTMLAudioElement;
|
||||||
|
|
||||||
|
@ -57,6 +63,13 @@ export class Signup extends Component<any, State> {
|
||||||
siteRes: this.isoData.site_res,
|
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) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Badges } from "../common/badges";
|
||||||
import { BannerIconHeader } from "../common/banner-icon-header";
|
import { BannerIconHeader } from "../common/banner-icon-header";
|
||||||
import { Icon } from "../common/icon";
|
import { Icon } from "../common/icon";
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface SiteSidebarProps {
|
interface SiteSidebarProps {
|
||||||
site: Site;
|
site: Site;
|
||||||
|
@ -20,6 +21,7 @@ interface SiteSidebarState {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
|
export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
|
||||||
state: SiteSidebarState = {
|
state: SiteSidebarState = {
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { EditSite, Tagline } from "lemmy-js-client";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface TaglineFormProps {
|
interface TaglineFormProps {
|
||||||
taglines: Array<Tagline>;
|
taglines: Array<Tagline>;
|
||||||
|
@ -16,6 +17,7 @@ interface TaglineFormState {
|
||||||
editingRow?: number;
|
editingRow?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
||||||
state: TaglineFormState = {
|
state: TaglineFormState = {
|
||||||
editingRow: undefined,
|
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,
|
getPageFromString,
|
||||||
getQueryParams,
|
getQueryParams,
|
||||||
getQueryString,
|
getQueryString,
|
||||||
|
resourcesSettled,
|
||||||
} from "@utils/helpers";
|
} from "@utils/helpers";
|
||||||
|
import { scrollMixin } from "./mixins/scroll-mixin";
|
||||||
import { amAdmin, amMod } from "@utils/roles";
|
import { amAdmin, amMod } from "@utils/roles";
|
||||||
import type { QueryParams } from "@utils/types";
|
import type { QueryParams } from "@utils/types";
|
||||||
import { Choice, RouteDataResponse } from "@utils/types";
|
import { Choice, RouteDataResponse } from "@utils/types";
|
||||||
|
@ -645,6 +647,7 @@ export type ModlogFetchConfig = IRoutePropsWithFetch<
|
||||||
ModlogProps
|
ModlogProps
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
export class Modlog extends Component<ModlogRouteProps, ModlogState> {
|
export class Modlog extends Component<ModlogRouteProps, ModlogState> {
|
||||||
private isoData = setIsoData<ModlogData>(this.context);
|
private isoData = setIsoData<ModlogData>(this.context);
|
||||||
|
|
||||||
|
@ -658,6 +661,10 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadingSettled() {
|
||||||
|
return resourcesSettled([this.state.res]);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: ModlogRouteProps, context: any) {
|
constructor(props: ModlogRouteProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.handlePageChange = this.handlePageChange.bind(this);
|
this.handlePageChange = this.handlePageChange.bind(this);
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
import { Icon } from "../common/icon";
|
import { Icon } from "../common/icon";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface CakeDayProps {
|
interface CakeDayProps {
|
||||||
creatorName: string;
|
creatorName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class CakeDay extends Component<CakeDayProps, any> {
|
export class CakeDay extends Component<CakeDayProps, any> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -10,7 +10,12 @@ import {
|
||||||
setIsoData,
|
setIsoData,
|
||||||
updatePersonBlock,
|
updatePersonBlock,
|
||||||
} from "@utils/app";
|
} 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 { RouteDataResponse } from "@utils/types";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
|
@ -137,6 +142,7 @@ export type InboxFetchConfig = IRoutePropsWithFetch<
|
||||||
Record<string, never>
|
Record<string, never>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
export class Inbox extends Component<InboxRouteProps, InboxState> {
|
export class Inbox extends Component<InboxRouteProps, InboxState> {
|
||||||
private isoData = setIsoData<InboxData>(this.context);
|
private isoData = setIsoData<InboxData>(this.context);
|
||||||
state: InboxState = {
|
state: InboxState = {
|
||||||
|
@ -153,6 +159,14 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadingSettled() {
|
||||||
|
return resourcesSettled([
|
||||||
|
this.state.repliesRes,
|
||||||
|
this.state.mentionsRes,
|
||||||
|
this.state.messagesRes,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
import PasswordInput from "../common/password-input";
|
import PasswordInput from "../common/password-input";
|
||||||
import { toast } from "../../toast";
|
import { toast } from "../../toast";
|
||||||
|
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||||
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
passwordChangeRes: RequestState<SuccessResponse>;
|
passwordChangeRes: RequestState<SuccessResponse>;
|
||||||
|
@ -23,7 +25,11 @@ interface State {
|
||||||
siteRes: GetSiteResponse;
|
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);
|
private isoData = setIsoData(this.context);
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
|
|
|
@ -41,7 +41,6 @@ import {
|
||||||
TransferCommunity,
|
TransferCommunity,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { CommentViewType, PersonDetailsView } from "../../interfaces";
|
import { CommentViewType, PersonDetailsView } from "../../interfaces";
|
||||||
import { setupTippy } from "../../tippy";
|
|
||||||
import { CommentNodes } from "../comment/comment-nodes";
|
import { CommentNodes } from "../comment/comment-nodes";
|
||||||
import { Paginator } from "../common/paginator";
|
import { Paginator } from "../common/paginator";
|
||||||
import { PostListing } from "../post/post-listing";
|
import { PostListing } from "../post/post-listing";
|
||||||
|
@ -109,10 +108,6 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
|
||||||
this.handlePageChange = this.handlePageChange.bind(this);
|
this.handlePageChange = this.handlePageChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
setupTippy();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="person-details">
|
<div className="person-details">
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
setIsoData,
|
setIsoData,
|
||||||
updatePersonBlock,
|
updatePersonBlock,
|
||||||
} from "@utils/app";
|
} from "@utils/app";
|
||||||
import { restoreScrollPosition, saveScrollPosition } from "@utils/browser";
|
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||||
import {
|
import {
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
futureDaysToUnixTime,
|
futureDaysToUnixTime,
|
||||||
|
@ -17,6 +17,7 @@ import {
|
||||||
getQueryString,
|
getQueryString,
|
||||||
numToSI,
|
numToSI,
|
||||||
randomStr,
|
randomStr,
|
||||||
|
resourcesSettled,
|
||||||
} from "@utils/helpers";
|
} from "@utils/helpers";
|
||||||
import { canMod, isBanned } from "@utils/roles";
|
import { canMod, isBanned } from "@utils/roles";
|
||||||
import type { QueryParams } from "@utils/types";
|
import type { QueryParams } from "@utils/types";
|
||||||
|
@ -82,7 +83,6 @@ import {
|
||||||
RequestState,
|
RequestState,
|
||||||
wrapClient,
|
wrapClient,
|
||||||
} from "../../services/HttpService";
|
} from "../../services/HttpService";
|
||||||
import { setupTippy } from "../../tippy";
|
|
||||||
import { toast } from "../../toast";
|
import { toast } from "../../toast";
|
||||||
import { BannerIconHeader } from "../common/banner-icon-header";
|
import { BannerIconHeader } from "../common/banner-icon-header";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
|
@ -183,6 +183,7 @@ export type ProfileFetchConfig = IRoutePropsWithFetch<
|
||||||
ProfileProps
|
ProfileProps
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
private isoData = setIsoData<ProfileData>(this.context);
|
private isoData = setIsoData<ProfileData>(this.context);
|
||||||
state: ProfileState = {
|
state: ProfileState = {
|
||||||
|
@ -195,6 +196,10 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadingSettled() {
|
||||||
|
return resourcesSettled([this.state.personRes]);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: ProfileRouteProps, context: any) {
|
constructor(props: ProfileRouteProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
@ -249,11 +254,6 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
if (!this.state.isIsomorphic) {
|
if (!this.state.isIsomorphic) {
|
||||||
await this.fetchUserData();
|
await this.fetchUserData();
|
||||||
}
|
}
|
||||||
setupTippy();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
saveScrollPosition(this.context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchUserData() {
|
async fetchUserData() {
|
||||||
|
@ -271,7 +271,6 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
personRes,
|
personRes,
|
||||||
personBlocked: isPersonBlocked(personRes),
|
personBlocked: isPersonBlocked(personRes),
|
||||||
});
|
});
|
||||||
restoreScrollPosition(this.context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get amCurrentUser() {
|
get amCurrentUser() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { editRegistrationApplication, setIsoData } from "@utils/app";
|
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 { RouteDataResponse } from "@utils/types";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
|
@ -20,7 +21,6 @@ import {
|
||||||
RequestState,
|
RequestState,
|
||||||
wrapClient,
|
wrapClient,
|
||||||
} from "../../services/HttpService";
|
} from "../../services/HttpService";
|
||||||
import { setupTippy } from "../../tippy";
|
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
import { Paginator } from "../common/paginator";
|
import { Paginator } from "../common/paginator";
|
||||||
|
@ -58,6 +58,7 @@ export type RegistrationApplicationsFetchConfig = IRoutePropsWithFetch<
|
||||||
Record<string, never>
|
Record<string, never>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
export class RegistrationApplications extends Component<
|
export class RegistrationApplications extends Component<
|
||||||
RegistrationApplicationsRouteProps,
|
RegistrationApplicationsRouteProps,
|
||||||
RegistrationApplicationsState
|
RegistrationApplicationsState
|
||||||
|
@ -71,6 +72,10 @@ export class RegistrationApplications extends Component<
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadingSettled() {
|
||||||
|
return resourcesSettled([this.state.appsRes]);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
@ -91,7 +96,6 @@ export class RegistrationApplications extends Component<
|
||||||
if (!this.state.isIsomorphic) {
|
if (!this.state.isIsomorphic) {
|
||||||
await this.refetch();
|
await this.refetch();
|
||||||
}
|
}
|
||||||
setupTippy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
|
|
|
@ -4,7 +4,8 @@ import {
|
||||||
editPrivateMessageReport,
|
editPrivateMessageReport,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
} from "@utils/app";
|
} 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 { amAdmin } from "@utils/roles";
|
||||||
import { RouteDataResponse } from "@utils/types";
|
import { RouteDataResponse } from "@utils/types";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
@ -103,6 +104,7 @@ export type ReportsFetchConfig = IRoutePropsWithFetch<
|
||||||
Record<string, never>
|
Record<string, never>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
export class Reports extends Component<ReportsRouteProps, ReportsState> {
|
export class Reports extends Component<ReportsRouteProps, ReportsState> {
|
||||||
private isoData = setIsoData<ReportsData>(this.context);
|
private isoData = setIsoData<ReportsData>(this.context);
|
||||||
state: ReportsState = {
|
state: ReportsState = {
|
||||||
|
@ -116,6 +118,14 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadingSettled() {
|
||||||
|
return resourcesSettled([
|
||||||
|
this.state.commentReportsRes,
|
||||||
|
this.state.postReportsRes,
|
||||||
|
this.state.messageReportsRes,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ import {
|
||||||
languages,
|
languages,
|
||||||
loadUserLanguage,
|
loadUserLanguage,
|
||||||
} from "../../services/I18NextService";
|
} from "../../services/I18NextService";
|
||||||
import { setupTippy } from "../../tippy";
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
import { toast } from "../../toast";
|
import { toast } from "../../toast";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
|
@ -66,10 +66,11 @@ import { PersonListing } from "./person-listing";
|
||||||
import { InitialFetchRequest } from "../../interfaces";
|
import { InitialFetchRequest } from "../../interfaces";
|
||||||
import TotpModal from "../common/totp-modal";
|
import TotpModal from "../common/totp-modal";
|
||||||
import { LoadingEllipses } from "../common/loading-ellipses";
|
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 { getHttpBaseInternal } from "../../utils/env";
|
||||||
import { IRoutePropsWithFetch } from "../../routes";
|
import { IRoutePropsWithFetch } from "../../routes";
|
||||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
|
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||||
|
|
||||||
type SettingsData = RouteDataResponse<{
|
type SettingsData = RouteDataResponse<{
|
||||||
instancesRes: GetFederatedInstancesResponse;
|
instancesRes: GetFederatedInstancesResponse;
|
||||||
|
@ -203,6 +204,8 @@ export type SettingsFetchConfig = IRoutePropsWithFetch<
|
||||||
Record<string, never>
|
Record<string, never>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@simpleScrollMixin
|
||||||
|
@tippyMixin
|
||||||
export class Settings extends Component<SettingsRouteProps, SettingsState> {
|
export class Settings extends Component<SettingsRouteProps, SettingsState> {
|
||||||
private isoData = setIsoData<SettingsData>(this.context);
|
private isoData = setIsoData<SettingsData>(this.context);
|
||||||
exportSettingsLink = createRef<HTMLAnchorElement>();
|
exportSettingsLink = createRef<HTMLAnchorElement>();
|
||||||
|
@ -334,7 +337,6 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
setupTippy();
|
|
||||||
this.setState({ themeList: await fetchThemeList() });
|
this.setState({ themeList: await fetchThemeList() });
|
||||||
|
|
||||||
if (!this.state.isIsomorphic) {
|
if (!this.state.isIsomorphic) {
|
||||||
|
@ -1578,7 +1580,7 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
toast(I18NextService.i18n.t("saved"));
|
toast(I18NextService.i18n.t("saved"));
|
||||||
window.scrollTo(0, 0);
|
snapToTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
setThemeOverride(undefined);
|
setThemeOverride(undefined);
|
||||||
|
@ -1598,7 +1600,7 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
|
||||||
old_password,
|
old_password,
|
||||||
});
|
});
|
||||||
if (changePasswordRes.state === "success") {
|
if (changePasswordRes.state === "success") {
|
||||||
window.scrollTo(0, 0);
|
snapToTop();
|
||||||
toast(I18NextService.i18n.t("password_changed"));
|
toast(I18NextService.i18n.t("password_changed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,19 @@ import {
|
||||||
import { toast } from "../../toast";
|
import { toast } from "../../toast";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
|
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||||
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
verifyRes: RequestState<SuccessResponse>;
|
verifyRes: RequestState<SuccessResponse>;
|
||||||
siteRes: GetSiteResponse;
|
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);
|
private isoData = setIsoData(this.context);
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { Spinner } from "../common/icon";
|
||||||
import { PostForm } from "./post-form";
|
import { PostForm } from "./post-form";
|
||||||
import { getHttpBaseInternal } from "../../utils/env";
|
import { getHttpBaseInternal } from "../../utils/env";
|
||||||
import { IRoutePropsWithFetch } from "../../routes";
|
import { IRoutePropsWithFetch } from "../../routes";
|
||||||
|
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||||
|
|
||||||
export interface CreatePostProps {
|
export interface CreatePostProps {
|
||||||
communityId?: number;
|
communityId?: number;
|
||||||
|
@ -70,6 +71,7 @@ export type CreatePostFetchConfig = IRoutePropsWithFetch<
|
||||||
CreatePostProps
|
CreatePostProps
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@simpleScrollMixin
|
||||||
export class CreatePost extends Component<
|
export class CreatePost extends Component<
|
||||||
CreatePostRouteProps,
|
CreatePostRouteProps,
|
||||||
CreatePostState
|
CreatePostState
|
||||||
|
|
|
@ -37,7 +37,6 @@ import {
|
||||||
LOADING_REQUEST,
|
LOADING_REQUEST,
|
||||||
RequestState,
|
RequestState,
|
||||||
} from "../../services/HttpService";
|
} from "../../services/HttpService";
|
||||||
import { setupTippy } from "../../tippy";
|
|
||||||
import { toast } from "../../toast";
|
import { toast } from "../../toast";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { LanguageSelect } from "../common/language-select";
|
import { LanguageSelect } from "../common/language-select";
|
||||||
|
@ -306,7 +305,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
setupTippy();
|
|
||||||
const textarea: any = document.getElementById("post-title");
|
const textarea: any = document.getElementById("post-title");
|
||||||
|
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
|
|
|
@ -35,7 +35,7 @@ import { relTags } from "../../config";
|
||||||
import { VoteContentType } from "../../interfaces";
|
import { VoteContentType } from "../../interfaces";
|
||||||
import { mdToHtml, mdToHtmlInline } from "../../markdown";
|
import { mdToHtml, mdToHtmlInline } from "../../markdown";
|
||||||
import { I18NextService, UserService } from "../../services";
|
import { I18NextService, UserService } from "../../services";
|
||||||
import { setupTippy } from "../../tippy";
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
import { Icon } from "../common/icon";
|
import { Icon } from "../common/icon";
|
||||||
import { MomentTime } from "../common/moment-time";
|
import { MomentTime } from "../common/moment-time";
|
||||||
import { PictrsImage } from "../common/pictrs-image";
|
import { PictrsImage } from "../common/pictrs-image";
|
||||||
|
@ -91,8 +91,10 @@ interface PostListingProps {
|
||||||
onAddAdmin(form: AddAdmin): Promise<void>;
|
onAddAdmin(form: AddAdmin): Promise<void>;
|
||||||
onTransferCommunity(form: TransferCommunity): Promise<void>;
|
onTransferCommunity(form: TransferCommunity): Promise<void>;
|
||||||
onMarkPostAsRead(form: MarkPostAsRead): void;
|
onMarkPostAsRead(form: MarkPostAsRead): void;
|
||||||
|
onScrollIntoCommentsClick?(e: MouseEvent): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class PostListing extends Component<PostListingProps, PostListingState> {
|
export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
state: PostListingState = {
|
state: PostListingState = {
|
||||||
showEdit: false,
|
showEdit: false,
|
||||||
|
@ -636,6 +638,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
title={title}
|
title={title}
|
||||||
to={`/post/${pv.post.id}?scrollToComments=true`}
|
to={`/post/${pv.post.id}?scrollToComments=true`}
|
||||||
data-tippy-content={title}
|
data-tippy-content={title}
|
||||||
|
onClick={this.props.onScrollIntoCommentsClick}
|
||||||
>
|
>
|
||||||
<Icon icon="message-square" classes="me-1" inline />
|
<Icon icon="message-square" classes="me-1" inline />
|
||||||
{pv.counts.comments}
|
{pv.counts.comments}
|
||||||
|
@ -982,7 +985,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
handleImageExpandClick(i: PostListing, event: any) {
|
handleImageExpandClick(i: PostListing, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.setState({ imageExpanded: !i.state.imageExpanded });
|
i.setState({ imageExpanded: !i.state.imageExpanded });
|
||||||
setupTippy();
|
|
||||||
|
|
||||||
if (myAuth() && !i.postView.read) {
|
if (myAuth() && !i.postView.read) {
|
||||||
i.props.onMarkPostAsRead({
|
i.props.onMarkPostAsRead({
|
||||||
|
@ -998,7 +1000,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
handleShowBody(i: PostListing) {
|
handleShowBody(i: PostListing) {
|
||||||
i.setState({ showBody: !i.state.showBody });
|
i.setState({ showBody: !i.state.showBody });
|
||||||
setupTippy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get pointsTippy(): string {
|
get pointsTippy(): string {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Icon, Spinner } from "../common/icon";
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
import { PostListing } from "./post-listing";
|
import { PostListing } from "./post-listing";
|
||||||
import { EMPTY_REQUEST } from "../../services/HttpService";
|
import { EMPTY_REQUEST } from "../../services/HttpService";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface PostReportProps {
|
interface PostReportProps {
|
||||||
report: PostReportView;
|
report: PostReportView;
|
||||||
|
@ -16,6 +17,7 @@ interface PostReportState {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class PostReport extends Component<PostReportProps, PostReportState> {
|
export class PostReport extends Component<PostReportProps, PostReportState> {
|
||||||
state: PostReportState = {
|
state: PostReportState = {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
|
@ -13,12 +13,14 @@ import {
|
||||||
updateCommunityBlock,
|
updateCommunityBlock,
|
||||||
updatePersonBlock,
|
updatePersonBlock,
|
||||||
} from "@utils/app";
|
} from "@utils/app";
|
||||||
|
import { isBrowser } from "@utils/browser";
|
||||||
import {
|
import {
|
||||||
isBrowser,
|
debounce,
|
||||||
restoreScrollPosition,
|
getApubName,
|
||||||
saveScrollPosition,
|
randomStr,
|
||||||
} from "@utils/browser";
|
resourcesSettled,
|
||||||
import { debounce, getApubName, randomStr } from "@utils/helpers";
|
} from "@utils/helpers";
|
||||||
|
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||||
import { isImage } from "@utils/media";
|
import { isImage } from "@utils/media";
|
||||||
import { RouteDataResponse } from "@utils/types";
|
import { RouteDataResponse } from "@utils/types";
|
||||||
import autosize from "autosize";
|
import autosize from "autosize";
|
||||||
|
@ -89,7 +91,6 @@ import {
|
||||||
RequestState,
|
RequestState,
|
||||||
wrapClient,
|
wrapClient,
|
||||||
} from "../../services/HttpService";
|
} from "../../services/HttpService";
|
||||||
import { setupTippy } from "../../tippy";
|
|
||||||
import { toast } from "../../toast";
|
import { toast } from "../../toast";
|
||||||
import { CommentForm } from "../comment/comment-form";
|
import { CommentForm } from "../comment/comment-form";
|
||||||
import { CommentNodes } from "../comment/comment-nodes";
|
import { CommentNodes } from "../comment/comment-nodes";
|
||||||
|
@ -135,6 +136,7 @@ export type PostFetchConfig = IRoutePropsWithFetch<
|
||||||
Record<string, never>
|
Record<string, never>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
export class Post extends Component<PostRouteProps, PostState> {
|
export class Post extends Component<PostRouteProps, PostState> {
|
||||||
private isoData = setIsoData<PostData>(this.context);
|
private isoData = setIsoData<PostData>(this.context);
|
||||||
private commentScrollDebounced: () => void;
|
private commentScrollDebounced: () => void;
|
||||||
|
@ -153,6 +155,10 @@ export class Post extends Component<PostRouteProps, PostState> {
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadingSettled() {
|
||||||
|
return resourcesSettled([this.state.postRes, this.state.commentsRes]);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
@ -189,6 +195,8 @@ export class Post extends Component<PostRouteProps, PostState> {
|
||||||
this.handleSavePost = this.handleSavePost.bind(this);
|
this.handleSavePost = this.handleSavePost.bind(this);
|
||||||
this.handlePurgePost = this.handlePurgePost.bind(this);
|
this.handlePurgePost = this.handlePurgePost.bind(this);
|
||||||
this.handleFeaturePost = this.handleFeaturePost.bind(this);
|
this.handleFeaturePost = this.handleFeaturePost.bind(this);
|
||||||
|
this.handleScrollIntoCommentsClick =
|
||||||
|
this.handleScrollIntoCommentsClick.bind(this);
|
||||||
|
|
||||||
this.state = { ...this.state, commentSectionRef: createRef() };
|
this.state = { ...this.state, commentSectionRef: createRef() };
|
||||||
|
|
||||||
|
@ -237,10 +245,6 @@ export class Post extends Component<PostRouteProps, PostState> {
|
||||||
commentsRes,
|
commentsRes,
|
||||||
});
|
});
|
||||||
|
|
||||||
setupTippy();
|
|
||||||
|
|
||||||
if (!this.state.commentId) restoreScrollPosition(this.context);
|
|
||||||
|
|
||||||
if (this.checkScrollIntoCommentsParam) {
|
if (this.checkScrollIntoCommentsParam) {
|
||||||
this.scrollIntoCommentSection();
|
this.scrollIntoCommentSection();
|
||||||
}
|
}
|
||||||
|
@ -284,8 +288,6 @@ export class Post extends Component<PostRouteProps, PostState> {
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener("scroll", this.commentScrollDebounced);
|
document.removeEventListener("scroll", this.commentScrollDebounced);
|
||||||
|
|
||||||
saveScrollPosition(this.context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
|
@ -299,16 +301,16 @@ export class Post extends Component<PostRouteProps, PostState> {
|
||||||
document.addEventListener("scroll", this.commentScrollDebounced);
|
document.addEventListener("scroll", this.commentScrollDebounced);
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidUpdate(_lastProps: any) {
|
handleScrollIntoCommentsClick(e: MouseEvent) {
|
||||||
// Necessary if you are on a post and you click another post (same route)
|
this.scrollIntoCommentSection();
|
||||||
if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
|
e.preventDefault();
|
||||||
await this.fetchPost();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get checkScrollIntoCommentsParam() {
|
get checkScrollIntoCommentsParam() {
|
||||||
return Boolean(
|
return (
|
||||||
|
Boolean(
|
||||||
new URLSearchParams(this.props.location.search).get("scrollToComments"),
|
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}
|
onTransferCommunity={this.handleTransferCommunity}
|
||||||
onFeaturePost={this.handleFeaturePost}
|
onFeaturePost={this.handleFeaturePost}
|
||||||
onMarkPostAsRead={() => {}}
|
onMarkPostAsRead={() => {}}
|
||||||
|
onScrollIntoCommentsClick={this.handleScrollIntoCommentsClick}
|
||||||
/>
|
/>
|
||||||
<div ref={this.state.commentSectionRef} className="mb-2" />
|
<div ref={this.state.commentSectionRef} className="mb-2" />
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,8 @@ import { PrivateMessageForm } from "./private-message-form";
|
||||||
import { getHttpBaseInternal } from "../../utils/env";
|
import { getHttpBaseInternal } from "../../utils/env";
|
||||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
import { IRoutePropsWithFetch } from "../../routes";
|
import { IRoutePropsWithFetch } from "../../routes";
|
||||||
|
import { resourcesSettled } from "@utils/helpers";
|
||||||
|
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||||
|
|
||||||
type CreatePrivateMessageData = RouteDataResponse<{
|
type CreatePrivateMessageData = RouteDataResponse<{
|
||||||
recipientDetailsResponse: GetPersonDetailsResponse;
|
recipientDetailsResponse: GetPersonDetailsResponse;
|
||||||
|
@ -45,6 +47,7 @@ export type CreatePrivateMessageFetchConfig = IRoutePropsWithFetch<
|
||||||
Record<string, never>
|
Record<string, never>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
export class CreatePrivateMessage extends Component<
|
export class CreatePrivateMessage extends Component<
|
||||||
CreatePrivateMessageRouteProps,
|
CreatePrivateMessageRouteProps,
|
||||||
CreatePrivateMessageState
|
CreatePrivateMessageState
|
||||||
|
@ -57,6 +60,10 @@ export class CreatePrivateMessage extends Component<
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadingSettled() {
|
||||||
|
return resourcesSettled([this.state.recipientRes]);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.handlePrivateMessageCreate =
|
this.handlePrivateMessageCreate =
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { relTags } from "../../config";
|
import { relTags } from "../../config";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
import { setupTippy } from "../../tippy";
|
|
||||||
import { Icon } from "../common/icon";
|
import { Icon } from "../common/icon";
|
||||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
|
@ -50,10 +49,6 @@ export class PrivateMessageForm extends Component<
|
||||||
this.handleContentChange = this.handleContentChange.bind(this);
|
this.handleContentChange = this.handleContentChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
setupTippy();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(
|
componentWillReceiveProps(
|
||||||
nextProps: Readonly<{ children?: InfernoNode } & PrivateMessageFormProps>,
|
nextProps: Readonly<{ children?: InfernoNode } & PrivateMessageFormProps>,
|
||||||
): void {
|
): void {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { mdToHtml } from "../../markdown";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
report: PrivateMessageReportView;
|
report: PrivateMessageReportView;
|
||||||
|
@ -18,6 +19,7 @@ interface State {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class PrivateMessageReport extends Component<Props, State> {
|
export class PrivateMessageReport extends Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { MomentTime } from "../common/moment-time";
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
import { PrivateMessageForm } from "./private-message-form";
|
import { PrivateMessageForm } from "./private-message-form";
|
||||||
import ModActionFormModal from "../common/mod-action-form-modal";
|
import ModActionFormModal from "../common/mod-action-form-modal";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
|
||||||
interface PrivateMessageState {
|
interface PrivateMessageState {
|
||||||
showReply: boolean;
|
showReply: boolean;
|
||||||
|
@ -35,6 +36,7 @@ interface PrivateMessageProps {
|
||||||
onEdit(form: EditPrivateMessage): void;
|
onEdit(form: EditPrivateMessage): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
export class PrivateMessage extends Component<
|
export class PrivateMessage extends Component<
|
||||||
PrivateMessageProps,
|
PrivateMessageProps,
|
||||||
PrivateMessageState
|
PrivateMessageState
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { setIsoData } from "@utils/app";
|
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 { RouteDataResponse } from "@utils/types";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import {
|
import {
|
||||||
|
@ -97,6 +98,7 @@ export type RemoteFetchFetchConfig = IRoutePropsWithFetch<
|
||||||
RemoteFetchProps
|
RemoteFetchProps
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
export class RemoteFetch extends Component<
|
export class RemoteFetch extends Component<
|
||||||
RemoteFetchRouteProps,
|
RemoteFetchRouteProps,
|
||||||
RemoteFetchState
|
RemoteFetchState
|
||||||
|
@ -108,6 +110,10 @@ export class RemoteFetch extends Component<
|
||||||
followCommunityLoading: false,
|
followCommunityLoading: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadingSettled() {
|
||||||
|
return resourcesSettled([this.state.resolveObjectRes]);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: RemoteFetchRouteProps, context: any) {
|
constructor(props: RemoteFetchRouteProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
setIsoData,
|
setIsoData,
|
||||||
showLocal,
|
showLocal,
|
||||||
} from "@utils/app";
|
} from "@utils/app";
|
||||||
import { restoreScrollPosition, saveScrollPosition } from "@utils/browser";
|
import { scrollMixin } from "./mixins/scroll-mixin";
|
||||||
import {
|
import {
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
debounce,
|
debounce,
|
||||||
|
@ -21,6 +21,7 @@ import {
|
||||||
getQueryParams,
|
getQueryParams,
|
||||||
getQueryString,
|
getQueryString,
|
||||||
numToSI,
|
numToSI,
|
||||||
|
resourcesSettled,
|
||||||
} from "@utils/helpers";
|
} from "@utils/helpers";
|
||||||
import type { QueryParams } from "@utils/types";
|
import type { QueryParams } from "@utils/types";
|
||||||
import { Choice, RouteDataResponse } from "@utils/types";
|
import { Choice, RouteDataResponse } from "@utils/types";
|
||||||
|
@ -253,6 +254,7 @@ export type SearchFetchConfig = IRoutePropsWithFetch<
|
||||||
SearchProps
|
SearchProps
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@scrollMixin
|
||||||
export class Search extends Component<SearchRouteProps, SearchState> {
|
export class Search extends Component<SearchRouteProps, SearchState> {
|
||||||
private isoData = setIsoData<SearchData>(this.context);
|
private isoData = setIsoData<SearchData>(this.context);
|
||||||
searchInput = createRef<HTMLInputElement>();
|
searchInput = createRef<HTMLInputElement>();
|
||||||
|
@ -268,6 +270,10 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadingSettled() {
|
||||||
|
return resourcesSettled([this.state.searchRes]);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: SearchRouteProps, context: any) {
|
constructor(props: SearchRouteProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
@ -323,7 +329,9 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
|
if (this.props.history.action !== "POP") {
|
||||||
this.searchInput.current?.select();
|
this.searchInput.current?.select();
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.state.isIsomorphic) {
|
if (!this.state.isIsomorphic) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -397,10 +405,6 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
saveScrollPosition(this.context);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async fetchInitialData({
|
static async fetchInitialData({
|
||||||
headers,
|
headers,
|
||||||
query: {
|
query: {
|
||||||
|
@ -991,8 +995,6 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
window.scrollTo(0, 0);
|
|
||||||
restoreScrollPosition(this.context);
|
|
||||||
|
|
||||||
if (myAuth()) {
|
if (myAuth()) {
|
||||||
this.setState({ resolveObjectRes: LOADING_REQUEST });
|
this.setState({ resolveObjectRes: LOADING_REQUEST });
|
||||||
|
|
|
@ -1,19 +1,55 @@
|
||||||
import { isBrowser } from "@utils/browser";
|
import { RefObject } from "inferno";
|
||||||
import tippy from "tippy.js";
|
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()) {
|
const tippyDelegateOptions: Partial<TippyProps> & { target: string } = {
|
||||||
tippyInstance = tippy("[data-tippy-content]");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupTippy() {
|
|
||||||
if (isBrowser()) {
|
|
||||||
tippyInstance.forEach((e: any) => e.destroy());
|
|
||||||
tippyInstance = tippy("[data-tippy-content]", {
|
|
||||||
delay: [500, 0],
|
delay: [500, 0],
|
||||||
// Display on "long press"
|
// Display on "long press"
|
||||||
touch: ["hold", 500],
|
touch: ["hold", 500],
|
||||||
});
|
target: tippySelector,
|
||||||
|
onShow(i: TippyInstance<TippyProps>) {
|
||||||
|
shownInstances.add(i);
|
||||||
|
},
|
||||||
|
onHidden(i: TippyInstance<TippyProps>) {
|
||||||
|
shownInstances.delete(i);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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 dataBsTheme from "./data-bs-theme";
|
||||||
import isBrowser from "./is-browser";
|
import isBrowser from "./is-browser";
|
||||||
import isDark from "./is-dark";
|
import isDark from "./is-dark";
|
||||||
|
import nextUserAction from "./next-user-action";
|
||||||
import platform from "./platform";
|
import platform from "./platform";
|
||||||
import refreshTheme from "./refresh-theme";
|
import refreshTheme from "./refresh-theme";
|
||||||
import restoreScrollPosition from "./restore-scroll-position";
|
|
||||||
import saveScrollPosition from "./save-scroll-position";
|
|
||||||
import setAuthCookie from "./set-auth-cookie";
|
import setAuthCookie from "./set-auth-cookie";
|
||||||
import setThemeOverride from "./set-theme-override";
|
import setThemeOverride from "./set-theme-override";
|
||||||
import share from "./share";
|
import share from "./share";
|
||||||
|
import snapToTop from "./snap-to-top";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
canShare,
|
canShare,
|
||||||
|
@ -17,11 +17,11 @@ export {
|
||||||
dataBsTheme,
|
dataBsTheme,
|
||||||
isBrowser,
|
isBrowser,
|
||||||
isDark,
|
isDark,
|
||||||
|
nextUserAction,
|
||||||
platform,
|
platform,
|
||||||
refreshTheme,
|
refreshTheme,
|
||||||
restoreScrollPosition,
|
|
||||||
saveScrollPosition,
|
|
||||||
setAuthCookie,
|
setAuthCookie,
|
||||||
setThemeOverride,
|
setThemeOverride,
|
||||||
share,
|
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 numToSI from "./num-to-si";
|
||||||
import poll from "./poll";
|
import poll from "./poll";
|
||||||
import randomStr from "./random-str";
|
import randomStr from "./random-str";
|
||||||
|
import resourcesSettled from "./resources-settled";
|
||||||
import sleep from "./sleep";
|
import sleep from "./sleep";
|
||||||
import validEmail from "./valid-email";
|
import validEmail from "./valid-email";
|
||||||
import validInstanceTLD from "./valid-instance-tld";
|
import validInstanceTLD from "./valid-instance-tld";
|
||||||
|
@ -45,6 +46,7 @@ export {
|
||||||
numToSI,
|
numToSI,
|
||||||
poll,
|
poll,
|
||||||
randomStr,
|
randomStr,
|
||||||
|
resourcesSettled,
|
||||||
sleep,
|
sleep,
|
||||||
validEmail,
|
validEmail,
|
||||||
validInstanceTLD,
|
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,
|
"skipLibCheck": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true, // false for non-legacy decorators
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|
Loading…
Reference in a new issue