-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
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
Changes from 17 commits
b57f1dc
27a1354
1f33fdd
cec869c
8142462
420bd56
583abd0
4876d93
33b5162
9ab7db2
98a2ce5
d8df6d1
d171a32
a638777
bcfe23a
586291a
7911be8
584495b
271e08f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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()], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now let's put only |
||
); | ||
} | ||
|
||
|
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
enum AccountType { | ||
Default = 0, | ||
Basic = 1, | ||
Pro = 2, | ||
Team = 3, | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
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}`); | ||
} |
There was a problem hiding this comment.
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 valueThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 thatprops.joplinCloudWebsite
changes from""
to"https://joplincloud.com/"
:props.joplinCloudWebsite
is initially""
. This is the initial render.goToJoplinCloudProfile
is set to a function.props.joplinCloudWebsite
is""
in its scope. As such, ifgoToJoplinCloudProfile
is called, it willopenExternal('/users/me')
.Button
is givengoToJoplinCloudProfile
as a prop. This is the first render of thatButton
.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, ifgoToJoplinCloudProfile
is called, it willopenExternal('https://joplincloud.com//users/me')
.Button
is givengoToJoplinCloudProfile
as a prop. This prop is different from its previous value (becausefunction(){} !== function(){}
). As a result, theButton
re-renders.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/"
forprops.joplinCloudWebsite
in its scope.Also see react.dev's "Should you add useCallback everywhere".
There was a problem hiding this comment.
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