Merge pull request #1402 from hydralauncher/feat/manage-account-buttons

feat: manage account buttons
This commit is contained in:
Zamitto 2025-01-16 13:45:24 -03:00 committed by GitHub
commit 69787ee068
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 421 additions and 205 deletions

View File

@ -280,7 +280,23 @@
"launch_minimized": "Launch Hydra minimized", "launch_minimized": "Launch Hydra minimized",
"disable_nsfw_alert": "Disable NSFW alert", "disable_nsfw_alert": "Disable NSFW alert",
"seed_after_download_complete": "Seed after download complete", "seed_after_download_complete": "Seed after download complete",
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them" "show_hidden_achievement_description": "Show hidden achievements description before unlocking them",
"account": "Account",
"no_users_blocked": "You have no blocked users",
"subscription_active_until": "Your Hydra Cloud is active until {{date}}",
"manage_subscription": "Manage subscription",
"update_email": "Update email",
"update_password": "Update password",
"current_email": "Current email:",
"no_email_account": "You have not set an email yet",
"account_data_updated_successfully": "Account data updated successfully",
"renew_subscription": "Renew Hydra Cloud",
"subscription_expired_at": "Your subscription expired at {{date}}",
"no_subscription": "Enjoy Hydra in the best possible way",
"become_subscriber": "Be Hydra Cloud",
"subscription_renew_cancelled": "Automatic renewal is disabled",
"subscription_renews_on": "Your subscription renews on {{date}}",
"bill_sent_until": "Your next bill will be sent until this day"
}, },
"notifications": { "notifications": {
"download_complete": "Download complete", "download_complete": "Download complete",

View File

@ -268,7 +268,23 @@
"launch_minimized": "Iniciar o Hydra minimizado", "launch_minimized": "Iniciar o Hydra minimizado",
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado", "disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
"seed_after_download_complete": "Semear após a conclusão do download", "seed_after_download_complete": "Semear após a conclusão do download",
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las" "show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las",
"account": "Conta",
"no_users_blocked": "Você não bloqueou nenhum usuário",
"subscription_active_until": "Sua assinatura Hydra Cloud ficará ativa até {{date}}",
"manage_subscription": "Gerenciar assinatura",
"update_email": "Atualizar email",
"update_password": "Atualizar senha",
"current_email": "Email atual:",
"no_email_account": "Você ainda não adicionou um email a sua conta",
"account_data_updated_successfully": "Dados da conta atualizados com sucesso",
"renew_subscription": "Renovar Hydra Cloud",
"subscription_expired_at": "Sua assinatura expirou em {{date}}",
"no_subscription": "Aproveite o Hydra da melhor forma possível",
"become_subscriber": "Seja Hydra Cloud",
"subscription_renew_cancelled": "A renovação automática está desativada",
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia"
}, },
"notifications": { "notifications": {
"download_complete": "Download concluído", "download_complete": "Download concluído",
@ -397,7 +413,7 @@
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos", "new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas", "achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}", "achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
"hidden_achievement_tooltip": "Está é uma conquista oculta", "hidden_achievement_tooltip": "Esta é uma conquista oculta",
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista", "achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
"earned_points": "Pontos ganhos:", "earned_points": "Pontos ganhos:",
"available_points": "Pontos disponíveis:", "available_points": "Pontos disponíveis:",

View File

@ -1,7 +1,24 @@
import i18next from "i18next";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { WindowManager } from "@main/services"; import { HydraApi, WindowManager } from "@main/services";
import { AuthPage } from "@shared";
const openAuthWindow = async (_event: Electron.IpcMainInvokeEvent) => const openAuthWindow = async (
WindowManager.openAuthWindow(); _event: Electron.IpcMainInvokeEvent,
page: AuthPage
) => {
const searchParams = new URLSearchParams({
lng: i18next.language,
});
if ([AuthPage.UpdateEmail, AuthPage.UpdatePassword].includes(page)) {
const { accessToken } = await HydraApi.refreshToken().catch(() => {
return { accessToken: "" };
});
searchParams.set("token", accessToken);
}
WindowManager.openAuthWindow(page, searchParams);
};
registerEvent("openAuthWindow", openAuthWindow); registerEvent("openAuthWindow", openAuthWindow);

View File

@ -33,7 +33,8 @@ export class DatanodesApi {
"User-Agent": "User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
}, },
maxRedirects: 0, validateStatus: (status: number) => status === 302 || status < 400, maxRedirects: 0,
validateStatus: (status: number) => status === 302 || status < 400,
} }
); );

View File

@ -215,38 +215,42 @@ export class HydraApi {
} }
} }
public static async refreshToken() {
const { accessToken, expiresIn } = await this.instance
.post<{ accessToken: string; expiresIn: number }>(`/auth/refresh`, {
refreshToken: this.userAuth.refreshToken,
})
.then((response) => response.data);
const tokenExpirationTimestamp =
Date.now() +
this.secondsToMilliseconds(expiresIn) -
this.EXPIRATION_OFFSET_IN_MS;
this.userAuth.authToken = accessToken;
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
logger.log(
"Token refreshed. New expiration:",
this.userAuth.expirationTimestamp
);
userAuthRepository.upsert(
{
id: 1,
accessToken,
tokenExpirationTimestamp,
},
["id"]
);
return { accessToken, expiresIn };
}
private static async revalidateAccessTokenIfExpired() { private static async revalidateAccessTokenIfExpired() {
const now = new Date(); if (this.userAuth.expirationTimestamp < Date.now()) {
if (this.userAuth.expirationTimestamp < now.getTime()) {
try { try {
const response = await this.instance.post(`/auth/refresh`, { await this.refreshToken();
refreshToken: this.userAuth.refreshToken,
});
const { accessToken, expiresIn } = response.data;
const tokenExpirationTimestamp =
now.getTime() +
this.secondsToMilliseconds(expiresIn) -
this.EXPIRATION_OFFSET_IN_MS;
this.userAuth.authToken = accessToken;
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
logger.log(
"Token refreshed. New expiration:",
this.userAuth.expirationTimestamp
);
userAuthRepository.upsert(
{
id: 1,
accessToken,
tokenExpirationTimestamp,
},
["id"]
);
} catch (err) { } catch (err) {
this.handleUnauthorizedError(err); this.handleUnauthorizedError(err);
} }
@ -261,7 +265,7 @@ export class HydraApi {
}; };
} }
private static handleUnauthorizedError = (err) => { private static readonly handleUnauthorizedError = (err) => {
if (err instanceof AxiosError && err.response?.status === 401) { if (err instanceof AxiosError && err.response?.status === 401) {
logger.error( logger.error(
"401 - Current credentials:", "401 - Current credentials:",

View File

@ -9,7 +9,7 @@ import {
shell, shell,
} from "electron"; } from "electron";
import { is } from "@electron-toolkit/utils"; import { is } from "@electron-toolkit/utils";
import i18next, { t } from "i18next"; import { t } from "i18next";
import path from "node:path"; import path from "node:path";
import icon from "@resources/icon.png?asset"; import icon from "@resources/icon.png?asset";
import trayIcon from "@resources/tray-icon.png?asset"; import trayIcon from "@resources/tray-icon.png?asset";
@ -17,6 +17,7 @@ import { gameRepository, userPreferencesRepository } from "@main/repository";
import { IsNull, Not } from "typeorm"; import { IsNull, Not } from "typeorm";
import { HydraApi } from "./hydra-api"; import { HydraApi } from "./hydra-api";
import UserAgent from "user-agents"; import UserAgent from "user-agents";
import { AuthPage } from "@shared";
export class WindowManager { export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null; public static mainWindow: Electron.BrowserWindow | null = null;
@ -142,7 +143,7 @@ export class WindowManager {
}); });
} }
public static openAuthWindow() { public static openAuthWindow(page: AuthPage, searchParams: URLSearchParams) {
if (this.mainWindow) { if (this.mainWindow) {
const authWindow = new BrowserWindow({ const authWindow = new BrowserWindow({
width: 600, width: 600,
@ -164,12 +165,8 @@ export class WindowManager {
if (!app.isPackaged) authWindow.webContents.openDevTools(); if (!app.isPackaged) authWindow.webContents.openDevTools();
const searchParams = new URLSearchParams({
lng: i18next.language,
});
authWindow.loadURL( authWindow.loadURL(
`${import.meta.env.MAIN_VITE_AUTH_URL}/?${searchParams.toString()}` `${import.meta.env.MAIN_VITE_AUTH_URL}${page}?${searchParams.toString()}`
); );
authWindow.once("ready-to-show", () => { authWindow.once("ready-to-show", () => {
@ -181,6 +178,13 @@ export class WindowManager {
authWindow.close(); authWindow.close();
HydraApi.handleExternalAuth(url); HydraApi.handleExternalAuth(url);
return;
}
if (url.startsWith("hydralauncher://update-account")) {
authWindow.close();
WindowManager.mainWindow?.webContents.send("on-account-updated");
} }
}); });
} }

View File

@ -15,7 +15,7 @@ import type {
SeedingStatus, SeedingStatus,
GameAchievement, GameAchievement,
} from "@types"; } from "@types";
import type { CatalogueCategory } from "@shared"; import type { AuthPage, CatalogueCategory } from "@shared";
import type { AxiosProgressEvent } from "axios"; import type { AxiosProgressEvent } from "axios";
contextBridge.exposeInMainWorld("electron", { contextBridge.exposeInMainWorld("electron", {
@ -291,13 +291,19 @@ contextBridge.exposeInMainWorld("electron", {
/* Auth */ /* Auth */
signOut: () => ipcRenderer.invoke("signOut"), signOut: () => ipcRenderer.invoke("signOut"),
openAuthWindow: () => ipcRenderer.invoke("openAuthWindow"), openAuthWindow: (page: AuthPage) =>
ipcRenderer.invoke("openAuthWindow", page),
getSessionHash: () => ipcRenderer.invoke("getSessionHash"), getSessionHash: () => ipcRenderer.invoke("getSessionHash"),
onSignIn: (cb: () => void) => { onSignIn: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb(); const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-signin", listener); ipcRenderer.on("on-signin", listener);
return () => ipcRenderer.removeListener("on-signin", listener); return () => ipcRenderer.removeListener("on-signin", listener);
}, },
onAccountUpdated: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-account-updated", listener);
return () => ipcRenderer.removeListener("on-account-updated", listener);
},
onSignOut: (cb: () => void) => { onSignOut: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb(); const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-signout", listener); ipcRenderer.on("on-signout", listener);

View File

@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { Avatar } from "../avatar/avatar"; import { Avatar } from "../avatar/avatar";
import { AuthPage } from "@shared";
const LONG_POLLING_INTERVAL = 120_000; const LONG_POLLING_INTERVAL = 120_000;
@ -26,11 +27,11 @@ export function SidebarProfile() {
const handleProfileClick = () => { const handleProfileClick = () => {
if (userDetails === null) { if (userDetails === null) {
window.electron.openAuthWindow(); window.electron.openAuthWindow(AuthPage.SignIn);
return; return;
} }
navigate(`/profile/${userDetails!.id}`); navigate(`/profile/${userDetails.id}`);
}; };
useEffect(() => { useEffect(() => {

View File

@ -1,4 +1,4 @@
import type { CatalogueCategory } from "@shared"; import type { AuthPage, CatalogueCategory } from "@shared";
import type { import type {
AppUpdaterEvent, AppUpdaterEvent,
Game, Game,
@ -208,9 +208,10 @@ declare global {
/* Auth */ /* Auth */
signOut: () => Promise<void>; signOut: () => Promise<void>;
openAuthWindow: () => Promise<void>; openAuthWindow: (page: AuthPage) => Promise<void>;
getSessionHash: () => Promise<string | null>; getSessionHash: () => Promise<string | null>;
onSignIn: (cb: () => void) => () => Electron.IpcRenderer; onSignIn: (cb: () => void) => () => Electron.IpcRenderer;
onAccountUpdated: (cb: () => void) => () => Electron.IpcRenderer;
onSignOut: (cb: () => void) => () => Electron.IpcRenderer; onSignOut: (cb: () => void) => () => Electron.IpcRenderer;
/* User */ /* User */

View File

@ -10,7 +10,7 @@ import { Sidebar } from "./sidebar/sidebar";
import * as styles from "./game-details.css"; import * as styles from "./game-details.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { cloudSyncContext, gameDetailsContext } from "@renderer/context"; import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
import { steamUrlBuilder } from "@shared"; import { AuthPage, steamUrlBuilder } from "@shared";
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif"; import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
import { useUserDetails } from "@renderer/hooks"; import { useUserDetails } from "@renderer/hooks";
@ -69,7 +69,7 @@ export function GameDetailsContent() {
}); });
const backgroundColor = output const backgroundColor = output
? (new Color(output).darken(0.7).toString() as string) ? new Color(output).darken(0.7).toString()
: ""; : "";
setGameColor(backgroundColor); setGameColor(backgroundColor);
@ -101,7 +101,7 @@ export function GameDetailsContent() {
const handleCloudSaveButtonClick = () => { const handleCloudSaveButtonClick = () => {
if (!userDetails) { if (!userDetails) {
window.electron.openAuthWindow(); window.electron.openAuthWindow(AuthPage.SignIn);
return; return;
} }

View File

@ -5,14 +5,7 @@ import { SPACING_UNIT, vars } from "../../theme.css";
export const form = style({ export const form = style({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT * 3}px`,
});
export const blockedUserAvatar = style({
width: "32px",
height: "32px",
borderRadius: "4px",
filter: "grayscale(100%)",
}); });
export const blockedUser = style({ export const blockedUser = style({
@ -43,5 +36,4 @@ export const blockedUsersList = style({
flexDirection: "column", flexDirection: "column",
alignItems: "flex-start", alignItems: "flex-start",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
marginTop: `${SPACING_UNIT}px`,
}); });

View File

@ -0,0 +1,291 @@
import { Avatar, Button, SelectField } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import * as styles from "./settings-account.css";
import { useDate, useToast, useUserDetails } from "@renderer/hooks";
import { useCallback, useContext, useEffect, useState } from "react";
import {
CloudIcon,
KeyIcon,
MailIcon,
XCircleFillIcon,
} from "@primer/octicons-react";
import { settingsContext } from "@renderer/context";
import { AuthPage } from "@shared";
interface FormValues {
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
}
export function SettingsAccount() {
const { t } = useTranslation("settings");
const [isUnblocking, setIsUnblocking] = useState(false);
const { showSuccessToast } = useToast();
const { blockedUsers, fetchBlockedUsers } = useContext(settingsContext);
const { formatDate } = useDate();
const {
control,
formState: { isSubmitting },
setValue,
handleSubmit,
} = useForm<FormValues>();
const {
userDetails,
hasActiveSubscription,
patchUser,
fetchUserDetails,
updateUserDetails,
unblockUser,
} = useUserDetails();
useEffect(() => {
if (userDetails?.profileVisibility) {
setValue("profileVisibility", userDetails.profileVisibility);
}
}, [userDetails, setValue]);
useEffect(() => {
const unsubscribe = window.electron.onAccountUpdated(() => {
fetchUserDetails().then((response) => {
if (response) {
updateUserDetails(response);
}
});
showSuccessToast(t("account_data_updated_successfully"));
});
return () => {
unsubscribe();
};
}, [fetchUserDetails, updateUserDetails]);
const visibilityOptions = [
{ value: "PUBLIC", label: t("public") },
{ value: "FRIENDS", label: t("friends_only") },
{ value: "PRIVATE", label: t("private") },
];
const onSubmit = async (values: FormValues) => {
await patchUser(values);
showSuccessToast(t("changes_saved"));
};
const handleUnblockClick = useCallback(
(id: string) => {
setIsUnblocking(true);
unblockUser(id)
.then(() => {
fetchBlockedUsers();
showSuccessToast(t("user_unblocked"));
})
.finally(() => {
setIsUnblocking(false);
});
},
[unblockUser, fetchBlockedUsers, t, showSuccessToast]
);
const getHydraCloudSectionContent = () => {
const hasSubscribedBefore = Boolean(userDetails?.subscription?.expiresAt);
const isRenewalActive = userDetails?.subscription?.status === "active";
if (!hasSubscribedBefore) {
return {
description: <small>{t("no_subscription")}</small>,
callToAction: t("become_subscriber"),
};
}
if (hasActiveSubscription) {
return {
description: isRenewalActive ? (
<>
<small>
{t("subscription_renews_on", {
date: formatDate(userDetails.subscription!.expiresAt!),
})}
</small>
<small>{t("bill_sent_until")}</small>
</>
) : (
<>
<small>{t("subscription_renew_cancelled")}</small>
<small>
{t("subscription_active_until", {
date: formatDate(userDetails!.subscription!.expiresAt!),
})}
</small>
</>
),
callToAction: t("manage_subscription"),
};
}
return {
description: (
<small>
{t("subscription_expired_at", {
date: formatDate(userDetails!.subscription!.expiresAt!),
})}
</small>
),
callToAction: t("renew_subscription"),
};
};
if (!userDetails) return null;
return (
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="profileVisibility"
render={({ field }) => {
const handleChange = (
event: React.ChangeEvent<HTMLSelectElement>
) => {
field.onChange(event);
handleSubmit(onSubmit)();
};
return (
<section>
<SelectField
label={t("profile_visibility")}
value={field.value}
onChange={handleChange}
options={visibilityOptions.map((visiblity) => ({
key: visiblity.value,
value: visiblity.value,
label: visiblity.label,
}))}
disabled={isSubmitting}
/>
<small>{t("profile_visibility_description")}</small>
</section>
);
}}
/>
<section>
<h4>{t("current_email")}</h4>
<p>{userDetails?.email ?? t("no_email_account")}</p>
<div
style={{
display: "flex",
justifyContent: "start",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
marginTop: `${SPACING_UNIT * 2}px`,
}}
>
<Button
theme="outline"
onClick={() => window.electron.openAuthWindow(AuthPage.UpdateEmail)}
>
<MailIcon />
{t("update_email")}
</Button>
<Button
theme="outline"
onClick={() =>
window.electron.openAuthWindow(AuthPage.UpdatePassword)
}
>
<KeyIcon />
{t("update_password")}
</Button>
</div>
</section>
<section
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
}}
>
<h3>Hydra Cloud</h3>
<div
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
}}
>
{getHydraCloudSectionContent().description}
</div>
<Button
style={{
placeSelf: "flex-start",
}}
theme="outline"
onClick={() => window.electron.openCheckout()}
>
<CloudIcon />
{getHydraCloudSectionContent().callToAction}
</Button>
</section>
<section
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
}}
>
<h3>{t("blocked_users")}</h3>
{blockedUsers.length > 0 ? (
<ul className={styles.blockedUsersList}>
{blockedUsers.map((user) => {
return (
<li key={user.id} className={styles.blockedUser}>
<div
style={{
display: "flex",
gap: `${SPACING_UNIT}px`,
alignItems: "center",
}}
>
<Avatar
style={{ filter: "grayscale(100%)" }}
size={32}
src={user.profileImageUrl}
alt={user.displayName}
/>
<span>{user.displayName}</span>
</div>
<button
type="button"
className={styles.unblockButton}
onClick={() => handleUnblockClick(user.id)}
disabled={isUnblocking}
>
<XCircleFillIcon />
</button>
</li>
);
})}
</ul>
) : (
<small>{t("no_users_blocked")}</small>
)}
</section>
</form>
);
}

View File

@ -1,139 +0,0 @@
import { SelectField } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import * as styles from "./settings-privacy.css";
import { useToast, useUserDetails } from "@renderer/hooks";
import { useCallback, useContext, useEffect, useState } from "react";
import { XCircleFillIcon } from "@primer/octicons-react";
import { settingsContext } from "@renderer/context";
interface FormValues {
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
}
export function SettingsPrivacy() {
const { t } = useTranslation("settings");
const [isUnblocking, setIsUnblocking] = useState(false);
const { showSuccessToast } = useToast();
const { blockedUsers, fetchBlockedUsers } = useContext(settingsContext);
const {
control,
formState: { isSubmitting },
setValue,
handleSubmit,
} = useForm<FormValues>();
const { patchUser, userDetails } = useUserDetails();
const { unblockUser } = useUserDetails();
useEffect(() => {
if (userDetails?.profileVisibility) {
setValue("profileVisibility", userDetails.profileVisibility);
}
}, [userDetails, setValue]);
const visibilityOptions = [
{ value: "PUBLIC", label: t("public") },
{ value: "FRIENDS", label: t("friends_only") },
{ value: "PRIVATE", label: t("private") },
];
const onSubmit = async (values: FormValues) => {
await patchUser(values);
showSuccessToast(t("changes_saved"));
};
const handleUnblockClick = useCallback(
(id: string) => {
setIsUnblocking(true);
unblockUser(id)
.then(() => {
fetchBlockedUsers();
showSuccessToast(t("user_unblocked"));
})
.finally(() => {
setIsUnblocking(false);
});
},
[unblockUser, fetchBlockedUsers, t, showSuccessToast]
);
return (
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="profileVisibility"
render={({ field }) => {
const handleChange = (
event: React.ChangeEvent<HTMLSelectElement>
) => {
field.onChange(event);
handleSubmit(onSubmit)();
};
return (
<>
<SelectField
label={t("profile_visibility")}
value={field.value}
onChange={handleChange}
options={visibilityOptions.map((visiblity) => ({
key: visiblity.value,
value: visiblity.value,
label: visiblity.label,
}))}
disabled={isSubmitting}
/>
<small>{t("profile_visibility_description")}</small>
</>
);
}}
/>
<h3 style={{ marginTop: `${SPACING_UNIT * 2}px` }}>
{t("blocked_users")}
</h3>
<ul className={styles.blockedUsersList}>
{blockedUsers.map((user) => {
return (
<li key={user.id} className={styles.blockedUser}>
<div
style={{
display: "flex",
gap: `${SPACING_UNIT}px`,
alignItems: "center",
}}
>
<img
src={user.profileImageUrl!}
alt={user.displayName}
className={styles.blockedUserAvatar}
/>
<span>{user.displayName}</span>
</div>
<button
type="button"
className={styles.unblockButton}
onClick={() => handleUnblockClick(user.id)}
disabled={isUnblocking}
>
<XCircleFillIcon />
</button>
</li>
);
})}
</ul>
</form>
);
}

View File

@ -11,7 +11,7 @@ import {
SettingsContextConsumer, SettingsContextConsumer,
SettingsContextProvider, SettingsContextProvider,
} from "@renderer/context"; } from "@renderer/context";
import { SettingsPrivacy } from "./settings-privacy"; import { SettingsAccount } from "./settings-account";
import { useUserDetails } from "@renderer/hooks"; import { useUserDetails } from "@renderer/hooks";
import { useMemo } from "react"; import { useMemo } from "react";
@ -28,7 +28,7 @@ export default function Settings() {
"Real-Debrid", "Real-Debrid",
]; ];
if (userDetails) return [...categories, t("privacy")]; if (userDetails) return [...categories, t("account")];
return categories; return categories;
}, [userDetails, t]); }, [userDetails, t]);
@ -53,7 +53,7 @@ export default function Settings() {
return <SettingsRealDebrid />; return <SettingsRealDebrid />;
} }
return <SettingsPrivacy />; return <SettingsAccount />;
}; };
return ( return (

View File

@ -42,3 +42,9 @@ export enum Cracker {
rle = "RLE", rle = "RLE",
razor1911 = "RAZOR1911", razor1911 = "RAZOR1911",
} }
export enum AuthPage {
SignIn = "/",
UpdateEmail = "/update-email",
UpdatePassword = "/update-password",
}