Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve login instance selector ease of use for newcomers #1568

Merged
merged 1 commit into from
Aug 10, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 61 additions & 36 deletions src/features/auth/login/login/PickLoginServer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
IonBackButton,
IonButton,
Expand All @@ -12,7 +18,7 @@ import {
IonTitle,
IonToolbar,
} from "@ionic/react";
import { VList } from "virtua";
import { VList, VListHandle } from "virtua";
import { styled } from "@linaria/react";
import { LOGIN_SERVERS } from "../data/servers";
import { getClient } from "../../../../services/lemmy";
Expand Down Expand Up @@ -44,7 +50,7 @@ const StyledIonList = styled(IonList)`
export default function PickLoginServer() {
const presentToast = useAppToast();
const [search, setSearch] = useState("");
const [dirty, setDirty] = useState(false);
const [shouldSubmit, setShouldSubmit] = useState(false);
const searchHostname = stripProtocol(search.trim());
const instances = useMemo(
() =>
Expand All @@ -55,6 +61,8 @@ export default function PickLoginServer() {
);
const [loading, setLoading] = useState(false);

const vHandle = useRef<VListHandle>(null);

const ref = useRef<HTMLDivElement>(null);
const searchbarRef = useRef<HTMLIonSearchbarElement>(null);

Expand All @@ -68,20 +76,22 @@ export default function PickLoginServer() {
[searchHostname],
);

useEffect(() => {
vHandle.current?.scrollTo(0);
}, [search]);

useEffect(() => {
setTimeout(() => {
searchbarRef.current?.setFocus();
}, 300);
}, []);

async function submit() {
const submit = useCallback(async () => {
if (loading) return;

const potentialServer = searchHostname.toLowerCase();

// Dirty input with candidate
if (instances[0] && search !== potentialServer) {
setDirty(false);
setSearch(instances[0]);
return;
}
Expand Down Expand Up @@ -120,7 +130,14 @@ export default function PickLoginServer() {
?.push(() => (
<Login url={potentialServer} siteIcon={site.site_view.site.icon} />
));
}
}, [instances, loading, presentToast, search, searchHostname]);

useEffect(() => {
if (!shouldSubmit) return;

setShouldSubmit(false);
submit();
}, [shouldSubmit, submit]);

return (
<>
Expand All @@ -141,7 +158,7 @@ export default function PickLoginServer() {
</IonButtons>
</IonToolbar>
</AppHeader>
<IonContent>
<IonContent scrollY={false}>
<Container ref={ref}>
<div className="ion-padding">
<IonText color="medium">
Expand All @@ -157,19 +174,26 @@ export default function PickLoginServer() {
onKeyDown={(e) => {
if (e.key !== "Enter") return;

// Invalid search and there is a candidate for autocomplete
if (
searchInvalid &&
instances[0] &&
instances[0] !== searchHostname
) {
setSearch(instances[0]);
return;
}

// Already selected a server
if (!dirty && search) return submit();
if (search) return submit();

// Valid with TLD (for autocomplete search)
if (!searchInvalid) {
setDirty(false);
submit();
return;
}

// Dirty input with candidate
if (instances[0]) {
setDirty(false);
setSearch(instances[0]);
return;
}
Expand All @@ -182,33 +206,34 @@ export default function PickLoginServer() {
}}
value={search}
onIonInput={(e) => {
setDirty(true);
setSearch(e.detail.value || "");
setSearch(e.detail.value ?? "");
}}
/>

{dirty && (
<StyledIonList>
<VList count={instances.length}>
{(i) => {
const instance = instances[i]!;

return (
<IonItem
detail
onClick={() => {
setSearch(instance);
setDirty(false);
searchbarRef.current?.setFocus();
}}
>
{instance}
</IonItem>
);
}}
</VList>
</StyledIonList>
)}
<StyledIonList>
<VList
count={instances.length}
ref={vHandle}
className="ion-content-scroll-host"
>
{(i) => {
const instance = instances[i]!;

return (
<IonItem
detail
onClick={() => {
setSearch(instance);
setShouldSubmit(true);
searchbarRef.current?.setFocus();
}}
>
{instance}
</IonItem>
);
}}
</VList>
</StyledIonList>
</Container>
</IonContent>
</>
Expand Down