diff --git a/package-lock.json b/package-lock.json index f397e14..9d51b77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "react-number-format": "^5.3.1", "react-redux": "^8.1.1", "react-router-dom": "^6.20.1", + "react-window": "^1.8.10", "redux": "^4.2.1", "redux-persist": "^6.0.0", "remove-markdown": "^0.5.0", @@ -5840,6 +5841,11 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -7104,6 +7110,22 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-window": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", diff --git a/package.json b/package.json index bb4a097..480112c 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "css-minimizer-webpack-plugin": "^5.0.1", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.3", - "lemmy-js-client18": "npm:lemmy-js-client@^0.18.1", "lemmy-js-client": "npm:lemmy-js-client@^0.19.0-rc.19", + "lemmy-js-client18": "npm:lemmy-js-client@^0.18.1", "moment": "^2.29.4", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -45,9 +45,10 @@ "react-number-format": "^5.3.1", "react-redux": "^8.1.1", "react-router-dom": "^6.20.1", + "react-window": "^1.8.10", "redux": "^4.2.1", - "remove-markdown": "^0.5.0", "redux-persist": "^6.0.0", + "remove-markdown": "^0.5.0", "sass": "^1.69.5", "sass-loader": "^13.3.2", "sonner": "^1.2.4", diff --git a/src/components/InstanceSelect.jsx b/src/components/InstanceSelect.jsx new file mode 100644 index 0000000..1ba159b --- /dev/null +++ b/src/components/InstanceSelect.jsx @@ -0,0 +1,195 @@ +import React from "react"; +// import { connect } from "react-redux"; + +// import useQueryCache from "../../hooks/useQueryCache"; + +import { FixedSizeList } from "react-window"; + +import { Popper } from "@mui/base/Popper"; +import Autocomplete, { createFilterOptions } from "@mui/joy/Autocomplete"; +import AutocompleteListbox from "@mui/joy/AutocompleteListbox"; +import AutocompleteOption from "@mui/joy/AutocompleteOption"; +import FormControl from "@mui/joy/FormControl"; +import ListItemDecorator from "@mui/joy/ListItemDecorator"; + +import Add from "@mui/icons-material/Add"; + +// import { setHomeInstance } from "../../reducers/configReducer"; + +import useLVQueryCache from "../hooks/useLVQueryCache"; +/** + * This component renders a button that allows the user to select a home instance. + * + * It uses a react-window Virtualized List to render the list of instances. + */ + +const filterOptions = createFilterOptions({ + // matchFrom: "start", + stringify: (option) => option.base, + trim: true, + ignoreCase: true, +}); + +const LISTBOX_PADDING = 6; // px + +function renderRow(props) { + const { data, index, style } = props; + const dataSet = data[index]; + const inlineStyle = { + ...style, + top: style.top + LISTBOX_PADDING, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }; + + return ( + + {dataSet[1].name?.startsWith('Add "') && ( + + + + )} + {typeof dataSet[1] == "string" && dataSet[1]} + {dataSet[1].base && ( + <> + {dataSet[1].name} ({dataSet[1].base}) + + )} + + ); +} + +const OuterElementContext = React.createContext({}); + +const OuterElementType = React.forwardRef((props, ref) => { + const outerProps = React.useContext(OuterElementContext); + return ( + + ); +}); + +// Adapter for react-window +const ListboxComponent = React.forwardRef(function ListboxComponent(props, ref) { + const { children, anchorEl, open, modifiers, ...other } = props; + const itemData = []; + + children[0].forEach((item) => { + if (item) { + itemData.push(item); + itemData.push(...(item.children || [])); + } + }); + + const itemCount = itemData.length; + const itemSize = 40; + + return ( + + + ({ + zIndex: theme.zIndex.modal + 1000, + })} + > + {renderRow} + + + + ); +}); + +export default function InstanceSelect({ + placeholder, + value, + onChange, + sx, + variant, + color, + disabled, + ...props +}) { + const { isLoading, error, data } = useLVQueryCache("instanceMinData", "instance.min"); + + const handleChange = (event, newValue) => { + console.log("onChange", newValue); + + onChange(newValue ? newValue.base : ""); + }; + + return ( + option.code === value.code} + renderOption={(props, option) => [props, option]} + // TODO: Post React 18 update - validate this conversion, look like a hidden bug + // renderGroup={(params) => params} + filterOptions={(options, params) => { + const filtered = filterOptions(options, params); + + const { inputValue } = params; + + // Suggest the creation of a new value + const isExisting = options.some((option) => inputValue === option.base); + if (inputValue !== "" && !isExisting) { + const cleanedUrl = inputValue + .replace("http:", "") + .replace("https:", "") + .replace("//", "") + .replace("/", ""); + filtered.push({ + name: `Other`, + base: cleanedUrl, + }); + } + + return filtered; + }} + getOptionLabel={(option) => { + // console.log("getOptionLabel", option); + // Value selected with enter, right from the input + if (typeof option === "string") { + return option; + } + + // Regular option + return option.base; + }} + /> + ); +} diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index ba771cc..ef88414 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -18,6 +18,8 @@ import ListItemContent from "@mui/joy/ListItemContent"; import IconButton from "@mui/joy/IconButton"; import Delete from "@mui/icons-material/Delete"; +import InstanceSelect from "../components/InstanceSelect.jsx"; + import LemmyHttpMixed from "../lib/LemmyHttpMixed"; import { LemmyHttp } from "lemmy-js-client"; @@ -154,7 +156,17 @@ export default function LoginForm() { width: "60%", }} > - (domainLock ? null : setInstanceBase(newValue))} + sx={{ mb: 1, width: "100%" }} + disabled={domainLock || accountIsLoading} + variant="outlined" + color="neutral" + /> + + {/* (domainLock ? null : setInstanceBase(e.target.value))} @@ -162,7 +174,7 @@ export default function LoginForm() { color="neutral" sx={{ mb: 1, width: "100%" }} disabled={domainLock || accountIsLoading} - /> + /> */}