Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Desktop, Mobile: Add Joplin Cloud account information to configuration screen #10553

Merged
merged 19 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/global-style.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
Expand Down Expand Up @@ -1236,7 +1237,8 @@ packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.js
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.test.js
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.js
packages/lib/utils/ipc/utils/separateCallbacksFromSerializableArray.js
packages/lib/utils/joplinCloud.js
packages/lib/utils/joplinCloud/accountTypeToString.js
packages/lib/utils/joplinCloud/index.js
packages/lib/utils/processStartFlags.js
packages/lib/utils/replaceUnsupportedCharacters.test.js
packages/lib/utils/replaceUnsupportedCharacters.js
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,7 @@ packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/global-style.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
Expand Down Expand Up @@ -1215,7 +1216,8 @@ packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.js
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.test.js
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.js
packages/lib/utils/ipc/utils/separateCallbacksFromSerializableArray.js
packages/lib/utils/joplinCloud.js
packages/lib/utils/joplinCloud/accountTypeToString.js
packages/lib/utils/joplinCloud/index.js
packages/lib/utils/processStartFlags.js
packages/lib/utils/replaceUnsupportedCharacters.test.js
packages/lib/utils/replaceUnsupportedCharacters.js
Expand Down
21 changes: 21 additions & 0 deletions packages/app-desktop/gui/JoplinCloudConfigScreen.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,25 @@
p {
padding: calc(var(--joplin-font-size) * 0.8);
}
}

.joplin-cloud-account-information {
margin-bottom: var(--joplin-margin);

table {
margin-bottom: var(--joplin-margin);
border: none;

td {
border: none;
border-bottom: 1px solid var(--joplin-border-color4);
padding: var(--joplin-font-size);
}

tr:last-child {
td {
border: none;
}
}
}
}
27 changes: 27 additions & 0 deletions packages/app-desktop/gui/JoplinCloudConfigScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { _ } from '@joplin/lib/locale';
import { clipboard } from 'electron';
import Button from './Button/Button';
import { Fragment } from 'react';
import accountTypeToString from '@joplin/lib/utils/joplinCloud/accountTypeToString';
import bridge from '../services/bridge';

type JoplinCloudConfigScreenProps = {
inboxEmail: string;
joplinCloudAccountType: number;
userEmail: string;
joplinCloudWebsite: string;
};

const JoplinCloudConfigScreen = (props: JoplinCloudConfigScreenProps) => {
Expand All @@ -17,8 +21,29 @@ const JoplinCloudConfigScreen = (props: JoplinCloudConfigScreenProps) => {

const isEmailToNoteAvailableInAccount = props.joplinCloudAccountType !== 1;

const goToJoplinCloudProfile = async () => {
await bridge().openExternal(`${props.joplinCloudWebsite}/users/me`);
};
Comment on lines +24 to +26
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably default to using useCallback for event handlers, especially when it's dependent on a prop like here. Probably the prop won't change but if for some reason it's initially empty, then set to a value, the component will incorrectly use the empty value

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but if for some reason it's initially empty, then set to a value, the component will incorrectly use the empty value

I'm not sure that's true... I think useCallback prevents unnecessary re-renders of child components (rather than forcing the function to reload). More specifically, suppose that props.joplinCloudWebsite changes from "" to "https://joplincloud.com/":

  1. props.joplinCloudWebsite is initially "". This is the initial render.
    • goToJoplinCloudProfile is set to a function. props.joplinCloudWebsite is "" in its scope. As such, if goToJoplinCloudProfile is called, it will openExternal('/users/me').
    • The component renders. A Button is given goToJoplinCloudProfile as a prop. This is the first render of that Button.
  2. props.joplinCloudWebsite changes to "https://joplincloud.com/". This causes the component to re-render.
    • goToJoplinCloudProfile is set to a function. props.joplinCloudWebsite is "https://joplincloud.com/" in its scope. As such, if goToJoplinCloudProfile is called, it will openExternal('https://joplincloud.com//users/me').
    • The component renders. A Button is given goToJoplinCloudProfile as a prop. This prop is different from its previous value (because function(){} !== function(){}). As a result, the Button re-renders.
      • This re-render means that the button will use the new version of goToJoplinCloudProfile.

Observe that in step 2, because React considers different functions to be unequal, the Button uses the new version of the function that includes "https://joplincloud.com/" for props.joplinCloudWebsite in its scope.

Also see react.dev's "Should you add useCallback everywhere".

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, that's true, it will re-render anyway.

But I actually think that it's reasonable to default to adding useCallback rather than wondering whether it needs to be optimised or not. In this case maybe it doesn't make a difference that it re-render even when the prop doesn't change, but maybe it does, who knows. That's the idea of mostly defaulting to using useCallback - then we don't need to wonder about this, and it feels like it's more future proof


return (
<div>
<div className="joplin-cloud-account-information">
<h2>{_('Account information')}</h2>
<table>
<tbody>
<tr>
<td><strong>{_('Account type')}</strong></td>
<td>{accountTypeToString(props.joplinCloudAccountType)}</td>
</tr>
<tr>
<td><strong>{_('Email')}</strong></td>
<td>{props.userEmail}</td>
</tr>
</tbody>
</table>
<Button onClick={goToJoplinCloudProfile} title={_('Go to Joplin Cloud profile')}/>
</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>
{
Expand All @@ -38,6 +63,8 @@ const mapStateToProps = (state: AppState) => {
return {
inboxEmail: state.settings['sync.10.inboxEmail'],
joplinCloudAccountType: state.settings['sync.10.accountType'],
userEmail: state.settings['sync.10.userEmail'],
joplinCloudWebsite: state.settings['sync.10.website'],
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import NoteImportButton, { importButtonDefaultTitle, importButtonDescription } f
import SectionDescription from './SectionDescription';
import EnablePluginSupportPage from './plugins/EnablePluginSupportPage';
import getVersionInfoText from '../../../utils/getVersionInfoText';
import JoplinCloudConfig, { accountEmailLabel, accountInformationLabel, accountTypeLabel, emailToNoteDescription, emailToNoteLabel } from './JoplinCloudConfig';

interface ConfigScreenState {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
Expand Down Expand Up @@ -530,33 +531,16 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
}

if (section.name === 'joplinCloud') {
const label = _('Email to note');
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');
const isEmailToNoteAvailableInAccount = this.props.settings['sync.10.accountType'] !== 1;
const inboxEmailValue = isEmailToNoteAvailableInAccount ? this.props.settings['sync.10.inboxEmail'] : '-';
addSettingComponent(
<View key="joplinCloud">
<View style={this.styles().styleSheet.settingContainerNoBottomBorder}>
<Text style={this.styles().styleSheet.settingText}>{label}</Text>
<Text style={this.styles().styleSheet.settingTextEmphasis}>{inboxEmailValue}</Text>
</View>
{
!isEmailToNoteAvailableInAccount && (
<View style={this.styles().styleSheet.settingContainerNoBottomBorder}>
<Text style={this.styles().styleSheet.descriptionAlert}>{_('Your account doesn\'t have access to this feature')}</Text>
</View>
)
}
{
this.renderButton(
'sync.10.inboxEmail',
_('Copy to clipboard'),
() => isEmailToNoteAvailableInAccount && Clipboard.setString(this.props.settings['sync.10.inboxEmail']),
{ description, disabled: !isEmailToNoteAvailableInAccount },
)
}
</View>,
[label, description],
<JoplinCloudConfig
key="joplin-cloud-config"
accountType={this.props.settings['sync.10.accountType']}
inboxEmail={this.props.settings['sync.10.inboxEmail']}
userEmail={this.props.settings['sync.10.userEmail']}
website={this.props.settings['sync.10.website']}
styles={this.styles()}
/>,
[emailToNoteDescription(), emailToNoteLabel(), accountEmailLabel(), accountTypeLabel(), accountInformationLabel()],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way of adding search text for custom components may need to be refactored at some point (though not in this pull request :) ).

One option could be to create a custom <SearchableText>...</SearchableText> component that sends search text to the settings screen with a custom React context and provider.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now let's put only emailToNoteLabel() and emailToNoteDescription() in there

);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { _ } from '@joplin/lib/locale';
import * as React from 'react';
import { useCallback } from 'react';
import { View, Text, Linking } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import SettingsButton from './SettingsButton';
import accountTypeToString from '@joplin/lib/utils/joplinCloud/accountTypeToString';
import { LinkButton } from '../../buttons';
import { ConfigScreenStyles } from './configScreenStyles';
import { Divider } from 'react-native-paper';
import Logger from '@joplin/utils/Logger';

const logger = Logger.create('JoplinCloudConfig');

type JoplinCloudConfigProps = {
styles: ConfigScreenStyles;
accountType: string;
inboxEmail: string;
website: string;
userEmail: string;
};

export const emailToNoteLabel = () => _('Email to note');
export const emailToNoteDescription = () => _('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');
export const accountInformationLabel = () => _('Account information');
export const accountTypeLabel = () => _('Account type');
export const accountEmailLabel = () => _('Email');

const JoplinCloudConfig = (props: JoplinCloudConfigProps) => {

const isEmailToNoteAvailableInAccount = props.accountType !== '1';
const inboxEmailValue = props.inboxEmail ?? '-';

const goToJoplinCloudProfile = useCallback(async () => {
await Linking.openURL(`${props.website}/users/me`);
}, [props.website]);

const accountTypeName = () => {
try {
if (!props.accountType) return 'Unknown';
return accountTypeToString(parseInt(props.accountType, 10));
} catch (error) {
logger.error(error);
return 'Unknown';
}
};

return (
<View>
<View style={props.styles.styleSheet.settingContainerNoBottomBorder}>
<Text style={props.styles.styleSheet.settingTextEmphasis}>{accountInformationLabel()}</Text>
</View>
<View style={props.styles.styleSheet.settingContainerNoBottomBorder}>
<Text style={props.styles.styleSheet.settingText}>{accountTypeLabel()}</Text>
<Text style={props.styles.styleSheet.settingTextEmphasis}>{accountTypeName()}</Text>
</View>
<View style={props.styles.styleSheet.settingContainerNoBottomBorder}>
<Text style={props.styles.styleSheet.settingText}>{accountEmailLabel()}</Text>
<Text selectable style={props.styles.styleSheet.settingTextEmphasis}>{props.userEmail}</Text>
</View>
<LinkButton onPress={goToJoplinCloudProfile}>
{_('Go to Joplin Cloud profile')}
</LinkButton>
<Divider bold />

<View style={props.styles.styleSheet.settingContainerNoBottomBorder}>
<Text style={props.styles.styleSheet.settingText}>{emailToNoteLabel()}</Text>
<Text style={props.styles.styleSheet.settingTextEmphasis}>{inboxEmailValue}</Text>
</View>
{
!isEmailToNoteAvailableInAccount && (
<View style={props.styles.styleSheet.settingContainerNoBottomBorder}>
<Text style={props.styles.styleSheet.descriptionAlert}>{_('Your account doesn\'t have access to this feature')}</Text>
</View>
)
}
<SettingsButton
title={_('Copy to clipboard')}
clickHandler={() => isEmailToNoteAvailableInAccount && Clipboard.setString(props.inboxEmail)}
description={emailToNoteDescription()}
statusComponent={undefined}
styles={props.styles}
disabled={!isEmailToNoteAvailableInAccount}
/>
</View>
);

};

export default JoplinCloudConfig;
2 changes: 2 additions & 0 deletions packages/lib/models/Setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,8 @@ class Setting extends BaseModel {

'sync.10.accountType': { value: 0, type: SettingItemType.Int, public: false },

'sync.10.userEmail': { value: '', type: SettingItemType.String, public: false },

'sync.5.syncTargets': { value: {}, type: SettingItemType.Object, public: false },

'sync.resourceDownloadMode': {
Expand Down
15 changes: 15 additions & 0 deletions packages/lib/utils/joplinCloud/accountTypeToString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
enum AccountType {
Default = 0,
Basic = 1,
Pro = 2,
Team = 3,
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This AccountType could be used elsewhere, including in joplinCloud.ts so I think this file should be named more generically. Maybe just types.ts.

Even accountTypeToString is type-related since it converts from one type to another


export default function accountTypeToString(accountType: AccountType): string {
if (accountType === AccountType.Default) return 'Default';
if (accountType === AccountType.Basic) return 'Basic';
if (accountType === AccountType.Pro) return 'Pro';
if (accountType === AccountType.Team) return 'Team';
const exhaustivenessCheck: never = accountType;
throw new Error(`Invalid type: ${exhaustivenessCheck}`);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fs from 'fs-extra';
import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from '../markdownUtils';
import { _ } from '../locale';
import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from '../../markdownUtils';
import { _ } from '../../locale';
import { htmlentities } from '@joplin/utils/html';

type FeatureId = string;
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/utils/userFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface UserApiResponse {
inbox_email?: string;
can_use_share_permissions?: number;
account_type?: number;
email: string;
}

const userFetcher = async () => {
Expand Down Expand Up @@ -40,6 +41,7 @@ const userFetcher = async () => {
Setting.setValue('sync.10.inboxEmail', owner.inbox_email ? owner.inbox_email : '');
Setting.setValue('sync.10.canUseSharePermissions', !!owner.can_use_share_permissions);
Setting.setValue('sync.10.accountType', owner.account_type);
Setting.setValue('sync.10.userEmail', owner.email);
};

// Listen to the event only once
Expand Down
Loading