import * as React from "react";
import * as Constants from "~/common/constants";
import * as Validations from "~/common/validations";
import * as Window from "~/common/window";
import * as SVG from "~/common/svg";
import * as Actions from "~/common/actions";
import * as Events from "~/common/custom-events";
import * as System from "~/components/system";
import { css } from "@emotion/react";
import { PrimaryTabGroup, SecondaryTabGroup } from "~/components/core/TabGroup";
import EmptyState from "~/components/core/EmptyState";
import ScenePage from "~/components/core/ScenePage";
import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview";
import ScenePageHeader from "~/components/core/ScenePageHeader";
const STYLES_VIDEO_BIG = css`
display: block;
background-color: ${Constants.system.moonstone};
padding: 0;
outline: 0;
margin: 48px auto 88px auto;
border-radius: 4px;
width: 100%;
box-shadow: 0px 10px 50px 20px rgba(0, 0, 0, 0.1);
@media (max-width: ${Constants.sizes.tablet}px) {
margin: 32px auto 64px auto;
}
@media (max-width: ${Constants.sizes.mobile}px) {
margin: 24px auto 48px auto;
}
`;
const STYLES_IMAGE_BOX = css`
cursor: pointer;
position: relative;
box-shadow: ${Constants.shadow.light};
margin: 10px;
:hover {
box-shadow: ${Constants.shadow.medium};
}
`;
const STYLES_TEXT_AREA = css`
position: absolute;
bottom: 0px;
left: 0px;
`;
const STYLES_TITLE = css`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: ${Constants.system.white};
font-family: ${Constants.font.medium};
margin-bottom: 4px;
width: calc(100% - 32px);
padding: 0px 16px;
box-sizing: content-box;
`;
const STYLES_SECONDARY = css`
${STYLES_TITLE}
font-size: ${Constants.typescale.lvlN1};
margin-bottom: 16px;
width: 100%;
`;
const STYLES_GRADIENT = css`
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.2) 26.56%,
rgba(0, 0, 0, 0.3) 100%
);
backdrop-filter: blur(2px);
width: 100%;
height: 72px;
position: absolute;
bottom: 0px;
left: 0px;
`;
const STYLES_ACTIVITY_GRID = css`
margin: -10px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
`;
class ActivitySquare extends React.Component {
state = {
showText: false,
};
render() {
const item = this.props.item;
const size = this.props.size;
const isImage = Validations.isPreviewableImage(item.file.type);
return (
this.setState({ showText: true }) : () => {}}
onMouseLeave={isImage ? () => this.setState({ showText: false }) : () => {}}
>
{isImage && this.state.showText ?
: null}
{this.state.showText || !isImage ? (
{isImage ? null : (
{item.file.title || item.file.name}
)}
{isImage ? (
) : null}
{item.slate.data.name || item.slate.slatename}
) : null}
);
}
}
const ActivityRectangle = ({ item, size }) => {
let file;
for (let obj of item.slate?.data?.objects || []) {
if (Validations.isPreviewableImage(obj.type) || obj.coverImage) {
file = obj;
}
}
let numObjects = item.slate?.data?.objects?.length || 0;
return (
{file ? (
) : null}
{item.slate.data.name || item.slate.slatename}
{numObjects} File{numObjects == 1 ? "" : "s"}
);
};
export default class SceneActivity extends React.Component {
counter = 0;
state = {
imageSize: 200,
tab: 0,
loading: null,
};
async componentDidMount() {
this.calculateWidth();
this.debounceInstance = Window.debounce(this.calculateWidth, 200);
this.scrollDebounceInstance = Window.debounce(this._handleScroll, 200);
window.addEventListener("resize", this.debounceInstance);
window.addEventListener("scroll", this.scrollDebounceInstance);
//slates with no previewable images in them?
//filter to remove ones you no longer follow
this.fetchActivityItems(true);
}
componentDidUpdate(prevProps) {
if (prevProps.tab !== this.props.tab) {
this.fetchActivityItems(true);
}
}
componentWillUnmount() {
window.removeEventListener("resize", this.debounceInstance);
window.removeEventListener("scroll", this.scrollDebounceInstance);
}
_handleScroll = (e) => {
if (this.state.loading) {
return;
}
const windowHeight =
"innerHeight" in window ? window.innerHeight : document.documentElement.offsetHeight;
const body = document.body;
const html = document.documentElement;
const docHeight = Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight
);
const windowBottom = windowHeight + window.pageYOffset;
if (windowBottom >= docHeight - 600) {
this.fetchActivityItems();
}
};
//maybe it shouldn't save the extra loaded ones b/c that could take a lot of space. just the first 100
//what if a slate is deleted and you click on an activity item for it? what does it show?
fetchActivityItems = async (update = false) => {
this.setState({ loading: "loading" });
let activity =
this.props.tab === 0 ? this.props.viewer.activity || [] : this.props.viewer.explore || [];
let response;
if (activity.length) {
if (update) {
//NOTE(martina): fetch any new updates since last fetched
response = await Actions.getActivity({
explore: this.props.tab === 1,
latestTimestamp: activity[0].created_at,
});
if (Events.hasError(response)) {
this.setState({ loading: "failed" });
return;
}
if (!response.activity.length) {
this.setState({ loading: false });
return;
}
activity.unshift(...response.activity);
this.count = 0;
activity = this.formatActivity(activity);
} else {
//NOTE(martina): pagination -- fetch the next 100 events upon scrolling to the bottom
response = await Actions.getActivity({
explore: this.props.tab === 1,
earliestTimestamp: activity[activity.length - 1].created_at,
});
if (Events.hasError(response)) {
this.setState({ loading: "failed" });
return;
}
if (!response.activity.length) {
this.setState({ loading: false });
return;
}
let newItems = this.formatActivity(response.activity) || [];
activity.push(...newItems);
}
} else {
//NOTE(martina): fetch the most recent 100 events
response = await Actions.getActivity({ explore: this.props.tab === 1 });
if (Events.hasError(response)) {
this.setState({ loading: "failed" });
return;
}
if (!response.activity.length) {
this.setState({ loading: false });
return;
}
this.count = 0;
activity = this.formatActivity(response.activity);
}
if (this.props.tab === 0) {
this.props.onUpdateViewer({ activity: activity });
} else {
this.props.onUpdateViewer({ explore: activity });
}
this.setState({ loading: false });
};
formatActivity = (userActivity) => {
//NOTE(martina): rearrange order to always get an even row of 6 squares
let activity = userActivity || [];
for (let i = 0; i < activity.length; i++) {
let item = activity[i];
if (item.data.type === "SUBSCRIBED_CREATE_SLATE") {
this.counter += 2;
} else if (item.data.type === "SUBSCRIBED_ADD_TO_SLATE") {
this.counter += 1;
}
if (this.counter === 6) {
this.counter = 0;
} else if (this.counter > 6) {
let j = i - 1;
while (activity[j].data.type !== "SUBSCRIBED_ADD_TO_SLATE") {
j -= 1;
}
let temp = activity[j];
activity[j] = activity[i];
activity[i] = temp;
this.counter = 0;
i -= 1;
}
}
return activity;
};
_handleCreateSlate = () => {
this.props.onAction({
type: "NAVIGATE",
value: "NAV_SLATES",
data: null,
});
};
calculateWidth = () => {
let windowWidth = window.innerWidth;
let imageSize;
if (windowWidth < Constants.sizes.mobile) {
imageSize = (windowWidth - 2 * 24 - 20) / 2;
} else {
imageSize = (windowWidth - 2 * 56 - 5 * 20) / 6;
}
this.setState({ imageSize });
};
render() {
let activity =
this.props.tab === 0 ? this.props.viewer.activity || [] : this.props.viewer.explore || [];
return (
}
actions={
}
/>
{activity.length ? (
{activity.map((item) => {
if (item.data.type === "SUBSCRIBED_CREATE_SLATE") {
return (
this.props.onAction({
type: "NAVIGATE",
value: "NAV_SLATE",
data: { decorator: "SLATE", ...item.data.context.slate },
})
}
>
);
} else if (item.data.type === "SUBSCRIBED_ADD_TO_SLATE") {
return (
{
this.props.onAction({
type: "NAVIGATE",
value: "NAV_SLATE",
data: {
decorator: "SLATE",
...item.data.context.slate,
pageState: {
cid: item.data.context.file.cid,
},
},
});
}}
>
);
} else {
return null;
}
})}
) : (
Start following people and slates to see their activity here
)}
);
}
}
{
/*
When you're ready, create a slate!
Create a slate
*/
}