Invitations: implement viewing, accepting and rejecting invitations.

master
Tom Hacohen 4 years ago
parent aed87399c0
commit c7a4110cbb

@ -81,6 +81,12 @@ export const routeResolver = new RouteResolver({
members: { members: {
}, },
}, },
invitations: {
incoming: {
},
outgoing: {
},
},
new: "new", new: "new",
import: "import", import: "import",
}, },

@ -7,6 +7,7 @@ import { Link, useHistory } from "react-router-dom";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import IconAdd from "@material-ui/icons/Add"; import IconAdd from "@material-ui/icons/Add";
import IconInvitation from "@material-ui/icons/MailOutline";
import ContactsIcon from "@material-ui/icons/Contacts"; import ContactsIcon from "@material-ui/icons/Contacts";
import CalendarTodayIcon from "@material-ui/icons/CalendarToday"; import CalendarTodayIcon from "@material-ui/icons/CalendarToday";
import FormatListBulletedIcon from "@material-ui/icons/FormatListBulleted"; import FormatListBulletedIcon from "@material-ui/icons/FormatListBulleted";
@ -56,6 +57,13 @@ export default function CollectionList(props: PropsType) {
return ( return (
<Container> <Container>
<AppBarOverride title="Collections"> <AppBarOverride title="Collections">
<IconButton
component={Link}
title="Invitations"
{...{ to: routeResolver.getRoute("collections.invitations") }}
>
<IconInvitation />
</IconButton>
<IconButton <IconButton
component={Link} component={Link}
title="New" title="New"

@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: © 2020 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import { Switch, Route, Redirect } from "react-router";
import * as Etebase from "etebase";
import { useCredentials } from "../credentials";
import { routeResolver } from "../App";
import LoadingIndicator from "../widgets/LoadingIndicator";
import AppBarOverride from "../widgets/AppBarOverride";
import { List, ListItem } from "../widgets/List";
import Container from "../widgets/Container";
import { IconButton } from "@material-ui/core";
import IconAccept from "@material-ui/icons/Done";
import IconReject from "@material-ui/icons/Close";
import ConfirmationDialog from "../widgets/ConfirmationDialog";
import PrettyFingerprint from "../widgets/PrettyFingerprint";
async function loadInvitations(etebase: Etebase.Account) {
const ret: Etebase.SignedInvitation[] = [];
const invitationManager = etebase.getInvitationManager();
let iterator: string | null = null;
let done = false;
while (!done) {
// FIXME: shouldn't be any
const invitations: any = await invitationManager.listIncoming({ iterator, limit: 30 });
iterator = invitations.iterator;
done = invitations.done;
ret.push(...invitations.data);
}
return ret;
}
export default function Invitations() {
return (
<Switch>
<Route
path={routeResolver.getRoute("collections.invitations")}
exact
>
<Redirect to={routeResolver.getRoute("collections.invitations.incoming")} />
</Route>
<Route
path={routeResolver.getRoute("collections.invitations.incoming")}
exact
>
<InvitationsIncoming
/>
</Route>
</Switch>
);
}
function InvitationsIncoming() {
const [invitations, setInvitations] = React.useState<Etebase.SignedInvitation[]>();
const [chosenInvitation, setChosenInvitation] = React.useState<Etebase.SignedInvitation>();
const etebase = useCredentials()!;
React.useEffect(() => {
loadInvitations(etebase).then(setInvitations);
}, [etebase]);
function removeInvitation(invite: Etebase.SignedInvitation) {
setInvitations(invitations?.filter((x) => x.uid !== invite.uid));
}
async function reject(invite: Etebase.SignedInvitation) {
const invitationManager = etebase.getInvitationManager();
await invitationManager.reject(invite);
removeInvitation(invite);
}
async function accept(invite: Etebase.SignedInvitation) {
const invitationManager = etebase.getInvitationManager();
await invitationManager.accept(invite);
setChosenInvitation(undefined);
removeInvitation(invite);
}
return (
<>
<AppBarOverride title="Incoming Invitations" />
<Container style={{ maxWidth: "30rem" }}>
{invitations ?
<List>
{(invitations.length > 0 ?
invitations.map((invite, idx) => (
<ListItem
key={invite.uid}
rightIcon={(
<>
<IconButton title="Reject" onClick={() => reject(invite)}>
<IconReject color="error" />
</IconButton>
<IconButton title="Accept" onClick={() => setChosenInvitation(invite)}>
<IconAccept color="secondary" />
</IconButton>
</>
)}
>
Invitation {idx + 1}
</ListItem>
))
:
<ListItem>
No invitations
</ListItem>
)}
</List>
:
<LoadingIndicator />
}
</Container>
{chosenInvitation && (
<ConfirmationDialog
title="Accept invitation"
labelOk="OK"
open={!!chosenInvitation}
onOk={() => accept(chosenInvitation)}
onCancel={() => setChosenInvitation(undefined)}
>
Please verify the inviter's security fingerprint to ensure the invitation is secure:
<div style={{ textAlign: "center" }}>
<PrettyFingerprint publicKey={chosenInvitation.fromPubkey} />
</div>
</ConfirmationDialog>
)}
</>
);
}

@ -20,6 +20,7 @@ import CollectionMembers from "./CollectionMembers";
import Collection from "./Collection"; import Collection from "./Collection";
import { useAsyncDispatch } from "../store"; import { useAsyncDispatch } from "../store";
import { collectionUpload } from "../store/actions"; import { collectionUpload } from "../store/actions";
import Invitations from "./Invitations";
const decryptCollections = getDecryptCollectionsFunction(); const decryptCollections = getDecryptCollectionsFunction();
@ -96,6 +97,11 @@ export default function CollectionsMain() {
onCancel={onCancel} onCancel={onCancel}
/> />
</Route> </Route>
<Route
path={routeResolver.getRoute("collections.invitations")}
>
<Invitations />
</Route>
<Route <Route
path={routeResolver.getRoute("collections._id")} path={routeResolver.getRoute("collections._id")}
render={({ match }) => { render={({ match }) => {

Loading…
Cancel
Save