wip ui
This commit is contained in:
+9
-1
@@ -22,6 +22,7 @@ export interface PublicKeyMismatch {
|
||||
*/
|
||||
export function usePublicKeyRegistry(
|
||||
accessesPublicKeysPerUser: Record<string, string> | undefined,
|
||||
currentUserId?: string,
|
||||
) {
|
||||
const [mismatches, setMismatches] = useState<PublicKeyMismatch[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -43,6 +44,13 @@ export function usePublicKeyRegistry(
|
||||
for (const [userId, currentKey] of Object.entries(
|
||||
accessesPublicKeysPerUser!,
|
||||
)) {
|
||||
// Skip the current user — they know about their own key changes
|
||||
if (currentUserId && userId === currentUserId) {
|
||||
// Still store the key so it stays up to date locally
|
||||
await db.put(STORE_KNOWN_PUBLIC_KEYS, currentKey, `user:${userId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const knownKey: string | undefined = await db.get(
|
||||
STORE_KNOWN_PUBLIC_KEYS,
|
||||
`user:${userId}`,
|
||||
@@ -74,7 +82,7 @@ export function usePublicKeyRegistry(
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [accessesPublicKeysPerUser]);
|
||||
}, [accessesPublicKeysPerUser, currentUserId]);
|
||||
|
||||
const acceptNewKey = useCallback(
|
||||
async (userId: string) => {
|
||||
|
||||
@@ -43,15 +43,6 @@ export const BoutonShare = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const warningIcon = hasKeyWarning ? (
|
||||
<Icon
|
||||
iconName="warning"
|
||||
$color="var(--c--theme--colors--warning-600)"
|
||||
$size="sm"
|
||||
aria-label={t('Public key mismatch detected')}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
if (hasAccesses) {
|
||||
return (
|
||||
<Box
|
||||
@@ -66,13 +57,13 @@ export const BoutonShare = ({
|
||||
}
|
||||
`}
|
||||
>
|
||||
{warningIcon}
|
||||
<Button
|
||||
aria-label={t('Share button')}
|
||||
variant="secondary"
|
||||
color={hasKeyWarning ? 'warning' : undefined}
|
||||
icon={
|
||||
<Icon
|
||||
iconName="group"
|
||||
iconName={hasKeyWarning ? 'warning' : 'group'}
|
||||
$color="inherit"
|
||||
variant="filled"
|
||||
disabled={isDisabled}
|
||||
@@ -89,17 +80,17 @@ export const BoutonShare = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box $direction="row" $align="center" $gap="4px">
|
||||
{warningIcon}
|
||||
<Button
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
onClick={open}
|
||||
size="medium"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{t('Share')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Button
|
||||
color={hasKeyWarning ? 'warning' : 'brand'}
|
||||
variant="tertiary"
|
||||
icon={
|
||||
hasKeyWarning ? <Icon iconName="warning" $color="inherit" /> : undefined
|
||||
}
|
||||
onClick={open}
|
||||
size="medium"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{t('Share')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -80,6 +80,7 @@ export const DocToolBox = ({
|
||||
|
||||
const { hasMismatches: hasKeyWarnings } = usePublicKeyRegistry(
|
||||
doc.accesses_public_keys_per_user,
|
||||
encryptionSettings?.userId,
|
||||
);
|
||||
|
||||
const { isSmallMobile, isMobile } = useResponsiveStore();
|
||||
@@ -239,10 +240,25 @@ export const DocToolBox = ({
|
||||
$gap={spacingsTokens['2xs']}
|
||||
>
|
||||
{doc.is_encrypted && (
|
||||
<>
|
||||
[chiffrement activé]
|
||||
{/* TODO */}
|
||||
</>
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="5px"
|
||||
$css={css`
|
||||
margin-right: 5px;
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
iconName="lock"
|
||||
$size="md"
|
||||
$color={colorsTokens['success-600']}
|
||||
/>
|
||||
<span
|
||||
style={{ fontSize: '0.8rem', color: colorsTokens['success-600'] }}
|
||||
>
|
||||
{t('Encrypted')}
|
||||
</span>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<BoutonShare
|
||||
@@ -332,18 +348,18 @@ export const DocToolBox = ({
|
||||
)}
|
||||
{isModalRemoveEncryptionOpen &&
|
||||
documentEncryptionSettings?.documentSymmetricKey && (
|
||||
<ModalRemoveDocEncryption
|
||||
doc={doc}
|
||||
symmetricKey={documentEncryptionSettings.documentSymmetricKey}
|
||||
onClose={() => setIsModalRemoveEncryptionOpen(false)}
|
||||
onSuccess={() => {
|
||||
//
|
||||
// TODO: probably it should make an hard refresh to get the setup
|
||||
// but it should before register content in database with clean accesses, and broadcast the information through websocket
|
||||
//
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ModalRemoveDocEncryption
|
||||
doc={doc}
|
||||
symmetricKey={documentEncryptionSettings.documentSymmetricKey}
|
||||
onClose={() => setIsModalRemoveEncryptionOpen(false)}
|
||||
onSuccess={() => {
|
||||
//
|
||||
// TODO: probably it should make an hard refresh to get the setup
|
||||
// but it should before register content in database with clean accesses, and broadcast the information through websocket
|
||||
//
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{selectHistoryModal.isOpen && (
|
||||
<ModalSelectVersion
|
||||
onClose={() => selectHistoryModal.close()}
|
||||
|
||||
+3
-4
@@ -95,10 +95,9 @@
|
||||
stroke="white"
|
||||
stroke-width="1.21628"
|
||||
/>
|
||||
<path
|
||||
d="M17.9595 16.5L18.9325 17.473V18.2028H16.8648V20.5137L16.4999 20.8786L16.1351 20.5137V18.2028H14.0674V17.473L15.0404 16.5V13.8242H14.5539V13.0944H18.446V13.8242H17.9595V16.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
<g transform="translate(16.5 16.5) scale(0.27)">
|
||||
<path transform="translate(-12 -12)" d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
|
||||
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.8 KiB |
+21
-8
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useDate } from '@/hooks/useDate';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
@@ -59,23 +59,24 @@ export const SimpleDocItem = ({
|
||||
$css={css`
|
||||
background-color: transparent;
|
||||
filter: drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.05));
|
||||
position: relative;
|
||||
`}
|
||||
$padding={`${spacingsTokens['3xs']} 0`}
|
||||
data-testid={isPinned ? `doc-pinned-${doc.id}` : undefined}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{isPinned ? (
|
||||
<PinnedDocumentIcon
|
||||
aria-hidden="true"
|
||||
data-testid="doc-pinned-icon"
|
||||
color="var(--c--contextuals--content--semantic--info--tertiary)"
|
||||
/>
|
||||
) : isEncrypted ? (
|
||||
{isEncrypted ? (
|
||||
<EncryptedDocumentIcon
|
||||
aria-hidden="true"
|
||||
data-testid="doc-encryption-icon"
|
||||
color="var(--c--contextuals--content--semantic--info--tertiary)"
|
||||
/>
|
||||
) : isPinned ? (
|
||||
<PinnedDocumentIcon
|
||||
aria-hidden="true"
|
||||
data-testid="doc-pinned-icon"
|
||||
color="var(--c--contextuals--content--semantic--info--tertiary)"
|
||||
/>
|
||||
) : isChild ? (
|
||||
<ChildDocument
|
||||
aria-hidden="true"
|
||||
@@ -91,6 +92,18 @@ export const SimpleDocItem = ({
|
||||
color="var(--c--contextuals--content--semantic--info--tertiary)"
|
||||
/>
|
||||
)}
|
||||
{isPinned && isEncrypted && (
|
||||
<Icon
|
||||
iconName="push_pin"
|
||||
$size="12px"
|
||||
$color="var(--c--contextuals--content--semantic--info--tertiary)"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: -4,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box $justify="center" $overflow="auto">
|
||||
<Text
|
||||
|
||||
@@ -148,7 +148,11 @@ export const QuickSearchGroupMember = ({
|
||||
<DocShareMemberItem
|
||||
doc={doc}
|
||||
access={access}
|
||||
suffix={hasMismatch ? t('DIFFERENT PUBLIC KEY') : undefined}
|
||||
suffix={
|
||||
hasMismatch
|
||||
? t('DIFFERENT PUBLIC KEY, PLEASE VERIFY')
|
||||
: undefined
|
||||
}
|
||||
onSuffixClick={
|
||||
hasMismatch
|
||||
? () => setMismatchUserId(access.user.id)
|
||||
|
||||
+28
-27
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalSize,
|
||||
} from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Button, Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -24,7 +20,7 @@ import {
|
||||
import { usePublicKeyRegistry } from '@/docs/doc-collaboration';
|
||||
import type { PublicKeyMismatch } from '@/docs/doc-collaboration/hook/usePublicKeyRegistry';
|
||||
import { Doc } from '@/docs/doc-management';
|
||||
import { User } from '@/features/auth';
|
||||
import { User, useAuth } from '@/features/auth';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
import { isValidEmail } from '@/utils';
|
||||
|
||||
@@ -80,8 +76,10 @@ export const DocShareModal = ({
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { user } = useAuth();
|
||||
const { mismatches: keyMismatches, acceptNewKey } = usePublicKeyRegistry(
|
||||
doc.accesses_public_keys_per_user,
|
||||
user?.id,
|
||||
);
|
||||
const keyMismatchUserIds = useMemo(
|
||||
() => new Set(keyMismatches.map((m) => m.userId)),
|
||||
@@ -448,10 +446,10 @@ const QuickSearchInviteInputSection = ({
|
||||
const getUserSuffix = useCallback(
|
||||
(user: User): string | undefined => {
|
||||
if (keyMismatchUserIds?.has(user.id)) {
|
||||
return t('DIFFERENT PUBLIC KEY');
|
||||
return t('DIFFERENT PUBLIC KEY, PLEASE VERIFY');
|
||||
}
|
||||
if (isEncrypted && !user.encryption_public_key) {
|
||||
return t(`(chiffrement non activé)`);
|
||||
return t(`(encryption not enabled)`);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
@@ -526,25 +524,28 @@ const QuickSearchInviteInputSection = ({
|
||||
</Box>
|
||||
</Modal>
|
||||
)}
|
||||
{mismatchUser && (() => {
|
||||
const mismatch = keyMismatches?.find((m) => m.userId === mismatchUser.id);
|
||||
return (
|
||||
<ModalKeyMismatch
|
||||
onClose={() => setMismatchUser(null)}
|
||||
onAcceptKey={
|
||||
acceptNewKey
|
||||
? () => {
|
||||
void acceptNewKey(mismatchUser.id).then(() => {
|
||||
onSelect(mismatchUser);
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
knownKey={mismatch?.knownKey}
|
||||
currentKey={mismatch?.currentKey}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
{mismatchUser &&
|
||||
(() => {
|
||||
const mismatch = keyMismatches?.find(
|
||||
(m) => m.userId === mismatchUser.id,
|
||||
);
|
||||
return (
|
||||
<ModalKeyMismatch
|
||||
onClose={() => setMismatchUser(null)}
|
||||
onAcceptKey={
|
||||
acceptNewKey
|
||||
? () => {
|
||||
void acceptNewKey(mismatchUser.id).then(() => {
|
||||
onSelect(mismatchUser);
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
knownKey={mismatch?.knownKey}
|
||||
currentKey={mismatch?.currentKey}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -82,7 +82,9 @@ export const SearchUserRow = ({
|
||||
</Text>
|
||||
)}
|
||||
{fingerprint && (
|
||||
<Badge style={{ gap: '0.3rem', margin: '5px 0' }}>
|
||||
<Badge
|
||||
style={{ width: 'fit-content', gap: '0.3rem', margin: '5px 0' }}
|
||||
>
|
||||
<Text
|
||||
$size="xs"
|
||||
$weight="600"
|
||||
|
||||
@@ -7,8 +7,10 @@ import { css } from 'styled-components';
|
||||
import { Box, Icon, StyledLink, Text } from '@/components';
|
||||
import { useConfig } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { usePublicKeyRegistry } from '@/docs/doc-collaboration';
|
||||
import { Doc, LinkReach, SimpleDocItem } from '@/docs/doc-management';
|
||||
import { DocShareModal } from '@/docs/doc-share';
|
||||
import { useAuth } from '@/features/auth';
|
||||
import { useDate } from '@/hooks';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
@@ -33,6 +35,11 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
||||
const { flexLeft, flexRight } = useResponsiveDocGrid();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const shareModal = useModal();
|
||||
const { user } = useAuth();
|
||||
const { hasMismatches: hasKeyWarning } = usePublicKeyRegistry(
|
||||
doc.accesses_public_keys_per_user,
|
||||
user?.id,
|
||||
);
|
||||
const isPublic = doc.link_reach === LinkReach.PUBLIC;
|
||||
const isAuthenticated = doc.link_reach === LinkReach.AUTHENTICATED;
|
||||
const isShared = isPublic || isAuthenticated;
|
||||
@@ -181,6 +188,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
||||
doc={doc}
|
||||
handleClick={handleShareClick}
|
||||
disabled={isInTrashbin}
|
||||
hasKeyWarning={hasKeyWarning}
|
||||
/>
|
||||
)}
|
||||
{isInTrashbin ? (
|
||||
|
||||
+5
-3
@@ -8,11 +8,13 @@ type Props = {
|
||||
doc: Doc;
|
||||
handleClick: () => void;
|
||||
disabled: boolean;
|
||||
hasKeyWarning?: boolean;
|
||||
};
|
||||
export const DocsGridItemSharedButton = ({
|
||||
doc,
|
||||
handleClick,
|
||||
disabled,
|
||||
hasKeyWarning,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const sharedCount = doc.nb_accesses_direct;
|
||||
@@ -44,14 +46,14 @@ export const DocsGridItemSharedButton = ({
|
||||
event.stopPropagation();
|
||||
handleClick();
|
||||
}}
|
||||
color="brand"
|
||||
color={hasKeyWarning ? 'warning' : 'brand'}
|
||||
variant="secondary"
|
||||
size="nano"
|
||||
icon={
|
||||
<Icon
|
||||
$theme="brand"
|
||||
$theme={hasKeyWarning ? 'warning' : 'brand'}
|
||||
$variation="secondary"
|
||||
iconName="group"
|
||||
iconName={hasKeyWarning ? 'warning' : 'group'}
|
||||
disabled={disabled}
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user