mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
Merge branch 'fix/http-downloads-duplicate' of github.com:hydralauncher/hydra into fix/http-downloads-duplicate
This commit is contained in:
commit
c218070463
@ -261,6 +261,18 @@
|
|||||||
"undo_friendship": "Undo friendship",
|
"undo_friendship": "Undo friendship",
|
||||||
"request_accepted": "Request accepted",
|
"request_accepted": "Request accepted",
|
||||||
"user_blocked_successfully": "User blocked successfully",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -261,6 +261,18 @@
|
|||||||
"undo_friendship": "Desfazer amizade",
|
"undo_friendship": "Desfazer amizade",
|
||||||
"request_accepted": "Pedido de amizade aceito",
|
"request_accepted": "Pedido de amizade aceito",
|
||||||
"user_blocked_successfully": "Usuário bloqueado com sucesso",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,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/block-user";
|
import "./user/block-user";
|
||||||
import "./user/unblock-user";
|
import "./user/unblock-user";
|
||||||
import "./user/get-user-friends";
|
import "./user/get-user-friends";
|
||||||
@ -52,11 +53,9 @@ import "./profile/undo-friendship";
|
|||||||
import "./profile/update-friend-request";
|
import "./profile/update-friend-request";
|
||||||
import "./profile/update-profile";
|
import "./profile/update-profile";
|
||||||
import "./profile/send-friend-request";
|
import "./profile/send-friend-request";
|
||||||
|
import { isPortableVersion } from "@main/helpers";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
ipcMain.handle("getVersion", () => app.getVersion());
|
ipcMain.handle("getVersion", () => app.getVersion());
|
||||||
ipcMain.handle(
|
ipcMain.handle("isPortableVersion", () => isPortableVersion());
|
||||||
"isPortableVersion",
|
|
||||||
() => process.env.PORTABLE_EXECUTABLE_FILE != null
|
|
||||||
);
|
|
||||||
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
|
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
|
||||||
|
@ -4,33 +4,22 @@ import axios from "axios";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileTypeFromFile } from "file-type";
|
import { fileTypeFromFile } from "file-type";
|
||||||
import { UserProfile } from "@types";
|
import { UpdateProfileProps, UserProfile } from "@types";
|
||||||
|
|
||||||
const patchUserProfile = async (
|
const patchUserProfile = async (updateProfile: UpdateProfileProps) => {
|
||||||
displayName: string,
|
return HydraApi.patch("/profile", updateProfile);
|
||||||
profileImageUrl?: string
|
|
||||||
) => {
|
|
||||||
if (profileImageUrl) {
|
|
||||||
return HydraApi.patch("/profile", {
|
|
||||||
displayName,
|
|
||||||
profileImageUrl,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return HydraApi.patch("/profile", {
|
|
||||||
displayName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateProfile = async (
|
const updateProfile = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
displayName: string,
|
updateProfile: UpdateProfileProps
|
||||||
newProfileImagePath: string | null
|
|
||||||
): Promise<UserProfile> => {
|
): Promise<UserProfile> => {
|
||||||
if (!newProfileImagePath) {
|
if (!updateProfile.profileImageUrl) {
|
||||||
return patchUserProfile(displayName);
|
return patchUserProfile(updateProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newProfileImagePath = updateProfile.profileImageUrl;
|
||||||
|
|
||||||
const stats = fs.statSync(newProfileImagePath);
|
const stats = fs.statSync(newProfileImagePath);
|
||||||
const fileBuffer = fs.readFileSync(newProfileImagePath);
|
const fileBuffer = fs.readFileSync(newProfileImagePath);
|
||||||
const fileSizeInBytes = stats.size;
|
const fileSizeInBytes = stats.size;
|
||||||
@ -53,7 +42,7 @@ const updateProfile = async (
|
|||||||
})
|
})
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
|
|
||||||
return patchUserProfile(displayName, profileImageUrl);
|
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("updateProfile", updateProfile);
|
registerEvent("updateProfile", updateProfile);
|
||||||
|
13
src/main/events/user/get-user-blocks.ts
Normal file
13
src/main/events/user/get-user-blocks.ts
Normal file
@ -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<UserBlocks> => {
|
||||||
|
return HydraApi.get(`/profile/blocks`, { take, skip });
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getUserBlocks", getUserBlocks);
|
@ -57,4 +57,7 @@ export const requestWebPage = async (url: string) => {
|
|||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isPortableVersion = () =>
|
||||||
|
process.env.PORTABLE_EXECUTABLE_FILE != null;
|
||||||
|
|
||||||
export * from "./download-source";
|
export * from "./download-source";
|
||||||
|
@ -10,6 +10,7 @@ import type {
|
|||||||
StartGameDownloadPayload,
|
StartGameDownloadPayload,
|
||||||
GameRunning,
|
GameRunning,
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
|
UpdateProfileProps,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
@ -137,8 +138,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
getMe: () => ipcRenderer.invoke("getMe"),
|
getMe: () => ipcRenderer.invoke("getMe"),
|
||||||
undoFriendship: (userId: string) =>
|
undoFriendship: (userId: string) =>
|
||||||
ipcRenderer.invoke("undoFriendship", userId),
|
ipcRenderer.invoke("undoFriendship", userId),
|
||||||
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
|
updateProfile: (updateProfile: UpdateProfileProps) =>
|
||||||
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
|
ipcRenderer.invoke("updateProfile", updateProfile),
|
||||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||||
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
||||||
@ -151,6 +152,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) =>
|
||||||
|
ipcRenderer.invoke("getUserBlocks", take, skip),
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
signOut: () => ipcRenderer.invoke("signOut"),
|
signOut: () => ipcRenderer.invoke("signOut"),
|
||||||
|
@ -108,7 +108,7 @@ export function App() {
|
|||||||
fetchFriendRequests();
|
fetchFriendRequests();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [fetchUserDetails, updateUserDetails, dispatch]);
|
}, [fetchUserDetails, fetchFriendRequests, updateUserDetails, dispatch]);
|
||||||
|
|
||||||
const onSignIn = useCallback(() => {
|
const onSignIn = useCallback(() => {
|
||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
@ -118,7 +118,13 @@ export function App() {
|
|||||||
showSuccessToast(t("successfully_signed_in"));
|
showSuccessToast(t("successfully_signed_in"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [fetchUserDetails, t, showSuccessToast, updateUserDetails]);
|
}, [
|
||||||
|
fetchUserDetails,
|
||||||
|
fetchFriendRequests,
|
||||||
|
t,
|
||||||
|
showSuccessToast,
|
||||||
|
updateUserDetails,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
|
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
|
||||||
|
7
src/renderer/src/declaration.d.ts
vendored
7
src/renderer/src/declaration.d.ts
vendored
@ -17,6 +17,7 @@ import type {
|
|||||||
FriendRequest,
|
FriendRequest,
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
UserFriends,
|
UserFriends,
|
||||||
|
UserBlocks,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
@ -135,14 +136,12 @@ declare global {
|
|||||||
take: number,
|
take: number,
|
||||||
skip: number
|
skip: number
|
||||||
) => Promise<UserFriends>;
|
) => Promise<UserFriends>;
|
||||||
|
getUserBlocks: (take: number, skip: number) => Promise<UserBlocks>;
|
||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => Promise<UserProfile | null>;
|
getMe: () => Promise<UserProfile | null>;
|
||||||
undoFriendship: (userId: string) => Promise<void>;
|
undoFriendship: (userId: string) => Promise<void>;
|
||||||
updateProfile: (
|
updateProfile: (updateProfile: UpdateProfileProps) => Promise<UserProfile>;
|
||||||
displayName: string,
|
|
||||||
newProfileImagePath: string | null
|
|
||||||
) => Promise<UserProfile>;
|
|
||||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||||
updateFriendRequest: (
|
updateFriendRequest: (
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -8,8 +8,9 @@ import {
|
|||||||
setFriendsModalHidden,
|
setFriendsModalHidden,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import { profileBackgroundFromProfileImage } from "@renderer/helpers";
|
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 { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
import { logger } from "@renderer/logger";
|
||||||
|
|
||||||
export function useUserDetails() {
|
export function useUserDetails() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -43,7 +44,10 @@ export function useUserDetails() {
|
|||||||
if (userDetails.profileImageUrl) {
|
if (userDetails.profileImageUrl) {
|
||||||
const profileBackground = await profileBackgroundFromProfileImage(
|
const profileBackground = await profileBackgroundFromProfileImage(
|
||||||
userDetails.profileImageUrl
|
userDetails.profileImageUrl
|
||||||
);
|
).catch((err) => {
|
||||||
|
logger.error("profileBackgroundFromProfileImage", err);
|
||||||
|
return `#151515B3`;
|
||||||
|
});
|
||||||
dispatch(setProfileBackground(profileBackground));
|
dispatch(setProfileBackground(profileBackground));
|
||||||
|
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
@ -74,12 +78,8 @@ export function useUserDetails() {
|
|||||||
}, [clearUserDetails]);
|
}, [clearUserDetails]);
|
||||||
|
|
||||||
const patchUser = useCallback(
|
const patchUser = useCallback(
|
||||||
async (displayName: string, imageProfileUrl: string | null) => {
|
async (props: UpdateProfileProps) => {
|
||||||
const response = await window.electron.updateProfile(
|
const response = await window.electron.updateProfile(props);
|
||||||
displayName,
|
|
||||||
imageProfileUrl
|
|
||||||
);
|
|
||||||
|
|
||||||
return updateUserDetails(response);
|
return updateUserDetails(response);
|
||||||
},
|
},
|
||||||
[updateUserDetails]
|
[updateUserDetails]
|
||||||
@ -99,7 +99,7 @@ export function useUserDetails() {
|
|||||||
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
||||||
fetchFriendRequests();
|
fetchFriendRequests();
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch, fetchFriendRequests]
|
||||||
);
|
);
|
||||||
|
|
||||||
const hideFriendsModal = useCallback(() => {
|
const hideFriendsModal = useCallback(() => {
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import * as styles from "./user-friend-modal.css";
|
import * as styles from "./user-friend-modal.css";
|
||||||
import cn from "classnames";
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -12,21 +11,26 @@ export type UserFriendItemProps = {
|
|||||||
userId: string;
|
userId: string;
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
displayName: string;
|
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";
|
type: "SENT" | "RECEIVED";
|
||||||
onClickCancelRequest: (userId: string) => void;
|
onClickCancelRequest: (userId: string) => void;
|
||||||
onClickAcceptRequest: (userId: string) => void;
|
onClickAcceptRequest: (userId: string) => void;
|
||||||
onClickRefuseRequest: (userId: string) => void;
|
onClickRefuseRequest: (userId: string) => void;
|
||||||
|
onClickItem: (userId: string) => void;
|
||||||
}
|
}
|
||||||
| { type: null }
|
| { type: null; onClickItem: (userId: string) => void }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UserFriendItem = (props: UserFriendItemProps) => {
|
export const UserFriendItem = (props: UserFriendItemProps) => {
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
const { userId, profileImageUrl, displayName, type, onClickItem } = props;
|
const { userId, profileImageUrl, displayName, type } = props;
|
||||||
|
|
||||||
const getRequestDescription = () => {
|
const getRequestDescription = () => {
|
||||||
if (type === "ACCEPTED" || type === null) return null;
|
if (type === "ACCEPTED" || type === null) return null;
|
||||||
@ -86,15 +90,69 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "BLOCKED") {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={styles.cancelRequestButton}
|
||||||
|
onClick={() => props.onClickUnblock(userId)}
|
||||||
|
title={t("unblock")}
|
||||||
|
>
|
||||||
|
<XCircleIcon size={28} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (type === "BLOCKED") {
|
||||||
|
return (
|
||||||
|
<div className={styles.friendListContainer}>
|
||||||
|
<div className={styles.friendListButton} style={{ cursor: "inherit" }}>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "8px",
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getRequestActions()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(styles.friendListContainer, styles.profileContentBox)}>
|
<div className={styles.friendListContainer}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.friendListButton}
|
className={styles.friendListButton}
|
||||||
onClick={() => onClickItem(userId)}
|
onClick={() => props.onClickItem(userId)}
|
||||||
>
|
>
|
||||||
<div className={styles.friendAvatarContainer}>
|
<div className={styles.friendAvatarContainer}>
|
||||||
{profileImageUrl ? (
|
{profileImageUrl ? (
|
||||||
|
@ -40,20 +40,16 @@ export const UserFriendModalAddFriend = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetAndClose = () => {
|
|
||||||
setFriendCode("");
|
|
||||||
closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickRequest = (userId: string) => {
|
const handleClickRequest = (userId: string) => {
|
||||||
resetAndClose();
|
closeModal();
|
||||||
navigate(`/user/${userId}`);
|
navigate(`/user/${userId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickSeeProfile = () => {
|
const handleClickSeeProfile = () => {
|
||||||
resetAndClose();
|
closeModal();
|
||||||
// TODO: add validation for this input?
|
if (friendCode.length === 8) {
|
||||||
navigate(`/user/${friendCode}`);
|
navigate(`/user/${friendCode}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelFriendRequest = (userId: string) => {
|
const handleCancelFriendRequest = (userId: string) => {
|
||||||
@ -122,7 +118,8 @@ export const UserFriendModalAddFriend = ({
|
|||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3>Pendentes</h3>
|
<h3>{t("pending")}</h3>
|
||||||
|
{friendRequests.length === 0 && <p>{t("no_pending_invites")}</p>}
|
||||||
{friendRequests.map((request) => {
|
{friendRequests.map((request) => {
|
||||||
return (
|
return (
|
||||||
<UserFriendItem
|
<UserFriendItem
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
import { UserFriend } from "@types";
|
import { UserFriend } from "@types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { UserFriendItem } from "./user-friend-item";
|
import { UserFriendItem } from "./user-friend-item";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||||
|
|
||||||
export interface UserFriendModalListProps {
|
export interface UserFriendModalListProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -22,14 +23,17 @@ export const UserFriendModalList = ({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [maxPage, setMaxPage] = useState(0);
|
const [maxPage, setMaxPage] = useState(0);
|
||||||
const [friends, setFriends] = useState<UserFriend[]>([]);
|
const [friends, setFriends] = useState<UserFriend[]>([]);
|
||||||
|
const listContainer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { userDetails, undoFriendship } = useUserDetails();
|
const { userDetails, undoFriendship } = useUserDetails();
|
||||||
const isMe = userDetails?.id == userId;
|
const isMe = userDetails?.id == userId;
|
||||||
|
|
||||||
const loadNextPage = () => {
|
const loadNextPage = () => {
|
||||||
if (page > maxPage) return;
|
if (page > maxPage) return;
|
||||||
|
setIsLoading(true);
|
||||||
window.electron
|
window.electron
|
||||||
.getUserFriends(userId, pageSize, page * pageSize)
|
.getUserFriends(userId, pageSize, page * pageSize)
|
||||||
.then((newPage) => {
|
.then((newPage) => {
|
||||||
@ -40,9 +44,29 @@ export const UserFriendModalList = ({
|
|||||||
setFriends([...friends, ...newPage.friends]);
|
setFriends([...friends, ...newPage.friends]);
|
||||||
setPage(page + 1);
|
setPage(page + 1);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.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 = () => {
|
const reloadList = () => {
|
||||||
setPage(0);
|
setPage(0);
|
||||||
setMaxPage(0);
|
setMaxPage(0);
|
||||||
@ -70,26 +94,42 @@ export const UserFriendModalList = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
style={{
|
<div
|
||||||
display: "flex",
|
ref={listContainer}
|
||||||
flexDirection: "column",
|
style={{
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
display: "flex",
|
||||||
}}
|
flexDirection: "column",
|
||||||
>
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
{friends.map((friend) => {
|
maxHeight: "400px",
|
||||||
return (
|
overflowY: "scroll",
|
||||||
<UserFriendItem
|
}}
|
||||||
userId={friend.id}
|
>
|
||||||
displayName={friend.displayName}
|
{!isLoading && friends.length === 0 && <p>{t("no_friends_added")}</p>}
|
||||||
profileImageUrl={friend.profileImageUrl}
|
{friends.map((friend) => {
|
||||||
onClickItem={handleClickFriend}
|
return (
|
||||||
onClickUndoFriendship={handleUndoFriendship}
|
<UserFriendItem
|
||||||
type={isMe ? "ACCEPTED" : null}
|
userId={friend.id}
|
||||||
key={friend.id}
|
displayName={friend.displayName}
|
||||||
|
profileImageUrl={friend.profileImageUrl}
|
||||||
|
onClickItem={handleClickFriend}
|
||||||
|
onClickUndoFriendship={handleUndoFriendship}
|
||||||
|
type={isMe ? "ACCEPTED" : null}
|
||||||
|
key={"modal" + friend.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{isLoading && (
|
||||||
|
<Skeleton
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "54px",
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</SkeletonTheme>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,6 @@
|
|||||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
export const profileContentBox = style({
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
width: "100%",
|
|
||||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.7)",
|
|
||||||
transition: "all ease 0.3s",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const friendAvatarContainer = style({
|
export const friendAvatarContainer = style({
|
||||||
width: "35px",
|
width: "35px",
|
||||||
minWidth: "35px",
|
minWidth: "35px",
|
||||||
@ -42,8 +31,14 @@ export const profileAvatar = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const friendListContainer = style({
|
export const friendListContainer = style({
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "4px",
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "54px",
|
height: "54px",
|
||||||
|
minHeight: "54px",
|
||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
":hover": {
|
":hover": {
|
||||||
@ -90,3 +85,15 @@ export const cancelRequestButton = style({
|
|||||||
color: vars.color.danger,
|
color: vars.color.danger,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const friendCodeButton = style({
|
||||||
|
color: vars.color.body,
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT / 2}px`,
|
||||||
|
alignItems: "center",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
":hover": {
|
||||||
|
color: vars.color.muted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { Button, Modal } from "@renderer/components";
|
import { Button, Modal } from "@renderer/components";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserFriendModalAddFriend } from "./user-friend-modal-add-friend";
|
import { UserFriendModalAddFriend } from "./user-friend-modal-add-friend";
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { UserFriendModalList } from "./user-friend-modal-list";
|
import { UserFriendModalList } from "./user-friend-modal-list";
|
||||||
|
import { CopyIcon } from "@primer/octicons-react";
|
||||||
|
import * as styles from "./user-friend-modal.css";
|
||||||
|
|
||||||
export enum UserFriendModalTab {
|
export enum UserFriendModalTab {
|
||||||
FriendsList,
|
FriendsList,
|
||||||
@ -32,6 +34,8 @@ export const UserFriendModal = ({
|
|||||||
initialTab || UserFriendModalTab.FriendsList
|
initialTab || UserFriendModalTab.FriendsList
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
const { userDetails } = useUserDetails();
|
const { userDetails } = useUserDetails();
|
||||||
const isMe = userDetails?.id == userId;
|
const isMe = userDetails?.id == userId;
|
||||||
|
|
||||||
@ -53,6 +57,11 @@ export const UserFriendModal = ({
|
|||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = useCallback(() => {
|
||||||
|
navigator.clipboard.writeText(userDetails!.id);
|
||||||
|
showSuccessToast(t("friend_code_copied"));
|
||||||
|
}, [userDetails, showSuccessToast, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={visible} title={t("friends")} onClose={onClose}>
|
<Modal visible={visible} title={t("friends")} onClose={onClose}>
|
||||||
<div
|
<div
|
||||||
@ -64,19 +73,37 @@ export const UserFriendModal = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isMe && (
|
{isMe && (
|
||||||
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
<>
|
||||||
{tabs.map((tab, index) => {
|
<div
|
||||||
return (
|
style={{
|
||||||
<Button
|
display: "flex",
|
||||||
key={tab}
|
gap: `${SPACING_UNIT}px`,
|
||||||
theme={index === currentTab ? "primary" : "outline"}
|
alignItems: "center",
|
||||||
onClick={() => setCurrentTab(index)}
|
}}
|
||||||
>
|
>
|
||||||
{tab}
|
<p>Seu código de amigo: </p>
|
||||||
</Button>
|
<button
|
||||||
);
|
className={styles.friendCodeButton}
|
||||||
})}
|
onClick={copyToClipboard}
|
||||||
</section>
|
>
|
||||||
|
<h3>{userDetails.id}</h3>
|
||||||
|
<CopyIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<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()}
|
{renderTab()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,7 +25,7 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import { Button, Link } from "@renderer/components";
|
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 { UserSignOutModal } from "./user-sign-out-modal";
|
||||||
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
|
||||||
import { UserBlockModal } from "./user-block-modal";
|
import { UserBlockModal } from "./user-block-modal";
|
||||||
@ -60,7 +60,8 @@ export function UserContent({
|
|||||||
|
|
||||||
const [profileContentBoxBackground, setProfileContentBoxBackground] =
|
const [profileContentBoxBackground, setProfileContentBoxBackground] =
|
||||||
useState<string | undefined>();
|
useState<string | undefined>();
|
||||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
const [showProfileSettingsModal, setShowProfileSettingsModal] =
|
||||||
|
useState(false);
|
||||||
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
||||||
const [showUserBlockModal, setShowUserBlockModal] = useState(false);
|
const [showUserBlockModal, setShowUserBlockModal] = useState(false);
|
||||||
|
|
||||||
@ -95,7 +96,7 @@ export function UserContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEditProfile = () => {
|
const handleEditProfile = () => {
|
||||||
setShowEditProfileModal(true);
|
setShowProfileSettingsModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnClickFriend = (userId: string) => {
|
const handleOnClickFriend = (userId: string) => {
|
||||||
@ -114,7 +115,7 @@ export function UserContent({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMe) fetchFriendRequests();
|
if (isMe) fetchFriendRequests();
|
||||||
}, [isMe]);
|
}, [isMe, fetchFriendRequests]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMe && profileBackground) {
|
if (isMe && profileBackground) {
|
||||||
@ -128,7 +129,7 @@ export function UserContent({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [profileBackground, isMe]);
|
}, [profileBackground, isMe, userProfile.profileImageUrl]);
|
||||||
|
|
||||||
const handleFriendAction = (userId: string, action: FriendAction) => {
|
const handleFriendAction = (userId: string, action: FriendAction) => {
|
||||||
try {
|
try {
|
||||||
@ -159,13 +160,18 @@ export function UserContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showFriends = isMe || userProfile.totalFriends > 0;
|
const showFriends = isMe || userProfile.totalFriends > 0;
|
||||||
|
const showProfileContent =
|
||||||
|
isMe ||
|
||||||
|
userProfile.profileVisibility === "PUBLIC" ||
|
||||||
|
(userProfile.relation?.status === "ACCEPTED" &&
|
||||||
|
userProfile.profileVisibility === "FRIENDS");
|
||||||
|
|
||||||
const getProfileActions = () => {
|
const getProfileActions = () => {
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button theme="outline" onClick={handleEditProfile}>
|
<Button theme="outline" onClick={handleEditProfile}>
|
||||||
{t("edit_profile")}
|
{t("settings")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button theme="danger" onClick={() => setShowSignOutModal(true)}>
|
<Button theme="danger" onClick={() => setShowSignOutModal(true)}>
|
||||||
@ -251,9 +257,9 @@ export function UserContent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserEditProfileModal
|
<UserProfileSettingsModal
|
||||||
visible={showEditProfileModal}
|
visible={showProfileSettingsModal}
|
||||||
onClose={() => setShowEditProfileModal(false)}
|
onClose={() => setShowProfileSettingsModal(false)}
|
||||||
updateUserProfile={updateUserProfile}
|
updateUserProfile={updateUserProfile}
|
||||||
userProfile={userProfile}
|
userProfile={userProfile}
|
||||||
/>
|
/>
|
||||||
@ -361,121 +367,69 @@ export function UserContent({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className={styles.profileContent}>
|
{showProfileContent && (
|
||||||
<div className={styles.profileGameSection}>
|
<div className={styles.profileContent}>
|
||||||
<h2>{t("activity")}</h2>
|
|
||||||
|
|
||||||
{!userProfile.recentGames.length ? (
|
|
||||||
<div className={styles.noDownloads}>
|
|
||||||
<div className={styles.telescopeIcon}>
|
|
||||||
<TelescopeIcon size={24} />
|
|
||||||
</div>
|
|
||||||
<h2>{t("no_recent_activity_title")}</h2>
|
|
||||||
{isMe && <p>{t("no_recent_activity_description")}</p>}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{userProfile.recentGames.map((game) => (
|
|
||||||
<button
|
|
||||||
key={game.objectID}
|
|
||||||
className={cn(styles.feedItem, styles.profileContentBox)}
|
|
||||||
onClick={() => handleGameClick(game)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className={styles.feedGameIcon}
|
|
||||||
src={game.cover}
|
|
||||||
alt={game.title}
|
|
||||||
/>
|
|
||||||
<div className={styles.gameInformation}>
|
|
||||||
<h4>{game.title}</h4>
|
|
||||||
<small>
|
|
||||||
{t("last_time_played", {
|
|
||||||
period: formatDistance(
|
|
||||||
game.lastTimePlayed!,
|
|
||||||
new Date(),
|
|
||||||
{
|
|
||||||
addSuffix: true,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.contentSidebar}>
|
|
||||||
<div className={styles.profileGameSection}>
|
<div className={styles.profileGameSection}>
|
||||||
<div
|
<h2>{t("activity")}</h2>
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2>{t("library")}</h2>
|
|
||||||
|
|
||||||
|
{!userProfile.recentGames.length ? (
|
||||||
|
<div className={styles.noDownloads}>
|
||||||
|
<div className={styles.telescopeIcon}>
|
||||||
|
<TelescopeIcon size={24} />
|
||||||
|
</div>
|
||||||
|
<h2>{t("no_recent_activity_title")}</h2>
|
||||||
|
{isMe && <p>{t("no_recent_activity_description")}</p>}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
display: "flex",
|
||||||
backgroundColor: vars.color.border,
|
flexDirection: "column",
|
||||||
height: "1px",
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<h3 style={{ fontWeight: "400" }}>
|
{userProfile.recentGames.map((game) => (
|
||||||
{userProfile.libraryGames.length}
|
<button
|
||||||
</h3>
|
key={game.objectID}
|
||||||
</div>
|
className={cn(styles.feedItem, styles.profileContentBox)}
|
||||||
<small>{t("total_play_time", { amount: formatPlayTime() })}</small>
|
onClick={() => handleGameClick(game)}
|
||||||
<div
|
>
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(4, 1fr)",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{userProfile.libraryGames.map((game) => (
|
|
||||||
<button
|
|
||||||
key={game.objectID}
|
|
||||||
className={cn(styles.gameListItem, styles.profileContentBox)}
|
|
||||||
onClick={() => handleGameClick(game)}
|
|
||||||
title={game.title}
|
|
||||||
>
|
|
||||||
{game.iconUrl ? (
|
|
||||||
<img
|
<img
|
||||||
className={styles.libraryGameIcon}
|
className={styles.feedGameIcon}
|
||||||
src={game.iconUrl}
|
src={game.cover}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
/>
|
/>
|
||||||
) : (
|
<div className={styles.gameInformation}>
|
||||||
<SteamLogo className={styles.libraryGameIcon} />
|
<h4>{game.title}</h4>
|
||||||
)}
|
<small>
|
||||||
</button>
|
{t("last_time_played", {
|
||||||
))}
|
period: formatDistance(
|
||||||
</div>
|
game.lastTimePlayed!,
|
||||||
|
new Date(),
|
||||||
|
{
|
||||||
|
addSuffix: true,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showFriends && (
|
<div className={styles.contentSidebar}>
|
||||||
<div className={styles.friendsSection}>
|
<div className={styles.profileGameSection}>
|
||||||
<button
|
<div
|
||||||
className={styles.friendsSectionHeader}
|
style={{
|
||||||
onClick={() =>
|
display: "flex",
|
||||||
showFriendsModal(
|
alignItems: "center",
|
||||||
UserFriendModalTab.FriendsList,
|
justifyContent: "space-between",
|
||||||
userProfile.id
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
)
|
}}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<h2>{t("friends")}</h2>
|
<h2>{t("library")}</h2>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -485,64 +439,123 @@ export function UserContent({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<h3 style={{ fontWeight: "400" }}>
|
<h3 style={{ fontWeight: "400" }}>
|
||||||
{userProfile.totalFriends}
|
{userProfile.libraryGames.length}
|
||||||
</h3>
|
</h3>
|
||||||
</button>
|
</div>
|
||||||
|
<small>
|
||||||
|
{t("total_play_time", { amount: formatPlayTime() })}
|
||||||
|
</small>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "grid",
|
||||||
flexDirection: "column",
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{userProfile.friends.map((friend) => {
|
{userProfile.libraryGames.map((game) => (
|
||||||
return (
|
<button
|
||||||
<button
|
key={game.objectID}
|
||||||
key={friend.id}
|
className={cn(
|
||||||
className={cn(
|
styles.gameListItem,
|
||||||
styles.profileContentBox,
|
styles.profileContentBox
|
||||||
styles.friendListContainer
|
)}
|
||||||
)}
|
onClick={() => handleGameClick(game)}
|
||||||
onClick={() => handleOnClickFriend(friend.id)}
|
title={game.title}
|
||||||
>
|
|
||||||
<div className={styles.friendAvatarContainer}>
|
|
||||||
{friend.profileImageUrl ? (
|
|
||||||
<img
|
|
||||||
className={styles.friendProfileIcon}
|
|
||||||
src={friend.profileImageUrl}
|
|
||||||
alt={friend.displayName}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PersonIcon size={24} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className={styles.friendListDisplayName}>
|
|
||||||
{friend.displayName}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{isMe && (
|
|
||||||
<Button
|
|
||||||
theme="outline"
|
|
||||||
onClick={() =>
|
|
||||||
showFriendsModal(
|
|
||||||
UserFriendModalTab.AddFriend,
|
|
||||||
userProfile.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<PlusIcon /> {t("add")}
|
{game.iconUrl ? (
|
||||||
</Button>
|
<img
|
||||||
)}
|
className={styles.libraryGameIcon}
|
||||||
|
src={game.iconUrl}
|
||||||
|
alt={game.title}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SteamLogo className={styles.libraryGameIcon} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{showFriends && (
|
||||||
|
<div className={styles.friendsSection}>
|
||||||
|
<button
|
||||||
|
className={styles.friendsSectionHeader}
|
||||||
|
onClick={() =>
|
||||||
|
showFriendsModal(
|
||||||
|
UserFriendModalTab.FriendsList,
|
||||||
|
userProfile.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<h2>{t("friends")}</h2>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: vars.color.border,
|
||||||
|
height: "1px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<h3 style={{ fontWeight: "400" }}>
|
||||||
|
{userProfile.totalFriends}
|
||||||
|
</h3>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{userProfile.friends.map((friend) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={friend.id}
|
||||||
|
className={cn(
|
||||||
|
styles.profileContentBox,
|
||||||
|
styles.friendListContainer
|
||||||
|
)}
|
||||||
|
onClick={() => handleOnClickFriend(friend.id)}
|
||||||
|
>
|
||||||
|
<div className={styles.friendAvatarContainer}>
|
||||||
|
{friend.profileImageUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.friendProfileIcon}
|
||||||
|
src={friend.profileImageUrl}
|
||||||
|
alt={friend.displayName}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={24} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className={styles.friendListDisplayName}>
|
||||||
|
{friend.displayName}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{isMe && (
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
onClick={() =>
|
||||||
|
showFriendsModal(
|
||||||
|
UserFriendModalTab.AddFriend,
|
||||||
|
userProfile.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PlusIcon /> {t("add")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserEditProfileModal = ({
|
|
||||||
userProfile,
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
updateUserProfile,
|
|
||||||
}: UserEditProfileModalProps) => {
|
|
||||||
const { t } = useTranslation("user_profile");
|
|
||||||
|
|
||||||
const [displayName, setDisplayName] = useState("");
|
|
||||||
const [newImagePath, setNewImagePath] = useState<string | null>(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<HTMLFormElement> = 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 (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
visible={visible}
|
|
||||||
title={t("edit_profile")}
|
|
||||||
onClose={cleanFormAndClose}
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
onSubmit={handleSaveProfile}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
|
||||||
width: "350px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.profileAvatarEditContainer}
|
|
||||||
onClick={handleChangeProfileAvatar}
|
|
||||||
>
|
|
||||||
{avatarUrl ? (
|
|
||||||
<img
|
|
||||||
className={styles.profileAvatar}
|
|
||||||
alt={userProfile.displayName}
|
|
||||||
src={avatarUrl}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PersonIcon size={96} />
|
|
||||||
)}
|
|
||||||
<div className={styles.editProfileImageBadge}>
|
|
||||||
<DeviceCameraIcon size={16} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label={t("display_name")}
|
|
||||||
value={displayName}
|
|
||||||
required
|
|
||||||
minLength={3}
|
|
||||||
containerProps={{ style: { width: "100%" } }}
|
|
||||||
onChange={(e) => setDisplayName(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
disabled={isSaving}
|
|
||||||
style={{ alignSelf: "end" }}
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{isSaving ? t("saving") : t("save")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./user-profile-settings-modal";
|
@ -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<UserFriend[]>([]);
|
||||||
|
const listContainer = useRef<HTMLDivElement>(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 (
|
||||||
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
|
<div
|
||||||
|
ref={listContainer}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
maxHeight: "400px",
|
||||||
|
overflowY: "scroll",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!isLoading && blocks.length === 0 && <p>{t("no_blocked_users")}</p>}
|
||||||
|
{blocks.map((friend) => {
|
||||||
|
return (
|
||||||
|
<UserFriendItem
|
||||||
|
userId={friend.id}
|
||||||
|
displayName={friend.displayName}
|
||||||
|
profileImageUrl={friend.profileImageUrl}
|
||||||
|
onClickUnblock={handleUnblock}
|
||||||
|
type={"BLOCKED"}
|
||||||
|
key={friend.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{isLoading && (
|
||||||
|
<Skeleton
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "54px",
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SkeletonTheme>
|
||||||
|
);
|
||||||
|
};
|
@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<HTMLFormElement> = 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 (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSaveProfile}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
|
width: "350px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.profileAvatarEditContainer}
|
||||||
|
onClick={handleChangeProfileAvatar}
|
||||||
|
>
|
||||||
|
{avatarUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.profileAvatar}
|
||||||
|
alt={userProfile.displayName}
|
||||||
|
src={avatarUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={96} />
|
||||||
|
)}
|
||||||
|
<div className={styles.editProfileImageBadge}>
|
||||||
|
<DeviceCameraIcon size={16} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={t("display_name")}
|
||||||
|
value={form.displayName}
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
containerProps={{ style: { width: "100%" } }}
|
||||||
|
onChange={(e) => setForm({ ...form, displayName: e.target.value })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectField
|
||||||
|
label={t("privacy")}
|
||||||
|
value={form.profileVisibility}
|
||||||
|
onChange={handleProfileVisibilityChange}
|
||||||
|
options={profileVisibilityOptions.map((visiblity) => ({
|
||||||
|
key: visiblity.value,
|
||||||
|
value: visiblity.value,
|
||||||
|
label: visiblity.label,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button disabled={isSaving} style={{ alignSelf: "end" }} type="submit">
|
||||||
|
{isSaving ? t("saving") : t("save")}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<UserEditProfile
|
||||||
|
userProfile={userProfile}
|
||||||
|
updateUserProfile={updateUserProfile}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTabIndex == 1) {
|
||||||
|
return <UserEditProfileBlockList />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal visible={visible} title={t("settings")} onClose={onClose}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||||
|
{tabs.map((tab, index) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={tab}
|
||||||
|
theme={index === currentTabIndex ? "primary" : "outline"}
|
||||||
|
onClick={() => setCurrentTabIndex(index)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
{renderTab()}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -60,6 +60,7 @@ export const friendListDisplayName = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const profileAvatarEditContainer = style({
|
export const profileAvatarEditContainer = style({
|
||||||
|
alignSelf: "center",
|
||||||
width: "128px",
|
width: "128px",
|
||||||
height: "128px",
|
height: "128px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -31,7 +31,7 @@ export const User = () => {
|
|||||||
navigate(-1);
|
navigate(-1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [dispatch, userId, t]);
|
}, [dispatch, navigate, showErrorToast, userId, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUserProfile();
|
getUserProfile();
|
||||||
|
@ -282,6 +282,11 @@ export interface UserFriends {
|
|||||||
friends: UserFriend[];
|
friends: UserFriend[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserBlocks {
|
||||||
|
totalBlocks: number;
|
||||||
|
blocks: UserFriend[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface FriendRequest {
|
export interface FriendRequest {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -310,6 +315,13 @@ export interface UserProfile {
|
|||||||
relation: UserRelation | null;
|
relation: UserRelation | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateProfileProps {
|
||||||
|
displayName?: string;
|
||||||
|
profileVisibility?: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||||
|
profileImageUrl?: string | null;
|
||||||
|
bio?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DownloadSource {
|
export interface DownloadSource {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user