Merge pull request #478 from filecoin-project/@martinalong/optimistic-updates

Optimistic updates
This commit is contained in:
martinalong 2020-12-14 17:54:32 -08:00 committed by GitHub
commit 47b3df2152
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 173 additions and 122 deletions

View File

@ -183,7 +183,7 @@ export const deleteFiles = async (fileCids, fileIds = [], noAlert) => {
return false;
}
Events.dispatchMessage({ message: "Files successfully deleted!", status: "INFO" });
// Events.dispatchMessage({ message: "Files successfully deleted!", status: "INFO" });
return response;
}

View File

@ -147,11 +147,24 @@ export default class ApplicationPage extends React.Component {
}
_handleUpdateViewer = (newViewerState) => {
if (this.state.viewer && newViewerState.id && newViewerState.id === this.state.viewer.id) {
// let setAsyncState = (newState) =>
// new Promise((resolve) =>
// this.setState(
// {
// viewer: { ...this.state.viewer, ...newState, type: "VIEWER" },
// },
// resolve
// )
// );
// await setAsyncState(newViewerState);
this.setState({
viewer: { ...this.state.viewer, ...newViewerState, type: "VIEWER" },
});
}
};
_handleUpdateData = ({ data }) => {
this.setState({ data });
};
_handleSetupWebsocket = async () => {
@ -424,12 +437,6 @@ export default class ApplicationPage extends React.Component {
return false;
};
_handleViewerChange = (e) => {
this.setState({
viewer: { ...this.state.viewer, [e.target.name]: e.target.value },
});
};
_handleSelectedChange = (e) => {
this.setState({
selected: { ...this.state.selected, [e.target.name]: e.target.value },
@ -484,10 +491,6 @@ export default class ApplicationPage extends React.Component {
return alert(JSON.stringify(options));
};
_handleUpdateData = ({ data }) => {
this.setState({ data });
};
_handleNavigateTo = (next, data = null, redirect = false) => {
if (next.id) {
window.history.replaceState(
@ -582,6 +585,7 @@ export default class ApplicationPage extends React.Component {
render() {
// NOTE(jim): Not authenticated.
console.log(this.state.viewer);
if (!this.state.viewer) {
return (
<WebsitePrototypeWrapper
@ -630,12 +634,12 @@ export default class ApplicationPage extends React.Component {
viewer: this.state.viewer,
selected: this.state.selected,
onSelectedChange: this._handleSelectedChange,
onViewerChange: this._handleViewerChange,
onAction: this._handleAction,
onUpload: this._handleUploadFiles,
onBack: this._handleBack,
onForward: this._handleForward,
onUpdateData: this._handleUpdateData,
onUpdateViewer: this._handleUpdateViewer,
sceneId: current.target.id,
mobile: this.state.mobile,
resources: this.props.resources,
@ -654,6 +658,7 @@ export default class ApplicationPage extends React.Component {
onCancel: this._handleDismissSidebar,
onUpload: this._handleUploadFiles,
onAction: this._handleAction,
onUpdateViewer: this._handleUpdateViewer,
resources: this.props.resources,
});
}
@ -682,6 +687,7 @@ export default class ApplicationPage extends React.Component {
{scene}
</ApplicationLayout>
<GlobalCarousel
onUpdateViewer={this._handleUpdateViewer}
resources={this.props.resources}
viewer={this.state.viewer}
current={

View File

@ -289,22 +289,30 @@ export default class CarouselSidebarData extends React.Component {
}, 1000);
};
_handleDelete = async (cid) => {
_handleDelete = (cid) => {
const message = `Are you sure you want to delete this? It will be deleted from your slates as well`;
if (!window.confirm(message)) {
return;
}
await this.setState({ loading: cid });
let library = this.props.viewer.library;
library[0].children = library[0].children.filter((obj) => obj.cid !== cid);
this.props.onUpdateViewer({ library });
// await this.setState({ loading: cid });
// NOTE(jim): Accepts ID as well if CID can't be found.
// Since our IDS are unique.
await UserBehaviors.deleteFiles(cid, this.props.data.id);
this.setState({ loading: false });
UserBehaviors.deleteFiles(cid, this.props.data.id);
// this.setState({ loading: false });
};
_handleAdd = async (slate) => {
await this.setState({ pickerLoading: slate.id });
this.setState({
selected: { ...this.state.selected, [slate.id]: !this.state.selected[slate.id] },
// pickerLoading: null,
});
// await this.setState({ pickerLoading: slate.id });
if (this.state.selected[slate.id]) {
await UserBehaviors.removeFromSlate({ slate, ids: [this.props.data.id] });
} else {
@ -314,10 +322,6 @@ export default class CarouselSidebarData extends React.Component {
fromSlate: this.props.fromSlate,
});
}
this.setState({
selected: { ...this.state.selected, [slate.id]: !this.state.selected[slate.id] },
pickerLoading: null,
});
};
render() {

View File

@ -286,8 +286,9 @@ export default class CarouselSidebarSlate extends React.Component {
this.setState({ loading: false });
};
_handleDelete = async (cid) => {
_handleDelete = (cid) => {
if (this.props.external || !this.props.isOwner) return;
if (
!window.confirm(
"Are you sure you want to delete this? It will be removed from all your slates too."
@ -295,13 +296,22 @@ export default class CarouselSidebarSlate extends React.Component {
) {
return;
}
console.log(this.props.data);
await this.setState({ loading: "deleting" });
let slates = this.props.viewer.slates;
let slateId = this.props.current.id;
for (let slate of slates) {
if (slate.id === slateId) {
slate.data.objects = slate.data.objects.filter((obj) => obj.cid !== cid);
this.props.onUpdateViewer({ slates });
break;
}
}
// await this.setState({ loading: "deleting" });
// NOTE(jim): Accepts ID as well if CID can't be found.
// Since our IDS are unique.
await UserBehaviors.deleteFiles(cid, this.props.data.id);
this.setState({ loading: false });
UserBehaviors.deleteFiles(cid, this.props.data.id);
// this.setState({ loading: false });
};
_toggleAccordion = (tab) => {
@ -309,7 +319,11 @@ export default class CarouselSidebarSlate extends React.Component {
};
_handleAdd = async (slate) => {
await this.setState({ pickerLoading: slate.id });
this.setState({
selected: { ...this.state.selected, [slate.id]: !this.state.selected[slate.id] },
// pickerLoading: null,
});
// await this.setState({ pickerLoading: slate.id });
if (this.state.selected[slate.id]) {
await UserBehaviors.removeFromSlate({ slate, ids: [this.props.data.id] });
} else {
@ -319,10 +333,6 @@ export default class CarouselSidebarSlate extends React.Component {
fromSlate: this.props.fromSlate,
});
}
this.setState({
selected: { ...this.state.selected, [slate.id]: !this.state.selected[slate.id] },
pickerLoading: null,
});
};
render() {

View File

@ -341,7 +341,7 @@ export default class DataView extends React.Component {
return;
};
_handleDelete = async (cid, id) => {
_handleDelete = (cid, id) => {
const message = `Are you sure you want to delete these files? They will be deleted from your slates as well`;
if (!window.confirm(message)) {
return;
@ -365,10 +365,16 @@ export default class DataView extends React.Component {
});
}
await this._handleLoading({ cids });
await UserBehaviors.deleteFiles(cids, ids);
this._handleLoading({ cids });
await this.setState({ checked: {} });
let library = this.props.viewer.library;
library[0].children = library[0].children.filter(
(obj) => !ids.includes(obj.id) && !cids.includes(obj.cid)
);
this.props.onUpdateViewer({ library });
// await this._handleLoading({ cids });
UserBehaviors.deleteFiles(cids, ids);
// this._handleLoading({ cids });
this.setState({ checked: {} });
};
_handleSelect = (index) => {

View File

@ -1037,6 +1037,17 @@ export class SlateLayout extends React.Component {
}
this.setState({ checked: {} });
}
let slates = this.props.viewer.slates;
let slateId = this.props.current.id;
for (let slate of slates) {
if (slate.id === slateId) {
slate.data.objects = slate.data.objects.filter((obj) => !ids.includes(obj.id));
this.props.onUpdateViewer({ slates });
break;
}
}
UserBehaviors.removeFromSlate({ slate: this.props.current, ids });
};
@ -1063,10 +1074,22 @@ export class SlateLayout extends React.Component {
}
}
await this._handleLoading({ cids });
let slates = this.props.viewer.slates;
let slateId = this.props.current.id;
for (let slate of slates) {
if (slate.id === slateId) {
slate.data.objects = slate.data.objects.filter(
(obj) => !ids.includes(obj.id) && !cids.includes(obj.cid)
);
this.props.onUpdateViewer({ slates });
break;
}
}
// await this._handleLoading({ cids });
await UserBehaviors.deleteFiles(cids, ids);
this._handleLoading({ cids });
await this.setState({ checked: {} });
this.setState({ checked: {} });
};
_handleLoading = ({ cids }) => {

View File

@ -140,9 +140,7 @@ export class SlatePicker extends React.Component {
onClick={() => this.props.onAdd(slate)}
>
<div css={STYLES_ICON_BOX}>
{this.props.loading && this.props.loading === slate.id ? (
<LoaderSpinner style={{ height: 20, width: 20, margin: "2px 8px 2px 2px" }} />
) : selected[slate.id] ? (
{selected[slate.id] ? (
<SVG.Slate
height="24px"
style={{

View File

@ -57,7 +57,8 @@ export default class SidebarAddFileToSlate extends React.Component {
};
_handleSubmit = async () => {
await this.setState({ loading: true });
this.props.onCancel();
// await this.setState({ loading: true });
for (let slate of Object.values(this.state.selected)) {
if (!slate) continue;
await UserBehaviors.addToSlate({
@ -66,8 +67,7 @@ export default class SidebarAddFileToSlate extends React.Component {
fromSlate: this.props.sidebarData.fromSlate,
});
}
this.setState({ loading: false });
this.props.onCancel();
// this.setState({ loading: false });
};
render() {

View File

@ -53,8 +53,20 @@ export default class SidebarSingleSlateSettings extends React.Component {
};
_handleSubmit = async () => {
this.setState({ loading: true });
// this.setState({ loading: true });
let slates = this.props.viewer.slates;
for (let slate of slates) {
if (slate.id === this.props.current.id) {
slate.data.name = this.state.name;
slate.data.public = this.state.public;
slate.data.body = this.state.body;
this.props.onUpdateViewer({ slates });
break;
}
}
this.props.onCancel();
const response = await Actions.updateSlate({
id: this.props.current.id,
data: {
@ -65,10 +77,9 @@ export default class SidebarSingleSlateSettings extends React.Component {
});
if (Events.hasError(response)) {
this.setState({ loading: false });
// this.setState({ loading: false });
return;
}
this.props.onCancel();
};
_handleCancel = () => {

View File

@ -392,6 +392,7 @@ export class GlobalCarousel extends React.Component {
<CarouselSidebarData
viewer={this.props.viewer}
display={this.state.showSidebar && !isUnityGame ? "block" : "none"}
onUpdateViewer={this.props.onUpdateViewer}
onClose={this._handleClose}
key={data.id}
saving={this.state.saving}
@ -405,6 +406,9 @@ export class GlobalCarousel extends React.Component {
) : (
<CarouselSidebarSlate
display={this.state.showSidebar && !isUnityGame ? "block" : "none"}
viewer={this.props.viewer}
onUpdateViewer={this.props.onUpdateViewer}
current={this.props.current}
key={data.id}
saving={this.state.saving}
loading={this.state.loading}

View File

@ -99,10 +99,12 @@ export default async (req, res) => {
return res.status(500).send({ decorator: "SERVER_UPDATE_SLATE", error: true });
}
if (!layoutOnly && isOwner) {
let slates = await Data.getSlatesByUserId({ userId: id });
if (slates) {
ViewerManager.hydratePartialSlates(slates, id);
}
}
if (!layoutOnly && !req.body.data.autoSave) {
if (response.data.public !== slate.data.public) {

View File

@ -39,6 +39,7 @@ export default class SceneFilesFolder extends React.Component {
onAction={this.props.onAction}
viewer={this.props.viewer}
items={this.props.viewer.library[0].children}
onUpdateViewer={this.props.onUpdateViewer}
/>
) : (
<EmptyState>

View File

@ -11,7 +11,6 @@ import { css } from "@emotion/react";
import ScenePage from "~/components/core/ScenePage";
import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview";
import DataView from "~/components/core/DataView";
import ScenePageHeader from "~/components/core/ScenePageHeader";
const STYLES_VIDEO_BIG = css`

View File

@ -13,14 +13,26 @@ const STYLES_BUTTONS = css`
align-items: center;
`;
const STATUS_BUTTON_MAP = {
trusted: "Remove peer",
untrusted: "Add peer",
sent: "Cancel request",
received: "Accept request",
export default class SceneProfile extends React.Component {
state = {
isFollowing: !!this.props.viewer.subscriptions.filter((entry) => {
return entry.target_user_id === this.props.data.id;
}).length,
};
componentDidUpdate = (prevProps) => {
if (
this.props.data.id !== prevProps.data.id ||
this.props.viewer.subscriptions !== prevProps.viewer.subscriptions
) {
this.setState({
isFollowing: !!this.props.viewer.subscriptions.filter((entry) => {
return entry.target_user_id === this.props.data.id;
}).length,
});
}
};
export default class SceneProfile extends React.Component {
// _handleTrust = async (trustStatus, trustId) => {
// if (trustStatus === "untrusted" || trustStatus === "sent") {
// await Actions.createTrustRelationship({
@ -38,46 +50,16 @@ export default class SceneProfile extends React.Component {
// };
_handleFollow = async () => {
this.setState({ isFollowing: !this.state.isFollowing });
await Actions.createSubscription({
userId: this.props.data.id,
});
};
render() {
let trustId, followStatus, relation;
// let trustStatus = "untrusted";
let viewer = this.props.viewer;
// let trust = viewer.trusted.filter((entry) => {
// return entry.target_user_id === this.props.data.id;
// });
// if (trust.length) {
// relation = trust[0];
// trustId = relation.id;
// if (relation.data.verified) {
// trustStatus = "trusted";
// } else {
// trustStatus = "sent";
// }
// }
// let pendingTrust = viewer.pendingTrusted.filter((entry) => {
// return entry.owner_user_id === this.props.data.id;
// });
// if (pendingTrust.length) {
// relation = pendingTrust[0];
// trustId = relation.id;
// if (pendingTrust[0].data.verified) {
// trustStatus = "trusted";
// } else {
// trustStatus = "received";
// }
// }
followStatus = !!viewer.subscriptions.filter((entry) => {
return entry.target_user_id === this.props.data.id;
}).length;
let buttons = (
<div css={STYLES_BUTTONS}>
{followStatus ? (
{this.state.isFollowing ? (
<ButtonSecondary style={{ marginRight: 8 }} onClick={this._handleFollow}>
Unfollow
</ButtonSecondary>

View File

@ -99,26 +99,30 @@ export default class SceneSlate extends React.Component {
state = {
...(this.props.current, this.props.viewer),
loading: false,
saving: "IDLE",
isOwner: this.props.current.data.ownerId === this.props.viewer.id,
editing: false,
isFollowing: !!this.props.viewer.subscriptions.filter((subscription) => {
return subscription.target_slate_id === this.props.current.id;
}).length,
};
// NOTE(jim):
// The purpose of this is to update the Scene appropriately when
// it changes but isn't mounted.
async componentDidUpdate(prevProps) {
if (prevProps.current.id !== this.props.current.id) {
if (
prevProps.current.id !== this.props.current.id ||
this.props.viewer.subscriptions !== prevProps.viewer.subscriptions
) {
await this.setState({
loading: false,
saving: "IDLE",
isOwner: this.props.current.data.ownerId === this.props.viewer.id,
isFollowing: !!this.props.viewer.subscriptions.filter((subscription) => {
return subscription.target_slate_id === this.props.current.id;
}).length,
});
}
}
_handleFollow = () => {
this.setState({ isFollowing: !this.state.isFollowing });
Actions.createSubscription({
slateId: this.props.current.id,
});
@ -129,8 +133,6 @@ export default class SceneSlate extends React.Component {
};
_handleSave = async (e, objects, layouts, autoSave = false, preview) => {
this.setState({ loading: true, saving: "SAVING" });
let layoutOnly = layouts && !objects;
let data = {};
@ -141,6 +143,15 @@ export default class SceneSlate extends React.Component {
data.layouts = layouts;
}
if (preview) {
let slates = this.props.viewer.slates;
let slateId = this.props.current.id;
for (let slate of slates) {
if (slate.id === slateId) {
slate.data.preview = preview;
break;
}
}
this.props.onUpdateViewer({ slates });
data.preview = preview;
}
const response = await Actions.updateSlate({
@ -153,10 +164,6 @@ export default class SceneSlate extends React.Component {
if (!autoSave) {
Events.hasError(response);
}
this.setState({
saving: "SAVED",
});
};
_handleSelect = (index) =>
@ -196,16 +203,12 @@ export default class SceneSlate extends React.Component {
render() {
const { user, data } = this.props.current;
const { body = "", preview } = data;
console.log(body);
let objects = this.props.current.data.objects;
let layouts = this.props.current.data.layouts;
const isPublic = data.public;
const isOwner = this.props.current.data.ownerId === this.props.viewer.id;
let following = !!this.props.viewer.subscriptions.filter((subscription) => {
return subscription.target_slate_id === this.props.current.id;
}).length;
let actions = this.state.isOwner ? (
let actions = isOwner ? (
<span>
<CircleButtonGray onClick={this._handleAdd} style={{ marginRight: 16 }}>
<SVG.Plus height="16px" />
@ -218,7 +221,7 @@ export default class SceneSlate extends React.Component {
e,
user
? Strings.getURLFromPath(`/${user.username}/${this.props.current.slatename}`)
: this.state.isOwner
: isOwner
? Strings.getURLFromPath(
`/${this.props.viewer.username}/${this.props.current.slatename}`
)
@ -236,7 +239,7 @@ export default class SceneSlate extends React.Component {
) : (
<div style={{ display: `flex` }}>
<div onClick={this._handleFollow}>
{following ? (
{this.state.isFollowing ? (
<ButtonSecondary style={{ minHeight: 36 }}>Unfollow</ButtonSecondary>
) : (
<ButtonPrimary style={{ minHeight: 36 }}>Follow</ButtonPrimary>
@ -249,7 +252,7 @@ export default class SceneSlate extends React.Component {
e,
user
? Strings.getURLFromPath(`/${user.username}/${this.props.current.slatename}`)
: this.state.isOwner
: isOwner
? Strings.getURLFromPath(
`/${this.props.viewer.username}/${this.props.current.slatename}`
)
@ -295,29 +298,31 @@ export default class SceneSlate extends React.Component {
{objects && objects.length ? (
this.props.mobile ? (
<SlateLayoutMobile
isOwner={this.state.isOwner}
isOwner={isOwner}
items={objects}
fileNames={layouts && layouts.ver === "2.0" ? layouts.fileNames : false}
onSelect={this._handleSelect}
/>
) : (
<div style={{ marginTop: this.state.isOwner ? 24 : 48 }}>
<div style={{ marginTop: isOwner ? 24 : 48 }}>
<SlateLayout
link={
user
? Strings.getURLFromPath(`/${user.username}/${this.props.current.slatename}`)
: this.state.isOwner
: isOwner
? Strings.getURLFromPath(
`/${this.props.viewer.username}/${this.props.current.slatename}`
)
: ""
}
current={this.props.current}
onUpdateViewer={this.props.onUpdateViewer}
viewer={this.props.viewer}
slateId={this.props.current.id}
layout={layouts && layouts.ver === "2.0" ? layouts.layout || [] : null}
onSaveLayout={this._handleSaveLayout}
isOwner={this.state.isOwner}
onSave={this._handleSave}
isOwner={isOwner}
fileNames={layouts && layouts.ver === "2.0" ? layouts.fileNames : false}
preview={preview}
onSavePreview={(preview) => this._handleSave(null, null, null, false, preview)}
@ -328,7 +333,7 @@ export default class SceneSlate extends React.Component {
/>
</div>
)
) : this.state.isOwner ? (
) : isOwner ? (
<div>
<EmptyState>
<FileTypeGroup />