2020-09-15 15:01:07 +02:00
import * as React from 'react' ;
2024-11-08 17:32:05 +02:00
import ResizableLayout from './ResizableLayout/ResizableLayout' ;
import findItemByKey from './ResizableLayout/utils/findItemByKey' ;
import { MoveButtonClickEvent } from './ResizableLayout/MoveButtons' ;
import { move } from './ResizableLayout/utils/movements' ;
import { LayoutItem } from './ResizableLayout/utils/types' ;
2020-11-07 17:59:37 +02:00
import CommandService from '@joplin/lib/services/CommandService' ;
2022-04-17 13:41:27 +02:00
import { PluginHtmlContents , PluginStates , utils as pluginUtils } from '@joplin/lib/services/plugins/reducer' ;
2024-11-08 17:32:05 +02:00
import Sidebar from './Sidebar/Sidebar' ;
import UserWebview from '../services/plugins/UserWebview' ;
import UserWebviewDialog from '../services/plugins/UserWebviewDialog' ;
2020-11-07 17:59:37 +02:00
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController' ;
2024-11-08 17:32:05 +02:00
import { defaultWindowId , StateLastDeletion , stateUtils } from '@joplin/lib/reducer' ;
2020-11-07 17:59:37 +02:00
import { _ } from '@joplin/lib/locale' ;
2024-11-08 17:32:05 +02:00
import NoteListWrapper from './NoteListWrapper/NoteListWrapper' ;
import { AppState } from '../app.reducer' ;
import { saveLayout , loadLayout } from './ResizableLayout/utils/persist' ;
2020-11-13 19:09:28 +02:00
import Setting from '@joplin/lib/models/Setting' ;
2023-08-14 19:12:49 +02:00
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning' ;
2020-11-13 19:09:28 +02:00
import produce from 'immer' ;
import shim from '@joplin/lib/shim' ;
2024-11-08 17:32:05 +02:00
import bridge from '../services/bridge' ;
2020-11-13 19:09:28 +02:00
import styled from 'styled-components' ;
2024-04-11 09:35:20 +02:00
import { themeStyle , ThemeStyle } from '@joplin/lib/theme' ;
2024-11-08 17:32:05 +02:00
import validateLayout from './ResizableLayout/utils/validateLayout' ;
import iterateItems from './ResizableLayout/utils/iterateItems' ;
import removeItem from './ResizableLayout/utils/removeItem' ;
2021-08-23 19:47:07 +02:00
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService' ;
2021-05-13 18:57:37 +02:00
import { ShareInvitation } from '@joplin/lib/services/share/reducer' ;
2024-11-08 17:32:05 +02:00
import removeKeylessItems from './ResizableLayout/utils/removeKeylessItems' ;
2021-08-12 17:54:10 +02:00
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils' ;
2023-06-14 16:51:35 +02:00
import { isCallbackUrl , parseCallbackUrl } from '@joplin/lib/callbackUrlUtils' ;
2024-11-08 17:32:05 +02:00
import ElectronAppWrapper from '../ElectronAppWrapper' ;
2021-08-17 13:03:19 +02:00
import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils' ;
2022-04-14 10:58:34 +02:00
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types' ;
2024-04-15 19:17:34 +02:00
import invitationRespond from '@joplin/lib/services/share/invitationRespond' ;
2024-11-08 17:32:05 +02:00
import restart from '../services/restart' ;
import { connect } from 'react-redux' ;
2024-03-02 17:29:18 +02:00
import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType' ;
2024-11-08 17:32:05 +02:00
import validateColumns from './NoteListHeader/utils/validateColumns' ;
import TrashNotification from './TrashNotification/TrashNotification' ;
import UpdateNotification from './UpdateNotification/UpdateNotification' ;
import NoteEditor from './NoteEditor/NoteEditor' ;
2024-03-02 16:25:27 +02:00
2020-04-09 19:57:20 +02:00
const ipcRenderer = require ( 'electron' ) . ipcRenderer ;
2020-11-13 19:09:28 +02:00
interface Props {
plugins : PluginStates ;
2022-04-17 13:41:27 +02:00
pluginHtmlContents : PluginHtmlContents ;
2020-11-13 19:09:28 +02:00
pluginsLoaded : boolean ;
hasNotesBeingSaved : boolean ;
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-11-13 19:09:28 +02:00
dispatch : Function ;
mainLayout : LayoutItem ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-13 19:09:28 +02:00
style : any ;
layoutMoveMode : boolean ;
shouldUpgradeSyncTarget : boolean ;
hasDisabledSyncItems : boolean ;
hasDisabledEncryptionItems : boolean ;
2023-08-14 19:12:49 +02:00
hasMissingSyncCredentials : boolean ;
2020-11-13 19:09:28 +02:00
showMissingMasterKeyMessage : boolean ;
showNeedUpgradingMasterKeyMessage : boolean ;
showShouldReencryptMessage : boolean ;
themeId : number ;
2021-02-07 18:47:56 +02:00
startupPluginsLoaded : boolean ;
2021-05-13 18:57:37 +02:00
shareInvitations : ShareInvitation [ ] ;
2021-04-24 20:23:33 +02:00
isSafeMode : boolean ;
2024-08-02 15:47:26 +02:00
enableLegacyMarkdownEditor : boolean ;
2021-08-06 11:58:32 +02:00
needApiAuth : boolean ;
2021-09-25 19:00:43 +02:00
processingShareInvitationResponse : boolean ;
2023-02-17 15:07:18 +02:00
isResettingLayout : boolean ;
2023-09-18 18:40:36 +02:00
listRendererId : string ;
2024-03-02 16:25:27 +02:00
lastDeletion : StateLastDeletion ;
lastDeletionNotificationTime : number ;
selectedFolderId : string ;
2024-01-26 12:32:35 +02:00
mustUpgradeAppMessage : string ;
2024-03-02 17:29:18 +02:00
notesSortOrderField : string ;
notesSortOrderReverse : boolean ;
notesColumns : NoteListColumns ;
2024-07-01 17:21:17 +02:00
showInvalidJoplinCloudCredential : boolean ;
2020-11-13 19:09:28 +02:00
}
2021-05-13 18:57:37 +02:00
interface ShareFolderDialogOptions {
folderId : string ;
visible : boolean ;
}
2020-11-13 19:09:28 +02:00
interface State {
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-13 19:09:28 +02:00
promptOptions : any ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-13 19:09:28 +02:00
notePropertiesDialogOptions : any ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-13 19:09:28 +02:00
noteContentPropertiesDialogOptions : any ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-13 19:09:28 +02:00
shareNoteDialogOptions : any ;
2021-05-13 18:57:37 +02:00
shareFolderDialogOptions : ShareFolderDialogOptions ;
2020-11-13 19:09:28 +02:00
}
2020-10-09 19:35:46 +02:00
const StyledUserWebviewDialogContainer = styled . div `
display : flex ;
position : absolute ;
top : 0 ;
left : 0 ;
width : 100 % ;
height : 100 % ;
z - index : 1000 ;
box - sizing : border - box ;
` ;
2017-11-06 20:35:04 +02:00
2020-11-13 19:09:28 +02:00
const defaultLayout : LayoutItem = {
key : 'root' ,
children : [
{ key : 'sideBar' , width : 250 } ,
{ key : 'noteList' , width : 250 } ,
{ key : 'editor' } ,
] ,
} ;
class MainScreenComponent extends React . Component < Props , State > {
2020-09-15 15:01:07 +02:00
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-13 19:09:28 +02:00
private waitForNotesSavedIID_ : any ;
private styleKey_ : string ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-13 19:09:28 +02:00
private styles_ : any ;
2020-09-15 15:01:07 +02:00
2023-03-06 16:22:01 +02:00
public constructor ( props : Props ) {
2020-09-15 15:01:07 +02:00
super ( props ) ;
2020-10-09 19:35:46 +02:00
this . state = {
promptOptions : null ,
notePropertiesDialogOptions : { } ,
noteContentPropertiesDialogOptions : { } ,
shareNoteDialogOptions : { } ,
2021-05-13 18:57:37 +02:00
shareFolderDialogOptions : {
visible : false ,
folderId : '' ,
} ,
2020-10-09 19:35:46 +02:00
} ;
2020-11-13 19:09:28 +02:00
this . updateMainLayout ( this . buildLayout ( props . plugins ) ) ;
2020-10-09 19:35:46 +02:00
this . setupAppCloseHandling ( ) ;
this . resizableLayout_resize = this . resizableLayout_resize . bind ( this ) ;
this . resizableLayout_renderItem = this . resizableLayout_renderItem . bind ( this ) ;
2020-11-13 19:09:28 +02:00
this . resizableLayout_moveButtonClick = this . resizableLayout_moveButtonClick . bind ( this ) ;
2020-10-09 19:35:46 +02:00
this . window_resize = this . window_resize . bind ( this ) ;
this . rowHeight = this . rowHeight . bind ( this ) ;
2020-11-13 19:09:28 +02:00
this . layoutModeListenerKeyDown = this . layoutModeListenerKeyDown . bind ( this ) ;
2020-10-09 19:35:46 +02:00
window . addEventListener ( 'resize' , this . window_resize ) ;
2021-10-16 11:07:41 +02:00
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-10-16 11:07:41 +02:00
ipcRenderer . on ( 'asynchronous-message' , ( _event : any , message : string , args : any ) = > {
if ( message === 'openCallbackUrl' ) {
this . openCallbackUrl ( args . url ) ;
}
} ) ;
const initialCallbackUrl = ( bridge ( ) . electronApp ( ) as ElectronAppWrapper ) . initialCallbackUrl ( ) ;
if ( initialCallbackUrl ) {
this . openCallbackUrl ( initialCallbackUrl ) ;
}
}
private openCallbackUrl ( url : string ) {
2023-06-14 16:51:35 +02:00
if ( ! isCallbackUrl ( url ) ) throw new Error ( ` Invalid callback URL: ${ url } ` ) ;
2021-10-16 11:07:41 +02:00
const { command , params } = parseCallbackUrl ( url ) ;
void CommandService . instance ( ) . execute ( command . toString ( ) , params . id ) ;
2020-10-09 19:35:46 +02:00
}
2020-11-13 19:09:28 +02:00
private updateLayoutPluginViews ( layout : LayoutItem , plugins : PluginStates ) {
const infos = pluginUtils . viewInfosByType ( plugins , 'webview' ) ;
2020-09-15 15:01:07 +02:00
2020-11-13 19:09:28 +02:00
let newLayout = produce ( layout , ( draftLayout : LayoutItem ) = > {
for ( const info of infos ) {
if ( info . view . containerType !== ContainerType . Panel ) continue ;
2020-10-09 19:35:46 +02:00
2020-11-13 19:09:28 +02:00
const viewId = info . view . id ;
const existingItem = findItemByKey ( draftLayout , viewId ) ;
2020-10-09 19:35:46 +02:00
2020-11-13 19:09:28 +02:00
if ( ! existingItem ) {
draftLayout . children . push ( {
key : viewId ,
context : {
pluginId : info.plugin.id ,
} ,
} ) ;
}
}
} ) ;
2020-10-09 19:35:46 +02:00
2020-11-13 19:09:28 +02:00
// Remove layout items that belong to plugins that are no longer
// active.
const pluginIds = Object . keys ( plugins ) ;
const itemsToRemove : string [ ] = [ ] ;
iterateItems ( newLayout , ( _itemIndex : number , item : LayoutItem , _parent : LayoutItem ) = > {
if ( item . context && item . context . pluginId && ! pluginIds . includes ( item . context . pluginId ) ) {
itemsToRemove . push ( item . key ) ;
}
return true ;
} ) ;
2020-10-09 19:35:46 +02:00
2020-11-13 19:09:28 +02:00
for ( const itemKey of itemsToRemove ) {
newLayout = removeItem ( newLayout , itemKey ) ;
}
return newLayout !== layout ? validateLayout ( newLayout ) : layout ;
}
2020-10-09 19:35:46 +02:00
2021-05-13 18:57:37 +02:00
private showShareInvitationNotification ( props : Props ) : boolean {
2021-09-25 19:00:43 +02:00
if ( props . processingShareInvitationResponse ) return false ;
2021-05-13 18:57:37 +02:00
return ! ! props . shareInvitations . find ( i = > i . status === 0 ) ;
}
2020-11-13 19:09:28 +02:00
private buildLayout ( plugins : PluginStates ) : LayoutItem {
const rootLayoutSize = this . rootLayoutSize ( ) ;
2020-10-09 19:35:46 +02:00
2020-11-13 19:09:28 +02:00
const userLayout = Setting . value ( 'ui.layout' ) ;
let output = null ;
2020-10-09 19:35:46 +02:00
2020-11-13 19:09:28 +02:00
try {
2020-12-29 20:04:57 +02:00
output = loadLayout ( Object . keys ( userLayout ) . length ? userLayout : null , defaultLayout , rootLayoutSize ) ;
2020-11-13 19:09:28 +02:00
2024-02-26 12:16:23 +02:00
// For unclear reasons, layout items sometimes end up without a key.
2021-05-15 17:30:56 +02:00
// In that case, we can't do anything with them, so remove them
// here. It could be due to the deprecated plugin API, which allowed
// creating panel without a key, although in this case it should
// have been set automatically.
// https://github.com/laurent22/joplin/issues/4926
output = removeKeylessItems ( output ) ;
2020-11-13 19:09:28 +02:00
if ( ! findItemByKey ( output , 'sideBar' ) || ! findItemByKey ( output , 'noteList' ) || ! findItemByKey ( output , 'editor' ) ) {
throw new Error ( '"sideBar", "noteList" and "editor" must be present in the layout' ) ;
}
} catch ( error ) {
console . warn ( 'Could not load layout - restoring default layout:' , error ) ;
console . warn ( 'Layout was:' , userLayout ) ;
output = loadLayout ( null , defaultLayout , rootLayoutSize ) ;
2020-10-09 19:35:46 +02:00
}
2020-11-13 19:09:28 +02:00
return this . updateLayoutPluginViews ( output , plugins ) ;
2020-09-15 15:01:07 +02:00
}
2023-03-06 16:22:01 +02:00
private window_resize() {
2020-09-15 15:01:07 +02:00
this . updateRootLayoutSize ( ) ;
2019-02-16 03:12:43 +02:00
}
2023-03-06 16:22:01 +02:00
public setupAppCloseHandling() {
2020-04-09 19:57:20 +02:00
this . waitForNotesSavedIID_ = null ;
2024-02-26 12:16:23 +02:00
// This event is dispatched from the main process when the app is about
2020-04-09 19:57:20 +02:00
// to close. The renderer process must respond with the "appCloseReply"
// and tell the main process whether the app can really be closed or not.
// For example, it cannot be closed right away if a note is being saved.
// If a note is being saved, we wait till it is saved and then call
// "appCloseReply" again.
2021-04-24 20:23:33 +02:00
ipcRenderer . on ( 'appClose' , async ( ) = > {
2020-10-09 19:35:46 +02:00
if ( this . waitForNotesSavedIID_ ) shim . clearInterval ( this . waitForNotesSavedIID_ ) ;
2020-04-09 19:57:20 +02:00
this . waitForNotesSavedIID_ = null ;
2022-04-20 18:34:58 +02:00
const sendCanClose = async ( canClose : boolean ) = > {
if ( canClose ) {
Setting . setValue ( 'wasClosedSuccessfully' , true ) ;
await Setting . saveAll ( ) ;
}
ipcRenderer . send ( 'asynchronous-message' , 'appCloseReply' , { canClose } ) ;
} ;
await sendCanClose ( ! this . props . hasNotesBeingSaved ) ;
2020-04-09 19:57:20 +02:00
if ( this . props . hasNotesBeingSaved ) {
2020-10-09 19:35:46 +02:00
this . waitForNotesSavedIID_ = shim . setInterval ( ( ) = > {
2020-04-09 19:57:20 +02:00
if ( ! this . props . hasNotesBeingSaved ) {
2020-10-09 19:35:46 +02:00
shim . clearInterval ( this . waitForNotesSavedIID_ ) ;
2020-04-09 19:57:20 +02:00
this . waitForNotesSavedIID_ = null ;
2022-04-20 18:34:58 +02:00
void sendCanClose ( true ) ;
2020-04-09 19:57:20 +02:00
}
} , 50 ) ;
}
} ) ;
}
2023-03-06 16:22:01 +02:00
public updateMainLayout ( layout : LayoutItem ) {
2020-11-13 19:09:28 +02:00
this . props . dispatch ( {
type : 'MAIN_LAYOUT_SET' ,
value : layout ,
} ) ;
}
2023-03-06 16:22:01 +02:00
public updateRootLayoutSize() {
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-13 19:09:28 +02:00
this . updateMainLayout ( produce ( this . props . mainLayout , ( draft : any ) = > {
2020-09-15 15:01:07 +02:00
const s = this . rootLayoutSize ( ) ;
2020-10-09 19:35:46 +02:00
draft . width = s . width ;
draft . height = s . height ;
2020-11-13 19:09:28 +02:00
} ) ) ;
2020-09-15 15:01:07 +02:00
}
2023-03-06 16:22:01 +02:00
public componentDidUpdate ( prevProps : Props , prevState : State ) {
2020-10-09 19:35:46 +02:00
if ( prevProps . style . width !== this . props . style . width ||
prevProps . style . height !== this . props . style . height ||
this . messageBoxVisible ( prevProps ) !== this . messageBoxVisible ( this . props )
) {
2020-09-15 15:01:07 +02:00
this . updateRootLayoutSize ( ) ;
}
2020-09-21 18:31:25 +02:00
2020-10-09 19:35:46 +02:00
if ( prevProps . plugins !== this . props . plugins ) {
2020-11-13 19:09:28 +02:00
this . updateMainLayout ( this . updateLayoutPluginViews ( this . props . mainLayout , this . props . plugins ) ) ;
// this.setState({ layout: this.buildLayout(this.props.plugins) });
2020-10-09 19:35:46 +02:00
}
2020-11-13 19:09:28 +02:00
if ( this . props . mainLayout !== prevProps . mainLayout ) {
const toSave = saveLayout ( this . props . mainLayout ) ;
Setting . setValue ( 'ui.layout' , toSave ) ;
}
2021-02-06 14:17:30 +02:00
if ( prevState . promptOptions !== this . state . promptOptions ) {
this . props . dispatch ( {
type : ! prevState . promptOptions ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE' ,
name : 'promptDialog' ,
} ) ;
}
2023-02-17 15:07:18 +02:00
if ( this . props . isResettingLayout ) {
Setting . setValue ( 'ui.layout' , null ) ;
this . updateMainLayout ( this . buildLayout ( this . props . plugins ) ) ;
this . props . dispatch ( {
type : 'RESET_LAYOUT' ,
value : false ,
} ) ;
}
2020-11-13 19:09:28 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public layoutModeListenerKeyDown ( event : any ) {
2020-11-13 19:09:28 +02:00
if ( event . key !== 'Escape' ) return ;
if ( ! this . props . layoutMoveMode ) return ;
2020-11-25 16:40:25 +02:00
void CommandService . instance ( ) . execute ( 'toggleLayoutMoveMode' ) ;
2020-09-15 15:01:07 +02:00
}
2023-03-06 16:22:01 +02:00
public componentDidMount() {
2020-11-13 19:09:28 +02:00
window . addEventListener ( 'keydown' , this . layoutModeListenerKeyDown ) ;
2020-07-03 23:32:39 +02:00
}
2023-03-06 16:22:01 +02:00
public componentWillUnmount() {
2020-09-15 15:01:07 +02:00
window . removeEventListener ( 'resize' , this . window_resize ) ;
2020-11-13 19:09:28 +02:00
window . removeEventListener ( 'keydown' , this . layoutModeListenerKeyDown ) ;
2019-10-30 11:40:34 +02:00
}
2023-03-06 16:22:01 +02:00
public rootLayoutSize() {
2020-09-15 15:01:07 +02:00
return {
width : window.innerWidth ,
height : this.rowHeight ( ) ,
} ;
}
2023-03-06 16:22:01 +02:00
public rowHeight() {
2020-09-15 15:01:07 +02:00
if ( ! this . props ) return 0 ;
return this . props . style . height - ( this . messageBoxVisible ( ) ? this . messageBoxHeight ( ) : 0 ) ;
}
2023-03-06 16:22:01 +02:00
public messageBoxHeight() {
2020-09-15 15:01:07 +02:00
return 50 ;
}
2023-03-06 16:22:01 +02:00
public styles ( themeId : number , width : number , height : number , messageBoxVisible : boolean ) {
2020-11-13 19:09:28 +02:00
const styleKey = [ themeId , width , height , messageBoxVisible ] . join ( '_' ) ;
2017-11-30 01:03:10 +02:00
if ( styleKey === this . styleKey_ ) return this . styles_ ;
2017-11-06 22:54:58 +02:00
2017-11-30 01:03:10 +02:00
const theme = themeStyle ( themeId ) ;
2017-11-06 22:54:58 +02:00
2017-11-30 01:03:10 +02:00
this . styleKey_ = styleKey ;
2017-11-06 20:35:04 +02:00
2017-11-30 01:03:10 +02:00
this . styles_ = { } ;
this . styles_ . header = {
width : width ,
} ;
2017-12-05 01:57:13 +02:00
this . styles_ . messageBox = {
width : width ,
2020-09-15 15:01:07 +02:00
height : this.messageBoxHeight ( ) ,
2017-12-05 01:57:13 +02:00
display : 'flex' ,
alignItems : 'center' ,
paddingLeft : 10 ,
backgroundColor : theme.warningBackgroundColor ,
2019-07-29 14:13:23 +02:00
} ;
2017-12-05 01:57:13 +02:00
2020-09-15 15:01:07 +02:00
const rowHeight = height - ( messageBoxVisible ? this . styles_.messageBox.height : 0 ) ;
this . styles_ . rowHeight = rowHeight ;
2019-12-30 14:00:53 +02:00
2020-09-15 15:01:07 +02:00
this . styles_ . resizableLayout = {
height : rowHeight ,
} ;
2017-11-30 01:03:10 +02:00
this . styles_ . prompt = {
width : width ,
height : height ,
2017-11-08 19:51:55 +02:00
} ;
2017-11-30 01:03:10 +02:00
return this . styles_ ;
}
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2024-01-26 12:32:35 +02:00
private renderNotificationMessage ( message : string , callForAction : string = null , callForActionHandler : Function = null , callForAction2 : string = null , callForActionHandler2 : Function = null ) {
2021-06-21 20:30:20 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-06-21 20:30:20 +02:00
const urlStyle : any = { color : theme.colorWarnUrl , textDecoration : 'underline' } ;
2024-01-26 12:32:35 +02:00
if ( ! callForAction ) return < span > { message } < / span > ;
2021-06-21 20:30:20 +02:00
const cfa = (
< a href = "#" style = { urlStyle } onClick = { ( ) = > callForActionHandler ( ) } >
{ callForAction }
< / a >
) ;
const cfa2 = ! callForAction2 ? null : (
< a href = "#" style = { urlStyle } onClick = { ( ) = > callForActionHandler2 ( ) } >
{ callForAction2 }
< / a >
) ;
return (
< span >
{ message } { callForAction ? ' ' : '' }
{ cfa } { callForAction2 ? ' / ' : '' } { cfa2 }
< / span >
) ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2024-04-11 09:35:20 +02:00
public renderNotification ( theme : ThemeStyle , styles : any ) {
2020-03-13 19:42:50 +02:00
if ( ! this . messageBoxVisible ( ) ) return null ;
const onViewStatusScreen = ( ) = > {
this . props . dispatch ( {
type : 'NAV_GO' ,
routeName : 'Status' ,
} ) ;
} ;
const onViewEncryptionConfigScreen = ( ) = > {
this . props . dispatch ( {
type : 'NAV_GO' ,
routeName : 'Config' ,
props : {
defaultSection : 'encryption' ,
} ,
} ) ;
} ;
2024-07-01 17:21:17 +02:00
const onViewJoplinCloudLoginScreen = ( ) = > {
this . props . dispatch ( {
type : 'NAV_GO' ,
routeName : 'JoplinCloudLogin' ,
} ) ;
} ;
2023-08-14 19:12:49 +02:00
const onViewSyncSettingsScreen = ( ) = > {
this . props . dispatch ( {
type : 'NAV_GO' ,
routeName : 'Config' ,
props : {
defaultSection : 'sync' ,
} ,
} ) ;
} ;
2020-08-02 13:28:50 +02:00
const onRestartAndUpgrade = async ( ) = > {
Setting . setValue ( 'sync.upgradeState' , Setting . SYNC_UPGRADE_STATE_MUST_DO ) ;
await Setting . saveAll ( ) ;
2022-04-20 18:34:58 +02:00
await restart ( ) ;
2020-08-02 13:28:50 +02:00
} ;
2021-04-24 20:23:33 +02:00
const onDisableSafeModeAndRestart = async ( ) = > {
Setting . setValue ( 'isSafeMode' , false ) ;
await Setting . saveAll ( ) ;
2022-04-20 18:34:58 +02:00
await restart ( ) ;
2021-04-24 20:23:33 +02:00
} ;
2021-11-03 18:24:40 +02:00
const onInvitationRespond = async ( shareUserId : string , folderId : string , masterKey : MasterKeyEntity , accept : boolean ) = > {
await invitationRespond ( shareUserId , folderId , masterKey , accept ) ;
2021-05-13 18:57:37 +02:00
} ;
2020-03-13 19:42:50 +02:00
let msg = null ;
2021-04-24 20:23:33 +02:00
2021-06-22 20:57:04 +02:00
// When adding something here, don't forget to update the condition in
// this.messageBoxVisible()
2021-07-25 14:15:56 +02:00
if ( this . props . isSafeMode ) {
2021-06-21 20:30:20 +02:00
msg = this . renderNotificationMessage (
_ ( 'Safe mode is currently active. Note rendering and all plugins are temporarily disabled.' ) ,
_ ( 'Disable safe mode and restart' ) ,
2023-08-22 12:58:53 +02:00
onDisableSafeModeAndRestart ,
2021-04-24 20:23:33 +02:00
) ;
2023-08-14 19:12:49 +02:00
} else if ( this . props . hasMissingSyncCredentials ) {
msg = this . renderNotificationMessage (
_ ( 'The synchronisation password is missing.' ) ,
_ ( 'Set the password' ) ,
2023-08-22 12:58:53 +02:00
onViewSyncSettingsScreen ,
2023-08-14 19:12:49 +02:00
) ;
2021-04-24 20:23:33 +02:00
} else if ( this . props . shouldUpgradeSyncTarget ) {
2021-06-21 20:30:20 +02:00
msg = this . renderNotificationMessage (
_ ( 'The sync target needs to be upgraded before Joplin can sync. The operation may take a few minutes to complete and the app needs to be restarted. To proceed please click on the link.' ) ,
_ ( 'Restart and upgrade' ) ,
2023-08-22 12:58:53 +02:00
onRestartAndUpgrade ,
2020-08-02 13:28:50 +02:00
) ;
2021-05-13 18:57:37 +02:00
} else if ( this . props . hasDisabledEncryptionItems ) {
2021-06-21 20:30:20 +02:00
msg = this . renderNotificationMessage (
_ ( 'Some items cannot be decrypted.' ) ,
_ ( 'View them now' ) ,
2023-08-22 12:58:53 +02:00
onViewStatusScreen ,
2020-03-13 19:42:50 +02:00
) ;
2021-05-13 18:57:37 +02:00
} else if ( this . props . showNeedUpgradingMasterKeyMessage ) {
2021-06-21 20:30:20 +02:00
msg = this . renderNotificationMessage (
_ ( 'One of your master keys use an obsolete encryption method.' ) ,
_ ( 'View them now' ) ,
2023-08-22 12:58:53 +02:00
onViewEncryptionConfigScreen ,
2020-03-13 19:42:50 +02:00
) ;
2021-05-13 18:57:37 +02:00
} else if ( this . props . showShouldReencryptMessage ) {
2021-06-21 20:30:20 +02:00
msg = this . renderNotificationMessage (
_ ( 'The default encryption method has been changed, you should re-encrypt your data.' ) ,
_ ( 'More info' ) ,
2023-08-22 12:58:53 +02:00
onViewEncryptionConfigScreen ,
2020-03-13 19:42:50 +02:00
) ;
2021-05-13 18:57:37 +02:00
} else if ( this . showShareInvitationNotification ( this . props ) ) {
2021-11-14 20:33:47 +02:00
const invitation = this . props . shareInvitations . find ( inv = > inv . status === 0 ) ;
2021-05-13 18:57:37 +02:00
const sharer = invitation . share . user ;
2021-06-21 20:30:20 +02:00
msg = this . renderNotificationMessage (
_ ( '%s (%s) would like to share a notebook with you.' , sharer . full_name , sharer . email ) ,
_ ( 'Accept' ) ,
2021-11-03 18:24:40 +02:00
( ) = > onInvitationRespond ( invitation . id , invitation . share . folder_id , invitation . master_key , true ) ,
2021-06-21 20:30:20 +02:00
_ ( 'Reject' ) ,
2023-08-22 12:58:53 +02:00
( ) = > onInvitationRespond ( invitation . id , invitation . share . folder_id , invitation . master_key , false ) ,
2021-05-13 18:57:37 +02:00
) ;
} else if ( this . props . hasDisabledSyncItems ) {
2021-06-21 20:30:20 +02:00
msg = this . renderNotificationMessage (
_ ( 'Some items cannot be synchronised.' ) ,
_ ( 'View them now' ) ,
2023-08-22 12:58:53 +02:00
onViewStatusScreen ,
2020-03-13 19:42:50 +02:00
) ;
2021-05-13 18:57:37 +02:00
} else if ( this . props . showMissingMasterKeyMessage ) {
2021-06-21 20:30:20 +02:00
msg = this . renderNotificationMessage (
_ ( 'One or more master keys need a password.' ) ,
_ ( 'Set the password' ) ,
2023-08-22 12:58:53 +02:00
onViewEncryptionConfigScreen ,
2020-03-13 19:42:50 +02:00
) ;
2024-01-26 12:32:35 +02:00
} else if ( this . props . mustUpgradeAppMessage ) {
msg = this . renderNotificationMessage ( this . props . mustUpgradeAppMessage ) ;
2024-07-01 17:21:17 +02:00
} else if ( this . props . showInvalidJoplinCloudCredential ) {
msg = this . renderNotificationMessage (
_ ( 'Your Joplin Cloud credentials are invalid, please login.' ) ,
_ ( 'Login to Joplin Cloud.' ) ,
onViewJoplinCloudLoginScreen ,
) ;
2020-03-13 19:42:50 +02:00
}
return (
< div style = { styles . messageBox } >
< span style = { theme . textStyle } > { msg } < / span >
< / div >
) ;
}
2023-03-06 16:22:01 +02:00
public messageBoxVisible ( props : Props = null ) {
2020-10-09 19:35:46 +02:00
if ( ! props ) props = this . props ;
2024-01-26 12:32:35 +02:00
return props . hasDisabledSyncItems ||
props . showMissingMasterKeyMessage ||
props . hasMissingSyncCredentials ||
props . showNeedUpgradingMasterKeyMessage ||
props . showShouldReencryptMessage ||
props . hasDisabledEncryptionItems ||
this . props . shouldUpgradeSyncTarget ||
props . isSafeMode ||
this . showShareInvitationNotification ( props ) ||
this . props . needApiAuth ||
2024-07-01 17:21:17 +02:00
! ! this . props . mustUpgradeAppMessage ||
props . showInvalidJoplinCloudCredential ;
2020-03-13 19:42:50 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
private resizableLayout_resize ( event : any ) {
2020-11-13 19:09:28 +02:00
this . updateMainLayout ( event . layout ) ;
}
2023-03-06 16:22:01 +02:00
private resizableLayout_moveButtonClick ( event : MoveButtonClickEvent ) {
2020-11-13 19:09:28 +02:00
const newLayout = move ( this . props . mainLayout , event . itemKey , event . direction ) ;
this . updateMainLayout ( newLayout ) ;
2020-09-15 15:01:07 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
private resizableLayout_renderItem ( key : string , event : any ) {
2021-01-28 00:52:54 +02:00
// Key should never be undefined but somehow it can happen, also not
// clear how. For now in this case render nothing so that the app
// doesn't crash.
// https://discourse.joplinapp.org/t/rearranging-the-pannels-crushed-the-app-and-generated-fatal-error/14373?u=laurent
if ( ! key ) {
console . error ( 'resizableLayout_renderItem: Trying to render an item using an empty key. Full layout is:' , this . props . mainLayout ) ;
return null ;
}
2020-09-15 15:01:07 +02:00
const eventEmitter = event . eventEmitter ;
2021-01-12 01:33:10 +02:00
// const viewsToRemove:string[] = [];
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-13 19:09:28 +02:00
const components : any = {
sideBar : ( ) = > {
2021-01-12 14:28:55 +02:00
return < Sidebar key = { key } / > ;
2020-11-13 19:09:28 +02:00
} ,
noteList : ( ) = > {
return < NoteListWrapper
key = { key }
resizableLayoutEventEmitter = { eventEmitter }
visible = { event . visible }
size = { event . size }
themeId = { this . props . themeId }
2023-09-18 18:40:36 +02:00
listRendererId = { this . props . listRendererId }
startupPluginsLoaded = { this . props . startupPluginsLoaded }
2024-03-02 17:29:18 +02:00
notesSortOrderField = { this . props . notesSortOrderField }
notesSortOrderReverse = { this . props . notesSortOrderReverse }
columns = { this . props . notesColumns }
2024-03-02 16:25:27 +02:00
selectedFolderId = { this . props . selectedFolderId }
2020-11-13 19:09:28 +02:00
/ > ;
} ,
editor : ( ) = > {
2024-11-09 14:50:06 +02:00
return < div className = 'note-editor-wrapper' role = 'main' aria - label = { _ ( 'Note' ) } >
< NoteEditor
windowId = { defaultWindowId }
key = { key }
2024-11-10 16:04:46 +02:00
startupPluginsLoaded = { this . props . startupPluginsLoaded }
2024-11-09 14:50:06 +02:00
/ >
< / div > ;
2020-11-13 19:09:28 +02:00
} ,
} ;
if ( components [ key ] ) return components [ key ] ( ) ;
2021-02-07 18:47:56 +02:00
const viewsToRemove : string [ ] = [ ] ;
2020-11-13 19:09:28 +02:00
if ( key . indexOf ( 'plugin-view' ) === 0 ) {
const viewInfo = pluginUtils . viewInfoByViewId ( this . props . plugins , event . item . key ) ;
if ( ! viewInfo ) {
2021-02-07 18:47:56 +02:00
// Once all startup plugins have loaded, we know that all the
// views are ready so we can remove the orphans ones.
2021-01-12 01:33:10 +02:00
//
2021-02-07 18:47:56 +02:00
// Before they are loaded, there might be views that don't match
// any plugins, but that's only because it hasn't loaded yet.
if ( this . props . startupPluginsLoaded ) {
console . warn ( ` Could not find plugin associated with view: ${ event . item . key } ` ) ;
viewsToRemove . push ( event . item . key ) ;
}
2021-01-12 01:33:10 +02:00
} else {
const { view , plugin } = viewInfo ;
2022-04-17 13:41:27 +02:00
const html = this . props . pluginHtmlContents [ plugin . id ] ? . [ view . id ] ? ? '' ;
2020-11-13 19:09:28 +02:00
2021-01-12 01:33:10 +02:00
return < UserWebview
key = { view . id }
viewId = { view . id }
themeId = { this . props . themeId }
2022-04-17 13:41:27 +02:00
html = { html }
2021-01-12 01:33:10 +02:00
scripts = { view . scripts }
pluginId = { plugin . id }
borderBottom = { true }
fitToContent = { false }
/ > ;
}
} else {
throw new Error ( ` Invalid layout component: ${ key } ` ) ;
2020-09-15 15:01:07 +02:00
}
2021-02-07 18:47:56 +02:00
if ( viewsToRemove . length ) {
window . requestAnimationFrame ( ( ) = > {
let newLayout = this . props . mainLayout ;
for ( const itemKey of viewsToRemove ) {
newLayout = removeItem ( newLayout , itemKey ) ;
}
if ( newLayout !== this . props . mainLayout ) {
console . warn ( 'Removed invalid views:' , viewsToRemove ) ;
this . updateMainLayout ( newLayout ) ;
}
} ) ;
}
2020-09-15 15:01:07 +02:00
}
2023-03-06 16:22:01 +02:00
public renderPluginDialogs() {
2020-10-09 19:35:46 +02:00
const output = [ ] ;
const infos = pluginUtils . viewInfosByType ( this . props . plugins , 'webview' ) ;
for ( const info of infos ) {
const { plugin , view } = info ;
if ( view . containerType !== ContainerType . Dialog ) continue ;
if ( ! view . opened ) continue ;
2022-04-17 13:41:27 +02:00
const html = this . props . pluginHtmlContents [ plugin . id ] ? . [ view . id ] ? ? '' ;
2020-10-09 19:35:46 +02:00
output . push ( < UserWebviewDialog
key = { view . id }
viewId = { view . id }
themeId = { this . props . themeId }
2022-04-17 13:41:27 +02:00
html = { html }
2020-10-09 19:35:46 +02:00
scripts = { view . scripts }
pluginId = { plugin . id }
buttons = { view . buttons }
2021-08-18 13:09:45 +02:00
fitToContent = { view . fitToContent }
2020-10-09 19:35:46 +02:00
/ > ) ;
}
if ( ! output . length ) return null ;
return (
< StyledUserWebviewDialogContainer >
{ output }
< / StyledUserWebviewDialogContainer >
) ;
}
2023-03-06 16:22:01 +02:00
public render() {
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2023-06-01 13:02:36 +02:00
const style = {
color : theme.color ,
backgroundColor : theme.backgroundColor ,
. . . this . props . style ,
} ;
2020-11-13 19:09:28 +02:00
const styles = this . styles ( this . props . themeId , style . width , style . height , this . messageBoxVisible ( ) ) ;
2018-03-20 01:04:48 +02:00
2020-03-13 19:42:50 +02:00
const messageComp = this . renderNotification ( theme , styles ) ;
2017-12-05 01:57:13 +02:00
2020-11-13 19:09:28 +02:00
const layoutComp = this . props . mainLayout ? (
< ResizableLayout
height = { styles . rowHeight }
layout = { this . props . mainLayout }
onResize = { this . resizableLayout_resize }
onMoveButtonClick = { this . resizableLayout_moveButtonClick }
renderItem = { this . resizableLayout_renderItem }
moveMode = { this . props . layoutMoveMode }
moveModeMessage = { _ ( 'Use the arrows to move the layout items. Press "Escape" to exit.' ) }
/ >
) : null ;
2017-11-06 20:35:04 +02:00
return (
< div style = { style } >
2024-03-02 16:25:27 +02:00
< TrashNotification
lastDeletion = { this . props . lastDeletion }
lastDeletionNotificationTime = { this . props . lastDeletionNotificationTime }
themeId = { this . props . themeId }
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2024-03-02 16:25:27 +02:00
dispatch = { this . props . dispatch as any }
/ >
2024-08-08 11:49:21 +02:00
< UpdateNotification themeId = { this . props . themeId } / >
2017-12-05 01:57:13 +02:00
{ messageComp }
2020-11-13 19:09:28 +02:00
{ layoutComp }
2017-11-06 20:35:04 +02:00
< / div >
) ;
}
}
2020-11-13 19:09:28 +02:00
const mapStateToProps = ( state : AppState ) = > {
2021-08-12 17:54:10 +02:00
const syncInfo = localSyncInfoFromState ( state ) ;
2023-05-29 12:27:53 +02:00
const showNeedUpgradingEnabledMasterKeyMessage = ! ! EncryptionService . instance ( ) . masterKeysThatNeedUpgrading ( syncInfo . masterKeys . filter ( ( k ) = > ! ! k . enabled ) ) . length ;
2024-11-08 17:32:05 +02:00
const windowState = stateUtils . windowStateById ( state , defaultWindowId ) ;
2021-08-12 17:54:10 +02:00
2017-11-06 22:54:58 +02:00
return {
2020-09-15 15:01:07 +02:00
themeId : state.settings.theme ,
2017-12-05 20:56:39 +02:00
hasDisabledSyncItems : state.hasDisabledSyncItems ,
2020-03-13 19:42:50 +02:00
hasDisabledEncryptionItems : state.hasDisabledEncryptionItems ,
2021-08-17 13:03:19 +02:00
showMissingMasterKeyMessage : showMissingMasterKeyMessage ( syncInfo , state . notLoadedMasterKeys ) ,
2023-05-29 12:27:53 +02:00
showNeedUpgradingMasterKeyMessage : showNeedUpgradingEnabledMasterKeyMessage ,
2020-03-13 19:42:50 +02:00
showShouldReencryptMessage : state.settings [ 'encryption.shouldReencrypt' ] >= Setting . SHOULD_REENCRYPT_YES ,
2020-08-02 13:28:50 +02:00
shouldUpgradeSyncTarget : state.settings [ 'sync.upgradeState' ] === Setting . SYNC_UPGRADE_STATE_SHOULD_DO ,
2023-08-14 19:12:49 +02:00
hasMissingSyncCredentials : shouldShowMissingPasswordWarning ( state . settings [ 'sync.target' ] , state . settings ) ,
2020-10-09 19:35:46 +02:00
plugins : state.pluginService.plugins ,
2022-04-17 13:41:27 +02:00
pluginHtmlContents : state.pluginService.pluginHtmlContents ,
2020-04-09 19:57:20 +02:00
hasNotesBeingSaved : stateUtils.hasNotesBeingSaved ( state ) ,
2020-11-13 19:09:28 +02:00
layoutMoveMode : state.layoutMoveMode ,
mainLayout : state.mainLayout ,
2021-02-07 18:47:56 +02:00
startupPluginsLoaded : state.startupPluginsLoaded ,
2021-05-13 18:57:37 +02:00
shareInvitations : state.shareService.shareInvitations ,
2021-09-25 19:00:43 +02:00
processingShareInvitationResponse : state.shareService.processingShareInvitationResponse ,
2021-04-24 20:23:33 +02:00
isSafeMode : state.settings.isSafeMode ,
2024-08-02 15:47:26 +02:00
enableLegacyMarkdownEditor : state.settings [ 'editor.legacyMarkdown' ] ,
2021-08-06 11:58:32 +02:00
needApiAuth : state.needApiAuth ,
2023-02-17 15:07:18 +02:00
isResettingLayout : state.isResettingLayout ,
2023-09-18 18:40:36 +02:00
listRendererId : state.settings [ 'notes.listRendererId' ] ,
2024-03-02 16:25:27 +02:00
lastDeletion : state.lastDeletion ,
lastDeletionNotificationTime : state.lastDeletionNotificationTime ,
2024-11-08 17:32:05 +02:00
selectedFolderId : windowState.selectedFolderId ,
2024-01-26 12:32:35 +02:00
mustUpgradeAppMessage : state.mustUpgradeAppMessage ,
2024-03-02 17:29:18 +02:00
notesSortOrderField : state.settings [ 'notes.sortOrder.field' ] ,
notesSortOrderReverse : state.settings [ 'notes.sortOrder.reverse' ] ,
notesColumns : validateColumns ( state . settings [ 'notes.columns' ] ) ,
2024-07-01 17:21:17 +02:00
showInvalidJoplinCloudCredential : state.settings [ 'sync.target' ] === 10 && state . mustAuthenticate ,
2017-11-06 22:54:58 +02:00
} ;
2017-11-06 20:35:04 +02:00
} ;
2020-09-15 15:01:07 +02:00
export default connect ( mapStateToProps ) ( MainScreenComponent ) ;