feat: swipe to remove individual server logins

This commit is contained in:
Fredrik Burmester
2026-01-06 15:46:12 +01:00
parent a24e254a9e
commit 055357de60
4 changed files with 119 additions and 33 deletions

View File

@@ -283,7 +283,8 @@ const Login: React.FC = () => {
keyboardType='default'
returnKeyType='done'
autoCapitalize='none'
textContentType='oneTimeCode'
autoCorrect={false}
textContentType='username'
clearButtonMode='while-editing'
maxLength={500}
extraClassName='mb-4'
@@ -440,9 +441,8 @@ const Login: React.FC = () => {
keyboardType='default'
returnKeyType='done'
autoCapitalize='none'
// Changed from username to oneTimeCode because it is a known issue in RN
// https://github.com/facebook/react-native/issues/47106#issuecomment-2521270037
textContentType='oneTimeCode'
autoCorrect={false}
textContentType='username'
clearButtonMode='while-editing'
maxLength={500}
/>

View File

@@ -1,14 +1,17 @@
import { Ionicons } from "@expo/vector-icons";
import type React from "react";
import { useMemo, useState } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, Alert, TouchableOpacity, View } from "react-native";
import { Swipeable } from "react-native-gesture-handler";
import { useMMKVString } from "react-native-mmkv";
import { Colors } from "@/constants/Colors";
import {
deleteServerCredential,
removeServerFromList,
type SavedServer,
} from "@/utils/secureCredentials";
import { Text } from "./common/Text";
import { ListGroup } from "./list/ListGroup";
import { ListItem } from "./list/ListItem";
@@ -78,41 +81,46 @@ export const PreviousServersList: React.FC<PreviousServersListProps> = ({
);
};
const handleRemoveServer = useCallback(
async (serverUrl: string) => {
await removeServerFromList(serverUrl);
// Update UI
const filtered = previousServers.filter((s) => s.address !== serverUrl);
setPreviousServers(JSON.stringify(filtered));
},
[previousServers, setPreviousServers],
);
const renderRightActions = useCallback(
(serverUrl: string, swipeableRef: React.RefObject<Swipeable | null>) => (
<TouchableOpacity
onPress={() => {
swipeableRef.current?.close();
handleRemoveServer(serverUrl);
}}
className='bg-red-600 justify-center items-center px-5'
>
<Ionicons name='trash' size={20} color='white' />
</TouchableOpacity>
),
[handleRemoveServer],
);
if (!previousServers.length) return null;
return (
<View>
<ListGroup title={t("server.previous_servers")} className='mt-4'>
{previousServers.map((s) => (
<ListItem
<ServerItem
key={s.address}
server={s}
loadingServer={loadingServer}
onPress={() => handleServerPress(s)}
title={s.name || s.address}
subtitle={
s.hasCredentials
? `${s.username}${t("server.saved")}`
: s.name
? s.address
: undefined
}
showArrow={loadingServer !== s.address}
disabled={loadingServer === s.address}
>
{loadingServer === s.address ? (
<ActivityIndicator size='small' color={Colors.primary} />
) : s.hasCredentials ? (
<TouchableOpacity
onPress={(e) => {
e.stopPropagation();
handleRemoveCredential(s.address);
}}
className='p-1'
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Ionicons name='key' size={16} color={Colors.primary} />
</TouchableOpacity>
) : null}
</ListItem>
onRemoveCredential={() => handleRemoveCredential(s.address)}
renderRightActions={renderRightActions}
t={t}
/>
))}
<ListItem
onPress={() => {
@@ -122,6 +130,71 @@ export const PreviousServersList: React.FC<PreviousServersListProps> = ({
textColor='red'
/>
</ListGroup>
<Text className='text-xs text-neutral-500 mt-2 ml-4'>
{t("server.swipe_to_remove")}
</Text>
</View>
);
};
interface ServerItemProps {
server: SavedServer;
loadingServer: string | null;
onPress: () => void;
onRemoveCredential: () => void;
renderRightActions: (
serverUrl: string,
swipeableRef: React.RefObject<Swipeable | null>,
) => React.ReactNode;
t: (key: string) => string;
}
const ServerItem: React.FC<ServerItemProps> = ({
server,
loadingServer,
onPress,
onRemoveCredential,
renderRightActions,
t,
}) => {
const swipeableRef = useRef<Swipeable>(null);
return (
<Swipeable
ref={swipeableRef}
renderRightActions={() =>
renderRightActions(server.address, swipeableRef)
}
overshootRight={false}
>
<ListItem
onPress={onPress}
title={server.name || server.address}
subtitle={
server.hasCredentials
? `${server.username}${t("server.saved")}`
: server.name
? server.address
: undefined
}
showArrow={loadingServer !== server.address}
disabled={loadingServer === server.address}
>
{loadingServer === server.address ? (
<ActivityIndicator size='small' color={Colors.primary} />
) : server.hasCredentials ? (
<TouchableOpacity
onPress={(e) => {
e.stopPropagation();
onRemoveCredential();
}}
className='p-1'
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Ionicons name='key' size={16} color={Colors.primary} />
</TouchableOpacity>
) : null}
</ListItem>
</Swipeable>
);
};

View File

@@ -29,7 +29,8 @@
"server_url_placeholder": "http(s)://your-server.com",
"connect_button": "Connect",
"previous_servers": "Previous Servers",
"clear_button": "Clear",
"clear_button": "Clear all",
"swipe_to_remove": "Swipe to remove",
"search_for_local_servers": "Search for Local Servers",
"searching": "Searching...",
"servers": "Servers",

View File

@@ -136,6 +136,18 @@ export function getPreviousServers(): SavedServer[] {
return [];
}
/**
* Remove a server from the previous servers list and delete its credentials.
*/
export async function removeServerFromList(serverUrl: string): Promise<void> {
// First delete any saved credentials
await deleteServerCredential(serverUrl);
// Then remove from the list
const previousServers = getPreviousServers();
const filtered = previousServers.filter((s) => s.address !== serverUrl);
storage.set("previousServers", JSON.stringify(filtered));
}
/**
* Migrate existing previousServers to new format (add hasCredentials: false).
* Should be called on app startup.