mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-14 20:22:10 +00:00
feat: set profile visibility
This commit is contained in:
parent
42a78802a6
commit
6806787ca0
15 changed files with 276 additions and 194 deletions
|
@ -261,6 +261,11 @@
|
|||
"undo_friendship": "Undo friendship",
|
||||
"request_accepted": "Request accepted",
|
||||
"user_blocked_successfully": "User blocked successfully",
|
||||
"user_block_modal_text": "This will block {{displayName}}"
|
||||
"user_block_modal_text": "This will block {{displayName}}",
|
||||
"settings": "Settings",
|
||||
"public": "Public",
|
||||
"private": "Private",
|
||||
"friends_only": "Friends only",
|
||||
"privacy": "Privacy"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -261,6 +261,11 @@
|
|||
"undo_friendship": "Desfazer amizade",
|
||||
"request_accepted": "Pedido de amizade aceito",
|
||||
"user_blocked_successfully": "Usuário bloqueado com sucesso",
|
||||
"user_block_modal_text": "Bloquear {{displayName}}"
|
||||
"user_block_modal_text": "Bloquear {{displayName}}",
|
||||
"settings": "Configurações",
|
||||
"privacy": "Privacidade",
|
||||
"private": "Privado",
|
||||
"friends_only": "Apenas amigos",
|
||||
"public": "Público"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,11 +52,9 @@ import "./profile/undo-friendship";
|
|||
import "./profile/update-friend-request";
|
||||
import "./profile/update-profile";
|
||||
import "./profile/send-friend-request";
|
||||
import { isPortableVersion } from "@main/helpers";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
ipcMain.handle("getVersion", () => app.getVersion());
|
||||
ipcMain.handle(
|
||||
"isPortableVersion",
|
||||
() => process.env.PORTABLE_EXECUTABLE_FILE != null
|
||||
);
|
||||
ipcMain.handle("isPortableVersion", () => isPortableVersion());
|
||||
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
|
||||
|
|
|
@ -4,33 +4,22 @@ import axios from "axios";
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileTypeFromFile } from "file-type";
|
||||
import { UserProfile } from "@types";
|
||||
import { UpdateProfileProps, UserProfile } from "@types";
|
||||
|
||||
const patchUserProfile = async (
|
||||
displayName: string,
|
||||
profileImageUrl?: string
|
||||
) => {
|
||||
if (profileImageUrl) {
|
||||
return HydraApi.patch("/profile", {
|
||||
displayName,
|
||||
profileImageUrl,
|
||||
});
|
||||
} else {
|
||||
return HydraApi.patch("/profile", {
|
||||
displayName,
|
||||
});
|
||||
}
|
||||
const patchUserProfile = async (updateProfile: UpdateProfileProps) => {
|
||||
return HydraApi.patch("/profile", updateProfile);
|
||||
};
|
||||
|
||||
const updateProfile = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
displayName: string,
|
||||
newProfileImagePath: string | null
|
||||
updateProfile: UpdateProfileProps
|
||||
): Promise<UserProfile> => {
|
||||
if (!newProfileImagePath) {
|
||||
return patchUserProfile(displayName);
|
||||
if (!updateProfile.profileImageUrl) {
|
||||
return patchUserProfile(updateProfile);
|
||||
}
|
||||
|
||||
const newProfileImagePath = updateProfile.profileImageUrl;
|
||||
|
||||
const stats = fs.statSync(newProfileImagePath);
|
||||
const fileBuffer = fs.readFileSync(newProfileImagePath);
|
||||
const fileSizeInBytes = stats.size;
|
||||
|
@ -53,7 +42,7 @@ const updateProfile = async (
|
|||
})
|
||||
.catch(() => undefined);
|
||||
|
||||
return patchUserProfile(displayName, profileImageUrl);
|
||||
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
||||
};
|
||||
|
||||
registerEvent("updateProfile", updateProfile);
|
||||
|
|
|
@ -57,4 +57,7 @@ export const requestWebPage = async (url: string) => {
|
|||
.then((response) => response.data);
|
||||
};
|
||||
|
||||
export const isPortableVersion = () =>
|
||||
process.env.PORTABLE_EXECUTABLE_FILE != null;
|
||||
|
||||
export * from "./download-source";
|
||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
|||
StartGameDownloadPayload,
|
||||
GameRunning,
|
||||
FriendRequestAction,
|
||||
UpdateProfileProps,
|
||||
} from "@types";
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
|
@ -137,8 +138,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
getMe: () => ipcRenderer.invoke("getMe"),
|
||||
undoFriendship: (userId: string) =>
|
||||
ipcRenderer.invoke("undoFriendship", userId),
|
||||
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
|
||||
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
|
||||
updateProfile: (updateProfile: UpdateProfileProps) =>
|
||||
ipcRenderer.invoke("updateProfile", updateProfile),
|
||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
||||
|
|
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
|
@ -139,10 +139,7 @@ declare global {
|
|||
/* Profile */
|
||||
getMe: () => Promise<UserProfile | null>;
|
||||
undoFriendship: (userId: string) => Promise<void>;
|
||||
updateProfile: (
|
||||
displayName: string,
|
||||
newProfileImagePath: string | null
|
||||
) => Promise<UserProfile>;
|
||||
updateProfile: (updateProfile: UpdateProfileProps) => Promise<UserProfile>;
|
||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||
updateFriendRequest: (
|
||||
userId: string,
|
||||
|
|
|
@ -8,8 +8,9 @@ import {
|
|||
setFriendsModalHidden,
|
||||
} from "@renderer/features";
|
||||
import { profileBackgroundFromProfileImage } from "@renderer/helpers";
|
||||
import { FriendRequestAction, UserDetails } from "@types";
|
||||
import { FriendRequestAction, UpdateProfileProps, UserDetails } from "@types";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import { logger } from "@renderer/logger";
|
||||
|
||||
export function useUserDetails() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
@ -43,7 +44,10 @@ export function useUserDetails() {
|
|||
if (userDetails.profileImageUrl) {
|
||||
const profileBackground = await profileBackgroundFromProfileImage(
|
||||
userDetails.profileImageUrl
|
||||
);
|
||||
).catch((err) => {
|
||||
logger.error("profileBackgroundFromProfileImage", err);
|
||||
return `#151515B3`;
|
||||
});
|
||||
dispatch(setProfileBackground(profileBackground));
|
||||
|
||||
window.localStorage.setItem(
|
||||
|
@ -74,12 +78,8 @@ export function useUserDetails() {
|
|||
}, [clearUserDetails]);
|
||||
|
||||
const patchUser = useCallback(
|
||||
async (displayName: string, imageProfileUrl: string | null) => {
|
||||
const response = await window.electron.updateProfile(
|
||||
displayName,
|
||||
imageProfileUrl
|
||||
);
|
||||
|
||||
async (props: UpdateProfileProps) => {
|
||||
const response = await window.electron.updateProfile(props);
|
||||
return updateUserDetails(response);
|
||||
},
|
||||
[updateUserDetails]
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
XCircleIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { Button, Link } from "@renderer/components";
|
||||
import { UserEditProfileModal } from "./user-edit-modal";
|
||||
import { UserProfileSettingsModal } from "./user-profile-settings-modal";
|
||||
import { UserSignOutModal } from "./user-sign-out-modal";
|
||||
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
|
||||
import { UserBlockModal } from "./user-block-modal";
|
||||
|
@ -60,7 +60,8 @@ export function UserContent({
|
|||
|
||||
const [profileContentBoxBackground, setProfileContentBoxBackground] =
|
||||
useState<string | undefined>();
|
||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||
const [showProfileSettingsModal, setShowProfileSettingsModal] =
|
||||
useState(false);
|
||||
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
||||
const [showUserBlockModal, setShowUserBlockModal] = useState(false);
|
||||
|
||||
|
@ -95,7 +96,7 @@ export function UserContent({
|
|||
};
|
||||
|
||||
const handleEditProfile = () => {
|
||||
setShowEditProfileModal(true);
|
||||
setShowProfileSettingsModal(true);
|
||||
};
|
||||
|
||||
const handleOnClickFriend = (userId: string) => {
|
||||
|
@ -165,7 +166,7 @@ export function UserContent({
|
|||
return (
|
||||
<>
|
||||
<Button theme="outline" onClick={handleEditProfile}>
|
||||
{t("edit_profile")}
|
||||
{t("settings")}
|
||||
</Button>
|
||||
|
||||
<Button theme="danger" onClick={() => setShowSignOutModal(true)}>
|
||||
|
@ -251,9 +252,9 @@ export function UserContent({
|
|||
|
||||
return (
|
||||
<>
|
||||
<UserEditProfileModal
|
||||
visible={showEditProfileModal}
|
||||
onClose={() => setShowEditProfileModal(false)}
|
||||
<UserProfileSettingsModal
|
||||
visible={showProfileSettingsModal}
|
||||
onClose={() => setShowProfileSettingsModal(false)}
|
||||
updateUserProfile={updateUserProfile}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
|
|
|
@ -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,150 @@
|
|||
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",
|
||||
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={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,72 @@
|
|||
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";
|
||||
|
||||
export interface UserEditProfileModalProps {
|
||||
userProfile: UserProfile;
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
updateUserProfile: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const UserProfileSettingsModal = ({
|
||||
userProfile,
|
||||
visible,
|
||||
onClose,
|
||||
updateUserProfile,
|
||||
}: UserEditProfileModalProps) => {
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
const tabs = [t("edit_profile"), "Ban list"];
|
||||
|
||||
const [currentTabIndex, setCurrentTabIndex] = useState(0);
|
||||
|
||||
const renderTab = () => {
|
||||
if (currentTabIndex == 0) {
|
||||
return (
|
||||
<UserEditProfile
|
||||
userProfile={userProfile}
|
||||
updateUserProfile={updateUserProfile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentTabIndex == 1) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -310,6 +310,13 @@ export interface UserProfile {
|
|||
relation: UserRelation | null;
|
||||
}
|
||||
|
||||
export interface UpdateProfileProps {
|
||||
displayName?: string;
|
||||
profileVisibility?: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||
profileImageUrl?: string | null;
|
||||
bio?: string;
|
||||
}
|
||||
|
||||
export interface DownloadSource {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
Loading…
Reference in a new issue