1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-08 13:06:15 +02:00

Desktop, mobile: Add support for Joplin Cloud email to note functionality (#8460)

This commit is contained in:
pedr 2023-07-18 16:15:45 -03:00 committed by GitHub
parent 85079ad213
commit 06b2ba9d75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 157 additions and 19 deletions

View File

@ -154,6 +154,7 @@ packages/app-desktop/gui/HelpButton.js
packages/app-desktop/gui/IconButton.js
packages/app-desktop/gui/ImportScreen.js
packages/app-desktop/gui/ItemList.js
packages/app-desktop/gui/JoplinCloudConfigScreen.js
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
packages/app-desktop/gui/KeymapConfig/styles/index.js
@ -800,6 +801,7 @@ packages/lib/themes/solarizedLight.js
packages/lib/themes/type.js
packages/lib/time.js
packages/lib/utils/credentialFiles.js
packages/lib/utils/inboxFetcher.js
packages/lib/utils/joplinCloud.js
packages/lib/utils/webDAVUtils.js
packages/lib/utils/webDAVUtils.test.js

2
.gitignore vendored
View File

@ -139,6 +139,7 @@ packages/app-desktop/gui/HelpButton.js
packages/app-desktop/gui/IconButton.js
packages/app-desktop/gui/ImportScreen.js
packages/app-desktop/gui/ItemList.js
packages/app-desktop/gui/JoplinCloudConfigScreen.js
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
packages/app-desktop/gui/KeymapConfig/styles/index.js
@ -785,6 +786,7 @@ packages/lib/themes/solarizedLight.js
packages/lib/themes/type.js
packages/lib/time.js
packages/lib/utils/credentialFiles.js
packages/lib/utils/inboxFetcher.js
packages/lib/utils/joplinCloud.js
packages/lib/utils/webDAVUtils.js
packages/lib/utils/webDAVUtils.test.js

View File

@ -67,6 +67,7 @@ import eventManager from '@joplin/lib/eventManager';
import path = require('path');
import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
// import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
import { initializeInboxFetcher, inboxFetcher } from '@joplin/lib/utils/inboxFetcher';
const pluginClasses = [
require('./plugins/GotoAnything').default,
@ -487,6 +488,9 @@ class Application extends BaseApplication {
shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
}
initializeInboxFetcher();
shim.setInterval(() => { void inboxFetcher(); }, 1000 * 60 * 60);
this.updateTray();
shim.setTimeout(() => {

View File

@ -18,6 +18,7 @@ import restart from '../../services/restart';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import { getDefaultPluginsInstallState, updateDefaultPluginsInstallState } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
import getDefaultPluginsInfo from '@joplin/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo';
import JoplinCloudConfigScreen from '../JoplinCloudConfigScreen';
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
const settingKeyToControl: any = {
@ -106,6 +107,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
if (screenName === 'encryption') return <EncryptionConfigScreen/>;
if (screenName === 'server') return <ClipperConfigScreen themeId={this.props.themeId}/>;
if (screenName === 'keymap') return <KeymapConfigScreen themeId={this.props.themeId}/>;
if (screenName === 'joplinCloud') return <JoplinCloudConfigScreen />;
throw new Error(`Invalid screen name: ${screenName}`);
}

View File

@ -0,0 +1,3 @@
.inbox-email-value {
font-weight: bold;
}

View File

@ -0,0 +1,32 @@
const { connect } = require('react-redux');
import { AppState } from '../app.reducer';
import { _ } from '@joplin/lib/locale';
import { clipboard } from 'electron';
import Button from './Button/Button';
type JoplinCloudConfigScreenProps = {
inboxEmail: string;
};
const JoplinCloudConfigScreen = (props: JoplinCloudConfigScreenProps) => {
const copyToClipboard = () => {
clipboard.writeText(props.inboxEmail);
};
return (
<div>
<h2>{_('Email to note')}</h2>
<p>{_('Any email sent to this address will be converted into a note and added to your collection. The note will be saved into the Inbox notebook')}</p>
<p className='inbox-email-value'>{props.inboxEmail}</p>
<Button onClick={copyToClipboard} title={_('Copy to clipboard')} />
</div>
);
};
const mapStateToProps = (state: AppState) => {
return {
inboxEmail: state.settings['emailToNote.inboxEmail'],
};
};
export default connect(mapStateToProps)(JoplinCloudConfigScreen);

View File

@ -17,10 +17,12 @@ export const runtime = (): CommandRuntime => {
const folder = await Folder.load(folderId);
if (!folder) throw new Error(`No such folder: ${folderId}`);
const ok = bridge().showConfirmMessageBox(_('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', substrWithEllipsis(folder.title, 0, 32)), {
buttons: [_('Delete'), _('Cancel')],
defaultId: 1,
});
let deleteMessage = _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', substrWithEllipsis(folder.title, 0, 32));
if (folderId === context.state.settings['emailToNote.inboxJopId']) {
deleteMessage = _('Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that\'s recently been sent to it may be lost.');
}
const ok = bridge().showConfirmMessageBox(deleteMessage);
if (!ok) return;
await Folder.delete(folderId);

View File

@ -2,6 +2,7 @@
@use 'gui/EditFolderDialog/style.scss' as edit-folder-dialog;
@use 'gui/EncryptionConfigScreen/style.scss' as encryption-config-screen;
@use 'gui/PasswordInput/style.scss' as password-input;
@use 'gui/JoplinCloudConfigScreen.scss' as joplin-cloud-config-screen;
@use 'gui/Dropdown/style.scss' as dropdown-control;
@use 'gui/ShareFolderDialog/style.scss' as share-folder-dialog;
@use 'main.scss' as main;

View File

@ -27,6 +27,7 @@ import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
import configScreenStyles from './configScreenStyles';
import NoteExportButton from './NoteExportSection/NoteExportButton';
import ConfigScreenButton from './ConfigScreenButton';
import Clipboard from '@react-native-community/clipboard';
class ConfigScreenComponent extends BaseScreenComponent {
public static navigationOptions(): any {
@ -355,6 +356,26 @@ class ConfigScreenComponent extends BaseScreenComponent {
settingComps.push(this.renderButton('e2ee_config_button', _('Encryption Config'), this.e2eeConfig_));
}
if (section.name === 'joplinCloud') {
const description = _('Any email sent to this address will be converted into a note and added to your collection. The note will be saved into the Inbox notebook');
settingComps.push(
<View key="joplinCloud">
<View style={this.styles().settingContainerNoBottomBorder}>
<Text style={this.styles().settingText}>{_('Email to note')}</Text>
<Text style={{ fontWeight: 'bold' }}>{this.props.settings['emailToNote.inboxEmail']}</Text>
</View>
{
this.renderButton(
'emailToNote.inboxEmail',
_('Copy to clipboard'),
() => Clipboard.setString(this.props.settings['emailToNote.inboxEmail']),
{ description }
)
}
</View>
);
}
if (!settingComps.length) return null;
return (

View File

@ -35,6 +35,7 @@ interface Props {
folders: FolderEntity[];
opacity: number;
profileConfig: ProfileConfig;
inboxJopId: string;
}
const syncIconRotationValue = new Animated.Value(0);
@ -136,6 +137,31 @@ const SideMenuContentComponent = (props: Props) => {
const folder = folderOrAll as FolderEntity;
const generateFolderDeletion = () => {
const folderDeletion = (message: string) => {
Alert.alert('', message, [
{
text: _('OK'),
onPress: () => {
void Folder.delete(folder.id);
},
},
{
text: _('Cancel'),
onPress: () => { },
style: 'cancel',
},
]);
};
if (folder.id === props.inboxJopId) {
return folderDeletion(
_('Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that\'s recently been sent to it may be lost.')
);
}
return folderDeletion(_('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', folder.title));
};
Alert.alert(
'',
_('Notebook: %s', folder.title),
@ -154,21 +180,7 @@ const SideMenuContentComponent = (props: Props) => {
},
{
text: _('Delete'),
onPress: () => {
Alert.alert('', _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', folder.title), [
{
text: _('OK'),
onPress: () => {
void Folder.delete(folder.id);
},
},
{
text: _('Cancel'),
onPress: () => {},
style: 'cancel',
},
]);
},
onPress: generateFolderDeletion,
style: 'destructive',
},
{
@ -516,5 +528,6 @@ export default connect((state: AppState) => {
isOnMobileData: state.isOnMobileData,
syncOnlyOverWifi: state.settings['sync.mobileWifiOnly'],
profileConfig: state.profileConfig,
inboxJopId: state.settings['emailToNote.inboxJopId'],
};
})(SideMenuContentComponent);

View File

@ -117,6 +117,7 @@ import sensorInfo, { SensorInfo } from './components/biometrics/sensorInfo';
import { getCurrentProfile } from '@joplin/lib/services/profileConfig';
import { getDatabaseName, getProfilesRootDir, getResourceDir, setDispatch } from './services/profiles';
import { ReactNode } from 'react';
import { initializeInboxFetcher, inboxFetcher } from '@joplin/lib/utils/inboxFetcher';
import { parseShareCache } from '@joplin/lib/services/share/reducer';
import autodetectTheme, { onSystemColorSchemeChange } from './utils/autodetectTheme';
@ -664,6 +665,9 @@ async function initialize(dispatch: Function) {
reg.setupRecurrentSync();
initializeInboxFetcher();
PoorManIntervals.setInterval(() => { void inboxFetcher(); }, 1000 * 60 * 60);
PoorManIntervals.setTimeout(() => {
void AlarmService.garbageCollect();
}, 1000 * 60 * 60);

View File

@ -452,6 +452,7 @@ export default class Synchronizer {
try {
let remoteInfo = await fetchSyncInfo(this.api());
logger.info('Sync target remote info:', remoteInfo);
eventManager.emit('sessionEstablished');
let syncTargetIsNew = false;

View File

@ -186,6 +186,17 @@ shared.settingsSections = createSelector(
isScreen: true,
});
// Ideallly we would also check if the user was able to synchronize
// but we don't have a way of doing that besides making a request to Joplin Cloud
const syncTargetIsJoplinCloud = settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud');
if (syncTargetIsJoplinCloud) {
output.push({
name: 'joplinCloud',
metadatas: [],
isScreen: true,
});
}
return output;
}
);

View File

@ -132,6 +132,10 @@ export class EventManager {
}
}
public once(eventName: string, callback: any) {
return this.emitter_.once(eventName, callback);
}
}
const eventManager = new EventManager();

View File

@ -1714,6 +1714,10 @@ class Setting extends BaseModel {
label: () => _('Voice typing language files (URL)'),
section: 'note',
},
'emailToNote.inboxEmail': { value: '', type: SettingItemType.String, public: false },
'emailToNote.inboxJopId': { value: '', type: SettingItemType.String, public: false },
};
this.metadata_ = { ...this.buildInMetadata_ };
@ -2528,6 +2532,7 @@ class Setting extends BaseModel {
if (name === 'encryption') return _('Encryption');
if (name === 'server') return _('Web Clipper');
if (name === 'keymap') return _('Keyboard Shortcuts');
if (name === 'joplinCloud') return _('Joplin Cloud');
if (this.customSections_[name] && this.customSections_[name].label) return this.customSections_[name].label;
@ -2556,6 +2561,7 @@ class Setting extends BaseModel {
if (name === 'encryption') return 'icon-encryption';
if (name === 'server') return 'far fa-hand-scissors';
if (name === 'keymap') return 'fa fa-keyboard';
if (name === 'joplinCloud') return 'fa fa-cloud';
if (this.customSections_[name] && this.customSections_[name].iconName) return this.customSections_[name].iconName;

View File

@ -0,0 +1,30 @@
import SyncTargetRegistry from '../SyncTargetRegistry';
import eventManager from '../eventManager';
import Setting from '../models/Setting';
import { reg } from '../registry';
export const inboxFetcher = async () => {
if (Setting.value('sync.target') !== SyncTargetRegistry.nameToId('joplinCloud')) {
return;
}
const syncTarget = reg.syncTarget();
const fileApi = await syncTarget.fileApi();
const api = fileApi.driver().api();
const owner = await api.exec('GET', `api/users/${api.userId}`);
if (owner.inbox) {
Setting.setValue('emailToNote.inboxJopId', owner.inbox.jop_id);
}
if (owner.inbox_email) {
Setting.setValue('emailToNote.inboxEmail', owner.inbox_email);
}
};
// Listen to the event only once
export const initializeInboxFetcher = () => {
eventManager.once('sessionEstablished', inboxFetcher);
};