2020-09-15 15:01:07 +02:00
import * as React from 'react' ;
2020-11-13 19:09:28 +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-10-09 19:35:46 +02:00
import NoteEditor from '../NoteEditor/NoteEditor' ;
import NoteContentPropertiesDialog from '../NoteContentPropertiesDialog' ;
import ShareNoteDialog from '../ShareNoteDialog' ;
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' ;
2021-01-12 14:28:55 +02:00
import Sidebar from '../Sidebar/Sidebar' ;
2020-10-09 19:35:46 +02:00
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' ;
import { stateUtils } from '@joplin/lib/reducer' ;
2020-10-09 19:35:46 +02:00
import InteropServiceHelper from '../../InteropServiceHelper' ;
2020-11-07 17:59:37 +02:00
import { _ } from '@joplin/lib/locale' ;
2020-11-13 19:09:28 +02:00
import NoteListWrapper from '../NoteListWrapper/NoteListWrapper' ;
2021-09-04 19:11:29 +02:00
import { AppState } from '../../app.reducer' ;
2020-11-13 19:09:28 +02:00
import { saveLayout , loadLayout } from '../ResizableLayout/utils/persist' ;
import Setting from '@joplin/lib/models/Setting' ;
import produce from 'immer' ;
import shim from '@joplin/lib/shim' ;
import bridge from '../../services/bridge' ;
import time from '@joplin/lib/time' ;
import styled from 'styled-components' ;
import { themeStyle } from '@joplin/lib/theme' ;
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 ShareFolderDialog from '../ShareFolderDialog/ShareFolderDialog' ;
import { ShareInvitation } from '@joplin/lib/services/share/reducer' ;
2021-05-15 17:30:56 +02:00
import removeKeylessItems from '../ResizableLayout/utils/removeKeylessItems' ;
2021-08-12 17:54:10 +02:00
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils' ;
2021-10-16 11:07:41 +02:00
import { parseCallbackUrl } from '@joplin/lib/callbackUrlUtils' ;
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' ;
2021-09-04 14:43:25 +02:00
import commands from './commands/index' ;
2021-10-15 17:16:02 +02:00
import invitationRespond from '../../services/share/invitationRespond' ;
2022-04-20 18:34:58 +02:00
import restart from '../../services/restart' ;
2017-11-06 20:35:04 +02:00
const { connect } = require ( 'react-redux' ) ;
2022-11-13 14:13:33 +02:00
import PromptDialog from '../PromptDialog' ;
2023-01-19 19:19:06 +02:00
import NotePropertiesDialog from '../NotePropertiesDialog' ;
2020-11-07 17:59:37 +02:00
const PluginManager = require ( '@joplin/lib/services/PluginManager' ) ;
2020-04-09 19:57:20 +02:00
const ipcRenderer = require ( 'electron' ) . ipcRenderer ;
2020-11-13 19:09:28 +02:00
interface LayerModalState {
visible : boolean ;
message : string ;
}
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 ;
dispatch : Function ;
mainLayout : LayoutItem ;
style : any ;
layoutMoveMode : boolean ;
editorNoteStatuses : any ;
customCss : string ;
shouldUpgradeSyncTarget : boolean ;
hasDisabledSyncItems : boolean ;
hasDisabledEncryptionItems : boolean ;
showMissingMasterKeyMessage : boolean ;
showNeedUpgradingMasterKeyMessage : boolean ;
showShouldReencryptMessage : boolean ;
2021-08-06 11:58:32 +02:00
showInstallTemplatesPlugin : boolean ;
2020-11-13 19:09:28 +02:00
themeId : number ;
settingEditorCodeView : boolean ;
pluginsLegacy : any ;
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 ;
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 ;
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 {
promptOptions : any ;
modalLayer : LayerModalState ;
notePropertiesDialogOptions : any ;
noteContentPropertiesDialogOptions : any ;
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
2020-11-13 19:09:28 +02:00
private waitForNotesSavedIID_ : any ;
private isPrinting_ : boolean ;
private styleKey_ : string ;
private styles_ : any ;
private promptOnClose_ : Function ;
2020-09-15 15:01:07 +02:00
2020-11-13 19:09:28 +02:00
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 ,
modalLayer : {
visible : false ,
message : '' ,
} ,
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 . registerCommands ( ) ;
this . setupAppCloseHandling ( ) ;
this . notePropertiesDialog_close = this . notePropertiesDialog_close . bind ( this ) ;
this . noteContentPropertiesDialog_close = this . noteContentPropertiesDialog_close . bind ( this ) ;
this . shareNoteDialog_close = this . shareNoteDialog_close . bind ( this ) ;
2021-05-13 18:57:37 +02:00
this . shareFolderDialog_close = this . shareFolderDialog_close . bind ( this ) ;
2020-10-09 19:35:46 +02:00
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
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 ) {
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
2021-05-15 17:30:56 +02:00
// For unclear reasons, layout items sometimes end up witout a key.
// 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
}
window_resize() {
this . updateRootLayoutSize ( ) ;
2019-02-16 03:12:43 +02:00
}
2020-04-09 19:57:20 +02:00
setupAppCloseHandling() {
this . waitForNotesSavedIID_ = null ;
// This event is dispached from the main process when the app is about
// 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 ) ;
}
} ) ;
}
2018-09-16 20:37:31 +02:00
notePropertiesDialog_close() {
this . setState ( { notePropertiesDialogOptions : { } } ) ;
}
2020-02-25 11:43:31 +02:00
noteContentPropertiesDialog_close() {
this . setState ( { noteContentPropertiesDialogOptions : { } } ) ;
}
2021-05-13 18:57:37 +02:00
private shareNoteDialog_close() {
2019-12-13 03:16:34 +02:00
this . setState ( { shareNoteDialogOptions : { } } ) ;
}
2021-05-13 18:57:37 +02:00
private shareFolderDialog_close() {
this . setState ( { shareFolderDialogOptions : { visible : false , folderId : '' } } ) ;
}
2020-11-13 19:09:28 +02:00
updateMainLayout ( layout : LayoutItem ) {
this . props . dispatch ( {
type : 'MAIN_LAYOUT_SET' ,
value : layout ,
} ) ;
}
2020-09-15 15:01:07 +02:00
updateRootLayoutSize() {
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
}
2020-11-13 19:09:28 +02:00
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-09-21 18:31:25 +02:00
if ( this . state . notePropertiesDialogOptions !== prevState . notePropertiesDialogOptions ) {
this . props . dispatch ( {
type : this . state . notePropertiesDialogOptions && this . state . notePropertiesDialogOptions . visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE' ,
name : 'noteProperties' ,
} ) ;
}
if ( this . state . noteContentPropertiesDialogOptions !== prevState . noteContentPropertiesDialogOptions ) {
this . props . dispatch ( {
type : this . state . noteContentPropertiesDialogOptions && this . state . noteContentPropertiesDialogOptions . visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE' ,
name : 'noteContentProperties' ,
} ) ;
}
if ( this . state . shareNoteDialogOptions !== prevState . shareNoteDialogOptions ) {
this . props . dispatch ( {
type : this . state . shareNoteDialogOptions && this . state . shareNoteDialogOptions . visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE' ,
name : 'shareNote' ,
} ) ;
}
2020-11-13 19:09:28 +02:00
2021-05-13 18:57:37 +02:00
if ( this . state . shareFolderDialogOptions !== prevState . shareFolderDialogOptions ) {
this . props . dispatch ( {
type : this . state . shareFolderDialogOptions && this . state . shareFolderDialogOptions . visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE' ,
name : 'shareFolder' ,
} ) ;
}
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
}
layoutModeListenerKeyDown ( event : any ) {
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
}
2020-07-03 23:32:39 +02:00
componentDidMount() {
2020-11-13 19:09:28 +02:00
window . addEventListener ( 'keydown' , this . layoutModeListenerKeyDown ) ;
2020-07-03 23:32:39 +02:00
}
componentWillUnmount() {
this . unregisterCommands ( ) ;
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
}
2020-11-12 21:13:28 +02:00
async waitForNoteToSaved ( noteId : string ) {
2020-05-02 17:41:07 +02:00
while ( noteId && this . props . editorNoteStatuses [ noteId ] === 'saving' ) {
2023-02-16 12:55:24 +02:00
// eslint-disable-next-line no-console
2020-05-02 17:41:07 +02:00
console . info ( 'Waiting for note to be saved...' , this . props . editorNoteStatuses ) ;
await time . msleep ( 100 ) ;
}
}
2020-11-12 21:13:28 +02:00
async printTo_ ( target : string , options : any ) {
2020-05-02 17:41:07 +02:00
// Concurrent print calls are disallowed to avoid incorrect settings being restored upon completion
if ( this . isPrinting_ ) {
2023-02-16 12:55:24 +02:00
// eslint-disable-next-line no-console
2020-05-02 17:41:07 +02:00
console . info ( ` Printing ${ options . path } to ${ target } disallowed, already printing. ` ) ;
return ;
}
this . isPrinting_ = true ;
// Need to wait for save because the interop service reloads the note from the database
await this . waitForNoteToSaved ( options . noteId ) ;
if ( target === 'pdf' ) {
try {
const pdfData = await InteropServiceHelper . exportNoteToPdf ( options . noteId , {
printBackground : true ,
pageSize : Setting.value ( 'export.pdfPageSize' ) ,
landscape : Setting.value ( 'export.pdfPageOrientation' ) === 'landscape' ,
customCss : this.props.customCss ,
2020-12-19 19:42:18 +02:00
plugins : this.props.plugins ,
2020-05-02 17:41:07 +02:00
} ) ;
await shim . fsDriver ( ) . writeFile ( options . path , pdfData , 'buffer' ) ;
} catch ( error ) {
console . error ( error ) ;
bridge ( ) . showErrorMessageBox ( error . message ) ;
}
} else if ( target === 'printer' ) {
try {
await InteropServiceHelper . printNote ( options . noteId , {
printBackground : true ,
customCss : this.props.customCss ,
} ) ;
} catch ( error ) {
console . error ( error ) ;
bridge ( ) . showErrorMessageBox ( error . message ) ;
}
}
this . isPrinting_ = false ;
}
2020-09-15 15:01:07 +02:00
rootLayoutSize() {
return {
width : window.innerWidth ,
height : this.rowHeight ( ) ,
} ;
}
rowHeight() {
if ( ! this . props ) return 0 ;
return this . props . style . height - ( this . messageBoxVisible ( ) ? this . messageBoxHeight ( ) : 0 ) ;
}
messageBoxHeight() {
return 50 ;
}
2020-11-13 19:09:28 +02:00
styles ( themeId : number , width : number , height : number , messageBoxVisible : boolean ) {
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
} ;
2018-02-27 22:04:38 +02:00
this . styles_ . modalLayer = Object . assign ( { } , theme . textStyle , {
zIndex : 10000 ,
position : 'absolute' ,
top : 0 ,
left : 0 ,
2018-03-23 19:59:18 +02:00
backgroundColor : theme.backgroundColor ,
2018-02-27 22:04:38 +02:00
width : width - 20 ,
height : height - 20 ,
padding : 10 ,
} ) ;
2017-11-30 01:03:10 +02:00
return this . styles_ ;
}
2021-06-21 20:30:20 +02:00
private renderNotificationMessage ( message : string , callForAction : string , callForActionHandler : Function , callForAction2 : string = null , callForActionHandler2 : Function = null ) {
const theme = themeStyle ( this . props . themeId ) ;
const urlStyle : any = { color : theme.colorWarnUrl , textDecoration : 'underline' } ;
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 >
) ;
}
2020-11-12 21:13:28 +02:00
renderNotification ( theme : any , 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' ,
} ,
} ) ;
} ;
2021-08-06 11:58:32 +02:00
const onViewPluginScreen = ( ) = > {
this . props . dispatch ( {
type : 'NAV_GO' ,
routeName : 'Config' ,
props : {
defaultSection : 'plugins' ,
} ,
} ) ;
} ;
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' ) ,
onDisableSafeModeAndRestart
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' ) ,
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' ) ,
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' ) ,
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' ) ,
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' ) ,
2021-11-03 18:24:40 +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' ) ,
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' ) ,
onViewEncryptionConfigScreen
2020-03-13 19:42:50 +02:00
) ;
2021-08-06 11:58:32 +02:00
} else if ( this . props . showInstallTemplatesPlugin ) {
msg = this . renderNotificationMessage (
'The template feature has been moved to a plugin called "Templates".' ,
'Install plugin' ,
onViewPluginScreen
) ;
2020-03-13 19:42:50 +02:00
}
return (
< div style = { styles . messageBox } >
< span style = { theme . textStyle } > { msg } < / span >
< / div >
) ;
}
2021-04-24 20:23:33 +02:00
messageBoxVisible ( props : Props = null ) {
2020-10-09 19:35:46 +02:00
if ( ! props ) props = this . props ;
2021-08-06 11:58:32 +02:00
return props . hasDisabledSyncItems || props . showMissingMasterKeyMessage || props . showNeedUpgradingMasterKeyMessage || props . showShouldReencryptMessage || props . hasDisabledEncryptionItems || this . props . shouldUpgradeSyncTarget || props . isSafeMode || this . showShareInvitationNotification ( props ) || this . props . needApiAuth || this . props . showInstallTemplatesPlugin ;
2020-03-13 19:42:50 +02:00
}
2020-07-03 23:32:39 +02:00
registerCommands() {
for ( const command of commands ) {
CommandService . instance ( ) . registerRuntime ( command . declaration . name , command . runtime ( this ) ) ;
}
}
unregisterCommands() {
for ( const command of commands ) {
CommandService . instance ( ) . unregisterRuntime ( command . declaration . name ) ;
}
}
2020-11-12 21:13:28 +02:00
resizableLayout_resize ( event : any ) {
2020-11-13 19:09:28 +02:00
this . updateMainLayout ( event . layout ) ;
}
resizableLayout_moveButtonClick ( event : MoveButtonClickEvent ) {
const newLayout = move ( this . props . mainLayout , event . itemKey , event . direction ) ;
this . updateMainLayout ( newLayout ) ;
2020-09-15 15:01:07 +02:00
}
2020-11-12 21:13:28 +02:00
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[] = [];
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 }
/ > ;
} ,
editor : ( ) = > {
const bodyEditor = this . props . settingEditorCodeView ? 'CodeMirror' : 'TinyMCE' ;
return < NoteEditor key = { key } bodyEditor = { bodyEditor } / > ;
} ,
} ;
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
}
2020-10-09 19:35:46 +02:00
renderPluginDialogs() {
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 >
) ;
}
2017-11-30 01:03:10 +02:00
render() {
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2019-07-29 14:13:23 +02:00
const style = Object . assign (
{
color : theme.color ,
backgroundColor : theme.backgroundColor ,
} ,
2020-08-05 00:00:11 +02:00
this . props . style
2019-07-29 14:13:23 +02:00
) ;
2017-11-30 01:03:10 +02:00
const promptOptions = this . state . promptOptions ;
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
2017-11-30 01:03:10 +02:00
if ( ! this . promptOnClose_ ) {
2020-11-12 21:13:28 +02:00
this . promptOnClose_ = ( answer : any , buttonType : any ) = > {
2017-11-30 01:03:10 +02:00
return this . state . promptOptions . onClose ( answer , buttonType ) ;
2019-07-29 14:13:23 +02:00
} ;
2017-11-30 01:03:10 +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-10-09 19:35:46 +02:00
const dialogInfo = PluginManager . instance ( ) . pluginDialogToShow ( this . props . pluginsLegacy ) ;
2019-07-29 14:13:23 +02:00
const pluginDialog = ! dialogInfo ? null : < dialogInfo.Dialog { ...dialogInfo.props } / > ;
2019-04-01 21:43:13 +02:00
2018-02-27 22:04:38 +02:00
const modalLayerStyle = Object . assign ( { } , styles . modalLayer , { display : this.state.modalLayer.visible ? 'block' : 'none' } ) ;
2018-09-16 20:37:31 +02:00
const notePropertiesDialogOptions = this . state . notePropertiesDialogOptions ;
2020-02-25 11:43:31 +02:00
const noteContentPropertiesDialogOptions = this . state . noteContentPropertiesDialogOptions ;
2019-12-13 03:16:34 +02:00
const shareNoteDialogOptions = this . state . shareNoteDialogOptions ;
2021-05-13 18:57:37 +02:00
const shareFolderDialogOptions = this . state . shareFolderDialogOptions ;
2018-09-16 20:37:31 +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 } >
2018-02-27 22:04:38 +02:00
< div style = { modalLayerStyle } > { this . state . modalLayer . message } < / div >
2020-10-09 19:35:46 +02:00
{ this . renderPluginDialogs ( ) }
2020-09-15 15:01:07 +02:00
{ noteContentPropertiesDialogOptions . visible && < NoteContentPropertiesDialog markupLanguage = { noteContentPropertiesDialogOptions . markupLanguage } themeId = { this . props . themeId } onClose = { this . noteContentPropertiesDialog_close } text = { noteContentPropertiesDialogOptions . text } / > }
{ notePropertiesDialogOptions . visible && < NotePropertiesDialog themeId = { this . props . themeId } noteId = { notePropertiesDialogOptions . noteId } onClose = { this . notePropertiesDialog_close } onRevisionLinkClick = { notePropertiesDialogOptions . onRevisionLinkClick } / > }
2021-12-20 17:08:43 +02:00
{ /* @ts-ignore */ }
2020-09-15 15:01:07 +02:00
{ shareNoteDialogOptions . visible && < ShareNoteDialog themeId = { this . props . themeId } noteIds = { shareNoteDialogOptions . noteIds } onClose = { this . shareNoteDialog_close } / > }
2021-12-20 17:08:43 +02:00
{ /* @ts-ignore */ }
2021-05-13 18:57:37 +02:00
{ shareFolderDialogOptions . visible && < ShareFolderDialog themeId = { this . props . themeId } folderId = { shareFolderDialogOptions . folderId } onClose = { this . shareFolderDialog_close } / > }
2019-07-29 14:13:23 +02:00
2020-09-15 15:01:07 +02:00
< PromptDialog autocomplete = { promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null } defaultValue = { promptOptions && promptOptions . value ? promptOptions . value : '' } themeId = { this . props . themeId } style = { styles . prompt } onClose = { this . promptOnClose_ } label = { promptOptions ? promptOptions . label : '' } description = { promptOptions ? promptOptions.description : null } visible = { ! ! this . state . promptOptions } buttons = { promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null } inputType = { promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null } / >
2018-09-16 20:37:31 +02:00
2017-12-05 01:57:13 +02:00
{ messageComp }
2020-11-13 19:09:28 +02:00
{ layoutComp }
2019-07-29 14:13:23 +02:00
{ pluginDialog }
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 ) ;
2017-11-06 22:54:58 +02:00
return {
2020-09-15 15:01:07 +02:00
themeId : state.settings.theme ,
2020-05-02 17:41:07 +02:00
settingEditorCodeView : state.settings [ 'editor.codeView' ] ,
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 ) ,
2021-08-12 17:54:10 +02:00
showNeedUpgradingMasterKeyMessage : ! ! EncryptionService . instance ( ) . masterKeysThatNeedUpgrading ( syncInfo . masterKeys ) . length ,
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 ,
2020-10-09 19:35:46 +02:00
pluginsLegacy : state.pluginsLegacy ,
plugins : state.pluginService.plugins ,
2022-04-17 13:41:27 +02:00
pluginHtmlContents : state.pluginService.pluginHtmlContents ,
2020-05-02 17:41:07 +02:00
customCss : state.customCss ,
editorNoteStatuses : state.editorNoteStatuses ,
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 ,
2021-08-06 11:58:32 +02:00
needApiAuth : state.needApiAuth ,
showInstallTemplatesPlugin : state.hasLegacyTemplates && ! state . pluginService . plugins [ 'joplin.plugin.templates' ] ,
2023-02-17 15:07:18 +02:00
isResettingLayout : state.isResettingLayout ,
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 ) ;