From a32acd14495fe2d17f9c502811452d3018072b4f Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Mon, 9 Sep 2024 17:31:05 +0100 Subject: [PATCH 01/22] Bootstrap flow for importing blog post --- src/ui/App.tsx | 8 +++++++- src/ui/flows/blog-post/BlogPostFlow.tsx | 5 +++++ src/ui/session/ViewSession.tsx | 17 ++++++++++------- 3 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 src/ui/flows/blog-post/BlogPostFlow.tsx diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 6cf4322c..6980ac01 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -20,11 +20,14 @@ import { getSession, listSessions, Session } from '@/storage/session'; import { PlaceholderPreview } from '@/ui/preview/PlaceholderPreview'; import { SessionContext, SessionProvider } from '@/ui/session/SessionProvider'; import { ApiClient } from '@/api/ApiClient'; +import { BlogPostFlow } from '@/ui/flows/blog-post/BlogPostFlow'; export const Screens = { home: () => '/start/home', newSession: () => '/start/new-session', - viewSession: ( id: string ) => `/session/${ id }`, + viewSession: ( sessionId: string ) => `/session/${ sessionId }`, + flowBlogPost: ( sessionId: string ) => + `/session/${ sessionId }/flow/blog-post`, }; const homeLoader: LoaderFunction = async () => { @@ -63,6 +66,9 @@ function Routes( props: { initialScreen: string } ) { loader={ sessionLoader } > } /> + + } /> + ); diff --git a/src/ui/flows/blog-post/BlogPostFlow.tsx b/src/ui/flows/blog-post/BlogPostFlow.tsx new file mode 100644 index 00000000..65834034 --- /dev/null +++ b/src/ui/flows/blog-post/BlogPostFlow.tsx @@ -0,0 +1,5 @@ +export function BlogPostFlow() { + return ( + Navigate to the page of the post you'd like to import + ); +} diff --git a/src/ui/session/ViewSession.tsx b/src/ui/session/ViewSession.tsx index 4f6a1597..4bfef26f 100644 --- a/src/ui/session/ViewSession.tsx +++ b/src/ui/session/ViewSession.tsx @@ -1,6 +1,8 @@ import { useSessionContext } from '@/ui/session/SessionProvider'; import { Post } from '@/api/ApiClient'; import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { Screens } from '@/ui/App'; export function ViewSession() { const { session, apiClient } = useSessionContext(); @@ -18,14 +20,15 @@ export function ViewSession() { return ( <> -
view session: { session.id }
- { apiClient?.siteUrl ? ( -
url: { apiClient.siteUrl }
- ) : null } +

+ { session.title } ({ session.url }) +

); From 1e64dcc6788090caf549fa0380c1761fef185acc Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 10 Sep 2024 11:35:05 +0100 Subject: [PATCH 02/22] Scaffold import post flow --- src/ui/flows/blog-post/BlogPostFlow.tsx | 78 ++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/src/ui/flows/blog-post/BlogPostFlow.tsx b/src/ui/flows/blog-post/BlogPostFlow.tsx index 65834034..501480e3 100644 --- a/src/ui/flows/blog-post/BlogPostFlow.tsx +++ b/src/ui/flows/blog-post/BlogPostFlow.tsx @@ -1,5 +1,81 @@ +import { useState } from 'react'; + +enum Steps { + Start = 1, + SelectTitle, + SelectContent, + Import, +} + export function BlogPostFlow() { + const [ currentStep, setCurrentStep ] = useState( Steps.Start ); + + const startElement = + currentStep !== Steps.Start ? null : ( + <> +
+ Navigate to the page of the post you'd like to import +
+ + + ); + + const selectTitleElement = + currentStep !== Steps.SelectTitle ? null : ( + <> +
Select the title of the post
+ + + ); + + const selectContentElement = + currentStep !== Steps.SelectContent ? null : ( + <> +
Select the content of the post
+ + + ); + + const importElement = + currentStep !== Steps.Import ? null : ( + <> +
Click import to import the post
+ + + ); + return ( - Navigate to the page of the post you'd like to import + <> + { currentStep === Steps.Start ? startElement : null } + { currentStep === Steps.SelectTitle ? selectTitleElement : null } + { currentStep === Steps.SelectContent + ? selectContentElement + : null } + { currentStep === Steps.Import ? importElement : null } + ); } From e12c3aaeb54995f6247d15c00d82dfc4e683d9ac Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 10 Sep 2024 11:45:18 +0100 Subject: [PATCH 03/22] Send message to the content script to enable and disable highlighting --- src/api/ContentApi.ts | 52 +++++++++++++++++++++++++ src/extension/content.ts | 21 ++++++++++ src/ui/flows/blog-post/BlogPostFlow.tsx | 9 ++++- 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/api/ContentApi.ts diff --git a/src/api/ContentApi.ts b/src/api/ContentApi.ts new file mode 100644 index 00000000..49d78135 --- /dev/null +++ b/src/api/ContentApi.ts @@ -0,0 +1,52 @@ +export const Namespace = 'TRY_WORDPRESS'; + +export enum Actions { + EnableHighlighting = 1, + DisableHighlighting, +} + +export interface Message { + namespace: string; + action: number; + payload?: object; +} + +export class ContentApi { + async enableHighlighting(): Promise< void > { + return this.sendMessageToContent( { + action: Actions.EnableHighlighting, + } ); + } + + async disableHighlighting(): Promise< void > { + return this.sendMessageToContent( { + action: Actions.DisableHighlighting, + } ); + } + + private async sendMessageToContent( + message: Omit< Message, 'namespace' > + ): Promise< void > { + const currentTabId = await this.getCurrentTabId(); + if ( ! currentTabId ) { + throw Error( 'current tab not found' ); + } + const messageWithNamespace: Message = { + namespace: Namespace, + action: message.action, + payload: message.payload, + }; + await browser.tabs.sendMessage( currentTabId, messageWithNamespace ); + } + + private async getCurrentTabId(): Promise< number | undefined > { + const tabs = await browser.tabs.query( { + currentWindow: true, + active: true, + } ); + if ( tabs.length !== 1 ) { + return; + } + return tabs[ 0 ]?.id; + } +} diff --git a/src/extension/content.ts b/src/extension/content.ts index e69de29b..fe9109c9 100644 --- a/src/extension/content.ts +++ b/src/extension/content.ts @@ -0,0 +1,21 @@ +import { Actions, Message, Namespace } from '@/api/ContentApi'; + +browser.runtime.onMessage.addListener( + ( message: Message, sender, sendResponse ) => { + if ( message.namespace !== Namespace ) { + return; + } + + switch ( message.action ) { + case Actions.EnableHighlighting: + console.log( message.action ); + break; + case Actions.DisableHighlighting: + console.log( message.action ); + break; + default: + console.error( `Unknown action: ${ message.action }` ); + break; + } + } +); diff --git a/src/ui/flows/blog-post/BlogPostFlow.tsx b/src/ui/flows/blog-post/BlogPostFlow.tsx index 501480e3..dc5dbace 100644 --- a/src/ui/flows/blog-post/BlogPostFlow.tsx +++ b/src/ui/flows/blog-post/BlogPostFlow.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { ContentApi } from '@/api/ContentApi'; enum Steps { Start = 1, @@ -7,6 +8,8 @@ enum Steps { Import, } +const contentApi = new ContentApi(); + export function BlogPostFlow() { const [ currentStep, setCurrentStep ] = useState( Steps.Start ); @@ -17,7 +20,8 @@ export function BlogPostFlow() { Navigate to the page of the post you'd like to import - - ); - - const selectTitleElement = - currentStep !== Steps.SelectTitle ? null : ( - <> -
Select the title of the post
- - - ); + const startElement = ( + setCurrentStep( Steps.selectContent ) } /> + ); - const selectContentElement = - currentStep !== Steps.SelectContent ? null : ( - <> -
Select the content of the post
- - - ); + const selectContentElement = ( + setCurrentStep( Steps.import ) } /> + ); - const importElement = - currentStep !== Steps.Import ? null : ( - <> -
Click import to import the post
- - - ); + const importElement = console.log( 'import' ) } />; return ( <> - { currentStep === Steps.Start ? startElement : null } - { currentStep === Steps.SelectTitle ? selectTitleElement : null } - { currentStep === Steps.SelectContent + { currentStep === Steps.start ? startElement : null } + { currentStep === Steps.selectContent ? selectContentElement : null } - { currentStep === Steps.Import ? importElement : null } + { currentStep === Steps.import ? importElement : null } + + ); +} + +function Start( props: { onExit: () => void } ) { + const { onExit } = props; + return ( + <> +
+ Navigate to the page of the post you'd like to import +
+ + + ); +} + +function SelectContent( props: { onExit: () => void } ) { + const { onExit } = props; + return ( + <> +
Select the content of the post
+ + + ); +} + +function Import( props: { onExit: () => void } ) { + const { onExit } = props; + return ( + <> +
Click import to import the post
+ ); } From d71ab39e31d6c1a924ce63ca6e8e107296ae5f5e Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 10 Sep 2024 17:59:43 +0100 Subject: [PATCH 06/22] Send message when user clicks on post --- src/api/ContentApi.ts | 16 ++++++++++++++++ src/extension/content.ts | 5 ++++- src/ui/flows/blog-post/BlogPostFlow.tsx | 13 ++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/api/ContentApi.ts b/src/api/ContentApi.ts index 49d78135..21289281 100644 --- a/src/api/ContentApi.ts +++ b/src/api/ContentApi.ts @@ -3,6 +3,7 @@ export const Namespace = 'TRY_WORDPRESS'; export enum Actions { EnableHighlighting = 1, DisableHighlighting, + ImportPost, } export interface Message { @@ -24,6 +25,21 @@ export class ContentApi { } ); } + async importPost(): Promise< void > { + return this.sendMessageToApp( { action: Actions.ImportPost } ); + } + + private async sendMessageToApp( + message: Omit< Message, 'namespace' > + ): Promise< void > { + const messageWithNamespace: Message = { + namespace: Namespace, + action: message.action, + payload: message.payload, + }; + await browser.runtime.sendMessage( messageWithNamespace ); + } + private async sendMessageToContent( message: Omit< Message, 'namespace' > ): Promise< void > { diff --git a/src/extension/content.ts b/src/extension/content.ts index 4cb32659..9495334b 100644 --- a/src/extension/content.ts +++ b/src/extension/content.ts @@ -1,4 +1,6 @@ -import { Actions, Message, Namespace } from '@/api/ContentApi'; +import { Actions, ContentApi, Message, Namespace } from '@/api/ContentApi'; + +const contentApi = new ContentApi(); browser.runtime.onMessage.addListener( ( message: Message, sender, sendResponse ) => { @@ -26,6 +28,7 @@ browser.runtime.onMessage.addListener( function onClick( event: MouseEvent ) { event.preventDefault(); + void contentApi.importPost(); } function onMouseOver( event: MouseEvent ) { diff --git a/src/ui/flows/blog-post/BlogPostFlow.tsx b/src/ui/flows/blog-post/BlogPostFlow.tsx index d8b4dbb6..6c4f4852 100644 --- a/src/ui/flows/blog-post/BlogPostFlow.tsx +++ b/src/ui/flows/blog-post/BlogPostFlow.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { ContentApi } from '@/api/ContentApi'; enum Steps { @@ -54,6 +54,17 @@ function Start( props: { onExit: () => void } ) { function SelectContent( props: { onExit: () => void } ) { const { onExit } = props; + + useEffect( () => { + const listener = ( message: any ) => { + console.log( message ); + }; + browser.runtime.onMessage.addListener( listener ); + return () => { + browser.runtime.onMessage.removeListener( listener ); + }; + }, [] ); + return ( <>
Select the content of the post
From dc459c3a0eb5416020a31abc7fa9cabedff16b81 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 10 Sep 2024 18:06:34 +0100 Subject: [PATCH 07/22] Remove styles when disabling highlighting --- src/extension/content.ts | 15 +++++++++++++-- src/ui/flows/blog-post/BlogPostFlow.tsx | 3 ++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/extension/content.ts b/src/extension/content.ts index 9495334b..6d98b78c 100644 --- a/src/extension/content.ts +++ b/src/extension/content.ts @@ -1,6 +1,7 @@ import { Actions, ContentApi, Message, Namespace } from '@/api/ContentApi'; const contentApi = new ContentApi(); +let currentElement: HTMLElement | null = null; browser.runtime.onMessage.addListener( ( message: Message, sender, sendResponse ) => { @@ -18,6 +19,7 @@ browser.runtime.onMessage.addListener( document.body.removeEventListener( 'mouseover', onMouseOver ); document.body.removeEventListener( 'mouseout', onMouseOut ); document.body.removeEventListener( 'click', onClick ); + removeStyle(); break; default: console.error( `Unknown action: ${ message.action }` ); @@ -36,7 +38,8 @@ function onMouseOver( event: MouseEvent ) { if ( ! element ) { return; } - element.style.outline = '1px solid blue'; + currentElement = element; + currentElement.style.outline = '1px solid blue'; toggleCursorStyle(); } @@ -45,7 +48,15 @@ function onMouseOut( event: MouseEvent ) { if ( ! element ) { return; } - element.style.outline = ''; + removeStyle(); + currentElement = null; +} + +function removeStyle() { + if ( ! currentElement ) { + return; + } + currentElement.style.outline = ''; toggleCursorStyle(); } diff --git a/src/ui/flows/blog-post/BlogPostFlow.tsx b/src/ui/flows/blog-post/BlogPostFlow.tsx index 6c4f4852..de6f6cfd 100644 --- a/src/ui/flows/blog-post/BlogPostFlow.tsx +++ b/src/ui/flows/blog-post/BlogPostFlow.tsx @@ -56,7 +56,8 @@ function SelectContent( props: { onExit: () => void } ) { const { onExit } = props; useEffect( () => { - const listener = ( message: any ) => { + const listener = async ( message: any ) => { + await contentApi.disableHighlighting(); console.log( message ); }; browser.runtime.onMessage.addListener( listener ); From 8fbaf91c5bfbd8d45d8e3137f3874434a4d0cd94 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 10 Sep 2024 18:15:40 +0100 Subject: [PATCH 08/22] Pass the content of the selected element to the app --- src/api/ContentApi.ts | 11 ++++++++--- src/extension/content.ts | 7 ++++++- src/ui/flows/blog-post/BlogPostFlow.tsx | 8 +++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/api/ContentApi.ts b/src/api/ContentApi.ts index 21289281..88b85c1b 100644 --- a/src/api/ContentApi.ts +++ b/src/api/ContentApi.ts @@ -9,24 +9,29 @@ export enum Actions { export interface Message { namespace: string; action: number; - payload?: object; + payload: object; } export class ContentApi { async enableHighlighting(): Promise< void > { return this.sendMessageToContent( { action: Actions.EnableHighlighting, + payload: {}, } ); } async disableHighlighting(): Promise< void > { return this.sendMessageToContent( { action: Actions.DisableHighlighting, + payload: {}, } ); } - async importPost(): Promise< void > { - return this.sendMessageToApp( { action: Actions.ImportPost } ); + async importPost( content: string ): Promise< void > { + return this.sendMessageToApp( { + action: Actions.ImportPost, + payload: { content }, + } ); } private async sendMessageToApp( diff --git a/src/extension/content.ts b/src/extension/content.ts index 6d98b78c..3bb7d52d 100644 --- a/src/extension/content.ts +++ b/src/extension/content.ts @@ -30,7 +30,12 @@ browser.runtime.onMessage.addListener( function onClick( event: MouseEvent ) { event.preventDefault(); - void contentApi.importPost(); + const element = event.target as HTMLElement; + if ( ! element ) { + return; + } + const content = element.outerHTML.trim(); + void contentApi.importPost( content ); } function onMouseOver( event: MouseEvent ) { diff --git a/src/ui/flows/blog-post/BlogPostFlow.tsx b/src/ui/flows/blog-post/BlogPostFlow.tsx index de6f6cfd..0f2b23bb 100644 --- a/src/ui/flows/blog-post/BlogPostFlow.tsx +++ b/src/ui/flows/blog-post/BlogPostFlow.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { ContentApi } from '@/api/ContentApi'; +import { ContentApi, Message } from '@/api/ContentApi'; enum Steps { start = 1, @@ -54,11 +54,12 @@ function Start( props: { onExit: () => void } ) { function SelectContent( props: { onExit: () => void } ) { const { onExit } = props; + const [ content, setContent ] = useState< string >(); useEffect( () => { - const listener = async ( message: any ) => { + const listener = async ( message: Message ) => { await contentApi.disableHighlighting(); - console.log( message ); + setContent( ( message.payload as any ).content ); }; browser.runtime.onMessage.addListener( listener ); return () => { @@ -77,6 +78,7 @@ function SelectContent( props: { onExit: () => void } ) { > Continue + { ! content ? null :
{ content }
} ); } From 576b35d178fc755e649b1b027dab05ac69aa99a3 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Tue, 10 Sep 2024 18:37:25 +0100 Subject: [PATCH 09/22] Split ContentApi into a ContentBus and AppBus --- src/api/ContentApi.ts | 73 ------------------------ src/bus/AppBus.ts | 50 +++++++++++++++++ src/bus/ContentBus.ts | 74 +++++++++++++++++++++++++ src/bus/Message.ts | 9 +++ src/extension/content.ts | 47 +++++++--------- src/ui/flows/blog-post/BlogPostFlow.tsx | 25 +++++---- 6 files changed, 168 insertions(+), 110 deletions(-) delete mode 100644 src/api/ContentApi.ts create mode 100644 src/bus/AppBus.ts create mode 100644 src/bus/ContentBus.ts create mode 100644 src/bus/Message.ts diff --git a/src/api/ContentApi.ts b/src/api/ContentApi.ts deleted file mode 100644 index 88b85c1b..00000000 --- a/src/api/ContentApi.ts +++ /dev/null @@ -1,73 +0,0 @@ -export const Namespace = 'TRY_WORDPRESS'; - -export enum Actions { - EnableHighlighting = 1, - DisableHighlighting, - ImportPost, -} - -export interface Message { - namespace: string; - action: number; - payload: object; -} - -export class ContentApi { - async enableHighlighting(): Promise< void > { - return this.sendMessageToContent( { - action: Actions.EnableHighlighting, - payload: {}, - } ); - } - - async disableHighlighting(): Promise< void > { - return this.sendMessageToContent( { - action: Actions.DisableHighlighting, - payload: {}, - } ); - } - - async importPost( content: string ): Promise< void > { - return this.sendMessageToApp( { - action: Actions.ImportPost, - payload: { content }, - } ); - } - - private async sendMessageToApp( - message: Omit< Message, 'namespace' > - ): Promise< void > { - const messageWithNamespace: Message = { - namespace: Namespace, - action: message.action, - payload: message.payload, - }; - await browser.runtime.sendMessage( messageWithNamespace ); - } - - private async sendMessageToContent( - message: Omit< Message, 'namespace' > - ): Promise< void > { - const currentTabId = await this.getCurrentTabId(); - if ( ! currentTabId ) { - throw Error( 'current tab not found' ); - } - const messageWithNamespace: Message = { - namespace: Namespace, - action: message.action, - payload: message.payload, - }; - await browser.tabs.sendMessage( currentTabId, messageWithNamespace ); - } - - private async getCurrentTabId(): Promise< number | undefined > { - const tabs = await browser.tabs.query( { - currentWindow: true, - active: true, - } ); - if ( tabs.length !== 1 ) { - return; - } - return tabs[ 0 ]?.id; - } -} diff --git a/src/bus/AppBus.ts b/src/bus/AppBus.ts new file mode 100644 index 00000000..68a9b91b --- /dev/null +++ b/src/bus/AppBus.ts @@ -0,0 +1,50 @@ +import { Listener, Message, Namespace } from '@/bus/Message'; + +enum Actions { + ElementClicked = 1, +} + +export const AppBus = { + namespace: `${ Namespace }_APP`, + actions: Actions, + listen, + stopListening, + elementClicked, +}; + +let listener: Listener; + +function listen( list: Listener ) { + stopListening(); + listener = ( message: Message ) => { + if ( message.namespace !== AppBus.namespace ) { + return; + } + list( message ); + }; + browser.runtime.onMessage.addListener( listener ); +} + +function stopListening() { + if ( listener ) { + browser.runtime.onMessage.removeListener( listener ); + } +} + +async function elementClicked( content: string ): Promise< void > { + return sendMessageToApp( { + action: Actions.ElementClicked, + payload: { content }, + } ); +} + +async function sendMessageToApp( + message: Omit< Message, 'namespace' > +): Promise< void > { + const messageWithNamespace: Message = { + namespace: AppBus.namespace, + action: message.action, + payload: message.payload, + }; + await browser.runtime.sendMessage( messageWithNamespace ); +} diff --git a/src/bus/ContentBus.ts b/src/bus/ContentBus.ts new file mode 100644 index 00000000..a4871928 --- /dev/null +++ b/src/bus/ContentBus.ts @@ -0,0 +1,74 @@ +import { Listener, Message, Namespace } from '@/bus/Message'; + +enum Actions { + EnableHighlighting = 1, + DisableHighlighting, +} + +export const ContentBus = { + namespace: `${ Namespace }_CONTENT`, + actions: Actions, + listen, + stopListening, + enableHighlighting, + disableHighlighting, +}; + +let listener: Listener; + +function listen( list: Listener ) { + stopListening(); + listener = ( message: Message ) => { + if ( message.namespace !== ContentBus.namespace ) { + return; + } + list( message ); + }; + browser.runtime.onMessage.addListener( listener ); +} + +function stopListening() { + if ( listener ) { + browser.runtime.onMessage.removeListener( listener ); + } +} + +async function enableHighlighting(): Promise< void > { + return sendMessageToContent( { + action: Actions.EnableHighlighting, + payload: {}, + } ); +} + +async function disableHighlighting(): Promise< void > { + return sendMessageToContent( { + action: Actions.DisableHighlighting, + payload: {}, + } ); +} + +async function sendMessageToContent( + message: Omit< Message, 'namespace' > +): Promise< void > { + const currentTabId = await getCurrentTabId(); + if ( ! currentTabId ) { + throw Error( 'current tab not found' ); + } + const messageWithNamespace: Message = { + namespace: ContentBus.namespace, + action: message.action, + payload: message.payload, + }; + await browser.tabs.sendMessage( currentTabId, messageWithNamespace ); +} + +async function getCurrentTabId(): Promise< number | undefined > { + const tabs = await browser.tabs.query( { + currentWindow: true, + active: true, + } ); + if ( tabs.length !== 1 ) { + return; + } + return tabs[ 0 ]?.id; +} diff --git a/src/bus/Message.ts b/src/bus/Message.ts new file mode 100644 index 00000000..877815a1 --- /dev/null +++ b/src/bus/Message.ts @@ -0,0 +1,9 @@ +export const Namespace = 'TRY_WORDPRESS'; + +export interface Message { + namespace: string; + action: number; + payload: object; +} + +export type Listener = ( message: Message ) => void; diff --git a/src/extension/content.ts b/src/extension/content.ts index 3bb7d52d..5ae9a498 100644 --- a/src/extension/content.ts +++ b/src/extension/content.ts @@ -1,32 +1,27 @@ -import { Actions, ContentApi, Message, Namespace } from '@/api/ContentApi'; +import { Message } from '@/bus/Message'; +import { ContentBus } from '@/bus/ContentBus'; +import { AppBus } from '@/bus/AppBus'; -const contentApi = new ContentApi(); let currentElement: HTMLElement | null = null; -browser.runtime.onMessage.addListener( - ( message: Message, sender, sendResponse ) => { - if ( message.namespace !== Namespace ) { - return; - } - - switch ( message.action ) { - case Actions.EnableHighlighting: - document.body.addEventListener( 'mouseover', onMouseOver ); - document.body.addEventListener( 'mouseout', onMouseOut ); - document.body.addEventListener( 'click', onClick ); - break; - case Actions.DisableHighlighting: - document.body.removeEventListener( 'mouseover', onMouseOver ); - document.body.removeEventListener( 'mouseout', onMouseOut ); - document.body.removeEventListener( 'click', onClick ); - removeStyle(); - break; - default: - console.error( `Unknown action: ${ message.action }` ); - break; - } +ContentBus.listen( ( message: Message ) => { + switch ( message.action ) { + case ContentBus.actions.EnableHighlighting: + document.body.addEventListener( 'mouseover', onMouseOver ); + document.body.addEventListener( 'mouseout', onMouseOut ); + document.body.addEventListener( 'click', onClick ); + break; + case ContentBus.actions.DisableHighlighting: + document.body.removeEventListener( 'mouseover', onMouseOver ); + document.body.removeEventListener( 'mouseout', onMouseOut ); + document.body.removeEventListener( 'click', onClick ); + removeStyle(); + break; + default: + console.error( `Unknown action: ${ message.action }` ); + break; } -); +} ); function onClick( event: MouseEvent ) { event.preventDefault(); @@ -35,7 +30,7 @@ function onClick( event: MouseEvent ) { return; } const content = element.outerHTML.trim(); - void contentApi.importPost( content ); + void AppBus.elementClicked( content ); } function onMouseOver( event: MouseEvent ) { diff --git a/src/ui/flows/blog-post/BlogPostFlow.tsx b/src/ui/flows/blog-post/BlogPostFlow.tsx index 0f2b23bb..762e0422 100644 --- a/src/ui/flows/blog-post/BlogPostFlow.tsx +++ b/src/ui/flows/blog-post/BlogPostFlow.tsx @@ -1,5 +1,7 @@ import { useEffect, useState } from 'react'; -import { ContentApi, Message } from '@/api/ContentApi'; +import { ContentBus } from '@/bus/ContentBus'; +import { Message } from '@/bus/Message'; +import { AppBus } from '@/bus/AppBus'; enum Steps { start = 1, @@ -7,8 +9,6 @@ enum Steps { import, } -const contentApi = new ContentApi(); - export function BlogPostFlow() { const [ currentStep, setCurrentStep ] = useState( Steps.start ); @@ -42,7 +42,7 @@ function Start( props: { onExit: () => void } ) { From e6542acd4f63c1cbec27f6eebec6b25431796c81 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Wed, 11 Sep 2024 14:28:09 +0100 Subject: [PATCH 11/22] Move action functions up --- src/bus/AppBus.ts | 15 +++++++-------- src/bus/ContentBus.ts | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/bus/AppBus.ts b/src/bus/AppBus.ts index 68a9b91b..eeab717c 100644 --- a/src/bus/AppBus.ts +++ b/src/bus/AppBus.ts @@ -4,6 +4,13 @@ enum Actions { ElementClicked = 1, } +async function elementClicked( content: string ): Promise< void > { + return sendMessageToApp( { + action: Actions.ElementClicked, + payload: { content }, + } ); +} + export const AppBus = { namespace: `${ Namespace }_APP`, actions: Actions, @@ -30,14 +37,6 @@ function stopListening() { browser.runtime.onMessage.removeListener( listener ); } } - -async function elementClicked( content: string ): Promise< void > { - return sendMessageToApp( { - action: Actions.ElementClicked, - payload: { content }, - } ); -} - async function sendMessageToApp( message: Omit< Message, 'namespace' > ): Promise< void > { diff --git a/src/bus/ContentBus.ts b/src/bus/ContentBus.ts index a4871928..36200f40 100644 --- a/src/bus/ContentBus.ts +++ b/src/bus/ContentBus.ts @@ -5,6 +5,20 @@ enum Actions { DisableHighlighting, } +async function enableHighlighting(): Promise< void > { + return sendMessageToContent( { + action: Actions.EnableHighlighting, + payload: {}, + } ); +} + +async function disableHighlighting(): Promise< void > { + return sendMessageToContent( { + action: Actions.DisableHighlighting, + payload: {}, + } ); +} + export const ContentBus = { namespace: `${ Namespace }_CONTENT`, actions: Actions, @@ -33,20 +47,6 @@ function stopListening() { } } -async function enableHighlighting(): Promise< void > { - return sendMessageToContent( { - action: Actions.EnableHighlighting, - payload: {}, - } ); -} - -async function disableHighlighting(): Promise< void > { - return sendMessageToContent( { - action: Actions.DisableHighlighting, - payload: {}, - } ); -} - async function sendMessageToContent( message: Omit< Message, 'namespace' > ): Promise< void > { From 0a91f7cad3fcd2c21fcf8d44b68e0c01f727d5f7 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Wed, 11 Sep 2024 14:31:53 +0100 Subject: [PATCH 12/22] Extract components to separate files --- src/ui/flows/blog-post/BlogPostFlow.tsx | 72 +----------------------- src/ui/flows/blog-post/Import.tsx | 15 +++++ src/ui/flows/blog-post/SelectContent.tsx | 38 +++++++++++++ src/ui/flows/blog-post/Start.tsx | 20 +++++++ 4 files changed, 76 insertions(+), 69 deletions(-) create mode 100644 src/ui/flows/blog-post/Import.tsx create mode 100644 src/ui/flows/blog-post/SelectContent.tsx create mode 100644 src/ui/flows/blog-post/Start.tsx diff --git a/src/ui/flows/blog-post/BlogPostFlow.tsx b/src/ui/flows/blog-post/BlogPostFlow.tsx index 762e0422..820d4e4b 100644 --- a/src/ui/flows/blog-post/BlogPostFlow.tsx +++ b/src/ui/flows/blog-post/BlogPostFlow.tsx @@ -2,6 +2,9 @@ import { useEffect, useState } from 'react'; import { ContentBus } from '@/bus/ContentBus'; import { Message } from '@/bus/Message'; import { AppBus } from '@/bus/AppBus'; +import { Start } from '@/ui/flows/blog-post/Start'; +import { SelectContent } from '@/ui/flows/blog-post/SelectContent'; +import { Import } from '@/ui/flows/blog-post/Import'; enum Steps { start = 1, @@ -32,72 +35,3 @@ export function BlogPostFlow() { ); } - -function Start( props: { onExit: () => void } ) { - const { onExit } = props; - return ( - <> -
- Navigate to the page of the post you'd like to import -
- - - ); -} - -function SelectContent( props: { onExit: () => void } ) { - const { onExit } = props; - const [ content, setContent ] = useState< string >(); - - useEffect( () => { - AppBus.listen( async ( message: Message ) => { - switch ( message.action ) { - case AppBus.actions.ElementClicked: - await ContentBus.disableHighlighting(); - setContent( ( message.payload as any ).content ); - break; - } - } ); - return () => { - AppBus.stopListening(); - }; - }, [] ); - - return ( - <> -
Select the content of the post
- - { ! content ? null :
{ content }
} - - ); -} - -function Import( props: { onExit: () => void } ) { - const { onExit } = props; - return ( - <> -
Click import to import the post
- - - ); -} diff --git a/src/ui/flows/blog-post/Import.tsx b/src/ui/flows/blog-post/Import.tsx new file mode 100644 index 00000000..a63bf386 --- /dev/null +++ b/src/ui/flows/blog-post/Import.tsx @@ -0,0 +1,15 @@ +export function Import( props: { onExit: () => void } ) { + const { onExit } = props; + return ( + <> +
Click import to import the post
+ + + ); +} diff --git a/src/ui/flows/blog-post/SelectContent.tsx b/src/ui/flows/blog-post/SelectContent.tsx new file mode 100644 index 00000000..69ec1ecd --- /dev/null +++ b/src/ui/flows/blog-post/SelectContent.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react'; +import { AppBus } from '@/bus/AppBus'; +import { Message } from '@/bus/Message'; +import { ContentBus } from '@/bus/ContentBus'; + +export function SelectContent( props: { onExit: () => void } ) { + const { onExit } = props; + const [ content, setContent ] = useState< string >(); + + useEffect( () => { + AppBus.listen( async ( message: Message ) => { + switch ( message.action ) { + case AppBus.actions.ElementClicked: + await ContentBus.disableHighlighting(); + setContent( ( message.payload as any ).content ); + break; + } + } ); + return () => { + AppBus.stopListening(); + }; + }, [] ); + + return ( + <> +
Select the content of the post
+ + { ! content ? null :
{ content }
} + + ); +} diff --git a/src/ui/flows/blog-post/Start.tsx b/src/ui/flows/blog-post/Start.tsx new file mode 100644 index 00000000..7724c66e --- /dev/null +++ b/src/ui/flows/blog-post/Start.tsx @@ -0,0 +1,20 @@ +import { ContentBus } from '@/bus/ContentBus'; + +export function Start( props: { onExit: () => void } ) { + const { onExit } = props; + return ( + <> +
+ Navigate to the page of the post you'd like to import +
+ + + ); +} From 588ebf0bac05e077f6784513946b42d0a9acd5e0 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Wed, 11 Sep 2024 16:22:33 +0100 Subject: [PATCH 13/22] Select different sections of the post --- src/ui/flows/blog-post/SelectContent.tsx | 114 ++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/src/ui/flows/blog-post/SelectContent.tsx b/src/ui/flows/blog-post/SelectContent.tsx index 69ec1ecd..78ebb0f5 100644 --- a/src/ui/flows/blog-post/SelectContent.tsx +++ b/src/ui/flows/blog-post/SelectContent.tsx @@ -3,17 +3,28 @@ import { AppBus } from '@/bus/AppBus'; import { Message } from '@/bus/Message'; import { ContentBus } from '@/bus/ContentBus'; +enum section { + title = 1, + date, + content, +} + export function SelectContent( props: { onExit: () => void } ) { const { onExit } = props; + const [ title, setTitle ] = useState< string >(); const [ content, setContent ] = useState< string >(); + const [ date, setDate ] = useState< string >(); + const [ lastClickedElement, setLastClickedElement ] = useState< string >(); + const [ waitingForSelection, setWaitingForSelection ] = useState< + section | false + >( false ); useEffect( () => { AppBus.listen( async ( message: Message ) => { switch ( message.action ) { case AppBus.actions.ElementClicked: await ContentBus.disableHighlighting(); - setContent( ( message.payload as any ).content ); - break; + setLastClickedElement( ( message.payload as any ).content ); } } ); return () => { @@ -21,10 +32,27 @@ export function SelectContent( props: { onExit: () => void } ) { }; }, [] ); + if ( lastClickedElement ) { + switch ( waitingForSelection ) { + case section.title: + setTitle( lastClickedElement ); + break; + case section.date: + setDate( lastClickedElement ); + break; + case section.content: + setContent( lastClickedElement ); + break; + } + setWaitingForSelection( false ); + setLastClickedElement( undefined ); + } + return ( <>
Select the content of the post
- { ! content ? null :
{ content }
} +
{ + setWaitingForSelection( isWaiting ? section.title : false ); + } } + /> +
{ + setWaitingForSelection( isWaiting ? section.date : false ); + } } + /> +
{ + setWaitingForSelection( + isWaiting ? section.content : false + ); + } } + /> + + ); +} + +function Section( props: { + label: string; + value: string | undefined; + disabled: boolean; + waitingForSelection: boolean; + onWaitingForSelection: ( isWaiting: boolean ) => void; +} ) { + const { + label, + value, + disabled, + waitingForSelection, + onWaitingForSelection, + } = props; + + return ( + <> +
+ { label }: { value ?? 'Not found' } +
+ + { ! waitingForSelection ? null : ( + + ) } ); } From 535a6d8dba551b5a1f2f088c6da6e219dc1af3d2 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Wed, 11 Sep 2024 17:11:57 +0100 Subject: [PATCH 14/22] Simplify component --- src/ui/flows/blog-post/BlogPostFlow.tsx | 26 +++++++++++-------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/ui/flows/blog-post/BlogPostFlow.tsx b/src/ui/flows/blog-post/BlogPostFlow.tsx index 820d4e4b..c324bac0 100644 --- a/src/ui/flows/blog-post/BlogPostFlow.tsx +++ b/src/ui/flows/blog-post/BlogPostFlow.tsx @@ -15,23 +15,19 @@ enum Steps { export function BlogPostFlow() { const [ currentStep, setCurrentStep ] = useState( Steps.start ); - const startElement = ( - setCurrentStep( Steps.selectContent ) } /> - ); - - const selectContentElement = ( - setCurrentStep( Steps.import ) } /> - ); - - const importElement = console.log( 'import' ) } />; - return ( <> - { currentStep === Steps.start ? startElement : null } - { currentStep === Steps.selectContent - ? selectContentElement - : null } - { currentStep === Steps.import ? importElement : null } + { currentStep !== Steps.start ? null : ( + setCurrentStep( Steps.selectContent ) } /> + ) } + { currentStep !== Steps.selectContent ? null : ( + setCurrentStep( Steps.import ) } + /> + ) } + { currentStep !== Steps.import ? null : ( + console.log( 'import' ) } /> + ) } ); } From 95ed780ebe83df2b6a9826ef9dcd610612f447c2 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Wed, 11 Sep 2024 17:15:43 +0100 Subject: [PATCH 15/22] Disable continue button unless all sections are filled --- src/ui/flows/blog-post/SelectContent.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/flows/blog-post/SelectContent.tsx b/src/ui/flows/blog-post/SelectContent.tsx index 78ebb0f5..5ea77384 100644 --- a/src/ui/flows/blog-post/SelectContent.tsx +++ b/src/ui/flows/blog-post/SelectContent.tsx @@ -48,11 +48,13 @@ export function SelectContent( props: { onExit: () => void } ) { setLastClickedElement( undefined ); } + const isValid = title && date && content; + return ( <>
Select the content of the post
- { ! waitingForSelection ? null : ( + { label }{ ' ' } - ) } - + { ! waitingForSelection ? null : ( + + ) } + +
{ value ?? 'Not found' }
+ ); } From 3c1b058d6c5983222b21b36f263f1aeaccd50220 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Thu, 12 Sep 2024 11:38:57 +0100 Subject: [PATCH 19/22] Add a Finish screen --- src/ui/flows/blog-post/BlogPostFlow.tsx | 15 +++++---------- src/ui/flows/blog-post/Finish.tsx | 7 +++++++ src/ui/flows/blog-post/Import.tsx | 15 --------------- src/ui/flows/blog-post/SelectContent.tsx | 3 ++- 4 files changed, 14 insertions(+), 26 deletions(-) create mode 100644 src/ui/flows/blog-post/Finish.tsx delete mode 100644 src/ui/flows/blog-post/Import.tsx diff --git a/src/ui/flows/blog-post/BlogPostFlow.tsx b/src/ui/flows/blog-post/BlogPostFlow.tsx index c324bac0..ed0c552f 100644 --- a/src/ui/flows/blog-post/BlogPostFlow.tsx +++ b/src/ui/flows/blog-post/BlogPostFlow.tsx @@ -1,15 +1,12 @@ -import { useEffect, useState } from 'react'; -import { ContentBus } from '@/bus/ContentBus'; -import { Message } from '@/bus/Message'; -import { AppBus } from '@/bus/AppBus'; +import { useState } from 'react'; import { Start } from '@/ui/flows/blog-post/Start'; import { SelectContent } from '@/ui/flows/blog-post/SelectContent'; -import { Import } from '@/ui/flows/blog-post/Import'; +import { Finish } from '@/ui/flows/blog-post/Finish'; enum Steps { start = 1, selectContent, - import, + finish, } export function BlogPostFlow() { @@ -22,12 +19,10 @@ export function BlogPostFlow() { ) } { currentStep !== Steps.selectContent ? null : ( setCurrentStep( Steps.import ) } + onExit={ () => setCurrentStep( Steps.finish ) } /> ) } - { currentStep !== Steps.import ? null : ( - console.log( 'import' ) } /> - ) } + { currentStep !== Steps.finish ? null : } ); } diff --git a/src/ui/flows/blog-post/Finish.tsx b/src/ui/flows/blog-post/Finish.tsx new file mode 100644 index 00000000..e8a7fd0d --- /dev/null +++ b/src/ui/flows/blog-post/Finish.tsx @@ -0,0 +1,7 @@ +export function Finish() { + return ( + <> + The post has been imported + + ); +} diff --git a/src/ui/flows/blog-post/Import.tsx b/src/ui/flows/blog-post/Import.tsx deleted file mode 100644 index a63bf386..00000000 --- a/src/ui/flows/blog-post/Import.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export function Import( props: { onExit: () => void } ) { - const { onExit } = props; - return ( - <> -
Click import to import the post
- - - ); -} diff --git a/src/ui/flows/blog-post/SelectContent.tsx b/src/ui/flows/blog-post/SelectContent.tsx index d1059ea5..566b1e10 100644 --- a/src/ui/flows/blog-post/SelectContent.tsx +++ b/src/ui/flows/blog-post/SelectContent.tsx @@ -57,10 +57,11 @@ export function SelectContent( props: { onExit: () => void } ) { disabled={ ! isValid } onClick={ async () => { await ContentBus.disableHighlighting(); + console.log( 'TODO: import' ); onExit(); } } > - Continue + Import
Date: Thu, 12 Sep 2024 11:39:54 +0100 Subject: [PATCH 20/22] Remove unused code --- src/ui/session/ViewSession.tsx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/ui/session/ViewSession.tsx b/src/ui/session/ViewSession.tsx index 8cec1a5e..7bdea24d 100644 --- a/src/ui/session/ViewSession.tsx +++ b/src/ui/session/ViewSession.tsx @@ -1,24 +1,11 @@ import { useSessionContext } from '@/ui/session/SessionProvider'; -import { Post } from '@/api/ApiClient'; -import { useEffect, useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { Screens } from '@/ui/App'; export function ViewSession() { - const { session, apiClient } = useSessionContext(); + const { session } = useSessionContext(); const navigate = useNavigate(); - const [ posts, setPosts ] = useState< Post[] >( [] ); - useEffect( () => { - if ( ! apiClient ) { - return; - } - const getPosts = async () => { - setPosts( await apiClient.getPosts() ); - }; - void getPosts(); - }, [ apiClient?.siteUrl ] ); - return ( <>

From 675a3a0f4e13f9a6fe0a7c7f32036e942979faf1 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Thu, 12 Sep 2024 12:06:44 +0100 Subject: [PATCH 21/22] Disable highlighting when component unmounts --- src/ui/flows/blog-post/SelectContent.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/flows/blog-post/SelectContent.tsx b/src/ui/flows/blog-post/SelectContent.tsx index 566b1e10..5931fe86 100644 --- a/src/ui/flows/blog-post/SelectContent.tsx +++ b/src/ui/flows/blog-post/SelectContent.tsx @@ -28,6 +28,7 @@ export function SelectContent( props: { onExit: () => void } ) { } } ); return () => { + void ContentBus.disableHighlighting(); AppBus.stopListening(); }; }, [] ); From 1e721a3cfb581856971b98d0d88390dd1235110f Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Thu, 12 Sep 2024 12:38:50 +0100 Subject: [PATCH 22/22] Correctly disable cursor --- src/extension/content.ts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/extension/content.ts b/src/extension/content.ts index 66bc8321..6b5a49a7 100644 --- a/src/extension/content.ts +++ b/src/extension/content.ts @@ -10,11 +10,13 @@ ContentBus.listen( ( message: Message ) => { document.body.addEventListener( 'mouseover', onMouseOver ); document.body.addEventListener( 'mouseout', onMouseOut ); document.body.addEventListener( 'click', onClick ); + enableHighlightingCursor(); break; case ContentBus.actions.DisableHighlighting: document.body.removeEventListener( 'mouseover', onMouseOver ); document.body.removeEventListener( 'mouseout', onMouseOut ); document.body.removeEventListener( 'click', onClick ); + disableHighlightingCursor(); removeStyle(); break; default: @@ -43,7 +45,6 @@ function onMouseOver( event: MouseEvent ) { } currentElement = element; currentElement.style.outline = '1px solid blue'; - toggleCursorStyle(); } function onMouseOut( event: MouseEvent ) { @@ -60,24 +61,25 @@ function removeStyle() { return; } currentElement.style.outline = ''; - toggleCursorStyle(); } -function toggleCursorStyle() { - const styleId = 'hover-highlighter-style'; - let style = document.getElementById( styleId ); +const cursorStyleId = 'hover-highlighter-style'; + +function enableHighlightingCursor() { + let style = document.getElementById( cursorStyleId ); + if ( style ) { + // The highlighting cursor is already enabled. + return; + } + style = document.createElement( 'style' ); + style.id = cursorStyleId; + style.textContent = '* { cursor: crosshair !important; }'; + document.head.append( style ); +} + +function disableHighlightingCursor() { + const style = document.getElementById( cursorStyleId ); if ( style ) { - // If the style element exists, remove it style.remove(); - } else { - // If the style element does not exist, create and inject it - style = document.createElement( 'style' ); - style.id = styleId; - style.textContent = ` - *:hover { - cursor: crosshair !important; - } - `; - document.head.append( style ); } }