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:
parent
85079ad213
commit
06b2ba9d75
@ -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
2
.gitignore
vendored
@ -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
|
||||
|
@ -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(() => {
|
||||
|
@ -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}`);
|
||||
}
|
||||
|
3
packages/app-desktop/gui/JoplinCloudConfigScreen.scss
Normal file
3
packages/app-desktop/gui/JoplinCloudConfigScreen.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.inbox-email-value {
|
||||
font-weight: bold;
|
||||
}
|
32
packages/app-desktop/gui/JoplinCloudConfigScreen.tsx
Normal file
32
packages/app-desktop/gui/JoplinCloudConfigScreen.tsx
Normal 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);
|
@ -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);
|
||||
|
@ -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;
|
@ -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 (
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
@ -132,6 +132,10 @@ export class EventManager {
|
||||
}
|
||||
}
|
||||
|
||||
public once(eventName: string, callback: any) {
|
||||
return this.emitter_.once(eventName, callback);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const eventManager = new EventManager();
|
||||
|
@ -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;
|
||||
|
||||
|
30
packages/lib/utils/inboxFetcher.ts
Normal file
30
packages/lib/utils/inboxFetcher.ts
Normal 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);
|
||||
};
|
Loading…
Reference in New Issue
Block a user