feat: add infinite scrolling for memos (#4715)

* feat(WIP): add prop-driven infinite scroll

* feat(WIP): add global toggle for infinite scrolling under settings

* feat: integrate newly added scroll-mode selection state with backend for persists across refreshes

* fix: default to infinite scrolling over load more button & remove redundant setting option

* fix: rectify linting issues

* Update web/src/components/PagedMemoList/PagedMemoList.tsx

---------

Co-authored-by: Johnny <yourselfhosted@gmail.com>
pull/4722/head
Brian 1 month ago committed by GitHub
parent 46d5307d7f
commit c6e6b7b93b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +1,5 @@
import { Button } from "@usememos/mui"; import { Button } from "@usememos/mui";
import { ArrowDownIcon, ArrowUpIcon, LoaderIcon } from "lucide-react"; import { ArrowUpIcon, LoaderIcon } from "lucide-react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { matchPath } from "react-router-dom"; import { matchPath } from "react-router-dom";
@ -71,6 +71,18 @@ const PagedMemoList = observer((props: Props) => {
refreshList(); refreshList();
}, [props.owner, props.state, props.direction, props.filter, props.oldFilter, props.pageSize]); }, [props.owner, props.state, props.direction, props.filter, props.oldFilter, props.pageSize]);
useEffect(() => {
if (!state.nextPageToken) return;
const handleScroll = () => {
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 300;
if (nearBottom && !state.isRequesting) {
fetchMoreMemos(state.nextPageToken);
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [state.nextPageToken, state.isRequesting]);
const children = ( const children = (
<div className="flex flex-col justify-start items-start w-full max-w-full"> <div className="flex flex-col justify-start items-start w-full max-w-full">
<MasonryView <MasonryView
@ -93,12 +105,6 @@ const PagedMemoList = observer((props: Props) => {
</div> </div>
) : ( ) : (
<div className="w-full opacity-70 flex flex-row justify-center items-center my-4"> <div className="w-full opacity-70 flex flex-row justify-center items-center my-4">
{state.nextPageToken && (
<Button variant="plain" onClick={() => fetchMoreMemos(state.nextPageToken)}>
{t("memo.load-more")}
<ArrowDownIcon className="ml-1 w-4 h-auto" />
</Button>
)}
<BackToTop /> <BackToTop />
</div> </div>
)} )}

@ -15,44 +15,33 @@ const PreferencesSection = observer(() => {
const setting = userStore.state.userSetting as UserSetting; const setting = userStore.state.userSetting as UserSetting;
const handleLocaleSelectChange = async (locale: Locale) => { const handleLocaleSelectChange = async (locale: Locale) => {
await userStore.updateUserSetting( await userStore.updateUserSetting({ locale }, ["locale"]);
{
locale,
},
["locale"],
);
}; };
const handleAppearanceSelectChange = async (appearance: Appearance) => { const handleAppearanceSelectChange = async (appearance: Appearance) => {
await userStore.updateUserSetting( await userStore.updateUserSetting({ appearance }, ["appearance"]);
{
appearance,
},
["appearance"],
);
}; };
const handleDefaultMemoVisibilityChanged = async (value: string) => { const handleDefaultMemoVisibilityChanged = async (value: string) => {
await userStore.updateUserSetting( await userStore.updateUserSetting({ memoVisibility: value }, ["memo_visibility"]);
{
memoVisibility: value,
},
["memo_visibility"],
);
}; };
return ( return (
<div className="w-full flex flex-col gap-2 pt-2 pb-4"> <div className="w-full flex flex-col gap-2 pt-2 pb-4">
<p className="font-medium text-gray-700 dark:text-gray-500">{t("common.basic")}</p> <p className="font-medium text-gray-700 dark:text-gray-500">{t("common.basic")}</p>
<div className="w-full flex flex-row justify-between items-center"> <div className="w-full flex flex-row justify-between items-center">
<span>{t("common.language")}</span> <span>{t("common.language")}</span>
<LocaleSelect value={setting.locale} onChange={handleLocaleSelectChange} /> <LocaleSelect value={setting.locale} onChange={handleLocaleSelectChange} />
</div> </div>
<div className="w-full flex flex-row justify-between items-center"> <div className="w-full flex flex-row justify-between items-center">
<span>{t("setting.preference-section.theme")}</span> <span>{t("setting.preference-section.theme")}</span>
<AppearanceSelect value={setting.appearance as Appearance} onChange={handleAppearanceSelectChange} /> <AppearanceSelect value={setting.appearance as Appearance} onChange={handleAppearanceSelectChange} />
</div> </div>
<p className="font-medium text-gray-700 dark:text-gray-500">{t("setting.preference")}</p> <p className="font-medium text-gray-700 dark:text-gray-500">{t("setting.preference")}</p>
<div className="w-full flex flex-row justify-between items-center"> <div className="w-full flex flex-row justify-between items-center">
<span className="truncate">{t("setting.preference-section.default-memo-visibility")}</span> <span className="truncate">{t("setting.preference-section.default-memo-visibility")}</span>
<Select <Select

@ -35,7 +35,7 @@ export enum Edition {
EDITION_2024 = "EDITION_2024", EDITION_2024 = "EDITION_2024",
/** /**
* EDITION_1_TEST_ONLY - Placeholder editions for testing feature resolution. These should not be * EDITION_1_TEST_ONLY - Placeholder editions for testing feature resolution. These should not be
* used or relyed on outside of tests. * used or relied on outside of tests.
*/ */
EDITION_1_TEST_ONLY = "EDITION_1_TEST_ONLY", EDITION_1_TEST_ONLY = "EDITION_1_TEST_ONLY",
EDITION_2_TEST_ONLY = "EDITION_2_TEST_ONLY", EDITION_2_TEST_ONLY = "EDITION_2_TEST_ONLY",
@ -177,11 +177,19 @@ export interface FileDescriptorProto {
* The supported values are "proto2", "proto3", and "editions". * The supported values are "proto2", "proto3", and "editions".
* *
* If `edition` is present, this value must be "editions". * If `edition` is present, this value must be "editions".
* WARNING: This field should only be used by protobuf plugins or special
* cases like the proto compiler. Other uses are discouraged and
* developers should rely on the protoreflect APIs for their client language.
*/ */
syntax?: syntax?:
| string | string
| undefined; | undefined;
/** The edition of the proto file. */ /**
* The edition of the proto file.
* WARNING: This field should only be used by protobuf plugins or special
* cases like the proto compiler. Other uses are discouraged and
* developers should rely on the protoreflect APIs for their client language.
*/
edition?: Edition | undefined; edition?: Edition | undefined;
} }
@ -828,7 +836,12 @@ export interface FileOptions {
rubyPackage?: rubyPackage?:
| string | string
| undefined; | undefined;
/** Any features defined in the specific edition. */ /**
* Any features defined in the specific edition.
* WARNING: This field should only be used by protobuf plugins or special
* cases like the proto compiler. Other uses are discouraged and
* developers should rely on the protoreflect APIs for their client language.
*/
features?: features?:
| FeatureSet | FeatureSet
| undefined; | undefined;
@ -966,7 +979,12 @@ export interface MessageOptions {
deprecatedLegacyJsonFieldConflicts?: deprecatedLegacyJsonFieldConflicts?:
| boolean | boolean
| undefined; | undefined;
/** Any features defined in the specific edition. */ /**
* Any features defined in the specific edition.
* WARNING: This field should only be used by protobuf plugins or special
* cases like the proto compiler. Other uses are discouraged and
* developers should rely on the protoreflect APIs for their client language.
*/
features?: features?:
| FeatureSet | FeatureSet
| undefined; | undefined;
@ -976,12 +994,13 @@ export interface MessageOptions {
export interface FieldOptions { export interface FieldOptions {
/** /**
* NOTE: ctype is deprecated. Use `features.(pb.cpp).string_type` instead.
* The ctype option instructs the C++ code generator to use a different * The ctype option instructs the C++ code generator to use a different
* representation of the field than it normally would. See the specific * representation of the field than it normally would. See the specific
* options below. This option is only implemented to support use of * options below. This option is only implemented to support use of
* [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of * [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of
* type "bytes" in the open source release -- sorry, we'll try to include * type "bytes" in the open source release.
* other types in a future version! * TODO: make ctype actually deprecated.
*/ */
ctype?: ctype?:
| FieldOptions_CType | FieldOptions_CType
@ -1070,7 +1089,12 @@ export interface FieldOptions {
retention?: FieldOptions_OptionRetention | undefined; retention?: FieldOptions_OptionRetention | undefined;
targets: FieldOptions_OptionTargetType[]; targets: FieldOptions_OptionTargetType[];
editionDefaults: FieldOptions_EditionDefault[]; editionDefaults: FieldOptions_EditionDefault[];
/** Any features defined in the specific edition. */ /**
* Any features defined in the specific edition.
* WARNING: This field should only be used by protobuf plugins or special
* cases like the proto compiler. Other uses are discouraged and
* developers should rely on the protoreflect APIs for their client language.
*/
features?: FeatureSet | undefined; features?: FeatureSet | undefined;
featureSupport?: featureSupport?:
| FieldOptions_FeatureSupport | FieldOptions_FeatureSupport
@ -1169,11 +1193,7 @@ export function fieldOptions_JSTypeToNumber(object: FieldOptions_JSType): number
} }
} }
/** /** If set to RETENTION_SOURCE, the option will be omitted from the binary. */
* If set to RETENTION_SOURCE, the option will be omitted from the binary.
* Note: as of January 2023, support for this is in progress and does not yet
* have an effect (b/264593489).
*/
export enum FieldOptions_OptionRetention { export enum FieldOptions_OptionRetention {
RETENTION_UNKNOWN = "RETENTION_UNKNOWN", RETENTION_UNKNOWN = "RETENTION_UNKNOWN",
RETENTION_RUNTIME = "RETENTION_RUNTIME", RETENTION_RUNTIME = "RETENTION_RUNTIME",
@ -1216,8 +1236,7 @@ export function fieldOptions_OptionRetentionToNumber(object: FieldOptions_Option
/** /**
* This indicates the types of entities that the field may apply to when used * This indicates the types of entities that the field may apply to when used
* as an option. If it is unset, then the field may be freely used as an * as an option. If it is unset, then the field may be freely used as an
* option on any kind of entity. Note: as of January 2023, support for this is * option on any kind of entity.
* in progress and does not yet have an effect (b/264593489).
*/ */
export enum FieldOptions_OptionTargetType { export enum FieldOptions_OptionTargetType {
TARGET_TYPE_UNKNOWN = "TARGET_TYPE_UNKNOWN", TARGET_TYPE_UNKNOWN = "TARGET_TYPE_UNKNOWN",
@ -1341,7 +1360,12 @@ export interface FieldOptions_FeatureSupport {
} }
export interface OneofOptions { export interface OneofOptions {
/** Any features defined in the specific edition. */ /**
* Any features defined in the specific edition.
* WARNING: This field should only be used by protobuf plugins or special
* cases like the proto compiler. Other uses are discouraged and
* developers should rely on the protoreflect APIs for their client language.
*/
features?: features?:
| FeatureSet | FeatureSet
| undefined; | undefined;
@ -1379,7 +1403,12 @@ export interface EnumOptions {
deprecatedLegacyJsonFieldConflicts?: deprecatedLegacyJsonFieldConflicts?:
| boolean | boolean
| undefined; | undefined;
/** Any features defined in the specific edition. */ /**
* Any features defined in the specific edition.
* WARNING: This field should only be used by protobuf plugins or special
* cases like the proto compiler. Other uses are discouraged and
* developers should rely on the protoreflect APIs for their client language.
*/
features?: features?:
| FeatureSet | FeatureSet
| undefined; | undefined;
@ -1397,7 +1426,12 @@ export interface EnumValueOptions {
deprecated?: deprecated?:
| boolean | boolean
| undefined; | undefined;
/** Any features defined in the specific edition. */ /**
* Any features defined in the specific edition.
* WARNING: This field should only be used by protobuf plugins or special
* cases like the proto compiler. Other uses are discouraged and
* developers should rely on the protoreflect APIs for their client language.
*/
features?: features?:
| FeatureSet | FeatureSet
| undefined; | undefined;
@ -1418,7 +1452,12 @@ export interface EnumValueOptions {
} }
export interface ServiceOptions { export interface ServiceOptions {
/** Any features defined in the specific edition. */ /**
* Any features defined in the specific edition.
* WARNING: This field should only be used by protobuf plugins or special
* cases like the proto compiler. Other uses are discouraged and
* developers should rely on the protoreflect APIs for their client language.
*/
features?: features?:
| FeatureSet | FeatureSet
| undefined; | undefined;
@ -1446,7 +1485,12 @@ export interface MethodOptions {
idempotencyLevel?: idempotencyLevel?:
| MethodOptions_IdempotencyLevel | MethodOptions_IdempotencyLevel
| undefined; | undefined;
/** Any features defined in the specific edition. */ /**
* Any features defined in the specific edition.
* WARNING: This field should only be used by protobuf plugins or special
* cases like the proto compiler. Other uses are discouraged and
* developers should rely on the protoreflect APIs for their client language.
*/
features?: features?:
| FeatureSet | FeatureSet
| undefined; | undefined;
@ -1549,6 +1593,7 @@ export interface FeatureSet {
utf8Validation?: FeatureSet_Utf8Validation | undefined; utf8Validation?: FeatureSet_Utf8Validation | undefined;
messageEncoding?: FeatureSet_MessageEncoding | undefined; messageEncoding?: FeatureSet_MessageEncoding | undefined;
jsonFormat?: FeatureSet_JsonFormat | undefined; jsonFormat?: FeatureSet_JsonFormat | undefined;
enforceNamingStyle?: FeatureSet_EnforceNamingStyle | undefined;
} }
export enum FeatureSet_FieldPresence { export enum FeatureSet_FieldPresence {
@ -1791,6 +1836,45 @@ export function featureSet_JsonFormatToNumber(object: FeatureSet_JsonFormat): nu
} }
} }
export enum FeatureSet_EnforceNamingStyle {
ENFORCE_NAMING_STYLE_UNKNOWN = "ENFORCE_NAMING_STYLE_UNKNOWN",
STYLE2024 = "STYLE2024",
STYLE_LEGACY = "STYLE_LEGACY",
UNRECOGNIZED = "UNRECOGNIZED",
}
export function featureSet_EnforceNamingStyleFromJSON(object: any): FeatureSet_EnforceNamingStyle {
switch (object) {
case 0:
case "ENFORCE_NAMING_STYLE_UNKNOWN":
return FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN;
case 1:
case "STYLE2024":
return FeatureSet_EnforceNamingStyle.STYLE2024;
case 2:
case "STYLE_LEGACY":
return FeatureSet_EnforceNamingStyle.STYLE_LEGACY;
case -1:
case "UNRECOGNIZED":
default:
return FeatureSet_EnforceNamingStyle.UNRECOGNIZED;
}
}
export function featureSet_EnforceNamingStyleToNumber(object: FeatureSet_EnforceNamingStyle): number {
switch (object) {
case FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN:
return 0;
case FeatureSet_EnforceNamingStyle.STYLE2024:
return 1;
case FeatureSet_EnforceNamingStyle.STYLE_LEGACY:
return 2;
case FeatureSet_EnforceNamingStyle.UNRECOGNIZED:
default:
return -1;
}
}
/** /**
* A compiled specification for the defaults of a set of features. These * A compiled specification for the defaults of a set of features. These
* messages are generated from FeatureSet extensions and can be used to seed * messages are generated from FeatureSet extensions and can be used to seed
@ -4914,6 +4998,7 @@ function createBaseFeatureSet(): FeatureSet {
utf8Validation: FeatureSet_Utf8Validation.UTF8_VALIDATION_UNKNOWN, utf8Validation: FeatureSet_Utf8Validation.UTF8_VALIDATION_UNKNOWN,
messageEncoding: FeatureSet_MessageEncoding.MESSAGE_ENCODING_UNKNOWN, messageEncoding: FeatureSet_MessageEncoding.MESSAGE_ENCODING_UNKNOWN,
jsonFormat: FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN, jsonFormat: FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN,
enforceNamingStyle: FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN,
}; };
} }
@ -4948,6 +5033,12 @@ export const FeatureSet: MessageFns<FeatureSet> = {
if (message.jsonFormat !== undefined && message.jsonFormat !== FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN) { if (message.jsonFormat !== undefined && message.jsonFormat !== FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN) {
writer.uint32(48).int32(featureSet_JsonFormatToNumber(message.jsonFormat)); writer.uint32(48).int32(featureSet_JsonFormatToNumber(message.jsonFormat));
} }
if (
message.enforceNamingStyle !== undefined &&
message.enforceNamingStyle !== FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN
) {
writer.uint32(56).int32(featureSet_EnforceNamingStyleToNumber(message.enforceNamingStyle));
}
return writer; return writer;
}, },
@ -5006,6 +5097,14 @@ export const FeatureSet: MessageFns<FeatureSet> = {
message.jsonFormat = featureSet_JsonFormatFromJSON(reader.int32()); message.jsonFormat = featureSet_JsonFormatFromJSON(reader.int32());
continue; continue;
} }
case 7: {
if (tag !== 56) {
break;
}
message.enforceNamingStyle = featureSet_EnforceNamingStyleFromJSON(reader.int32());
continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
@ -5027,6 +5126,8 @@ export const FeatureSet: MessageFns<FeatureSet> = {
message.utf8Validation = object.utf8Validation ?? FeatureSet_Utf8Validation.UTF8_VALIDATION_UNKNOWN; message.utf8Validation = object.utf8Validation ?? FeatureSet_Utf8Validation.UTF8_VALIDATION_UNKNOWN;
message.messageEncoding = object.messageEncoding ?? FeatureSet_MessageEncoding.MESSAGE_ENCODING_UNKNOWN; message.messageEncoding = object.messageEncoding ?? FeatureSet_MessageEncoding.MESSAGE_ENCODING_UNKNOWN;
message.jsonFormat = object.jsonFormat ?? FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN; message.jsonFormat = object.jsonFormat ?? FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN;
message.enforceNamingStyle = object.enforceNamingStyle ??
FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN;
return message; return message;
}, },
}; };

Loading…
Cancel
Save