internal: incomplete vault systems behind feature flag (#2340)
This commit is contained in:
@@ -10,6 +10,7 @@ import Listed from './Panes/Listed/Listed'
|
||||
import HelpAndFeedback from './Panes/HelpFeedback'
|
||||
import { PreferencesProps } from './PreferencesProps'
|
||||
import WhatsNew from './Panes/WhatsNew/WhatsNew'
|
||||
import Vaults from './Panes/Vaults/Vaults'
|
||||
|
||||
const PaneSelector: FunctionComponent<PreferencesProps & { menu: PreferencesMenu }> = ({
|
||||
menu,
|
||||
@@ -40,6 +41,8 @@ const PaneSelector: FunctionComponent<PreferencesProps & { menu: PreferencesMenu
|
||||
application={application}
|
||||
/>
|
||||
)
|
||||
case 'vaults':
|
||||
return <Vaults />
|
||||
case 'backups':
|
||||
return <Backups application={application} viewControllerManager={viewControllerManager} />
|
||||
case 'listed':
|
||||
|
||||
@@ -109,7 +109,7 @@ const DataBackups = ({ application, viewControllerManager }: Props) => {
|
||||
const performImport = async (data: BackupFile) => {
|
||||
setIsImportDataLoading(true)
|
||||
|
||||
const result = await application.mutator.importData(data)
|
||||
const result = await application.importData(data)
|
||||
|
||||
setIsImportDataLoading(false)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const PackageEntry: FunctionComponent<PackageEntryProps> = ({ application, exten
|
||||
const toggleOfflineOnly = () => {
|
||||
const newOfflineOnly = !offlineOnly
|
||||
setOfflineOnly(newOfflineOnly)
|
||||
application.mutator
|
||||
application
|
||||
.changeAndSaveItem<ComponentMutator>(extension, (mutator) => {
|
||||
mutator.offlineOnly = newOfflineOnly
|
||||
})
|
||||
@@ -49,7 +49,7 @@ const PackageEntry: FunctionComponent<PackageEntryProps> = ({ application, exten
|
||||
|
||||
const changeExtensionName = (newName: string) => {
|
||||
setExtensionName(newName)
|
||||
application.mutator
|
||||
application
|
||||
.changeAndSaveItem<ComponentMutator>(extension, (mutator) => {
|
||||
mutator.name = newName
|
||||
})
|
||||
|
||||
@@ -53,6 +53,7 @@ const PackagesPreferencesSection: FunctionComponent<Props> = ({
|
||||
.then(async (shouldRemove: boolean) => {
|
||||
if (shouldRemove) {
|
||||
await application.mutator.deleteItem(extension)
|
||||
void application.sync.sync()
|
||||
setExtensions(loadExtensions(application))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,7 +12,9 @@ type Props = {
|
||||
export const ShouldPersistNoteStateKey = 'ShouldPersistNoteState'
|
||||
|
||||
const Persistence = ({ application }: Props) => {
|
||||
const [shouldPersistNoteState, setShouldPersistNoteState] = useState(application.getValue(ShouldPersistNoteStateKey))
|
||||
const [shouldPersistNoteState, setShouldPersistNoteState] = useState(
|
||||
application.getValue<boolean>(ShouldPersistNoteStateKey),
|
||||
)
|
||||
|
||||
const toggleStatePersistence = (shouldPersist: boolean) => {
|
||||
application.setValue(ShouldPersistNoteStateKey, shouldPersist)
|
||||
|
||||
@@ -88,7 +88,7 @@ export class EditSmartViewModalController {
|
||||
|
||||
this.setIsSaving(true)
|
||||
|
||||
await this.application.mutator.changeAndSaveItem<SmartViewMutator>(this.view, (mutator) => {
|
||||
await this.application.changeAndSaveItem<SmartViewMutator>(this.view, (mutator) => {
|
||||
mutator.title = this.title
|
||||
mutator.iconString = (this.icon as string) || SmartViewDefaultIconName
|
||||
mutator.predicate = JSON.parse(this.predicateJson) as PredicateJsonForm
|
||||
@@ -111,7 +111,10 @@ export class EditSmartViewModalController {
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
if (shouldDelete) {
|
||||
this.application.mutator.deleteItem(view).catch(console.error)
|
||||
this.application.mutator
|
||||
.deleteItem(view)
|
||||
.then(() => this.application.sync.sync())
|
||||
.catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,10 +47,13 @@ const SmartViews = ({ application, featuresController }: Props) => {
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
if (shouldDelete) {
|
||||
application.mutator.deleteItem(view).catch(console.error)
|
||||
application.mutator
|
||||
.deleteItem(view)
|
||||
.then(() => application.sync.sync())
|
||||
.catch(console.error)
|
||||
}
|
||||
},
|
||||
[application.mutator],
|
||||
[application.mutator, application.sync],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { ContentType, ItemCounter } from '@standardnotes/snjs'
|
||||
import { ContentType, StaticItemCounter } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import EncryptionStatusItem from './EncryptionStatusItem'
|
||||
@@ -8,7 +8,7 @@ import { formatCount } from './formatCount'
|
||||
|
||||
const EncryptionEnabled: FunctionComponent = () => {
|
||||
const application = useApplication()
|
||||
const itemCounter = new ItemCounter()
|
||||
const itemCounter = new StaticItemCounter()
|
||||
const count = itemCounter.countNotesAndTags(application.items.getItems([ContentType.Note, ContentType.Tag]))
|
||||
const files = application.items.getItems([ContentType.File])
|
||||
const notes = formatCount(count.notes, 'notes')
|
||||
|
||||
@@ -18,7 +18,7 @@ type Props = { viewControllerManager: ViewControllerManager }
|
||||
const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props) => {
|
||||
const app = viewControllerManager.application
|
||||
|
||||
const [erroredItems, setErroredItems] = useState(app.items.invalidItems)
|
||||
const [erroredItems, setErroredItems] = useState(app.items.invalidNonVaultedItems)
|
||||
|
||||
const getContentTypeDisplay = (item: EncryptedItemInterface): string => {
|
||||
const display = DisplayStringForContentType(item.content_type)
|
||||
@@ -44,7 +44,9 @@ const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props
|
||||
return
|
||||
}
|
||||
|
||||
void app.mutator.deleteItems(items)
|
||||
void app.mutator.deleteItems(items).then(() => {
|
||||
void app.sync.sync()
|
||||
})
|
||||
|
||||
setErroredItems(app.items.invalidItems)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ const Security: FunctionComponent<SecurityProps> = (props) => {
|
||||
return (
|
||||
<PreferencesPane>
|
||||
<Encryption viewControllerManager={props.viewControllerManager} />
|
||||
{props.application.items.invalidItems.length > 0 && (
|
||||
{props.application.items.invalidNonVaultedItems.length > 0 && (
|
||||
<ErroredItems viewControllerManager={props.viewControllerManager} />
|
||||
)}
|
||||
<Protections application={props.application} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
|
||||
export const securityPrefsHasBubble = (application: WebApplication): boolean => {
|
||||
return application.items.invalidItems.length > 0
|
||||
return application.items.invalidNonVaultedItems.length > 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||
import { TrustedContactInterface } from '@standardnotes/snjs'
|
||||
import EditContactModal from './EditContactModal'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
contact: TrustedContactInterface
|
||||
}
|
||||
|
||||
const ContactItem = ({ contact }: Props) => {
|
||||
const application = useApplication()
|
||||
|
||||
const [isContactModalOpen, setIsContactModalOpen] = useState(false)
|
||||
const closeContactModal = () => setIsContactModalOpen(false)
|
||||
|
||||
const collaborationID = application.contacts.getCollaborationIDForTrustedContact(contact)
|
||||
|
||||
const deleteContact = useCallback(async () => {
|
||||
void application.contacts.deleteContact(contact)
|
||||
}, [application.contacts, contact])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalOverlay isOpen={isContactModalOpen} close={closeContactModal}>
|
||||
<EditContactModal editContactUuid={contact.uuid} onCloseDialog={closeContactModal} />
|
||||
</ModalOverlay>
|
||||
|
||||
<div className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
||||
<Icon type={'user'} size="custom" className="mt-2.5 h-5.5 w-5.5 flex-shrink-0" />
|
||||
<div className="flex flex-col gap-2 py-1.5">
|
||||
<span
|
||||
className={`mr-auto overflow-hidden text-ellipsis text-base font-bold ${contact.isMe ? 'text-info' : ''}`}
|
||||
>
|
||||
{contact.name}
|
||||
</span>
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-sm">{collaborationID}</span>
|
||||
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
<Button label="Edit" className={'mr-3 text-xs'} onClick={() => setIsContactModalOpen(true)} />
|
||||
<Button label="Delete" className={'mr-3 text-xs'} onClick={deleteContact} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContactItem
|
||||
@@ -0,0 +1,127 @@
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import Modal, { ModalAction } from '@/Components/Modal/Modal'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { PendingSharedVaultInviteRecord, TrustedContactInterface } from '@standardnotes/snjs'
|
||||
|
||||
type Props = {
|
||||
fromInvite?: PendingSharedVaultInviteRecord
|
||||
editContactUuid?: string
|
||||
onCloseDialog: () => void
|
||||
onAddContact?: (contact: TrustedContactInterface) => void
|
||||
}
|
||||
|
||||
const EditContactModal: FunctionComponent<Props> = ({ onCloseDialog, fromInvite, onAddContact, editContactUuid }) => {
|
||||
const application = useApplication()
|
||||
|
||||
const [name, setName] = useState<string>('')
|
||||
const [collaborationID, setCollaborationID] = useState<string>('')
|
||||
const [editingContact, setEditingContact] = useState<TrustedContactInterface | undefined>(undefined)
|
||||
|
||||
const handleDialogClose = useCallback(() => {
|
||||
onCloseDialog()
|
||||
}, [onCloseDialog])
|
||||
|
||||
useEffect(() => {
|
||||
if (fromInvite) {
|
||||
setCollaborationID(application.contacts.getCollaborationIDFromInvite(fromInvite.invite))
|
||||
}
|
||||
}, [application.contacts, fromInvite])
|
||||
|
||||
useEffect(() => {
|
||||
if (editContactUuid) {
|
||||
const contact = application.contacts.findTrustedContact(editContactUuid)
|
||||
if (!contact) {
|
||||
throw new Error(`Contact with uuid ${editContactUuid} not found`)
|
||||
}
|
||||
|
||||
setEditingContact(contact)
|
||||
setName(contact.name)
|
||||
setCollaborationID(application.contacts.getCollaborationIDForTrustedContact(contact))
|
||||
}
|
||||
}, [application.contacts, application.vaults, editContactUuid])
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (editingContact) {
|
||||
void application.contacts.editTrustedContactFromCollaborationID(editingContact, { name, collaborationID })
|
||||
handleDialogClose()
|
||||
} else {
|
||||
const contact = await application.contacts.addTrustedContactFromCollaborationID(collaborationID, name)
|
||||
if (contact) {
|
||||
onAddContact?.(contact)
|
||||
handleDialogClose()
|
||||
} else {
|
||||
void application.alertService.alert('Unable to create contact. Please try again.')
|
||||
}
|
||||
}
|
||||
}, [
|
||||
editingContact,
|
||||
application.contacts,
|
||||
application.alertService,
|
||||
name,
|
||||
collaborationID,
|
||||
handleDialogClose,
|
||||
onAddContact,
|
||||
])
|
||||
|
||||
const modalActions = useMemo(
|
||||
(): ModalAction[] => [
|
||||
{
|
||||
label: editContactUuid ? 'Save Contact' : 'Add Contact',
|
||||
onClick: handleSubmit,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: handleDialogClose,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
],
|
||||
[editContactUuid, handleDialogClose, handleSubmit],
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={editContactUuid ? 'Edit Contact' : 'Add New Contact'}
|
||||
close={handleDialogClose}
|
||||
actions={modalActions}
|
||||
>
|
||||
<div className="px-4.5 pt-4 pb-1.5">
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="mb-3">
|
||||
<DecoratedInput
|
||||
className={{ container: 'mt-4' }}
|
||||
id="invite-name-input"
|
||||
value={name}
|
||||
placeholder="Contact Name"
|
||||
onChange={(value) => {
|
||||
setName(value)
|
||||
}}
|
||||
/>
|
||||
|
||||
<DecoratedInput
|
||||
className={{ container: 'mt-4' }}
|
||||
id="invite-email-input"
|
||||
value={collaborationID}
|
||||
placeholder="Contact CollaborationID"
|
||||
onChange={(value) => {
|
||||
setCollaborationID(value)
|
||||
}}
|
||||
/>
|
||||
|
||||
{!editContactUuid && (
|
||||
<p className="mt-4">
|
||||
Ask your contact for their Standard Notes CollaborationID via secure email or chat. Then, enter it here
|
||||
to add them as a contact.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditContactModal
|
||||
@@ -0,0 +1,97 @@
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import Modal, { ModalAction } from '@/Components/Modal/Modal'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { SharedVaultPermission, SharedVaultListingInterface, TrustedContactInterface } from '@standardnotes/snjs'
|
||||
|
||||
type Props = {
|
||||
vault: SharedVaultListingInterface
|
||||
onCloseDialog: () => void
|
||||
}
|
||||
|
||||
const ContactInviteModal: FunctionComponent<Props> = ({ vault, onCloseDialog }) => {
|
||||
const application = useApplication()
|
||||
|
||||
const [selectedContacts, setSelectedContacts] = useState<TrustedContactInterface[]>([])
|
||||
const [contacts, setContacts] = useState<TrustedContactInterface[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const loadContacts = async () => {
|
||||
const contacts = await application.sharedVaults.getInvitableContactsForSharedVault(vault)
|
||||
setContacts(contacts)
|
||||
}
|
||||
void loadContacts()
|
||||
}, [application.sharedVaults, vault])
|
||||
|
||||
const handleDialogClose = useCallback(() => {
|
||||
onCloseDialog()
|
||||
}, [onCloseDialog])
|
||||
|
||||
const inviteSelectedContacts = useCallback(async () => {
|
||||
for (const contact of selectedContacts) {
|
||||
await application.sharedVaults.inviteContactToSharedVault(vault, contact, SharedVaultPermission.Write)
|
||||
}
|
||||
handleDialogClose()
|
||||
}, [application.sharedVaults, vault, handleDialogClose, selectedContacts])
|
||||
|
||||
const toggleContact = useCallback(
|
||||
(contact: TrustedContactInterface) => {
|
||||
if (selectedContacts.includes(contact)) {
|
||||
const index = selectedContacts.indexOf(contact)
|
||||
const updatedContacts = [...selectedContacts]
|
||||
updatedContacts.splice(index, 1)
|
||||
setSelectedContacts(updatedContacts)
|
||||
} else {
|
||||
setSelectedContacts([...selectedContacts, contact])
|
||||
}
|
||||
},
|
||||
[selectedContacts, setSelectedContacts],
|
||||
)
|
||||
|
||||
const modalActions = useMemo(
|
||||
(): ModalAction[] => [
|
||||
{
|
||||
label: 'Invite Selected Contacts',
|
||||
onClick: inviteSelectedContacts,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: handleDialogClose,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
],
|
||||
[handleDialogClose, inviteSelectedContacts],
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal title="Add New Contact" close={handleDialogClose} actions={modalActions}>
|
||||
<div className="px-4.5 py-4">
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="mb-3">
|
||||
{contacts.map((contact) => {
|
||||
return (
|
||||
<div key={contact.uuid} onClick={() => toggleContact(contact)}>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedContacts.includes(contact)}
|
||||
onChange={() => toggleContact(contact)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{contact.name}
|
||||
{contact.contactUuid}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContactInviteModal
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||
import { PendingSharedVaultInviteRecord } from '@standardnotes/snjs'
|
||||
import { useCallback, useState } from 'react'
|
||||
import EditContactModal from '../Contacts/EditContactModal'
|
||||
|
||||
type Props = {
|
||||
invite: PendingSharedVaultInviteRecord
|
||||
}
|
||||
|
||||
const InviteItem = ({ invite }: Props) => {
|
||||
const application = useApplication()
|
||||
const [isAddContactModalOpen, setIsAddContactModalOpen] = useState(false)
|
||||
|
||||
const isTrusted = invite.trusted
|
||||
const inviteData = invite.message.data
|
||||
|
||||
const addAsTrustedContact = useCallback(() => {
|
||||
setIsAddContactModalOpen(true)
|
||||
}, [])
|
||||
|
||||
const acceptInvite = useCallback(async () => {
|
||||
await application.sharedVaults.acceptPendingSharedVaultInvite(invite)
|
||||
}, [application.sharedVaults, invite])
|
||||
|
||||
const closeAddContactModal = () => setIsAddContactModalOpen(false)
|
||||
const collaborationId = application.contacts.getCollaborationIDFromInvite(invite.invite)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalOverlay isOpen={isAddContactModalOpen} close={closeAddContactModal}>
|
||||
<EditContactModal fromInvite={invite} onCloseDialog={closeAddContactModal} />
|
||||
</ModalOverlay>
|
||||
|
||||
<div className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
||||
<Icon type={'archive'} size="custom" className="mt-2.5 h-5.5 w-5.5 flex-shrink-0" />
|
||||
<div className="flex flex-col gap-2 py-1.5">
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-sm">Vault Name: {inviteData.metadata.name}</span>
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-sm">
|
||||
Vault Description: {inviteData.metadata.description}
|
||||
</span>
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-sm">
|
||||
Sender CollaborationID: {collaborationId}
|
||||
</span>
|
||||
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
{isTrusted ? (
|
||||
<Button label="Accept Invite" className={'mr-3 text-xs'} onClick={acceptInvite} />
|
||||
) : (
|
||||
<div>
|
||||
<div>
|
||||
The sender of this invite is not trusted. To accept this invite, first add the sender as a trusted
|
||||
contact.
|
||||
</div>
|
||||
<Button label="Add Trusted Contact" className={'mr-3 text-xs'} onClick={addAsTrustedContact} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default InviteItem
|
||||
@@ -0,0 +1,157 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Subtitle, Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import ContactItem from './Contacts/ContactItem'
|
||||
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||
import EditContactModal from './Contacts/EditContactModal'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
VaultListingInterface,
|
||||
TrustedContactInterface,
|
||||
PendingSharedVaultInviteRecord,
|
||||
ContentType,
|
||||
SharedVaultServiceEvent,
|
||||
} from '@standardnotes/snjs'
|
||||
import VaultItem from './Vaults/VaultItem'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import InviteItem from './Invites/InviteItem'
|
||||
import EditVaultModal from './Vaults/VaultModal/EditVaultModal'
|
||||
|
||||
const Vaults = () => {
|
||||
const application = useApplication()
|
||||
|
||||
const [vaults, setVaults] = useState<VaultListingInterface[]>([])
|
||||
const [invites, setInvites] = useState<PendingSharedVaultInviteRecord[]>([])
|
||||
const [contacts, setContacts] = useState<TrustedContactInterface[]>([])
|
||||
|
||||
const [isAddContactModalOpen, setIsAddContactModalOpen] = useState(false)
|
||||
const closeAddContactModal = () => setIsAddContactModalOpen(false)
|
||||
|
||||
const [isVaultModalOpen, setIsVaultModalOpen] = useState(false)
|
||||
const closeVaultModal = () => setIsVaultModalOpen(false)
|
||||
|
||||
const vaultService = application.vaults
|
||||
const sharedVaultService = application.sharedVaults
|
||||
const contactService = application.contacts
|
||||
|
||||
const updateVaults = useCallback(async () => {
|
||||
setVaults(vaultService.getVaults())
|
||||
}, [vaultService])
|
||||
|
||||
const fetchInvites = useCallback(async () => {
|
||||
await sharedVaultService.downloadInboundInvites()
|
||||
const invites = sharedVaultService.getCachedPendingInviteRecords()
|
||||
setInvites(invites)
|
||||
}, [sharedVaultService])
|
||||
|
||||
const updateContacts = useCallback(async () => {
|
||||
setContacts(contactService.getAllContacts())
|
||||
}, [contactService])
|
||||
|
||||
useEffect(() => {
|
||||
return application.sharedVaults.addEventObserver((event) => {
|
||||
if (event === SharedVaultServiceEvent.SharedVaultStatusChanged) {
|
||||
void fetchInvites()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
return application.streamItems([ContentType.VaultListing, ContentType.TrustedContact], () => {
|
||||
void updateVaults()
|
||||
void fetchInvites()
|
||||
void updateContacts()
|
||||
})
|
||||
}, [application, updateVaults, fetchInvites, updateContacts])
|
||||
|
||||
const createNewVault = useCallback(async () => {
|
||||
setIsVaultModalOpen(true)
|
||||
}, [])
|
||||
|
||||
const createNewContact = useCallback(() => {
|
||||
setIsAddContactModalOpen(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void updateVaults()
|
||||
void fetchInvites()
|
||||
void updateContacts()
|
||||
}, [updateContacts, updateVaults, fetchInvites])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalOverlay isOpen={isAddContactModalOpen} close={closeAddContactModal}>
|
||||
<EditContactModal onCloseDialog={closeAddContactModal} />
|
||||
</ModalOverlay>
|
||||
|
||||
<ModalOverlay isOpen={isVaultModalOpen} close={closeVaultModal}>
|
||||
<EditVaultModal onCloseDialog={closeVaultModal} />
|
||||
</ModalOverlay>
|
||||
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Incoming Invites</Title>
|
||||
<div className="my-2 flex flex-col">
|
||||
{invites.map((invite) => {
|
||||
return <InviteItem invite={invite} key={invite.invite.uuid} />
|
||||
})}
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Contacts</Title>
|
||||
<div className="my-2 flex flex-col">
|
||||
{contacts.map((contact) => {
|
||||
return <ContactItem contact={contact} key={contact.uuid} />
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
<Button label="Add New Contact" className={'mr-3 text-xs'} onClick={createNewContact} />
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Vaults</Title>
|
||||
<div className="my-2 flex flex-col">
|
||||
{vaults.map((vault) => {
|
||||
return <VaultItem vault={vault} key={vault.uuid} />
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
<Button label="Create New Vault" className={'mr-3 text-xs'} onClick={createNewVault} />
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>CollaborationID</Title>
|
||||
<Subtitle>Share your CollaborationID with collaborators to join their vaults.</Subtitle>
|
||||
{contactService.isCollaborationEnabled() ? (
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
<code>
|
||||
<pre>{contactService.getCollaborationID()}</pre>
|
||||
</code>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
<Button
|
||||
label="Enable Vault Sharing"
|
||||
className={'mr-3 text-xs'}
|
||||
onClick={() => contactService.enableCollaboration()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(Vaults)
|
||||
@@ -0,0 +1,146 @@
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||
import { ButtonType, VaultListingInterface, isClientDisplayableError } from '@standardnotes/snjs'
|
||||
import { useCallback, useState } from 'react'
|
||||
import ContactInviteModal from '../Invites/ContactInviteModal'
|
||||
import EditVaultModal from './VaultModal/EditVaultModal'
|
||||
|
||||
type Props = {
|
||||
vault: VaultListingInterface
|
||||
}
|
||||
|
||||
const VaultItem = ({ vault }: Props) => {
|
||||
const application = useApplication()
|
||||
|
||||
const [isInviteModalOpen, setIsAddContactModalOpen] = useState(false)
|
||||
const closeInviteModal = () => setIsAddContactModalOpen(false)
|
||||
|
||||
const [isVaultModalOpen, setIsVaultModalOpen] = useState(false)
|
||||
const closeVaultModal = () => setIsVaultModalOpen(false)
|
||||
|
||||
const isAdmin = !vault.isSharedVaultListing() ? true : application.sharedVaults.isCurrentUserSharedVaultAdmin(vault)
|
||||
|
||||
const deleteVault = useCallback(async () => {
|
||||
const confirm = await application.alerts.confirm(
|
||||
'Deleting a vault will permanently delete all its items and files.',
|
||||
'Are you sure you want to delete this vault?',
|
||||
undefined,
|
||||
ButtonType.Danger,
|
||||
)
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
|
||||
if (vault.isSharedVaultListing()) {
|
||||
const result = await application.sharedVaults.deleteSharedVault(vault)
|
||||
if (isClientDisplayableError(result)) {
|
||||
void application.alerts.showErrorAlert(result)
|
||||
}
|
||||
} else {
|
||||
const success = await application.vaults.deleteVault(vault)
|
||||
if (!success) {
|
||||
void application.alerts.alert('Unable to delete vault. Please try again.')
|
||||
}
|
||||
}
|
||||
}, [application.alerts, application.sharedVaults, application.vaults, vault])
|
||||
|
||||
const leaveVault = useCallback(async () => {
|
||||
if (!vault.isSharedVaultListing()) {
|
||||
return
|
||||
}
|
||||
|
||||
const confirm = await application.alerts.confirm(
|
||||
'All items and files in this vault will be removed from your account.',
|
||||
'Are you sure you want to leave this vault?',
|
||||
undefined,
|
||||
ButtonType.Danger,
|
||||
)
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
|
||||
const success = await application.sharedVaults.leaveSharedVault(vault)
|
||||
if (!success) {
|
||||
void application.alerts.alert('Unable to leave vault. Please try again.')
|
||||
}
|
||||
}, [application.alerts, application.sharedVaults, vault])
|
||||
|
||||
const convertToSharedVault = useCallback(async () => {
|
||||
await application.sharedVaults.convertVaultToSharedVault(vault)
|
||||
}, [application.sharedVaults, vault])
|
||||
|
||||
const ensureVaultIsUnlocked = useCallback(async () => {
|
||||
if (!application.vaults.isVaultLocked(vault)) {
|
||||
return true
|
||||
}
|
||||
const unlocked = await application.vaultDisplayService.unlockVault(vault)
|
||||
return unlocked
|
||||
}, [application.vaultDisplayService, application.vaults, vault])
|
||||
|
||||
const openEditModal = useCallback(async () => {
|
||||
if (!(await ensureVaultIsUnlocked())) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsVaultModalOpen(true)
|
||||
}, [ensureVaultIsUnlocked])
|
||||
|
||||
const openInviteModal = useCallback(async () => {
|
||||
if (!(await ensureVaultIsUnlocked())) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsAddContactModalOpen(true)
|
||||
}, [ensureVaultIsUnlocked])
|
||||
|
||||
return (
|
||||
<>
|
||||
{vault.isSharedVaultListing() && (
|
||||
<ModalOverlay isOpen={isInviteModalOpen} close={closeInviteModal}>
|
||||
<ContactInviteModal vault={vault} onCloseDialog={closeInviteModal} />
|
||||
</ModalOverlay>
|
||||
)}
|
||||
|
||||
<ModalOverlay isOpen={isVaultModalOpen} close={closeVaultModal}>
|
||||
<EditVaultModal existingVaultUuid={vault.uuid} onCloseDialog={closeVaultModal} />
|
||||
</ModalOverlay>
|
||||
|
||||
<div className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
||||
<Icon type={'safe-square'} size="custom" className="mt-2.5 h-5.5 w-5.5 flex-shrink-0" />
|
||||
<div className="flex flex-col gap-2 py-1.5">
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-base font-bold">{vault.name}</span>
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-sm">{vault.description}</span>
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-sm">Vault ID: {vault.systemIdentifier}</span>
|
||||
|
||||
<div className="mt-2.5 flex w-full flex-row justify-between">
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
<Button label="Edit" className={'mr-3 text-xs'} onClick={openEditModal} />
|
||||
{isAdmin && (
|
||||
<Button colorStyle="danger" label="Delete" className={'mr-3 text-xs'} onClick={deleteVault} />
|
||||
)}
|
||||
{!isAdmin && vault.isSharedVaultListing() && (
|
||||
<Button label="Leave Vault" className={'mr-3 text-xs'} onClick={leaveVault} />
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
{vault.isSharedVaultListing() ? (
|
||||
<Button label="Invite Contacts" className={'mr-3 text-xs'} onClick={openInviteModal} />
|
||||
) : (
|
||||
<Button
|
||||
colorStyle="info"
|
||||
label="Enable Collaboration"
|
||||
className={'mr-3 text-xs'}
|
||||
onClick={convertToSharedVault}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default VaultItem
|
||||
@@ -0,0 +1,224 @@
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import Modal, { ModalAction } from '@/Components/Modal/Modal'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import {
|
||||
ChangeVaultOptionsDTO,
|
||||
KeySystemRootKeyPasswordType,
|
||||
KeySystemRootKeyStorageMode,
|
||||
SharedVaultInviteServerHash,
|
||||
SharedVaultUserServerHash,
|
||||
VaultListingInterface,
|
||||
isClientDisplayableError,
|
||||
} from '@standardnotes/snjs'
|
||||
import { VaultModalMembers } from './VaultModalMembers'
|
||||
import { VaultModalInvites } from './VaultModalInvites'
|
||||
import { PasswordTypePreference } from './PasswordTypePreference'
|
||||
import { KeyStoragePreference } from './KeyStoragePreference'
|
||||
import useItem from '@/Hooks/useItem'
|
||||
|
||||
type Props = {
|
||||
existingVaultUuid?: string
|
||||
onCloseDialog: () => void
|
||||
}
|
||||
|
||||
const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVaultUuid }) => {
|
||||
const application = useApplication()
|
||||
|
||||
const existingVault = useItem<VaultListingInterface>(existingVaultUuid)
|
||||
|
||||
const [name, setName] = useState<string>('')
|
||||
const [description, setDescription] = useState<string>('')
|
||||
const [members, setMembers] = useState<SharedVaultUserServerHash[]>([])
|
||||
const [invites, setInvites] = useState<SharedVaultInviteServerHash[]>([])
|
||||
const [isAdmin, setIsAdmin] = useState<boolean>(true)
|
||||
const [passwordType, setPasswordType] = useState<KeySystemRootKeyPasswordType>(
|
||||
KeySystemRootKeyPasswordType.Randomized,
|
||||
)
|
||||
const [keyStorageMode, setKeyStorageMode] = useState<KeySystemRootKeyStorageMode>(KeySystemRootKeyStorageMode.Synced)
|
||||
const [customPassword, setCustomPassword] = useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (existingVault) {
|
||||
setName(existingVault.name ?? '')
|
||||
setDescription(existingVault.description ?? '')
|
||||
setPasswordType(existingVault.rootKeyParams.passwordType)
|
||||
setKeyStorageMode(existingVault.keyStorageMode)
|
||||
}
|
||||
}, [application.vaults, existingVault])
|
||||
|
||||
const reloadVaultInfo = useCallback(async () => {
|
||||
if (!existingVault) {
|
||||
return
|
||||
}
|
||||
|
||||
if (existingVault.isSharedVaultListing()) {
|
||||
setIsAdmin(
|
||||
existingVault.isSharedVaultListing() && application.sharedVaults.isCurrentUserSharedVaultAdmin(existingVault),
|
||||
)
|
||||
|
||||
const users = await application.sharedVaults.getSharedVaultUsers(existingVault)
|
||||
if (users) {
|
||||
setMembers(users)
|
||||
}
|
||||
|
||||
const invites = await application.sharedVaults.getOutboundInvites(existingVault)
|
||||
if (!isClientDisplayableError(invites)) {
|
||||
setInvites(invites)
|
||||
}
|
||||
}
|
||||
}, [application.sharedVaults, existingVault])
|
||||
|
||||
useEffect(() => {
|
||||
void reloadVaultInfo()
|
||||
}, [application.vaults, reloadVaultInfo])
|
||||
|
||||
const handleDialogClose = useCallback(() => {
|
||||
onCloseDialog()
|
||||
}, [onCloseDialog])
|
||||
|
||||
const saveExistingVault = useCallback(
|
||||
async (vault: VaultListingInterface) => {
|
||||
if (vault.name !== name || vault.description !== description) {
|
||||
await application.vaults.changeVaultNameAndDescription(vault, {
|
||||
name: name,
|
||||
description: description,
|
||||
})
|
||||
}
|
||||
|
||||
const isChangingPasswordType = vault.keyPasswordType !== passwordType
|
||||
const isChangingKeyStorageMode = vault.keyStorageMode !== keyStorageMode
|
||||
|
||||
const getPasswordTypeParams = (): ChangeVaultOptionsDTO['newPasswordType'] => {
|
||||
if (!isChangingPasswordType) {
|
||||
throw new Error('Password type is not changing')
|
||||
}
|
||||
|
||||
if (passwordType === KeySystemRootKeyPasswordType.UserInputted) {
|
||||
if (!customPassword) {
|
||||
throw new Error('Custom password is not set')
|
||||
}
|
||||
return {
|
||||
passwordType,
|
||||
userInputtedPassword: customPassword,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
passwordType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isChangingPasswordType || isChangingKeyStorageMode) {
|
||||
await application.vaults.changeVaultOptions({
|
||||
vault,
|
||||
newPasswordType: isChangingPasswordType ? getPasswordTypeParams() : undefined,
|
||||
newKeyStorageMode: isChangingKeyStorageMode ? keyStorageMode : undefined,
|
||||
})
|
||||
}
|
||||
},
|
||||
[application.vaults, customPassword, description, keyStorageMode, name, passwordType],
|
||||
)
|
||||
|
||||
const createNewVault = useCallback(async () => {
|
||||
if (passwordType === KeySystemRootKeyPasswordType.UserInputted) {
|
||||
if (!customPassword) {
|
||||
throw new Error('Custom key is not set')
|
||||
}
|
||||
await application.vaults.createUserInputtedPasswordVault({
|
||||
name,
|
||||
description,
|
||||
storagePreference: keyStorageMode,
|
||||
userInputtedPassword: customPassword,
|
||||
})
|
||||
} else {
|
||||
await application.vaults.createRandomizedVault({
|
||||
name,
|
||||
description,
|
||||
storagePreference: keyStorageMode,
|
||||
})
|
||||
}
|
||||
|
||||
handleDialogClose()
|
||||
}, [application.vaults, customPassword, description, handleDialogClose, keyStorageMode, name, passwordType])
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (existingVault) {
|
||||
await saveExistingVault(existingVault)
|
||||
} else {
|
||||
await createNewVault()
|
||||
}
|
||||
handleDialogClose()
|
||||
}, [existingVault, handleDialogClose, saveExistingVault, createNewVault])
|
||||
|
||||
const modalActions = useMemo(
|
||||
(): ModalAction[] => [
|
||||
{
|
||||
label: existingVault ? 'Save Vault' : 'Create Vault',
|
||||
onClick: handleSubmit,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: handleDialogClose,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
],
|
||||
[existingVault, handleDialogClose, handleSubmit],
|
||||
)
|
||||
|
||||
if (existingVault && application.vaults.isVaultLocked(existingVault)) {
|
||||
return <div>Vault is locked.</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title={existingVault ? 'Edit Vault' : 'Create New Vault'} close={handleDialogClose} actions={modalActions}>
|
||||
<div className="px-4.5 pt-4 pb-1.5">
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="mb-3">
|
||||
<div className="text-lg">Vault Info</div>
|
||||
<div className="mt-1">The vault name and description are end-to-end encrypted.</div>
|
||||
|
||||
<DecoratedInput
|
||||
className={{ container: 'mt-4' }}
|
||||
id="vault-name-input"
|
||||
value={name}
|
||||
placeholder="Vault Name"
|
||||
onChange={(value) => {
|
||||
setName(value)
|
||||
}}
|
||||
/>
|
||||
|
||||
<DecoratedInput
|
||||
className={{ container: 'mt-4' }}
|
||||
id="vault-email-input"
|
||||
value={description}
|
||||
placeholder="Vault description"
|
||||
onChange={(value) => {
|
||||
setDescription(value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{existingVault && (
|
||||
<VaultModalMembers vault={existingVault} members={members} onChange={reloadVaultInfo} isAdmin={isAdmin} />
|
||||
)}
|
||||
|
||||
{existingVault && <VaultModalInvites invites={invites} onChange={reloadVaultInfo} isAdmin={isAdmin} />}
|
||||
|
||||
<PasswordTypePreference
|
||||
value={passwordType}
|
||||
onChange={setPasswordType}
|
||||
onCustomKeyChange={setCustomPassword}
|
||||
/>
|
||||
|
||||
<KeyStoragePreference value={keyStorageMode} onChange={setKeyStorageMode} />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditVaultModal
|
||||
@@ -0,0 +1,57 @@
|
||||
import { KeySystemRootKeyStorageMode } from '@standardnotes/snjs'
|
||||
import StyledRadioInput from '@/Components/Radio/StyledRadioInput'
|
||||
|
||||
type KeyStorageOption = {
|
||||
value: KeySystemRootKeyStorageMode
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const options: KeyStorageOption[] = [
|
||||
{
|
||||
value: KeySystemRootKeyStorageMode.Synced,
|
||||
label: 'Synced (Recommended)',
|
||||
description:
|
||||
'Your vault key will be encrypted and synced to your account and automatically available on your other devices.',
|
||||
},
|
||||
{
|
||||
value: KeySystemRootKeyStorageMode.Local,
|
||||
label: 'Local',
|
||||
description:
|
||||
'Your vault key will be encrypted and saved locally on this device. You will need to manually enter your vault key on your other devices.',
|
||||
},
|
||||
{
|
||||
value: KeySystemRootKeyStorageMode.Ephemeral,
|
||||
label: 'Ephemeral',
|
||||
description:
|
||||
'Your vault key will only be stored in memory and will be forgotten when you close the app. You will need to manually enter your vault key on your other devices.',
|
||||
},
|
||||
]
|
||||
|
||||
export const KeyStoragePreference = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: KeySystemRootKeyStorageMode
|
||||
onChange: (value: KeySystemRootKeyStorageMode) => void
|
||||
}) => {
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mb-3 text-lg">Vault Key Type</div>
|
||||
{options.map((option) => {
|
||||
return (
|
||||
<label key={option.value} className="mb-2 flex items-center gap-2 text-base font-medium md:text-sm">
|
||||
<StyledRadioInput
|
||||
name="option"
|
||||
checked={value === option.value}
|
||||
onChange={() => {
|
||||
onChange(option.value)
|
||||
}}
|
||||
/>
|
||||
{option.label}
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { KeySystemRootKeyPasswordType } from '@standardnotes/snjs'
|
||||
import StyledRadioInput from '@/Components/Radio/StyledRadioInput'
|
||||
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
|
||||
import { useState } from 'react'
|
||||
|
||||
type PasswordTypePreference = {
|
||||
value: KeySystemRootKeyPasswordType
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const options: PasswordTypePreference[] = [
|
||||
{
|
||||
value: KeySystemRootKeyPasswordType.Randomized,
|
||||
label: 'Randomized (Recommended)',
|
||||
description: 'Your vault key will be randomly generated and synced to your account.',
|
||||
},
|
||||
{
|
||||
value: KeySystemRootKeyPasswordType.UserInputted,
|
||||
label: 'Custom (Advanced)',
|
||||
description:
|
||||
'Choose your own key for your vault. This is an advanced option and is not recommended for most users.',
|
||||
},
|
||||
]
|
||||
|
||||
export const PasswordTypePreference = ({
|
||||
value,
|
||||
onChange,
|
||||
onCustomKeyChange,
|
||||
}: {
|
||||
value: KeySystemRootKeyPasswordType
|
||||
onChange: (value: KeySystemRootKeyPasswordType) => void
|
||||
onCustomKeyChange: (value: string) => void
|
||||
}) => {
|
||||
const [customKey, setCustomKey] = useState('')
|
||||
|
||||
const onKeyInputChange = (value: string) => {
|
||||
setCustomKey(value)
|
||||
onCustomKeyChange(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mb-3 text-lg">Vault Key Type</div>
|
||||
{options.map((option) => {
|
||||
return (
|
||||
<label key={option.value} className="mb-2 flex items-center gap-2 text-base font-medium md:text-sm">
|
||||
<StyledRadioInput
|
||||
name="option"
|
||||
checked={value === option.value}
|
||||
onChange={() => {
|
||||
onChange(option.value)
|
||||
}}
|
||||
/>
|
||||
{option.label}
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
|
||||
{value === KeySystemRootKeyPasswordType.UserInputted && (
|
||||
<div>
|
||||
<div className="text-gray-500 mt-3 text-sm">{options[1].description}</div>
|
||||
|
||||
<DecoratedPasswordInput
|
||||
placeholder="Choose a password"
|
||||
id="key-input"
|
||||
value={customKey}
|
||||
onChange={onKeyInputChange}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { SharedVaultInviteServerHash } from '@standardnotes/snjs'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Button from '@/Components/Button/Button'
|
||||
|
||||
export const VaultModalInvites = ({
|
||||
invites,
|
||||
onChange,
|
||||
isAdmin,
|
||||
}: {
|
||||
invites: SharedVaultInviteServerHash[]
|
||||
onChange: () => void
|
||||
isAdmin: boolean
|
||||
}) => {
|
||||
const application = useApplication()
|
||||
|
||||
const deleteInvite = useCallback(
|
||||
async (invite: SharedVaultInviteServerHash) => {
|
||||
await application.sharedVaults.deleteInvite(invite)
|
||||
onChange()
|
||||
},
|
||||
[application.sharedVaults, onChange],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mb-3 text-lg">Pending Invites</div>
|
||||
{invites.map((invite) => {
|
||||
const contact = application.contacts.findTrustedContactForInvite(invite)
|
||||
|
||||
return (
|
||||
<div className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
||||
<Icon type={'user'} size="custom" className="mt-2.5 h-5.5 w-5.5 flex-shrink-0" />
|
||||
<div className="flex flex-col gap-2 py-1.5">
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-base font-bold">
|
||||
{contact?.name || invite.user_uuid}
|
||||
</span>
|
||||
{contact && <span className="text-info">Trusted</span>}
|
||||
{!contact && (
|
||||
<div>
|
||||
<span className="text-base">Untrusted</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
<Button label="Cancel Invite" className={'mr-3 text-xs'} onClick={() => deleteInvite(invite)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { SharedVaultUserServerHash, VaultListingInterface } from '@standardnotes/snjs'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Button from '@/Components/Button/Button'
|
||||
|
||||
export const VaultModalMembers = ({
|
||||
members,
|
||||
isAdmin,
|
||||
vault,
|
||||
onChange,
|
||||
}: {
|
||||
members: SharedVaultUserServerHash[]
|
||||
vault: VaultListingInterface
|
||||
isAdmin: boolean
|
||||
onChange: () => void
|
||||
}) => {
|
||||
const application = useApplication()
|
||||
|
||||
const removeMemberFromVault = useCallback(
|
||||
async (memberItem: SharedVaultUserServerHash) => {
|
||||
if (vault.isSharedVaultListing()) {
|
||||
await application.sharedVaults.removeUserFromSharedVault(vault, memberItem.user_uuid)
|
||||
onChange()
|
||||
}
|
||||
},
|
||||
[application.sharedVaults, vault, onChange],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mb-3 text-lg">Vault Members</div>
|
||||
{members.map((member) => {
|
||||
if (application.sharedVaults.isSharedVaultUserSharedVaultOwner(member)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const contact = application.contacts.findTrustedContactForServerUser(member)
|
||||
return (
|
||||
<div
|
||||
key={contact?.uuid || member.user_uuid}
|
||||
className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md"
|
||||
>
|
||||
<Icon type={'user'} size="custom" className="mt-2.5 h-5.5 w-5.5 flex-shrink-0" />
|
||||
<div className="flex flex-col gap-2 py-1.5">
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-base font-bold">
|
||||
{contact?.name || member.user_uuid}
|
||||
</span>
|
||||
{contact && <span className="text-info">Trusted</span>}
|
||||
{!contact && (
|
||||
<div>
|
||||
<span className="text-base">Untrusted</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
<Button
|
||||
label="Remove From Vault"
|
||||
className={'mr-3 text-xs'}
|
||||
onClick={() => removeMemberFromVault(member)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { WebApplication } from '@/Application/WebApplication'
|
||||
import { PackageProvider } from './Panes/General/Advanced/Packages/Provider/PackageProvider'
|
||||
import { securityPrefsHasBubble } from './Panes/Security/securityPrefsHasBubble'
|
||||
import { PreferenceId } from '@standardnotes/ui-services'
|
||||
import { featureTrunkVaultsEnabled } from '@/FeatureTrunk'
|
||||
|
||||
interface PreferencesMenuItem {
|
||||
readonly id: PreferenceId
|
||||
@@ -44,6 +45,11 @@ const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
||||
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help' },
|
||||
]
|
||||
|
||||
if (featureTrunkVaultsEnabled()) {
|
||||
PREFERENCES_MENU_ITEMS.splice(3, 0, { id: 'vaults', label: 'Vaults', icon: 'safe-square' })
|
||||
READY_PREFERENCES_MENU_ITEMS.splice(3, 0, { id: 'vaults', label: 'Vaults', icon: 'safe-square' })
|
||||
}
|
||||
|
||||
export class PreferencesMenu {
|
||||
private _selectedPane: PreferenceId = 'account'
|
||||
private _menu: PreferencesMenuItem[]
|
||||
|
||||
Reference in New Issue
Block a user