Journals: remove unused journals components.

master
Tom Hacohen 4 years ago
parent 2f565994b7
commit c69f5e27ad

@ -1,207 +0,0 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import Dropzone from "react-dropzone";
import LoadingIndicator from "../widgets/LoadingIndicator";
import { SyncInfoJournal } from "../SyncGate";
import { store, CredentialsData, UserInfoData } from "../store";
import { addEntries } from "../store/actions";
import { createJournalEntry } from "../etesync-helpers";
import * as EteSync from "etesync";
import * as uuid from "uuid";
import * as ICAL from "ical.js";
import { ContactType, EventType, TaskType, PimType } from "../pim-types";
interface PropsType {
etesync: CredentialsData;
userInfo: UserInfoData;
syncJournal: SyncInfoJournal;
open: boolean;
onClose?: () => void;
}
export default function ImportDialog(props: PropsType) {
const [loading, setLoading] = React.useState(false);
function onFileDropCommon(itemsCreator: (fileText: string) => PimType[], acceptedFiles: File[], rejectedFiles: File[]) {
// XXX: implement handling of rejectedFiles
const reader = new FileReader();
reader.onabort = () => {
setLoading(false);
console.error("Import Aborted");
alert("file reading was aborted");
};
reader.onerror = (e) => {
setLoading(false);
console.error(e);
alert("file reading has failed");
};
reader.onload = async () => {
try {
const fileText = reader.result as string;
const items = itemsCreator(fileText);
const { syncJournal } = props;
const last = syncJournal.journalEntries.last() as EteSync.Entry;
const lastUid = last ? last.uid : null;
// XXX implement chunked push most likely...
let prevUid = lastUid;
const journalItems = items.map((item) => {
const ret = createJournalEntry(
props.etesync, props.userInfo, syncJournal.journal,
prevUid, EteSync.SyncEntryAction.Add, item.toIcal());
prevUid = ret.uid;
return ret;
});
await store.dispatch<any>(
addEntries(props.etesync, syncJournal.journal.uid, journalItems, lastUid)
);
} catch (e) {
console.error(e);
alert("An error has occurred, please contact developers.");
throw e;
} finally {
if (props.onClose) {
setLoading(false);
props.onClose();
}
}
};
if (acceptedFiles.length > 0) {
setLoading(true);
acceptedFiles.forEach((file) => {
reader.readAsText(file);
});
} else {
alert("Failed importing file. Is the file type supported?");
console.log("Failed importing files. Rejected:", rejectedFiles);
}
}
function onFileDropContact(acceptedFiles: File[], rejectedFiles: File[]) {
const itemsCreator = (fileText: string) => {
const mainComp = ICAL.parse(fileText);
return mainComp.map((comp) => {
const ret = new ContactType(new ICAL.Component(comp));
if (!ret.uid) {
ret.uid = uuid.v4();
}
return ret;
});
};
onFileDropCommon(itemsCreator, acceptedFiles, rejectedFiles);
}
function onFileDropEvent(acceptedFiles: File[], rejectedFiles: File[]) {
const itemsCreator = (fileText: string) => {
const calendarComp = new ICAL.Component(ICAL.parse(fileText));
return calendarComp.getAllSubcomponents("vevent").map((comp) => {
const ret = new EventType(comp);
if (!ret.uid) {
ret.uid = uuid.v4();
}
return ret;
});
};
onFileDropCommon(itemsCreator, acceptedFiles, rejectedFiles);
}
function onFileDropTask(acceptedFiles: File[], rejectedFiles: File[]) {
const itemsCreator = (fileText: string) => {
const calendarComp = new ICAL.Component(ICAL.parse(fileText));
return calendarComp.getAllSubcomponents("vtodo").map((comp) => {
const ret = new TaskType(comp);
if (!ret.uid) {
ret.uid = uuid.v4();
}
return ret;
});
};
onFileDropCommon(itemsCreator, acceptedFiles, rejectedFiles);
}
function onClose() {
if (loading) {
return;
}
if (props.onClose) {
props.onClose();
}
}
const { syncJournal } = props;
const collectionInfo = syncJournal.collection;
let acceptTypes;
let dropFunction;
if (collectionInfo.type === "ADDRESS_BOOK") {
acceptTypes = ["text/vcard", "text/directory", "text/x-vcard", ".vcf"];
dropFunction = onFileDropContact;
} else if (collectionInfo.type === "CALENDAR") {
acceptTypes = ["text/calendar", ".ics", ".ical"];
dropFunction = onFileDropEvent;
} else if (collectionInfo.type === "TASKS") {
acceptTypes = ["text/calendar", ".ics", ".ical"];
dropFunction = onFileDropTask;
}
return (
<React.Fragment>
<Dialog
open={props.open}
onClose={onClose}
>
<DialogTitle>Import entries from file?</DialogTitle>
<DialogContent>
{loading ?
<LoadingIndicator style={{ display: "block", margin: "auto" }} />
:
<Dropzone
onDrop={dropFunction}
multiple={false}
accept={acceptTypes}
>
{({ getRootProps, getInputProps }) => (
<section>
<div {...getRootProps()}>
<input {...getInputProps()} />
<DialogContentText id="alert-dialog-description">
To import entries from a file, drag 'n' drop it here, or click to open the file selector.
</DialogContentText>
</div>
</section>
)}
</Dropzone>
}
</DialogContent>
<DialogActions>
<Button disabled={loading} onClick={onClose} color="primary">
Cancel
</Button>
</DialogActions>
</Dialog>
</React.Fragment>
);
}

@ -1,104 +0,0 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import IconButton from "@material-ui/core/IconButton";
import IconEdit from "@material-ui/icons/Edit";
import IconMembers from "@material-ui/icons/People";
import IconImport from "@material-ui/icons/ImportExport";
import AppBarOverride from "../widgets/AppBarOverride";
import Container from "../widgets/Container";
import JournalEntries from "../components/JournalEntries";
import ImportDialog from "./ImportDialog";
import { SyncInfo, SyncInfoJournal } from "../SyncGate";
import { Link } from "react-router-dom";
import { routeResolver } from "../App";
import { CredentialsData, UserInfoData } from "../store";
interface PropsType {
etesync: CredentialsData;
userInfo: UserInfoData;
syncInfo: SyncInfo;
syncJournal: SyncInfoJournal;
isOwner: boolean;
}
class Journal extends React.Component<PropsType> {
public state: {
tab: number;
importDialogOpen: boolean;
};
constructor(props: PropsType) {
super(props);
this.importDialogToggle = this.importDialogToggle.bind(this);
this.state = {
tab: 0,
importDialogOpen: false,
};
}
public render() {
const { isOwner, syncJournal } = this.props;
const journal = syncJournal.journal;
const collectionInfo = syncJournal.collection;
const syncEntries = syncJournal.entries;
return (
<React.Fragment>
<AppBarOverride title={collectionInfo.displayName}>
{isOwner &&
<>
<IconButton
component={Link}
title="Edit"
{...{ to: routeResolver.getRoute("journals._id.edit", { journalUid: journal.uid }) }}
>
<IconEdit />
</IconButton>
<IconButton
component={Link}
title="Members"
{...{ to: routeResolver.getRoute("journals._id.members", { journalUid: journal.uid }) }}
>
<IconMembers />
</IconButton>
</>
}
<IconButton
title="Import"
onClick={this.importDialogToggle}
>
<IconImport />
</IconButton>
</AppBarOverride>
<Container>
<JournalEntries journal={journal} entries={syncEntries} />
</Container>
<ImportDialog
etesync={this.props.etesync}
userInfo={this.props.userInfo}
syncJournal={this.props.syncJournal}
open={this.state.importDialogOpen}
onClose={this.importDialogToggle}
/>
</React.Fragment>
);
}
private importDialogToggle() {
this.setState((state: any) => ({ importDialogOpen: !state.importDialogOpen }));
}
}
export default Journal;

@ -1,214 +0,0 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import IconDelete from "@material-ui/icons/Delete";
import IconCancel from "@material-ui/icons/Clear";
import IconSave from "@material-ui/icons/Save";
import * as colors from "@material-ui/core/colors";
import AppBarOverride from "../widgets/AppBarOverride";
import Container from "../widgets/Container";
import ConfirmationDialog from "../widgets/ConfirmationDialog";
import * as EteSync from "etesync";
import { SyncInfo } from "../SyncGate";
import ColorPicker from "../widgets/ColorPicker";
import { defaultColor, colorHtmlToInt, colorIntToHtml } from "../journal-processors";
interface PropsType {
syncInfo: SyncInfo;
item?: EteSync.CollectionInfo;
onSave: (info: EteSync.CollectionInfo, originalInfo?: EteSync.CollectionInfo) => void;
onDelete: (info: EteSync.CollectionInfo) => void;
onCancel: () => void;
}
interface FormErrors {
displayName?: string;
color?: string;
}
export default function JournalEdit(props: PropsType) {
const [errors, setErrors] = React.useState<FormErrors>({});
const [showDeleteDialog, setShowDeleteDialog] = React.useState(false);
const [info, setInfo] = React.useState<EteSync.CollectionInfo>();
const [selectedColor, setSelectedColor] = React.useState("");
React.useEffect(() => {
if (props.item !== undefined) {
setInfo(props.item);
if (props.item.color) {
setSelectedColor(colorIntToHtml(props.item.color));
}
} else {
setInfo({
uid: EteSync.genUid(),
type: "ADDRESS_BOOK",
displayName: "",
description: "",
} as EteSync.CollectionInfo);
}
}, [props.item]);
if (info === undefined) {
return <React.Fragment />;
}
function onSubmit(e: React.FormEvent<any>) {
e.preventDefault();
const saveErrors: FormErrors = {};
const fieldRequired = "This field is required!";
const { onSave } = props;
const displayName = info?.displayName;
const color = colorHtmlToInt(selectedColor);
if (!displayName) {
saveErrors.displayName = fieldRequired;
}
if (selectedColor && !color) {
saveErrors.color = "Must be of the form #RRGGBB or #RRGGBBAA or empty";
}
if (Object.keys(saveErrors).length > 0) {
setErrors(saveErrors);
return;
}
const item = new EteSync.CollectionInfo({ ...info, color: color });
onSave(item, props.item);
}
function onDeleteRequest() {
setShowDeleteDialog(true);
}
const { item, onDelete, onCancel } = props;
const pageTitle = (item !== undefined) ? item.displayName : "New Journal";
const styles = {
fullWidth: {
width: "100%",
},
submit: {
marginTop: 40,
marginBottom: 20,
textAlign: "right" as any,
},
};
const journalTypes = {
ADDRESS_BOOK: "Address Book",
CALENDAR: "Calendar",
TASKS: "Task List",
};
let collectionColorBox: React.ReactNode;
switch (info.type) {
case "CALENDAR":
case "TASKS":
collectionColorBox = (
<ColorPicker
defaultColor={defaultColor}
color={selectedColor}
onChange={(color: string) => setSelectedColor(color)}
error={errors.color}
/>
);
break;
}
return (
<>
<AppBarOverride title={pageTitle} />
<Container style={{ maxWidth: "30rem" }}>
<form onSubmit={onSubmit}>
<FormControl disabled={props.item !== undefined} style={styles.fullWidth}>
<InputLabel>
Collection type
</InputLabel>
<Select
name="type"
required
value={info.type}
onChange={(event: React.ChangeEvent<{ value: string }>) => setInfo({ ...info, type: event.target.value })}
>
{Object.keys(journalTypes).map((x) => (
<MenuItem key={x} value={x}>{journalTypes[x]}</MenuItem>
))}
</Select>
</FormControl>
<TextField
name="displayName"
required
label="Display name of this collection"
value={info.displayName}
onChange={(event: React.ChangeEvent<{ value: string }>) => setInfo({ ...info, displayName: event.target.value })}
style={styles.fullWidth}
margin="normal"
error={!!errors.displayName}
helperText={errors.displayName}
/>
<TextField
name="description"
label="Description (optional)"
value={info.description}
onChange={(event: React.ChangeEvent<{ value: string }>) => setInfo({ ...info, description: event.target.value })}
style={styles.fullWidth}
margin="normal"
/>
{collectionColorBox}
<div style={styles.submit}>
<Button
variant="contained"
onClick={onCancel}
>
<IconCancel style={{ marginRight: 8 }} />
Cancel
</Button>
{props.item &&
<Button
variant="contained"
style={{ marginLeft: 15, backgroundColor: colors.red[500], color: "white" }}
onClick={onDeleteRequest}
>
<IconDelete style={{ marginRight: 8 }} />
Delete
</Button>
}
<Button
type="submit"
variant="contained"
color="secondary"
style={{ marginLeft: 15 }}
>
<IconSave style={{ marginRight: 8 }} />
Save
</Button>
</div>
</form>
</Container>
<ConfirmationDialog
title="Delete Confirmation"
labelOk="Delete"
open={showDeleteDialog}
onOk={() => onDelete(props.item!)}
onCancel={() => setShowDeleteDialog(false)}
>
Are you sure you would like to delete this journal?
</ConfirmationDialog>
</>
);
}

@ -1,125 +0,0 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import TextField from "@material-ui/core/TextField";
import Checkbox from "@material-ui/core/Checkbox";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import LoadingIndicator from "../widgets/LoadingIndicator";
import ConfirmationDialog from "../widgets/ConfirmationDialog";
import PrettyFingerprint from "../widgets/PrettyFingerprint";
import * as EteSync from "etesync";
import { CredentialsData } from "../store";
interface PropsType {
etesync: CredentialsData;
info: EteSync.CollectionInfo;
onOk: (user: string, publicKey: string, readOnly: boolean) => void;
onClose: () => void;
}
export default function JournalMemberAddDialog(props: PropsType) {
const [addUser, setAddUser] = React.useState("");
const [publicKey, setPublicKey] = React.useState("");
const [readOnly, setReadOnly] = React.useState(false);
const [userChosen, setUserChosen] = React.useState(false);
const [error, setError] = React.useState<Error>();
function onAddRequest(_user: string) {
setUserChosen(true);
const { etesync } = props;
const creds = etesync.credentials;
const apiBase = etesync.serviceApiUrl;
const userInfoManager = new EteSync.UserInfoManager(creds, apiBase);
userInfoManager.fetch(addUser).then((userInfo) => {
setPublicKey(userInfo.publicKey);
}).catch((error) => {
setError(error);
});
}
function onOk() {
props.onOk(addUser, publicKey, readOnly);
}
const { onClose } = props;
if (error) {
return (
<>
<ConfirmationDialog
title="Error adding member"
labelOk="OK"
open
onOk={onClose}
onCancel={onClose}
>
User ({addUser}) not found. Have they setup their encryption password from one of the apps?
</ConfirmationDialog>
</>
);
}
if (publicKey) {
return (
<>
<ConfirmationDialog
title="Verify security fingerprint"
labelOk="OK"
open
onOk={onOk}
onCancel={onClose}
>
<p>
Verify {addUser}'s security fingerprint to ensure the encryption is secure.
</p>
<div style={{ textAlign: "center" }}>
<PrettyFingerprint publicKey={publicKey as any} />
</div>
</ConfirmationDialog>
</>
);
} else {
return (
<>
<ConfirmationDialog
title="Add member"
labelOk="OK"
open={!userChosen}
onOk={onAddRequest}
onCancel={onClose}
>
{userChosen ?
<LoadingIndicator />
:
<>
<TextField
name="addUser"
type="email"
placeholder="User email"
style={{ width: "100%" }}
value={addUser}
onChange={(ev) => setAddUser(ev.target.value)}
/>
<FormControlLabel
control={
<Checkbox
checked={readOnly}
onChange={(event) => setReadOnly(event.target.checked)}
/>
}
label="Read only?"
/>
</>
}
</ConfirmationDialog>
</>
);
}
}

@ -1,155 +0,0 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import sjcl from "sjcl";
import { List, ListItem } from "../widgets/List";
import IconMemberAdd from "@material-ui/icons/PersonAdd";
import VisibilityIcon from "@material-ui/icons/Visibility";
import AppBarOverride from "../widgets/AppBarOverride";
import Container from "../widgets/Container";
import LoadingIndicator from "../widgets/LoadingIndicator";
import ConfirmationDialog from "../widgets/ConfirmationDialog";
import JournalMemberAddDialog from "./JournalMemberAddDialog";
import * as EteSync from "etesync";
import { CredentialsData, UserInfoData } from "../store";
import { SyncInfoJournal } from "../SyncGate";
interface PropsType {
etesync: CredentialsData;
syncJournal: SyncInfoJournal;
userInfo: UserInfoData;
}
export default function JournalMembers(props: PropsType) {
const [members, setMembers] = React.useState<EteSync.JournalMemberJson[] | null>(null);
const [revokeUser, setRevokeUser] = React.useState<string | null>(null);
const [addMemberOpen, setAddMemberOpen] = React.useState(false);
function fetchMembers() {
const { etesync, syncJournal } = props;
const info = syncJournal.collection;
const creds = etesync.credentials;
const apiBase = etesync.serviceApiUrl;
const journalMembersManager = new EteSync.JournalMembersManager(creds, apiBase, info.uid);
journalMembersManager.list().then((members) => {
setMembers(members);
});
}
React.useEffect(() => {
fetchMembers();
}, []);
function onRevokeRequest(user: string) {
setRevokeUser(user);
}
function onRevokeDo() {
const { etesync, syncJournal } = props;
const info = syncJournal.collection;
const creds = etesync.credentials;
const apiBase = etesync.serviceApiUrl;
const journalMembersManager = new EteSync.JournalMembersManager(creds, apiBase, info.uid);
journalMembersManager.delete({ user: revokeUser!, key: "" }).then(() => {
fetchMembers();
});
setRevokeUser(null);
}
function onMemberAdd(user: string, publicKey: string, readOnly: boolean) {
const { etesync, syncJournal, userInfo } = props;
const journal = syncJournal.journal;
const derived = props.etesync.encryptionKey;
const keyPair = userInfo.getKeyPair(userInfo.getCryptoManager(derived));
const cryptoManager = journal.getCryptoManager(derived, keyPair);
const pubkeyBytes = sjcl.codec.bytes.fromBits(sjcl.codec.base64.toBits(publicKey));
const encryptedKey = sjcl.codec.base64.fromBits(sjcl.codec.bytes.toBits(cryptoManager.getEncryptedKey(keyPair, pubkeyBytes)));
const creds = etesync.credentials;
const apiBase = etesync.serviceApiUrl;
const journalMembersManager = new EteSync.JournalMembersManager(creds, apiBase, journal.uid);
journalMembersManager.create({ user, key: encryptedKey, readOnly }).then(() => {
fetchMembers();
});
setAddMemberOpen(false);
}
const { syncJournal } = props;
const info = syncJournal.collection;
const sharingAllowed = syncJournal.journal.version > 1;
return (
<>
<AppBarOverride title={`${info.displayName} - Members`} />
<Container style={{ maxWidth: "30rem" }}>
{members ?
<List>
<ListItem rightIcon={<IconMemberAdd />} onClick={() => setAddMemberOpen(true)}>
Add member
</ListItem>
{(members.length > 0 ?
members.map((member) => (
<ListItem
key={member.user}
onClick={() => onRevokeRequest(member.user)}
rightIcon={(member.readOnly) ? (<VisibilityIcon />) : undefined}
>
{member.user}
</ListItem>
))
:
<ListItem>
No members
</ListItem>
)}
</List>
:
<LoadingIndicator />
}
</Container>
<ConfirmationDialog
title="Remove member"
labelOk="OK"
open={revokeUser !== null}
onOk={onRevokeDo}
onCancel={() => setRevokeUser(null)}
>
Would you like to revoke {revokeUser}'s access?<br />
Please be advised that a malicious user would potentially be able to retain access to encryption keys. Please refer to the FAQ for more information.
</ConfirmationDialog>
{addMemberOpen &&
(sharingAllowed ?
<JournalMemberAddDialog
etesync={props.etesync}
info={info}
onOk={onMemberAdd}
onClose={() => setAddMemberOpen(false)}
/>
:
<ConfirmationDialog
title="Now Allowed"
labelOk="OK"
open
onOk={() => setAddMemberOpen(false)}
onClose={() => setAddMemberOpen(false)}
>
Sharing of old-style journals is not allowed. In order to share this journal, create a new one, and copy its contents over using the "import" dialog. If you are experiencing any issues, please contact support.
</ConfirmationDialog>
)
}
</>
);
}

@ -1,103 +0,0 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import { History } from "history";
import { Link } from "react-router-dom";
import IconButton from "@material-ui/core/IconButton";
import IconAdd from "@material-ui/icons/Add";
import ContactsIcon from "@material-ui/icons/Contacts";
import CalendarTodayIcon from "@material-ui/icons/CalendarToday";
import FormatListBulletedIcon from "@material-ui/icons/FormatListBulleted";
import { List, ListItem } from "../widgets/List";
import AppBarOverride from "../widgets/AppBarOverride";
import Container from "../widgets/Container";
import { routeResolver } from "../App";
import { JournalsData, UserInfoData, CredentialsData } from "../store";
import ColorBox from "../widgets/ColorBox";
import { colorIntToHtml } from "../journal-processors";
interface PropsType {
etesync: CredentialsData;
journals: JournalsData;
userInfo: UserInfoData;
history: History;
}
export default function JournalsList(props: PropsType) {
const derived = props.etesync.encryptionKey;
function journalClicked(journalUid: string) {
props.history.push(routeResolver.getRoute("journals._id", { journalUid }));
}
const journalMap = props.journals.reduce(
(ret, journal) => {
const userInfo = props.userInfo;
const keyPair = userInfo.getKeyPair(userInfo.getCryptoManager(derived));
const cryptoManager = journal.getCryptoManager(derived, keyPair);
const info = journal.getInfo(cryptoManager);
let colorBox: React.ReactElement | undefined;
switch (info.type) {
case "CALENDAR":
case "TASKS":
colorBox = (
<ColorBox size={24} color={colorIntToHtml(info.color)} />
);
break;
}
ret[info.type] = ret[info.type] || [];
ret[info.type].push(
<ListItem key={journal.uid} rightIcon={colorBox} insetChildren
onClick={() => journalClicked(journal.uid)}>
{info.displayName}
</ListItem>
);
return ret;
},
{
CALENDAR: [],
ADDRESS_BOOK: [],
TASKS: [],
});
return (
<Container>
<AppBarOverride title="Journals">
<IconButton
component={Link}
title="New"
{...{ to: routeResolver.getRoute("journals.new") }}
>
<IconAdd />
</IconButton>
</AppBarOverride>
<List>
<ListItem
primaryText="Address Books"
leftIcon={<ContactsIcon />}
nestedItems={journalMap.ADDRESS_BOOK}
/>
<ListItem
primaryText="Calendars"
leftIcon={<CalendarTodayIcon />}
nestedItems={journalMap.CALENDAR}
/>
<ListItem
primaryText="Tasks"
leftIcon={<FormatListBulletedIcon />}
nestedItems={journalMap.TASKS}
/>
</List>
</Container>
);
}

@ -1,96 +0,0 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import ContactsIcon from "@material-ui/icons/Contacts";
import CalendarTodayIcon from "@material-ui/icons/CalendarToday";
import FormatListBulletedIcon from "@material-ui/icons/FormatListBulleted";
import { List, ListItem } from "../widgets/List";
import AppBarOverride from "../widgets/AppBarOverride";
import Container from "../widgets/Container";
import { JournalsData, UserInfoData, CredentialsData } from "../store";
import ImportDialog from "./ImportDialog";
import { SyncInfo, SyncInfoJournal } from "../SyncGate";
import { colorIntToHtml } from "../journal-processors";
import ColorBox from "../widgets/ColorBox";
interface PropsType {
etesync: CredentialsData;
journals: JournalsData;
userInfo: UserInfoData;
syncInfo: SyncInfo;
}
export default function JournalsList(props: PropsType) {
const [selectedJournal, setSelectedJournal] = React.useState<SyncInfoJournal>();
const derived = props.etesync.encryptionKey;
const journalMap = props.journals.reduce(
(ret, journal) => {
const userInfo = props.userInfo;
const keyPair = userInfo.getKeyPair(userInfo.getCryptoManager(derived));
const cryptoManager = journal.getCryptoManager(derived, keyPair);
const info = journal.getInfo(cryptoManager);
let colorBox: React.ReactElement | undefined;
switch (info.type) {
case "CALENDAR":
case "TASKS":
colorBox = (
<ColorBox size={24} color={colorIntToHtml(info.color)} />
);
break;
}
ret[info.type] = ret[info.type] || [];
ret[info.type].push(
<ListItem key={journal.uid} rightIcon={colorBox} insetChildren
onClick={() => setSelectedJournal(props.syncInfo.get(journal.uid))}>
{info.displayName} ({journal.uid.slice(0, 5)})
</ListItem>
);
return ret;
},
{
CALENDAR: [],
ADDRESS_BOOK: [],
TASKS: [],
});
return (
<Container>
<AppBarOverride title="Journals Import" />
<List>
<ListItem
primaryText="Address Books"
leftIcon={<ContactsIcon />}
nestedItems={journalMap.ADDRESS_BOOK}
/>
<ListItem
primaryText="Calendars"
leftIcon={<CalendarTodayIcon />}
nestedItems={journalMap.CALENDAR}
/>
<ListItem
primaryText="Tasks"
leftIcon={<FormatListBulletedIcon />}
nestedItems={journalMap.TASKS}
/>
</List>
{selectedJournal && (
<ImportDialog
etesync={props.etesync}
userInfo={props.userInfo}
syncJournal={selectedJournal}
open={!!selectedJournal}
onClose={() => setSelectedJournal(undefined)}
/>
)}
</Container>
);
}

@ -1,181 +0,0 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import { Location, History } from "history";
import { Route, Switch } from "react-router";
import Journal from "./Journal";
import JournalEdit from "./JournalEdit";
import JournalMembers from "./JournalMembers";
import JournalsList from "./JournalsList";
import JournalsListImport from "./JournalsListImport";
import { routeResolver } from "../App";
import { store, JournalsData, UserInfoData, CredentialsData } from "../store";
import { addJournal, deleteJournal, updateJournal } from "../store/actions";
import { SyncInfo } from "../SyncGate";
import * as EteSync from "etesync";
class Journals extends React.PureComponent {
public props: {
etesync: CredentialsData;
journals: JournalsData;
userInfo: UserInfoData;
syncInfo: SyncInfo;
history: History;
location: Location;
};
constructor(props: any) {
super(props);
this.onCancel = this.onCancel.bind(this);
this.onItemDelete = this.onItemDelete.bind(this);
this.onItemSave = this.onItemSave.bind(this);
}
public render() {
return (
<Switch>
<Route
path={routeResolver.getRoute("journals")}
exact
render={({ history }) => (
<>
<JournalsList
userInfo={this.props.userInfo}
etesync={this.props.etesync}
journals={this.props.journals}
history={history}
/>
</>
)}
/>
<Route
path={routeResolver.getRoute("journals.import")}
exact
render={() => (
<>
<JournalsListImport
userInfo={this.props.userInfo}
etesync={this.props.etesync}
journals={this.props.journals}
syncInfo={this.props.syncInfo}
/>
</>
)}
/>
<Route
path={routeResolver.getRoute("journals.new")}
render={() => (
<JournalEdit
syncInfo={this.props.syncInfo}
onSave={this.onItemSave}
onDelete={this.onItemDelete}
onCancel={this.onCancel}
/>
)}
/>
<Route
path={routeResolver.getRoute("journals._id")}
render={({ match }) => {
const journalUid = match.params.journalUid;
const syncJournal = this.props.syncInfo.get(journalUid);
if (!syncJournal) {
return (<div>Journal not found!</div>);
}
const isOwner = syncJournal.journal.owner === this.props.etesync.credentials.email;
const collectionInfo = syncJournal.collection;
return (
<Switch>
<Route
path={routeResolver.getRoute("journals._id.edit")}
render={() => (
<JournalEdit
syncInfo={this.props.syncInfo}
item={collectionInfo}
onSave={this.onItemSave}
onDelete={this.onItemDelete}
onCancel={this.onCancel}
/>
)}
/>
<Route
path={routeResolver.getRoute("journals._id.members")}
render={() => (
<JournalMembers
etesync={this.props.etesync}
syncJournal={syncJournal}
userInfo={this.props.userInfo}
/>
)}
/>
<Route
path={routeResolver.getRoute("journals._id")}
render={() => (
<Journal
etesync={this.props.etesync}
userInfo={this.props.userInfo}
syncInfo={this.props.syncInfo}
syncJournal={syncJournal}
isOwner={isOwner}
/>
)}
/>
</Switch>
);
}}
/>
</Switch>
);
}
public onItemSave(info: EteSync.CollectionInfo, originalInfo?: EteSync.CollectionInfo) {
const syncJournal = this.props.syncInfo.get(info.uid);
const derived = this.props.etesync.encryptionKey;
const userInfo = this.props.userInfo;
const existingJournal = (syncJournal) ? syncJournal.journal.serialize() : { uid: info.uid };
const journal = new EteSync.Journal(existingJournal);
const keyPair = userInfo.getKeyPair(userInfo.getCryptoManager(derived));
const cryptoManager = journal.getCryptoManager(derived, keyPair);
journal.setInfo(cryptoManager, info);
if (originalInfo) {
store.dispatch<any>(updateJournal(this.props.etesync, journal)).then(() =>
this.props.history.goBack()
);
} else {
store.dispatch<any>(addJournal(this.props.etesync, journal)).then(() =>
this.props.history.goBack()
);
}
}
public onItemDelete(info: EteSync.CollectionInfo) {
const syncJournal = this.props.syncInfo.get(info.uid);
const derived = this.props.etesync.encryptionKey;
const userInfo = this.props.userInfo;
const existingJournal = (syncJournal) ? syncJournal.journal.serialize() : { uid: info.uid };
const journal = new EteSync.Journal(existingJournal);
const keyPair = userInfo.getKeyPair(userInfo.getCryptoManager(derived));
const cryptoManager = journal.getCryptoManager(derived, keyPair);
journal.setInfo(cryptoManager, info);
store.dispatch<any>(deleteJournal(this.props.etesync, journal)).then(() =>
this.props.history.push(routeResolver.getRoute("journals"))
);
}
public onCancel() {
this.props.history.goBack();
}
}
export default Journals;

@ -1,68 +0,0 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import { Route, Switch, withRouter } from "react-router";
import * as EteSync from "etesync";
import { routeResolver } from "../App";
import { History } from "history";
import { SyncInfo } from "../SyncGate";
function objValues(obj: any) {
return Object.keys(obj).map((x) => obj[x]);
}
export function journalView(JournalList: any, JournalItem: any) {
return withRouter(class extends React.PureComponent {
public props: {
journal: EteSync.Journal;
entries: {[key: string]: any};
syncInfo?: SyncInfo;
history?: History;
};
constructor(props: any) {
super(props);
this.itemClicked = this.itemClicked.bind(this);
}
public itemClicked(contact: any) {
const uid = contact.uid;
this.props.history!.push(
routeResolver.getRoute("journals._id.items._id", { journalUid: this.props.journal.uid, itemUid: encodeURIComponent(uid) }));
}
public render() {
const items = this.props.entries;
return (
<Switch>
<Route
path={routeResolver.getRoute("journals._id")}
exact
render={() => (
<JournalList syncInfo={this.props.syncInfo} entries={objValues(items)} onItemClick={this.itemClicked} />
)
}
/>
<Route
path={routeResolver.getRoute("journals._id.items._id")}
exact
render={({ match }) => {
return (
<JournalItem item={items[`${match.params.journalUid}|${decodeURIComponent(match.params.itemUid)}`]} />
);
}}
/>
</Switch>
);
}
});
}
export default journalView;

@ -1,261 +0,0 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as Immutable from "immutable";
import { AutoSizer, List as VirtualizedList } from "react-virtualized";
import * as React from "react";
import { List, ListItem } from "../widgets/List";
import Dialog from "@material-ui/core/Dialog";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import IconAdd from "@material-ui/icons/Add";
import IconDelete from "@material-ui/icons/Delete";
import IconEdit from "@material-ui/icons/Edit";
import IconError from "@material-ui/icons/Error";
import { TaskType, EventType, ContactType, parseString } from "../pim-types";
import * as EteSync from "etesync";
import LoadingIndicator from "../widgets/LoadingIndicator";
import { useCredentials } from "../login";
import { createJournalEntry } from "../etesync-helpers";
import { useSelector, useDispatch } from "react-redux";
import { StoreState } from "../store";
import { addEntries } from "../store/actions";
interface RollbackToHereDialogPropsType {
journal: EteSync.Journal;
entries: Immutable.List<EteSync.SyncEntry>;
entryUid: string;
open: boolean;
onClose: () => void;
}
function RollbackToHereDialog(props: RollbackToHereDialogPropsType) {
const [loading, setLoading] = React.useState(false);
const etesync = useCredentials()!;
const dispatch = useDispatch();
const userInfo = useSelector((state: StoreState) => state.cache.userInfo);
async function go() {
setLoading(true);
const changes = new Map<string, EteSync.SyncEntry>();
for (const entry of props.entries.reverse()) {
const comp = parseString(entry.content);
const itemComp = comp.getFirstSubcomponent("vevent") ?? comp.getFirstSubcomponent("vtodo") ?? comp;
const itemUid = itemComp.getFirstPropertyValue("uid");
if (itemUid && !changes.has(itemUid)) {
changes.set(itemUid, entry);
}
if (entry.uid === props.entryUid) {
break;
}
}
const last = props.entries.last(null);
const lastUid = last?.uid ? last.uid : null;
// XXX implement chunked push most likely...
let prevUid = lastUid;
const journalItems = [];
for (const syncEntry of changes.values()) {
if (syncEntry.action === EteSync.SyncEntryAction.Delete) {
const ret = createJournalEntry(etesync, userInfo, props.journal, prevUid, EteSync.SyncEntryAction.Add, syncEntry.content);
journalItems.push(ret);
prevUid = ret.uid;
}
}
if (journalItems.length > 0) {
await dispatch<any>(
addEntries(etesync, props.journal.uid, journalItems, lastUid)
);
}
props.onClose();
}
return (
<Dialog
open={props.open}
onClose={props.onClose}
>
<DialogTitle>
Recover items
</DialogTitle>
<DialogContent>
{loading ? (
<LoadingIndicator style={{ display: "block", margin: "auto" }} />
) : (
<p>
This function restores all of the deleted items that happened after this change entry. It will not modify any items that haven't been changed since the item was deleted.
</p>
)}
</DialogContent>
<DialogActions>
<Button
color="primary"
disabled={loading}
onClick={props.onClose}
>
Cancel
</Button>
<Button
color="primary"
disabled={loading}
onClick={go}
>
Go
</Button>
</DialogActions>
</Dialog>
);
}
interface PropsType {
journal: EteSync.Journal;
entries: Immutable.List<EteSync.SyncEntry>;
uid?: string;
}
export default function JournalEntries(props: PropsType) {
const [dialog, setDialog] = React.useState<EteSync.SyncEntry>();
const [rollbackDialogId, setRollbackDialogId] = React.useState<string>();
if (props.journal === undefined) {
return (<div>Loading</div>);
}
const rowRenderer = (params: { index: number, key: string, style: React.CSSProperties }) => {
const { key, index, style } = params;
// eslint-disable-next-line react/prop-types
const syncEntry = props.entries.get(props.entries.size - index - 1)!;
let comp;
try {
comp = parseString(syncEntry.content);
} catch (e) {
const icon = (<IconError style={{ color: "red" }} />);
return (
<ListItem
key={key}
style={style}
leftIcon={icon}
primaryText="Failed parsing item"
secondaryText="Unknown"
onClick={() => setDialog(syncEntry)}
/>
);
}
let icon;
if (syncEntry.action === EteSync.SyncEntryAction.Add) {
icon = (<IconAdd style={{ color: "#16B14B" }} />);
} else if (syncEntry.action === EteSync.SyncEntryAction.Change) {
icon = (<IconEdit style={{ color: "#FEB115" }} />);
} else if (syncEntry.action === EteSync.SyncEntryAction.Delete) {
icon = (<IconDelete style={{ color: "#F20C0C" }} />);
}
let name;
let uid;
if (comp.name === "vcalendar") {
if (EventType.isEvent(comp)) {
const vevent = EventType.fromVCalendar(comp);
name = vevent.summary;
uid = vevent.uid;
} else {
const vtodo = TaskType.fromVCalendar(comp);
name = vtodo.summary;
uid = vtodo.uid;
}
} else if (comp.name === "vcard") {
const vcard = new ContactType(comp);
name = vcard.fn;
uid = vcard.uid;
} else {
name = "Error processing entry";
uid = "";
}
// eslint-disable-next-line react/prop-types
if (props.uid && (props.uid !== uid)) {
return undefined;
}
return (
<ListItem
key={key}
style={style}
leftIcon={icon}
primaryText={name}
secondaryText={uid}
onClick={() => setDialog(syncEntry)}
/>
);
};
return (
<div>
<RollbackToHereDialog
journal={props.journal}
entries={props.entries}
entryUid={rollbackDialogId!}
open={!!rollbackDialogId}
onClose={() => setRollbackDialogId(undefined)}
/>
<Dialog
open={dialog !== undefined}
onClose={() => setDialog(undefined)}
>
<DialogTitle>
Raw Content
</DialogTitle>
<DialogContent>
<div>Entry UID: <pre className="d-inline-block">{dialog?.uid}</pre></div>
<div>Content:
<pre>{dialog?.content}</pre>
</div>
</DialogContent>
<DialogActions>
<Button
color="primary"
onClick={() => {
setDialog(undefined);
setRollbackDialogId(dialog?.uid);
}}
>
Recover items until here
</Button>
<Button
color="primary"
onClick={() => setDialog(undefined)}
>
Close
</Button>
</DialogActions>
</Dialog>
<List style={{ height: "calc(100vh - 300px)" }}>
<AutoSizer>
{({ height, width }) => (
<VirtualizedList
width={width}
height={height}
rowCount={props.entries.size}
rowHeight={56}
rowRenderer={rowRenderer}
/>
)}
</AutoSizer>
</List>
</div>
);
}
Loading…
Cancel
Save