From 3d2ac91b8a474739b86e44ba04f4a2373fafb7f0 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:04:09 -0700 Subject: [PATCH] Mobile: Joplin Cloud/Server: Support publishing notes (#12350) --- .eslintignore | 7 + .gitignore | 7 + packages/app-desktop/gui/ShareNoteDialog.tsx | 98 ++------- .../components/screens/Note/Note.tsx | 30 +++ .../screens/ShareNoteDialog.test.tsx | 71 ++++++ .../components/screens/ShareNoteDialog.tsx | 208 ++++++++++++++++++ .../ShareNoteDialog/onUnshareNoteClick.ts | 12 + .../shared/ShareNoteDialog/types.ts | 9 + .../useEncryptionWarningMessage.ts | 9 + .../ShareNoteDialog/useOnShareLinkClick.ts | 88 ++++++++ .../ShareNoteDialog/useShareStatusMessage.ts | 29 +++ 11 files changed, 492 insertions(+), 76 deletions(-) create mode 100644 packages/app-mobile/components/screens/ShareNoteDialog.test.tsx create mode 100644 packages/app-mobile/components/screens/ShareNoteDialog.tsx create mode 100644 packages/lib/components/shared/ShareNoteDialog/onUnshareNoteClick.ts create mode 100644 packages/lib/components/shared/ShareNoteDialog/types.ts create mode 100644 packages/lib/components/shared/ShareNoteDialog/useEncryptionWarningMessage.ts create mode 100644 packages/lib/components/shared/ShareNoteDialog/useOnShareLinkClick.ts create mode 100644 packages/lib/components/shared/ShareNoteDialog/useShareStatusMessage.ts diff --git a/.eslintignore b/.eslintignore index e2a3aeb43e..c5ef6defde 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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 diff --git a/.gitignore b/.gitignore index c9cd5c8a3e..59f57c73a1 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/packages/app-desktop/gui/ShareNoteDialog.tsx b/packages/app-desktop/gui/ShareNoteDialog.tsx index 7f9e4c7232..d3a175cd47 100644 --- a/packages/app-desktop/gui/ShareNoteDialog.tsx +++ b/packages/app-desktop/gui/ShareNoteDialog.tsx @@ -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([]); const [recursiveShare, setRecursiveShare] = useState(false); - const [sharesState, setSharesState] = useState('unknown'); + const [sharesState, setSharesState] = useState(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 : ( - -
{statusMessage(sharesState)}
+ +
{statusMessage}
{renderEncryptionWarningMessage()} imp alarmDialogShown: false, heightBumpView: 0, noteTagDialogShown: false, + publishDialogShown: false, fromShare: false, showCamera: false, showImageEditor: false, @@ -422,6 +427,18 @@ class NoteScreenComponent extends BaseScreenComponent 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 imp }, }); } + if (this.props.canPublish) { + output.push({ + title: _('Publish/unpublish'), + onPress: this.onPublishDialogShow_, + }); + } return output; } @@ -1722,6 +1745,11 @@ class NoteScreenComponent extends BaseScreenComponent imp {noteTagDialog} + ); } @@ -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); diff --git a/packages/app-mobile/components/screens/ShareNoteDialog.test.tsx b/packages/app-mobile/components/screens/ShareNoteDialog.test.tsx new file mode 100644 index 0000000000..b6c68bd80f --- /dev/null +++ b/packages/app-mobile/components/screens/ShareNoteDialog.test.tsx @@ -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; +const WrappedShareDialog: React.FC = ({ + noteId, onClose = () => {}, +}) => { + return + + ; +}; + +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(); + + 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, + }); + }); +}); diff --git a/packages/app-mobile/components/screens/ShareNoteDialog.tsx b/packages/app-mobile/components/screens/ShareNoteDialog.tsx new file mode 100644 index 0000000000..74d6b5c146 --- /dev/null +++ b/packages/app-mobile/components/screens/ShareNoteDialog.tsx @@ -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 = ({ 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 ; +}; + +const ShareNoteDialogContent: React.FC = ({ + themeId, noteId, shares, +}) => { + const [notes, setNotes] = useState([]); + const recursiveShare = false; + const [sharesState, setSharesState] = useState(SharingStatus.Unknown); + const [shareLinks, setShareLinks] = useState([]); + + 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) ? ( + + ) : null; + + return ( + + {note.title}{unshareButton} + + ); + }; + + const renderNoteList = (notes: NoteEntity[]) => { + const noteComps = []; + for (const note of notes) { + noteComps.push(renderNote(note)); + } + return {noteComps}; + }; + + const statusMessage = useShareStatusMessage({ + sharesState, noteCount, + }); + const encryptionMessage = useEncryptionWarningMessage(); + + const renderEncryptionWarningMessage = () => { + if (!encryptionMessage) return null; + return <> + {encryptionMessage} + + ; + }; + + const renderLinks = () => { + if (shareLinks.length === 0) return null; + return <> + + {_('Links')} + {shareLinks.map((link, index) => { + return Linking.openURL(link)} + key={`link-${index}`} + >{link}; + })} + ; + }; + + const copyButtonLoading = [SharingStatus.Creating, SharingStatus.Synchronizing].includes(sharesState); + const copyLinkButton = { + _n('Copy Shareable Link', 'Copy Shareable Links', noteCount) + }; + + return + + {renderEncryptionWarningMessage()} + + {renderNoteList(notes)} + {renderLinks()} + + {statusMessage} + {copyLinkButton} + ; +}; + +const ShareNoteDialog: React.FC = props => { + return + {props.visible ? : null} + ; +}; + +const mapStateToProps = (state: AppState) => { + return { + themeId: state.settings.theme, + shares: state.shareService.shares.filter(s => !!s.note_id), + }; +}; + +export default connect(mapStateToProps)(ShareNoteDialog); diff --git a/packages/lib/components/shared/ShareNoteDialog/onUnshareNoteClick.ts b/packages/lib/components/shared/ShareNoteDialog/onUnshareNoteClick.ts new file mode 100644 index 0000000000..ec3644dbfd --- /dev/null +++ b/packages/lib/components/shared/ShareNoteDialog/onUnshareNoteClick.ts @@ -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; diff --git a/packages/lib/components/shared/ShareNoteDialog/types.ts b/packages/lib/components/shared/ShareNoteDialog/types.ts new file mode 100644 index 0000000000..f9dcacdc73 --- /dev/null +++ b/packages/lib/components/shared/ShareNoteDialog/types.ts @@ -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', +} diff --git a/packages/lib/components/shared/ShareNoteDialog/useEncryptionWarningMessage.ts b/packages/lib/components/shared/ShareNoteDialog/useEncryptionWarningMessage.ts new file mode 100644 index 0000000000..15254e6292 --- /dev/null +++ b/packages/lib/components/shared/ShareNoteDialog/useEncryptionWarningMessage.ts @@ -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; diff --git a/packages/lib/components/shared/ShareNoteDialog/useOnShareLinkClick.ts b/packages/lib/components/shared/ShareNoteDialog/useOnShareLinkClick.ts new file mode 100644 index 0000000000..4d4cd7931d --- /dev/null +++ b/packages/lib/components/shared/ShareNoteDialog/useOnShareLinkClick.ts @@ -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; diff --git a/packages/lib/components/shared/ShareNoteDialog/useShareStatusMessage.ts b/packages/lib/components/shared/ShareNoteDialog/useShareStatusMessage.ts new file mode 100644 index 0000000000..588d743395 --- /dev/null +++ b/packages/lib/components/shared/ShareNoteDialog/useShareStatusMessage.ts @@ -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;