Skip to content

Commit

Permalink
feat(project): upload avatar
Browse files Browse the repository at this point in the history
  • Loading branch information
hyrious committed Jun 8, 2022
1 parent fcbccae commit c58ca91
Show file tree
Hide file tree
Showing 14 changed files with 478 additions and 27 deletions.
43 changes: 43 additions & 0 deletions desktop/renderer-app/src/api-middleware/flatServer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,3 +564,46 @@ export async function rename(name: string): Promise<RenameResult> {
name,
});
}

export interface UploadAvatarStartPayload {
fileName: string;
fileSize: number;
region: Region;
}

export interface UploadAvatarResult {
fileUUID: string;
filePath: string;
policy: string;
policyURL: string;
signature: string;
}

export async function uploadAvatarStart(
fileName: string,
fileSize: number,
region: Region,
): Promise<UploadAvatarResult> {
return await post<UploadAvatarStartPayload, UploadAvatarResult>("user/upload-avatar/start", {
fileName,
fileSize,
region,
});
}

export interface UploadAvatarFinishPayload {
fileUUID: string;
}

export interface UploadAvatarFinishResult {
avatarURL: string;
}

export async function uploadAvatarFinish(fileUUID: string): Promise<UploadAvatarFinishResult> {
return await post<UploadAvatarFinishPayload, UploadAvatarFinishResult>(
"user/upload-avatar/finish",
{
fileUUID,
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const ConfirmButtons: React.FC<ConfirmButtonsProps> = ({ onConfirm }) =>

const confirm = useCallback(async () => {
setLoading(true);
await sp(onConfirm());
await sp(onConfirm().catch(console.error));
setLoading(false);
setPhase("idle");
}, [onConfirm, sp]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Axios from "axios";
import React, { ChangeEvent, useContext, useRef, useState } from "react";
import classNames from "classnames";
import { Region } from "flat-components";
import { observer } from "mobx-react-lite";
import { useTranslation } from "react-i18next";

import { GlobalStoreContext } from "../../../components/StoreProvider";
import { useSafePromise } from "../../../utils/hooks/lifecycle";
import { CLOUD_STORAGE_OSS_ALIBABA_CONFIG } from "../../../constants/process";
import { uploadAvatarFinish, uploadAvatarStart } from "../../../api-middleware/flatServer";
import { globalStore } from "../../../stores/global-store";

export interface UploadAvatarProps {
fileRef?: React.MutableRefObject<File | undefined>;
}

export function useFileRef(): React.MutableRefObject<File | undefined> {
return useRef<File>();
}

export const UploadAvatar = observer<UploadAvatarProps>(function UploadAvatar({ fileRef }) {
const globalStore = useContext(GlobalStoreContext);
const sp = useSafePromise();
const { t } = useTranslation();

const [loading, setLoading] = useState(false);
const [imageUrl, setImageUrl] = useState(globalStore.userInfo?.avatar || "");

const updateInput = (event: ChangeEvent<HTMLInputElement>): void => {
const file: File | undefined = (event.target.files || [])[0];
if (fileRef) {
fileRef.current = file;
}
if (file) {
setLoading(true);
sp(fileToDataUrl(file)).then(url => {
setLoading(false);
setImageUrl(url);
});
} else {
setImageUrl(globalStore.userInfo?.avatar || "");
}
};

return (
<div
className={classNames("general-setting-user-avatar", {
"is-loading": loading,
})}
>
<input className="user-avatar-input" type="file" onChange={updateInput} />
{imageUrl ? (
<img alt="avatar" className="user-avatar-image" src={imageUrl} />
) : (
<span className="user-avatar-text">{t("upload-avatar")}</span>
)}
</div>
);
});

function fileToDataUrl(file: File): Promise<string> {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}

export async function uploadAvatar(file: File): Promise<void> {
const ticket = await uploadAvatarStart(
file.name,
file.size,
globalStore.region ?? Region.CN_HZ,
);

const formData = new FormData();
const encodedFileName = encodeURIComponent(file.name);
formData.append("key", ticket.filePath);
formData.append("name", file.name);
formData.append("policy", ticket.policy);
formData.append("OSSAccessKeyId", CLOUD_STORAGE_OSS_ALIBABA_CONFIG.accessKey);
formData.append("success_action_status", "200");
formData.append("callback", "");
formData.append("signature", ticket.signature);
formData.append(
"Content-Disposition",
`attachment; filename="${encodedFileName}"; filename*=UTF-8''${encodedFileName}`,
);
formData.append("file", file);

await Axios.post(ticket.policyURL, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});

const { avatarURL } = await uploadAvatarFinish(ticket.fileUUID);

globalStore.updateUserAvatar(avatarURL);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import "./style.less";

import React, { useContext, useEffect, useState } from "react";
import { Checkbox, Input, Radio, RadioChangeEvent } from "antd";
import { Checkbox, Input, message, Radio, RadioChangeEvent } from "antd";
import { UserSettingLayoutContainer } from "../UserSettingLayoutContainer";
import { ipcSyncByApp, ipcAsyncByApp } from "../../../utils/ipc";
import { useTranslation } from "react-i18next";
Expand All @@ -10,6 +10,7 @@ import { ConfigStoreContext, GlobalStoreContext } from "../../../components/Stor
import { useSafePromise } from "../../../utils/hooks/lifecycle";
import { loginCheck, rename } from "../../../api-middleware/flatServer";
import { ConfirmButtons } from "./ConfirmButtons";
import { UploadAvatar, uploadAvatar, useFileRef } from "./UploadAvatar";

enum SelectLanguage {
Chinese,
Expand All @@ -25,6 +26,7 @@ export const GeneralSettingPage = (): React.ReactElement => {

const [name, setName] = useState(globalStore.userName || "");
const [isRenaming, setRenaming] = useState(false);
const fileRef = useFileRef();

async function changeUserName(): Promise<void> {
if (name !== globalStore.userName) {
Expand All @@ -38,6 +40,19 @@ export const GeneralSettingPage = (): React.ReactElement => {
}
}

async function changeAvatar(): Promise<void> {
if (fileRef.current) {
try {
await uploadAvatar(fileRef.current);
} catch (error) {
console.error(error);
message.info(t("upload-avatar-failed"));
} finally {
fileRef.current = undefined;
}
}
}

useEffect(() => {
ipcSyncByApp("get-open-at-login")
.then(data => {
Expand Down Expand Up @@ -76,15 +91,21 @@ export const GeneralSettingPage = (): React.ReactElement => {
</Checkbox>
</div>
<div className="general-setting-user-profile">
<span>{t("user-profile")}</span>
<Input
disabled={isRenaming}
id="username"
spellCheck={false}
value={name}
onChange={ev => setName(ev.currentTarget.value)}
/>
<ConfirmButtons onConfirm={changeUserName} />
<span className="general-setting-title">{t("user-profile")}</span>
<div className="general-setting-user-avatar-wrapper">
<UploadAvatar fileRef={fileRef} />
<ConfirmButtons onConfirm={changeAvatar} />
</div>
<div>
<Input
disabled={isRenaming}
id="username"
spellCheck={false}
value={name}
onChange={ev => setName(ev.currentTarget.value)}
/>
<ConfirmButtons onConfirm={changeUserName} />
</div>
</div>
<div className="general-setting-select-language">
<span>{t("language-settings")}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
}

.general-setting-user-profile {
> span {
.general-setting-title {
display: block;
padding-bottom: 4px;
}
Expand All @@ -24,6 +24,57 @@
}
}

.general-setting-user-avatar-wrapper {
display: flex;
align-items: center;
padding: 8px 0;
}

.general-setting-user-avatar {
display: inline-block;
width: 96px;
height: 96px;
margin-right: 16px;
border-radius: 50%;
border: 1px solid var(--grey-6);
overflow: hidden;
position: relative;
transition: all 0.2s ease-in-out;

&.is-loading {
opacity: 0.5;
}

&:hover {
border-color: var(--primary);
}

.user-avatar-input {
display: block;
position: absolute;
width: 100%;
height: 100%;
z-index: 100;
opacity: 0;
cursor: pointer;
}

.user-avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
}

.user-avatar-text {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}

.general-setting-checkbox {
margin-bottom: 8px;
}
Expand Down
6 changes: 6 additions & 0 deletions desktop/renderer-app/src/stores/global-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ export class GlobalStore {
this.userInfo = userInfo;
};

public updateUserAvatar = (avatarURL: string): void => {
if (this.userInfo) {
this.userInfo.avatar = avatarURL;
}
};

public updateLastLoginCheck = (val: number | null): void => {
this.lastLoginCheck = val;
};
Expand Down
4 changes: 3 additions & 1 deletion packages/flat-i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -454,5 +454,7 @@
"english_1": "Four Lines and Three Grids",
"chinese_1": "Tin Word Format"
},
"user-profile": "User Profile"
"user-profile": "User Profile",
"upload-avatar": "Upload Avatar",
"upload-avatar-failed": "Upload avatar failed"
}
4 changes: 3 additions & 1 deletion packages/flat-i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -454,5 +454,7 @@
"english_1": "四线三格",
"chinese_1": "田字格"
},
"user-profile": "用户资料"
"user-profile": "用户资料",
"upload-avatar": "上传头像",
"upload-avatar-failed": "上传头像失败"
}
43 changes: 43 additions & 0 deletions web/flat-web/src/api-middleware/flatServer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,3 +564,46 @@ export async function rename(name: string): Promise<RenameResult> {
name,
});
}

export interface UploadAvatarStartPayload {
fileName: string;
fileSize: number;
region: Region;
}

export interface UploadAvatarResult {
fileUUID: string;
filePath: string;
policy: string;
policyURL: string;
signature: string;
}

export async function uploadAvatarStart(
fileName: string,
fileSize: number,
region: Region,
): Promise<UploadAvatarResult> {
return await post<UploadAvatarStartPayload, UploadAvatarResult>("user/upload-avatar/start", {
fileName,
fileSize,
region,
});
}

export interface UploadAvatarFinishPayload {
fileUUID: string;
}

export interface UploadAvatarFinishResult {
avatarURL: string;
}

export async function uploadAvatarFinish(fileUUID: string): Promise<UploadAvatarFinishResult> {
return await post<UploadAvatarFinishPayload, UploadAvatarFinishResult>(
"user/upload-avatar/finish",
{
fileUUID,
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const ConfirmButtons: React.FC<ConfirmButtonsProps> = ({ onConfirm }) =>

const confirm = useCallback(async () => {
setLoading(true);
await sp(onConfirm());
await sp(onConfirm().catch(console.error));
setLoading(false);
setPhase("idle");
}, [onConfirm, sp]);
Expand Down
Loading

0 comments on commit c58ca91

Please sign in to comment.