1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-10 22:11:50 +02:00

Mobile: Joplin Cloud/Server: Support publishing notes (#12350)

This commit is contained in:
Henry Heino
2025-06-06 15:04:09 -07:00
committed by GitHub
parent 0fc665d6d8
commit 3d2ac91b8a
11 changed files with 492 additions and 76 deletions

View File

@@ -822,6 +822,8 @@ packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
packages/app-mobile/components/screens/ShareManager/IncomingShareItem.js
packages/app-mobile/components/screens/ShareManager/index.test.js
packages/app-mobile/components/screens/ShareManager/index.js
packages/app-mobile/components/screens/ShareNoteDialog.test.js
packages/app-mobile/components/screens/ShareNoteDialog.js
packages/app-mobile/components/screens/SsoLoginScreen.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/dropbox-login.js
@@ -1085,6 +1087,11 @@ packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
packages/lib/components/shared/NoteRevisionViewer/getHelpMessage.js
packages/lib/components/shared/SamlShared.js
packages/lib/components/shared/ShareNoteDialog/onUnshareNoteClick.js
packages/lib/components/shared/ShareNoteDialog/types.js
packages/lib/components/shared/ShareNoteDialog/useEncryptionWarningMessage.js
packages/lib/components/shared/ShareNoteDialog/useOnShareLinkClick.js
packages/lib/components/shared/ShareNoteDialog/useShareStatusMessage.js
packages/lib/components/shared/SsoScreenShared.js
packages/lib/components/shared/config/config-shared.js
packages/lib/components/shared/config/plugins/types.js

7
.gitignore vendored
View File

@@ -797,6 +797,8 @@ packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
packages/app-mobile/components/screens/ShareManager/IncomingShareItem.js
packages/app-mobile/components/screens/ShareManager/index.test.js
packages/app-mobile/components/screens/ShareManager/index.js
packages/app-mobile/components/screens/ShareNoteDialog.test.js
packages/app-mobile/components/screens/ShareNoteDialog.js
packages/app-mobile/components/screens/SsoLoginScreen.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/dropbox-login.js
@@ -1060,6 +1062,11 @@ packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
packages/lib/components/shared/NoteRevisionViewer/getHelpMessage.js
packages/lib/components/shared/SamlShared.js
packages/lib/components/shared/ShareNoteDialog/onUnshareNoteClick.js
packages/lib/components/shared/ShareNoteDialog/types.js
packages/lib/components/shared/ShareNoteDialog/useEncryptionWarningMessage.js
packages/lib/components/shared/ShareNoteDialog/useOnShareLinkClick.js
packages/lib/components/shared/ShareNoteDialog/useShareStatusMessage.js
packages/lib/components/shared/SsoScreenShared.js
packages/lib/components/shared/config/config-shared.js
packages/lib/components/shared/config/plugins/types.js

View File

@@ -1,11 +1,9 @@
import * as React from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
import { _, _n } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
import DialogButtonRow from './DialogButtonRow';
import { themeStyle, buildStyle } from '@joplin/lib/theme';
import { reg } from '@joplin/lib/registry';
import Dialog from './Dialog';
import DialogTitle from './DialogTitle';
import ShareService from '@joplin/lib/services/share/ShareService';
@@ -14,9 +12,12 @@ import { NoteEntity } from '@joplin/lib/services/database/types';
import Button from './Button/Button';
import { connect } from 'react-redux';
import { AppState } from '../app.reducer';
import { getEncryptionEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import shim from '@joplin/lib/shim';
import useOnShareLinkClick from '@joplin/lib/components/shared/ShareNoteDialog/useOnShareLinkClick';
import onUnshareNoteClick from '@joplin/lib/components/shared/ShareNoteDialog/onUnshareNoteClick';
import useShareStatusMessage from '@joplin/lib/components/shared/ShareNoteDialog/useShareStatusMessage';
import useEncryptionWarningMessage from '@joplin/lib/components/shared/ShareNoteDialog/useEncryptionWarningMessage';
import { SharingStatus } from '@joplin/lib/components/shared/ShareNoteDialog/types';
const { clipboard } = require('electron');
interface Props {
@@ -72,7 +73,7 @@ function styles_(props: Props) {
export function ShareNoteDialog(props: Props) {
const [notes, setNotes] = useState<NoteEntity[]>([]);
const [recursiveShare, setRecursiveShare] = useState<boolean>(false);
const [sharesState, setSharesState] = useState<string>('unknown');
const [sharesState, setSharesState] = useState<SharingStatus>(SharingStatus.Unknown);
const syncTargetInfo = useMemo(() => SyncTargetRegistry.infoById(props.syncTargetId), [props.syncTargetId]);
const noteCount = notes.length;
@@ -99,70 +100,20 @@ export function ShareNoteDialog(props: Props) {
props.onClose();
};
const copyLinksToClipboard = (shares: StateShare[]) => {
const links = [];
for (const share of shares) links.push(ShareService.instance().shareUrl(ShareService.instance().userId, share));
const onCopyLinks = useCallback((links: string[]) => {
clipboard.writeText(links.join('\n'));
};
}, []);
const shareLinkButton_click = useCallback(async () => {
const service = ShareService.instance();
let hasSynced = false;
let tryToSync = false;
while (true) {
try {
if (tryToSync) {
setSharesState('synchronizing');
await reg.waitForSyncFinishedThenSync();
tryToSync = false;
hasSynced = true;
}
setSharesState('creating');
const newShares: StateShare[] = [];
for (const note of notes) {
const share = await service.shareNote(note.id, recursiveShare);
newShares.push(share);
}
setSharesState('synchronizing');
await reg.waitForSyncFinishedThenSync();
setSharesState('creating');
copyLinksToClipboard(newShares);
setSharesState('created');
await ShareService.instance().refreshShares();
} catch (error) {
if (error.code === 404 && !hasSynced) {
reg.logger().info('ShareNoteDialog: Note does not exist on server - trying to sync it.', error);
tryToSync = true;
continue;
}
reg.logger().error('ShareNoteDialog: Cannot publish note:', error);
setSharesState('idle');
void shim.showErrorDialog(JoplinServerApi.connectionErrorMessage(error));
}
break;
}
}, [recursiveShare, notes]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const unshareNoteButton_click = async (event: any) => {
await ShareService.instance().unshareNote(event.noteId);
await ShareService.instance().refreshShares();
};
const shareLinkButton_click = useOnShareLinkClick({
setSharesState,
onShareUrlsReady: onCopyLinks,
notes,
recursiveShare,
});
const renderNote = (note: NoteEntity) => {
const unshareButton = !props.shares.find(s => s.note_id === note.id) ? null : (
<Button tooltip={_('Unpublish note')} iconName="fas fa-share-alt" onClick={() => unshareNoteButton_click({ noteId: note.id })}/>
<Button tooltip={_('Unpublish note')} iconName="fas fa-share-alt" onClick={() => onUnshareNoteClick({ noteId: note.id })}/>
);
return (
@@ -172,8 +123,7 @@ export function ShareNoteDialog(props: Props) {
);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const renderNoteList = (notes: any) => {
const renderNoteList = (notes: NoteEntity[]) => {
const noteComps = [];
for (const note of notes) {
noteComps.push(renderNote(note));
@@ -181,16 +131,12 @@ export function ShareNoteDialog(props: Props) {
return <div style={styles.noteList}>{noteComps}</div>;
};
const statusMessage = (sharesState: string): string => {
if (sharesState === 'synchronizing') return _('Synchronising...');
if (sharesState === 'creating') return _n('Generating link...', 'Generating links...', noteCount);
if (sharesState === 'created') return _n('Link has been copied to clipboard!', 'Links have been copied to clipboard!', noteCount);
return '';
};
const statusMessage = useShareStatusMessage({ sharesState, noteCount });
const encryptionWarning = useEncryptionWarningMessage();
function renderEncryptionWarningMessage() {
if (!getEncryptionEnabled()) return null;
return <div style={theme.textStyle}>{_('Note: When a note is shared, it will no longer be encrypted on the server.')}<hr/></div>;
if (!encryptionWarning) return null;
return <div style={theme.textStyle}>{encryptionWarning}<hr/></div>;
}
const onRecursiveShareChange = useCallback(() => {
@@ -213,8 +159,8 @@ export function ShareNoteDialog(props: Props) {
<DialogTitle title={_('Publish Notes')}/>
{renderNoteList(notes)}
{renderRecursiveShareCheckbox()}
<button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
<button disabled={[SharingStatus.Creating, SharingStatus.Synchronizing].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div style={theme.textStyle}>{statusMessage}</div>
{renderEncryptionWarningMessage()}
<DialogButtonRow
themeId={props.themeId}

View File

@@ -68,6 +68,8 @@ import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getAct
import EditorPluginHandler from '@joplin/lib/services/plugins/EditorPluginHandler';
import AudioRecordingBanner from '../../voiceTyping/AudioRecordingBanner';
import SpeechToTextBanner from '../../voiceTyping/SpeechToTextBanner';
import ShareNoteDialog from '../ShareNoteDialog';
import stateToWhenClauseContext from '../../../services/commands/stateToWhenClauseContext';
import { defaultWindowId } from '@joplin/lib/reducer';
import useVisiblePluginEditorViewIds from '@joplin/lib/hooks/plugins/useVisiblePluginEditorViewIds';
@@ -107,6 +109,7 @@ interface Props extends BaseProps {
toolbarEnabled: boolean;
pluginHtmlContents: PluginHtmlContents;
editorNoteReloadTimeRequest: number;
canPublish: boolean;
}
interface ComponentProps extends Props {
@@ -126,6 +129,7 @@ interface State {
alarmDialogShown: boolean;
heightBumpView: number;
noteTagDialogShown: boolean;
publishDialogShown: boolean;
fromShare: boolean;
showCamera: boolean;
showImageEditor: boolean;
@@ -196,6 +200,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
alarmDialogShown: false,
heightBumpView: 0,
noteTagDialogShown: false,
publishDialogShown: false,
fromShare: false,
showCamera: false,
showImageEditor: false,
@@ -422,6 +427,18 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
};
}
private onPublishDialogClose_ = () => {
this.setState({
publishDialogShown: false,
});
};
private onPublishDialogShow_ = () => {
this.setState({
publishDialogShown: true,
});
};
public styles() {
const themeId = this.props.themeId;
const theme = themeStyle(themeId);
@@ -1120,6 +1137,12 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
},
});
}
if (this.props.canPublish) {
output.push({
title: _('Publish/unpublish'),
onPress: this.onPublishDialogShow_,
});
}
return output;
}
@@ -1722,6 +1745,11 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
<SelectDateTimeDialog themeId={this.props.themeId} shown={this.state.alarmDialogShown} date={dueDate} onAccept={this.onAlarmDialogAccept} onReject={this.onAlarmDialogReject} />
{noteTagDialog}
<ShareNoteDialog
noteId={this.props.noteId}
visible={this.state.publishDialogShown}
onClose={this.onPublishDialogClose_}
/>
</View>
);
}
@@ -1741,6 +1769,7 @@ const NoteScreenWrapper = (props: Props) => {
};
const NoteScreen = connect((state: AppState) => {
const whenClause = stateToWhenClauseContext(state);
return {
windowId: state.windowId,
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
@@ -1766,6 +1795,7 @@ const NoteScreen = connect((state: AppState) => {
// default) CodeMirror editor. That should be refactored to make it less
// confusing.
useEditorBeta: !state.settings['editor.usePlainText'],
canPublish: whenClause.joplinServerConnected && !whenClause.inTrash,
};
})(NoteScreenWrapper);

View File

@@ -0,0 +1,71 @@
import * as React from 'react';
import { AppState } from '../../utils/types';
import { Store } from 'redux';
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import '@testing-library/jest-native/extend-expect';
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
import TestProviderStack from '../testing/TestProviderStack';
import ShareNoteDialog from './ShareNoteDialog';
import Note from '@joplin/lib/models/Note';
import mockShareService from '@joplin/lib/testing/share/mockShareService';
import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
import Folder from '@joplin/lib/models/Folder';
import ShareService from '@joplin/lib/services/share/ShareService';
const mockServiceForNoteSharing = () => {
mockShareService({
getShares: async () => {
return { items: [] };
},
postShares: async () => ({ id: 'test-id' }),
getShareInvitations: async () => null,
}, ShareService.instance());
};
interface WrapperProps {
noteId: string;
onClose?: ()=> void;
}
let store: Store<AppState>;
const WrappedShareDialog: React.FC<WrapperProps> = ({
noteId, onClose = () => {},
}) => {
return <TestProviderStack store={store}>
<ShareNoteDialog
noteId={noteId}
visible={true}
onClose={onClose}
/>
</TestProviderStack>;
};
describe('ShareNoteDialog', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
store = createMockReduxStore();
setupGlobalStore(store);
mockServiceForNoteSharing();
});
test('pressing "Copy Shareable Link" should publish the note', async () => {
const folder = await Folder.save({ title: 'Folder' });
const note = await Note.save({ title: 'Test', parent_id: folder.id });
render(<WrappedShareDialog noteId={note.id}/>);
const linkButton = await screen.findByRole('button', { name: 'Copy Shareable Link' });
expect(linkButton).not.toBeDisabled();
fireEvent.press(linkButton);
await waitFor(() => {
expect(screen.getByText('Link has been copied to clipboard!')).toBeVisible();
});
expect(await Note.load(note.id)).toMatchObject({
is_shared: 1,
});
});
});

View File

@@ -0,0 +1,208 @@
import * as React from 'react';
import { Button, Divider, Text } from 'react-native-paper';
import { View, StyleSheet, ScrollView, Linking } from 'react-native';
import Note from '@joplin/lib/models/Note';
import { NoteEntity } from '@joplin/lib/services/database/types';
import { StateShare } from '@joplin/lib/services/share/reducer';
import ShareService from '@joplin/lib/services/share/ShareService';
import { useCallback, useEffect, useMemo, useState } from 'react';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import Clipboard from '@react-native-clipboard/clipboard';
import useOnShareLinkClick from '@joplin/lib/components/shared/ShareNoteDialog/useOnShareLinkClick';
import onUnshareNoteClick from '@joplin/lib/components/shared/ShareNoteDialog/onUnshareNoteClick';
import useShareStatusMessage from '@joplin/lib/components/shared/ShareNoteDialog/useShareStatusMessage';
import useEncryptionWarningMessage from '@joplin/lib/components/shared/ShareNoteDialog/useEncryptionWarningMessage';
import { SharingStatus } from '@joplin/lib/components/shared/ShareNoteDialog/types';
import { AppState } from '../../utils/types';
import { connect } from 'react-redux';
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
import { _, _n } from '@joplin/lib/locale';
import { LinkButton, PrimaryButton } from '../buttons';
import { themeStyle } from '../global-style';
interface Props {
themeId: number;
noteId: string;
visible: boolean;
onClose: ()=> void;
shares: StateShare[];
}
const useStyles = (themeId: number) => {
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
root: {
flexGrow: 1,
},
scrollingRegion: {
flexGrow: 1,
},
noteItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 8,
paddingBottom: 8,
minHeight: 32,
},
noteTitle: {
fontSize: theme.fontSize,
},
});
}, [themeId]);
};
interface UnpublishProps {
note: NoteEntity;
onUnpublishStart: ()=> void;
}
const UnpublishButton: React.FC<UnpublishProps> = ({ note, onUnpublishStart }) => {
const [unpublishing, setUnpublishing] = useState(false);
const onPress = useCallback(async () => {
onUnpublishStart();
try {
setUnpublishing(true);
await onUnshareNoteClick({ noteId: note.id });
} finally {
setUnpublishing(false);
}
}, [note, onUnpublishStart]);
return <Button
accessibilityLabel={_('Unpublish "%s"', note.title)}
loading={unpublishing}
disabled={unpublishing}
icon='share-off'
onPress={onPress}
>{_('Unpublish')}</Button>;
};
const ShareNoteDialogContent: React.FC<Props> = ({
themeId, noteId, shares,
}) => {
const [notes, setNotes] = useState<NoteEntity[]>([]);
const recursiveShare = false;
const [sharesState, setSharesState] = useState<SharingStatus>(SharingStatus.Unknown);
const [shareLinks, setShareLinks] = useState<string[]>([]);
const noteCount = notes.length;
useEffect(() => {
void ShareService.instance().refreshShares();
}, []);
useAsyncEffect(async (event) => {
const note = await Note.load(noteId);
if (event.cancelled) return;
setNotes([note]);
}, [noteId]);
const onCopyLinks = useCallback(async (links: string[]) => {
setShareLinks(links);
const linkText = links.join('\n');
Clipboard.setString(linkText);
}, []);
const onUnpublishStart = useCallback(() => {
setShareLinks([]);
}, []);
const shareLinkButton_click = useOnShareLinkClick({
setSharesState,
onShareUrlsReady: onCopyLinks,
notes,
recursiveShare,
});
const styles = useStyles(themeId);
const renderNote = (note: NoteEntity) => {
const unshareButton = shares.find(s => s.note_id === note.id) ? (
<UnpublishButton note={note} onUnpublishStart={onUnpublishStart}/>
) : null;
return (
<View key={note.id} style={styles.noteItem}>
<Text style={styles.noteTitle}>{note.title}</Text>{unshareButton}
</View>
);
};
const renderNoteList = (notes: NoteEntity[]) => {
const noteComps = [];
for (const note of notes) {
noteComps.push(renderNote(note));
}
return <View>{noteComps}</View>;
};
const statusMessage = useShareStatusMessage({
sharesState, noteCount,
});
const encryptionMessage = useEncryptionWarningMessage();
const renderEncryptionWarningMessage = () => {
if (!encryptionMessage) return null;
return <>
<Text>{encryptionMessage}</Text>
<Divider/>
</>;
};
const renderLinks = () => {
if (shareLinks.length === 0) return null;
return <>
<Divider/>
<Text variant='titleMedium' accessibilityRole='header'>{_('Links')}</Text>
{shareLinks.map((link, index) => {
return <LinkButton
onPress={() => Linking.openURL(link)}
key={`link-${index}`}
>{link}</LinkButton>;
})}
</>;
};
const copyButtonLoading = [SharingStatus.Creating, SharingStatus.Synchronizing].includes(sharesState);
const copyLinkButton = <PrimaryButton
disabled={copyButtonLoading}
loading={copyButtonLoading}
onPress={shareLinkButton_click}
>{
_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)
}</PrimaryButton>;
return <View style={styles.root}>
<ScrollView style={styles.scrollingRegion}>
{renderEncryptionWarningMessage()}
<Divider/>
{renderNoteList(notes)}
{renderLinks()}
</ScrollView>
<Text aria-live='polite'>{statusMessage}</Text>
{copyLinkButton}
</View>;
};
const ShareNoteDialog: React.FC<Props> = props => {
return <DismissibleDialog
themeId={props.themeId}
visible={props.visible}
onDismiss={props.onClose}
size={DialogSize.Small}
heading={_('Publish Note')}
>
{props.visible ? <ShareNoteDialogContent {...props}/> : null}
</DismissibleDialog>;
};
const mapStateToProps = (state: AppState) => {
return {
themeId: state.settings.theme,
shares: state.shareService.shares.filter(s => !!s.note_id),
};
};
export default connect(mapStateToProps)(ShareNoteDialog);

View File

@@ -0,0 +1,12 @@
import ShareService from '../../../services/share/ShareService';
interface UnshareNoteEvent {
noteId: string;
}
const onUnshareNoteClick = async (event: UnshareNoteEvent) => {
await ShareService.instance().unshareNote(event.noteId);
await ShareService.instance().refreshShares();
};
export default onUnshareNoteClick;

View File

@@ -0,0 +1,9 @@
// eslint-disable-next-line import/prefer-default-export -- types file with a single type
export enum SharingStatus {
Unknown = 'unknown',
Synchronizing = 'synchronizing',
Creating = 'creating',
Idle = 'idle',
Created = 'created',
}

View File

@@ -0,0 +1,9 @@
import { _ } from '../../../locale';
import { getEncryptionEnabled } from '../../../services/synchronizer/syncInfoUtils';
const useEncryptionWarningMessage = () => {
if (!getEncryptionEnabled()) return null;
return _('Note: When a note is shared, it will no longer be encrypted on the server.');
};
export default useEncryptionWarningMessage;

View File

@@ -0,0 +1,88 @@
import Logger from '@joplin/utils/Logger';
import JoplinServerApi from '../../../JoplinServerApi';
import { reg } from '../../../registry';
import { NoteEntity } from '../../../services/database/types';
import ShareService from '../../../services/share/ShareService';
import { StateShare } from '../../../services/share/reducer';
import shim from '../../../shim';
import { SharingStatus } from './types';
const { useCallback } = shim.react();
const logger = Logger.create('useOnShareLinkClick');
interface Props {
notes: NoteEntity[];
recursiveShare: boolean;
setSharesState(state: SharingStatus): void;
onShareUrlsReady(urls: string[]): void;
}
const getShareLinks = (shares: StateShare[]) => {
const links = [];
for (const share of shares) {
if (!share) {
throw new Error('Error: Empty share.');
}
links.push(ShareService.instance().shareUrl(ShareService.instance().userId, share));
}
return links;
};
const useOnShareLinkClick = ({
setSharesState, onShareUrlsReady, notes, recursiveShare,
}: Props) => {
return useCallback(async () => {
const service = ShareService.instance();
let hasSynced = false;
let tryToSync = false;
while (true) {
try {
if (tryToSync) {
setSharesState(SharingStatus.Synchronizing);
await reg.waitForSyncFinishedThenSync();
tryToSync = false;
hasSynced = true;
}
setSharesState(SharingStatus.Creating);
const newShares: StateShare[] = [];
for (const note of notes) {
const share = await service.shareNote(note.id, recursiveShare);
newShares.push(share);
}
setSharesState(SharingStatus.Synchronizing);
await reg.waitForSyncFinishedThenSync();
setSharesState(SharingStatus.Creating);
onShareUrlsReady(getShareLinks(newShares));
setSharesState(SharingStatus.Created);
await ShareService.instance().refreshShares();
} catch (error) {
if (error.code === 404 && !hasSynced) {
logger.info('ShareNoteDialog: Note does not exist on server - trying to sync it.', error);
tryToSync = true;
continue;
}
console.error(error);
logger.error('ShareNoteDialog: Cannot publish note:', error);
setSharesState(SharingStatus.Idle);
void shim.showErrorDialog(JoplinServerApi.connectionErrorMessage(error));
}
break;
}
}, [recursiveShare, notes, onShareUrlsReady, setSharesState]);
};
export default useOnShareLinkClick;

View File

@@ -0,0 +1,29 @@
import { _, _n } from '../../../locale';
import shim from '../../../shim';
import { SharingStatus } from './types';
interface Props {
sharesState: SharingStatus;
noteCount: number;
}
const useShareStatusMessage = ({ sharesState, noteCount }: Props): string => {
if (sharesState === SharingStatus.Synchronizing) {
return _('Synchronising...');
}
if (sharesState === SharingStatus.Creating) {
return _n('Generating link...', 'Generating links...', noteCount);
}
if (sharesState === SharingStatus.Created) {
// On web, copying text after a long delay (e.g. to sync) fails.
// As such, the web UI for copying links is a bit different:
if (shim.mobilePlatform() === 'web') {
return _n('Link created!', 'Links created!', noteCount);
} else {
return _n('Link has been copied to clipboard!', 'Links have been copied to clipboard!', noteCount);
}
}
return '';
};
export default useShareStatusMessage;