diff --git a/src/containers/JoinServerPrompt/index.tsx b/src/containers/JoinServerPrompt/index.tsx index 1fca04ed..29e92688 100644 --- a/src/containers/JoinServerPrompt/index.tsx +++ b/src/containers/JoinServerPrompt/index.tsx @@ -1,8 +1,9 @@ import { fs } from "@tauri-apps/api"; import { t } from "i18next"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Image, + Pressable, StyleSheet, TextInput, TouchableOpacity, @@ -46,7 +47,10 @@ const JoinServerPrompt = () => { SAMPDLLVersions | undefined >(); const [perServerNickname, setPerServerNickname] = useState(""); - const { nickName, gtasaPath, sampVersion, setSampVersion } = useSettings(); + const [dropdownOpen, setDropdownOpen] = useState(false); + const hideTimer = useRef | null>(null); + const { nickName, gtasaPath, sampVersion, setSampVersion, recentNicknames } = + useSettings(); const settings = useMemo(() => { if (server) { @@ -192,6 +196,32 @@ const JoinServerPrompt = () => { [server, settings, setServerSettings] ); + const openDropdown = useCallback(() => { + if (hideTimer.current) clearTimeout(hideTimer.current); + setDropdownOpen(true); + }, []); + + const closeDropdown = useCallback(() => { + if (hideTimer.current) clearTimeout(hideTimer.current); + hideTimer.current = setTimeout(() => setDropdownOpen(false), 150); + }, []); + + const handleRecentNicknamePress = useCallback( + (name: string) => { + setPerServerNickname(name); + if (server) { + if (settings) { + setServerSettings(server, name, settings.sampVersion); + } else { + setServerSettings(server, name, undefined); + } + } + if (hideTimer.current) clearTimeout(hideTimer.current); + setDropdownOpen(false); + }, + [server, settings, setServerSettings] + ); + const handleConnect = useCallback(() => { if (server) { if (server.hasPassword && password.length) { @@ -326,14 +356,47 @@ const JoinServerPrompt = () => { {t("nickname")}: - + + + {dropdownOpen && recentNicknames.length > 0 && ( + + {recentNicknames.map((name) => ( + handleRecentNicknamePress(name)} + style={styles.dropdownItem} + > + + {name} + + + ))} + + )} + { const { t, i18n } = useTranslation(); const { theme } = useTheme(); - const { nickName, setNickName } = useSettings(); + const { nickName, setNickName, recentNicknames } = useSettings(); const { setListType, listType } = useGenericTempState(); + const [dropdownOpen, setDropdownOpen] = useState(false); + const hideTimer = useRef | null>(null); + + const openDropdown = useCallback(() => { + if (hideTimer.current) clearTimeout(hideTimer.current); + setDropdownOpen(true); + }, []); + + const closeDropdown = useCallback(() => { + if (hideTimer.current) clearTimeout(hideTimer.current); + hideTimer.current = setTimeout(() => setDropdownOpen(false), 150); + }, []); + + const handleRecentNicknamePress = useCallback( + (name: string) => { + setNickName(name); + if (hideTimer.current) clearTimeout(hideTimer.current); + setDropdownOpen(false); + }, + [setNickName] + ); + const tabList = useMemo( () => [ { @@ -88,13 +110,46 @@ const NavBar = memo(() => { color={theme.textSecondary} /> - + + + {dropdownOpen && recentNicknames.length > 0 && ( + + {recentNicknames.map((name) => ( + handleRecentNicknamePress(name)} + style={styles.dropdownItem} + > + + {name} + + + ))} + + )} + @@ -128,6 +183,27 @@ const styles = StyleSheet.create({ flexDirection: "row", alignItems: "center", }, + nicknameInputWrapper: { + position: "relative", + }, + dropdown: { + position: "absolute", + top: sc(36), + left: 0, + width: sc(160), + borderRadius: sc(5), + borderWidth: 1, + overflow: "hidden", + zIndex: 100, + }, + dropdownItem: { + paddingHorizontal: sc(8), + paddingVertical: sc(7), + }, + dropdownItemText: { + fontFamily: "Proxima Nova Regular", + fontSize: sc(15), + }, nicknameIconContainer: { height: sc(35), width: sc(35), diff --git a/src/states/settings.ts b/src/states/settings.ts index 7cd7784a..bf126112 100644 --- a/src/states/settings.ts +++ b/src/states/settings.ts @@ -5,13 +5,17 @@ import { createJSONStorage, persist } from "zustand/middleware"; import { stateStorage } from "../utils/stateStorage"; import { SAMPDLLVersions } from "../utils/types"; +const MAX_RECENT_NICKNAMES = 5; + interface SettingsPersistentState { nickName: string; gtasaPath: string; customGameExe: string; sampVersion: SAMPDLLVersions; dataMerged: boolean; + recentNicknames: string[]; setNickName: (name: string) => void; + addRecentNickname: (name: string) => void; setGTASAPath: (path: string) => void; setCustomGameExe: (fileName: string) => void; setSampVersion: (version: SAMPDLLVersions) => void; @@ -28,11 +32,24 @@ const useSettings = create()( customGameExe: "", sampVersion: "custom", dataMerged: false, + recentNicknames: [], setNickName: (name) => set(() => { emitWithDelay("setNickName", name); return { nickName: name }; }), + addRecentNickname: (name) => + set((state) => { + const trimmed = name.trim(); + if (!trimmed) return state; + emitWithDelay("setRecentNickname", trimmed); + const rest = state.recentNicknames.filter( + (n) => n.toLowerCase() !== trimmed.toLowerCase() + ); + return { + recentNicknames: [trimmed, ...rest].slice(0, MAX_RECENT_NICKNAMES), + }; + }), setGTASAPath: (path) => set({ gtasaPath: path }), setCustomGameExe: (fileName) => set({ customGameExe: fileName }), setSampVersion: (version) => set({ sampVersion: version }), @@ -44,7 +61,7 @@ const useSettings = create()( ) ); -["setNickName"].forEach((event) => +["setNickName", "setRecentNickname"].forEach((event) => listen(event, (ev) => { if (ev.windowLabel !== appWindow.label) { useSettings.persist.rehydrate(); diff --git a/src/utils/game.ts b/src/utils/game.ts index a0bb3442..0469bce8 100644 --- a/src/utils/game.ts +++ b/src/utils/game.ts @@ -82,6 +82,7 @@ export const startGame = async ( }:${nickname}:${password}` : `connect:${await getIpAddress(server.ip)}:${server.port}:${nickname}`, }); + useSettings.getState().addRecentNickname(nickname); return; } @@ -216,8 +217,8 @@ export const startGame = async ( sampVersion === "custom" ? idealSAMPDllPath : file - ? await getLocalPath(file.path, file.name) - : idealSAMPDllPath; + ? await getLocalPath(file.path, file.name) + : idealSAMPDllPath; invoke("inject", { name: nickname, @@ -232,6 +233,7 @@ export const startGame = async ( .then(() => { addToRecentlyJoined(server); setSelected(undefined); + useSettings.getState().addRecentNickname(nickname); }) .catch((e) => { if (e === "need_admin") {