chore: allow setting custom icon to vault

This commit is contained in:
Aman Harwara
2023-08-08 19:00:04 +05:30
parent cd1b488769
commit f82974633b
14 changed files with 174 additions and 48 deletions

View File

@@ -8,6 +8,9 @@ import { VaultListingContent } from './VaultListingContent'
import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode'
import { VaultListingSharingInfo } from './VaultListingSharingInfo'
import { KeySystemIdentifier } from '../KeySystemRootKey/KeySystemIdentifier'
import { EmojiString, IconType } from '../../Utilities/Icon/IconType'
export const DefaultVaultIconName: IconType = 'safe-square'
export class VaultListing extends DecryptedItem<VaultListingContent> implements VaultListingInterface {
systemIdentifier: KeySystemIdentifier
@@ -17,6 +20,7 @@ export class VaultListing extends DecryptedItem<VaultListingContent> implements
name: string
description?: string
iconString: IconType | EmojiString
sharing?: VaultListingSharingInfo
@@ -30,6 +34,7 @@ export class VaultListing extends DecryptedItem<VaultListingContent> implements
this.name = payload.content.name
this.description = payload.content.description
this.iconString = payload.content.iconString || DefaultVaultIconName
this.sharing = payload.content.sharing
}

View File

@@ -3,6 +3,7 @@ import { KeySystemIdentifier } from '../KeySystemRootKey/KeySystemIdentifier'
import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface'
import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode'
import { VaultListingSharingInfo } from './VaultListingSharingInfo'
import { EmojiString, IconType } from '../../Utilities/Icon/IconType'
export interface VaultListingContentSpecialized extends SpecializedContent {
systemIdentifier: KeySystemIdentifier
@@ -12,6 +13,7 @@ export interface VaultListingContentSpecialized extends SpecializedContent {
name: string
description?: string
iconString: IconType | EmojiString
sharing?: VaultListingSharingInfo
}

View File

@@ -5,6 +5,7 @@ import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKe
import { VaultListingSharingInfo } from './VaultListingSharingInfo'
import { VaultListingContent } from './VaultListingContent'
import { DecryptedItemInterface } from '../../Abstract/Item'
import { EmojiString, IconType } from '../../Utilities/Icon/IconType'
export interface VaultListingInterface extends DecryptedItemInterface<VaultListingContent> {
systemIdentifier: KeySystemIdentifier
@@ -14,6 +15,7 @@ export interface VaultListingInterface extends DecryptedItemInterface<VaultListi
name: string
description?: string
iconString: IconType | EmojiString
sharing?: VaultListingSharingInfo

View File

@@ -1,5 +1,6 @@
import { DecryptedItemMutator } from '../../Abstract/Item'
import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface'
import { EmojiString, IconType } from '../../Utilities/Icon/IconType'
import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode'
import { VaultListingContent } from './VaultListingContent'
import { VaultListingSharingInfo } from './VaultListingSharingInfo'
@@ -13,6 +14,10 @@ export class VaultListingMutator extends DecryptedItemMutator<VaultListingConten
this.mutableContent.description = description
}
set iconString(iconString: IconType | EmojiString) {
this.mutableContent.iconString = iconString
}
set sharing(sharing: VaultListingSharingInfo | undefined) {
this.mutableContent.sharing = sharing
}

View File

@@ -8,6 +8,8 @@ import {
KeySystemRootKeyStorageMode,
FillItemContentSpecialized,
KeySystemRootKeyInterface,
EmojiString,
IconType,
} from '@standardnotes/models'
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { ContentType } from '@standardnotes/domain-core'
@@ -25,6 +27,7 @@ export class CreateVault {
async execute(dto: {
vaultName: string
vaultDescription?: string
vaultIcon: IconType | EmojiString
userInputtedPassword: string | undefined
storagePreference: KeySystemRootKeyStorageMode
}): Promise<VaultListingInterface> {
@@ -44,6 +47,7 @@ export class CreateVault {
keySystemIdentifier,
vaultName: dto.vaultName,
vaultDescription: dto.vaultDescription,
vaultIcon: dto.vaultIcon,
passwordType: dto.userInputtedPassword ? KeySystemPasswordType.UserInputted : KeySystemPasswordType.Randomized,
rootKeyParams: rootKey.keyParams,
storage: dto.storagePreference,
@@ -58,6 +62,7 @@ export class CreateVault {
keySystemIdentifier: string
vaultName: string
vaultDescription?: string
vaultIcon: IconType | EmojiString
passwordType: KeySystemPasswordType
rootKeyParams: KeySystemRootKeyParamsInterface
storage: KeySystemRootKeyStorageMode
@@ -68,6 +73,7 @@ export class CreateVault {
keyStorageMode: dto.storage,
name: dto.vaultName,
description: dto.vaultDescription,
iconString: dto.vaultIcon,
}
return this.mutator.createItem(ContentType.TYPES.VaultListing, FillItemContentSpecialized(content), true)

View File

@@ -4,7 +4,9 @@ import { SendVaultDataChangedMessage } from './../SharedVaults/UseCase/SendVault
import { isClientDisplayableError } from '@standardnotes/responses'
import {
DecryptedItemInterface,
EmojiString,
FileItem,
IconType,
KeySystemIdentifier,
KeySystemRootKeyStorageMode,
SharedVaultListingInterface,
@@ -97,10 +99,15 @@ export class VaultService
return vault
}
async createRandomizedVault(dto: { name: string; description?: string }): Promise<VaultListingInterface> {
async createRandomizedVault(dto: {
name: string
description?: string
iconString: IconType | EmojiString
}): Promise<VaultListingInterface> {
return this.createVaultWithParameters({
name: dto.name,
description: dto.description,
iconString: dto.iconString,
userInputtedPassword: undefined,
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
@@ -109,6 +116,7 @@ export class VaultService
async createUserInputtedPasswordVault(dto: {
name: string
description?: string
iconString: IconType | EmojiString
userInputtedPassword: string
storagePreference: KeySystemRootKeyStorageMode
}): Promise<VaultListingInterface> {
@@ -118,12 +126,14 @@ export class VaultService
private async createVaultWithParameters(dto: {
name: string
description?: string
iconString: IconType | EmojiString
userInputtedPassword: string | undefined
storagePreference: KeySystemRootKeyStorageMode
}): Promise<VaultListingInterface> {
const result = await this._createVault.execute({
vaultName: dto.name,
vaultDescription: dto.description,
vaultIcon: dto.iconString,
userInputtedPassword: dto.userInputtedPassword,
storagePreference: dto.storagePreference,
})
@@ -188,13 +198,14 @@ export class VaultService
return true
}
async changeVaultNameAndDescription(
async changeVaultMetadata(
vault: VaultListingInterface,
params: { name: string; description?: string },
params: { name: string; description?: string; iconString: IconType | EmojiString },
): Promise<VaultListingInterface> {
const updatedVault = await this.mutator.changeItem<VaultListingMutator, VaultListingInterface>(vault, (mutator) => {
mutator.name = params.name
mutator.description = params.description
mutator.iconString = params.iconString
})
await this.sync.sync()

View File

@@ -1,5 +1,7 @@
import {
DecryptedItemInterface,
EmojiString,
IconType,
KeySystemIdentifier,
KeySystemRootKeyStorageMode,
SharedVaultListingInterface,
@@ -12,10 +14,15 @@ import { Result } from '@standardnotes/domain-core'
export interface VaultServiceInterface
extends AbstractService<VaultServiceEvent, VaultServiceEventPayload[VaultServiceEvent]> {
createRandomizedVault(dto: { name: string; description?: string }): Promise<VaultListingInterface>
createRandomizedVault(dto: {
name: string
description?: string
iconString: IconType | EmojiString
}): Promise<VaultListingInterface>
createUserInputtedPasswordVault(dto: {
name: string
description?: string
iconString: IconType | EmojiString
userInputtedPassword: string
storagePreference: KeySystemRootKeyStorageMode
}): Promise<VaultListingInterface>
@@ -32,9 +39,9 @@ export interface VaultServiceInterface
isItemInVault(item: DecryptedItemInterface): boolean
getItemVault(item: DecryptedItemInterface): VaultListingInterface | undefined
changeVaultNameAndDescription(
changeVaultMetadata(
vault: VaultListingInterface,
params: { name: string; description: string },
params: { name: string; description: string; iconString: IconType | EmojiString },
): Promise<VaultListingInterface>
rotateVaultRootKey(vault: VaultListingInterface, vaultPassword?: string): Promise<void>

View File

@@ -1,6 +1,6 @@
import { classNames } from '@standardnotes/utils'
import { EmojiString, Platform, VectorIconNameOrEmoji } from '@standardnotes/snjs'
import { FunctionComponent, useMemo, useRef, useState } from 'react'
import { ForwardedRef, forwardRef, useCallback, useMemo, useRef, useState } from 'react'
import Dropdown from '../Dropdown/Dropdown'
import { DropdownItem } from '../Dropdown/DropdownItem'
import { getEmojiLength } from './EmojiLength'
@@ -17,6 +17,39 @@ type Props = {
className?: string
}
const TabButton = forwardRef(
(
{
type,
label,
currentType,
selectTab,
}: {
label: string
type: IconPickerType | 'reset'
currentType: IconPickerType
selectTab: (type: IconPickerType | 'reset') => void
},
ref: ForwardedRef<HTMLButtonElement>,
) => {
const isSelected = currentType === type
return (
<button
className={`relative mr-2 cursor-pointer border-0 pb-1.5 text-mobile-menu-item focus:shadow-none md:text-tablet-menu-item lg:text-menu-item ${
isSelected ? 'font-medium text-info' : 'text-text'
}`}
onClick={() => {
selectTab(type)
}}
ref={ref}
>
{label}
</button>
)
},
)
const IconPicker = ({ selectedValue, onIconChange, platform, className, useIconGrid, iconGridClassName }: Props) => {
const iconKeys = useMemo(() => Object.keys(IconNameToSvgMapping), [])
@@ -51,26 +84,6 @@ const IconPicker = ({ selectedValue, onIconChange, platform, className, useIconG
}
}
const TabButton: FunctionComponent<{
label: string
type: IconPickerType | 'reset'
}> = ({ type, label }) => {
const isSelected = currentType === type
return (
<button
className={`relative mr-2 cursor-pointer border-0 pb-1.5 text-mobile-menu-item focus:shadow-none md:text-tablet-menu-item lg:text-menu-item ${
isSelected ? 'font-medium text-info' : 'text-text'
}`}
onClick={() => {
selectTab(type)
}}
>
{label}
</button>
)
}
const handleIconChange = (value: string) => {
onIconChange(value)
}
@@ -88,12 +101,20 @@ const IconPicker = ({ selectedValue, onIconChange, platform, className, useIconG
}
}
const focusOnMount = useCallback((element: HTMLButtonElement | null) => {
if (element) {
setTimeout(() => {
element.focus()
})
}
}, [])
return (
<div className={`flex h-full flex-grow flex-col overflow-auto ${className}`}>
<div className="flex">
<TabButton label="Icon" type={'icon'} />
<TabButton label="Emoji" type={'emoji'} />
<TabButton label="Reset" type={'reset'} />
<TabButton label="Icon" type={'icon'} currentType={currentType} selectTab={selectTab} />
<TabButton label="Emoji" type={'emoji'} currentType={currentType} selectTab={selectTab} />
<TabButton label="Reset" type={'reset'} currentType={currentType} selectTab={selectTab} />
</div>
<div className={'mt-2 h-full min-h-0 overflow-auto'}>
{currentType === 'icon' &&
@@ -104,12 +125,13 @@ const IconPicker = ({ selectedValue, onIconChange, platform, className, useIconG
iconGridClassName,
)}
>
{iconKeys.map((iconName) => (
{iconKeys.map((iconName, index) => (
<button
key={iconName}
onClick={() => {
handleIconChange(iconName)
}}
ref={index === 0 ? focusOnMount : undefined}
>
<Icon type={iconName} />
</button>

View File

@@ -48,6 +48,7 @@ const ModalOverlay = forwardRef(
modal={false}
portal={true}
preventBodyScroll={true}
hideOnInteractOutside={false}
{...props}
>
{children}

View File

@@ -108,7 +108,7 @@ const VaultItem = ({ vault }: Props) => {
</ModalOverlay>
<div className="flex flex-row gap-3.5 rounded-lg px-3.5 py-2.5 border border-border shadow">
<Icon type="safe-square" size="custom" className="mt-2.5 h-5.5 w-5.5 flex-shrink-0" />
<Icon type={vault.iconString} size="custom" className="mt-2.5 h-5.5 w-5.5 flex-shrink-0" />
<div className="flex flex-col gap-1.5 py-1.5">
<span className="mr-auto overflow-hidden text-ellipsis text-base font-bold">{vault.name}</span>
{vault.description && (

View File

@@ -9,6 +9,7 @@ import {
SharedVaultInviteServerHash,
SharedVaultUserServerHash,
VaultListingInterface,
VectorIconNameOrEmoji,
isClientDisplayableError,
} from '@standardnotes/snjs'
import { VaultModalMembers } from './VaultModalMembers'
@@ -16,6 +17,11 @@ import { VaultModalInvites } from './VaultModalInvites'
import { PasswordTypePreference } from './PasswordTypePreference'
import { KeyStoragePreference } from './KeyStoragePreference'
import useItem from '@/Hooks/useItem'
import Button from '@/Components/Button/Button'
import Icon from '@/Components/Icon/Icon'
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
import Popover from '@/Components/Popover/Popover'
import IconPicker from '@/Components/Icon/IconPicker'
type Props = {
existingVaultUuid?: string
@@ -29,6 +35,7 @@ const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVault
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [iconString, setIconString] = useState<VectorIconNameOrEmoji>('safe-square')
const [members, setMembers] = useState<SharedVaultUserServerHash[]>([])
const [invites, setInvites] = useState<SharedVaultInviteServerHash[]>([])
const [isAdmin, setIsAdmin] = useState(true)
@@ -43,6 +50,7 @@ const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVault
if (existingVault) {
setName(existingVault.name ?? '')
setDescription(existingVault.description ?? '')
setIconString(existingVault.iconString)
setPasswordType(existingVault.rootKeyParams.passwordType)
setKeyStorageMode(existingVault.keyStorageMode)
}
@@ -85,10 +93,11 @@ const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVault
return
}
if (vault.name !== name || vault.description !== description) {
await application.vaults.changeVaultNameAndDescription(vault, {
if (vault.name !== name || vault.description !== description || vault.iconString !== iconString) {
await application.vaults.changeVaultMetadata(vault, {
name: name,
description: description,
iconString: iconString,
})
}
@@ -125,7 +134,16 @@ const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVault
handleDialogClose()
},
[application.vaults, customPassword, description, handleDialogClose, keyStorageMode, name, passwordType],
[
application.vaults,
customPassword,
description,
handleDialogClose,
iconString,
keyStorageMode,
name,
passwordType,
],
)
const createNewVault = useCallback(async () => {
@@ -141,6 +159,7 @@ const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVault
await application.vaults.createUserInputtedPasswordVault({
name,
description,
iconString: iconString,
storagePreference: keyStorageMode,
userInputtedPassword: customPassword,
})
@@ -148,11 +167,21 @@ const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVault
await application.vaults.createRandomizedVault({
name,
description,
iconString: iconString,
})
}
handleDialogClose()
}, [application.vaults, customPassword, description, handleDialogClose, keyStorageMode, name, passwordType])
}, [
application.vaults,
customPassword,
description,
handleDialogClose,
iconString,
keyStorageMode,
name,
passwordType,
])
const handleSubmit = useCallback(async () => {
if (isSubmitting) {
@@ -186,6 +215,12 @@ const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVault
[existingVault, handleDialogClose, handleSubmit, isSubmitting],
)
const [shouldShowIconPicker, setShouldShowIconPicker] = useState(false)
const iconPickerButtonRef = useRef<HTMLButtonElement>(null)
const toggleIconPicker = useCallback(() => {
setShouldShowIconPicker((shouldShow) => !shouldShow)
}, [])
if (existingVault && application.vaultLocks.isVaultLocked(existingVault)) {
return <div>Vault is locked.</div>
}
@@ -198,15 +233,45 @@ const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVault
<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' }}
ref={nameInputRef}
value={name}
placeholder="Vault Name"
onChange={(value) => {
setName(value)
}}
/>
<div className="flex items-center mt-4 gap-4">
<StyledTooltip className="!z-modal" label="Choose icon">
<Button className="!px-1.5" ref={iconPickerButtonRef} onClick={toggleIconPicker}>
<Icon type={iconString} />
</Button>
</StyledTooltip>
<Popover
title="Choose icon"
open={shouldShowIconPicker}
anchorElement={iconPickerButtonRef.current}
togglePopover={toggleIconPicker}
align="start"
overrideZIndex="z-modal"
hideOnClickInModal
>
<div className="p-2">
<IconPicker
selectedValue={iconString || 'safe-square'}
onIconChange={(value?: VectorIconNameOrEmoji) => {
setIconString(value ?? 'safe-square')
toggleIconPicker()
}}
platform={application.platform}
useIconGrid={true}
/>
</div>
</Popover>
<DecoratedInput
className={{
container: 'flex-grow',
}}
ref={nameInputRef}
value={name}
placeholder="Vault Name"
onChange={(value) => {
setName(value)
}}
/>
</div>
<DecoratedInput
className={{ container: 'mt-4' }}

View File

@@ -39,7 +39,7 @@ const ManyVaultSelectionMenu: FunctionComponent = () => {
checked={isVaultVisible(vault)}
key={vault.uuid}
>
<Icon type="safe-square" className="mr-2 text-neutral" />
<Icon type={vault.iconString} className="mr-2 text-neutral" />
<div className="flex w-full items-center gap-1">
{vault.name}
{application.vaultLocks.isVaultLocked(vault) && <Icon className="ml-1" type="lock" size={'small'} />}

View File

@@ -90,7 +90,7 @@ const VaultMenu = ({ items }: { items: DecryptedItemInterface[] }) => {
)}
>
<Icon
type="safe-square"
type={vault.iconString}
size="large"
className={classNames('mr-2 text-neutral', doesVaultContainItems(vault) ? 'text-info' : '')}
/>

View File

@@ -9,7 +9,7 @@ type Props = {
const VaultNameBadge: FunctionComponent<Props> = ({ vault }) => {
return (
<div title="Vault name" className="flex rounded bg-success px-1.5 py-1 text-success-contrast select-none">
<Icon ariaLabel="Shared in vault" type="safe-square" className="mr-1 text-info-contrast" size="medium" />
<Icon ariaLabel="Shared in vault" type={vault.iconString} className="mr-1 text-info-contrast" size="medium" />
<span className="mr-auto overflow-hidden text-ellipsis text-xs">{vault.name}</span>
</div>
)