1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-03-23 21:09:30 +02:00

Desktop: Allow unsharing a note

This commit is contained in:
Laurent Cozic 2021-05-16 17:28:49 +02:00
parent 6f2f24171d
commit f7d164be6e
9 changed files with 99 additions and 39 deletions

View File

@ -18,6 +18,6 @@ export const runtime = (comp: any): CommandRuntime => {
},
});
},
enabledCondition: 'joplinServerConnected && oneNoteSelected',
enabledCondition: 'joplinServerConnected && someNotesSelected',
};
};

View File

@ -110,6 +110,7 @@ function NoteListItem(props: NoteListItemProps, ref: any) {
let listItemTitleStyle = Object.assign({}, props.style.listItemTitle);
listItemTitleStyle.paddingLeft = !item.is_todo ? hPadding : 4;
if (item.is_shared) listItemTitleStyle.color = theme.colorWarn;
if (item.is_todo && !!item.todo_completed) listItemTitleStyle = Object.assign(listItemTitleStyle, props.style.listItemTitleCompleted);
const displayTitle = Note.displayTitle(item);

View File

@ -10,19 +10,21 @@ import { reg } from '@joplin/lib/registry';
import Dialog from './Dialog';
import DialogTitle from './DialogTitle';
import ShareService from '@joplin/lib/services/share/ShareService';
import { StateShare } from '@joplin/lib/services/share/reducer';
import { NoteEntity } from '@joplin/lib/services/database/types';
import Button from './Button/Button';
import { connect } from 'react-redux';
import { AppState } from '../app';
const { clipboard } = require('electron');
interface ShareNoteDialogProps {
interface Props {
themeId: number;
noteIds: Array<string>;
onClose: Function;
shares: StateShare[];
}
interface SharesMap {
[key: string]: any;
}
function styles_(props: ShareNoteDialogProps) {
function styles_(props: Props) {
return buildStyle('ShareNoteDialog', props.themeId, (theme: any) => {
return {
noteList: {
@ -60,17 +62,21 @@ function styles_(props: ShareNoteDialogProps) {
});
}
export default function ShareNoteDialog(props: ShareNoteDialogProps) {
export function ShareNoteDialog(props: Props) {
console.info('Render ShareNoteDialog');
const [notes, setNotes] = useState<any[]>([]);
const [notes, setNotes] = useState<NoteEntity[]>([]);
const [sharesState, setSharesState] = useState<string>('unknown');
const [shares, setShares] = useState<SharesMap>({});
// const [shares, setShares] = useState<SharesMap>({});
const noteCount = notes.length;
const theme = themeStyle(props.themeId);
const styles = styles_(props);
useEffect(() => {
void ShareService.instance().refreshShares();
}, []);
useEffect(() => {
async function fetchNotes() {
const result = [];
@ -87,9 +93,9 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
props.onClose();
};
const copyLinksToClipboard = (shares: SharesMap) => {
const copyLinksToClipboard = (shares: StateShare[]) => {
const links = [];
for (const n in shares) links.push(ShareService.instance().shareUrl(shares[n]));
for (const share of shares) links.push(ShareService.instance().shareUrl(share));
clipboard.writeText(links.join('\n'));
};
@ -109,15 +115,13 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
setSharesState('creating');
const newShares = Object.assign({}, shares);
const newShares: StateShare[] = [];
for (const note of notes) {
const share = await service.shareNote(note.id);
newShares[note.id] = share;
newShares.push(share);
}
setShares(newShares);
setSharesState('synchronizing');
await reg.waitForSyncFinishedThenSync();
setSharesState('creating');
@ -125,6 +129,8 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
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);
@ -142,34 +148,53 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
}
};
const removeNoteButton_click = (event: any) => {
const newNotes = [];
for (let i = 0; i < notes.length; i++) {
const n = notes[i];
if (n.id === event.noteId) continue;
newNotes.push(n);
}
setNotes(newNotes);
// const removeNoteButton_click = (event: any) => {
// const newNotes = [];
// for (let i = 0; i < notes.length; i++) {
// const n = notes[i];
// if (n.id === event.noteId) continue;
// newNotes.push(n);
// }
// setNotes(newNotes);
// };
const unshareNoteButton_click = async (event: any) => {
await ShareService.instance().unshareNote(event.noteId);
await ShareService.instance().refreshShares();
};
const renderNote = (note: any) => {
const removeButton = notes.length <= 1 ? null : (
<button onClick={() => removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
<i style={styles.noteRemoveButtonIcon} className={'fa fa-times'}></i>
</button>
const renderNote = (note: NoteEntity) => {
const unshareButton = !props.shares.find(s => s.note_id === note.id) ? null : (
<Button tooltip={_('Unshare note')} iconName="fas fa-share-alt" onClick={() => unshareNoteButton_click({ noteId: note.id })}/>
);
// const removeButton = notes.length <= 1 ? null : (
// <Button iconName="fa fa-times" onClick={() => removeNoteButton_click({ noteId: note.id })}/>
// );
// const unshareButton = !shares[note.id] ? null : (
// <button onClick={() => unshareNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
// <i style={styles.noteRemoveButtonIcon} className={'fas fa-share-alt'}></i>
// </button>
// );
// const removeButton = notes.length <= 1 ? null : (
// <button onClick={() => removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
// <i style={styles.noteRemoveButtonIcon} className={'fa fa-times'}></i>
// </button>
// );
return (
<div key={note.id} style={styles.note}>
<span style={styles.noteTitle}>{note.title}</span>{removeButton}
<span style={styles.noteTitle}>{note.title}</span>{unshareButton}
</div>
);
};
const renderNoteList = (notes: any) => {
const noteComps = [];
for (const noteId of Object.keys(notes)) {
noteComps.push(renderNote(notes[noteId]));
for (const note of notes) {
noteComps.push(renderNote(note));
}
return <div style={styles.noteList}>{noteComps}</div>;
};
@ -194,7 +219,12 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
<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>
{renderEncryptionWarningMessage()}
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
<DialogButtonRow
themeId={props.themeId}
onClick={buttonRow_click}
okButtonShow={false}
cancelButtonLabel={_('Close')}
/>
</div>
);
}
@ -203,3 +233,11 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
<Dialog renderContent={renderContent}/>
);
}
const mapStateToProps = (state: AppState) => {
return {
shares: state.shareService.shares.filter(s => !!s.note_id),
};
};
export default connect(mapStateToProps)(ShareNoteDialog as any);

View File

@ -69,7 +69,6 @@ function listItemTextColor(props: any) {
export const StyledListItemAnchor = styled.a`
font-size: ${(props: any) => Math.round(props.theme.fontSize * 1.0833333)}px;
// font-weight: 500;
text-decoration: none;
color: ${(props: any) => listItemTextColor(props)};
cursor: default;

View File

@ -35,6 +35,7 @@ if [ "$1" == "1" ]; then
echo 'mkbook "other"' >> "$CMD_FILE"
echo 'use "shared"' >> "$CMD_FILE"
echo 'mknote "note 1"' >> "$CMD_FILE"
echo 'mknote "note 2"' >> "$CMD_FILE"
fi
cd "$ROOT_DIR/packages/app-cli"

View File

@ -347,7 +347,8 @@ export default class Folder extends BaseItem {
public static async updateResourceShareIds() {
// Find all resources where share_id is different from parent note
// share_id. Then update share_id on all these resources. Essentially it
// makes it match the resource share_id to the note share_id.
// makes it match the resource share_id to the note share_id. At the
// same time we also process the is_shared property.
const rows = await this.db().selectAll(`
SELECT r.id, n.share_id, n.is_shared
FROM note_resources nr

View File

@ -307,7 +307,7 @@ export default class Note extends BaseItem {
includeTimestamps: true,
}, options);
const output = ['id', 'title', 'is_todo', 'todo_completed', 'todo_due', 'parent_id', 'encryption_applied', 'order', 'markup_language', 'is_conflict'];
const output = ['id', 'title', 'is_todo', 'todo_completed', 'todo_due', 'parent_id', 'encryption_applied', 'order', 'markup_language', 'is_conflict', 'is_shared'];
if (options.includeTimestamps) {
output.push('updated_time');

View File

@ -104,7 +104,7 @@ export default class ShareService {
await Folder.updateAllShareIds();
}
public async shareNote(noteId: string) {
public async shareNote(noteId: string): Promise<StateShare> {
const note = await Note.load(noteId);
if (!note) throw new Error(`No such note: ${noteId}`);
@ -115,6 +115,24 @@ export default class ShareService {
return share;
}
public async unshareNote(noteId: string) {
const note = await Note.load(noteId);
if (!note) throw new Error(`No such note: ${noteId}`);
const shares = await this.refreshShares();
const noteShares = shares.filter(s => s.note_id === noteId);
const promises: Promise<void>[] = [];
for (const share of noteShares) {
promises.push(this.deleteShare(share.id));
}
await Promise.all(promises);
await Note.save({ id: note.id, is_shared: 0 });
}
public shareUrl(share: StateShare): string {
return `${this.api().baseUrl()}/shares/${share.id}`;
}
@ -170,13 +188,15 @@ export default class ShareService {
});
}
public async refreshShares() {
public async refreshShares(): Promise<StateShare[]> {
const result = await this.loadShares();
this.store.dispatch({
type: 'SHARE_SET',
shares: result.items,
});
return result.items;
}
public async refreshShareUsers(shareId: string) {

View File

@ -11,7 +11,7 @@ const theme: Theme = {
oddBackgroundColor: '#eeeeee',
color: '#32373F', // For regular text
colorError: 'red',
colorWarn: '#9A5B00',
colorWarn: 'rgb(228 86 0)',
colorFaded: '#7C8B9E', // For less important text
colorBright: '#000000', // For important text
dividerColor: '#dddddd',