diff --git a/package.json b/package.json index c9f9c2bf94..84a2b15b3a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "react-split-pane": "^0.1.92", "react-use": "^17.4.0", "react-virtualized": "^9.22.3", + "react-zoom-pan-pinch": "^3.2.0", "redux": "^4.0.4", "redux-saga": "^1.1.3", "reselect": "^4.0.0", diff --git a/src/constants.scss b/src/constants.scss index 987baf3f9b..dd8fbd33f1 100644 --- a/src/constants.scss +++ b/src/constants.scss @@ -143,6 +143,7 @@ $c-pink-active-feed-cards-light: #fcf4ff; $c-pink-active-feed-cards: #a75a93; $c-pink-hover-feed-cards: #fff9fd; $c-gray-5: #f4f5f5; +$c-gray-10: #eef0f4; $c-gray-20: #dcdee7; $c-gray-30: #b7bcd2; $c-gray-40: #8d91a9; diff --git a/src/pages/App/handlers/WebViewLoginHandler/WebViewLoginHandler.tsx b/src/pages/App/handlers/WebViewLoginHandler/WebViewLoginHandler.tsx index d3f1aac2e3..3a293f7004 100644 --- a/src/pages/App/handlers/WebViewLoginHandler/WebViewLoginHandler.tsx +++ b/src/pages/App/handlers/WebViewLoginHandler/WebViewLoginHandler.tsx @@ -4,7 +4,7 @@ import { webviewLogin } from "@/pages/Auth/store/actions"; import { history } from "@/shared/appConfig"; import { WebviewActions } from "@/shared/constants"; import { FirebaseCredentials } from "@/shared/interfaces/FirebaseCredentials"; -import { getInboxPagePath_v04 } from "@/shared/utils"; +import { getInboxPagePath } from "@/shared/utils"; import { parseJson } from "@/shared/utils/json"; const WebViewLoginHandler: FC = () => { @@ -25,7 +25,7 @@ const WebViewLoginHandler: FC = () => { window.ReactNativeWebView.postMessage( WebviewActions.loginSuccess, ); - history.push(getInboxPagePath_v04()); + history.push(getInboxPagePath()); } else { window.ReactNativeWebView.postMessage(WebviewActions.loginError); } diff --git a/src/pages/App/router/configuration/commonSidenavLayout.tsx b/src/pages/App/router/configuration/commonSidenavLayout.tsx index 1af33a09a6..4535543fda 100644 --- a/src/pages/App/router/configuration/commonSidenavLayout.tsx +++ b/src/pages/App/router/configuration/commonSidenavLayout.tsx @@ -8,6 +8,7 @@ import { CommonEditingPage_v04 } from "@/pages/commonEditing"; import { CommonFeedPage_v04 } from "@/pages/commonFeed"; import { InboxPage_v04 } from "@/pages/inbox"; import { ProfilePage_v04 } from "@/pages/profile"; +import { SettingsPage_v04 } from "@/pages/settings"; import { ROUTE_PATHS } from "@/shared/constants"; import { CommonSidenavLayout } from "@/shared/layouts"; import { LayoutConfiguration, RouteType } from "../types"; @@ -66,5 +67,10 @@ export const COMMON_SIDENAV_LAYOUT_CONFIGURATION: LayoutConfiguration void }, User, Error>(); +)< + { user: User; callback: (error: Error | null, user?: User) => void }, + User, + Error +>(); export const setLoginModalState = createStandardAction( AuthActionTypes.SET_LOGIN_MODAL_STATE, diff --git a/src/pages/Auth/store/saga.tsx b/src/pages/Auth/store/saga.tsx index 0abc3aca5a..6aa36334b2 100755 --- a/src/pages/Auth/store/saga.tsx +++ b/src/pages/Auth/store/saga.tsx @@ -4,7 +4,9 @@ import { seenNotification, subscribeToNotification, } from "@/pages/OldCommon/store/api"; +import { UserService } from "@/services"; import { store } from "@/shared/appConfig"; +import { Awaited } from "@/shared/interfaces"; import { FirebaseCredentials } from "@/shared/interfaces/FirebaseCredentials"; import { EventTypeState, NotificationItem } from "@/shared/models/Notification"; import { isFundsAllocationProposal } from "@/shared/models/governance/proposals"; @@ -12,6 +14,7 @@ import { showNotification } from "@/shared/store/actions"; import { getProvider } from "@/shared/utils/authProvider"; import { getFundingRequestNotification } from "@/shared/utils/notifications"; import { + cacheActions, commonLayoutActions, multipleSpacesLayoutActions, } from "@/store/states"; @@ -264,7 +267,7 @@ const confirmVerificationCode = async ( return verifyLoggedInUser(user, true, authCode); }; -const updateUserData = async (user: User) => { +const updateUserData = async (user: User): Promise => { const currentUser = await firebase.auth().currentUser; const profileData: { displayName?: string | null; @@ -283,40 +286,19 @@ const updateUserData = async (user: User) => { } await currentUser?.updateProfile(profileData); - const updatedCurrentUser = await firebase.auth().currentUser; - if (updatedCurrentUser) { - await firebase - .firestore() - .collection(Collection.Users) - .doc(updatedCurrentUser?.uid) - .update({ - firstName: user.firstName, - lastName: user.lastName, - email: user.email, - photoURL: updatedCurrentUser.photoURL || user.photo || "", - intro: user.intro, - displayName: `${user.firstName} ${user.lastName}`, - country: user.country, - phoneNumber: user.phoneNumber || "", - }) - .then(async () => { - const updatedCurrentUser = await firebase.auth().currentUser; - - if (updatedCurrentUser) { - const databaseUser = await getUserData(updatedCurrentUser?.uid ?? ""); - if (databaseUser) { - store.dispatch(actions.socialLogin.success(databaseUser)); - } - } - - return updatedCurrentUser; - }) - .catch((err) => console.error(err)); - } - - return getUserData(updatedCurrentUser?.uid ?? ""); + return await UserService.updateUser({ + ...user, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + photoURL: updatedCurrentUser?.photoURL || user.photo || "", + intro: user.intro, + displayName: `${user.firstName} ${user.lastName}`, + country: user.country, + phoneNumber: user.phoneNumber || "", + }); }; function* socialLoginSaga({ @@ -494,11 +476,24 @@ function* updateUserDetails({ }: ReturnType) { try { yield put(actions.startAuthLoading()); - const user: User = yield call(updateUserData, payload.user); + const user = (yield call(updateUserData, payload.user)) as Awaited< + ReturnType + >; yield put(actions.updateUserDetails.success(user)); + yield put(actions.socialLogin.success(user)); + yield put( + cacheActions.updateUserStateById({ + userId: user.uid, + state: { + loading: false, + fetched: true, + data: user, + }, + }), + ); tokenHandler.setUser(user); - yield payload.callback(null); + yield payload.callback(null, user); } catch (error) { if (isError(error)) { yield put(actions.updateUserDetails.failure(error)); diff --git a/src/pages/Login/components/LoginContainer/UserDetails/UserDetails.tsx b/src/pages/Login/components/LoginContainer/UserDetails/UserDetails.tsx index a3d6d91f6c..8fd8725cd5 100644 --- a/src/pages/Login/components/LoginContainer/UserDetails/UserDetails.tsx +++ b/src/pages/Login/components/LoginContainer/UserDetails/UserDetails.tsx @@ -16,6 +16,7 @@ import { ANONYMOUS_USER_FIRST_NAME, ANONYMOUS_USER_LAST_NAME, } from "@/shared/constants"; +import { useImageSizeCheck } from "@/shared/hooks"; import { countryList } from "../../../../../shared/assets/countries"; import { Button, @@ -112,6 +113,7 @@ const UserDetails: ForwardRefRenderFunction< onSubmitting, } = props; const formRef = useRef>(null); + const { checkImageSize } = useImageSizeCheck(); const [loading, setLoading] = useState(false); const [uploadedPhoto, setUploadedPhoto] = useState(""); const inputFile: any = useRef(null); @@ -143,6 +145,10 @@ const UserDetails: ForwardRefRenderFunction< return; } + if (!checkImageSize(file.name, file.size)) { + return; + } + setLoading(true); if (onLoading) { diff --git a/src/pages/MyAccount/components/Profile/Profile.module.scss b/src/pages/MyAccount/components/Profile/Profile.module.scss new file mode 100644 index 0000000000..20ec36bfc2 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/Profile.module.scss @@ -0,0 +1,50 @@ +@import "../../../../constants"; +@import "../../../../styles/sizes"; + +.container { + position: relative; + width: 100%; +} + +.header { + margin-bottom: 2.25rem; + + @include tablet { + margin-bottom: 0; + } +} + +.editButton { + position: absolute; + top: 1.5rem; + right: 0; + + @include tablet { + position: static; + top: unset; + right: unset; + } +} + +.formWrapper { + max-width: 43.625rem; + margin: 0 auto; + box-sizing: border-box; + + @include tablet { + margin-top: 1.375rem; + padding: 0 1rem; + } +} + +.menuButtonsWrapper { + border-top: 0.0625rem solid $c-gray-20; +} + +.menuButton { + border-bottom: 0.0625rem solid $c-gray-20; +} + +.logoutMenuButton { + color: $c-pink-mention; +} diff --git a/src/pages/MyAccount/components/Profile/Profile.tsx b/src/pages/MyAccount/components/Profile/Profile.tsx index 53cb21663a..3d548994fa 100644 --- a/src/pages/MyAccount/components/Profile/Profile.tsx +++ b/src/pages/MyAccount/components/Profile/Profile.tsx @@ -1,43 +1,51 @@ -import React, { useRef, useState } from "react"; -import { useSelector } from "react-redux"; +import React, { FC, useRef, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { logOut } from "@/pages/Auth/store/actions"; import { selectUser } from "@/pages/Auth/store/selectors"; import { UserDetails, UserDetailsRef, } from "@/pages/Login/components/LoginContainer/UserDetails"; import { ButtonIcon, Loader } from "@/shared/components"; -import { ScreenSize } from "@/shared/constants"; -import { useModal } from "@/shared/hooks"; +import { useRoutesContext } from "@/shared/contexts"; +import { useIsTabletView } from "@/shared/hooks/viewport"; +import { LogoutIcon } from "@/shared/icons"; import EditIcon from "@/shared/icons/edit.icon"; -import { getScreenSize } from "@/shared/store/selectors"; import { Button, ButtonVariant } from "@/shared/ui-kit"; -import { DeleteUserModal } from "../../components/Profile"; +import { Header, MenuButton } from "./components"; +import styles from "./Profile.module.scss"; import "./index.scss"; -export default function Profile() { +interface ProfileProps { + onEditingChange?: (isEditing: boolean) => void; +} + +const Profile: FC = (props) => { + const { onEditingChange } = props; + const dispatch = useDispatch(); + const { getBillingPagePath, getSettingsPagePath } = useRoutesContext(); const userDetailsRef = useRef(null); const [isEditing, setIsEditing] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - const { - isShowing: isDeleteAccountModalShowing, - onOpen: onDeleteAccountModalOpen, - onClose: onDeleteAccountModalClose, - } = useModal(false); const user = useSelector(selectUser()); - const screenSize = useSelector(getScreenSize()); - const isMobileView = screenSize === ScreenSize.Mobile; + const isMobileView = useIsTabletView(); + + const handleEditingChange = (isEditing: boolean) => { + setIsEditing(isEditing); + onEditingChange?.(isEditing); + }; const handleEditClick = () => { - setIsEditing(true); + handleEditingChange(true); }; const handleCancelClick = () => { - setIsEditing(false); + handleEditingChange(false); }; const handleSubmittingChange = (isSubmitting: boolean) => { if (!isSubmitting) { - setIsEditing(false); + handleEditingChange(false); } setIsSubmitting(isSubmitting); @@ -47,10 +55,13 @@ export default function Profile() { userDetailsRef.current?.submit(); }; + const handleLogout = () => { + dispatch(logOut()); + }; + const buttonsWrapperEl = (
); + const editButtonEl = ( + + + + ); return ( -
-
-

Profile

- {isEditing && !isMobileView && buttonsWrapperEl} - {!isEditing && ( - - - +
+ {!isMobileView && !isEditing && editButtonEl} +
+
+ {!user && } + {user && ( + <> +
+ + {isEditing && buttonsWrapperEl} +
+ {isMobileView && !isEditing && ( +
+ + + } + /> +
+ )} + )} -
- {!user ? ( - - ) : ( - <> - - {isEditing && isMobileView && buttonsWrapperEl} - {!isEditing && ( - - )} - - )} - +
); -} +}; + +export default Profile; diff --git a/src/pages/MyAccount/components/Profile/components/Header/Header.tsx b/src/pages/MyAccount/components/Profile/components/Header/Header.tsx new file mode 100644 index 0000000000..ce57d791ec --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/Header/Header.tsx @@ -0,0 +1,53 @@ +import React, { FC, ReactNode } from "react"; +import classNames from "classnames"; +import styles from "@/pages/settings/components/Settings/components/Header/Header.module.scss"; +import { useGoBack } from "@/shared/hooks"; +import { LongLeftArrowIcon } from "@/shared/icons"; +import { + TopNavigationBackButton, + TopNavigationWithBlocks, +} from "@/shared/ui-kit"; + +interface HeaderProps { + className?: string; + isEditing: boolean; + isMobileVersion?: boolean; + editButtonEl: ReactNode; +} + +const Header: FC = (props) => { + const { className, isEditing, isMobileVersion = false, editButtonEl } = props; + const { canGoBack, goBack } = useGoBack(); + + if (!isMobileVersion) { + return ( +
+

+ {isEditing ? "Edit Profile" : "My Profile"} +

+
+ ); + } + + return ( + } + onClick={goBack} + /> + ) : null + } + centralElement={ +

+ {isEditing ? "Edit profile" : "Profile"} +

+ } + rightElement={isEditing ? null : editButtonEl} + /> + ); +}; + +export default Header; diff --git a/src/pages/MyAccount/components/Profile/components/Header/index.ts b/src/pages/MyAccount/components/Profile/components/Header/index.ts new file mode 100644 index 0000000000..d88c989bbd --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/Header/index.ts @@ -0,0 +1 @@ +export { default as Header } from "./Header"; diff --git a/src/pages/MyAccount/components/Profile/components/MenuButton/MenuButton.module.scss b/src/pages/MyAccount/components/Profile/components/MenuButton/MenuButton.module.scss new file mode 100644 index 0000000000..17d645e6c2 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/MenuButton/MenuButton.module.scss @@ -0,0 +1,25 @@ +@import "../../../../../../constants"; + +.container { + height: 3.5rem; + padding: 0 1rem 0 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + color: $c-gray-100; + overflow: hidden; + text-decoration: none; + + &:hover { + text-decoration: none; + } +} + +.text { + font-family: PoppinsSans, sans-serif; + font-weight: normal; + font-size: 1rem; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} diff --git a/src/pages/MyAccount/components/Profile/components/MenuButton/MenuButton.tsx b/src/pages/MyAccount/components/Profile/components/MenuButton/MenuButton.tsx new file mode 100644 index 0000000000..54b711b978 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/MenuButton/MenuButton.tsx @@ -0,0 +1,41 @@ +import React, { FC, ReactNode } from "react"; +import { NavLink } from "react-router-dom"; +import classNames from "classnames"; +import { ButtonLink } from "@/shared/components"; +import { RightArrowThinIcon } from "@/shared/icons"; +import styles from "./MenuButton.module.scss"; + +interface MenuButtonProps { + className?: string; + text: string; + to?: string; + onClick?: () => void; + iconEl?: ReactNode; +} + +const MenuButton: FC = (props) => { + const { text, to, onClick, iconEl } = props; + const className = classNames(styles.container, props.className); + const contentEl = ( + <> + {text} + {iconEl || } + + ); + + if (!to) { + return ( + + {contentEl} + + ); + } + + return ( + + {contentEl} + + ); +}; + +export default MenuButton; diff --git a/src/pages/MyAccount/components/Profile/components/MenuButton/index.ts b/src/pages/MyAccount/components/Profile/components/MenuButton/index.ts new file mode 100644 index 0000000000..18aefc1403 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/MenuButton/index.ts @@ -0,0 +1 @@ +export { default as MenuButton } from "./MenuButton"; diff --git a/src/pages/MyAccount/components/Profile/components/index.ts b/src/pages/MyAccount/components/Profile/components/index.ts new file mode 100644 index 0000000000..6733922ac9 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/index.ts @@ -0,0 +1,2 @@ +export * from "./Header"; +export * from "./MenuButton"; diff --git a/src/pages/MyAccount/components/Profile/index.scss b/src/pages/MyAccount/components/Profile/index.scss index fe9bb8e258..8b12f92037 100644 --- a/src/pages/MyAccount/components/Profile/index.scss +++ b/src/pages/MyAccount/components/Profile/index.scss @@ -1,8 +1,16 @@ @import "../../../../constants"; +@import "../../../../styles/mixins"; @import "../../../../styles/sizes"; .profile-wrapper { + max-width: 36.25rem; width: 100%; + margin: 0 auto; + padding-top: 3.375rem; + + @include tablet { + padding-top: 0; + } .profile-wrapper__header { height: 4.5rem; @@ -11,54 +19,30 @@ align-items: center; justify-content: space-between; - @include big-phone { + @include tablet { margin-bottom: 0; } } .profile-wrapper__buttons-wrapper { - width: 100%; - display: flex; - justify-content: flex-end; - - @include big-phone { - margin-top: 1.5rem; - flex-wrap: wrap-reverse; - justify-content: space-between; - } + @include flex-list-with-gap(1rem); } - .profile-wrapper__button { - max-width: 9rem; - margin-right: 0.625rem; - - &:last-child { - margin-right: 0; - } - - @include big-phone { - margin-top: 0.625rem; - margin-right: 0; - width: 100%; + .profile-wrapper__user-details { + margin-bottom: 2.125rem; - &:last-child { - margin-top: 0; - } + @include tablet { + margin-bottom: 1.375rem; } } - .profile-wrapper__user-details { - max-width: 43.625rem; - margin: 0 auto 0 0; - } - .profile-wrapper__avatar-wrapper { margin-bottom: 2.5rem; display: flex; flex-direction: column; align-items: flex-start; - @include big-phone { + @include tablet { margin-bottom: 1.5rem; justify-content: center; align-items: center; @@ -92,7 +76,7 @@ "lastName intro" "email intro"; - @include big-phone { + @include tablet { grid-template-areas: "firstName" "lastName" @@ -105,7 +89,7 @@ .profile-wrapper__form-intro-input-wrapper { height: 100%; - @include big-phone { + @include tablet { height: 9rem; } } diff --git a/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalForm.tsx b/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalForm.tsx index 7a32861e5b..1034da0162 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalForm.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalForm.tsx @@ -15,6 +15,7 @@ import { MAX_LINK_TITLE_LENGTH, AllocateFundsTo, } from "@/shared/constants"; +import { useImageSizeCheck } from "@/shared/hooks"; import DeleteIcon from "@/shared/icons/delete.icon"; import { Common, Currency } from "@/shared/models"; import { formatPrice } from "@/shared/utils"; @@ -69,6 +70,7 @@ export const AddProposalForm = ({ hidden = false, }: AddProposalFormInterface) => { const dispatch = useDispatch(); + const { checkImageSize } = useImageSizeCheck(); const [isAmountAdded, addAmountToValidation] = useState(false); const [showFileLoader, setShowFileLoader] = useState(false); const [schema, setSchema] = useState(validationSchema); @@ -130,6 +132,10 @@ export const AddProposalForm = ({ return; } + if (!checkImageSize(file.name, file.size)) { + return; + } + setSelectedFiles((selectedFiles) => [...selectedFiles, file]); }; diff --git a/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/FileUploadButton/FileUploadButton.tsx b/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/FileUploadButton/FileUploadButton.tsx index f31f987b7c..d3f77a5f17 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/FileUploadButton/FileUploadButton.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/FileUploadButton/FileUploadButton.tsx @@ -1,5 +1,6 @@ import React, { ChangeEventHandler, FC } from "react"; import classNames from "classnames"; +import { useImageSizeCheck } from "@/shared/hooks"; import { DocInfo } from "@/shared/models"; import { FileInfo } from "../FileInfo"; import "./index.scss"; @@ -32,11 +33,12 @@ const FileUploadButton: FC = (props) => { onDelete, onUploadedFileClick, } = props; + const { checkImageSize } = useImageSizeCheck(); const handleChange: ChangeEventHandler = (event) => { const file = event.target.files ? event.target.files[0] : null; - if (file) { + if (file && checkImageSize(file.name, file.size)) { onUpload(file); } }; diff --git a/src/pages/OldCommon/components/CommonDetailContainer/CommonMenu/CommonMenu.tsx b/src/pages/OldCommon/components/CommonDetailContainer/CommonMenu/CommonMenu.tsx index 2bd70b1cbb..c2fccb8072 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/CommonMenu/CommonMenu.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/CommonMenu/CommonMenu.tsx @@ -338,7 +338,6 @@ const CommonMenu: FC = (props) => { onClose={handleMenuClose} commonId={common.id} memberCount={common.memberCount} - memberCircleIds={Object.values(currentCommonMember.circles.map)} /> )} { commonId: string; memberCount: number; - memberCircleIds: string[]; onSuccessfulLeave?: () => void; isSubCommon?: boolean; } @@ -30,31 +28,18 @@ const LeaveCommonModal: FC = (props) => { onClose, commonId, memberCount, - memberCircleIds = [], onSuccessfulLeave, isSubCommon = false, } = props; - const { - loading: areMemberAmountsLoading, - fetched: areMemberAmountsFetched, - data: memberAmountsWithCircleId, - fetchCommonMembersWithCircleIdAmount, - } = useCommonMembersWithCircleIdsAmount(); const dispatch = useDispatch(); const { notify } = useNotification(); const history = useHistory(); const [isLeaving, setIsLeaving] = useState(false); const [errorText, setErrorText] = useState(""); + const [isLastMemberInCircle, setIsLastMemberInCircle] = useState(false); const user = useSelector(selectUser()); const userId = user?.uid; const isDeleteCommonRequest = memberCount === 1; - const isLoading = !isDeleteCommonRequest && !areMemberAmountsFetched; - const isLastMemberInCircle = useMemo( - () => - areMemberAmountsFetched && - memberAmountsWithCircleId.some(({ amount }) => amount <= 1), - [areMemberAmountsFetched, memberAmountsWithCircleId], - ); const commonWord = isSubCommon ? "space" : "common"; const handleLeave = useCallback(() => { @@ -79,6 +64,14 @@ const LeaveCommonModal: FC = (props) => { : ""; setIsLeaving(false); + setIsLastMemberInCircle( + Boolean( + isRequestError(error) && + error.response?.data?.error?.includes( + ErrorCode.LastInCriticalCircle, + ), + ), + ); setErrorText(errorText); if (!isFinishedSuccessfully) { @@ -96,25 +89,6 @@ const LeaveCommonModal: FC = (props) => { ); }, [dispatch, notify, history, commonId, userId]); - useEffect(() => { - if ( - isDeleteCommonRequest || - areMemberAmountsLoading || - areMemberAmountsFetched - ) { - return; - } - - fetchCommonMembersWithCircleIdAmount(commonId, memberCircleIds); - }, [ - isDeleteCommonRequest, - areMemberAmountsLoading, - areMemberAmountsFetched, - fetchCommonMembersWithCircleIdAmount, - commonId, - memberCircleIds, - ]); - const renderStep = () => { if (isDeleteCommonRequest) { return ( @@ -146,7 +120,7 @@ const LeaveCommonModal: FC = (props) => { title={`Leave ${commonWord}`} className="leave-common-modal" > - {isLoading ? : renderStep()} + {renderStep()} ); }; diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestModal.tsx b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestModal.tsx index f9c980749a..3eab9c61a6 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestModal.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestModal.tsx @@ -22,7 +22,6 @@ import MembershipRequestIntroduce from "./MembershipRequestIntroduce"; import MembershipRequestPayment from "./MembershipRequestPayment"; import MembershipRequestProgressBar from "./MembershipRequestProgressBar"; import MembershipRequestRules from "./MembershipRequestRules"; -import MembershipRequestWelcome from "./MembershipRequestWelcome"; import { MembershipRequestStage } from "./constants"; import { getSteps } from "./helpers"; import "./index.scss"; @@ -131,9 +130,7 @@ export function MembershipRequestModal(props: IProps) { const payload: IMembershipRequestData = { ...INIT_DATA, - stage: isMember - ? MembershipRequestStage.Introduce - : MembershipRequestStage.Welcome, + stage: MembershipRequestStage.Introduce }; setUserData(payload); @@ -145,13 +142,6 @@ export function MembershipRequestModal(props: IProps) { const renderCurrentStage = (stage: number) => { switch (stage) { - case MembershipRequestStage.Welcome: - return ( - - ); case MembershipRequestStage.Introduce: return ( , -) { - const screenSize = useSelector(getScreenSize()); - const isMobileView = screenSize === ScreenSize.Mobile; - const { userData, setUserData } = props; - - return ( -
- - How to join a common - -
-
- introduce -
- Introduce yourself and add your personal contribution. -
-
- -
- vote -
- Community members vote to approve your request to join. -
-
- -
- membership -
Become a equal member with an equal vote.
-
-
- - -
- -
-
-
- ); -} diff --git a/src/pages/OldCommon/components/CommonListContainer/CreateCommonModal/CreationSteps/Review/GalleryButton/GalleryButton.tsx b/src/pages/OldCommon/components/CommonListContainer/CreateCommonModal/CreationSteps/Review/GalleryButton/GalleryButton.tsx index 498845fb5a..c477204701 100644 --- a/src/pages/OldCommon/components/CommonListContainer/CreateCommonModal/CreationSteps/Review/GalleryButton/GalleryButton.tsx +++ b/src/pages/OldCommon/components/CommonListContainer/CreateCommonModal/CreationSteps/Review/GalleryButton/GalleryButton.tsx @@ -1,6 +1,7 @@ import React, { ChangeEventHandler, FC } from "react"; import classNames from "classnames"; import { ButtonIcon } from "@/shared/components"; +import { useImageSizeCheck } from "@/shared/hooks"; import GalleryIcon from "@/shared/icons/gallery.icon"; import TrashIcon from "@/shared/icons/trash.icon"; import "./index.scss"; @@ -16,6 +17,7 @@ interface GalleryButtonProps { const GalleryButton: FC = (props) => { const { onImageSelect, ariaLabel, shouldDeleteFile = false } = props; + const { checkImageSize } = useImageSizeCheck(); const className = classNames( "create-common-review-gallery-button", props.className, @@ -24,8 +26,9 @@ const GalleryButton: FC = (props) => { const handleChange: ChangeEventHandler = (event) => { const { files } = event.target; - if (files && files[0]) { - onImageSelect(files[0]); + const file = files?.[0]; + if (file && checkImageSize(file.name, file.size)) { + onImageSelect(file); } }; diff --git a/src/pages/OldCommon/components/CommonListContainer/EditCommonModal/EditSteps/Review/GalleryButton/GalleryButton.tsx b/src/pages/OldCommon/components/CommonListContainer/EditCommonModal/EditSteps/Review/GalleryButton/GalleryButton.tsx index 498845fb5a..c477204701 100644 --- a/src/pages/OldCommon/components/CommonListContainer/EditCommonModal/EditSteps/Review/GalleryButton/GalleryButton.tsx +++ b/src/pages/OldCommon/components/CommonListContainer/EditCommonModal/EditSteps/Review/GalleryButton/GalleryButton.tsx @@ -1,6 +1,7 @@ import React, { ChangeEventHandler, FC } from "react"; import classNames from "classnames"; import { ButtonIcon } from "@/shared/components"; +import { useImageSizeCheck } from "@/shared/hooks"; import GalleryIcon from "@/shared/icons/gallery.icon"; import TrashIcon from "@/shared/icons/trash.icon"; import "./index.scss"; @@ -16,6 +17,7 @@ interface GalleryButtonProps { const GalleryButton: FC = (props) => { const { onImageSelect, ariaLabel, shouldDeleteFile = false } = props; + const { checkImageSize } = useImageSizeCheck(); const className = classNames( "create-common-review-gallery-button", props.className, @@ -24,8 +26,9 @@ const GalleryButton: FC = (props) => { const handleChange: ChangeEventHandler = (event) => { const { files } = event.target; - if (files && files[0]) { - onImageSelect(files[0]); + const file = files?.[0]; + if (file && checkImageSize(file.name, file.size)) { + onImageSelect(file); } }; diff --git a/src/pages/Trustee/components/ApproveInvoicesPrompt/ApproveInvoicesPrompt.tsx b/src/pages/Trustee/components/ApproveInvoicesPrompt/ApproveInvoicesPrompt.tsx index c98162eea3..e70b9b8a22 100644 --- a/src/pages/Trustee/components/ApproveInvoicesPrompt/ApproveInvoicesPrompt.tsx +++ b/src/pages/Trustee/components/ApproveInvoicesPrompt/ApproveInvoicesPrompt.tsx @@ -8,12 +8,20 @@ interface ApproveInvoicesPromptProps { isOpen: boolean; isLoading: boolean; isFinished: boolean; + isReapproval?: boolean; onApprove: () => void; onClose: () => void; } const ApproveInvoicesPrompt: FC = (props) => { - const { isOpen, isFinished, isLoading, onApprove, onClose } = props; + const { + isOpen, + isFinished, + isLoading, + isReapproval = false, + onApprove, + onClose, + } = props; const handleApprove = () => { onApprove(); @@ -45,7 +53,9 @@ const ApproveInvoicesPrompt: FC = (props) => { <>

- Are you sure you want to approve all invoices + {isReapproval + ? "Are you sure you want to reapprove this card?" + : "Are you sure you want to approve all invoices?"}

@@ -67,7 +77,7 @@ const ApproveInvoicesPrompt: FC = (props) => { <>

- Invoices are successfully approved! + Invoices are successfully {isReapproval ? "re" : ""}approved!

- + {!isPendingReapprovalProposal && ( + + )}
)} @@ -210,6 +216,7 @@ const InvoiceAcceptanceContainer: FC = () => { isOpen={isApprovePromptOpen} isLoading={submissionState.loading} isFinished={submissionState.finished} + isReapproval={isPendingReapprovalProposal} onApprove={handleSubmit} onClose={handleApprovePromptClose} /> diff --git a/src/pages/Trustee/helpers/proposalState.ts b/src/pages/Trustee/helpers/proposalState.ts index 214dce9fce..2c811e625a 100644 --- a/src/pages/Trustee/helpers/proposalState.ts +++ b/src/pages/Trustee/helpers/proposalState.ts @@ -9,6 +9,12 @@ export const checkPendingApprovalProposal = ( proposal.data.tracker.status === FundingAllocationStatus.PENDING_INVOICE_APPROVAL; +export const checkPendingReapprovalProposal = ( + proposal: FundsAllocation, +): boolean => + proposal.data.tracker.status === + FundingAllocationStatus.PENDING_SELLER_APPROVAL; + export const checkDeclinedProposal = (proposal: FundsAllocation): boolean => proposal.data.legal.payoutDocsRejectionReason !== null && proposal.data.tracker.status === diff --git a/src/pages/common/components/ChatComponent/ChatComponent.module.scss b/src/pages/common/components/ChatComponent/ChatComponent.module.scss index 629a0feb63..73a2fb1550 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.module.scss +++ b/src/pages/common/components/ChatComponent/ChatComponent.module.scss @@ -19,7 +19,7 @@ display: flex; flex-direction: column-reverse; padding: 0.5rem 2rem 0; - padding-bottom: var(--chat-input-wrapper-height); + padding-bottom: calc(var(--chat-input-wrapper-height) + 7rem); } .emptyChat { @@ -43,7 +43,7 @@ .chatInputWrapper { display: flex; flex-shrink: 0; - padding: 1rem 1.125rem 1rem 1.125rem; + padding: 0 1.125rem; background-color: $c-input-100; border: 0.0625rem solid $c-neutrals-100; border-left-width: 0; @@ -77,6 +77,7 @@ justify-content: center; word-break: break-word; padding: 0.125rem 1.75rem 0.125rem 0.25rem; + margin: 0.3rem 0; } .messageInputRtl { @@ -89,7 +90,8 @@ } .addFilesIcon { - margin-right: 0.9375rem; + margin-bottom: 0.8rem; + margin-right: 0.5rem; height: 1.5rem; } @@ -97,7 +99,7 @@ display: flex; justify-content: center; align-items: center; - margin-left: 1rem; + margin-bottom: 0.8rem; border: none; outline: none; background: $c-input-100; @@ -117,11 +119,6 @@ $phone-breakpoint: 415px; z-index: 10000; } -.emojiContainer { - bottom: 8px; - transform: none; -} - .singleEmojiText { font-size: $xlarge; line-height: 2.8125rem; diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index be49eb8f92..8bcb77ad89 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -22,9 +22,10 @@ import { GovernanceActions, LastSeenEntity, } from "@/shared/constants"; +import { FILES_ACCEPTED_EXTENSIONS } from "@/shared/constants"; import { HotKeys } from "@/shared/constants/keyboardKeys"; import { ChatMessageToUserDiscussionMessageConverter } from "@/shared/converters"; -import { useZoomDisabling } from "@/shared/hooks"; +import { useZoomDisabling, useImageSizeCheck } from "@/shared/hooks"; import { PlusIcon, SendIcon } from "@/shared/icons"; import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; import { @@ -49,7 +50,6 @@ import { TextEditorSize, removeTextEditorEmptyEndLinesValues, countTextEditorEmojiElements, - Loader, } from "@/shared/ui-kit"; import { getUserName, hasPermission, isMobile } from "@/shared/utils"; import { @@ -69,9 +69,6 @@ import { useChatChannelChatAdapter, useDiscussionChatAdapter } from "./hooks"; import { getLastNonUserMessage } from "./utils"; import styles from "./ChatComponent.module.scss"; -const ACCEPTED_EXTENSIONS = - ".pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .txt, .rtf, .odt, .ods, .odp, .pages, .numbers, .key, .jpg, .jpeg, .png, .gif, .tiff, .bmp, .webp, .mp4, .avi, .mov, .mkv, .mpeg, .mp3, .aac, .flac, .wav, .ogg, .zip, .rar, .7z, .tar, .gz, .apk, .epub, .vcf, .xml, .csv, .json, .docm, .dot, .dotm, .dotx, .fdf, .fodp, .fods, .fodt, .pot, .potm, .potx, .ppa, .ppam, .pps, .ppsm, .ppsx, .pptm, .sldx, .xlm, .xlsb, .xlsm, .xlt, .xltm, .xltx, .xps, .mobi, .azw, .azw3, .prc, .svg, .ico, .jp2, .3gp, .3g2, .flv, .m4v, .mk3d, .mks, .mpg, .mpeg2, .mpeg4, .mts, .vob, .wmv, .m4a, .opus, .wma, .cbr, .cbz, .tgz, .apng, .m4b, .m4p, .m4r, .webm, .sh, .py, .java, .cpp, .cs, .js, .html, .css, .php, .rb, .pl, .sql"; - const BASE_CHAT_INPUT_HEIGHT = 48; interface ChatComponentInterface { @@ -127,7 +124,6 @@ export default function ChatComponent({ feedItemId, isAuthorized, isHidden = false, - isCommonMemberFetched, onMessagesAmountChange, directParent, renderChatInput: renderChatInputOuter, @@ -136,6 +132,7 @@ export default function ChatComponent({ onInternalLinkClick, }: ChatComponentInterface) { const dispatch = useDispatch(); + const { checkImageSize } = useImageSizeCheck(); useZoomDisabling(); const editorRef = useRef(null); const [inputContainerRef, { height: chatInputHeight }] = @@ -183,6 +180,7 @@ export default function ChatComponent({ const currentFilesPreview = useSelector(selectFilesPreview()); const chatContentRef = useRef(null); const chatWrapperId = useMemo(() => `chat-wrapper-${uuidv4()}`, []); + const chatInputWrapperRef = useRef(null); const [message, setMessage] = useState( parseStringToTextEditorValue(), @@ -323,15 +321,30 @@ export default function ChatComponent({ [newMessages, discussionId, dispatch], ); - const uploadFiles = (event: ChangeEvent) => { - const newFilesPreview = Array.from(event.target.files || []).map((file) => { - return { - info: file, - src: URL.createObjectURL(file), - size: file.size, - name: file.name, - }; - }); + const uploadFiles = ( + event: ChangeEvent | ClipboardEvent, + ) => { + let files: FileList | undefined | null; + if (event instanceof ClipboardEvent) { + files = event.clipboardData?.files; + } else { + files = event.target.files; + } + + const newFilesPreview = Array.from(files || []) + .map((file) => { + if (!checkImageSize(file.name, file.size)) { + return null; + } + + return { + info: file, + src: URL.createObjectURL(file), + size: file.size, + name: file.name, + }; + }) + .filter(Boolean) as FileInfo[]; dispatch( chatActions.setFilesPreview( [...(currentFilesPreview ?? []), ...newFilesPreview].slice(0, 10), @@ -567,6 +580,20 @@ export default function ChatComponent({ } }, [discussionMessages.length]); + useEffect(() => { + const handlePaste = (event) => { + if (event.clipboardData.files.length) { + uploadFiles(event); + } + }; + + chatInputWrapperRef.current?.addEventListener("paste", handlePaste); + + return () => { + chatInputWrapperRef.current?.removeEventListener("paste", handlePaste); + }; + }, []); + const renderChatInput = (): ReactNode => { const shouldHideChatInput = !isChatChannel && (!hasAccess || isHidden); @@ -600,13 +627,10 @@ export default function ChatComponent({ onChange={uploadFiles} style={{ display: "none" }} multiple - accept={ACCEPTED_EXTENSIONS} + accept={FILES_ACCEPTED_EXTENSIONS} />
; dateList: string[]; @@ -60,6 +55,7 @@ interface ChatContentInterface { onUserClick?: (userId: string) => void; onFeedItemClick?: (feedItemId: string) => void; onInternalLinkClick?: (data: InternalLinkData) => void; + isEmpty?: boolean; } const isToday = (someDate: Date) => { @@ -79,10 +75,6 @@ const ChatContent: ForwardRefRenderFunction< type, commonMember, governanceCircles, - isCommonMemberFetched, - isJoiningPending, - hasAccess, - isHidden, chatWrapperId, messages, dateList, @@ -97,6 +89,7 @@ const ChatContent: ForwardRefRenderFunction< onUserClick, onFeedItemClick, onInternalLinkClick, + isEmpty, }, chatContentRef, ) => { @@ -193,23 +186,6 @@ const ChatContent: ForwardRefRenderFunction< [scrollToContainerBottom], ); - if (!hasAccess || isHidden) { - return ( -