diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b24509d3..57511227 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -261,6 +261,18 @@ "undo_friendship": "Undo friendship", "request_accepted": "Request accepted", "user_blocked_successfully": "User blocked successfully", - "user_block_modal_text": "This will block {{displayName}}" + "user_block_modal_text": "This will block {{displayName}}", + "settings": "Settings", + "public": "Public", + "private": "Private", + "friends_only": "Friends only", + "privacy": "Privacy", + "blocked_users": "Blocked users", + "unblock": "Unblock", + "no_friends_added": "You still don't have added friends", + "pending": "Pending", + "no_pending_invites": "You have no pending invites", + "no_blocked_users": "You have no blocked users", + "friend_code_copied": "Friend code copied" } } diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index ef94c31f..36d38c96 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -261,6 +261,18 @@ "undo_friendship": "Desfazer amizade", "request_accepted": "Pedido de amizade aceito", "user_blocked_successfully": "Usuário bloqueado com sucesso", - "user_block_modal_text": "Bloquear {{displayName}}" + "user_block_modal_text": "Bloquear {{displayName}}", + "settings": "Configurações", + "privacy": "Privacidade", + "private": "Privado", + "friends_only": "Apenas amigos", + "public": "Público", + "blocked_users": "Usuários bloqueados", + "unblock": "Desbloquear", + "no_friends_added": "Você ainda não possui amigos adicionados", + "pending": "Pendentes", + "no_pending_invites": "Você não possui convites de amizade pendentes", + "no_blocked_users": "Você não tem nenhum usuário bloqueado", + "friend_code_copied": "Código de amigo copiado" } } diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 57daf51c..3963e4b0 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -43,6 +43,7 @@ import "./auth/sign-out"; import "./auth/open-auth-window"; import "./auth/get-session-hash"; import "./user/get-user"; +import "./user/get-user-blocks"; import "./user/block-user"; import "./user/unblock-user"; import "./user/get-user-friends"; @@ -52,11 +53,9 @@ import "./profile/undo-friendship"; import "./profile/update-friend-request"; import "./profile/update-profile"; import "./profile/send-friend-request"; +import { isPortableVersion } from "@main/helpers"; ipcMain.handle("ping", () => "pong"); ipcMain.handle("getVersion", () => app.getVersion()); -ipcMain.handle( - "isPortableVersion", - () => process.env.PORTABLE_EXECUTABLE_FILE != null -); +ipcMain.handle("isPortableVersion", () => isPortableVersion()); ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath); diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts index 8620eaa1..50d2ab66 100644 --- a/src/main/events/profile/update-profile.ts +++ b/src/main/events/profile/update-profile.ts @@ -4,33 +4,22 @@ import axios from "axios"; import fs from "node:fs"; import path from "node:path"; import { fileTypeFromFile } from "file-type"; -import { UserProfile } from "@types"; +import { UpdateProfileProps, UserProfile } from "@types"; -const patchUserProfile = async ( - displayName: string, - profileImageUrl?: string -) => { - if (profileImageUrl) { - return HydraApi.patch("/profile", { - displayName, - profileImageUrl, - }); - } else { - return HydraApi.patch("/profile", { - displayName, - }); - } +const patchUserProfile = async (updateProfile: UpdateProfileProps) => { + return HydraApi.patch("/profile", updateProfile); }; const updateProfile = async ( _event: Electron.IpcMainInvokeEvent, - displayName: string, - newProfileImagePath: string | null + updateProfile: UpdateProfileProps ): Promise => { - if (!newProfileImagePath) { - return patchUserProfile(displayName); + if (!updateProfile.profileImageUrl) { + return patchUserProfile(updateProfile); } + const newProfileImagePath = updateProfile.profileImageUrl; + const stats = fs.statSync(newProfileImagePath); const fileBuffer = fs.readFileSync(newProfileImagePath); const fileSizeInBytes = stats.size; @@ -53,7 +42,7 @@ const updateProfile = async ( }) .catch(() => undefined); - return patchUserProfile(displayName, profileImageUrl); + return patchUserProfile({ ...updateProfile, profileImageUrl }); }; registerEvent("updateProfile", updateProfile); diff --git a/src/main/events/user/get-user-blocks.ts b/src/main/events/user/get-user-blocks.ts new file mode 100644 index 00000000..65bb3eb4 --- /dev/null +++ b/src/main/events/user/get-user-blocks.ts @@ -0,0 +1,13 @@ +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services"; +import { UserBlocks } from "@types"; + +export const getUserBlocks = async ( + _event: Electron.IpcMainInvokeEvent, + take: number, + skip: number +): Promise => { + return HydraApi.get(`/profile/blocks`, { take, skip }); +}; + +registerEvent("getUserBlocks", getUserBlocks); diff --git a/src/main/helpers/index.ts b/src/main/helpers/index.ts index 902b927d..f2b86e5a 100644 --- a/src/main/helpers/index.ts +++ b/src/main/helpers/index.ts @@ -57,4 +57,7 @@ export const requestWebPage = async (url: string) => { .then((response) => response.data); }; +export const isPortableVersion = () => + process.env.PORTABLE_EXECUTABLE_FILE != null; + export * from "./download-source"; diff --git a/src/preload/index.ts b/src/preload/index.ts index 3350a340..087d573a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -10,6 +10,7 @@ import type { StartGameDownloadPayload, GameRunning, FriendRequestAction, + UpdateProfileProps, } from "@types"; contextBridge.exposeInMainWorld("electron", { @@ -137,8 +138,8 @@ contextBridge.exposeInMainWorld("electron", { getMe: () => ipcRenderer.invoke("getMe"), undoFriendship: (userId: string) => ipcRenderer.invoke("undoFriendship", userId), - updateProfile: (displayName: string, newProfileImagePath: string | null) => - ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath), + updateProfile: (updateProfile: UpdateProfileProps) => + ipcRenderer.invoke("updateProfile", updateProfile), getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"), updateFriendRequest: (userId: string, action: FriendRequestAction) => ipcRenderer.invoke("updateFriendRequest", userId, action), @@ -151,6 +152,8 @@ contextBridge.exposeInMainWorld("electron", { unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId), getUserFriends: (userId: string, take: number, skip: number) => ipcRenderer.invoke("getUserFriends", userId, take, skip), + getUserBlocks: (take: number, skip: number) => + ipcRenderer.invoke("getUserBlocks", take, skip), /* Auth */ signOut: () => ipcRenderer.invoke("signOut"), diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 8c6f7604..2b9ac187 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -108,7 +108,7 @@ export function App() { fetchFriendRequests(); } }); - }, [fetchUserDetails, updateUserDetails, dispatch]); + }, [fetchUserDetails, fetchFriendRequests, updateUserDetails, dispatch]); const onSignIn = useCallback(() => { fetchUserDetails().then((response) => { @@ -118,7 +118,13 @@ export function App() { showSuccessToast(t("successfully_signed_in")); } }); - }, [fetchUserDetails, t, showSuccessToast, updateUserDetails]); + }, [ + fetchUserDetails, + fetchFriendRequests, + t, + showSuccessToast, + updateUserDetails, + ]); useEffect(() => { const unsubscribe = window.electron.onGamesRunning((gamesRunning) => { diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index e022cffe..29e4dcbb 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -17,6 +17,7 @@ import type { FriendRequest, FriendRequestAction, UserFriends, + UserBlocks, } from "@types"; import type { DiskSpace } from "check-disk-space"; @@ -135,14 +136,12 @@ declare global { take: number, skip: number ) => Promise; + getUserBlocks: (take: number, skip: number) => Promise; /* Profile */ getMe: () => Promise; undoFriendship: (userId: string) => Promise; - updateProfile: ( - displayName: string, - newProfileImagePath: string | null - ) => Promise; + updateProfile: (updateProfile: UpdateProfileProps) => Promise; getFriendRequests: () => Promise; updateFriendRequest: ( userId: string, diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index 21690e7e..0cf2a381 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -8,8 +8,9 @@ import { setFriendsModalHidden, } from "@renderer/features"; import { profileBackgroundFromProfileImage } from "@renderer/helpers"; -import { FriendRequestAction, UserDetails } from "@types"; +import { FriendRequestAction, UpdateProfileProps, UserDetails } from "@types"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; +import { logger } from "@renderer/logger"; export function useUserDetails() { const dispatch = useAppDispatch(); @@ -43,7 +44,10 @@ export function useUserDetails() { if (userDetails.profileImageUrl) { const profileBackground = await profileBackgroundFromProfileImage( userDetails.profileImageUrl - ); + ).catch((err) => { + logger.error("profileBackgroundFromProfileImage", err); + return `#151515B3`; + }); dispatch(setProfileBackground(profileBackground)); window.localStorage.setItem( @@ -74,12 +78,8 @@ export function useUserDetails() { }, [clearUserDetails]); const patchUser = useCallback( - async (displayName: string, imageProfileUrl: string | null) => { - const response = await window.electron.updateProfile( - displayName, - imageProfileUrl - ); - + async (props: UpdateProfileProps) => { + const response = await window.electron.updateProfile(props); return updateUserDetails(response); }, [updateUserDetails] @@ -99,7 +99,7 @@ export function useUserDetails() { dispatch(setFriendsModalVisible({ initialTab, userId })); fetchFriendRequests(); }, - [dispatch] + [dispatch, fetchFriendRequests] ); const hideFriendsModal = useCallback(() => { diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx index 7c34d040..3ca837fa 100644 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx +++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx @@ -4,7 +4,6 @@ import { XCircleIcon, } from "@primer/octicons-react"; import * as styles from "./user-friend-modal.css"; -import cn from "classnames"; import { SPACING_UNIT } from "@renderer/theme.css"; import { useTranslation } from "react-i18next"; @@ -12,21 +11,26 @@ export type UserFriendItemProps = { userId: string; profileImageUrl: string | null; displayName: string; - onClickItem: (userId: string) => void; } & ( - | { type: "ACCEPTED"; onClickUndoFriendship: (userId: string) => void } + | { + type: "ACCEPTED"; + onClickUndoFriendship: (userId: string) => void; + onClickItem: (userId: string) => void; + } + | { type: "BLOCKED"; onClickUnblock: (userId: string) => void } | { type: "SENT" | "RECEIVED"; onClickCancelRequest: (userId: string) => void; onClickAcceptRequest: (userId: string) => void; onClickRefuseRequest: (userId: string) => void; + onClickItem: (userId: string) => void; } - | { type: null } + | { type: null; onClickItem: (userId: string) => void } ); export const UserFriendItem = (props: UserFriendItemProps) => { const { t } = useTranslation("user_profile"); - const { userId, profileImageUrl, displayName, type, onClickItem } = props; + const { userId, profileImageUrl, displayName, type } = props; const getRequestDescription = () => { if (type === "ACCEPTED" || type === null) return null; @@ -86,15 +90,69 @@ export const UserFriendItem = (props: UserFriendItemProps) => { ); } + if (type === "BLOCKED") { + return ( + + ); + } + return null; }; + if (type === "BLOCKED") { + return ( +
+
+
+ {profileImageUrl ? ( + {displayName} + ) : ( + + )} +
+
+

{displayName}

+
+
+ +
+ {getRequestActions()} +
+
+ ); + } + return ( -
+
- ); - })} - + <> +
+

Seu código de amigo:

+ +
+
+ {tabs.map((tab, index) => { + return ( + + ); + })} +
+ )} {renderTab()}
diff --git a/src/renderer/src/pages/user/user-content.tsx b/src/renderer/src/pages/user/user-content.tsx index 7425f102..b16c49bb 100644 --- a/src/renderer/src/pages/user/user-content.tsx +++ b/src/renderer/src/pages/user/user-content.tsx @@ -25,7 +25,7 @@ import { XCircleIcon, } from "@primer/octicons-react"; import { Button, Link } from "@renderer/components"; -import { UserEditProfileModal } from "./user-edit-modal"; +import { UserProfileSettingsModal } from "./user-profile-settings-modal"; import { UserSignOutModal } from "./user-sign-out-modal"; import { UserFriendModalTab } from "../shared-modals/user-friend-modal"; import { UserBlockModal } from "./user-block-modal"; @@ -60,7 +60,8 @@ export function UserContent({ const [profileContentBoxBackground, setProfileContentBoxBackground] = useState(); - const [showEditProfileModal, setShowEditProfileModal] = useState(false); + const [showProfileSettingsModal, setShowProfileSettingsModal] = + useState(false); const [showSignOutModal, setShowSignOutModal] = useState(false); const [showUserBlockModal, setShowUserBlockModal] = useState(false); @@ -95,7 +96,7 @@ export function UserContent({ }; const handleEditProfile = () => { - setShowEditProfileModal(true); + setShowProfileSettingsModal(true); }; const handleOnClickFriend = (userId: string) => { @@ -114,7 +115,7 @@ export function UserContent({ useEffect(() => { if (isMe) fetchFriendRequests(); - }, [isMe]); + }, [isMe, fetchFriendRequests]); useEffect(() => { if (isMe && profileBackground) { @@ -128,7 +129,7 @@ export function UserContent({ } ); } - }, [profileBackground, isMe]); + }, [profileBackground, isMe, userProfile.profileImageUrl]); const handleFriendAction = (userId: string, action: FriendAction) => { try { @@ -159,13 +160,18 @@ export function UserContent({ }; const showFriends = isMe || userProfile.totalFriends > 0; + const showProfileContent = + isMe || + userProfile.profileVisibility === "PUBLIC" || + (userProfile.relation?.status === "ACCEPTED" && + userProfile.profileVisibility === "FRIENDS"); const getProfileActions = () => { if (isMe) { return ( <>
-
-
-

{t("activity")}

- - {!userProfile.recentGames.length ? ( -
-
- -
-

{t("no_recent_activity_title")}

- {isMe &&

{t("no_recent_activity_description")}

} -
- ) : ( -
- {userProfile.recentGames.map((game) => ( - - ))} -
- )} -
- -
+ {showProfileContent && ( +
-
-

{t("library")}

+

{t("activity")}

+ {!userProfile.recentGames.length ? ( +
+
+ +
+

{t("no_recent_activity_title")}

+ {isMe &&

{t("no_recent_activity_description")}

} +
+ ) : (
-

- {userProfile.libraryGames.length} -

-
- {t("total_play_time", { amount: formatPlayTime() })} -
- {userProfile.libraryGames.map((game) => ( - - ))} -
+
+

{game.title}

+ + {t("last_time_played", { + period: formatDistance( + game.lastTimePlayed!, + new Date(), + { + addSuffix: true, + } + ), + })} + +
+ + ))} +
+ )}
- {showFriends && ( -
- - +
+ + {t("total_play_time", { amount: formatPlayTime() })} +
- {userProfile.friends.map((friend) => { - return ( - - ); - })} - - {isMe && ( - - )} + {game.iconUrl ? ( + {game.title} + ) : ( + + )} + + ))}
- )} + + {showFriends && ( +
+ + +
+ {userProfile.friends.map((friend) => { + return ( + + ); + })} + + {isMe && ( + + )} +
+
+ )} +
- + )} ); } diff --git a/src/renderer/src/pages/user/user-edit-modal.tsx b/src/renderer/src/pages/user/user-edit-modal.tsx deleted file mode 100644 index a22650ee..00000000 --- a/src/renderer/src/pages/user/user-edit-modal.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { Button, Modal, TextField } from "@renderer/components"; -import { UserProfile } from "@types"; -import * as styles from "./user.css"; -import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react"; -import { SPACING_UNIT } from "@renderer/theme.css"; -import { useEffect, useMemo, useState } from "react"; -import { useToast, useUserDetails } from "@renderer/hooks"; -import { useTranslation } from "react-i18next"; - -export interface UserEditProfileModalProps { - userProfile: UserProfile; - visible: boolean; - onClose: () => void; - updateUserProfile: () => Promise; -} - -export const UserEditProfileModal = ({ - userProfile, - visible, - onClose, - updateUserProfile, -}: UserEditProfileModalProps) => { - const { t } = useTranslation("user_profile"); - - const [displayName, setDisplayName] = useState(""); - const [newImagePath, setNewImagePath] = useState(null); - const [isSaving, setIsSaving] = useState(false); - - const { patchUser } = useUserDetails(); - - const { showSuccessToast, showErrorToast } = useToast(); - - useEffect(() => { - setDisplayName(userProfile.displayName); - }, [userProfile.displayName]); - - const handleChangeProfileAvatar = async () => { - const { filePaths } = await window.electron.showOpenDialog({ - properties: ["openFile"], - filters: [ - { - name: "Image", - extensions: ["jpg", "jpeg", "png", "webp"], - }, - ], - }); - - if (filePaths && filePaths.length > 0) { - const path = filePaths[0]; - - setNewImagePath(path); - } - }; - - const handleSaveProfile: React.FormEventHandler = async ( - event - ) => { - event.preventDefault(); - setIsSaving(true); - - patchUser(displayName, newImagePath) - .then(async () => { - await updateUserProfile(); - showSuccessToast(t("saved_successfully")); - cleanFormAndClose(); - }) - .catch(() => { - showErrorToast(t("try_again")); - }) - .finally(() => { - setIsSaving(false); - }); - }; - - const resetModal = () => { - setDisplayName(userProfile.displayName); - setNewImagePath(null); - }; - - const cleanFormAndClose = () => { - resetModal(); - onClose(); - }; - - const avatarUrl = useMemo(() => { - if (newImagePath) return `local:${newImagePath}`; - if (userProfile.profileImageUrl) return userProfile.profileImageUrl; - return null; - }, [newImagePath, userProfile.profileImageUrl]); - - return ( - <> - -
- - - setDisplayName(e.target.value)} - /> - - -
- - ); -}; diff --git a/src/renderer/src/pages/user/user-profile-settings-modal/index.tsx b/src/renderer/src/pages/user/user-profile-settings-modal/index.tsx new file mode 100644 index 00000000..896d3684 --- /dev/null +++ b/src/renderer/src/pages/user/user-profile-settings-modal/index.tsx @@ -0,0 +1 @@ +export * from "./user-profile-settings-modal"; diff --git a/src/renderer/src/pages/user/user-profile-settings-modal/user-block-list.tsx b/src/renderer/src/pages/user/user-profile-settings-modal/user-block-list.tsx new file mode 100644 index 00000000..c062eabb --- /dev/null +++ b/src/renderer/src/pages/user/user-profile-settings-modal/user-block-list.tsx @@ -0,0 +1,118 @@ +import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import { UserFriend } from "@types"; +import { useEffect, useRef, useState } from "react"; +import { useToast, useUserDetails } from "@renderer/hooks"; +import { useTranslation } from "react-i18next"; +import { UserFriendItem } from "@renderer/pages/shared-modals/user-friend-modal/user-friend-item"; +import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; + +const pageSize = 12; + +export const UserEditProfileBlockList = () => { + const { t } = useTranslation("user_profile"); + const { showErrorToast } = useToast(); + + const [page, setPage] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [maxPage, setMaxPage] = useState(0); + const [blocks, setBlocks] = useState([]); + const listContainer = useRef(null); + + const { unblockUser } = useUserDetails(); + + const loadNextPage = () => { + if (page > maxPage) return; + setIsLoading(true); + window.electron + .getUserBlocks(pageSize, page * pageSize) + .then((newPage) => { + if (page === 0) { + setMaxPage(newPage.totalBlocks / pageSize); + } + + setBlocks([...blocks, ...newPage.blocks]); + setPage(page + 1); + }) + .catch(() => {}) + .finally(() => setIsLoading(false)); + }; + + const handleScroll = () => { + const scrollTop = listContainer.current?.scrollTop || 0; + const scrollHeight = listContainer.current?.scrollHeight || 0; + const clientHeight = listContainer.current?.clientHeight || 0; + const maxScrollTop = scrollHeight - clientHeight; + + if (scrollTop < maxScrollTop * 0.9 || isLoading) { + return; + } + + loadNextPage(); + }; + + useEffect(() => { + listContainer.current?.addEventListener("scroll", handleScroll); + return () => + listContainer.current?.removeEventListener("scroll", handleScroll); + }, [isLoading]); + + const reloadList = () => { + setPage(0); + setMaxPage(0); + setBlocks([]); + loadNextPage(); + }; + + useEffect(() => { + reloadList(); + }, []); + + const handleUnblock = (userId: string) => { + unblockUser(userId) + .then(() => { + reloadList(); + }) + .catch(() => { + showErrorToast(t("try_again")); + }); + }; + + return ( + +
+ {!isLoading && blocks.length === 0 &&

{t("no_blocked_users")}

} + {blocks.map((friend) => { + return ( + + ); + })} + {isLoading && ( + + )} +
+
+ ); +}; diff --git a/src/renderer/src/pages/user/user-profile-settings-modal/user-edit-profile.tsx b/src/renderer/src/pages/user/user-profile-settings-modal/user-edit-profile.tsx new file mode 100644 index 00000000..f6a430ba --- /dev/null +++ b/src/renderer/src/pages/user/user-profile-settings-modal/user-edit-profile.tsx @@ -0,0 +1,149 @@ +import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react"; +import { Button, SelectField, TextField } from "@renderer/components"; +import { useToast, useUserDetails } from "@renderer/hooks"; +import { UserProfile } from "@types"; +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import * as styles from "../user.css"; +import { SPACING_UNIT } from "@renderer/theme.css"; + +export interface UserEditProfileProps { + userProfile: UserProfile; + updateUserProfile: () => Promise; +} + +export const UserEditProfile = ({ + userProfile, + updateUserProfile, +}: UserEditProfileProps) => { + const { t } = useTranslation("user_profile"); + + const [form, setForm] = useState({ + displayName: userProfile.displayName, + profileVisibility: userProfile.profileVisibility, + imageProfileUrl: null as string | null, + }); + const [isSaving, setIsSaving] = useState(false); + + const { patchUser } = useUserDetails(); + + const { showSuccessToast, showErrorToast } = useToast(); + + const [profileVisibilityOptions, setProfileVisibilityOptions] = useState< + { value: string; label: string }[] + >([]); + + useEffect(() => { + setProfileVisibilityOptions([ + { value: "PUBLIC", label: t("public") }, + { value: "FRIENDS", label: t("friends_only") }, + { value: "PRIVATE", label: t("private") }, + ]); + }, [t]); + + const handleChangeProfileAvatar = async () => { + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openFile"], + filters: [ + { + name: "Image", + extensions: ["jpg", "jpeg", "png", "webp"], + }, + ], + }); + + if (filePaths && filePaths.length > 0) { + const path = filePaths[0]; + + setForm({ ...form, imageProfileUrl: path }); + } + }; + + const handleProfileVisibilityChange = (event) => { + setForm({ + ...form, + profileVisibility: event.target.value, + }); + }; + + const handleSaveProfile: React.FormEventHandler = async ( + event + ) => { + event.preventDefault(); + setIsSaving(true); + + patchUser(form) + .then(async () => { + await updateUserProfile(); + showSuccessToast(t("saved_successfully")); + }) + .catch(() => { + showErrorToast(t("try_again")); + }) + .finally(() => { + setIsSaving(false); + }); + }; + + const avatarUrl = useMemo(() => { + if (form.imageProfileUrl) return `local:${form.imageProfileUrl}`; + if (userProfile.profileImageUrl) return userProfile.profileImageUrl; + return null; + }, [form, userProfile]); + + return ( +
+ + + setForm({ ...form, displayName: e.target.value })} + /> + + ({ + key: visiblity.value, + value: visiblity.value, + label: visiblity.label, + }))} + /> + + + + ); +}; diff --git a/src/renderer/src/pages/user/user-profile-settings-modal/user-profile-settings-modal.tsx b/src/renderer/src/pages/user/user-profile-settings-modal/user-profile-settings-modal.tsx new file mode 100644 index 00000000..d71b1bd7 --- /dev/null +++ b/src/renderer/src/pages/user/user-profile-settings-modal/user-profile-settings-modal.tsx @@ -0,0 +1,73 @@ +import { Button, Modal } from "@renderer/components"; +import { UserProfile } from "@types"; +import { SPACING_UNIT } from "@renderer/theme.css"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { UserEditProfile } from "./user-edit-profile"; +import { UserEditProfileBlockList } from "./user-block-list"; + +export interface UserProfileSettingsModalProps { + userProfile: UserProfile; + visible: boolean; + onClose: () => void; + updateUserProfile: () => Promise; +} + +export const UserProfileSettingsModal = ({ + userProfile, + visible, + onClose, + updateUserProfile, +}: UserProfileSettingsModalProps) => { + const { t } = useTranslation("user_profile"); + + const tabs = [t("edit_profile"), t("blocked_users")]; + + const [currentTabIndex, setCurrentTabIndex] = useState(0); + + const renderTab = () => { + if (currentTabIndex == 0) { + return ( + + ); + } + + if (currentTabIndex == 1) { + return ; + } + + return <>; + }; + + return ( + <> + +
+
+ {tabs.map((tab, index) => { + return ( + + ); + })} +
+ {renderTab()} +
+
+ + ); +}; diff --git a/src/renderer/src/pages/user/user.css.ts b/src/renderer/src/pages/user/user.css.ts index f9b1b09a..4e1c2139 100644 --- a/src/renderer/src/pages/user/user.css.ts +++ b/src/renderer/src/pages/user/user.css.ts @@ -60,6 +60,7 @@ export const friendListDisplayName = style({ }); export const profileAvatarEditContainer = style({ + alignSelf: "center", width: "128px", height: "128px", display: "flex", diff --git a/src/renderer/src/pages/user/user.tsx b/src/renderer/src/pages/user/user.tsx index 4c45f789..565d412a 100644 --- a/src/renderer/src/pages/user/user.tsx +++ b/src/renderer/src/pages/user/user.tsx @@ -31,7 +31,7 @@ export const User = () => { navigate(-1); } }); - }, [dispatch, userId, t]); + }, [dispatch, navigate, showErrorToast, userId, t]); useEffect(() => { getUserProfile(); diff --git a/src/types/index.ts b/src/types/index.ts index ac352a91..6200d825 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -282,6 +282,11 @@ export interface UserFriends { friends: UserFriend[]; } +export interface UserBlocks { + totalBlocks: number; + blocks: UserFriend[]; +} + export interface FriendRequest { id: string; displayName: string; @@ -310,6 +315,13 @@ export interface UserProfile { relation: UserRelation | null; } +export interface UpdateProfileProps { + displayName?: string; + profileVisibility?: "PUBLIC" | "PRIVATE" | "FRIENDS"; + profileImageUrl?: string | null; + bio?: string; +} + export interface DownloadSource { id: number; name: string;