mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
Merge branch 'main' into patch-2
This commit is contained in:
commit
49ed55abfc
@ -250,6 +250,17 @@
|
||||
"friend_request_sent": "Friend request sent",
|
||||
"friends": "Friends",
|
||||
"friends_list": "Friends list",
|
||||
"user_not_found": "User not found"
|
||||
"user_not_found": "User not found",
|
||||
"block_user": "Block user",
|
||||
"add_friend": "Add friend",
|
||||
"request_sent": "Request sent",
|
||||
"request_received": "Request received",
|
||||
"accept_request": "Accept request",
|
||||
"ignore_request": "Ignore request",
|
||||
"cancel_request": "Cancel request",
|
||||
"undo_friendship": "Undo friendship",
|
||||
"request_accepted": "Request accepted",
|
||||
"user_blocked_successfully": "User blocked successfully",
|
||||
"user_block_modal_text": "This will block {{displayName}}"
|
||||
}
|
||||
}
|
||||
|
@ -241,6 +241,15 @@
|
||||
"successfully_signed_out": "Sesión cerrada exitosamente",
|
||||
"sign_out": "Cerrar sesión",
|
||||
"playing_for": "Jugando por {{amount}}",
|
||||
"sign_out_modal_text": "Tu biblioteca se ha vinculado con tu cuenta. Cuando cierres sesión, tú biblioteca ya no será visible y cualquier progreso no se guardará. ¿Continuar con el cierre de sesión?"
|
||||
"sign_out_modal_text": "Tu biblioteca se ha vinculado con tu cuenta. Cuando cierres sesión, tú biblioteca ya no será visible y cualquier progreso no se guardará. ¿Continuar con el cierre de sesión?",
|
||||
"add_friends": "Añadir amigos",
|
||||
"add": "Añadir",
|
||||
"friend_code": "Código de amigo",
|
||||
"see_profile": "Ver perfil",
|
||||
"sending": "Enviando",
|
||||
"friend_request_sent": "Solicitud de amistad enviada",
|
||||
"friends": "Amigos",
|
||||
"friends_list": "Lista de amigos",
|
||||
"user_not_found": "Usuario no encontrado"
|
||||
}
|
||||
}
|
||||
|
@ -250,6 +250,17 @@
|
||||
"add": "Adicionar",
|
||||
"sending": "Enviando",
|
||||
"friends_list": "Lista de amigos",
|
||||
"user_not_found": "Usuário não encontrado"
|
||||
"user_not_found": "Usuário não encontrado",
|
||||
"block_user": "Bloquear",
|
||||
"add_friend": "Adicionar amigo",
|
||||
"request_sent": "Pedido enviado",
|
||||
"request_received": "Pedido recebido",
|
||||
"accept_request": "Aceitar pedido",
|
||||
"ignore_request": "Ignorar pedido",
|
||||
"cancel_request": "Cancelar pedido",
|
||||
"undo_friendship": "Desfazer amizade",
|
||||
"request_accepted": "Pedido de amizade aceito",
|
||||
"user_blocked_successfully": "Usuário bloqueado com sucesso",
|
||||
"user_block_modal_text": "Bloquear {{displayName}}"
|
||||
}
|
||||
}
|
||||
|
@ -43,8 +43,12 @@ import "./auth/sign-out";
|
||||
import "./auth/open-auth-window";
|
||||
import "./auth/get-session-hash";
|
||||
import "./user/get-user";
|
||||
import "./user/block-user";
|
||||
import "./user/unblock-user";
|
||||
import "./user/get-user-friends";
|
||||
import "./profile/get-friend-requests";
|
||||
import "./profile/get-me";
|
||||
import "./profile/undo-friendship";
|
||||
import "./profile/update-friend-request";
|
||||
import "./profile/update-profile";
|
||||
import "./profile/send-friend-request";
|
||||
|
11
src/main/events/profile/undo-friendship.ts
Normal file
11
src/main/events/profile/undo-friendship.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const undoFriendship = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
userId: string
|
||||
) => {
|
||||
await HydraApi.delete(`/profile/friends/${userId}`);
|
||||
};
|
||||
|
||||
registerEvent("undoFriendship", undoFriendship);
|
11
src/main/events/user/block-user.ts
Normal file
11
src/main/events/user/block-user.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const blockUser = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
userId: string
|
||||
) => {
|
||||
await HydraApi.post(`/user/${userId}/block`);
|
||||
};
|
||||
|
||||
registerEvent("blockUser", blockUser);
|
29
src/main/events/user/get-user-friends.ts
Normal file
29
src/main/events/user/get-user-friends.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { userAuthRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { UserFriends } from "@types";
|
||||
|
||||
export const getUserFriends = async (
|
||||
userId: string,
|
||||
take: number,
|
||||
skip: number
|
||||
): Promise<UserFriends> => {
|
||||
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||
|
||||
if (loggedUser?.userId === userId) {
|
||||
return HydraApi.get(`/profile/friends`, { take, skip });
|
||||
}
|
||||
|
||||
return HydraApi.get(`/user/${userId}/friends`, { take, skip });
|
||||
};
|
||||
|
||||
const getUserFriendsEvent = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
userId: string,
|
||||
take: number,
|
||||
skip: number
|
||||
) => {
|
||||
return getUserFriends(userId, take, skip);
|
||||
};
|
||||
|
||||
registerEvent("getUserFriends", getUserFriendsEvent);
|
@ -4,13 +4,19 @@ import { steamGamesWorker } from "@main/workers";
|
||||
import { UserProfile } from "@types";
|
||||
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
||||
import { getSteamAppAsset } from "@main/helpers";
|
||||
import { getUserFriends } from "./get-user-friends";
|
||||
|
||||
const getUser = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
userId: string
|
||||
): Promise<UserProfile | null> => {
|
||||
try {
|
||||
const profile = await HydraApi.get(`/user/${userId}`);
|
||||
const [profile, friends] = await Promise.all([
|
||||
HydraApi.get(`/user/${userId}`),
|
||||
getUserFriends(userId, 12, 0).catch(() => {
|
||||
return { totalFriends: 0, friends: [] };
|
||||
}),
|
||||
]);
|
||||
|
||||
const recentGames = await Promise.all(
|
||||
profile.recentGames.map(async (game) => {
|
||||
@ -46,7 +52,13 @@ const getUser = async (
|
||||
})
|
||||
);
|
||||
|
||||
return { ...profile, libraryGames, recentGames };
|
||||
return {
|
||||
...profile,
|
||||
libraryGames,
|
||||
recentGames,
|
||||
friends: friends.friends,
|
||||
totalFriends: friends.totalFriends,
|
||||
};
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
11
src/main/events/user/unblock-user.ts
Normal file
11
src/main/events/user/unblock-user.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const unblockUser = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
userId: string
|
||||
) => {
|
||||
await HydraApi.post(`/user/${userId}/unblock`);
|
||||
};
|
||||
|
||||
registerEvent("unblockUser", unblockUser);
|
@ -72,7 +72,7 @@ export class HydraApi {
|
||||
this.instance.interceptors.request.use(
|
||||
(request) => {
|
||||
logger.log(" ---- REQUEST -----");
|
||||
logger.log(request.method, request.url, request.data);
|
||||
logger.log(request.method, request.url, request.params, request.data);
|
||||
return request;
|
||||
},
|
||||
(error) => {
|
||||
@ -196,52 +196,52 @@ export class HydraApi {
|
||||
throw err;
|
||||
};
|
||||
|
||||
static async get(url: string) {
|
||||
static async get<T = any>(url: string, params?: any) {
|
||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
return this.instance
|
||||
.get(url, this.getAxiosConfig())
|
||||
.get<T>(url, { params, ...this.getAxiosConfig() })
|
||||
.then((response) => response.data)
|
||||
.catch(this.handleUnauthorizedError);
|
||||
}
|
||||
|
||||
static async post(url: string, data?: any) {
|
||||
static async post<T = any>(url: string, data?: any) {
|
||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
return this.instance
|
||||
.post(url, data, this.getAxiosConfig())
|
||||
.post<T>(url, data, this.getAxiosConfig())
|
||||
.then((response) => response.data)
|
||||
.catch(this.handleUnauthorizedError);
|
||||
}
|
||||
|
||||
static async put(url: string, data?: any) {
|
||||
static async put<T = any>(url: string, data?: any) {
|
||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
return this.instance
|
||||
.put(url, data, this.getAxiosConfig())
|
||||
.put<T>(url, data, this.getAxiosConfig())
|
||||
.then((response) => response.data)
|
||||
.catch(this.handleUnauthorizedError);
|
||||
}
|
||||
|
||||
static async patch(url: string, data?: any) {
|
||||
static async patch<T = any>(url: string, data?: any) {
|
||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
return this.instance
|
||||
.patch(url, data, this.getAxiosConfig())
|
||||
.patch<T>(url, data, this.getAxiosConfig())
|
||||
.then((response) => response.data)
|
||||
.catch(this.handleUnauthorizedError);
|
||||
}
|
||||
|
||||
static async delete(url: string) {
|
||||
static async delete<T = any>(url: string) {
|
||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
return this.instance
|
||||
.delete(url, this.getAxiosConfig())
|
||||
.delete<T>(url, this.getAxiosConfig())
|
||||
.then((response) => response.data)
|
||||
.catch(this.handleUnauthorizedError);
|
||||
}
|
||||
|
@ -135,6 +135,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
|
||||
/* Profile */
|
||||
getMe: () => ipcRenderer.invoke("getMe"),
|
||||
undoFriendship: (userId: string) =>
|
||||
ipcRenderer.invoke("undoFriendship", userId),
|
||||
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
|
||||
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
|
||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||
@ -145,6 +147,10 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
|
||||
/* User */
|
||||
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
||||
blockUser: (userId: string) => ipcRenderer.invoke("blockUser", userId),
|
||||
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
||||
getUserFriends: (userId: string, take: number, skip: number) =>
|
||||
ipcRenderer.invoke("getUserFriends", userId, take, skip),
|
||||
|
||||
/* Auth */
|
||||
signOut: () => ipcRenderer.invoke("signOut"),
|
||||
|
@ -6,7 +6,7 @@
|
||||
<title>Hydra</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: https://cdn.discordapp.com https://*.cloudfront.net https://*.s3.amazonaws.com https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' local: data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: https://*.cloudfront.net https://*.s3.amazonaws.com https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' local: data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;"
|
||||
/>
|
||||
</head>
|
||||
<body style="background-color: #1c1c1c">
|
||||
|
@ -42,11 +42,12 @@ export function App() {
|
||||
const {
|
||||
isFriendsModalVisible,
|
||||
friendRequetsModalTab,
|
||||
updateFriendRequests,
|
||||
friendModalUserId,
|
||||
fetchFriendRequests,
|
||||
hideFriendsModal,
|
||||
} = useUserDetails();
|
||||
|
||||
const { fetchUserDetails, updateUserDetails, clearUserDetails } =
|
||||
const { userDetails, fetchUserDetails, updateUserDetails, clearUserDetails } =
|
||||
useUserDetails();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
@ -104,7 +105,7 @@ export function App() {
|
||||
fetchUserDetails().then((response) => {
|
||||
if (response) {
|
||||
updateUserDetails(response);
|
||||
updateFriendRequests();
|
||||
fetchFriendRequests();
|
||||
}
|
||||
});
|
||||
}, [fetchUserDetails, updateUserDetails, dispatch]);
|
||||
@ -113,7 +114,7 @@ export function App() {
|
||||
fetchUserDetails().then((response) => {
|
||||
if (response) {
|
||||
updateUserDetails(response);
|
||||
updateFriendRequests();
|
||||
fetchFriendRequests();
|
||||
showSuccessToast(t("successfully_signed_in"));
|
||||
}
|
||||
});
|
||||
@ -218,11 +219,14 @@ export function App() {
|
||||
onClose={handleToastClose}
|
||||
/>
|
||||
|
||||
<UserFriendModal
|
||||
visible={isFriendsModalVisible}
|
||||
initialTab={friendRequetsModalTab}
|
||||
onClose={hideFriendsModal}
|
||||
/>
|
||||
{userDetails && (
|
||||
<UserFriendModal
|
||||
visible={isFriendsModalVisible}
|
||||
initialTab={friendRequetsModalTab}
|
||||
onClose={hideFriendsModal}
|
||||
userId={friendModalUserId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main>
|
||||
<Sidebar />
|
||||
|
@ -3,10 +3,11 @@ import { PersonAddIcon, PersonIcon } from "@primer/octicons-react";
|
||||
import * as styles from "./sidebar-profile.css";
|
||||
import { assignInlineVars } from "@vanilla-extract/dynamic";
|
||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { profileContainerBackground } from "./sidebar-profile.css";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import { FriendRequest } from "@types";
|
||||
|
||||
export function SidebarProfile() {
|
||||
const navigate = useNavigate();
|
||||
@ -16,6 +17,14 @@ export function SidebarProfile() {
|
||||
const { userDetails, profileBackground, friendRequests, showFriendsModal } =
|
||||
useUserDetails();
|
||||
|
||||
const [receivedRequests, setReceivedRequests] = useState<FriendRequest[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setReceivedRequests(
|
||||
friendRequests.filter((request) => request.type === "RECEIVED")
|
||||
);
|
||||
}, [friendRequests]);
|
||||
|
||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
@ -79,15 +88,17 @@ export function SidebarProfile() {
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
{userDetails && friendRequests.length > 0 && !gameRunning && (
|
||||
{userDetails && receivedRequests.length > 0 && !gameRunning && (
|
||||
<div className={styles.friendRequestContainer}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.friendRequestButton}
|
||||
onClick={() => showFriendsModal(UserFriendModalTab.AddFriend)}
|
||||
onClick={() =>
|
||||
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
||||
}
|
||||
>
|
||||
<PersonAddIcon size={24} />
|
||||
{friendRequests.length}
|
||||
{receivedRequests.length}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
9
src/renderer/src/declaration.d.ts
vendored
9
src/renderer/src/declaration.d.ts
vendored
@ -16,6 +16,7 @@ import type {
|
||||
UserProfile,
|
||||
FriendRequest,
|
||||
FriendRequestAction,
|
||||
UserFriends,
|
||||
} from "@types";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
||||
@ -127,9 +128,17 @@ declare global {
|
||||
|
||||
/* User */
|
||||
getUser: (userId: string) => Promise<UserProfile | null>;
|
||||
blockUser: (userId: string) => Promise<void>;
|
||||
unblockUser: (userId: string) => Promise<void>;
|
||||
getUserFriends: (
|
||||
userId: string,
|
||||
take: number,
|
||||
skip: number
|
||||
) => Promise<UserFriends>;
|
||||
|
||||
/* Profile */
|
||||
getMe: () => Promise<UserProfile | null>;
|
||||
undoFriendship: (userId: string) => Promise<void>;
|
||||
updateProfile: (
|
||||
displayName: string,
|
||||
newProfileImagePath: string | null
|
||||
|
@ -8,6 +8,7 @@ export interface UserDetailsState {
|
||||
friendRequests: FriendRequest[];
|
||||
isFriendsModalVisible: boolean;
|
||||
friendRequetsModalTab: UserFriendModalTab | null;
|
||||
friendModalUserId: string;
|
||||
}
|
||||
|
||||
const initialState: UserDetailsState = {
|
||||
@ -16,6 +17,7 @@ const initialState: UserDetailsState = {
|
||||
friendRequests: [],
|
||||
isFriendsModalVisible: false,
|
||||
friendRequetsModalTab: null,
|
||||
friendModalUserId: "",
|
||||
};
|
||||
|
||||
export const userDetailsSlice = createSlice({
|
||||
@ -33,10 +35,11 @@ export const userDetailsSlice = createSlice({
|
||||
},
|
||||
setFriendsModalVisible: (
|
||||
state,
|
||||
action: PayloadAction<UserFriendModalTab>
|
||||
action: PayloadAction<{ initialTab: UserFriendModalTab; userId: string }>
|
||||
) => {
|
||||
state.isFriendsModalVisible = true;
|
||||
state.friendRequetsModalTab = action.payload;
|
||||
state.friendRequetsModalTab = action.payload.initialTab;
|
||||
state.friendModalUserId = action.payload.userId;
|
||||
},
|
||||
setFriendsModalHidden: (state) => {
|
||||
state.isFriendsModalVisible = false;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
import Color from "color";
|
||||
import { average } from "color.js";
|
||||
|
||||
export const steamUrlBuilder = {
|
||||
library: (objectID: string) =>
|
||||
@ -45,3 +46,14 @@ export const buildGameDetailsPath = (
|
||||
|
||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
||||
new Color(color).darken(amount).alpha(alpha).toString();
|
||||
|
||||
export const profileBackgroundFromProfileImage = async (
|
||||
profileImageUrl: string
|
||||
) => {
|
||||
const output = await average(profileImageUrl, {
|
||||
amount: 1,
|
||||
format: "hex",
|
||||
});
|
||||
|
||||
return `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8, 0.7)})`;
|
||||
};
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { useCallback } from "react";
|
||||
import { average } from "color.js";
|
||||
|
||||
import { useAppDispatch, useAppSelector } from "./redux";
|
||||
import {
|
||||
setProfileBackground,
|
||||
@ -9,7 +7,7 @@ import {
|
||||
setFriendsModalVisible,
|
||||
setFriendsModalHidden,
|
||||
} from "@renderer/features";
|
||||
import { darkenColor } from "@renderer/helpers";
|
||||
import { profileBackgroundFromProfileImage } from "@renderer/helpers";
|
||||
import { FriendRequestAction, UserDetails } from "@types";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
|
||||
@ -21,6 +19,7 @@ export function useUserDetails() {
|
||||
profileBackground,
|
||||
friendRequests,
|
||||
isFriendsModalVisible,
|
||||
friendModalUserId,
|
||||
friendRequetsModalTab,
|
||||
} = useAppSelector((state) => state.userDetails);
|
||||
|
||||
@ -42,12 +41,9 @@ export function useUserDetails() {
|
||||
dispatch(setUserDetails(userDetails));
|
||||
|
||||
if (userDetails.profileImageUrl) {
|
||||
const output = await average(userDetails.profileImageUrl, {
|
||||
amount: 1,
|
||||
format: "hex",
|
||||
});
|
||||
|
||||
const profileBackground = `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8, 0.7)})`;
|
||||
const profileBackground = await profileBackgroundFromProfileImage(
|
||||
userDetails.profileImageUrl
|
||||
);
|
||||
dispatch(setProfileBackground(profileBackground));
|
||||
|
||||
window.localStorage.setItem(
|
||||
@ -89,15 +85,19 @@ export function useUserDetails() {
|
||||
[updateUserDetails]
|
||||
);
|
||||
|
||||
const updateFriendRequests = useCallback(async () => {
|
||||
const friendRequests = await window.electron.getFriendRequests();
|
||||
dispatch(setFriendRequests(friendRequests));
|
||||
const fetchFriendRequests = useCallback(() => {
|
||||
return window.electron
|
||||
.getFriendRequests()
|
||||
.then((friendRequests) => {
|
||||
dispatch(setFriendRequests(friendRequests));
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [dispatch]);
|
||||
|
||||
const showFriendsModal = useCallback(
|
||||
(tab: UserFriendModalTab) => {
|
||||
dispatch(setFriendsModalVisible(tab));
|
||||
updateFriendRequests();
|
||||
(initialTab: UserFriendModalTab, userId: string) => {
|
||||
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
||||
fetchFriendRequests();
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
@ -110,26 +110,39 @@ export function useUserDetails() {
|
||||
async (userId: string) => {
|
||||
return window.electron
|
||||
.sendFriendRequest(userId)
|
||||
.then(() => updateFriendRequests());
|
||||
.then(() => fetchFriendRequests());
|
||||
},
|
||||
[updateFriendRequests]
|
||||
[fetchFriendRequests]
|
||||
);
|
||||
|
||||
const updateFriendRequestState = useCallback(
|
||||
async (userId: string, action: FriendRequestAction) => {
|
||||
return window.electron
|
||||
.updateFriendRequest(userId, action)
|
||||
.then(() => updateFriendRequests());
|
||||
.then(() => fetchFriendRequests());
|
||||
},
|
||||
[updateFriendRequests]
|
||||
[fetchFriendRequests]
|
||||
);
|
||||
|
||||
const undoFriendship = (userId: string) => {
|
||||
return window.electron.undoFriendship(userId);
|
||||
};
|
||||
|
||||
const blockUser = (userId: string) => {
|
||||
return window.electron.blockUser(userId);
|
||||
};
|
||||
|
||||
const unblockUser = (userId: string) => {
|
||||
return window.electron.unblockUser(userId);
|
||||
};
|
||||
|
||||
return {
|
||||
userDetails,
|
||||
profileBackground,
|
||||
friendRequests,
|
||||
friendRequetsModalTab,
|
||||
isFriendsModalVisible,
|
||||
friendModalUserId,
|
||||
showFriendsModal,
|
||||
hideFriendsModal,
|
||||
fetchUserDetails,
|
||||
@ -138,7 +151,10 @@ export function useUserDetails() {
|
||||
updateUserDetails,
|
||||
patchUser,
|
||||
sendFriendRequest,
|
||||
updateFriendRequests,
|
||||
fetchFriendRequests,
|
||||
updateFriendRequestState,
|
||||
blockUser,
|
||||
unblockUser,
|
||||
undoFriendship,
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,136 @@
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
PersonIcon,
|
||||
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";
|
||||
|
||||
export type UserFriendItemProps = {
|
||||
userId: string;
|
||||
profileImageUrl: string | null;
|
||||
displayName: string;
|
||||
onClickItem: (userId: string) => void;
|
||||
} & (
|
||||
| { type: "ACCEPTED"; onClickUndoFriendship: (userId: string) => void }
|
||||
| {
|
||||
type: "SENT" | "RECEIVED";
|
||||
onClickCancelRequest: (userId: string) => void;
|
||||
onClickAcceptRequest: (userId: string) => void;
|
||||
onClickRefuseRequest: (userId: string) => void;
|
||||
}
|
||||
| { type: null }
|
||||
);
|
||||
|
||||
export const UserFriendItem = (props: UserFriendItemProps) => {
|
||||
const { t } = useTranslation("user_profile");
|
||||
const { userId, profileImageUrl, displayName, type, onClickItem } = props;
|
||||
|
||||
const getRequestDescription = () => {
|
||||
if (type === "ACCEPTED" || type === null) return null;
|
||||
|
||||
return (
|
||||
<small>
|
||||
{type == "SENT" ? t("request_sent") : t("request_received")}
|
||||
</small>
|
||||
);
|
||||
};
|
||||
|
||||
const getRequestActions = () => {
|
||||
if (type === null) return null;
|
||||
|
||||
if (type === "SENT") {
|
||||
return (
|
||||
<button
|
||||
className={styles.cancelRequestButton}
|
||||
onClick={() => props.onClickCancelRequest(userId)}
|
||||
title={t("cancel_request")}
|
||||
>
|
||||
<XCircleIcon size={28} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "RECEIVED") {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className={styles.acceptRequestButton}
|
||||
onClick={() => props.onClickAcceptRequest(userId)}
|
||||
title={t("accept_request")}
|
||||
>
|
||||
<CheckCircleIcon size={28} />
|
||||
</button>
|
||||
<button
|
||||
className={styles.cancelRequestButton}
|
||||
onClick={() => props.onClickRefuseRequest(userId)}
|
||||
title={t("ignore_request")}
|
||||
>
|
||||
<XCircleIcon size={28} />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "ACCEPTED") {
|
||||
return (
|
||||
<button
|
||||
className={styles.cancelRequestButton}
|
||||
onClick={() => props.onClickUndoFriendship(userId)}
|
||||
title={t("undo_friendship")}
|
||||
>
|
||||
<XCircleIcon size={28} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(styles.friendListContainer, styles.profileContentBox)}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.friendListButton}
|
||||
onClick={() => onClickItem(userId)}
|
||||
>
|
||||
<div className={styles.friendAvatarContainer}>
|
||||
{profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={displayName}
|
||||
src={profileImageUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={24} />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
flex: "1",
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<p className={styles.friendListDisplayName}>{displayName}</p>
|
||||
{getRequestDescription()}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: "8px",
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
{getRequestActions()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -4,7 +4,7 @@ import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { UserFriendRequest } from "./user-friend-request";
|
||||
import { UserFriendItem } from "./user-friend-item";
|
||||
|
||||
export interface UserFriendModalAddFriendProps {
|
||||
closeModal: () => void;
|
||||
@ -23,7 +23,7 @@ export const UserFriendModalAddFriend = ({
|
||||
const { sendFriendRequest, updateFriendRequestState, friendRequests } =
|
||||
useUserDetails();
|
||||
|
||||
const { showErrorToast } = useToast();
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
const handleClickAddFriend = () => {
|
||||
setIsAddingFriend(true);
|
||||
@ -56,21 +56,25 @@ export const UserFriendModalAddFriend = ({
|
||||
navigate(`/user/${friendCode}`);
|
||||
};
|
||||
|
||||
const handleClickCancelFriendRequest = (userId: string) => {
|
||||
const handleCancelFriendRequest = (userId: string) => {
|
||||
updateFriendRequestState(userId, "CANCEL").catch(() => {
|
||||
showErrorToast("Falha ao cancelar convite");
|
||||
showErrorToast(t("try_again"));
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickAcceptFriendRequest = (userId: string) => {
|
||||
updateFriendRequestState(userId, "ACCEPTED").catch(() => {
|
||||
showErrorToast("Falha ao aceitar convite");
|
||||
});
|
||||
const handleAcceptFriendRequest = (userId: string) => {
|
||||
updateFriendRequestState(userId, "ACCEPTED")
|
||||
.then(() => {
|
||||
showSuccessToast(t("request_accepted"));
|
||||
})
|
||||
.catch(() => {
|
||||
showErrorToast(t("try_again"));
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickRefuseFriendRequest = (userId: string) => {
|
||||
const handleRefuseFriendRequest = (userId: string) => {
|
||||
updateFriendRequestState(userId, "REFUSED").catch(() => {
|
||||
showErrorToast("Falha ao recusar convite");
|
||||
showErrorToast(t("try_again"));
|
||||
});
|
||||
};
|
||||
|
||||
@ -121,16 +125,16 @@ export const UserFriendModalAddFriend = ({
|
||||
<h3>Pendentes</h3>
|
||||
{friendRequests.map((request) => {
|
||||
return (
|
||||
<UserFriendRequest
|
||||
<UserFriendItem
|
||||
key={request.id}
|
||||
displayName={request.displayName}
|
||||
isRequestSent={request.type === "SENT"}
|
||||
type={request.type}
|
||||
profileImageUrl={request.profileImageUrl}
|
||||
userId={request.id}
|
||||
onClickAcceptRequest={handleClickAcceptFriendRequest}
|
||||
onClickCancelRequest={handleClickCancelFriendRequest}
|
||||
onClickRefuseRequest={handleClickRefuseFriendRequest}
|
||||
onClickRequest={handleClickRequest}
|
||||
onClickAcceptRequest={handleAcceptFriendRequest}
|
||||
onClickCancelRequest={handleCancelFriendRequest}
|
||||
onClickRefuseRequest={handleRefuseFriendRequest}
|
||||
onClickItem={handleClickRequest}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -0,0 +1,95 @@
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { UserFriend } from "@types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { UserFriendItem } from "./user-friend-item";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface UserFriendModalListProps {
|
||||
userId: string;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const pageSize = 12;
|
||||
|
||||
export const UserFriendModalList = ({
|
||||
userId,
|
||||
closeModal,
|
||||
}: UserFriendModalListProps) => {
|
||||
const { t } = useTranslation("user_profile");
|
||||
const { showErrorToast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [maxPage, setMaxPage] = useState(0);
|
||||
const [friends, setFriends] = useState<UserFriend[]>([]);
|
||||
|
||||
const { userDetails, undoFriendship } = useUserDetails();
|
||||
const isMe = userDetails?.id == userId;
|
||||
|
||||
const loadNextPage = () => {
|
||||
if (page > maxPage) return;
|
||||
window.electron
|
||||
.getUserFriends(userId, pageSize, page * pageSize)
|
||||
.then((newPage) => {
|
||||
if (page === 0) {
|
||||
setMaxPage(newPage.totalFriends / pageSize);
|
||||
}
|
||||
|
||||
setFriends([...friends, ...newPage.friends]);
|
||||
setPage(page + 1);
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const reloadList = () => {
|
||||
setPage(0);
|
||||
setMaxPage(0);
|
||||
setFriends([]);
|
||||
loadNextPage();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reloadList();
|
||||
}, [userId]);
|
||||
|
||||
const handleClickFriend = (userId: string) => {
|
||||
closeModal();
|
||||
navigate(`/user/${userId}`);
|
||||
};
|
||||
|
||||
const handleUndoFriendship = (userId: string) => {
|
||||
undoFriendship(userId)
|
||||
.then(() => {
|
||||
reloadList();
|
||||
})
|
||||
.catch(() => {
|
||||
showErrorToast(t("try_again"));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
{friends.map((friend) => {
|
||||
return (
|
||||
<UserFriendItem
|
||||
userId={friend.id}
|
||||
displayName={friend.displayName}
|
||||
profileImageUrl={friend.profileImageUrl}
|
||||
onClickItem={handleClickFriend}
|
||||
onClickUndoFriendship={handleUndoFriendship}
|
||||
type={isMe ? "ACCEPTED" : null}
|
||||
key={friend.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -3,23 +3,27 @@ import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserFriendModalAddFriend } from "./user-friend-modal-add-friend";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
import { UserFriendModalList } from "./user-friend-modal-list";
|
||||
|
||||
export enum UserFriendModalTab {
|
||||
FriendsList,
|
||||
AddFriend,
|
||||
}
|
||||
|
||||
export interface UserAddFriendsModalProps {
|
||||
export interface UserFriendsModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
initialTab: UserFriendModalTab | null;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export const UserFriendModal = ({
|
||||
visible,
|
||||
onClose,
|
||||
initialTab,
|
||||
}: UserAddFriendsModalProps) => {
|
||||
userId,
|
||||
}: UserFriendsModalProps) => {
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
const tabs = [t("friends_list"), t("add_friends")];
|
||||
@ -28,6 +32,9 @@ export const UserFriendModal = ({
|
||||
initialTab || UserFriendModalTab.FriendsList
|
||||
);
|
||||
|
||||
const { userDetails } = useUserDetails();
|
||||
const isMe = userDetails?.id == userId;
|
||||
|
||||
useEffect(() => {
|
||||
if (initialTab != null) {
|
||||
setCurrentTab(initialTab);
|
||||
@ -36,7 +43,7 @@ export const UserFriendModal = ({
|
||||
|
||||
const renderTab = () => {
|
||||
if (currentTab == UserFriendModalTab.FriendsList) {
|
||||
return <></>;
|
||||
return <UserFriendModalList userId={userId} closeModal={onClose} />;
|
||||
}
|
||||
|
||||
if (currentTab == UserFriendModalTab.AddFriend) {
|
||||
@ -56,20 +63,21 @@ export const UserFriendModal = ({
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||
{tabs.map((tab, index) => {
|
||||
return (
|
||||
<Button
|
||||
key={tab}
|
||||
theme={index === currentTab ? "primary" : "outline"}
|
||||
onClick={() => setCurrentTab(index)}
|
||||
>
|
||||
{tab}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
<h2>{tabs[currentTab]}</h2>
|
||||
{isMe && (
|
||||
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||
{tabs.map((tab, index) => {
|
||||
return (
|
||||
<Button
|
||||
key={tab}
|
||||
theme={index === currentTab ? "primary" : "outline"}
|
||||
onClick={() => setCurrentTab(index)}
|
||||
>
|
||||
{tab}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
)}
|
||||
{renderTab()}
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -1,97 +0,0 @@
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
PersonIcon,
|
||||
XCircleIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import * as styles from "./user-friend-modal.css";
|
||||
import cn from "classnames";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
export interface UserFriendRequestProps {
|
||||
userId: string;
|
||||
profileImageUrl: string | null;
|
||||
displayName: string;
|
||||
isRequestSent: boolean;
|
||||
onClickCancelRequest: (userId: string) => void;
|
||||
onClickAcceptRequest: (userId: string) => void;
|
||||
onClickRefuseRequest: (userId: string) => void;
|
||||
onClickRequest: (userId: string) => void;
|
||||
}
|
||||
|
||||
export const UserFriendRequest = ({
|
||||
userId,
|
||||
profileImageUrl,
|
||||
displayName,
|
||||
isRequestSent,
|
||||
onClickCancelRequest,
|
||||
onClickAcceptRequest,
|
||||
onClickRefuseRequest,
|
||||
onClickRequest,
|
||||
}: UserFriendRequestProps) => {
|
||||
return (
|
||||
<div className={cn(styles.friendListContainer, styles.profileContentBox)}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.friendListButton}
|
||||
onClick={() => onClickRequest(userId)}
|
||||
>
|
||||
<div className={styles.friendAvatarContainer}>
|
||||
{profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={displayName}
|
||||
src={profileImageUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={24} />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
flex: "1",
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<p className={styles.friendListDisplayName}>{displayName}</p>
|
||||
<small>{isRequestSent ? "Pedido enviado" : "Pedido recebido"}</small>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: "8px",
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
{isRequestSent ? (
|
||||
<button
|
||||
className={styles.cancelRequestButton}
|
||||
onClick={() => onClickCancelRequest(userId)}
|
||||
>
|
||||
<XCircleIcon size={28} />
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className={styles.acceptRequestButton}
|
||||
onClick={() => onClickAcceptRequest(userId)}
|
||||
>
|
||||
<CheckCircleIcon size={28} />
|
||||
</button>
|
||||
<button
|
||||
className={styles.cancelRequestButton}
|
||||
onClick={() => onClickRefuseRequest(userId)}
|
||||
>
|
||||
<XCircleIcon size={28} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
44
src/renderer/src/pages/user/user-block-modal.tsx
Normal file
44
src/renderer/src/pages/user/user-block-modal.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
import * as styles from "./user.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface UserBlockModalProps {
|
||||
visible: boolean;
|
||||
displayName: string;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const UserBlockModal = ({
|
||||
visible,
|
||||
displayName,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: UserBlockModalProps) => {
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("sign_out_modal_title")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles.signOutModalContent}>
|
||||
<p style={{ fontFamily: "Fira Sans" }}>
|
||||
{t("user_block_modal_text", { displayName })}
|
||||
</p>
|
||||
<div className={styles.signOutModalButtonsContainer}>
|
||||
<Button onClick={onConfirm} theme="danger">
|
||||
{t("block_user")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onClose} theme="primary">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { UserGame, UserProfile } from "@types";
|
||||
import { FriendRequestAction, UserGame, UserProfile } from "@types";
|
||||
import cn from "classnames";
|
||||
import * as styles from "./user.css";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
@ -12,12 +12,23 @@ import {
|
||||
useUserDetails,
|
||||
} from "@renderer/hooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers";
|
||||
import { PersonIcon, PlusIcon, TelescopeIcon } from "@primer/octicons-react";
|
||||
import {
|
||||
buildGameDetailsPath,
|
||||
profileBackgroundFromProfileImage,
|
||||
steamUrlBuilder,
|
||||
} from "@renderer/helpers";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
PersonIcon,
|
||||
PlusIcon,
|
||||
TelescopeIcon,
|
||||
XCircleIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { Button, Link } from "@renderer/components";
|
||||
import { UserEditProfileModal } from "./user-edit-modal";
|
||||
import { UserSignOutModal } from "./user-signout-modal";
|
||||
import { UserSignOutModal } from "./user-sign-out-modal";
|
||||
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
|
||||
import { UserBlockModal } from "./user-block-modal";
|
||||
|
||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||
|
||||
@ -26,6 +37,8 @@ export interface ProfileContentProps {
|
||||
updateUserProfile: () => Promise<void>;
|
||||
}
|
||||
|
||||
type FriendAction = FriendRequestAction | ("BLOCK" | "UNDO" | "SEND");
|
||||
|
||||
export function UserContent({
|
||||
userProfile,
|
||||
updateUserProfile,
|
||||
@ -36,13 +49,20 @@ export function UserContent({
|
||||
userDetails,
|
||||
profileBackground,
|
||||
signOut,
|
||||
updateFriendRequests,
|
||||
sendFriendRequest,
|
||||
fetchFriendRequests,
|
||||
showFriendsModal,
|
||||
updateFriendRequestState,
|
||||
undoFriendship,
|
||||
blockUser,
|
||||
} = useUserDetails();
|
||||
const { showSuccessToast } = useToast();
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
const [profileContentBoxBackground, setProfileContentBoxBackground] =
|
||||
useState<string | undefined>();
|
||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
||||
const [showUserBlockModal, setShowUserBlockModal] = useState(false);
|
||||
|
||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||
|
||||
@ -93,14 +113,141 @@ export function UserContent({
|
||||
const isMe = userDetails?.id == userProfile.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (isMe) updateFriendRequests();
|
||||
if (isMe) fetchFriendRequests();
|
||||
}, [isMe]);
|
||||
|
||||
const profileContentBoxBackground = useMemo(() => {
|
||||
if (profileBackground) return profileBackground;
|
||||
/* TODO: Render background colors for other users */
|
||||
return undefined;
|
||||
}, [profileBackground]);
|
||||
useEffect(() => {
|
||||
if (isMe && profileBackground) {
|
||||
setProfileContentBoxBackground(profileBackground);
|
||||
}
|
||||
|
||||
if (userProfile.profileImageUrl) {
|
||||
profileBackgroundFromProfileImage(userProfile.profileImageUrl).then(
|
||||
(profileBackground) => {
|
||||
setProfileContentBoxBackground(profileBackground);
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [profileBackground, isMe]);
|
||||
|
||||
const handleFriendAction = (userId: string, action: FriendAction) => {
|
||||
try {
|
||||
if (action === "UNDO") {
|
||||
undoFriendship(userId).then(updateUserProfile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "BLOCK") {
|
||||
blockUser(userId).then(() => {
|
||||
setShowUserBlockModal(false);
|
||||
showSuccessToast(t("user_blocked_successfully"));
|
||||
navigate(-1);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "SEND") {
|
||||
sendFriendRequest(userProfile.id).then(updateUserProfile);
|
||||
return;
|
||||
}
|
||||
|
||||
updateFriendRequestState(userId, action).then(updateUserProfile);
|
||||
} catch (err) {
|
||||
showErrorToast(t("try_again"));
|
||||
}
|
||||
};
|
||||
|
||||
const showFriends = isMe || userProfile.totalFriends > 0;
|
||||
|
||||
const getProfileActions = () => {
|
||||
if (isMe) {
|
||||
return (
|
||||
<>
|
||||
<Button theme="outline" onClick={handleEditProfile}>
|
||||
{t("edit_profile")}
|
||||
</Button>
|
||||
|
||||
<Button theme="danger" onClick={() => setShowSignOutModal(true)}>
|
||||
{t("sign_out")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (userProfile.relation == null) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => handleFriendAction(userProfile.id, "SEND")}
|
||||
>
|
||||
{t("add_friend")}
|
||||
</Button>
|
||||
|
||||
<Button theme="danger" onClick={() => setShowUserBlockModal(true)}>
|
||||
{t("block_user")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (userProfile.relation.status === "ACCEPTED") {
|
||||
const userId =
|
||||
userProfile.relation.AId === userDetails?.id
|
||||
? userProfile.relation.BId
|
||||
: userProfile.relation.AId;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
theme="outline"
|
||||
className={styles.cancelRequestButton}
|
||||
onClick={() => handleFriendAction(userId, "UNDO")}
|
||||
>
|
||||
<XCircleIcon size={28} /> {t("undo_friendship")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (userProfile.relation.BId === userProfile.id) {
|
||||
return (
|
||||
<Button
|
||||
theme="outline"
|
||||
className={styles.cancelRequestButton}
|
||||
onClick={() =>
|
||||
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
||||
}
|
||||
>
|
||||
<XCircleIcon size={28} /> {t("cancel_request")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
theme="outline"
|
||||
className={styles.acceptRequestButton}
|
||||
onClick={() =>
|
||||
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
||||
}
|
||||
>
|
||||
<CheckCircleIcon size={28} /> {t("accept_request")}
|
||||
</Button>
|
||||
<Button
|
||||
theme="outline"
|
||||
className={styles.cancelRequestButton}
|
||||
onClick={() =>
|
||||
handleFriendAction(userProfile.relation!.AId, "REFUSED")
|
||||
}
|
||||
>
|
||||
<XCircleIcon size={28} /> {t("ignore_request")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -117,6 +264,13 @@ export function UserContent({
|
||||
onConfirm={handleConfirmSignout}
|
||||
/>
|
||||
|
||||
<UserBlockModal
|
||||
visible={showUserBlockModal}
|
||||
onClose={() => setShowUserBlockModal(false)}
|
||||
onConfirm={() => handleFriendAction(userProfile.id, "BLOCK")}
|
||||
displayName={userProfile.displayName}
|
||||
/>
|
||||
|
||||
<section
|
||||
className={styles.profileContentBox}
|
||||
style={{
|
||||
@ -187,37 +341,24 @@ export function UserContent({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isMe && (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
justifyContent: "end",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
justifyContent: "end",
|
||||
zIndex: 1,
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Button theme="outline" onClick={handleEditProfile}>
|
||||
{t("edit_profile")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
theme="danger"
|
||||
onClick={() => setShowSignOutModal(true)}
|
||||
>
|
||||
{t("sign_out")}
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
{getProfileActions()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className={styles.profileContent}>
|
||||
@ -327,12 +468,16 @@ export function UserContent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(isMe ||
|
||||
(userProfile.friends && userProfile.friends.length > 0)) && (
|
||||
{showFriends && (
|
||||
<div className={styles.friendsSection}>
|
||||
<button
|
||||
className={styles.friendsSectionHeader}
|
||||
onClick={() => showFriendsModal(UserFriendModalTab.FriendsList)}
|
||||
onClick={() =>
|
||||
showFriendsModal(
|
||||
UserFriendModalTab.FriendsList,
|
||||
userProfile.id
|
||||
)
|
||||
}
|
||||
>
|
||||
<h2>{t("friends")}</h2>
|
||||
|
||||
@ -344,7 +489,7 @@ export function UserContent({
|
||||
}}
|
||||
/>
|
||||
<h3 style={{ fontWeight: "400" }}>
|
||||
{userProfile.friends.length}
|
||||
{userProfile.totalFriends}
|
||||
</h3>
|
||||
</button>
|
||||
|
||||
@ -388,7 +533,10 @@ export function UserContent({
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() =>
|
||||
showFriendsModal(UserFriendModalTab.AddFriend)
|
||||
showFriendsModal(
|
||||
UserFriendModalTab.AddFriend,
|
||||
userProfile.id
|
||||
)
|
||||
}
|
||||
>
|
||||
<PlusIcon /> {t("add")}
|
||||
|
@ -2,7 +2,7 @@ import { Button, Modal } from "@renderer/components";
|
||||
import * as styles from "./user.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface UserEditProfileModalProps {
|
||||
export interface UserSignOutModalProps {
|
||||
visible: boolean;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
@ -12,7 +12,7 @@ export const UserSignOutModal = ({
|
||||
visible,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: UserEditProfileModalProps) => {
|
||||
}: UserSignOutModalProps) => {
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
return (
|
@ -78,6 +78,8 @@ export const profileAvatar = style({
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
borderRadius: "50%",
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const profileAvatarEditOverlay = style({
|
||||
@ -277,3 +279,16 @@ export const profileBackground = style({
|
||||
top: "0",
|
||||
borderRadius: "4px",
|
||||
});
|
||||
|
||||
export const cancelRequestButton = style({
|
||||
cursor: "pointer",
|
||||
color: vars.color.body,
|
||||
":hover": {
|
||||
color: vars.color.danger,
|
||||
},
|
||||
});
|
||||
|
||||
export const acceptRequestButton = style({
|
||||
cursor: "pointer",
|
||||
color: vars.color.success,
|
||||
});
|
||||
|
@ -277,6 +277,11 @@ export interface UserFriend {
|
||||
profileImageUrl: string | null;
|
||||
}
|
||||
|
||||
export interface UserFriends {
|
||||
totalFriends: number;
|
||||
friends: UserFriend[];
|
||||
}
|
||||
|
||||
export interface FriendRequest {
|
||||
id: string;
|
||||
displayName: string;
|
||||
@ -284,14 +289,25 @@ export interface FriendRequest {
|
||||
type: "SENT" | "RECEIVED";
|
||||
}
|
||||
|
||||
export interface UserRelation {
|
||||
AId: string;
|
||||
BId: string;
|
||||
status: "ACCEPTED" | "PENDING";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
displayName: string;
|
||||
profileImageUrl: string | null;
|
||||
profileVisibility: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||
totalPlayTimeInSeconds: number;
|
||||
libraryGames: UserGame[];
|
||||
recentGames: UserGame[];
|
||||
friends: UserFriend[];
|
||||
totalFriends: number;
|
||||
relation: UserRelation | null;
|
||||
}
|
||||
|
||||
export interface DownloadSource {
|
||||
|
Loading…
Reference in New Issue
Block a user