diff --git a/src/App.scss b/src/App.scss index c4fa323..29e7e8f 100644 --- a/src/App.scss +++ b/src/App.scss @@ -100,10 +100,10 @@ li { // - first-child Header height, default body padding, and center content border (10px) and padding (10px) // - others We don't include border and padding and scroll to the top of the image &:first-child { - scroll-margin-top: $header-height + $body-vertical-padding + 20px; + scroll-margin-top: $body-vertical-padding + 20px; } &:not(:first-child) { - scroll-margin-top: $header-height + $body-vertical-padding; + scroll-margin-top: $body-vertical-padding; } } @@ -148,7 +148,7 @@ li { justify-content: start; align-items: start; - scroll-margin-top: $header-height + $body-vertical-padding; + scroll-margin-top: $body-vertical-padding; } [data-dev='true'] { diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index aaabbd3..ff68469 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -28,14 +28,17 @@ import { } from '../../routes' import { clearAuthToken, + getProfileUsername, hexToNpub, - saveNsecBunkerDelegatedKey, - shorten + saveNsecBunkerDelegatedKey } from '../../utils' import styles from './style.module.scss' import { setUserRobotImage } from '../../store/userRobotImage/action' import { Container } from '../Container' import { ButtonIcon } from '../ButtonIcon' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faClose } from '@fortawesome/free-solid-svg-icons' +import useMediaQuery from '@mui/material/useMediaQuery' const metadataController = MetadataController.getInstance() @@ -55,9 +58,8 @@ export const AppBar = () => { useEffect(() => { if (metadataState) { if (metadataState.content) { - const { picture, display_name, name } = JSON.parse( - metadataState.content - ) + const profileMetadata = JSON.parse(metadataState.content) + const { picture } = profileMetadata if (picture || userRobotImage) { setUserAvatar(picture || userRobotImage) @@ -67,7 +69,7 @@ export const AppBar = () => { ? hexToNpub(authState.usersPubkey) : '' - setUsername(shorten(display_name || name || npub, 7)) + setUsername(getProfileUsername(npub, profileMetadata)) } else { setUserAvatar(userRobotImage || '') setUsername('') @@ -118,125 +120,152 @@ export const AppBar = () => { } const isAuthenticated = authState?.loggedIn === true + const matches = useMediaQuery('(max-width:767px)') + const [isBannerVisible, setIsBannerVisible] = useState(true) + const handleBannerHide = () => { + setIsBannerVisible(false) + } + return ( - - - - - Logo navigate('/')} /> - - - - {!isAuthenticated && ( + <> + {isAuthenticated && isBannerVisible && ( +
+ +
+

+ SIGit is currently Alpha software (available for internal + testing), use at your own risk! +

- )} +
+
+
+ )} + + + + + Logo navigate('/')} /> + - {isAuthenticated && ( - <> - - + {!isAuthenticated && ( + + )} - navigate(appPrivateRoutes.settings) + {isAuthenticated && ( + <> + + - Settings - - { - setAnchorElUser(null) - - navigate(appPublicRoutes.verify) - }} - sx={{ - justifyContent: 'center' - }} - > - Verify - - + {username} + + - Source + Profile - - - Logout - - - - )} - - - - + { + setAnchorElUser(null) + + navigate(appPrivateRoutes.settings) + }} + sx={{ + justifyContent: 'center' + }} + > + Settings + + { + setAnchorElUser(null) + + navigate(appPublicRoutes.verify) + }} + sx={{ + justifyContent: 'center' + }} + > + Verify + + + + Source + + + + Logout + + + + )} +
+
+
+
+ ) } diff --git a/src/components/AppBar/style.module.scss b/src/components/AppBar/style.module.scss index 6b15c93..d8c202f 100644 --- a/src/components/AppBar/style.module.scss +++ b/src/components/AppBar/style.module.scss @@ -34,3 +34,42 @@ justify-content: flex-end; } } + +.banner { + color: #ffffff; + background-color: $primary-main; +} + +.bannerInner { + display: flex; + gap: 10px; + padding-block: 10px; + z-index: 1; + + width: 100%; + + justify-content: space-between; + flex-direction: row; + + button { + min-width: 44px; + color: inherit; + } + + &:hover, + &.active, + &:focus-within { + background: $primary-main; + color: inherit; + + button { + color: inherit; + } + } +} + +.bannerText { + margin-left: 54px; + flex-grow: 1; + text-align: center; +} diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx index 7c1227d..076c6fb 100644 --- a/src/components/DrawPDFFields/index.tsx +++ b/src/components/DrawPDFFields/index.tsx @@ -11,16 +11,16 @@ import styles from './style.module.scss' import React, { useEffect, useState } from 'react' import { ProfileMetadata, User, UserRole } from '../../types' import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing' -import { truncate } from 'lodash' -import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils' -import { getSigitFile, SigitFile } from '../../utils/file' +import { hexToNpub, npubToHex, getProfileUsername } from '../../utils' +import { SigitFile } from '../../utils/file' import { getToolboxLabelByMarkType } from '../../utils/mark' import { FileDivider } from '../FileDivider' import { ExtensionFileBox } from '../ExtensionFileBox' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf' import { useScale } from '../../hooks/useScale' import { AvatarIconButton } from '../UserAvatarIconButton' -import { LoadingSpinner } from '../LoadingSpinner' +import { UserAvatar } from '../UserAvatar' +import _ from 'lodash' const DEFAULT_START_SIZE = { width: 140, @@ -28,52 +28,50 @@ const DEFAULT_START_SIZE = { } as const interface Props { - selectedFiles: File[] users: User[] metadata: { [key: string]: ProfileMetadata } - onDrawFieldsChange: (sigitFiles: SigitFile[]) => void + sigitFiles: SigitFile[] + setSigitFiles: React.Dispatch> selectedTool?: DrawTool } export const DrawPDFFields = (props: Props) => { - const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props - const { to, from } = useScale() + const { selectedTool, sigitFiles, setSigitFiles, users } = props - const [sigitFiles, setSigitFiles] = useState([]) - const [parsingPdf, setIsParsing] = useState(false) + const signers = users.filter((u) => u.role === UserRole.signer) + const defaultSignerNpub = signers.length ? hexToNpub(signers[0].pubkey) : '' + const [lastSigner, setLastSigner] = useState(defaultSignerNpub) + /** + * Return first pubkey that is present in the signers list + * @param pubkeys + * @returns available pubkey or empty string + */ + const getAvailableSigner = (...pubkeys: string[]) => { + const availableSigner: string | undefined = pubkeys.find((pubkey) => + signers.some((s) => s.pubkey === npubToHex(pubkey)) + ) + return availableSigner || '' + } + + const { to, from } = useScale() const [mouseState, setMouseState] = useState({ clicked: false }) - const [activeDrawField, setActiveDrawField] = useState() - - useEffect(() => { - if (selectedFiles) { - /** - * Reads the binary files and converts to internal file type - * and sets to a state (adds images if it's a PDF) - */ - const parsePages = async () => { - const files = await settleAllFullfilfedPromises( - selectedFiles, - getSigitFile - ) - - setSigitFiles(files) - } - - setIsParsing(true) - - parsePages().finally(() => { - setIsParsing(false) - }) - } - }, [selectedFiles]) - - useEffect(() => { - if (sigitFiles) onDrawFieldsChange(sigitFiles) - }, [onDrawFieldsChange, sigitFiles]) + const [activeDrawnField, setActiveDrawnField] = useState<{ + fileIndex: number + pageIndex: number + drawnFieldIndex: number + }>() + const isActiveDrawnField = ( + fileIndex: number, + pageIndex: number, + drawnFieldIndex: number + ) => + activeDrawnField?.fileIndex === fileIndex && + activeDrawnField?.pageIndex === pageIndex && + activeDrawnField?.drawnFieldIndex === drawnFieldIndex /** * Drawing events @@ -100,7 +98,12 @@ export const DrawPDFFields = (props: Props) => { * @param event Pointer event * @param page PdfPage where press happened */ - const handlePointerDown = (event: React.PointerEvent, page: PdfPage) => { + const handlePointerDown = ( + event: React.PointerEvent, + page: PdfPage, + fileIndex: number, + pageIndex: number + ) => { // Proceed only if left click if (event.button !== 0) return @@ -115,12 +118,17 @@ export const DrawPDFFields = (props: Props) => { top: to(page.width, y), width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width, height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height, - counterpart: '', + counterpart: getAvailableSigner(lastSigner, defaultSignerNpub), type: selectedTool.identifier } page.drawnFields.push(newField) + setActiveDrawnField({ + fileIndex, + pageIndex, + drawnFieldIndex: page.drawnFields.length - 1 + }) setMouseState((prev) => { return { ...prev, @@ -189,6 +197,8 @@ export const DrawPDFFields = (props: Props) => { */ const handleDrawnFieldPointerDown = ( event: React.PointerEvent, + fileIndex: number, + pageIndex: number, drawnFieldIndex: number ) => { event.stopPropagation() @@ -198,7 +208,7 @@ export const DrawPDFFields = (props: Props) => { const drawingRectangleCoords = getPointerCoordinates(event) - setActiveDrawField(drawnFieldIndex) + setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex }) setMouseState({ dragging: true, clicked: false, @@ -254,13 +264,15 @@ export const DrawPDFFields = (props: Props) => { */ const handleResizePointerDown = ( event: React.PointerEvent, + fileIndex: number, + pageIndex: number, drawnFieldIndex: number ) => { // Proceed only if left click if (event.button !== 0) return event.stopPropagation() - setActiveDrawField(drawnFieldIndex) + setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex }) setMouseState({ resizing: true }) @@ -369,7 +381,7 @@ export const DrawPDFFields = (props: Props) => { handlePointerMove(event, page) }} onPointerDown={(event) => { - handlePointerDown(event, page) + handlePointerDown(event, page, fileIndex, pageIndex) }} draggable="false" src={page.image} @@ -381,7 +393,12 @@ export const DrawPDFFields = (props: Props) => {
- handleDrawnFieldPointerDown(event, drawnFieldIndex) + handleDrawnFieldPointerDown( + event, + fileIndex, + pageIndex, + drawnFieldIndex + ) } onPointerMove={(event) => { handleDrawnFieldPointerMove(event, drawnField, page.width) @@ -402,7 +419,11 @@ export const DrawPDFFields = (props: Props) => { touchAction: 'none', opacity: mouseState.dragging && - activeDrawField === drawnFieldIndex + isActiveDrawnField( + fileIndex, + pageIndex, + drawnFieldIndex + ) ? 0.8 : undefined }} @@ -419,7 +440,12 @@ export const DrawPDFFields = (props: Props) => {
- handleResizePointerDown(event, drawnFieldIndex) + handleResizePointerDown( + event, + fileIndex, + pageIndex, + drawnFieldIndex + ) } onPointerMove={(event) => { handleResizePointerMove(event, drawnField, page.width) @@ -428,7 +454,11 @@ export const DrawPDFFields = (props: Props) => { style={{ background: mouseState.resizing && - activeDrawField === drawnFieldIndex + isActiveDrawnField( + fileIndex, + pageIndex, + drawnFieldIndex + ) ? 'var(--primary-main)' : undefined }} @@ -446,56 +476,59 @@ export const DrawPDFFields = (props: Props) => { > -
- - Counterpart - { + drawnField.counterpart = event.target.value + setLastSigner(event.target.value) + refreshPdfFiles() + }} + labelId="counterparts" + label="Counterparts" + sx={{ + background: 'white' + }} + renderValue={(value) => + renderCounterpartValue(value) + } + > + {signers.map((signer, index) => { + const npub = hexToNpub(signer.pubkey) + const metadata = props.metadata[signer.pubkey] + const displayValue = getProfileUsername( + npub, + metadata + ) return ( - + { ) })} - - -
+ + + + )} ) })} @@ -524,28 +558,19 @@ export const DrawPDFFields = (props: Props) => { ) } - const renderCounterpartValue = (value: string) => { - const user = users.find((u) => u.pubkey === npubToHex(value)) - if (user) { - let displayValue = truncate(value, { - length: 16 - }) + const renderCounterpartValue = (npub: string) => { + let displayValue = _.truncate(npub, { length: 16 }) - const metadata = props.metadata[user.pubkey] + const signer = signers.find((u) => u.pubkey === npubToHex(npub)) + if (signer) { + const metadata = props.metadata[signer.pubkey] + displayValue = getProfileUsername(npub, metadata) - if (metadata) { - displayValue = truncate( - metadata.name || metadata.display_name || metadata.username || value, - { - length: 16 - } - ) - } return ( - <> +
{ }} /> {displayValue} - +
) } - return value - } - - if (parsingPdf) { - return - } - - if (!sigitFiles.length) { - return '' + return displayValue } return ( @@ -589,7 +606,7 @@ export const DrawPDFFields = (props: Props) => { )} - {i < selectedFiles.length - 1 && } + {i < sigitFiles.length - 1 && } ) })} diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss index 13afb9f..8c0b3b8 100644 --- a/src/components/DrawPDFFields/style.module.scss +++ b/src/components/DrawPDFFields/style.module.scss @@ -84,3 +84,14 @@ padding: 5px 0; } } + +.counterpartSelectValue { + display: flex; +} + +.counterpartAvatar { + img { + width: 21px; + height: 21px; + } +} diff --git a/src/components/PDFView/PdfPageItem.tsx b/src/components/PDFView/PdfPageItem.tsx index bbf6e4e..5d4be7d 100644 --- a/src/components/PDFView/PdfPageItem.tsx +++ b/src/components/PDFView/PdfPageItem.tsx @@ -33,7 +33,8 @@ const PdfPageItem = ({ useEffect(() => { if (selectedMark !== null && !!markRefs.current[selectedMark.id]) { markRefs.current[selectedMark.id]?.scrollIntoView({ - behavior: 'smooth' + behavior: 'smooth', + block: 'center' }) } }, [selectedMark]) diff --git a/src/components/UserAvatar/index.tsx b/src/components/UserAvatar/index.tsx index 0ea1fc1..4c49e33 100644 --- a/src/components/UserAvatar/index.tsx +++ b/src/components/UserAvatar/index.tsx @@ -5,7 +5,7 @@ import { AvatarIconButton } from '../UserAvatarIconButton' import { Link } from 'react-router-dom' import { useProfileMetadata } from '../../hooks/useProfileMetadata' import { Tooltip } from '@mui/material' -import { shorten } from '../../utils' +import { getProfileUsername } from '../../utils' import { TooltipChild } from '../TooltipChild' interface UserAvatarProps { @@ -22,7 +22,7 @@ export const UserAvatar = ({ isNameVisible = false }: UserAvatarProps) => { const profile = useProfileMetadata(pubkey) - const name = profile?.display_name || profile?.name || shorten(pubkey) + const name = getProfileUsername(pubkey, profile) const image = profile?.picture return ( diff --git a/src/layouts/StickySideColumns.module.scss b/src/layouts/StickySideColumns.module.scss index a116720..5b6d890 100644 --- a/src/layouts/StickySideColumns.module.scss +++ b/src/layouts/StickySideColumns.module.scss @@ -42,15 +42,22 @@ .sides { @media only screen and (min-width: 768px) { position: sticky; - top: $header-height + $body-vertical-padding; + top: $body-vertical-padding; } > :first-child { + // We want to keep header on smaller devices at all times max-height: calc( 100dvh - $header-height - $body-vertical-padding * 2 - $tabs-height ); + + @media only screen and (min-width: 768px) { + max-height: calc(100dvh - $body-vertical-padding * 2); + } } } +// Adjust the content scroll on smaller screens +// Make sure only the inner tab is scrolling .scrollAdjust { @media only screen and (max-width: 767px) { max-height: calc( diff --git a/src/layouts/style.module.scss b/src/layouts/style.module.scss index 6c8aa59..1ad8647 100644 --- a/src/layouts/style.module.scss +++ b/src/layouts/style.module.scss @@ -3,5 +3,5 @@ .main { flex-grow: 1; - padding: $header-height + $body-vertical-padding 0 $body-vertical-padding 0; + padding: $body-vertical-padding 0 $body-vertical-padding 0; } diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index dd17450..59fdb4f 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -39,7 +39,8 @@ import { signEventForMetaFile, updateUsersAppData, uploadToFileStorage, - DEFAULT_TOOLBOX + DEFAULT_TOOLBOX, + settleAllFullfilfedPromises } from '../../utils' import { Container } from '../../components/Container' import fileListStyles from '../../components/FileList/style.module.scss' @@ -60,7 +61,8 @@ import { faTrash, faUpload } from '@fortawesome/free-solid-svg-icons' -import { SigitFile } from '../../utils/file.ts' +import { getSigitFile, SigitFile } from '../../utils/file.ts' +import _ from 'lodash' export const CreatePage = () => { const navigate = useNavigate() @@ -106,9 +108,31 @@ export const CreatePage = () => { {} ) const [drawnFiles, setDrawnFiles] = useState([]) + const [parsingPdf, setIsParsing] = useState(false) + useEffect(() => { + if (selectedFiles) { + /** + * Reads the binary files and converts to internal file type + * and sets to a state (adds images if it's a PDF) + */ + const parsePages = async () => { + const files = await settleAllFullfilfedPromises( + selectedFiles, + getSigitFile + ) + + setDrawnFiles(files) + } + + setIsParsing(true) + + parsePages().finally(() => { + setIsParsing(false) + }) + } + }, [selectedFiles]) const [selectedTool, setSelectedTool] = useState() - const [toolbox] = useState(DEFAULT_TOOLBOX) /** * Changes the drawing tool @@ -282,6 +306,19 @@ export const CreatePage = () => { const handleRemoveUser = (pubkey: string) => { setUsers((prev) => prev.filter((user) => user.pubkey !== pubkey)) + + // Set counterpart to '' + const drawnFilesCopy = _.cloneDeep(drawnFiles) + drawnFilesCopy.forEach((s) => { + s.pages?.forEach((p) => { + p.drawnFields.forEach((d) => { + if (d.counterpart === hexToNpub(pubkey)) { + d.counterpart = '' + } + }) + }) + }) + setDrawnFiles(drawnFilesCopy) } /** @@ -301,7 +338,15 @@ export const CreatePage = () => { const handleSelectFiles = (event: React.ChangeEvent) => { if (event.target.files) { - setSelectedFiles(Array.from(event.target.files)) + // Get the uploaded files + const files = Array.from(event.target.files) + + // Remove duplicates based on the file.name + setSelectedFiles((p) => + [...p, ...files].filter( + (file, i, array) => i === array.findIndex((t) => t.name === file.name) + ) + ) } } @@ -716,10 +761,6 @@ export const CreatePage = () => { } } - const onDrawFieldsChange = (sigitFiles: SigitFile[]) => { - setDrawnFiles(sigitFiles) - } - if (authUrl) { return (