Skip to content

Commit 3f6266d

Browse files
[TASK] Notes infobar, click to insert, toast and more (#213)
#213 * Added wait for click to insert snippet in sidebar; Signed-off-by: Sahil Shubham <[email protected]> * added nodes infobar with search; Signed-off-by: Sahil Shubham <[email protected]> * Added making public and private in extension sidebar; Signed-off-by: Sahil Shubham <[email protected]> * Added basic onboarding cards when no page highlights; Signed-off-by: Sahil Shubham <[email protected]> * Added changeset Signed-off-by: Sahil Shubham <[email protected]>
1 parent ea35c07 commit 3f6266d

File tree

17 files changed

+483
-99
lines changed

17 files changed

+483
-99
lines changed

.changeset/nine-dingos-arrive.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'mexit': minor
3+
'mexit-webapp': patch
4+
---
5+
6+
Added wait for click to paste snippets in sidebar, new public nodes infobar.

apps/extension/src/Components/Dibba/index.tsx

+1-79
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { useSputlitContext, VisualState } from '../../Hooks/useSputlitContext'
3535
import { useContentStore } from '../../Stores/useContentStore'
3636
import useDataStore from '../../Stores/useDataStore'
3737
import { getDibbaText } from '../../Utils/getDibbaText'
38+
import { supportedDomains, simulateOnChange, copySnippetToClipboard, getUpcomingData } from '../../Utils/pasteUtils'
3839
import EditorPreviewRenderer from '../EditorPreviewRenderer'
3940
import { ComboboxItem, ComboboxRoot, ItemCenterWrapper, ItemDesc, ItemRightIcons, ItemTitle } from './styled'
4041

@@ -47,85 +48,6 @@ interface PublicNode {
4748
content: NodeEditorContent
4849
}
4950

50-
// TODO: add more domains and their supported types
51-
// Also a better matching for the domains
52-
const supportedDomains: Record<string, 'plain' | 'html'> = {
53-
'https://mail.google.com': 'html',
54-
'https://www.figma.com': 'plain',
55-
'https://keep.google.com': 'plain'
56-
}
57-
58-
export const copySnippetToClipboard = async (item: Snippet) => {
59-
const text = convertContentToRawText(item.content, '\n')
60-
61-
let html = text
62-
63-
try {
64-
const filterdContent = convertToCopySnippet(item.content)
65-
const convertedContent = convertToCopySnippet(filterdContent, {
66-
filter: defaultCopyFilter,
67-
converter: defaultCopyConverter
68-
})
69-
70-
const tempEditor = createPlateEditor({
71-
plugins: getPlugins(
72-
createPlateUI({
73-
[ELEMENT_TAG]: CopyTag as any
74-
}),
75-
{
76-
exclude: { dnd: true }
77-
}
78-
)
79-
})
80-
81-
html = serializeHtml(tempEditor, {
82-
nodes: convertedContent
83-
})
84-
} catch (err) {
85-
mog('Something went wrong', { err })
86-
}
87-
88-
//Copying both the html and text in clipboard
89-
const textBlob = new Blob([text], { type: 'text/plain' })
90-
const htmlBlob = new Blob([html], { type: 'text/html' })
91-
const data = [new ClipboardItem({ ['text/plain']: textBlob, ['text/html']: htmlBlob })]
92-
93-
await navigator.clipboard.write(data)
94-
95-
toast.success('Snippet copied to clipboard!')
96-
}
97-
98-
// This functions provides the 'to be' range and text content
99-
// Needed because keydown event happens before there is a selection or content change
100-
function getUpcomingData(selection: Selection) {
101-
const ogRange = selection.getRangeAt(0)
102-
103-
// Shifitng both start and end offset to simulate backwards caret movement
104-
const range = ogRange.cloneRange()
105-
range.setStart(ogRange.startContainer, ogRange.startOffset - 1)
106-
range.setEnd(ogRange.endContainer, ogRange.endOffset - 1)
107-
108-
// delete last character of current content
109-
const text = selection.anchorNode.textContent.slice(0, -1)
110-
111-
return { range, text }
112-
}
113-
114-
function simulateOnChange() {
115-
const inputEvent = new InputEvent('input', {
116-
bubbles: true,
117-
cancelable: false
118-
})
119-
120-
const changeEvent = new Event('change', {
121-
bubbles: true,
122-
cancelable: false
123-
})
124-
125-
document.activeElement.dispatchEvent(inputEvent)
126-
document.activeElement.dispatchEvent(changeEvent)
127-
}
128-
12951
// TODO: whether or not to enable dibba should be a user's preference
13052
// we don't users to move away because they have doubts of us forcing a 'keylogger' on them
13153
export default function Dibba() {

apps/extension/src/Components/Sidebar/ContextInfoBar.tsx

+41-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React, { useEffect, useMemo, useRef, useState } from 'react'
22

3+
import edit2Line from '@iconify/icons-ri/edit-2-line'
4+
import linkM from '@iconify/icons-ri/link-m'
35
import searchLine from '@iconify/icons-ri/search-line'
46
import { Icon } from '@iconify/react'
57
import fuzzysort from 'fuzzysort'
@@ -14,14 +16,37 @@ import {
1416
SidebarListFilter,
1517
Input,
1618
SnippetSidebarHelp,
17-
HighlightSidebarHelp
19+
HighlightSidebarHelp,
20+
CenteredFlex
1821
} from '@mexit/shared'
1922

2023
import { useHighlightStore } from '../../Stores/useHighlightStore'
2124
import { getElementById } from '../../contentScript'
25+
import { GenericCard } from './GenericCard'
2226
import { HighlightGroups } from './HighlightGroup'
2327
import { ShortenerComponent } from './ShortenerComponent'
2428

29+
// TODO: add links to onboarding tutorials later
30+
// and maybe a check if the user doesn't want to see a card again
31+
const basicOnboarding = [
32+
{
33+
icon: 'ri:link-m',
34+
title: 'Shorten URLs and Tag them',
35+
description: 'Create shortcuts for important URLs and tag them to organize'
36+
},
37+
{
38+
icon: 'ri:edit-2-line',
39+
title: 'Highlight Content',
40+
description:
41+
'Select and open Spotlight to create a highlight and save it in a note. Highlights are shown here in the sidebar'
42+
},
43+
{
44+
icon: 'lucide:highlighter',
45+
title: 'Use your knowledge everywhere',
46+
description: 'Use [[ to link to your public notes, use snippets and insert website shortcuts that you have created'
47+
}
48+
]
49+
2550
export function ContextInfoBar() {
2651
// const [search, setSearch] = useState('')
2752
// const inputRef = useRef<HTMLInputElement>(null)
@@ -75,7 +100,21 @@ export function ContextInfoBar() {
75100
</SidebarListFilter>
76101
<Infobox root={getElementById('ext-side-nav')} text={HighlightSidebarHelp} />
77102
</SidebarListFilterWrapper> */}
78-
<HighlightGroups highlights={pageHighlights} />
103+
{pageHighlights ? (
104+
<HighlightGroups highlights={pageHighlights} />
105+
) : (
106+
<div>
107+
<CenteredFlex>
108+
<h2>Hi there</h2>
109+
<p>Let's get you started</p>
110+
</CenteredFlex>
111+
<SnippetCards>
112+
{basicOnboarding.map((item) => (
113+
<GenericCard icon={item.icon} title={item.title} description={item.description} />
114+
))}
115+
</SnippetCards>
116+
</div>
117+
)}
79118
</SnippetCards>
80119
)
81120
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react'
2+
3+
import { Icon } from '@iconify/react'
4+
import styled from 'styled-components'
5+
6+
import { SnippetCardHeader, SnippetCardWrapper, SnippetContentPreview } from '@mexit/shared'
7+
8+
const GenericContent = styled.div`
9+
color: ${({ theme }) => theme.colors.text.fade};
10+
`
11+
12+
export const GenericCard = ({ icon, title, description }: { icon: string; title: string; description: string }) => {
13+
return (
14+
<SnippetCardWrapper>
15+
<SnippetCardHeader>
16+
<Icon icon={icon} />
17+
{title}
18+
</SnippetCardHeader>
19+
<GenericContent>{description}</GenericContent>
20+
</SnippetCardWrapper>
21+
)
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import React, { useMemo } from 'react'
2+
3+
import { Icon } from '@iconify/react'
4+
import toast from 'react-hot-toast'
5+
import styled from 'styled-components'
6+
7+
import { convertContentToRawText, MEXIT_FRONTEND_URL_BASE, mog, WORKSPACE_HEADER } from '@mexit/core'
8+
import {
9+
CenteredFlex,
10+
CopyButton,
11+
GenericFlex,
12+
SnippetCardFooter,
13+
SnippetCardHeader,
14+
SnippetCardWrapper,
15+
SnippetContentPreview,
16+
TagsLabel
17+
} from '@mexit/shared'
18+
19+
import { useAuthStore } from '../../Hooks/useAuth'
20+
import { getTitleFromPath } from '../../Hooks/useLinks'
21+
import { useNodes } from '../../Hooks/useNodes'
22+
import { useContentStore } from '../../Stores/useContentStore'
23+
import useDataStore from '../../Stores/useDataStore'
24+
25+
export const NodeCardHeader = styled.div<{ $noHover?: boolean }>`
26+
display: flex;
27+
justify-content: space-between;
28+
align-items: center;
29+
gap: ${({ theme }) => theme.spacing.tiny};
30+
font-size: 1.15rem;
31+
cursor: pointer;
32+
user-select: none;
33+
`
34+
35+
export const NodeCard = ({ nodeId }: { nodeId: string }) => {
36+
const { publicNodes, setNodePrivate, setNodePublic, checkNodePublic } = useDataStore()
37+
const { getNode } = useNodes()
38+
const getContent = useContentStore((store) => store.getContent)
39+
const getWorkspaceId = useAuthStore((store) => store.getWorkspaceId)
40+
41+
const isNodePublic = useMemo(() => {
42+
return checkNodePublic(nodeId)
43+
}, [publicNodes])
44+
45+
const node = getNode(nodeId)
46+
const contents = getContent(nodeId)
47+
48+
const flipPublicAccess = async () => {
49+
const workspaceHeaders = () => ({
50+
[WORKSPACE_HEADER]: getWorkspaceId(),
51+
Accept: 'application/json, text/plain, */*'
52+
})
53+
54+
if (isNodePublic) {
55+
const request = {
56+
type: 'PUBLIC_SHARING',
57+
subType: 'MAKE_PRIVATE',
58+
body: {
59+
nodeId
60+
},
61+
headers: workspaceHeaders()
62+
}
63+
chrome.runtime.sendMessage(request, (response) => {
64+
const { message, error } = response
65+
66+
if (error) {
67+
mog('ErrorMakingNodePrivate', error)
68+
} else {
69+
setNodePrivate(nodeId)
70+
}
71+
})
72+
} else {
73+
const request = {
74+
type: 'PUBLIC_SHARING',
75+
subType: 'MAKE_PUBLIC',
76+
body: {
77+
nodeId
78+
},
79+
headers: workspaceHeaders()
80+
}
81+
chrome.runtime.sendMessage(request, (response) => {
82+
const { message, error } = response
83+
84+
if (error) {
85+
mog('ErrorMakingNodePublic', error)
86+
} else {
87+
setNodePublic(nodeId)
88+
}
89+
})
90+
}
91+
}
92+
93+
return (
94+
<SnippetCardWrapper>
95+
<NodeCardHeader $noHover>
96+
<GenericFlex>
97+
<Icon icon="gg:file-document" />
98+
{getTitleFromPath(node?.path)}
99+
</GenericFlex>
100+
<GenericFlex>
101+
{isNodePublic ? (
102+
<Icon icon="bi:cloud" onClick={() => flipPublicAccess()} />
103+
) : (
104+
<Icon icon="bi:cloud-slash" onClick={() => flipPublicAccess()} />
105+
)}
106+
{isNodePublic && (
107+
<CopyButton
108+
text={`${MEXIT_FRONTEND_URL_BASE}/share/${nodeId}`}
109+
size="20px"
110+
beforeCopyTooltip="Copy link"
111+
afterCopyTooltip="Link copied!"
112+
/>
113+
)}
114+
</GenericFlex>
115+
</NodeCardHeader>
116+
117+
{/* TODO: saving raw content for nodes as well would be grand */}
118+
<SnippetContentPreview>{contents && convertContentToRawText(contents.content, ' ')}</SnippetContentPreview>
119+
120+
<SnippetCardFooter>{/* <TagsLabel tags={}/> */}</SnippetCardFooter>
121+
</SnippetCardWrapper>
122+
)
123+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, { useEffect, useRef, useState } from 'react'
2+
3+
import searchLine from '@iconify/icons-ri/search-line'
4+
import { Icon } from '@iconify/react'
5+
import { debounce } from 'lodash'
6+
7+
import { MEXIT_FRONTEND_URL_BASE, mog } from '@mexit/core'
8+
import { SidebarListFilterWrapper, SidebarListFilter, Input, SnippetCards, copyTextToClipboard } from '@mexit/shared'
9+
10+
import useRaju from '../../Hooks/useRaju'
11+
import useDataStore from '../../Stores/useDataStore'
12+
import { NodeCard } from './NodeCard'
13+
14+
export const NotesInfoBar = () => {
15+
const publicNodes = useDataStore((state) => state.publicNodes)
16+
const [search, setSearch] = useState('')
17+
const [searchedNodes, setSearchedNodes] = useState<string[]>()
18+
const { dispatch } = useRaju()
19+
20+
const inputRef = useRef<HTMLInputElement>(null)
21+
22+
const onSearchChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
23+
setSearch(e.target.value)
24+
}
25+
26+
const onSearch = async (newSearchTerm: string) => {
27+
const res = await dispatch('SEARCH', ['node'], newSearchTerm)
28+
mog('node search results', { res, newSearchTerm })
29+
30+
setSearchedNodes(res?.map((item) => item.id))
31+
}
32+
33+
useEffect(() => {
34+
if (search !== '') {
35+
onSearch(search)
36+
} else {
37+
setSearchedNodes(publicNodes)
38+
}
39+
}, [search])
40+
41+
return (
42+
<SnippetCards>
43+
<SidebarListFilterWrapper>
44+
<SidebarListFilter>
45+
<Icon icon={searchLine} />
46+
<Input
47+
autoFocus
48+
placeholder={'Search notes'}
49+
onChange={debounce((e) => onSearchChange(e), 250)}
50+
ref={inputRef}
51+
/>
52+
</SidebarListFilter>
53+
</SidebarListFilterWrapper>
54+
{searchedNodes?.map((nodeId) => (
55+
<NodeCard key={nodeId} nodeId={nodeId} />
56+
))}
57+
</SnippetCards>
58+
)
59+
}

0 commit comments

Comments
 (0)