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
*/ }