ProfileImageSettings.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import React, { useCallback, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import AppContainer from '~/client/services/AppContainer';
  4. import { toastSuccess, toastError } from '~/client/util/apiNotification';
  5. import { apiPost } from '~/client/util/apiv1-client';
  6. import { apiv3Put } from '~/client/util/apiv3-client';
  7. import { useCurrentUser } from '~/stores/context';
  8. import { generateGravatarSrc, GRAVATAR_DEFAULT } from '~/utils/gravatar';
  9. import { withUnstatedContainers } from '../UnstatedUtils';
  10. import ImageCropModal from './ImageCropModal';
  11. const DEFAULT_IMAGE = '/images/icons/user.svg';
  12. type Props = {
  13. appContainer: AppContainer,
  14. }
  15. const ProfileImageSettings = (props: Props): JSX.Element => {
  16. const { t } = useTranslation();
  17. const { appContainer } = props;
  18. const { data: currentUser } = useCurrentUser();
  19. const [isGravatarEnabled, setGravatarEnabled] = useState(currentUser?.isGravatarEnabled);
  20. const [uploadedPictureSrc, setUploadedPictureSrc] = useState(() => {
  21. if (typeof currentUser?.imageAttachment === 'string') {
  22. return currentUser?.image;
  23. }
  24. return currentUser?.imageAttachment?.filePathProxied ?? currentUser?.image;
  25. });
  26. const [showImageCropModal, setShowImageCropModal] = useState(false);
  27. const [imageCropSrc, setImageCropSrc] = useState<string|ArrayBuffer|null>(null);
  28. const selectFileHandler = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
  29. if (e.target.files == null || e.target.files.length === 0) {
  30. return;
  31. }
  32. const reader = new FileReader();
  33. reader.addEventListener('load', () => setImageCropSrc(reader.result));
  34. reader.readAsDataURL(e.target.files[0]);
  35. setShowImageCropModal(true);
  36. }, []);
  37. const cropCompletedHandler = useCallback(async(croppedImage) => {
  38. try {
  39. const formData = new FormData();
  40. formData.append('file', croppedImage);
  41. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  42. formData.append('_csrf', appContainer.csrfToken!);
  43. const response = await apiPost('/attachments.uploadProfileImage', formData);
  44. toastSuccess(t('toaster.update_successed', { target: t('Current Image') }));
  45. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  46. setUploadedPictureSrc((response as any).attachment.filePathProxied);
  47. // close modal
  48. setShowImageCropModal(false);
  49. }
  50. catch (err) {
  51. toastError(err);
  52. }
  53. }, [appContainer.csrfToken, t]);
  54. const deleteImageHandler = useCallback(async() => {
  55. try {
  56. await apiPost('/attachments.removeProfileImage');
  57. setUploadedPictureSrc(undefined);
  58. toastSuccess(t('toaster.update_successed', { target: t('Current Image') }));
  59. }
  60. catch (err) {
  61. toastError(err);
  62. }
  63. }, [t]);
  64. const submit = useCallback(async() => {
  65. try {
  66. const response = await apiv3Put('/personal-setting/image-type', { isGravatarEnabled });
  67. const { userData } = response.data;
  68. setGravatarEnabled(userData.isGravatarEnabled);
  69. toastSuccess(t('toaster.update_successed', { target: t('Set Profile Image') }));
  70. }
  71. catch (err) {
  72. toastError(err);
  73. }
  74. }, [isGravatarEnabled, t]);
  75. if (currentUser == null) {
  76. return <></>;
  77. }
  78. return (
  79. <>
  80. <div className="row">
  81. <div className="col-md-6 col-12 mb-3 mb-md-0">
  82. <h4>
  83. <div className="custom-control custom-radio radio-primary">
  84. <input
  85. type="radio"
  86. id="radioGravatar"
  87. className="custom-control-input"
  88. form="formImageType"
  89. name="imagetypeForm[isGravatarEnabled]"
  90. checked={isGravatarEnabled}
  91. onChange={() => setGravatarEnabled(true)}
  92. />
  93. <label className="custom-control-label" htmlFor="radioGravatar">
  94. <img src={GRAVATAR_DEFAULT} data-hide-in-vrt /> Gravatar
  95. </label>
  96. <a href="https://gravatar.com/">
  97. <small><i className="icon-arrow-right-circle" aria-hidden="true"></i></small>
  98. </a>
  99. </div>
  100. </h4>
  101. <img src={generateGravatarSrc(currentUser.email)} width="64" data-hide-in-vrt />
  102. </div>
  103. <div className="col-md-6 col-12">
  104. <h4>
  105. <div className="custom-control custom-radio radio-primary">
  106. <input
  107. type="radio"
  108. id="radioUploadPicture"
  109. className="custom-control-input"
  110. form="formImageType"
  111. name="imagetypeForm[isGravatarEnabled]"
  112. checked={!isGravatarEnabled}
  113. onChange={() => setGravatarEnabled(false)}
  114. />
  115. <label className="custom-control-label" htmlFor="radioUploadPicture">
  116. { t('Upload Image') }
  117. </label>
  118. </div>
  119. </h4>
  120. <div className="row mb-3">
  121. <label className="col-sm-4 col-12 col-form-label text-left">
  122. { t('Current Image') }
  123. </label>
  124. <div className="col-sm-8 col-12">
  125. <p><img src={uploadedPictureSrc ?? DEFAULT_IMAGE} className="picture picture-lg rounded-circle" id="settingUserPicture" /></p>
  126. {uploadedPictureSrc && <button type="button" className="btn btn-danger" onClick={deleteImageHandler}>{ t('Delete Image') }</button>}
  127. </div>
  128. </div>
  129. <div className="row">
  130. <label className="col-sm-4 col-12 col-form-label text-left">
  131. {t('Upload new image')}
  132. </label>
  133. <div className="col-sm-8 col-12">
  134. <input type="file" onChange={selectFileHandler} name="profileImage" accept="image/*" />
  135. </div>
  136. </div>
  137. </div>
  138. </div>
  139. <ImageCropModal
  140. show={showImageCropModal}
  141. src={imageCropSrc}
  142. onModalClose={() => setShowImageCropModal(false)}
  143. onCropCompleted={cropCompletedHandler}
  144. />
  145. <div className="row my-3">
  146. <div className="offset-4 col-5">
  147. <button type="button" className="btn btn-primary" onClick={submit}>
  148. {t('Update')}
  149. </button>
  150. </div>
  151. </div>
  152. </>
  153. );
  154. };
  155. export default withUnstatedContainers(ProfileImageSettings, [AppContainer]);