mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-15 04:32:13 +00:00
feat: fixing download error for gofile
This commit is contained in:
parent
702b141f7b
commit
b122489b34
23 changed files with 441 additions and 297 deletions
|
@ -125,7 +125,8 @@
|
||||||
"refuse_nsfw_content": "Go back",
|
"refuse_nsfw_content": "Go back",
|
||||||
"stats": "Stats",
|
"stats": "Stats",
|
||||||
"download_count": "Downloads",
|
"download_count": "Downloads",
|
||||||
"player_count": "Active players"
|
"player_count": "Active players",
|
||||||
|
"download_error": "This download option is not available"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
|
@ -207,7 +208,10 @@
|
||||||
"friends_only": "Friends only",
|
"friends_only": "Friends only",
|
||||||
"privacy": "Privacy",
|
"privacy": "Privacy",
|
||||||
"profile_visibility": "Profile visibility",
|
"profile_visibility": "Profile visibility",
|
||||||
"profile_visibility_description": "Choose who can see your profile and library"
|
"profile_visibility_description": "Choose who can see your profile and library",
|
||||||
|
"required_field": "This field is required",
|
||||||
|
"source_already_exists": "This source has been already added",
|
||||||
|
"must_be_valid_url": "The source must be a valid URL"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
|
|
@ -121,7 +121,8 @@
|
||||||
"refuse_nsfw_content": "Voltar",
|
"refuse_nsfw_content": "Voltar",
|
||||||
"stats": "Estatísticas",
|
"stats": "Estatísticas",
|
||||||
"download_count": "Downloads",
|
"download_count": "Downloads",
|
||||||
"player_count": "Jogadores ativos"
|
"player_count": "Jogadores ativos",
|
||||||
|
"download_error": "Essa opção de download falhou"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
|
@ -206,7 +207,10 @@
|
||||||
"friends_only": "Apenas amigos",
|
"friends_only": "Apenas amigos",
|
||||||
"public": "Público",
|
"public": "Público",
|
||||||
"profile_visibility": "Visibilidade do perfil",
|
"profile_visibility": "Visibilidade do perfil",
|
||||||
"profile_visibility_description": "Escolha quem pode ver seu perfil e biblioteca"
|
"profile_visibility_description": "Escolha quem pode ver seu perfil e biblioteca",
|
||||||
|
"required_field": "Este campo é obrigatório",
|
||||||
|
"source_already_exists": "Essa fonte já foi adicionada",
|
||||||
|
"must_be_valid_url": "A fonte deve ser uma URL válida"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
|
|
|
@ -45,7 +45,7 @@ import "./auth/sign-out";
|
||||||
import "./auth/open-auth-window";
|
import "./auth/open-auth-window";
|
||||||
import "./auth/get-session-hash";
|
import "./auth/get-session-hash";
|
||||||
import "./user/get-user";
|
import "./user/get-user";
|
||||||
import "./user/get-user-blocks";
|
import "./user/get-blocked-users";
|
||||||
import "./user/block-user";
|
import "./user/block-user";
|
||||||
import "./user/unblock-user";
|
import "./user/unblock-user";
|
||||||
import "./user/get-user-friends";
|
import "./user/get-user-friends";
|
||||||
|
|
|
@ -1,9 +1,3 @@
|
||||||
import {
|
|
||||||
downloadQueueRepository,
|
|
||||||
gameRepository,
|
|
||||||
repackRepository,
|
|
||||||
} from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { StartGameDownloadPayload } from "@types";
|
import type { StartGameDownloadPayload } from "@types";
|
||||||
|
@ -14,6 +8,8 @@ import { Not } from "typeorm";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
import { dataSource } from "@main/data-source";
|
||||||
|
import { DownloadQueue, Game, Repack } from "@main/entity";
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
@ -22,6 +18,12 @@ const startGameDownload = async (
|
||||||
const { repackId, objectID, title, shop, downloadPath, downloader, uri } =
|
const { repackId, objectID, title, shop, downloadPath, downloader, uri } =
|
||||||
payload;
|
payload;
|
||||||
|
|
||||||
|
return dataSource.transaction(async (transactionalEntityManager) => {
|
||||||
|
const gameRepository = transactionalEntityManager.getRepository(Game);
|
||||||
|
const repackRepository = transactionalEntityManager.getRepository(Repack);
|
||||||
|
const downloadQueueRepository =
|
||||||
|
transactionalEntityManager.getRepository(DownloadQueue);
|
||||||
|
|
||||||
const [game, repack] = await Promise.all([
|
const [game, repack] = await Promise.all([
|
||||||
gameRepository.findOne({
|
gameRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
@ -99,11 +101,12 @@ const startGameDownload = async (
|
||||||
|
|
||||||
createGame(updatedGame!).catch(() => {});
|
createGame(updatedGame!).catch(() => {});
|
||||||
|
|
||||||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
|
||||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
|
||||||
|
|
||||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||||
await DownloadManager.startDownload(updatedGame!);
|
await DownloadManager.startDownload(updatedGame!);
|
||||||
|
|
||||||
|
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
||||||
|
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("startGameDownload", startGameDownload);
|
registerEvent("startGameDownload", startGameDownload);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { UserBlocks } from "@types";
|
import { UserBlocks } from "@types";
|
||||||
|
|
||||||
export const getUserBlocks = async (
|
export const getBlockedUsers = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
take: number,
|
take: number,
|
||||||
skip: number
|
skip: number
|
||||||
|
@ -10,4 +10,4 @@ export const getUserBlocks = async (
|
||||||
return HydraApi.get(`/profile/blocks`, { take, skip });
|
return HydraApi.get(`/profile/blocks`, { take, skip });
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getUserBlocks", getUserBlocks);
|
registerEvent("getBlockedUsers", getBlockedUsers);
|
|
@ -159,8 +159,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
||||||
getUserFriends: (userId: string, take: number, skip: number) =>
|
getUserFriends: (userId: string, take: number, skip: number) =>
|
||||||
ipcRenderer.invoke("getUserFriends", userId, take, skip),
|
ipcRenderer.invoke("getUserFriends", userId, take, skip),
|
||||||
getUserBlocks: (take: number, skip: number) =>
|
getBlockedUsers: (take: number, skip: number) =>
|
||||||
ipcRenderer.invoke("getUserBlocks", take, skip),
|
ipcRenderer.invoke("getBlockedUsers", take, skip),
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
signOut: () => ipcRenderer.invoke("signOut"),
|
signOut: () => ipcRenderer.invoke("signOut"),
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
import { ComplexStyleRule, globalStyle, style } from "@vanilla-extract/css";
|
import {
|
||||||
|
ComplexStyleRule,
|
||||||
|
createContainer,
|
||||||
|
globalStyle,
|
||||||
|
style,
|
||||||
|
} from "@vanilla-extract/css";
|
||||||
import { SPACING_UNIT, vars } from "./theme.css";
|
import { SPACING_UNIT, vars } from "./theme.css";
|
||||||
|
|
||||||
|
export const appContainer = createContainer();
|
||||||
|
|
||||||
globalStyle("*", {
|
globalStyle("*", {
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
});
|
});
|
||||||
|
@ -90,6 +97,8 @@ export const container = style({
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
containerName: appContainer,
|
||||||
|
containerType: "inline-size",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const content = style({
|
export const content = style({
|
||||||
|
|
|
@ -2,16 +2,21 @@ import { useNavigate } from "react-router-dom";
|
||||||
import { PeopleIcon, PersonIcon } from "@primer/octicons-react";
|
import { PeopleIcon, PersonIcon } from "@primer/octicons-react";
|
||||||
import * as styles from "./sidebar-profile.css";
|
import * as styles from "./sidebar-profile.css";
|
||||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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";
|
||||||
|
|
||||||
|
const LONG_POLLING_INTERVAL = 10_000;
|
||||||
|
|
||||||
export function SidebarProfile() {
|
export function SidebarProfile() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const pollingInterval = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const { t } = useTranslation("sidebar");
|
const { t } = useTranslation("sidebar");
|
||||||
|
|
||||||
const { userDetails, friendRequests, showFriendsModal } = useUserDetails();
|
const { userDetails, friendRequests, showFriendsModal, fetchFriendRequests } =
|
||||||
|
useUserDetails();
|
||||||
|
|
||||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||||
|
|
||||||
|
@ -28,6 +33,18 @@ export function SidebarProfile() {
|
||||||
navigate(`/profile/${userDetails!.id}`);
|
navigate(`/profile/${userDetails!.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
pollingInterval.current = setInterval(() => {
|
||||||
|
fetchFriendRequests();
|
||||||
|
}, LONG_POLLING_INTERVAL);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (pollingInterval.current) {
|
||||||
|
clearInterval(pollingInterval.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [fetchFriendRequests]);
|
||||||
|
|
||||||
const friendsButton = useMemo(() => {
|
const friendsButton = useMemo(() => {
|
||||||
if (!userDetails) return null;
|
if (!userDetails) return null;
|
||||||
|
|
||||||
|
|
2
src/renderer/src/declaration.d.ts
vendored
2
src/renderer/src/declaration.d.ts
vendored
|
@ -142,7 +142,7 @@ declare global {
|
||||||
take: number,
|
take: number,
|
||||||
skip: number
|
skip: number
|
||||||
) => Promise<UserFriends>;
|
) => Promise<UserFriends>;
|
||||||
getUserBlocks: (take: number, skip: number) => Promise<UserBlocks>;
|
getBlockedUsers: (take: number, skip: number) => Promise<UserBlocks>;
|
||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => Promise<UserProfile | null>;
|
getMe: () => Promise<UserProfile | null>;
|
||||||
|
|
|
@ -25,11 +25,10 @@ export function useDownload() {
|
||||||
const startDownload = async (payload: StartGameDownloadPayload) => {
|
const startDownload = async (payload: StartGameDownloadPayload) => {
|
||||||
dispatch(clearDownload());
|
dispatch(clearDownload());
|
||||||
|
|
||||||
return window.electron.startGameDownload(payload).then((game) => {
|
const game = await window.electron.startGameDownload(payload);
|
||||||
updateLibrary();
|
|
||||||
|
|
||||||
|
await updateLibrary();
|
||||||
return game;
|
return game;
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const pauseDownload = async (gameId: number) => {
|
const pauseDownload = async (gameId: number) => {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
|
||||||
import type { GameRepack } from "@types";
|
import type { GameRepack } from "@types";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||||
import { useAppSelector } from "@renderer/hooks";
|
import { useAppSelector, useToast } from "@renderer/hooks";
|
||||||
|
|
||||||
export interface DownloadSettingsModalProps {
|
export interface DownloadSettingsModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -31,6 +31,8 @@ export function DownloadSettingsModal({
|
||||||
}: DownloadSettingsModalProps) {
|
}: DownloadSettingsModalProps) {
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
const { showErrorToast } = useToast();
|
||||||
|
|
||||||
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
|
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
|
||||||
const [selectedPath, setSelectedPath] = useState("");
|
const [selectedPath, setSelectedPath] = useState("");
|
||||||
const [downloadStarting, setDownloadStarting] = useState(false);
|
const [downloadStarting, setDownloadStarting] = useState(false);
|
||||||
|
@ -104,9 +106,15 @@ export function DownloadSettingsModal({
|
||||||
if (repack) {
|
if (repack) {
|
||||||
setDownloadStarting(true);
|
setDownloadStarting(true);
|
||||||
|
|
||||||
startDownload(repack, selectedDownloader!, selectedPath).finally(() => {
|
startDownload(repack, selectedDownloader!, selectedPath)
|
||||||
setDownloadStarting(false);
|
.then(() => {
|
||||||
onClose();
|
onClose();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showErrorToast(t("download_error"));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setDownloadStarting(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -67,6 +67,7 @@ export function EditProfileModal(
|
||||||
return patchUser(values)
|
return patchUser(values)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await Promise.allSettled([fetchUserDetails(), getUserProfile()]);
|
await Promise.allSettled([fetchUserDetails(), getUserProfile()]);
|
||||||
|
props.onClose();
|
||||||
showSuccessToast(t("saved_successfully"));
|
showSuccessToast(t("saved_successfully"));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { appContainer } from "../../../app.css";
|
||||||
import { vars, SPACING_UNIT } from "../../../theme.css";
|
import { vars, SPACING_UNIT } from "../../../theme.css";
|
||||||
import { globalStyle, style } from "@vanilla-extract/css";
|
import { globalStyle, style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
@ -73,11 +74,8 @@ export const rightContent = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
"@media": {
|
"@media": {
|
||||||
"(min-width: 768px)": {
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: "200px",
|
|
||||||
},
|
|
||||||
"(min-width: 1024px)": {
|
"(min-width: 1024px)": {
|
||||||
maxWidth: "300px",
|
maxWidth: "300px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
@ -108,20 +106,27 @@ export const listItem = style({
|
||||||
|
|
||||||
export const gamesGrid = style({
|
export const gamesGrid = style({
|
||||||
listStyle: "none",
|
listStyle: "none",
|
||||||
margin: 0,
|
margin: "0",
|
||||||
padding: 0,
|
padding: "0",
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
"@media": {
|
|
||||||
"(min-width: 768px)": {
|
|
||||||
gridTemplateColumns: "repeat(2, 1fr)",
|
gridTemplateColumns: "repeat(2, 1fr)",
|
||||||
|
"@container": {
|
||||||
|
[`${appContainer} (min-width: 1000px)`]: {
|
||||||
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
},
|
},
|
||||||
"(min-width: 1250px)": {
|
[`${appContainer} (min-width: 1300px)`]: {
|
||||||
gridTemplateColumns: "repeat(3, 1fr)",
|
gridTemplateColumns: "repeat(5, 1fr)",
|
||||||
},
|
},
|
||||||
"(min-width: 1600px)": {
|
[`${appContainer} (min-width: 2000px)`]: {
|
||||||
|
gridTemplateColumns: "repeat(6, 1fr)",
|
||||||
|
},
|
||||||
|
[`${appContainer} (min-width: 2600px)`]: {
|
||||||
gridTemplateColumns: "repeat(8, 1fr)",
|
gridTemplateColumns: "repeat(8, 1fr)",
|
||||||
},
|
},
|
||||||
|
[`${appContainer} (min-width: 3000px)`]: {
|
||||||
|
gridTemplateColumns: "repeat(12, 1fr)",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { steamUrlBuilder } from "@shared";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
|
||||||
import * as styles from "./profile-content.css";
|
import * as styles from "./profile-content.css";
|
||||||
import { ClockIcon } from "@primer/octicons-react";
|
import { ClockIcon, TelescopeIcon } from "@primer/octicons-react";
|
||||||
import { Link } from "@renderer/components";
|
import { Link } from "@renderer/components";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserGame } from "@types";
|
import { UserGame } from "@types";
|
||||||
|
@ -71,6 +71,18 @@ export function ProfileContent() {
|
||||||
return <LockedProfile />;
|
return <LockedProfile />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userProfile.libraryGames.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className={styles.noGames}>
|
||||||
|
<div className={styles.telescopeIcon}>
|
||||||
|
<TelescopeIcon size={24} />
|
||||||
|
</div>
|
||||||
|
<h2>{t("no_recent_activity_title")}</h2>
|
||||||
|
{isMe && <p>{t("no_recent_activity_description")}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
style={{
|
style={{
|
||||||
|
@ -83,17 +95,11 @@ export function ProfileContent() {
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
<h2>{t("library")}</h2>
|
<h2>{t("library")}</h2>
|
||||||
|
|
||||||
<h3>{numberFormatter.format(userProfile.libraryGames.length)}</h3>
|
<span>
|
||||||
|
{numberFormatter.format(userProfile.libraryGames.length)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div className={styles.noGames}>
|
|
||||||
<div className={styles.telescopeIcon}>
|
|
||||||
<TelescopeIcon size={24} />
|
|
||||||
</div>
|
|
||||||
<h2>{t("no_recent_activity_title")}</h2>
|
|
||||||
{isMe && <p>{t("no_recent_activity_description")}</p>}
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
<ul className={styles.gamesGrid}>
|
<ul className={styles.gamesGrid}>
|
||||||
{userProfile?.libraryGames?.map((game) => (
|
{userProfile?.libraryGames?.map((game) => (
|
||||||
<li
|
<li
|
||||||
|
@ -129,6 +135,7 @@ export function ProfileContent() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.rightContent}>
|
<div className={styles.rightContent}>
|
||||||
|
{userProfile?.recentGames?.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
<h2>{t("activity")}</h2>
|
<h2>{t("activity")}</h2>
|
||||||
|
@ -146,8 +153,8 @@ export function ProfileContent() {
|
||||||
src={game.iconUrl!}
|
src={game.iconUrl!}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
style={{
|
style={{
|
||||||
width: "30px",
|
width: "32px",
|
||||||
height: "30px",
|
height: "32px",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -157,9 +164,19 @@ export function ProfileContent() {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: `${SPACING_UNIT / 2}px`,
|
gap: `${SPACING_UNIT / 2}px`,
|
||||||
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontWeight: "bold" }}>{game.title}</span>
|
<span
|
||||||
|
style={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
overflow: "hidden",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{game.title}
|
||||||
|
</span>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -178,11 +195,12 @@ export function ProfileContent() {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
<h2>{t("friends")}</h2>
|
<h2>{t("friends")}</h2>
|
||||||
<span>{userProfile?.totalFriends}</span>
|
<span>{numberFormatter.format(userProfile?.totalFriends)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.box}>
|
<div className={styles.box}>
|
||||||
|
@ -197,8 +215,8 @@ export function ProfileContent() {
|
||||||
src={friend.profileImageUrl!}
|
src={friend.profileImageUrl!}
|
||||||
alt={friend.displayName}
|
alt={friend.displayName}
|
||||||
style={{
|
style={{
|
||||||
width: "30px",
|
width: "32px",
|
||||||
height: "30px",
|
height: "32px",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -48,6 +48,9 @@ export const profileDisplayName = style({
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
position: "relative",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const heroPanel = style({
|
export const heroPanel = style({
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import type { FriendRequestAction } from "@types";
|
import type { FriendRequestAction } from "@types";
|
||||||
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
||||||
|
import Skeleton from "react-loading-skeleton";
|
||||||
|
|
||||||
type FriendAction =
|
type FriendAction =
|
||||||
| FriendRequestAction
|
| FriendRequestAction
|
||||||
|
@ -35,7 +36,8 @@ export function ProfileHero() {
|
||||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||||
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
||||||
|
|
||||||
const context = useContext(userProfileContext);
|
const { isMe, heroBackground, getUserProfile, userProfile } =
|
||||||
|
useContext(userProfileContext);
|
||||||
const {
|
const {
|
||||||
signOut,
|
signOut,
|
||||||
updateFriendRequestState,
|
updateFriendRequestState,
|
||||||
|
@ -46,10 +48,6 @@ export function ProfileHero() {
|
||||||
|
|
||||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||||
|
|
||||||
const { isMe, heroBackground, getUserProfile } = context;
|
|
||||||
|
|
||||||
const userProfile = context.userProfile!;
|
|
||||||
|
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
const { formatDistance } = useDate();
|
const { formatDistance } = useDate();
|
||||||
|
|
||||||
|
@ -72,6 +70,7 @@ export function ProfileHero() {
|
||||||
|
|
||||||
const handleFriendAction = useCallback(
|
const handleFriendAction = useCallback(
|
||||||
async (userId: string, action: FriendAction) => {
|
async (userId: string, action: FriendAction) => {
|
||||||
|
if (!userProfile) return;
|
||||||
setIsPerformingAction(true);
|
setIsPerformingAction(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -111,11 +110,13 @@ export function ProfileHero() {
|
||||||
getUserProfile,
|
getUserProfile,
|
||||||
navigate,
|
navigate,
|
||||||
showSuccessToast,
|
showSuccessToast,
|
||||||
userProfile.id,
|
userProfile,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const profileActions = useMemo(() => {
|
const profileActions = useMemo(() => {
|
||||||
|
if (!userProfile) return null;
|
||||||
|
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -239,7 +240,7 @@ export function ProfileHero() {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return userProfile.currentGame;
|
return userProfile?.currentGame;
|
||||||
}, [isMe, userProfile, gameRunning]);
|
}, [isMe, userProfile, gameRunning]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -267,11 +268,11 @@ export function ProfileHero() {
|
||||||
className={styles.profileAvatarButton}
|
className={styles.profileAvatarButton}
|
||||||
onClick={handleAvatarClick}
|
onClick={handleAvatarClick}
|
||||||
>
|
>
|
||||||
{userProfile.profileImageUrl ? (
|
{userProfile?.profileImageUrl ? (
|
||||||
<img
|
<img
|
||||||
className={styles.profileAvatar}
|
className={styles.profileAvatar}
|
||||||
alt={userProfile.displayName}
|
alt={userProfile?.displayName}
|
||||||
src={userProfile.profileImageUrl}
|
src={userProfile?.profileImageUrl}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PersonIcon size={72} />
|
<PersonIcon size={72} />
|
||||||
|
@ -279,9 +280,13 @@ export function ProfileHero() {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className={styles.profileInformation}>
|
<div className={styles.profileInformation}>
|
||||||
|
{userProfile ? (
|
||||||
<h2 className={styles.profileDisplayName}>
|
<h2 className={styles.profileDisplayName}>
|
||||||
{userProfile.displayName}
|
{userProfile?.displayName}
|
||||||
</h2>
|
</h2>
|
||||||
|
) : (
|
||||||
|
<Skeleton width={150} height={28} />
|
||||||
|
)}
|
||||||
|
|
||||||
{currentGame && (
|
{currentGame && (
|
||||||
<div className={styles.currentGameWrapper}>
|
<div className={styles.currentGameWrapper}>
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
import Skeleton from "react-loading-skeleton";
|
|
||||||
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export function ProfileSkeleton() {
|
|
||||||
const { t } = useTranslation("user_profile");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Skeleton />
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h2>{t("activity")}</h2>
|
|
||||||
{Array.from({ length: 3 }).map((_, index) => (
|
|
||||||
<Skeleton
|
|
||||||
key={index}
|
|
||||||
height={72}
|
|
||||||
style={{ flex: "1", width: "100%" }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2>{t("library")}</h2>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(4, 1fr)",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Array.from({ length: 8 }).map((_, index) => (
|
|
||||||
<Skeleton key={index} style={{ aspectRatio: "1" }} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { appContainer } from "@renderer/app.css";
|
||||||
import { SPACING_UNIT } from "../../theme.css";
|
import { SPACING_UNIT } from "../../theme.css";
|
||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { ProfileSkeleton } from "./profile-skeleton";
|
|
||||||
import { ProfileContent } from "./profile-content/profile-content";
|
import { ProfileContent } from "./profile-content/profile-content";
|
||||||
import { SkeletonTheme } from "react-loading-skeleton";
|
import { SkeletonTheme } from "react-loading-skeleton";
|
||||||
import { vars } from "@renderer/theme.css";
|
import { vars } from "@renderer/theme.css";
|
||||||
|
|
||||||
import * as styles from "./profile.css";
|
import * as styles from "./profile.css";
|
||||||
import {
|
import { UserProfileContextProvider } from "@renderer/context";
|
||||||
UserProfileContextConsumer,
|
import { useParams } from "react-router-dom";
|
||||||
UserProfileContextProvider,
|
|
||||||
} from "@renderer/context";
|
|
||||||
|
|
||||||
export function Profile() {
|
export function Profile() {
|
||||||
const { userId } = useParams();
|
const { userId } = useParams();
|
||||||
|
@ -17,11 +13,7 @@ export function Profile() {
|
||||||
<UserProfileContextProvider userId={userId!}>
|
<UserProfileContextProvider userId={userId!}>
|
||||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<UserProfileContextConsumer>
|
<ProfileContent />
|
||||||
{({ userProfile }) =>
|
|
||||||
userProfile ? <ProfileContent /> : <ProfileSkeleton />
|
|
||||||
}
|
|
||||||
</UserProfileContextConsumer>
|
|
||||||
</div>
|
</div>
|
||||||
</SkeletonTheme>
|
</SkeletonTheme>
|
||||||
</UserProfileContextProvider>
|
</UserProfileContextProvider>
|
||||||
|
|
|
@ -4,6 +4,10 @@ import { useTranslation } from "react-i18next";
|
||||||
import { Button, Modal, TextField } from "@renderer/components";
|
import { Button, Modal, TextField } from "@renderer/components";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { settingsContext } from "@renderer/context";
|
import { settingsContext } from "@renderer/context";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import * as yup from "yup";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
|
||||||
interface AddDownloadSourceModalProps {
|
interface AddDownloadSourceModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -11,47 +15,83 @@ interface AddDownloadSourceModalProps {
|
||||||
onAddDownloadSource: () => void;
|
onAddDownloadSource: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function AddDownloadSourceModal({
|
export function AddDownloadSourceModal({
|
||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
onAddDownloadSource,
|
onAddDownloadSource,
|
||||||
}: AddDownloadSourceModalProps) {
|
}: AddDownloadSourceModalProps) {
|
||||||
const [value, setValue] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
|
const schema = yup.object().shape({
|
||||||
|
url: yup.string().required(t("required_field")).url(t("must_be_valid_url")),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
setError,
|
||||||
|
clearErrors,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormValues>({
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
const [validationResult, setValidationResult] = useState<{
|
const [validationResult, setValidationResult] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
downloadCount: number;
|
downloadCount: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const { t } = useTranslation("settings");
|
|
||||||
|
|
||||||
const { sourceUrl } = useContext(settingsContext);
|
const { sourceUrl } = useContext(settingsContext);
|
||||||
|
|
||||||
const handleValidateDownloadSource = useCallback(async (url: string) => {
|
const onSubmit = useCallback(
|
||||||
|
async (values: FormValues) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electron.validateDownloadSource(url);
|
const result = await window.electron.validateDownloadSource(values.url);
|
||||||
setValidationResult(result);
|
setValidationResult(result);
|
||||||
|
|
||||||
|
setUrl(values.url);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (
|
||||||
|
error.message.endsWith("Source with the same url already exists")
|
||||||
|
) {
|
||||||
|
setError("url", {
|
||||||
|
type: "server",
|
||||||
|
message: t("source_already_exists"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
},
|
||||||
|
[setError, t]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue("");
|
setValue("url", "");
|
||||||
|
clearErrors();
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setValidationResult(null);
|
setValidationResult(null);
|
||||||
|
|
||||||
if (sourceUrl) {
|
if (sourceUrl) {
|
||||||
setValue(sourceUrl);
|
setValue("url", sourceUrl);
|
||||||
handleValidateDownloadSource(sourceUrl);
|
handleSubmit(onSubmit)();
|
||||||
}
|
}
|
||||||
}, [visible, handleValidateDownloadSource, sourceUrl]);
|
}, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]);
|
||||||
|
|
||||||
const handleAddDownloadSource = async () => {
|
const handleAddDownloadSource = async () => {
|
||||||
await window.electron.addDownloadSource(value);
|
await window.electron.addDownloadSource(url);
|
||||||
onClose();
|
onClose();
|
||||||
onAddDownloadSource();
|
onAddDownloadSource();
|
||||||
};
|
};
|
||||||
|
@ -72,17 +112,17 @@ export function AddDownloadSourceModal({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
|
{...register("url")}
|
||||||
label={t("download_source_url")}
|
label={t("download_source_url")}
|
||||||
placeholder={t("insert_valid_json_url")}
|
placeholder={t("insert_valid_json_url")}
|
||||||
value={value}
|
error={errors.url}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
|
||||||
rightContent={
|
rightContent={
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
theme="outline"
|
theme="outline"
|
||||||
style={{ alignSelf: "flex-end" }}
|
style={{ alignSelf: "flex-end" }}
|
||||||
onClick={() => handleValidateDownloadSource(value)}
|
onClick={handleSubmit(onSubmit)}
|
||||||
disabled={isLoading || !value}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{t("validate_download_source")}
|
{t("validate_download_source")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -115,7 +155,11 @@ export function AddDownloadSourceModal({
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="button" onClick={handleAddDownloadSource}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddDownloadSource}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
{t("import")}
|
{t("import")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -134,7 +134,7 @@ export function SettingsGeneral() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3>{t("notifications")}</h3>
|
<h3>{t("notifications")}</h3>
|
||||||
<>
|
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
label={t("enable_download_notifications")}
|
label={t("enable_download_notifications")}
|
||||||
checked={form.downloadNotificationsEnabled}
|
checked={form.downloadNotificationsEnabled}
|
||||||
|
@ -156,6 +156,5 @@ export function SettingsGeneral() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,31 @@
|
||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
import { SPACING_UNIT } from "../../theme.css";
|
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}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const blockedUserAvatar = style({
|
||||||
|
width: "32px",
|
||||||
|
height: "32px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const blockedUser = style({
|
||||||
|
display: "flex",
|
||||||
|
minWidth: "240px",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: vars.color.darkBackground,
|
||||||
|
border: `1px solid ${vars.color.border}`,
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: `${SPACING_UNIT}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const unblockButton = style({
|
||||||
|
color: vars.color.muted,
|
||||||
|
cursor: "pointer",
|
||||||
|
});
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import * as styles from "./settings-privacy.css";
|
import * as styles from "./settings-privacy.css";
|
||||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { XCircleFillIcon, XIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
|
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
|
||||||
|
@ -25,12 +26,22 @@ export function SettingsPrivacy() {
|
||||||
|
|
||||||
const { patchUser, userDetails } = useUserDetails();
|
const { patchUser, userDetails } = useUserDetails();
|
||||||
|
|
||||||
|
const [blockedUsers, setBlockedUsers] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userDetails?.profileVisibility) {
|
if (userDetails?.profileVisibility) {
|
||||||
setValue("profileVisibility", userDetails.profileVisibility);
|
setValue("profileVisibility", userDetails.profileVisibility);
|
||||||
}
|
}
|
||||||
}, [userDetails, setValue]);
|
}, [userDetails, setValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.electron.getBlockedUsers(12, 0).then((users) => {
|
||||||
|
setBlockedUsers(users.blocks);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
console.log("BLOCKED USERS", blockedUsers);
|
||||||
|
|
||||||
const visibilityOptions = [
|
const visibilityOptions = [
|
||||||
{ value: "PUBLIC", label: t("public") },
|
{ value: "PUBLIC", label: t("public") },
|
||||||
{ value: "FRIENDS", label: t("friends_only") },
|
{ value: "FRIENDS", label: t("friends_only") },
|
||||||
|
@ -47,31 +58,71 @@ export function SettingsPrivacy() {
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="profileVisibility"
|
name="profileVisibility"
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
|
const handleChange = (
|
||||||
|
event: React.ChangeEvent<HTMLSelectElement>
|
||||||
|
) => {
|
||||||
|
field.onChange(event);
|
||||||
|
handleSubmit(onSubmit)();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<SelectField
|
<SelectField
|
||||||
label={t("profile_visibility")}
|
label={t("profile_visibility")}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={field.onChange}
|
onChange={handleChange}
|
||||||
options={visibilityOptions.map((visiblity) => ({
|
options={visibilityOptions.map((visiblity) => ({
|
||||||
key: visiblity.value,
|
key: visiblity.value,
|
||||||
value: visiblity.value,
|
value: visiblity.value,
|
||||||
label: visiblity.label,
|
label: visiblity.label,
|
||||||
}))}
|
}))}
|
||||||
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<small>{t("profile_visibility_description")}</small>
|
<small>{t("profile_visibility_description")}</small>
|
||||||
</>
|
</>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<h3 style={{ marginTop: `${SPACING_UNIT * 2}px` }}>
|
||||||
type="submit"
|
Usuários bloqueados
|
||||||
style={{ alignSelf: "flex-end", marginTop: `${SPACING_UNIT * 2}px` }}
|
</h3>
|
||||||
disabled={isSubmitting}
|
|
||||||
|
<ul
|
||||||
|
style={{
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
listStyle: "none",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("save_changes")}
|
{blockedUsers.map((user) => {
|
||||||
</Button>
|
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}>
|
||||||
|
<XCircleFillIcon />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue