From 7fb84a265554f8b68c5b58a5c18b27fd0ef2d7b5 Mon Sep 17 00:00:00 2001 From: Aminejvm Date: Thu, 27 May 2021 09:20:34 +0100 Subject: [PATCH 01/27] added: new colors tokens --- common/actions.js | 7 + common/activity-utilities.js | 175 ++++++++++++ common/hooks.js | 192 ++++++++++++- common/styles.js | 63 ++++- common/styles/global.js | 2 +- common/svg.js | 61 +++++ common/utilities.js | 39 +++ common/validations.js | 11 + .../components/ActivityCollectionGroup.js | 100 +++++++ .../components/ActivityFileGroup.js | 111 ++++++++ .../components/ActivityProfileGroup.js | 104 +++++++ .../ActivityGroup/components/ProfileInfo.js | 101 +++++++ .../components/ViewMoreContent.js | 68 +++++ .../core/ActivityGroup/components/index.js | 5 + components/core/ActivityGroup/index.js | 46 ++++ components/core/ApplicationHeader.js | 5 +- components/core/ApplicationLayout.js | 4 +- components/core/Auth/TwitterSignup.js | 2 +- .../core/Auth/components/SignUpPopover.js | 2 +- .../FilesCollectionPreview.js | 259 ++++++++++++++++++ .../ImageCollectionPreview.js | 235 ++++++++++++++++ .../components/FollowButton.js | 85 ++++++ .../components/index.js | 1 + .../core/CollectionPreviewBlock/hooks.js | 50 ++++ .../core/CollectionPreviewBlock/index.js | 17 ++ components/core/DataMeter.js | 4 +- components/core/DataMeterDetailed.js | 7 +- components/core/DataView.js | 92 +++---- components/core/Field.js | 4 +- components/core/FontFrame/hooks.js | 9 +- components/core/FontFrame/index.js | 2 +- components/core/Link.js | 2 + components/core/Link/LinkCard.js | 2 +- .../core/ObjectPreview/3dObjectPreview.js | 38 +++ .../core/ObjectPreview/AudioObjectPreview.js | 41 +++ .../core/ObjectPreview/CodeObjectPreview.js | 40 +++ .../ObjectPreview/DefaultObjectPreview.js | 36 +++ .../core/ObjectPreview/EpubObjectPreview.js | 38 +++ .../core/ObjectPreview/FontObjectPreview.js | 40 +++ .../core/ObjectPreview/ImageObjectPreview.js | 91 ++++++ .../ObjectPreview/KeynoteObjectPreview.js | 38 +++ .../ObjectPreview/ObjectPreviewPremitive.js | 171 ++++++++++++ .../core/ObjectPreview/PdfObjectPreview.js | 39 +++ .../core/ObjectPreview/TextObjectPreview.js | 76 +++++ .../core/ObjectPreview/VideoObjectPreview.js | 40 +++ .../ObjectPreview/components/LikeButton.jsx | 72 +++++ .../ObjectPreview/components/SaveButton.jsx | 81 ++++++ .../core/ObjectPreview/components/index.js | 2 + components/core/ObjectPreview/index.js | 67 +++++ .../core/ObjectPreview/placeholders/3D.js | 175 ++++++++++++ .../core/ObjectPreview/placeholders/Audio.js | 57 ++++ .../core/ObjectPreview/placeholders/Code.js | 64 +++++ .../core/ObjectPreview/placeholders/EPUB.js | 106 +++++++ .../core/ObjectPreview/placeholders/File.js | 68 +++++ .../ObjectPreview/placeholders/Keynote.js | 107 ++++++++ .../core/ObjectPreview/placeholders/PDF.js | 89 ++++++ .../core/ObjectPreview/placeholders/Text.js | 90 ++++++ .../core/ObjectPreview/placeholders/Video.js | 56 ++++ .../core/ObjectPreview/placeholders/index.js | 110 ++++++++ components/core/Profile.js | 36 ++- components/core/ProfilePreviewBlock.js | 231 ++++++++++++++++ components/core/SlateLinkObject.js | 2 +- components/core/SlateMediaObjectPreview.js | 2 +- components/core/SlatePreviewBlock.js | 2 +- components/core/TabGroup.js | 2 +- components/sidebars/SidebarFAQ.js | 7 - components/system/components/AspectRatio.js | 36 +++ components/system/components/Buttons.js | 6 +- components/system/components/Typography.js | 194 ++++++++++--- components/system/index.js | 3 + node_common/constants.js | 2 + node_common/data/index.js | 2 + node_common/data/methods/create-file.js | 1 + node_common/data/methods/get-activity.js | 231 +++++++++------- node_common/data/methods/get-explore.js | 173 +++++++----- .../data/methods/get-likes-by-user-id.js | 2 +- .../data/methods/increment-file-savecount.js | 23 ++ node_common/managers/viewer.js | 16 +- pages/_/index.js | 2 +- pages/api/data/like.js | 11 +- pages/api/data/save-copy.js | 13 +- ...Activity.js => DeprecatedSceneActivity.js} | 155 ++--------- scenes/SceneActivity/hooks.js | 112 ++++++++ scenes/SceneActivity/index.js | 86 ++++++ scenes/SceneFilesFolder.js | 1 + scenes/SceneSlates.js | 119 ++++---- server.js | 2 +- 87 files changed, 4659 insertions(+), 512 deletions(-) create mode 100644 common/activity-utilities.js create mode 100644 components/core/ActivityGroup/components/ActivityCollectionGroup.js create mode 100644 components/core/ActivityGroup/components/ActivityFileGroup.js create mode 100644 components/core/ActivityGroup/components/ActivityProfileGroup.js create mode 100644 components/core/ActivityGroup/components/ProfileInfo.js create mode 100644 components/core/ActivityGroup/components/ViewMoreContent.js create mode 100644 components/core/ActivityGroup/components/index.js create mode 100644 components/core/ActivityGroup/index.js create mode 100644 components/core/CollectionPreviewBlock/FilesCollectionPreview.js create mode 100644 components/core/CollectionPreviewBlock/ImageCollectionPreview.js create mode 100644 components/core/CollectionPreviewBlock/components/FollowButton.js create mode 100644 components/core/CollectionPreviewBlock/components/index.js create mode 100644 components/core/CollectionPreviewBlock/hooks.js create mode 100644 components/core/CollectionPreviewBlock/index.js create mode 100644 components/core/ObjectPreview/3dObjectPreview.js create mode 100644 components/core/ObjectPreview/AudioObjectPreview.js create mode 100644 components/core/ObjectPreview/CodeObjectPreview.js create mode 100644 components/core/ObjectPreview/DefaultObjectPreview.js create mode 100644 components/core/ObjectPreview/EpubObjectPreview.js create mode 100644 components/core/ObjectPreview/FontObjectPreview.js create mode 100644 components/core/ObjectPreview/ImageObjectPreview.js create mode 100644 components/core/ObjectPreview/KeynoteObjectPreview.js create mode 100644 components/core/ObjectPreview/ObjectPreviewPremitive.js create mode 100644 components/core/ObjectPreview/PdfObjectPreview.js create mode 100644 components/core/ObjectPreview/TextObjectPreview.js create mode 100644 components/core/ObjectPreview/VideoObjectPreview.js create mode 100644 components/core/ObjectPreview/components/LikeButton.jsx create mode 100644 components/core/ObjectPreview/components/SaveButton.jsx create mode 100644 components/core/ObjectPreview/components/index.js create mode 100644 components/core/ObjectPreview/index.js create mode 100644 components/core/ObjectPreview/placeholders/3D.js create mode 100644 components/core/ObjectPreview/placeholders/Audio.js create mode 100644 components/core/ObjectPreview/placeholders/Code.js create mode 100644 components/core/ObjectPreview/placeholders/EPUB.js create mode 100644 components/core/ObjectPreview/placeholders/File.js create mode 100644 components/core/ObjectPreview/placeholders/Keynote.js create mode 100644 components/core/ObjectPreview/placeholders/PDF.js create mode 100644 components/core/ObjectPreview/placeholders/Text.js create mode 100644 components/core/ObjectPreview/placeholders/Video.js create mode 100644 components/core/ObjectPreview/placeholders/index.js create mode 100644 components/core/ProfilePreviewBlock.js create mode 100644 components/system/components/AspectRatio.js create mode 100644 node_common/data/methods/increment-file-savecount.js rename scenes/{SceneActivity.js => DeprecatedSceneActivity.js} (71%) create mode 100644 scenes/SceneActivity/hooks.js create mode 100644 scenes/SceneActivity/index.js diff --git a/common/actions.js b/common/actions.js index cb4d3625..77a68314 100644 --- a/common/actions.js +++ b/common/actions.js @@ -366,6 +366,13 @@ export const removeFileFromSlate = async (data) => { }); }; +export const like = async (data) => { + return await returnJSON(`/api/data/like`, { + ...DEFAULT_OPTIONS, + body: JSON.stringify({ data }), + }); +}; + export const generateAPIKey = async () => { await Websockets.checkWebsocket(); return await returnJSON(`/api/keys/generate`, { diff --git a/common/activity-utilities.js b/common/activity-utilities.js new file mode 100644 index 00000000..b0a963c5 --- /dev/null +++ b/common/activity-utilities.js @@ -0,0 +1,175 @@ +/* eslint-disable no-prototype-builtins */ +import shuffle from "lodash/shuffle"; +import * as Actions from "~/common/actions"; + +// NOTE(amine): fetch explore objects +export const fetchExploreItems = async ({ currentItems, update }) => { + const requestObject = {}; + if (currentItems.length) { + if (update) { + requestObject.latestTimestamp = currentItems[0].createdAt; + } else { + requestObject.earliestTimestamp = currentItems[currentItems.length - 1].createdAt; + } + } + const response = await Actions.getExplore(requestObject); + return response; +}; + +// NOTE(amine): fetch explore objects +export const fetchActivityItems = async ({ currentItems, viewer, update }) => { + const requestObject = {}; + + if (currentItems.length) { + if (update) { + requestObject.latestTimestamp = currentItems[0].createdAt; + } else { + requestObject.earliestTimestamp = currentItems[currentItems.length - 1].createdAt; + } + } + + requestObject.following = viewer.following.map((item) => item.id); + requestObject.subscriptions = viewer.subscriptions.map((item) => item.id); + + const response = await Actions.getActivity(requestObject); + return response; +}; +//NOTE(martina): our grouping schema is as follows: we group first by multiple people doing the same action to the same target, then by one person doing the same action to different targets +// We remove repeat targets so that the user is not shown the same file/slate twice + +let ids = {}; + +//NOTE(martina): this is used when grouping by multiple different users doing the same action on the same target. The value here is the target that should be the same (in addition to the type of action) +// e.g. "Martina, Tara, and Haris liked this file" +let fieldGroupings = { + SUBSCRIBE_SLATE: "slate", + SUBSCRIBE_USER: "user", + LIKE_FILE: "file", + SAVE_COPY: "file", +}; + +//NOTE(martina): this is used when grouping by one user doing the same action to different targets +// e.g. "Martina liked 3 files" + +//NOTE(martina): primary is the primary "target" of the action (the thing that can differ). If there is a secondary, it is something that should stay consistent when grouping items with different values for the primary +// For example, for CREATE_SLATE_OBJECT, primary = file and secondary = slate. +// In other words, "Martina added 3 files to this one slate" is a valid grouping (slate is consistent), whereas "Martina added 3 files to 3 different slates" is not a valid grouping (slate is not consistent) +const ownerIdGroupings = { + CREATE_SLATE_OBJECT: { primary: "file", secondary: "slate" }, + CREATE_SLATE: { primary: "slate" }, + CREATE_FILE: { primary: "file", secondary: "slate" }, + SUBSCRIBE_SLATE: { primary: "slate" }, + SUBSCRIBE_USER: { primary: "user" }, + FILE_VISIBLE: { primary: "file" }, + SLATE_VISIBLE: { primary: "slate" }, + LIKE_FILE: { primary: "file" }, + SAVE_COPY: { primary: "file" }, +}; + +//NOTE(martina): pass the new activity items through this to group and order them +export const processActivity = (activity) => { + let activityByType = {}; + for (let item of activity) { + if (item.type === "DOWNLOAD_FILE") continue; + const { primary } = ownerIdGroupings[item.type]; + const { id } = item[primary]; + if (ids[id]) { + continue; //NOTE(martina): removing repeats from previous activity + } + + if (activityByType[item.type]) { + activityByType[item.type].push(item); + } else { + activityByType[item.type] = [item]; + } + } + + //NOTE(martina): first grouping by multiple people doing the same action on the same target + let finalActivity = []; + for (let [type, events] of Object.entries(activityByType)) { + if (!fieldGroupings.hasOwnProperty(type)) continue; + let field = fieldGroupings[type]; + const { grouped, ungrouped } = groupByField(events, field); + if (grouped?.length) { + finalActivity.push(...grouped); + } + if (ungrouped?.length) { + activityByType[type] = ungrouped; + } else { + delete activityByType[type]; + } + } + + //NOTE(martina): removing repeats within the group + for (let item of finalActivity) { + const { primary } = ownerIdGroupings[item.type]; + let { id } = item[primary]; + ids[id] = true; + } + for (let [key, arr] of Object.entries(activityByType)) { + let filteredArr = []; + for (let item of arr) { + const { primary } = ownerIdGroupings[item.type]; + let { id } = item[primary]; + if (ids[id]) { + continue; + } else { + filteredArr.push(item); + ids[id] = true; + } + } + activityByType[key] = filteredArr; + } + + //NOTE(martina): second grouping of same owner doing same action on different targets + for (let [type, events] of Object.entries(activityByType)) { + if (!ownerIdGroupings.hasOwnProperty(type)) continue; + let field = ownerIdGroupings[type]; + const groupedActivity = groupByOwner(events, field.primary, field.secondary); + finalActivity.push(...groupedActivity); + } + + return shuffle(finalActivity); +}; + +const groupByField = (activity, key) => { + let ungrouped = {}; + let grouped = {}; + for (let item of activity) { + const { id } = item[key]; + let match = ungrouped[id]; + if (match) { + grouped[id] = match; + grouped[id].owner = [match.owner, item.owner]; + delete ungrouped[id]; + } else { + match = grouped[id]; + if (match) { + grouped[id].owner.push(item.owner); + } else { + ungrouped[id] = item; + } + } + } + return { grouped: Object.values(grouped), ungrouped: Object.values(ungrouped) }; +}; + +const groupByOwner = (activity, collateCol, sharedCol = "") => { + let grouped = {}; + for (let item of activity) { + let aggregateKey = `${item.owner.id}-${sharedCol ? item[sharedCol]?.id : ""}`; + let match = grouped[aggregateKey]; + if (match) { + if (Array.isArray(match[collateCol])) { + match[collateCol].push(item[collateCol]); + } else { + match[collateCol] = [match[collateCol], item[collateCol]]; + } + } else { + grouped[aggregateKey] = item; + } + } + return Object.values(grouped); +}; + +//TODO(martina): add ranking by score, removing repeats from a persistent pool of ids before the first grouping as well (for that one, don't add to the ids list yet) diff --git a/common/hooks.js b/common/hooks.js index 0e713860..3589756f 100644 --- a/common/hooks.js +++ b/common/hooks.js @@ -1,5 +1,7 @@ import * as React from "react"; import * as Logging from "~/common/logging"; +import * as Actions from "~/common/actions"; +import * as Events from "~/common/custom-events"; export const useMounted = () => { const isMounted = React.useRef(true); @@ -39,10 +41,12 @@ export const useForm = ({ }); const _hasError = (obj) => Object.keys(obj).some((name) => obj[name]); - const _mergeEventHandlers = (events = []) => (e) => - events.forEach((event) => { - if (event) event(e); - }); + const _mergeEventHandlers = + (events = []) => + (e) => + events.forEach((event) => { + if (event) event(e); + }); /** ---------- NOTE(amine): Input Handlers ---------- */ const handleFieldChange = (e) => @@ -162,10 +166,12 @@ export const useField = ({ touched: undefined, }); - const _mergeEventHandlers = (events = []) => (e) => - events.forEach((event) => { - if (event) event(e); - }); + const _mergeEventHandlers = + (events = []) => + (e) => + events.forEach((event) => { + if (event) event(e); + }); /** ---------- NOTE(amine): Input Handlers ---------- */ const handleFieldChange = (e) => @@ -176,14 +182,14 @@ export const useField = ({ touched: false, })); - const handleOnBlur = (e) => { + const handleOnBlur = () => { // NOTE(amine): validate the inputs onBlur and touch the current input let error = {}; if (validateOnBlur && validate) error = validate(state.value); setState((prev) => ({ ...prev, touched: validateOnBlur, error })); }; - const handleFormOnSubmit = (e) => { + const handleFormOnSubmit = () => { //NOTE(amine): touch all inputs setState((prev) => ({ ...prev, touched: true })); @@ -219,3 +225,169 @@ export const useField = ({ return { getFieldProps, value: state.value, isSubmitting: state.isSubmitting }; }; + +export const useIntersection = ({ onIntersect, ref }, dependencies = []) => { + // NOTE(amine): fix for stale closure caused by hooks + const onIntersectRef = React.useRef(); + onIntersectRef.current = onIntersect; + + React.useLayoutEffect(() => { + if (!ref.current) return; + const lazyObserver = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + if (onIntersectRef.current) onIntersectRef.current(lazyObserver, ref); + } + }); + }); + // start to observe element + lazyObserver.observe(ref.current); + return () => lazyObserver.unobserve(ref.current); + }, dependencies); +}; + +// NOTE(amine): the intersection will be called one time +export const useInView = ({ ref }) => { + const [isInView, setInView] = React.useState(false); + useIntersection({ + ref, + onIntersect: (lazyObserver, ref) => { + setInView(true); + lazyObserver.unobserve(ref.current); + }, + }); + return { isInView }; +}; + +// NOTE(amine): manage like state +export const useLikeHandler = ({ file, viewer }) => { + const likedFile = React.useMemo(() => viewer?.likes?.find((item) => item.id === file.id), []); + const [state, setState] = React.useState({ + isLiked: !!likedFile, + // NOTE(amine): viewer will have the hydrated state + likeCount: likedFile?.likeCount ?? file.likeCount, + }); + + const handleLikeState = () => { + setState((prev) => { + if (prev.isLiked) { + return { + isLiked: false, + likeCount: prev.likeCount - 1, + }; + } + return { + isLiked: true, + likeCount: prev.likeCount + 1, + }; + }); + }; + const like = async () => { + if (!viewer) { + Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} }); + return; + } + // NOTE(amine): optimistic update + handleLikeState(); + const response = await Actions.like({ id: file.id }); + if (Events.hasError(response)) { + // NOTE(amine): revert back to old state if there is an error + handleLikeState(); + return; + } + }; + + return { like, ...state }; +}; + +// NOTE(amine): manage file saving state +export const useSaveHandler = ({ file, viewer }) => { + const savedFile = React.useMemo(() => viewer?.libraryCids[file.cid], [viewer]); + const [state, setState] = React.useState({ + isSaved: !!savedFile, + // NOTE(amine): viewer will have the hydrated state + saveCount: file.saveCount, + }); + + const handleSaveState = () => { + setState((prev) => { + if (prev.isSaved) { + return { + isSaved: false, + saveCount: prev.saveCount - 1, + }; + } + return { + isSaved: true, + saveCount: prev.saveCount + 1, + }; + }); + }; + + const save = async () => { + if (!viewer) { + Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} }); + return; + } + // NOTE(amine): optimistic update + handleSaveState(); + const response = + state.isSaved && savedFile + ? await Actions.deleteFiles({ ids: [savedFile.id] }) + : await Actions.saveCopy({ files: [file] }); + if (Events.hasError(response)) { + // NOTE(amine): revert back to old state if there is an error + handleSaveState(); + return; + } + }; + + return { save, ...state }; +}; + +export const useFollowProfileHandler = ({ user, viewer, onAction }) => { + const [isFollowing, setFollowing] = React.useState( + !viewer + ? false + : !!viewer?.following.some((entry) => { + return entry.id === user.id; + }) + ); + + const handleFollow = async (userId) => { + if (!viewer) { + Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} }); + return; + } + + setFollowing((prev) => !prev); + const response = await Actions.createSubscription({ + userId, + }); + + if (Events.hasError(response)) { + setFollowing((prev) => !prev); + return; + } + + onAction({ + type: "UPDATE_VIEWER", + viewer: { + following: isFollowing + ? viewer.following.filter((user) => user.id !== userId) + : viewer.following.concat([ + { + id: user.id, + data: user.data, + fileCount: user.fileCount, + followerCount: user.followerCount + 1, + slateCount: user.slateCount, + username: user.username, + }, + ]), + }, + }); + }; + + return { handleFollow, isFollowing }; +}; diff --git a/common/styles.js b/common/styles.js index 1fa92951..c380bcca 100644 --- a/common/styles.js +++ b/common/styles.js @@ -47,7 +47,6 @@ export const H2 = css` export const H3 = css` font-family: ${Constants.font.text}; font-size: 1.25rem; - font-weight: medium; line-height: 1.5; letter-spacing: -0.017px; @@ -57,7 +56,6 @@ export const H3 = css` export const H4 = css` font-family: ${Constants.font.text}; font-size: 1rem; - font-weight: medium; line-height: 1.5; letter-spacing: -0.011px; @@ -67,7 +65,6 @@ export const H4 = css` export const H5 = css` font-family: ${Constants.font.text}; font-size: 0.875rem; - font-weight: medium; line-height: 1.5; letter-spacing: -0.006px; @@ -81,6 +78,14 @@ export const P1 = css` line-height: 1.5; letter-spacing: -0.011px; + @media (max-width: ${Constants.sizes.mobile}px) { + font-family: ${Constants.font.text}; + font-size: 0.875rem; + font-weight: regular; + line-height: 1.5; + letter-spacing: -0.006px; + } + ${TEXT} `; @@ -91,6 +96,14 @@ export const P2 = css` line-height: 1.5; letter-spacing: -0.006px; + @media (max-width: ${Constants.sizes.mobile}px) { + font-family: ${Constants.font.text}; + font-size: 0.75rem; + font-weight: normal; + line-height: 1.3; + letter-spacing: 0px; + } + ${TEXT} `; @@ -220,3 +233,47 @@ export const IMAGE_FIT = css` height: 100%; object-fit: contain; `; + +/* COMMON GRIDS */ +export const OBJECTS_PREVIEW_GRID = css` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(248px, 1fr)); + grid-gap: 24px 16px; + + @media (max-width: ${Constants.sizes.mobile}px) { + grid-gap: 20px 8px; + grid-template-columns: repeat(auto-fill, minmax(166px, 1fr)); + } +`; + +export const BUTTON_RESET = css` + padding: 0; + margin: 0; + background-color: unset; + border: none; + ${HOVERABLE} +`; + +export const COLLECTIONS_PREVIEW_GRID = css` + display: grid; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(432px, 1fr)); + grid-gap: 24px 16px; + + @media (max-width: ${Constants.sizes.desktop}px) { + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-gap: 20px 8px; + } +`; + +export const PROFILE_PREVIEW_GRID = css` + display: grid; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(432px, 1fr)); + grid-gap: 24px 16px; + + @media (max-width: ${Constants.sizes.mobile}px) { + grid-gap: 20px 8px; + grid-template-columns: repeat(auto-fill, minmax(344px, 1fr)); + } +`; diff --git a/common/styles/global.js b/common/styles/global.js index 62e32368..5f53a7b1 100644 --- a/common/styles/global.js +++ b/common/styles/global.js @@ -75,7 +75,7 @@ export const injectGlobalStyles = () => css` } html, body { - background: ${Constants.semantic.bgLight}; + background: ${Constants.system.white}; color: ${Constants.system.black}; font-size: 16px; font-family: ${Constants.font.text}; diff --git a/common/svg.js b/common/svg.js index 064d903e..9123a768 100644 --- a/common/svg.js +++ b/common/svg.js @@ -1890,6 +1890,18 @@ export const MehCircle = (props) => ( ); +export const Heart = (props) => ( + + + +); + export const SmileCircle = (props) => ( ( /> ); + +export const FolderPlus = (props) => ( + + + +); + +export const PlayButton = (props) => ( + + + + + + + + + + + + + + +); + +export const RSS = (props) => ( + + + +); diff --git a/common/utilities.js b/common/utilities.js index abb6e68d..0882f0c0 100644 --- a/common/utilities.js +++ b/common/utilities.js @@ -83,3 +83,42 @@ export const coerceToArray = (input) => { return [input]; } }; + +export const getFileExtension = (filename) => filename?.split(".").pop(); + +export const getTimeDifferenceFromNow = (date) => { + const pastDate = new Date(date); + const now = new Date(); + + const differenceInSeconds = Math.floor((now - pastDate) / 1000); + if (differenceInSeconds < 60) { + return differenceInSeconds + "s"; + } + + const differenceInMinutes = Math.floor(differenceInSeconds / 60); + if (differenceInMinutes < 60) { + return differenceInMinutes + "m"; + } + + const differenceInHours = Math.floor(differenceInMinutes / 60); + if (differenceInHours < 24) { + return differenceInHours + "h"; + } + + const differenceInDays = Math.floor(differenceInHours / 24); + if (differenceInDays < 24) { + return differenceInDays + "d"; + } + + const currentYear = now.getFullYear(); + + const day = pastDate.getDay(); + const month = pastDate.toLocaleString("default", { month: "long" }); + const year = pastDate.getFullYear(); + + if (year === currentYear) { + return `${day} ${month}`; + } + + return `${day} ${month} ${year}`; +}; diff --git a/common/validations.js b/common/validations.js index 8efe6730..0cbc7faf 100644 --- a/common/validations.js +++ b/common/validations.js @@ -245,6 +245,17 @@ export const isUnityType = (type = "") => { return type === "application/unity"; }; +export const is3dFile = (filename = "") => { + return endsWithAny( + [".stl", ".obj", ".fbx", ".blend", ".c4d", ".glb", ".dae", ".3ds", ".wrl"], + filename.toLowerCase() + ); +}; + +export const isCodeFile = (filename = "") => { + return endsWithAny([".js"], filename.toLowerCase()); +}; + export const isFontFile = (fileName = "") => { return endsWithAny([".ttf", ".otf", ".woff", ".woff2"], fileName.toLowerCase()); }; diff --git a/components/core/ActivityGroup/components/ActivityCollectionGroup.js b/components/core/ActivityGroup/components/ActivityCollectionGroup.js new file mode 100644 index 00000000..39d110f8 --- /dev/null +++ b/components/core/ActivityGroup/components/ActivityCollectionGroup.js @@ -0,0 +1,100 @@ +import * as React from "react"; +import * as Styles from "~/common/styles"; +import * as Strings from "~/common/strings"; +import * as Utilities from "~/common/utilities"; + +import { css } from "@emotion/react"; +import { Link } from "~/components/core/Link"; +import { motion } from "framer-motion"; +import { ViewMoreContent, ProfileInfo } from "~/components/core/ActivityGroup/components"; + +import CollectionPreviewBlock from "~/components/core/CollectionPreviewBlock"; + +const STYLES_GROUP_GRID = (theme) => css` + display: grid; + grid-template-columns: 260px 1fr; + grid-row-gap: 32px; + border-bottom: 1px solid ${theme.semantic.bgLight}; + padding-bottom: 24px; + @media (max-width: ${theme.sizes.mobile}px) { + grid-row-gap: 24px; + grid-template-columns: 1fr; + } +`; + +const STYLES_VIEWMORE_CONTAINER = (theme) => css` + @media (max-width: ${theme.sizes.mobile}px) { + display: flex; + justify-content: center; + } +`; + +export default function ActivityCollectionGroup({ onAction, viewer, group, ...props }) { + const { owner, slate, type, createdAt } = group; + const { elements, restElements } = React.useMemo(() => { + if (!Array.isArray(slate)) { + return { elements: [slate] }; + } + return { elements: slate.slice(0, 2), restElements: slate.slice(2) }; + }, [slate]); + + const [showMore, setShowMore] = React.useState(false); + const viewMoreFiles = () => setShowMore(true); + + const timeSinceUploaded = Utilities.getTimeDifferenceFromNow(createdAt); + // const timeSinceUploaded = Utilities.getTimeDifferenceFromNow(elements[0].createdAt); + const nbrOfFilesUploaded = elements.length + (restElements?.length || 0); + const action = React.useMemo(() => { + if (type === "SUBSCRIBE_SLATE") { + return "started following"; + } + return `created ${nbrOfFilesUploaded} ${Strings.pluralize("collection", nbrOfFilesUploaded)}`; + }, []); + + return ( +
+ +
+
+ {elements.map((collection) => ( + + + + ))} + {showMore && + restElements.map((collection, i) => + // NOTE(amine): animate only the first 8 elements + i < 8 ? ( + + + + + + ) : ( + + + + ) + )} +
+
+ {!showMore && restElements?.length ? ( + + View {restElements.length} more {Strings.pluralize("collection", restElements.length)} + + ) : null} +
+
+
+ ); +} diff --git a/components/core/ActivityGroup/components/ActivityFileGroup.js b/components/core/ActivityGroup/components/ActivityFileGroup.js new file mode 100644 index 00000000..ca5e5c29 --- /dev/null +++ b/components/core/ActivityGroup/components/ActivityFileGroup.js @@ -0,0 +1,111 @@ +import * as React from "react"; +import * as Strings from "~/common/strings"; +import * as Utilities from "~/common/utilities"; + +import { css } from "@emotion/react"; +import { motion } from "framer-motion"; +import { ViewMoreContent, ProfileInfo } from "~/components/core/ActivityGroup/components"; + +import ObjectPreview from "~/components/core/ObjectPreview"; + +const STYLES_OBJECT_GRID = (theme) => css` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(248px, 1fr)); + grid-gap: 20px 12px; + + @media (max-width: ${theme.sizes.mobile}px) { + grid-row-gap: 24px; + grid-template-columns: repeat(auto-fill, minmax(169px, 1fr)); + } +`; + +const STYLES_GROUP_GRID = (theme) => css` + display: grid; + grid-template-columns: 260px 1fr; + grid-row-gap: 32px; + border-bottom: 1px solid ${theme.semantic.bgLight}; + padding-bottom: 24px; + + @media (max-width: ${theme.sizes.mobile}px) { + grid-template-columns: 1fr; + } +`; + +const STYLES_VIEWMORE_CONTAINER = (theme) => css` + @media (max-width: ${theme.sizes.mobile}px) { + display: flex; + justify-content: center; + } +`; + +export default function ActivityFileGroup({ viewer, group, onAction }) { + const { file, owner, slate, type, createdAt } = group; + + const { elements, restElements } = React.useMemo(() => { + if (!Array.isArray(file)) { + return { elements: [file] }; + } + return { elements: file.slice(0, 4), restElements: file.slice(4) }; + }, [file]); + + const [showMore, setShowMore] = React.useState(false); + const viewMoreFiles = () => setShowMore(true); + + const timeSinceAction = Utilities.getTimeDifferenceFromNow(createdAt); + const nbrOfFiles = elements.length + (restElements?.length || 0); + const action = React.useMemo(() => { + if (type === "CREATE_FILE") + return `uploaded ${nbrOfFiles} ${Strings.pluralize("file", nbrOfFiles)} ${ + slate ? `to ${slate.slatename}` : "" + }`; + + if (type === "LIKE_FILE") return `liked ${nbrOfFiles} ${Strings.pluralize("file", nbrOfFiles)}`; + + if (type === "SAVE_COPY") return `saved ${nbrOfFiles} ${Strings.pluralize("file", nbrOfFiles)}`; + + return `added ${nbrOfFiles} ${Strings.pluralize("file", nbrOfFiles)} ${ + slate && `to ${slate.slatename}` + }`; + }, []); + + return ( +
+ +
+
+ {elements.map((file) => ( + + ))} + {showMore && + restElements.map((file, i) => + // NOTE(amine): animate only the first 8 elements + i < 8 ? ( + + + + ) : ( + + ) + )} +
+
+ {!showMore && restElements?.length ? ( + + View {restElements.length} more {Strings.pluralize("file", restElements.length)} + + ) : null} +
+
+
+ ); +} diff --git a/components/core/ActivityGroup/components/ActivityProfileGroup.js b/components/core/ActivityGroup/components/ActivityProfileGroup.js new file mode 100644 index 00000000..a070cd2a --- /dev/null +++ b/components/core/ActivityGroup/components/ActivityProfileGroup.js @@ -0,0 +1,104 @@ +import * as React from "react"; +import * as Styles from "~/common/styles"; +import * as Strings from "~/common/strings"; +import * as Utilities from "~/common/utilities"; + +import { css } from "@emotion/react"; +import { ProfileInfo } from "~/components/core/ActivityGroup/components"; +import { Link } from "~/components/core/Link"; +import { motion } from "framer-motion"; +import { ViewMoreContent } from "~/components/core/ActivityGroup/components"; + +import ProfilePreview from "~/components/core/ProfilePreviewBlock"; + +const STYLES_GROUP_GRID = (theme) => css` + display: grid; + grid-template-columns: 260px 1fr; + grid-row-gap: 32px; + border-bottom: 1px solid ${theme.semantic.bgLight}; + padding-bottom: 24px; + @media (max-width: ${theme.sizes.mobile}px) { + grid-row-gap: 24px; + grid-template-columns: 1fr; + } +`; + +const STYLES_VIEWMORE_CONTAINER = (theme) => css` + @media (max-width: ${theme.sizes.mobile}px) { + display: flex; + justify-content: center; + } +`; + +export default function ActivityProfileGroup({ viewer, external, group, onAction }) { + const { owner, user, createdAt } = group; + + const { elements, restElements } = React.useMemo(() => { + if (!Array.isArray(user)) { + return { elements: [user] }; + } + return { elements: user.slice(0, 3), restElements: user.slice(3) }; + }, [user]); + + const [showMore, setShowMore] = React.useState(false); + const viewMoreFiles = () => setShowMore(true); + + const timeSinceUploaded = Utilities.getTimeDifferenceFromNow(createdAt); + + return ( +
+ +
+
+ {elements.map((user) => ( + + + + ))} + {showMore && + restElements.map((user, i) => + // NOTE(amine): animate only the first 8 elements + i < 8 ? ( + + + + + + ) : ( + + + + ) + )} +
+
+ {!showMore && restElements?.length ? ( + + View {restElements.length} more {Strings.pluralize("profile", restElements.length)} + + ) : null} +
+
+
+ ); +} diff --git a/components/core/ActivityGroup/components/ProfileInfo.js b/components/core/ActivityGroup/components/ProfileInfo.js new file mode 100644 index 00000000..fb29a363 --- /dev/null +++ b/components/core/ActivityGroup/components/ProfileInfo.js @@ -0,0 +1,101 @@ +import * as React from "react"; +import * as Styles from "~/common/styles"; + +import { Link } from "~/components/core/Link"; +import { css } from "@emotion/react"; +import { P2, H4 } from "~/components/system/components/Typography"; +import { ButtonPrimary, ButtonTertiary } from "~/components/system"; +import { useFollowProfileHandler } from "~/common/hooks"; + +const STYLES_PROFILE_CONTAINER = css` + display: flex; + padding-right: 12px; + box-sizing: border-box; + & > * + * { + margin-left: 12px; + } +`; + +const STYLES_TEXT_BLACK = (theme) => + css` + color: ${theme.semantic.textBlack}; + display: inline; + `; + +const STYLES_PROFILE = css` + width: 48px; + height: 48px; + border-radius: 8px; +`; + +const STYLES_MOBILE_ALIGN = (theme) => css` + @media (max-width: ${theme.sizes.mobile}px) { + width: 100%; + & > span { + display: flex; + justify-content: space-between; + align-items: center; + } + } +`; + +export default function ProfileInfo({ owner, viewer, time, action, onAction }) { + const { isFollowing, handleFollow } = useFollowProfileHandler({ viewer, user: owner, onAction }); + const { username, data = {} } = owner; + const { photo } = data; + + const isOwner = viewer?.id === owner.id; + return ( + +
+ {`${username} +
+ +

+ {username} +

+

+  •  +

+ + {time} + +
+ + {action} + + {!isOwner && ( +
+ {isFollowing ? ( + { + e.stopPropagation(); + e.preventDefault(); + handleFollow(owner.id); + }} + > + Following + + ) : ( + { + e.stopPropagation(); + e.preventDefault(); + handleFollow(owner.id); + }} + > + Follow + + )} +
+ )} +
+
+ + ); +} diff --git a/components/core/ActivityGroup/components/ViewMoreContent.js b/components/core/ActivityGroup/components/ViewMoreContent.js new file mode 100644 index 00000000..47556f36 --- /dev/null +++ b/components/core/ActivityGroup/components/ViewMoreContent.js @@ -0,0 +1,68 @@ +import * as React from "react"; +import * as Styles from "~/common/styles"; +import * as Strings from "~/common/strings"; +import * as Validations from "~/common/validations"; + +import { css } from "@emotion/react"; +import { H5 } from "~/components/system/components/Typography"; + +import ObjectPlaceholder from "~/components/core/ObjectPreview/placeholders"; + +const STYLES_VIEW_MORE_CONTAINER = (theme) => css` + background-color: ${theme.semantic.bgLight}; + border: none; + padding: 8px; + border-radius: 8px; + margin-top: 24px; +`; + +const STYLES_SHOW_MORE_PREVIEWS = (theme) => css` + overflow: hidden; + border-radius: 4px; + height: 24px; + width: 24px; + background-color: ${theme.system.grayLight5}; + & > img { + width: 100%; + height: 100%; + } +`; + +const getImageCover = (item) => { + const coverImage = item?.data?.coverImage; + const imageUrl = Strings.getURLfromCID(coverImage ? coverImage?.cid : item.cid); + return imageUrl; +}; + +export default function ViewMoreContent({ items, children, ...props }) { + return ( + + ); +} diff --git a/components/core/ActivityGroup/components/index.js b/components/core/ActivityGroup/components/index.js new file mode 100644 index 00000000..d578f0b1 --- /dev/null +++ b/components/core/ActivityGroup/components/index.js @@ -0,0 +1,5 @@ +export { default as ViewMoreContent } from "./ViewMoreContent"; +export { default as ProfileInfo } from "./ProfileInfo"; +export { default as ActivityCollectionGroup } from "./ActivityCollectionGroup"; +export { default as ActivityFileGroup } from "./ActivityFileGroup"; +export { default as ActivityProfileGroup } from "./ActivityProfileGroup"; diff --git a/components/core/ActivityGroup/index.js b/components/core/ActivityGroup/index.js new file mode 100644 index 00000000..fb4d9357 --- /dev/null +++ b/components/core/ActivityGroup/index.js @@ -0,0 +1,46 @@ +import * as React from "react"; + +import { css } from "@emotion/react"; + +import { + ActivityFileGroup, + ActivityCollectionGroup, + ActivityProfileGroup, +} from "~/components/core/ActivityGroup/components"; + +const STYLES_GROUP_GRID = (theme) => css` + display: grid; + grid-template-columns: 260px 1fr; + grid-row-gap: 32px; + border-bottom: 1px solid ${theme.semantic.bgLight}; + padding-bottom: 24px; +`; + +export default function ActivityGroup({ onAction, viewer, external, group }) { + const { type } = group; + if ( + type === "CREATE_FILE" || + type === "CREATE_SLATE_OBJECT" || + type === "LIKE_FILE" || + type === "SAVE_COPY" + ) { + return ; + } + + if (type === "CREATE_SLATE" || type === "SUBSCRIBE_SLATE") { + return ; + } + + if (type === "SUBSCRIBE_USER") { + return ( + + ); + } + + // TODO(amine): grouping for making files/slate public + return ( +
+
{type}
+
+ ); +} diff --git a/components/core/ApplicationHeader.js b/components/core/ApplicationHeader.js index c9660ed9..cede71a4 100644 --- a/components/core/ApplicationHeader.js +++ b/components/core/ApplicationHeader.js @@ -47,9 +47,10 @@ const STYLES_NAV_LINK = css` } `; -const STYLES_APPLICATION_HEADER_CONTAINER = css` +const STYLES_APPLICATION_HEADER_CONTAINER = (theme) => css` width: 100%; - background-color: ${Constants.system.white}; + background-color: ${theme.system.white}; + box-shadow: 0 0 0 1px ${theme.semantic.bgGrayLight}; @supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) { -webkit-backdrop-filter: blur(25px); diff --git a/components/core/ApplicationLayout.js b/components/core/ApplicationLayout.js index 2672e59c..afe4e34f 100644 --- a/components/core/ApplicationLayout.js +++ b/components/core/ApplicationLayout.js @@ -40,7 +40,7 @@ const STYLES_HEADER = css` `; const STYLES_CONTENT = css` - background: ${Constants.semantic.bgLight}; + background: ${Constants.system.white}; width: 100%; min-width: 10%; min-height: 100vh; @@ -95,7 +95,7 @@ const STYLES_MODAL = css` left: 0; padding: ${MODAL_MARGIN}px; - background-color: ${Constants.system.bgBlurLight}; + background-color: ${Constants.semantic.bgBlurLight6}; @supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) { -webkit-backdrop-filter: blur(25px); diff --git a/components/core/Auth/TwitterSignup.js b/components/core/Auth/TwitterSignup.js index 3364dc98..891e4ca8 100644 --- a/components/core/Auth/TwitterSignup.js +++ b/components/core/Auth/TwitterSignup.js @@ -15,7 +15,7 @@ import { SignUpPopover, Verification, AuthCheckBox } from "~/components/core/Aut const STYLES_SMALL = (theme) => css` font-size: ${theme.typescale.lvlN1}; text-align: center; - color: ${theme.system.textGrayDark}; + color: ${theme.semantic.textGrayDark}; max-width: 228px; margin: 0 auto; `; diff --git a/components/core/Auth/components/SignUpPopover.js b/components/core/Auth/components/SignUpPopover.js index ed112229..d732d4a6 100644 --- a/components/core/Auth/components/SignUpPopover.js +++ b/components/core/Auth/components/SignUpPopover.js @@ -22,7 +22,7 @@ const STYLES_POPOVER = (theme) => css` background-color: white; - @supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) { + @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { background: radial-gradient( 80.79% 80.79% at 50% 50%, rgba(242, 242, 247, 0.5) 0%, diff --git a/components/core/CollectionPreviewBlock/FilesCollectionPreview.js b/components/core/CollectionPreviewBlock/FilesCollectionPreview.js new file mode 100644 index 00000000..27273dc1 --- /dev/null +++ b/components/core/CollectionPreviewBlock/FilesCollectionPreview.js @@ -0,0 +1,259 @@ +import * as React from "react"; +import * as Strings from "~/common/strings"; +import * as Typography from "~/components/system/components/Typography"; +import * as Styles from "~/common/styles"; + +import { Divider } from "~/components/system/components/Divider"; +import { Logo } from "~/common/logo"; +import { css } from "@emotion/react"; +import { LikeButton, SaveButton } from "~/components/core/ObjectPreview/components"; +import { useLikeHandler, useSaveHandler } from "~/common/hooks"; +import { FollowButton } from "~/components/core/CollectionPreviewBlock/components"; +import { useFollowHandler } from "~/components/core/CollectionPreviewBlock/hooks"; + +import ObjectPlaceholder from "~/components/core/ObjectPreview/placeholders"; + +const STYLES_CONTAINER = (theme) => css` + display: flex; + flex-direction: column; + position: relative; + border-radius: 8px; + width: 100%; + border-radius: 8px; + overflow: hidden; + background-color: ${theme.system.white}; + height: 311px; + box-shadow: 0 0 0 1px ${theme.semantic.bgGrayLight}; + @media (max-width: ${theme.sizes.mobile}px) { + height: 281px; + } +`; + +const STYLES_DESCRIPTION_CONTAINER = (theme) => css` + display: flex; + flex-direction: column; + position: relative; + padding: 12px 16px; + border-radius: 0px 0px 8px 8px; + background-color: ${theme.system.white}; + @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { + background-color: ${theme.semantic.bgGrayLight}; + -webkit-backdrop-filter: blur(75px); + backdrop-filter: blur(75px); + } + width: 100%; + margin-top: auto; + @media (max-width: ${theme.sizes.mobile}px) { + padding: 12px; + } +`; + +const STYLES_SPACE_BETWEEN = css` + justify-content: space-between; +`; + +const STYLES_CONTROLLS = css` + display: flex; + flex-direction: column; + align-items: flex-end; +`; + +const STYLES_TEXT_BLACK = (theme) => css` + color: ${theme.semantic.textBlack}; +`; + +const STYLES_PROFILE_IMAGE = (theme) => css` + background-color: ${theme.semantic.bgLight}; + height: 20px; + width: 20px; + border-radius: 4px; + object-fit: cover; +`; + +const STYLES_HIGHLIGHT_BUTTON = (theme) => css` + box-sizing: border-box; + display: block; + padding: 4px 16px; + border: none; + background-color: unset; + div { + width: 8px; + height: 8px; + background-color: ${theme.system.gray}; + border-radius: 50%; + } +`; + +const STYLES_METRICS = (theme) => css` + margin-top: 16px; + @media (max-width: ${theme.sizes.mobile}px) { + margin-top: 12px; + } + ${Styles.CONTAINER_CENTERED}; + ${STYLES_SPACE_BETWEEN}; +`; + +const STYLES_FILES_PREVIEWS = (theme) => css` + flex-grow: 1; + padding: 16px; + padding-right: 0px; + @media (max-width: ${theme.sizes.mobile}px) { + padding: 12px; + padding-right: 0px; + } +`; + +const STYLES_PLACEHOLDER = css` + height: 64px; + min-width: 86px; + width: 86px; +`; + +const CollectionPreviewFile = ({ file, viewer }) => { + const { like, isLiked, likeCount } = useLikeHandler({ file, viewer }); + const { save, isSaved, saveCount } = useSaveHandler({ file, viewer }); + + const title = file.data.name || file.filename; + const { body } = file.data; + + return ( +
+ +
+ + {title} + + + {body} + +
+
+ + + {likeCount} + +
+
+ + + {saveCount} + +
+
+
+
+ ); +}; + +const useCollectionCarrousel = ({ objects }) => { + const [selectedIdx, setSelectedIdx] = React.useState(0); + const selectBatchIdx = (idx) => setSelectedIdx(idx); + const selectedBatch = objects[selectedIdx]; + return { selectBatchIdx, selectedBatch, selectedIdx }; +}; + +export default function CollectionPreview({ collection, viewer }) { + const { follow, followCount, isFollowed } = useFollowHandler({ collection, viewer }); + const filePreviews = React.useMemo(() => { + const files = collection?.objects || []; + let previews = []; + for (let i = 0; i < files.length; i++) { + const batch = []; + if (files[i * 2]) batch.push(files[i * 2]); + if (files[i * 2 + 1]) batch.push(files[i * 2 + 1]); + if (batch.length > 0) previews.push(batch); + if (previews.length === 3 || batch.length < 2) break; + } + return previews; + }, [collection]); + + const { selectBatchIdx, selectedBatch, selectedIdx } = useCollectionCarrousel({ + objects: filePreviews, + }); + + const nbrOfFiles = collection?.objects?.length || 0; + const isCollectionEmpty = nbrOfFiles === 0; + + const showFollowButton = collection.ownerId !== viewer?.id; + + return ( +
+
+
+ {!isCollectionEmpty ? ( + selectedBatch.map((file, i) => ( + + {i === 1 && } + + + )) + ) : ( +
+ + No files in this collection +
+ )} +
+ { +
+ {filePreviews.map((preview, i) => ( + + ))} +
+ } +
+
+
+
+ + {collection.slatename} + + + {nbrOfFiles} {Strings.pluralize("Object", nbrOfFiles)} + +
+ + {collection?.data?.body && ( + + {collection?.data?.body} + + )} +
+ +
+
+ + + {followCount} + +
+
+ owner profile + + Wes Anderson + +
+
+
+
+ ); +} diff --git a/components/core/CollectionPreviewBlock/ImageCollectionPreview.js b/components/core/CollectionPreviewBlock/ImageCollectionPreview.js new file mode 100644 index 00000000..47ceff44 --- /dev/null +++ b/components/core/CollectionPreviewBlock/ImageCollectionPreview.js @@ -0,0 +1,235 @@ +import * as React from "react"; +import * as Strings from "~/common/strings"; +import * as Typography from "~/components/system/components/Typography"; +import * as Styles from "~/common/styles"; + +import { useInView } from "~/common/hooks"; +import { isBlurhashValid } from "blurhash"; +import { Blurhash } from "react-blurhash"; +import { css } from "@emotion/react"; +import { FollowButton } from "~/components/core/CollectionPreviewBlock/components"; +import { useFollowHandler } from "~/components/core/CollectionPreviewBlock/hooks"; + +const STYLES_CONTAINER = (theme) => css` + display: flex; + flex-direction: column; + position: relative; + border-radius: 8px; + width: 100%; + border-radius: 8px; + overflow: hidden; + + height: 311px; + @media (max-width: ${theme.sizes.mobile}px) { + height: 281px; + } +`; + +const STYLES_PREVIEW = (theme) => css` + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 100%; + background-color: ${theme.system.white}; + background-size: cover; + img { + width: 100%; + height: 100%; + object-fit: cover; + } +`; + +const STYLES_DESCRIPTION_CONTAINER = (theme) => css` + display: flex; + flex-direction: column; + position: relative; + padding: 12px 16px; + border-radius: 8px; + background-color: ${theme.system.white}; + @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { + background-color: ${theme.semantic.bgBlurLight6OP}; + -webkit-backdrop-filter: blur(75px); + backdrop-filter: blur(75px); + } + width: 100%; + margin-top: auto; +`; + +const STYLES_SPACE_BETWEEN = css` + justify-content: space-between; +`; + +const STYLES_CONTROLLS = css` + display: flex; + flex-direction: column; + align-items: flex-end; + position: relative; + padding: 16px 0px 16px 16px; +`; + +const STYLES_PROFILE_IMAGE = (theme) => css` + background-color: ${theme.semantic.bgLight}; + height: 20px; + width: 20px; + border-radius: 4px; + object-fit: cover; +`; + +const STYLES_HIGHLIGHT_BUTTON = (theme) => css` + box-sizing: border-box; + display: block; + padding: 4px 16px; + border: none; + background-color: unset; + div { + width: 8px; + height: 8px; + background-color: ${theme.system.gray}; + border-radius: 50%; + } +`; + +const STYLES_METRICS = (theme) => css` + margin-top: 16px; + @media (max-width: ${theme.sizes.mobile}px) { + margin-top: 12px; + } + ${Styles.CONTAINER_CENTERED}; + ${STYLES_SPACE_BETWEEN} +`; + +const getFileBlurHash = (file) => { + const coverImage = file?.data?.coverImage; + const coverImageBlurHash = coverImage?.data?.blurhash; + if (coverImage && isBlurhashValid(coverImageBlurHash)) return coverImageBlurHash; + + const blurhash = file?.data?.blurhash; + if (isBlurhashValid(blurhash)) return blurhash; + + return null; +}; + +const useCollectionCarrousel = ({ objects }) => { + const [selectedIdx, setSelectedIdx] = React.useState(0); + const selectImageByIdx = (idx) => setSelectedIdx(idx); + + const [imagesLoadedIdx, setImagesLoadedIdx] = React.useState({}); + const handleLoading = () => setImagesLoadedIdx((prev) => ({ ...prev, [selectedIdx]: true })); + + const isCurrentImageLoaded = imagesLoadedIdx[selectedIdx]; + const { image: selectedImage, blurhash } = objects[selectedIdx]; + return { + selectedImage, + selectedIdx, + isLoaded: isCurrentImageLoaded, + blurhash, + handleLoading, + selectImageByIdx, + }; +}; + +export default function ImageCollectionPreview({ collection, viewer }) { + const { follow, followCount, isFollowed } = useFollowHandler({ collection, viewer }); + + const filePreviews = React.useMemo(() => { + const previews = collection.objects.map((object) => { + const coverImage = object?.data?.coverImage; + const image = coverImage || Strings.getURLfromCID(object.cid); + const blurhash = getFileBlurHash(object); + return { image, blurhash }; + }); + return previews.slice(0, 3); + }, [collection]); + + const previewerRef = React.useRef(); + const { isInView } = useInView({ + ref: previewerRef, + }); + + const { + isLoaded, + blurhash, + selectedImage, + handleLoading, + selectedIdx, + selectImageByIdx, + } = useCollectionCarrousel({ objects: filePreviews }); + + const nbrOfFiles = collection?.objects?.length || 0; + + const showFollowButton = collection.ownerId !== viewer?.id; + return ( +
+ {isInView && ( +
+ {!isLoaded && blurhash && ( + + )} + Collection preview +
+ )} +
+ {filePreviews.map((preview, i) => ( + + ))} +
+
+
+
+ + {collection.slatename} + + + {nbrOfFiles} {Strings.pluralize("Object", nbrOfFiles)} + +
+ {collection?.data?.body && ( + + {collection?.data?.body} + + )} +
+ +
+
+ + + {followCount} + +
+
+ owner profile + + Wes Anderson + +
+
+
+
+ ); +} diff --git a/components/core/CollectionPreviewBlock/components/FollowButton.js b/components/core/CollectionPreviewBlock/components/FollowButton.js new file mode 100644 index 00000000..c59baf39 --- /dev/null +++ b/components/core/CollectionPreviewBlock/components/FollowButton.js @@ -0,0 +1,85 @@ +import * as React from "react"; +import * as Constants from "~/common/constants"; +import * as Styles from "~/common/styles"; + +import { css } from "@emotion/react"; +import { motion, useAnimation } from "framer-motion"; + +const STYLES_BUTTON_HOVER = (theme) => css` + :hover .button_path { + stroke: ${theme.system.blue}; + } +`; + +const STYLES_INLINE = css` + display: flex; +`; + +const animate = async (controls) => { + await controls.start({ x: -2, y: 2 }); + await controls.start({ x: 0, y: 0 }); +}; + +const useMounted = (callback, depedencies) => { + const mountedRef = React.useRef(false); + React.useLayoutEffect(() => { + if (mountedRef.current && callback) { + callback(); + } + mountedRef.current = true; + }, depedencies); +}; + +export default function FollowButton({ onFollow, isFollowed, disabled, ...props }) { + const controls = useAnimation(); + + useMounted(() => { + if (isFollowed) { + animate(controls); + return; + } + }, [isFollowed]); + + return ( + + ); +} diff --git a/components/core/CollectionPreviewBlock/components/index.js b/components/core/CollectionPreviewBlock/components/index.js new file mode 100644 index 00000000..dbdcfe8c --- /dev/null +++ b/components/core/CollectionPreviewBlock/components/index.js @@ -0,0 +1 @@ +export { default as FollowButton } from "./FollowButton"; diff --git a/components/core/CollectionPreviewBlock/hooks.js b/components/core/CollectionPreviewBlock/hooks.js new file mode 100644 index 00000000..792c93e7 --- /dev/null +++ b/components/core/CollectionPreviewBlock/hooks.js @@ -0,0 +1,50 @@ +import * as React from "react"; +import * as Events from "~/common/custom-events"; +import * as Actions from "~/common/actions"; + +export const useFollowHandler = ({ collection, viewer }) => { + const followedCollection = React.useMemo( + () => viewer?.subscriptions?.find((subscription) => subscription.id === collection.id), + [] + ); + const [state, setState] = React.useState({ + isFollowed: !!followedCollection, + // NOTE(amine): viewer will have the hydrated state + followCount: followedCollection?.subscriberCount ?? collection.subscriberCount, + }); + + const handleFollowState = () => { + setState((prev) => { + if (prev.isFollowed) { + return { + isFollowed: false, + followCount: prev.followCount - 1, + }; + } + return { + isFollowed: true, + followCount: prev.followCount + 1, + }; + }); + }; + + const follow = async () => { + if (!viewer) { + Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} }); + return; + } + // NOTE(amine): optimistic update + handleFollowState(); + const response = await Actions.createSubscription({ + slateId: collection.id, + }); + + if (Events.hasError(response)) { + // NOTE(amine): revert back to old state if there is an error + handleFollowState(); + return; + } + }; + + return { follow, ...state }; +}; diff --git a/components/core/CollectionPreviewBlock/index.js b/components/core/CollectionPreviewBlock/index.js new file mode 100644 index 00000000..475711b9 --- /dev/null +++ b/components/core/CollectionPreviewBlock/index.js @@ -0,0 +1,17 @@ +import * as React from "react"; +import * as Validations from "~/common/validations"; + +import ImageCollectionPreview from "./ImageCollectionPreview"; +import FilesCollectionPreview from "./FilesCollectionPreview"; + +export default function CollectionPreview({ collection, viewer }) { + const objects = collection.objects.filter((file) => + Validations.isPreviewableImage(file.data.type) + ); + + if (objects.length > 0) { + return ; + } + + return ; +} diff --git a/components/core/DataMeter.js b/components/core/DataMeter.js index 33b78dc0..d2e60d10 100644 --- a/components/core/DataMeter.js +++ b/components/core/DataMeter.js @@ -6,8 +6,7 @@ import { css } from "@emotion/react"; const STYLES_CONTAINER = css` border-radius: 4px; - box-shadow: 0 0 0 1px ${Constants.semantic.borderLight} inset, - 0 0 40px 0 ${Constants.shadow.lightSmall}; + box-shadow: 0 0 0 1px ${Constants.semantic.borderLight} inset, ${Constants.shadow.lightSmall}; padding: 32px; max-width: 100%; width: 100%; @@ -103,7 +102,6 @@ export const DataMeter = (props) => { used -
50GB coming soon when we add email verification
); }; diff --git a/components/core/DataMeterDetailed.js b/components/core/DataMeterDetailed.js index 2ff3e192..5d371237 100644 --- a/components/core/DataMeterDetailed.js +++ b/components/core/DataMeterDetailed.js @@ -6,12 +6,11 @@ import { css } from "@emotion/react"; const STYLES_CONTAINER = css` border-radius: 4px; - box-shadow: 0 0 0 1px ${Constants.semantic.borderLight} inset, - 0 0 40px 0 ${Constants.shadow.lightSmall}; + box-shadow: 0 0 0 1px ${Constants.semantic.borderLight} inset, ${Constants.shadow.lightSmall}; padding: 32px; max-width: 100%; width: 100%; - background-color: ${Constants.system.white}; + ${"" /* background-color: ${Constants.system.white}; */} @media (max-width: ${Constants.sizes.mobile}px) { padding: 24px; @@ -172,8 +171,6 @@ export const DataMeterDetailed = (props) => {
Audio
- -
50GB coming soon when we add email verification
{props.buttons ?
{props.buttons}
: null} ); diff --git a/components/core/DataView.js b/components/core/DataView.js index 99ab94e7..7a6f0daa 100644 --- a/components/core/DataView.js +++ b/components/core/DataView.js @@ -1,28 +1,27 @@ import * as React from "react"; import * as Constants from "~/common/constants"; import * as Strings from "~/common/strings"; -import * as System from "~/components/system"; import * as SVG from "~/common/svg"; import * as Window from "~/common/window"; import * as UserBehaviors from "~/common/user-behaviors"; import * as Events from "~/common/custom-events"; +import * as Styles from "~/common/styles"; import { Link } from "~/components/core/Link"; import { css } from "@emotion/react"; import { Boundary } from "~/components/system/components/fragments/Boundary"; import { PopoverNavigation } from "~/components/system/components/PopoverNavigation"; -import { LoaderSpinner } from "~/components/system/components/Loaders"; import { CheckBox } from "~/components/system/components/CheckBox"; import { Table } from "~/components/core/Table"; import { FileTypeIcon } from "~/components/core/FileTypeIcon"; import { ButtonPrimary, ButtonWarning } from "~/components/system/components/Buttons"; import { GroupSelectable, Selectable } from "~/components/core/Selectable/"; - -import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview"; -import FilePreviewBubble from "~/components/core/FilePreviewBubble"; -import isEqual from "lodash/isEqual"; import { ConfirmationModal } from "~/components/core/ConfirmationModal"; +import FilePreviewBubble from "~/components/core/FilePreviewBubble"; +import ObjectPreview from "~/components/core/ObjectPreview"; +import isEqual from "lodash/isEqual"; + const STYLES_CONTAINER_HOVER = css` display: flex; :hover { @@ -163,12 +162,11 @@ const STYLES_COPY_INPUT = css` const STYLES_IMAGE_GRID = css` display: grid; - grid-template-columns: repeat(5, 1fr); - grid-column-gap: 20px; - grid-row-gap: 20px; - width: 100%; + grid-template-columns: repeat(auto-fill, minmax(248px, 1fr)); + grid-gap: 20px 12px; + @media (max-width: ${Constants.sizes.mobile}px) { - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); } `; @@ -185,7 +183,7 @@ const STYLES_IMAGE_BOX = css` } :hover { box-shadow: 0px 0px 0px 1px ${Constants.semantic.borderLight} inset, - 0 0 40px 0 ${Constants.shadow.lightSmall}; + ${Constants.shadow.lightSmall}; } `; @@ -562,6 +560,7 @@ export default class DataView extends React.Component { const url = Strings.getURLfromCID(object.cid); const title = object.filename || object.data.name; const type = object.data.type; + console.log(e.dataTransfer, e.dataTransfer.setData); e.dataTransfer.setData("DownloadURL", `${type}:${title}:${url}`); }; @@ -711,7 +710,7 @@ export default class DataView extends React.Component { return ( -
+
{this.props.items.slice(0, this.state.viewLimit).map((each, i) => { return ( this._handleCheckBoxMouseEnter(i)} onMouseLeave={() => this._handleCheckBoxMouseLeave(i)} > - - - {numChecked || this.state.hover === i || this.state.menu === each.id ? ( - -
this._handleCheckBox(e, i)}> - -
-
- ) : null} -
+
+ + + {numChecked || this.state.hover === i || this.state.menu === each.id ? ( + +
this._handleCheckBox(e, i)} + > + +
+
+ ) : null} +
+
); diff --git a/components/core/Field.js b/components/core/Field.js index f44de3ea..a8dc2ef9 100644 --- a/components/core/Field.js +++ b/components/core/Field.js @@ -54,14 +54,14 @@ const STYLES_INPUT_ERROR = (theme) => css` background-color: rgba(242, 242, 247, 0.5); border: 1px solid ${theme.system.red}; &::placeholder { - color: ${theme.system.textGrayDark}; + color: ${theme.semantic.textGrayDark}; } `; const STYLES_INPUT_SUCCESS = (theme) => css` background-color: rgba(242, 242, 247, 0.5); border: 1px solid ${theme.system.green}; &::placeholder { - color: ${theme.system.textGrayDark}; + color: ${theme.semantic.textGrayDark}; } `; diff --git a/components/core/FontFrame/hooks.js b/components/core/FontFrame/hooks.js index ce7b3e17..21fa1a91 100644 --- a/components/core/FontFrame/hooks.js +++ b/components/core/FontFrame/hooks.js @@ -10,10 +10,15 @@ export const useFont = ({ cid }, deps) => { const [fetchState, setFetchState] = React.useState({ loading: false, error: null }); const prevName = React.useRef(cid); - if (!window.$SLATES_LOADED_FONTS) window.$SLATES_LOADED_FONTS = []; - const alreadyLoaded = window.$SLATES_LOADED_FONTS.includes(cid); + if (typeof window !== "undefined" && !window.$SLATES_LOADED_FONTS) { + window.$SLATES_LOADED_FONTS = []; + } + const alreadyLoaded = + (typeof window !== "undefined" && window.$SLATES_LOADED_FONTS.includes(cid)) || false; React.useEffect(() => { + if (!window) return; + if (alreadyLoaded) { setFetchState((prev) => ({ ...prev, error: null })); return; diff --git a/components/core/FontFrame/index.js b/components/core/FontFrame/index.js index dd6706cc..b266777b 100644 --- a/components/core/FontFrame/index.js +++ b/components/core/FontFrame/index.js @@ -86,7 +86,7 @@ export default function FontFrame({ cid, fallback, ...props }) { isSettingsVisible={currentState.context.showSettings} />
-
+
{isFontLoading && } {this.props.children} diff --git a/components/core/Link/LinkCard.js b/components/core/Link/LinkCard.js index 00d0fe17..9bc1d75d 100644 --- a/components/core/Link/LinkCard.js +++ b/components/core/Link/LinkCard.js @@ -96,7 +96,7 @@ export default function LinkCard({ file }) {
- + {name} diff --git a/components/core/ObjectPreview/3dObjectPreview.js b/components/core/ObjectPreview/3dObjectPreview.js new file mode 100644 index 00000000..675c2709 --- /dev/null +++ b/components/core/ObjectPreview/3dObjectPreview.js @@ -0,0 +1,38 @@ +import "isomorphic-fetch"; + +import * as React from "react"; +import * as Styles from "~/common/styles"; + +import { P3 } from "~/components/system"; +import { css } from "@emotion/react"; + +import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import ObjectPlaceholder from "./placeholders/3D"; + +const STYLES_CONTAINER = css` + height: 100%; +`; + +const STYLES_TAG = (theme) => css` + position: absolute; + text-transform: uppercase; + background-color: ${theme.semantic.bgLight}; + bottom: 25%; + left: 50%; + transform: translateX(-50%); + padding: 2px 8px; + border-radius: 4px; +`; + +export default function Object3DPreview(props) { + return ( + +
+ +
+ 3D +
+
+
+ ); +} diff --git a/components/core/ObjectPreview/AudioObjectPreview.js b/components/core/ObjectPreview/AudioObjectPreview.js new file mode 100644 index 00000000..b31fe52f --- /dev/null +++ b/components/core/ObjectPreview/AudioObjectPreview.js @@ -0,0 +1,41 @@ +import "isomorphic-fetch"; + +import * as React from "react"; +import * as Styles from "~/common/styles"; +import * as Utilities from "~/common/utilities"; + +import { P3 } from "~/components/system"; + +import { css } from "@emotion/react"; + +import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import AudioPlaceholder from "./placeholders/Audio"; + +const STYLES_CONTAINER = css` + height: 100%; +`; + +const STYLES_TAG = (theme) => css` + position: absolute; + text-transform: uppercase; + background-color: ${theme.semantic.bgLight}; + bottom: 23.7%; + left: 50%; + transform: translateX(-50%); + padding: 2px 8px; + border-radius: 4px; +`; + +export default function AudioObjectPreview({ file, ...props }) { + const tag = Utilities.getFileExtension(file.filename) || "audio"; + return ( + +
+ +
+ {tag} +
+
+
+ ); +} diff --git a/components/core/ObjectPreview/CodeObjectPreview.js b/components/core/ObjectPreview/CodeObjectPreview.js new file mode 100644 index 00000000..ff18fc27 --- /dev/null +++ b/components/core/ObjectPreview/CodeObjectPreview.js @@ -0,0 +1,40 @@ +import "isomorphic-fetch"; + +import * as React from "react"; +import * as Styles from "~/common/styles"; +import * as Utilities from "~/common/utilities"; + +import { P3 } from "~/components/system"; +import { css } from "@emotion/react"; + +import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import CodePlaceholder from "./placeholders/Code"; + +const STYLES_CONTAINER = css` + height: 100%; +`; + +const STYLES_TAG = (theme) => css` + position: absolute; + text-transform: uppercase; + background-color: ${theme.semantic.bgLight}; + bottom: 27%; + left: 50%; + transform: translateX(-50%); + padding: 2px 8px; + border-radius: 4px; +`; + +export default function CodeObjectPreview({ file, ...props }) { + const tag = Utilities.getFileExtension(file.filename) || "code"; + return ( + +
+ +
+ {tag} +
+
+
+ ); +} diff --git a/components/core/ObjectPreview/DefaultObjectPreview.js b/components/core/ObjectPreview/DefaultObjectPreview.js new file mode 100644 index 00000000..7632751f --- /dev/null +++ b/components/core/ObjectPreview/DefaultObjectPreview.js @@ -0,0 +1,36 @@ +import * as React from "react"; +import * as Styles from "~/common/styles"; + +import { P3 } from "~/components/system"; +import { css } from "@emotion/react"; + +import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import FilePlaceholder from "./placeholders/File"; + +const STYLES_CONTAINER = css` + height: 100%; +`; + +const STYLES_TAG = (theme) => css` + position: absolute; + text-transform: uppercase; + background-color: ${theme.semantic.bgLight}; + bottom: 26%; + left: 50%; + transform: translateX(-50%); + padding: 2px 8px; + border-radius: 4px; +`; + +export default function DefaultObjectPreview(props) { + return ( + +
+ +
+ FILE +
+
+
+ ); +} diff --git a/components/core/ObjectPreview/EpubObjectPreview.js b/components/core/ObjectPreview/EpubObjectPreview.js new file mode 100644 index 00000000..c935378d --- /dev/null +++ b/components/core/ObjectPreview/EpubObjectPreview.js @@ -0,0 +1,38 @@ +import "isomorphic-fetch"; + +import * as React from "react"; +import * as Styles from "~/common/styles"; + +import { P3 } from "~/components/system"; +import { css } from "@emotion/react"; + +import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import EpubPlaceholder from "./placeholders/EPUB"; + +const STYLES_CONTAINER = css` + height: 100%; +`; + +const STYLES_TAG = (theme) => css` + position: absolute; + text-transform: uppercase; + background-color: ${theme.semantic.bgLight}; + bottom: 32%; + left: 50%; + transform: translateX(-50%); + padding: 2px 8px; + border-radius: 4px; +`; + +export default function EpubObjectPreview(props) { + return ( + +
+ +
+ EPUB +
+
+
+ ); +} diff --git a/components/core/ObjectPreview/FontObjectPreview.js b/components/core/ObjectPreview/FontObjectPreview.js new file mode 100644 index 00000000..0c03ba89 --- /dev/null +++ b/components/core/ObjectPreview/FontObjectPreview.js @@ -0,0 +1,40 @@ +import "isomorphic-fetch"; + +import * as React from "react"; +import * as Styles from "~/common/styles"; +import * as Utilities from "~/common/utilities"; + +import { css } from "@emotion/react"; + +import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import { useFont } from "~/components/core/FontFrame/hooks"; + +const STYLES_TEXT_PREVIEW = (theme) => css` + position: relative; + height: 100%; + margin: 8px; + background-color: ${theme.system.white}; + border-radius: 8px; + box-shadow: ${theme.shadow.large}; +`; + +const STYLES_LETTER = (theme) => css` + transform: translateY(-25%); + overflow: hidden; + font-size: ${theme.typescale.lvl8}; +`; + +export default function FontObjectPreview({ file, ...props }) { + const { fontName } = useFont({ cid: file.cid }, [file.cid]); + + const tag = Utilities.getFileExtension(file.filename) || "font"; + return ( + +
+
+
Aa
+
+
+
+ ); +} diff --git a/components/core/ObjectPreview/ImageObjectPreview.js b/components/core/ObjectPreview/ImageObjectPreview.js new file mode 100644 index 00000000..5692dacc --- /dev/null +++ b/components/core/ObjectPreview/ImageObjectPreview.js @@ -0,0 +1,91 @@ +import * as React from "react"; +import * as Styles from "~/common/styles"; +import * as Strings from "~/common/strings"; + +import { AspectRatio } from "~/components/system"; +import { useInView } from "~/common/hooks"; +import { Blurhash } from "react-blurhash"; +import { isBlurhashValid } from "blurhash"; + +import { css } from "@emotion/react"; +import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; + +const STYLES_PLACEHOLDER_ABSOLUTE = css` + position: absolute; + top: 0%; + left: 0%; + width: 100%; + height: 100%; +`; +const STYLES_FLUID_CONTAINER = css` + position: relative; + width: 100%; + height: 100%; +`; + +const STYLES_IMAGE = css` + object-fit: cover; +`; + +const ImagePlaceholder = ({ blurhash }) => ( +
+
+ +
+ +
+
+
+
+); + +export default function ImageObjectPreview({ url, file, ...props }) { + const previewerRef = React.useRef(); + const [isLoading, setLoading] = React.useState(true); + const handleOnLoaded = () => setLoading(false); + + const { isInView } = useInView({ + ref: previewerRef, + }); + + const { type, coverImage } = file.data; + const tag = type.split("/")[1]; + + const blurhash = React.useMemo(() => { + return file.data.blurhash && isBlurhashValid(file.data.blurhash) + ? file.data.blurhash + : coverImage?.data.blurhash && isBlurhashValid(coverImage?.data.blurhash) + ? coverImage?.data.blurhash + : null; + }, [file]); + + const shouldShowPlaceholder = isLoading && blurhash; + + const imageUrl = coverImage ? Strings.getURLfromCID(coverImage?.cid) : url; + + return ( + +
+ {isInView && ( + + {/** NOTE(amine): if it's loaded */} + {`${file.name} + + )} + {shouldShowPlaceholder && } +
+
+ ); +} diff --git a/components/core/ObjectPreview/KeynoteObjectPreview.js b/components/core/ObjectPreview/KeynoteObjectPreview.js new file mode 100644 index 00000000..aaad4fa2 --- /dev/null +++ b/components/core/ObjectPreview/KeynoteObjectPreview.js @@ -0,0 +1,38 @@ +import "isomorphic-fetch"; + +import * as React from "react"; +import * as Styles from "~/common/styles"; + +import { P3 } from "~/components/system"; +import { css } from "@emotion/react"; + +import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import KeynotePlaceholder from "./placeholders/Keynote"; + +const STYLES_CONTAINER = css` + height: 100%; +`; + +const STYLES_TAG = (theme) => css` + position: absolute; + text-transform: uppercase; + background-color: ${theme.semantic.bgLight}; + bottom: 36%; + left: 50%; + transform: translateX(-50%); + padding: 2px 8px; + border-radius: 4px; +`; + +export default function KeynoteObjectPreview(props) { + return ( + +
+ +
+ KEYNOTE +
+
+
+ ); +} diff --git a/components/core/ObjectPreview/ObjectPreviewPremitive.js b/components/core/ObjectPreview/ObjectPreviewPremitive.js new file mode 100644 index 00000000..e1c5b802 --- /dev/null +++ b/components/core/ObjectPreview/ObjectPreviewPremitive.js @@ -0,0 +1,171 @@ +import * as React from "react"; +import * as Constants from "~/common/constants"; +import * as Styles from "~/common/styles"; + +import { css } from "@emotion/react"; +import { H4, P2, P3 } from "~/components/system/components/Typography"; +import { AspectRatio } from "~/components/system"; +import { LikeButton, SaveButton } from "./components"; +import { useLikeHandler, useSaveHandler } from "~/common/hooks"; +import { Link } from "~/components/core/Link"; + +import ImageObjectPreview from "./ImageObjectPreview"; + +const STYLES_BACKGROUND_LIGHT = (theme) => css` + background-color: ${theme.semantic.bgLight}; + box-shadow: 0 0 0 1px ${theme.semantic.bgLight}; + border-radius: 8px; +`; + +const STYLES_WRAPPER = css` + border-radius: 8px; + overflow: hidden; +`; + +const STYLES_DESCRIPTION = (theme) => css` + box-sizing: border-box; + width: 100%; + padding: 12px 16px 12px; + position: absolute; + bottom: 0; + left: 0; + background-color: ${theme.system.white}; + + @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { + background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + backdrop-filter: blur(75px); + -webkit-backdrop-filter: blur(75px); + } + + @media (max-width: ${theme.sizes.mobile}px) { + padding: 8px; + } +`; + +const STYLES_DESCRIPTION_META = css` + justify-content: space-between; + margin-top: 12px; +`; + +const STYLES_REACTIONS_CONTAINER = css` + display: flex; + & > * + * { + margin-left: 32px; + } +`; + +const STYLES_REACTION = css` + display: flex; + & > * + * { + margin-left: 8px; + } +`; + +const STYLES_PROFILE_IMAGE = css` + display: block; + background-color: ${Constants.semantic.bgLight}; + flex-shrink: 0; + object-fit: cover; + height: 20px; + width: 20px; + border-radius: 2px; +`; + +const STYLES_DESCRIPTION_TAG = (theme) => css` + position: absolute; + top: -32px; + left: 12px; + text-transform: uppercase; + border: 1px solid ${theme.system.grayLight5}; + background-color: ${theme.semantic.bgLight}; + padding: 2px 8px; + border-radius: 4px; +`; + +const STYLES_SELECTED_RING = (theme) => css` + box-shadow: 0 0 0 2px ${theme.system.blue}; +`; + +export default function ObjectPreviewPremitive({ + children, + tag, + file, + isSelected, + viewer, + owner, + // NOTE(amine): internal prop used to display + isImage, + onAction, +}) { + const { like, isLiked, likeCount } = useLikeHandler({ file, viewer }); + const { save, isSaved, saveCount } = useSaveHandler({ file, viewer }); + + const title = file.data.name || file.filename; + + if (file?.data?.coverImage && !isImage) { + return ( + + ); + } + const showSaveButton = viewer?.id !== file?.ownerId; + return ( +
+ +
+ +
{children}
+
+ +
+ {tag && ( +
+ {tag} +
+ )} +

+ {title} +

+ +
+
+
+ + {likeCount} +
+ {showSaveButton && ( +
+ + {saveCount} +
+ )} +
+ {owner && ( + + {`${owner.username} + + )} +
+
+
+
+
+ ); +} diff --git a/components/core/ObjectPreview/PdfObjectPreview.js b/components/core/ObjectPreview/PdfObjectPreview.js new file mode 100644 index 00000000..27a65888 --- /dev/null +++ b/components/core/ObjectPreview/PdfObjectPreview.js @@ -0,0 +1,39 @@ +import "isomorphic-fetch"; + +import * as React from "react"; +import * as Styles from "~/common/styles"; + +import { P3 } from "~/components/system"; +import { css } from "@emotion/react"; + +import PdfPlaceholder from "./placeholders/PDF"; +import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; + +const STYLES_CONTAINER = css` + position: relative; + height: 100%; +`; + +const STYLES_TAG = (theme) => css` + position: absolute; + text-transform: uppercase; + background-color: ${theme.semantic.bgLight}; + bottom: 27%; + left: 50%; + transform: translateX(-50%); + padding: 2px 8px; + border-radius: 4px; +`; + +export default function PDFObjectPreview(props) { + return ( + +
+ +
+ PDF +
+
+
+ ); +} diff --git a/components/core/ObjectPreview/TextObjectPreview.js b/components/core/ObjectPreview/TextObjectPreview.js new file mode 100644 index 00000000..b391db01 --- /dev/null +++ b/components/core/ObjectPreview/TextObjectPreview.js @@ -0,0 +1,76 @@ +import "isomorphic-fetch"; + +import * as React from "react"; +import * as Styles from "~/common/styles"; +import * as Utilities from "~/common/utilities"; + +import { P3 } from "~/components/system"; +import { css } from "@emotion/react"; + +import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import TextPlaceholder from "./placeholders/Text"; + +const STYLES_CONTAINER = css` + position: relative; + display: flex; + height: 100%; + justify-content: center; +`; + +const STYLES_TAG = (theme) => css` + position: absolute; + text-transform: uppercase; + background-color: ${theme.semantic.bgLight}; + bottom: 26%; + left: 50%; + transform: translateX(-50%); + padding: 2px 8px; + border-radius: 4px; +`; + +const STYLES_TEXT_PREVIEW = (theme) => + css({ + height: "100%", + width: "100%", + margin: "8px", + backgroundColor: "#FFF", + borderRadius: "8px", + boxShadow: theme.shadow.large, + padding: "16px", + }); + +export default function TextObjectPreview({ url, file, ...props }) { + const [{ content, error }, setState] = React.useState({ content: "", error: undefined }); + + React.useLayoutEffect(() => { + fetch(url) + .then(async (res) => { + const content = await res.text(); + setState({ content }); + }) + .catch((e) => { + setState({ error: e }); + }); + }, []); + + const tag = Utilities.getFileExtension(file.filename) || "text"; + + return ( + +
+ {error ? ( + <> + +
+ {tag} +
+ + ) : ( +
+ {content} +
+ )} +
+
+ ); +} diff --git a/components/core/ObjectPreview/VideoObjectPreview.js b/components/core/ObjectPreview/VideoObjectPreview.js new file mode 100644 index 00000000..0f1d4aff --- /dev/null +++ b/components/core/ObjectPreview/VideoObjectPreview.js @@ -0,0 +1,40 @@ +import "isomorphic-fetch"; + +import * as React from "react"; +import * as Styles from "~/common/styles"; + +import { P3 } from "~/components/system"; +import { css } from "@emotion/react"; + +import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import VideoPlaceholder from "./placeholders/Video"; + +const STYLES_CONTAINER = css` + height: 100%; +`; + +const STYLES_TAG = (theme) => css` + position: absolute; + text-transform: uppercase; + background-color: ${theme.semantic.bgLight}; + bottom: 31.5%; + left: 50%; + transform: translateX(-50%); + padding: 2px 8px; + border-radius: 4px; +`; + +export default function VideoObjectPreview({ file, ...props }) { + const { type } = file.data; + const tag = type.split("/")[1]; + return ( + +
+ +
+ {tag} +
+
+
+ ); +} diff --git a/components/core/ObjectPreview/components/LikeButton.jsx b/components/core/ObjectPreview/components/LikeButton.jsx new file mode 100644 index 00000000..27c5ef26 --- /dev/null +++ b/components/core/ObjectPreview/components/LikeButton.jsx @@ -0,0 +1,72 @@ +import * as React from "react"; +import * as Constants from "~/common/constants"; +import * as Styles from "~/common/styles"; + +import { css } from "@emotion/react"; +import { motion, useAnimation } from "framer-motion"; + +const STYLES_BUTTON_HOVER = (theme) => css` + display: flex; + :hover path { + stroke: ${theme.system.blue}; + } +`; + +const animate = async (controls) => { + await controls.start({ scale: 1.3, rotateY: 180, fill: "rgba(0, 132, 255, 1)" }); + await controls.start({ scale: 1 }); + controls.set({ rotateY: 0 }); +}; + +const useMounted = (callback, depedencies) => { + const mountedRef = React.useRef(false); + React.useLayoutEffect(() => { + if (mountedRef.current && callback) { + callback(); + } + mountedRef.current = true; + }, depedencies); +}; + +export default function LikeButton({ onClick, isLiked, ...props }) { + const controls = useAnimation(); + + useMounted(() => { + if (isLiked) { + animate(controls); + return; + } + controls.start({ fill: "#fff", scale: 1 }); + }, [isLiked]); + + return ( + + ); +} diff --git a/components/core/ObjectPreview/components/SaveButton.jsx b/components/core/ObjectPreview/components/SaveButton.jsx new file mode 100644 index 00000000..c0d1ff98 --- /dev/null +++ b/components/core/ObjectPreview/components/SaveButton.jsx @@ -0,0 +1,81 @@ +import * as React from "react"; +import * as Styles from "~/common/styles"; +import * as Constants from "~/common/constants"; + +import { css } from "@emotion/react"; +import { motion } from "framer-motion"; + +const STYLES_BUTTON_HOVER = (theme) => css` + display: flex; + :hover .button_path { + stroke: ${theme.system.blue}; + } +`; + +export default function SaveButton({ onSave, isSaved, ...props }) { + return ( + + ); +} diff --git a/components/core/ObjectPreview/components/index.js b/components/core/ObjectPreview/components/index.js new file mode 100644 index 00000000..cf0e14a5 --- /dev/null +++ b/components/core/ObjectPreview/components/index.js @@ -0,0 +1,2 @@ +export { default as LikeButton } from "./LikeButton"; +export { default as SaveButton } from "./SaveButton"; diff --git a/components/core/ObjectPreview/index.js b/components/core/ObjectPreview/index.js new file mode 100644 index 00000000..85f9a0d8 --- /dev/null +++ b/components/core/ObjectPreview/index.js @@ -0,0 +1,67 @@ +import * as React from "react"; +import * as Validations from "~/common/validations"; +import * as Strings from "~/common/strings"; + +import ImageObjectPreview from "./ImageObjectPreview"; +import VideoObjectPreview from "./VideoObjectPreview"; +import TextObjectPreview from "./TextObjectPreview"; +import PdfObjectPreview from "./PdfObjectPreview"; +import EpubObjectPreview from "./EpubObjectPreview"; +import AudioObjectPreview from "./AudioObjectPreview"; +import KeynoteObjectPreview from "./KeynoteObjectPreview"; +import DefaultObjectPreview from "./DefaultObjectPreview"; +import Object3DPreview from "./3dObjectPreview"; +import CodeObjectPreview from "./CodeObjectPreview"; +import FontObjectPreview from "./FontObjectPreview"; + +const ObjectPreview = ({ file, ...props }) => { + const { type, coverImage } = file.data; + + const url = Validations.isPreviewableImage(type) + ? Strings.getURLfromCID(file.cid) + : Strings.getURLfromCID(coverImage?.cid); + + if (Validations.isPreviewableImage(type)) { + return ; + } + + if (type.startsWith("video/")) { + return ; + } + + if (Validations.isPdfType(type)) { + return ; + } + + if (type.startsWith("audio/")) { + return ; + } + + if (type === "application/epub+zip") { + return ; + } + + if (file.filename.endsWith(".key")) { + return ; + } + + if (Validations.isCodeFile(file.filename)) { + return ; + } + + if (Validations.isFontFile(file.filename)) { + return ; + } + + if (Validations.isMarkdown(file.filename, type)) { + return ; + } + + if (Validations.is3dFile(file.filename)) { + return ; + } + + return ; +}; + +export default React.memo(ObjectPreview); diff --git a/components/core/ObjectPreview/placeholders/3D.js b/components/core/ObjectPreview/placeholders/3D.js new file mode 100644 index 00000000..308cc448 --- /dev/null +++ b/components/core/ObjectPreview/placeholders/3D.js @@ -0,0 +1,175 @@ +import * as React from "react"; + +import { css } from "@emotion/react"; + +export default function Object3DPlaceholder({ ratio = 1, ...props }) { + const STYLES_PLACEHOLDER = React.useMemo( + () => css` + overflow: visible !important; + width: ${(69 / 248) * 100 * ratio}%; + height: ${(76.65 / 248) * 100 * ratio}%; + `, + [ratio] + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/components/core/ObjectPreview/placeholders/Audio.js b/components/core/ObjectPreview/placeholders/Audio.js new file mode 100644 index 00000000..94209845 --- /dev/null +++ b/components/core/ObjectPreview/placeholders/Audio.js @@ -0,0 +1,57 @@ +import * as React from "react"; + +import { css } from "@emotion/react"; + +export default function AudioPlaceholder({ ratio = 1, ...props }) { + const STYLES_PLACEHOLDER = React.useMemo( + () => css` + overflow: visible !important; + width: ${(163 / 248) * 100 * ratio}%; + height: ${(163 / 248) * 100 * ratio}%; + `, + [ratio] + ); + + return ( + + + + + + + + + + + + ); +} diff --git a/components/core/ObjectPreview/placeholders/Code.js b/components/core/ObjectPreview/placeholders/Code.js new file mode 100644 index 00000000..7dcb350f --- /dev/null +++ b/components/core/ObjectPreview/placeholders/Code.js @@ -0,0 +1,64 @@ +import * as React from "react"; + +import { css } from "@emotion/react"; + +export default function CodePlaceholder({ ratio = 1, ...props }) { + const STYLES_PLACEHOLDER = React.useMemo( + () => css` + overflow: visible !important; + width: ${(121 / 248) * 100 * ratio}%; + height: ${(151 / 248) * 100 * ratio}%; + `, + [ratio] + ); + + return ( + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/components/core/ObjectPreview/placeholders/EPUB.js b/components/core/ObjectPreview/placeholders/EPUB.js new file mode 100644 index 00000000..3a778236 --- /dev/null +++ b/components/core/ObjectPreview/placeholders/EPUB.js @@ -0,0 +1,106 @@ +import * as React from "react"; + +import { css } from "@emotion/react"; + +function EpubPlaceholder({ ratio = 1, ...props }) { + const STYLES_PLACEHOLDER = React.useMemo( + () => css` + overflow: visible !important; + width: ${(199 / 248) * 100 * ratio}%; + height: ${(123 / 248) * 100 * ratio}%; + `, + [ratio] + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default EpubPlaceholder; diff --git a/components/core/ObjectPreview/placeholders/File.js b/components/core/ObjectPreview/placeholders/File.js new file mode 100644 index 00000000..b76743e0 --- /dev/null +++ b/components/core/ObjectPreview/placeholders/File.js @@ -0,0 +1,68 @@ +import * as React from "react"; + +import { css } from "@emotion/react"; + +export default function FilePlaceholder({ ratio = 1, ...props }) { + const STYLES_PLACEHOLDER = React.useMemo( + () => css` + overflow: visible !important; + width: ${(121 / 248) * 100 * ratio}%; + height: ${(151 / 248) * 100 * ratio}%; + `, + [ratio] + ); + + return ( + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/components/core/ObjectPreview/placeholders/Keynote.js b/components/core/ObjectPreview/placeholders/Keynote.js new file mode 100644 index 00000000..702133b5 --- /dev/null +++ b/components/core/ObjectPreview/placeholders/Keynote.js @@ -0,0 +1,107 @@ +import * as React from "react"; + +import { css } from "@emotion/react"; + +export default function KeynotePlaceholder({ ratio = 1, ...props }) { + const STYLES_PLACEHOLDER = React.useMemo( + () => css` + overflow: visible !important; + width: ${(183 / 248) * 100 * ratio}%; + height: ${(115 / 248) * 100 * ratio}%; + `, + [ratio] + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/components/core/ObjectPreview/placeholders/PDF.js b/components/core/ObjectPreview/placeholders/PDF.js new file mode 100644 index 00000000..0eb3649c --- /dev/null +++ b/components/core/ObjectPreview/placeholders/PDF.js @@ -0,0 +1,89 @@ +import * as React from "react"; + +import { css } from "@emotion/react"; + +export default function PdfPlaceholder({ ratio = 1, ...props }) { + const STYLES_PLACEHOLDER = React.useMemo( + () => css` + overflow: visible !important; + width: ${(123 / 248) * 100 * ratio}%; + height: ${(151 / 248) * 100 * ratio}%; + `, + [ratio] + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/components/core/ObjectPreview/placeholders/Text.js b/components/core/ObjectPreview/placeholders/Text.js new file mode 100644 index 00000000..2b399413 --- /dev/null +++ b/components/core/ObjectPreview/placeholders/Text.js @@ -0,0 +1,90 @@ +import * as React from "react"; + +import { css } from "@emotion/react"; + +export default function TextPlaceholder({ ratio = 1, ...props }) { + const STYLES_PLACEHOLDER = React.useMemo( + () => css` + overflow: visible !important; + width: ${(123 / 248) * 100 * ratio}%; + height: ${(151 / 248) * 100 * ratio}%; + `, + [ratio] + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/components/core/ObjectPreview/placeholders/Video.js b/components/core/ObjectPreview/placeholders/Video.js new file mode 100644 index 00000000..cc68793d --- /dev/null +++ b/components/core/ObjectPreview/placeholders/Video.js @@ -0,0 +1,56 @@ +import * as React from "react"; +import { css } from "@emotion/react"; + +export default function VideoPlaceholder({ ratio = 1, ...props }) { + const STYLES_PLACEHOLDER = React.useMemo( + () => css` + overflow: visible !important; + width: ${(188 / 248) * 100 * ratio}%; + height: ${(125 / 248) * 100 * ratio}%; + `, + [ratio] + ); + + return ( + + + + + + + + + + + + ); +} diff --git a/components/core/ObjectPreview/placeholders/index.js b/components/core/ObjectPreview/placeholders/index.js new file mode 100644 index 00000000..8295c212 --- /dev/null +++ b/components/core/ObjectPreview/placeholders/index.js @@ -0,0 +1,110 @@ +import * as React from "react"; +import * as Validations from "~/common/validations"; +import * as Utilities from "~/common/utilities"; +import * as Typography from "~/components/system/components/Typography"; + +import { css } from "@emotion/react"; + +import PdfPlaceholder from "./PDF"; +import AudioPlaceholder from "./Audio"; +import CodePlaceholder from "./Code"; +import EpubPlaceholder from "./EPUB"; +import TextPlaceholder from "./Text"; +import KeynotePlaceholder from "./Keynote"; +import Object3DPlaceholder from "./3D"; +import FilePlaceholder from "./File"; +import VideoPlaceholder from "./Video"; + +const STYLES_PLACEHOLDER_CONTAINER = (theme) => css` + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 64px; + width: 86px; + min-width: 64px; + border-radius: 4px; + background-color: ${theme.semantic.bgLight}; +`; + +const STYLES_TAG = (theme) => css` + position: absolute; + left: 50%; + bottom: 8px; + transform: translateX(-50%); + text-transform: uppercase; + border: 1px solid ${theme.system.grayLight5}; + background-color: ${theme.semantic.bgLight}; + padding: 2px 8px; + border-radius: 4px; +`; + +const PlaceholderPremitive = ({ file, ratio }) => { + const { type } = file.data; + + if (type.startsWith("video/")) { + return ; + } + + if (Validations.isPdfType(type)) { + return ; + } + + if (type.startsWith("audio/")) { + return ; + } + + if (type === "application/epub+zip") { + return ; + } + + if (file.filename.endsWith(".key")) { + return ; + } + + if (Validations.isCodeFile(file.filename)) { + return ; + } + + if (Validations.isMarkdown(file.filename, type)) { + return ; + } + + if (Validations.is3dFile(file.filename)) { + return ; + } + + return ; +}; + +export default function Placeholder({ file, containerCss, ratio, showTag }) { + const { type } = file.data; + + const tag = React.useMemo(() => { + if (!showTag) return false; + if (type.startsWith("video/")) return type.split("/")[1]; + if (Validations.isPdfType(type)) return "pdf"; + if (type.startsWith("audio/")) return Utilities.getFileExtension(file.filename) || "audio"; + if (type === "application/epub+zip") return "epub"; + if (file.filename.endsWith(".key")) return "keynote"; + if (Validations.isCodeFile(file.filename)) + return Utilities.getFileExtension(file.filename) || "code"; + if (Validations.isFontFile(file.filename)) + return Utilities.getFileExtension(file.filename) || "font"; + if (Validations.isMarkdown(file.filename, type)) + return Utilities.getFileExtension(file.filename) || "text"; + if (Validations.is3dFile(file.filename)) return "3d"; + return "file"; + }, [file]); + + return ( +
+ {showTag && ( +
+ {tag} +
+ )} + +
+ ); +} diff --git a/components/core/Profile.js b/components/core/Profile.js index 8273524a..83855b5b 100644 --- a/components/core/Profile.js +++ b/components/core/Profile.js @@ -6,6 +6,7 @@ import * as Actions from "~/common/actions"; import * as Utilities from "~/common/utilities"; import * as Events from "~/common/custom-events"; import * as Window from "~/common/window"; +import * as Styles from "~/common/styles"; import { useState } from "react"; import { Link } from "~/components/core/Link"; @@ -20,10 +21,10 @@ import { LoaderSpinner } from "~/components/system/components/Loaders"; import ProcessedText from "~/components/core/ProcessedText"; import SlatePreviewBlocks from "~/components/core/SlatePreviewBlock"; -import CTATransition from "~/components/core/CTATransition"; import DataView from "~/components/core/DataView"; import EmptyState from "~/components/core/EmptyState"; -import ProfilePhoto from "~/components/core/ProfilePhoto"; +import ProfilePhoto from "~/components/core/ProfilePhoto"; +import CollectionPreviewBlock from "~/components/core/CollectionPreviewBlock"; const STYLES_PROFILE_BACKGROUND = css` background-color: ${Constants.system.white}; @@ -249,10 +250,7 @@ function UserEntry({ user, button, onClick, message, checkStatus }) {
- + {isOnline &&
}
@@ -267,6 +265,7 @@ function UserEntry({ user, button, onClick, message, checkStatus }) { function FilesPage({ library, + user, isOwner, isMobile, viewer, @@ -297,6 +296,7 @@ function FilesPage({ {library.length ? ( {slates?.length ? ( - +
+ {slates.map((collection) => ( + + + + ))} +
) : ( {tab === "collections" || fetched ? ( @@ -438,7 +449,7 @@ function PeersPage({ ) : null; return ( - + ); @@ -601,10 +612,7 @@ export default class Profile extends React.Component {
- + {showStatusIndicator && this.checkStatus({ id: user.id }) && (
)} @@ -668,7 +676,9 @@ export default class Profile extends React.Component { style={{ marginTop: 0, marginBottom: 32 }} itemStyle={{ margin: "0px 16px" }} /> - {subtab === "files" ? : null} + {subtab === "files" ? ( + + ) : null} {subtab === "collections" ? ( { + const [selectedIdx, setSelectedIdx] = React.useState(0); + const selectBatchIdx = (idx) => setSelectedIdx(idx); + const selectedBatch = objects[selectedIdx]; + return { selectBatchIdx, selectedBatch, selectedIdx }; +}; + +const STYLES_HIGHLIGHT_BUTTON = (theme) => css` + box-sizing: border-box; + display: block; + padding: 4px 16px 4px 12px; + border: none; + background-color: unset; + div { + width: 8px; + height: 8px; + background-color: ${theme.system.gray}; + border-radius: 50%; + } +`; + +const STYLES_PLACEHOLDER = css` + height: 64px; + min-width: 86px; + width: 86px; +`; + +const ProfilePreviewFile = ({ file, viewer }) => { + const { like, isLiked, likeCount } = useLikeHandler({ file, viewer }); + const { save, isSaved, saveCount } = useSaveHandler({ file, viewer }); + + const title = file.data.name || file.filename; + const { body } = file.data; + return ( +
+ +
+ + {title} + + + {body} + +
+
+ + + {likeCount} + +
+
+ + + {saveCount} + +
+
+
+
+ ); +}; + +const STYLES_CONTAINER = (theme) => css` + border-radius: 8px; + overflow: hidden; + box-shadow: 0 0 0 1px ${theme.semantic.bgGrayLight}; + background-color: ${theme.semantic.bgGrayLight}; +`; + +const STYLES_PROFILE_DESCRIPTION = (theme) => css` + background-color: ${theme.system.white}; + padding: 16px; +`; + +const STYLES_PROFILE_PREVIEW = (theme) => css` + height: 120px; + width: 120px; + border-radius: 8px; + object-fit: cover; + @media (max-width: ${theme.sizes.mobile}px) { + height: 104px; + width: 104px; + } +`; + +const STYLES_FILES_PREVIEWS = css` + padding: 16px; + padding-right: 0px; + height: 176px; +`; + +export default function ProfilePreviewBlock({ onAction, viewer, profile }) { + const filePreviews = React.useMemo(() => { + const files = profile?.objects || []; + let previews = []; + for (let i = 0; i < files.length; i++) { + const batch = []; + if (files[i * 2]) batch.push(files[i * 2]); + if (files[i * 2 + 1]) batch.push(files[i * 2 + 1]); + if (batch.length > 0) previews.push(batch); + if (previews.length === 3 || batch.length < 2) break; + } + return previews; + }, [profile]); + + const { selectBatchIdx, selectedBatch, selectedIdx } = useProfileCarrousel({ + objects: filePreviews, + }); + + const { handleFollow, isFollowing } = useFollowProfileHandler({ + onAction, + viewer, + user: profile, + }); + + const isOwner = viewer?.id === profile.id; + + const nbrOfFiles = profile?.objects?.length || 0; + const doesProfileHaveFiles = nbrOfFiles === 0; + + return ( +
+
+ {`${profile.username}`} +
+
+ {profile.username} + {profile?.data?.body && ( + + {profile.data.body} + + )} +
+
+ + {profile.fileCount} {Strings.pluralize("file", profile.fileCount)} + + + {profile.slateCount} {Strings.pluralize("collection", profile.slateCount)} + +
+ + {!isOwner && + (isFollowing ? ( + { + e.stopPropagation(); + e.preventDefault(); + handleFollow(profile.id); + }} + > + Following + + ) : ( + { + e.stopPropagation(); + e.preventDefault(); + handleFollow(profile.id); + }} + > + Follow + + ))} +
+
+
+
+ {!doesProfileHaveFiles ? ( + selectedBatch.map((file, i) => ( + + {i === 1 && } + + + )) + ) : ( +
+ + No files in this collection +
+ )} +
+ { +
+ {filePreviews.map((preview, i) => ( + + ))} +
+ } +
+
+ ); +} diff --git a/components/core/SlateLinkObject.js b/components/core/SlateLinkObject.js index 8f238e46..54df8848 100644 --- a/components/core/SlateLinkObject.js +++ b/components/core/SlateLinkObject.js @@ -21,7 +21,7 @@ const STYLES_IFRAME = (theme) => css` width: 100%; height: 100%; ${"" /* NOTE(Amine): lightbackground as fallback when html file doesn't have any */} - background-color: ${theme.system.wallLight}; + background-color: ${theme.system.grayLight5Light}; `; export default class SlateLinkObject extends React.Component { diff --git a/components/core/SlateMediaObjectPreview.js b/components/core/SlateMediaObjectPreview.js index 8efa513f..d2a0c20a 100644 --- a/components/core/SlateMediaObjectPreview.js +++ b/components/core/SlateMediaObjectPreview.js @@ -182,7 +182,7 @@ export default class SlateMediaObjectPreview extends React.Component { ); } - let name = (file.data?.name || file.filename).substring(0, this.charCap); + let name = (file.data?.name || file.filename || "").substring(0, this.charCap); let extension = Strings.getFileExtension(file.filename); if (extension && extension.length) { extension = extension.toUpperCase(); diff --git a/components/core/SlatePreviewBlock.js b/components/core/SlatePreviewBlock.js index bb7e8fdf..f4c6e944 100644 --- a/components/core/SlatePreviewBlock.js +++ b/components/core/SlatePreviewBlock.js @@ -125,7 +125,7 @@ const STYLES_CREATE_NEW = css` const STYLES_BLOCK = css` border-radius: 4px; - box-shadow: 0 0 40px 0 ${Constants.shadow.lightSmall}; + box-shadow: ${Constants.shadow.lightSmall}; padding: 24px; font-size: 12px; text-align: left; diff --git a/components/core/TabGroup.js b/components/core/TabGroup.js index 3b421f74..4019eff7 100644 --- a/components/core/TabGroup.js +++ b/components/core/TabGroup.js @@ -107,7 +107,7 @@ export class SecondaryTabGroup extends React.Component { color: disabled || selected ? Constants.system.black : "rgba(0,0,0,0.25)", cursor: disabled ? "auto" : "pointer", ...this.props.itemStyle, - backgroundColor: selected ? Constants.system.white : "transparent", + backgroundColor: selected ? Constants.semantic.bgLight : "transparent", }} // onClick={disabled || selected ? () => {} : () => this.props.onChange(tab.value)} > diff --git a/components/sidebars/SidebarFAQ.js b/components/sidebars/SidebarFAQ.js index abdc71ee..550fc8ff 100644 --- a/components/sidebars/SidebarFAQ.js +++ b/components/sidebars/SidebarFAQ.js @@ -69,13 +69,6 @@ export default class SidebarFAQ extends React.Component {
-
- When will more storage be available? - - 50GB of storage free will be coming to Slate soon with email verification! - -
-
Can I get involved? diff --git a/components/system/components/AspectRatio.js b/components/system/components/AspectRatio.js new file mode 100644 index 00000000..b466b39a --- /dev/null +++ b/components/system/components/AspectRatio.js @@ -0,0 +1,36 @@ +import * as React from "react"; +import { css } from "@emotion/react"; +const STYLES_WRAPPER = css` + position: relative; + width: 100%; + & > * { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + } +`; + +const GET_ASPECT_STYLES = ({ minWidth, maxWith, ratio }) => css` + width: 100%; + height: 0; + padding-bottom: ${ratio * 100}%; + min-width: ${minWidth}; + max-width: ${maxWith}; +`; + +export const AspectRatio = ({ children, minWidth, maxWith, ratio = 4 / 3, css, ...props }) => { + const aspectStyles = React.useMemo(() => { + return GET_ASPECT_STYLES({ minWidth, maxWith, ratio }); + }, [minWidth, maxWith, ratio]); + + //NOTE(amine): enforce single child + const child = React.Children.only(children); + + return ( +
+ {child} +
+ ); +}; diff --git a/components/system/components/Buttons.js b/components/system/components/Buttons.js index 3334fe32..a2fd92fc 100644 --- a/components/system/components/Buttons.js +++ b/components/system/components/Buttons.js @@ -15,9 +15,9 @@ const STYLES_BUTTON = ` display: inline-flex; align-items: center; justify-content: center; - font-size: 14px; - letter-spacing: 0.2px; - font-family: ${Constants.font.semiBold}; + font-size: 0.875rem; + letter-spacing: -0.006px; + font-family: ${Constants.font.medium}; transition: 200ms ease all; overflow-wrap: break-word; user-select: none; diff --git a/components/system/components/Typography.js b/components/system/components/Typography.js index 104520e2..09dcc1f8 100644 --- a/components/system/components/Typography.js +++ b/components/system/components/Typography.js @@ -5,6 +5,60 @@ import * as Strings from "~/common/strings"; import { css } from "@emotion/react"; +const LINK_STYLES = ` + font-family: ${Constants.font.text}; + font-weight: 400; + text-decoration: none; + color: ${Constants.system.grayLight2}; + cursor: pointer; + transition: 200ms ease color; + :hover { + color: ${Constants.system.grayDark6}; + } +`; + +const useColorProp = (color) => + React.useMemo( + () => (theme) => { + if (!color) return; + if (!(color in theme.system) && !(color in theme.semantic)) { + console.warn(`${color} doesn't exist in our design system`); + return; + } + return css({ color: theme.system[color] || theme.semantic[color] }); + }, + [color] + ); + +const truncateElements = (nbrOfLines) => + nbrOfLines && + css` + overflow: hidden; + line-height: 1.5; + word-break: break-word; + text-overflow: ellipsis; + -webkit-line-clamp: ${nbrOfLines}; + display: -webkit-box; + -webkit-box-orient: vertical; + `; + +const STYLES_LINK = css` + ${LINK_STYLES} +`; + +const STYLES_LINK_DARK = css` + color: ${Constants.system.grayLight2}; + :hover { + color: ${Constants.system.white}; + } +`; + +const ANCHOR = ` + a { + ${LINK_STYLES} + } +`; + const onDeepLink = async (object) => { let slug = object.deeplink .split("/") @@ -28,6 +82,7 @@ export const A = ({ href, children, dark }) => { rel: isExternal(href) ? "external nofollow" : "", css: Styles.LINK, children, + // css: dark ? STYLES_LINK_DARK : STYLES_LINK, }; // process all types of Slate links @@ -52,7 +107,7 @@ export const A = ({ href, children, dark }) => { default: { } } - return ; + return {children}; }; // const STYLES_H1 = css` @@ -82,68 +137,124 @@ export const A = ({ href, children, dark }) => { // ${ANCHOR} // `; -export const H1 = (props) => { - return

; +export const H1 = ({ nbrOflines, children, color, ...props }) => { + const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); + const COLOR_STYLES = useColorProp(color); + return ( +

+ {children} +

+ ); }; -export const H2 = (props) => { - return

; +export const H2 = ({ nbrOflines, children, color, ...props }) => { + const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); + const COLOR_STYLES = useColorProp(color); + return ( +

+ {children} +

+ ); }; -export const H3 = (props) => { - return

; +export const H3 = ({ nbrOflines, children, color, ...props }) => { + const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); + const COLOR_STYLES = useColorProp(color); + return ( +

+ {children} +

+ ); }; -export const H4 = (props) => { - return

; +export const H4 = ({ nbrOflines, children, color, ...props }) => { + const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); + const COLOR_STYLES = useColorProp(color); + return ( +

+ {children} +

+ ); }; -export const H5 = (props) => { - return
; +export const H5 = ({ nbrOflines, children, color, ...props }) => { + const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); + const COLOR_STYLES = useColorProp(color); + return ( +
+ {children} +
+ ); }; -// const STYLES_P = css` -// box-sizing: border-box; -// font-family: ${Constants.font.text}; -// font-size: ${Constants.typescale.lvl1}; -// line-height: 1.5; -// overflow-wrap: break-word; - -// strong, -// b { -// font-family: ${Constants.font.semiBold}; -// font-weight: 400; -// } - -// ${ANCHOR} -// `; - -export const P1 = (props) => { - return

; +export const P1 = ({ nbrOflines, children, color, ...props }) => { + const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); + const COLOR_STYLES = useColorProp(color); + return ( +

+ {children} +

+ ); }; -export const P2 = (props) => { - return

; +export const P2 = ({ nbrOflines, children, color, ...props }) => { + const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); + const COLOR_STYLES = useColorProp(color); + return ( +

+ {children} +

+ ); }; -export const P3 = (props) => { - return

; +export const P3 = ({ nbrOflines, children, color, ...props }) => { + const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); + const COLOR_STYLES = useColorProp(color); + return ( +

+ {children} +

+ ); }; -export const C1 = (props) => { - return

; +export const C1 = ({ nbrOflines, children, color, ...props }) => { + const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); + const COLOR_STYLES = useColorProp(color); + return ( +

+ {children} +

+ ); }; -export const C2 = (props) => { - return

; +export const C2 = ({ nbrOflines, children, color, ...props }) => { + const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); + const COLOR_STYLES = useColorProp(color); + return ( +

+ {children} +

+ ); }; -export const C3 = (props) => { - return

; +export const C3 = ({ nbrOflines, children, color, ...props }) => { + const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); + const COLOR_STYLES = useColorProp(color); + return ( +

+ {children} +

+ ); }; -export const B1 = (props) => { - return

; +export const B1 = ({ nbrOflines, children, color, ...props }) => { + const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); + const COLOR_STYLES = useColorProp(color); + return ( +

+ {children} +

+ ); }; const STYLES_UL = css` @@ -167,7 +278,6 @@ export const OL = (props) => { const STYLES_LI = css` box-sizing: border-box; margin-top: 12px; - strong { font-family: ${Constants.font.semiBold}; font-weight: 400; diff --git a/components/system/index.js b/components/system/index.js index 13c86fdc..3a7d24a8 100644 --- a/components/system/index.js +++ b/components/system/index.js @@ -98,6 +98,8 @@ import * as SVG from "~/common/svg"; import ViewSourceLink from "~/components/system/ViewSourceLink"; +import { AspectRatio } from "~/components/system/components/AspectRatio"; + // NOTE(jim): Export everything. export { // NOTE(martina): Actions @@ -164,6 +166,7 @@ export { LI, A, ViewSourceLink, + AspectRatio, // NOTE(jim): Fragments, not meant to be used. Boundary, DescriptionGroup, diff --git a/node_common/constants.js b/node_common/constants.js index 17a76120..0c7c607a 100644 --- a/node_common/constants.js +++ b/node_common/constants.js @@ -17,3 +17,5 @@ export const MIN_ARCHIVE_SIZE_BYTES = 104857600; // NOTE(amine): 15 minutes export const TOKEN_EXPIRATION_TIME = 2 * 60 * 60 * 1000; + +export const userPreviewProperties = ["users.id", "users.data", "users.username"]; diff --git a/node_common/data/index.js b/node_common/data/index.js index 003926f5..30ab4e41 100644 --- a/node_common/data/index.js +++ b/node_common/data/index.js @@ -27,6 +27,7 @@ import deleteFilesByUserId from "~/node_common/data/methods/delete-files-by-user import updateFileById from "~/node_common/data/methods/update-file-by-id"; import updateFilePrivacy from "~/node_common/data/methods/update-file-privacy"; import updateFilesPublic from "~/node_common/data/methods/update-files-public"; +import incrementFileSavecount from "~/node_common/data/methods/increment-file-savecount"; // NOTE(martina): // Like postgres queries @@ -122,6 +123,7 @@ export { updateFileById, updateFilePrivacy, updateFilesPublic, + incrementFileSavecount, // NOTE(martina): Like postgres queries createLike, deleteLikeByFile, diff --git a/node_common/data/methods/create-file.js b/node_common/data/methods/create-file.js index c982cd13..30112afd 100644 --- a/node_common/data/methods/create-file.js +++ b/node_common/data/methods/create-file.js @@ -43,6 +43,7 @@ export default async ({ owner, files, saveCopy = false }) => { } } } + console.log({ activityItems }); if (activityItems.length) { const activityQuery = await DB.insert(activityItems).into("activity"); diff --git a/node_common/data/methods/get-activity.js b/node_common/data/methods/get-activity.js index 6bdb3042..2cb1437c 100644 --- a/node_common/data/methods/get-activity.js +++ b/node_common/data/methods/get-activity.js @@ -1,4 +1,4 @@ -import * as Logging from "~/common/logging"; +import * as Constants from "~/node_common/constants"; import { runQuery } from "~/node_common/data/utilities"; @@ -8,124 +8,153 @@ export default async ({ earliestTimestamp, latestTimestamp, }) => { + let usersFollowing = following || []; + if (!following?.length || following.length < 3) { + usersFollowing.push( + ...[ + "f292c19f-1337-426c-8002-65e128b95096", + "708559e6-cfc9-4b82-8241-3f4e5046028d", + "1195e8bb-3f94-4b47-9deb-b30d6a6f82c4", + "231d5d53-f341-448b-9e92-0b7847c5b667", + "a280fce9-d85f-455a-9523-88f2bacd7d63", + "d909728d-f699-4474-a7d4-584b62907c53", + "20c15eab-ad33-40fd-a434-958a5f1ccb67", + "3cad78ea-01ad-4c92-8983-a97524fb9e35", + ] + ); + } + // const slateFiles = DB.raw( + // "coalesce(json_agg(?? order by ?? asc) filter (where ?? is not null), '[]') as ??", + // ["files", "slate_files.createdAt", "files.id", "objects"] + // ); + const slateFilesFields = ["files", "slate_files.createdAt", "files.id", "objects"]; + const slateFilesQuery = `coalesce(json_agg(?? order by ?? asc) filter (where ?? is not null), '[]') as ??`; + + const slateFields = [ + "slate_table", + "slates.id", + "slates.slatename", + "slates.data", + "slates.ownerId", + "slates.isPublic", + "slates.subscriberCount", + "slates.fileCount", + ...slateFilesFields, + "slates", + "slate_files", + "slate_files.slateId", + "slates.id", + "files", + "files.id", + "slate_files.fileId", + "slates.id", + ]; + const slateQuery = `WITH ?? as (SELECT ??, ??, ??, ??, ??, ??, ??, ${slateFilesQuery} FROM ?? LEFT JOIN ?? on ?? = ?? LEFT JOIN ?? on ?? = ?? GROUP BY ??)`; + + const userFilesFields = ["files", "files.createdAt", "files.id", "objects"]; + const userFilesQuery = `coalesce(json_agg(?? order by ?? asc) filter (where ?? is not null), '[]') as ??`; + const userFields = [ + "user_table", + "users.id", + "users.createdAt", + "users.username", + "users.data", + "users.followerCount", + "users.fileCount", + "users.slateCount", + ...userFilesFields, + "users", + "files", + "users.id", + "files.ownerId", + "users.id", + ]; + const userQuery = `, ?? as (SELECT ??, ??, ??, ??, ??, ??, ??, ${userFilesQuery} FROM ?? LEFT JOIN ?? on ?? = ?? GROUP BY ??)`; + + const fileFields = [ + "files_table", + "files.*", + ...Constants.userPreviewProperties, + "owner", + "files", + "users", + "files.ownerId", + "users.id", + ]; + const fileQuery = `, ?? as (SELECT ??, json_build_object('id', ??, 'data', ??, 'username', ??) as ?? FROM ?? LEFT JOIN ?? on ?? = ??)`; + + const selectFields = [ + ...slateFields, + ...userFields, + ...fileFields, + "activity.id", + "activity.type", + "activity.createdAt", + "slate_table", + "slate", + "files_table", + "file", + "user_table", + "user", + "owners", + "owner", + "activity", + "slate_table", + "slate_table.id", + "activity.slateId", + "user_table", + "user_table.id", + "activity.userId", + "files_table", + "files_table.id", + "activity.fileId", + "user_table", + "owners", + "owners.id", + "activity.ownerId", + ]; + const selectQuery = `${slateQuery} ${userQuery} ${fileQuery} SELECT ??, ??, ??, row_to_json(??) as ??, row_to_json(??) as ??, row_to_json(??) as ??, row_to_json(??) as ?? FROM ?? LEFT JOIN ?? ON ?? = ?? LEFT JOIN ?? ON ?? = ?? LEFT JOIN ?? ON ?? = ?? LEFT JOIN ?? AS ?? ON ?? = ??`; + // const selectQuery = + // "SELECT ??, ??, ??, row_to_json(??) as ??, row_to_json(??) as ??, row_to_json(??) as ??, row_to_json(??) as ?? FROM ?? LEFT JOIN ?? ON ?? = ?? LEFT JOIN ?? ON ?? = ?? LEFT JOIN ?? ON ?? = ?? LEFT JOIN ?? AS ?? ON ?? = ??"; + + const conditionFields = ["activity.ownerId", usersFollowing, "activity.slateId", subscriptions]; return await runQuery({ label: "GET_ACTIVITY_FOR_USER_ID", queryFn: async (DB) => { - const users = () => DB.raw("row_to_json(??) as ??", ["users", "owner"]); - - const slates = () => DB.raw("row_to_json(??) as ??", ["slates", "slate"]); - - const files = () => DB.raw("row_to_json(??) as ??", ["files", "file"]); - let query; if (earliestTimestamp) { //NOTE(martina): for pagination, fetching the "next 100" results let date = new Date(earliestTimestamp); - let s = date.getSeconds(); - if (s < 0) { - s = 60 + s; - } - date.setSeconds(s - 1); - query = await DB.select( - "activity.id", - "activity.type", - "activity.createdAt", - "activity.slateId", - // users(), - // slates(), - files() - ) - .from("activity") - // .join("users", "users.id", "=", "activity.ownerId") - .leftJoin("files", "files.id", "=", "activity.fileId") - // .leftJoin("slates", "slates.id", "=", "activity.slateId") - .whereRaw("?? < ? and ?? = ? and (?? = any(?) or ?? = any(?))", [ - "activity.createdAt", - date.toISOString(), - "activity.type", - "CREATE_SLATE_OBJECT", - "activity.ownerId", - following, - "activity.slateId", - subscriptions, - ]) - // .where("activity.type", "CREATE_SLATE_OBJECT") - // .where("activity.createdAt", "<", date.toISOString()) - // .whereIn("activity.ownerId", following) - // .orWhereIn("activity.slateId", subscriptions) - .orderBy("activity.createdAt", "desc") - .limit(96); + date.setSeconds(date.getSeconds() - 1); + query = await DB.raw( + `${selectQuery} WHERE (?? = ANY(?) OR ?? = ANY(?)) AND ?? < '${date.toISOString()}' ORDER BY ?? DESC LIMIT 100`, + [...selectFields, ...conditionFields, "activity.createdAt", "activity.createdAt"] + ); } else if (latestTimestamp) { //NOTE(martina): for fetching new updates since the last time they loaded let date = new Date(latestTimestamp); date.setSeconds(date.getSeconds() + 1); - query = await DB.select( - "activity.id", - "activity.type", - "activity.createdAt", - "activity.slateId", - // users(), - // slates(), - files() - ) - .from("activity") - // .join("users", "users.id", "=", "activity.ownerId") - .leftJoin("files", "files.id", "=", "activity.fileId") - // .leftJoin("slates", "slates.id", "=", "activity.slateId") - .whereRaw("?? > ? and ?? = ? and (?? = any(?) or ?? = any(?))", [ - "activity.createdAt", - date.toISOString(), - "activity.type", - "CREATE_SLATE_OBJECT", - "activity.ownerId", - following, - "activity.slateId", - subscriptions, - ]) - // .where("activity.createdAt", ">", date.toISOString()) - // .where("activity.type", "CREATE_SLATE_OBJECT") - // .whereIn("activity.ownerId", following) - // .orWhereIn("activity.slateId", subscriptions) - .orderBy("activity.createdAt", "desc") - .limit(96); + query = await DB.raw( + `${selectQuery} WHERE (?? = ANY(?) OR ?? = ANY(?)) AND ?? > '${date.toISOString()}' ORDER BY ?? DESC LIMIT 100`, + [...selectFields, ...conditionFields, "activity.createdAt", "activity.createdAt"] + ); } else { //NOTE(martina): for the first fetch they make, when they have not loaded any explore events yet - query = await DB.select( - "activity.id", - "activity.type", - "activity.createdAt", - "activity.slateId", - // users(), - // slates(), - files() - ) - .from("activity") - // .join("users", "users.id", "=", "activity.ownerId") - .leftJoin("files", "files.id", "=", "activity.fileId") - // .leftJoin("slates", "slates.id", "=", "activity.slateId") - .whereRaw("?? = ? and (?? = any(?) or ?? = any(?))", [ - "activity.type", - "CREATE_SLATE_OBJECT", - "activity.ownerId", - following, - "activity.slateId", - subscriptions, - ]) - // .where("activity.type", "CREATE_SLATE_OBJECT") - // .whereIn("activity.ownerId", following) - // .orWhereIn("activity.slateId", subscriptions) - .orderBy("activity.createdAt", "desc") - .limit(96); + query = await DB.raw( + `${selectQuery} WHERE ?? = ANY(?) OR ?? = ANY(?) ORDER BY ?? DESC LIMIT 100`, + [...selectFields, ...conditionFields, "activity.createdAt"] + ); } - - if (!query) { - return []; + if (query?.rows) { + query = query.rows; + } else { + query = []; } return JSON.parse(JSON.stringify(query)); }, - errorFn: async (e) => { - Logging.error({ + errorFn: async () => { + console.log({ error: true, decorator: "GET_ACTIVITY_FOR_USER_ID", }); diff --git a/node_common/data/methods/get-explore.js b/node_common/data/methods/get-explore.js index 1140111f..e57b88c7 100644 --- a/node_common/data/methods/get-explore.js +++ b/node_common/data/methods/get-explore.js @@ -1,92 +1,133 @@ -import * as Logging from "~/common/logging"; +import * as Constants from "~/node_common/constants"; import { runQuery } from "~/node_common/data/utilities"; export default async ({ earliestTimestamp, latestTimestamp }) => { + const slateFilesFields = ["files", "slate_files.createdAt", "files.id", "objects"]; + const slateFilesQuery = `coalesce(json_agg(?? order by ?? asc) filter (where ?? is not null), '[]') as ??`; + + const slateFields = [ + "slate_table", + "slates.id", + "slates.slatename", + "slates.data", + "slates.ownerId", + "slates.isPublic", + "slates.subscriberCount", + "slates.fileCount", + ...slateFilesFields, + "slates", + "slate_files", + "slate_files.slateId", + "slates.id", + "files", + "files.id", + "slate_files.fileId", + "slates.id", + ]; + const slateQuery = `WITH ?? as (SELECT ??, ??, ??, ??, ??, ??, ??, ${slateFilesQuery} FROM ?? LEFT JOIN ?? on ?? = ?? LEFT JOIN ?? on ?? = ?? GROUP BY ??)`; + + const userFilesFields = ["files", "files.createdAt", "files.id", "objects"]; + const userFilesQuery = `coalesce(json_agg(?? order by ?? asc) filter (where ?? is not null), '[]') as ??`; + const userFields = [ + "user_table", + "users.id", + "users.createdAt", + "users.username", + "users.data", + "users.followerCount", + "users.fileCount", + "users.slateCount", + ...userFilesFields, + "users", + "files", + "users.id", + "files.ownerId", + "users.id", + ]; + const userQuery = `, ?? as (SELECT ??, ??, ??, ??, ??, ??, ??, ${userFilesQuery} FROM ?? LEFT JOIN ?? on ?? = ?? GROUP BY ??)`; + + const fileFields = [ + "files_table", + "files.*", + ...Constants.userPreviewProperties, + "owner", + "files", + "users", + "files.ownerId", + "users.id", + ]; + const fileQuery = `, ?? as (SELECT ??, json_build_object('id', ??, 'data', ??, 'username', ??) as ?? FROM ?? LEFT JOIN ?? on ?? = ??)`; + + const selectFields = [ + ...slateFields, + ...userFields, + ...fileFields, + "activity.id", + "activity.type", + "activity.createdAt", + "slate_table", + "slate", + "files_table", + "file", + "user_table", + "user", + "owners", + "owner", + "activity", + "slate_table", + "slate_table.id", + "activity.slateId", + "user_table", + "user_table.id", + "activity.userId", + "files_table", + "files_table.id", + "activity.fileId", + "user_table", + "owners", + "owners.id", + "activity.ownerId", + ]; + const selectQuery = `${slateQuery} ${userQuery} ${fileQuery} SELECT ??, ??, ??, row_to_json(??) as ??, row_to_json(??) as ??, row_to_json(??) as ??, row_to_json(??) as ?? FROM ?? LEFT JOIN ?? ON ?? = ?? LEFT JOIN ?? ON ?? = ?? LEFT JOIN ?? ON ?? = ?? LEFT JOIN ?? AS ?? ON ?? = ??`; + return await runQuery({ label: "GET_EXPLORE", queryFn: async (DB) => { - const users = () => DB.raw("row_to_json(??) as ??", ["users", "owner"]); - - const slates = () => DB.raw("row_to_json(??) as ??", ["slates", "slate"]); - - const files = () => DB.raw("row_to_json(??) as ??", ["files", "file"]); - let query; if (earliestTimestamp) { //NOTE(martina): for pagination, fetching the "next 100" results let date = new Date(earliestTimestamp); - let s = date.getSeconds(); - if (s < 0) { - s = 60 + s; - } - date.setSeconds(s - 1); - query = await DB.select( - "activity.id", - "activity.type", - "activity.createdAt", - "activity.slateId", - // users(), - // slates(), - files() - ) - .from("activity") - // .join("users", "users.id", "=", "activity.ownerId") - .leftJoin("files", "files.id", "=", "activity.fileId") - // .leftJoin("slates", "slates.id", "=", "activity.slateId") - .where("activity.createdAt", "<", date.toISOString()) - .where("activity.type", "CREATE_SLATE_OBJECT") - .orderBy("activity.createdAt", "desc") - .limit(96); + date.setSeconds(date.getSeconds() - 1); + query = await DB.raw( + `${selectQuery} WHERE ?? < '${date.toISOString()}' ORDER BY ?? DESC LIMIT 100`, + [...selectFields, "activity.createdAt", "activity.createdAt"] + ); } else if (latestTimestamp) { //NOTE(martina): for fetching new updates since the last time they loaded let date = new Date(latestTimestamp); date.setSeconds(date.getSeconds() + 1); - query = await DB.select( - "activity.id", - "activity.type", - "activity.createdAt", - "activity.slateId", - // users(), - // slates(), - files() - ) - .from("activity") - // .join("users", "users.id", "=", "activity.ownerId") - .leftJoin("files", "files.id", "=", "activity.fileId") - // .leftJoin("slates", "slates.id", "=", "activity.slateId") - .where("activity.createdAt", ">", date.toISOString()) - .where("activity.type", "CREATE_SLATE_OBJECT") - .orderBy("activity.createdAt", "desc") - .limit(96); + query = await DB.raw( + `${selectQuery} WHERE ?? > '${date.toISOString()}' ORDER BY ?? DESC LIMIT 100`, + [...selectFields, "activity.createdAt", "activity.createdAt"] + ); } else { //NOTE(martina): for the first fetch they make, when they have not loaded any explore events yet - query = await DB.select( - "activity.id", - "activity.type", + query = await DB.raw(`${selectQuery} ORDER BY ?? DESC LIMIT 100`, [ + ...selectFields, "activity.createdAt", - "activity.slateId", - // users(), - // slates(), - files() - ) - .from("activity") - // .join("users", "users.id", "=", "activity.ownerId") - .leftJoin("files", "files.id", "=", "activity.fileId") - // .leftJoin("slates", "slates.id", "=", "activity.slateId") - .where("activity.type", "CREATE_SLATE_OBJECT") - .orderBy("activity.createdAt", "desc") - .limit(96); + ]); } - - if (!query || query.error) { - return []; + if (query?.rows) { + query = query.rows; + } else { + query = []; } return JSON.parse(JSON.stringify(query)); }, - errorFn: async (e) => { - Logging.error({ + errorFn: async () => { + console.log({ error: true, decorator: "GET_EXPLORE", }); diff --git a/node_common/data/methods/get-likes-by-user-id.js b/node_common/data/methods/get-likes-by-user-id.js index 75f0dce1..4b0e749c 100644 --- a/node_common/data/methods/get-likes-by-user-id.js +++ b/node_common/data/methods/get-likes-by-user-id.js @@ -11,7 +11,7 @@ export default async ({ ownerId }) => { const query = await DB.select(...Serializers.fileProperties) .from("files") .join("likes", "likes.fileId", "=", "files.id") - .where({ "likes.userId": ownerId, "files.isPublic": true }) + .where({ "likes.userId": ownerId }) .groupBy("files.id"); if (!query || query.error) { diff --git a/node_common/data/methods/increment-file-savecount.js b/node_common/data/methods/increment-file-savecount.js new file mode 100644 index 00000000..ba7e0763 --- /dev/null +++ b/node_common/data/methods/increment-file-savecount.js @@ -0,0 +1,23 @@ +import { runQuery } from "~/node_common/data/utilities"; + +//NOTE(martina): remember to include an ownerId for the file +export default async ({ id }) => { + return await runQuery({ + label: "CREATE_FILE", + queryFn: async (DB) => { + const query = await DB.from("files").where("id", id).increment("saveCount", 1); + + if (!query) { + return null; + } + + return JSON.parse(JSON.stringify(query)); + }, + errorFn: async () => { + return { + error: true, + decorator: "CREATE_FILE", + }; + }, + }); +}; diff --git a/node_common/managers/viewer.js b/node_common/managers/viewer.js index 8881fd28..89959a83 100644 --- a/node_common/managers/viewer.js +++ b/node_common/managers/viewer.js @@ -44,7 +44,7 @@ const websocketSend = async (type, data) => { export const hydratePartial = async ( id, - { viewer, slates, keys, library, subscriptions, following, followers } + { viewer, slates, keys, library, subscriptions, following, followers, likes } ) => { if (!id) return; @@ -57,6 +57,8 @@ export const hydratePartial = async ( id, includeFiles: true, }); + update.libraryCids = + user?.library?.reduce((acc, file) => ({ ...acc, [file.cid]: file }), {}) || {}; } else { user = await Data.getUserById({ id, @@ -103,6 +105,11 @@ export const hydratePartial = async ( update.followers = followers; } + if (likes) { + const likes = await Data.getLikesByUserId({ ownerId: id }); + update.likes = likes; + } + websocketSend("UPDATE", update); }; @@ -155,6 +162,9 @@ export const getById = async ({ id }) => { const subscriptions = await Data.getSubscriptionsByUserId({ ownerId: id }); const following = await Data.getFollowingByUserId({ ownerId: id }); const followers = await Data.getFollowersByUserId({ userId: id }); + const likes = await Data.getLikesByUserId({ ownerId: id }); + const libraryCids = + user?.library?.reduce((acc, file) => ({ ...acc, [file.cid]: file }), {}) || {}; let cids = {}; let bytes = 0; @@ -178,7 +188,7 @@ export const getById = async ({ id }) => { } else if (each.data.type && each.data.type.startsWith("application/pdf")) { pdfBytes += each.data.size; } - let coverImage = each.data.coverImage; + let { coverImage } = each.data; if (coverImage && !cids[coverImage.cid]) { imageBytes += coverImage.data.size; cids[coverImage.cid] = true; @@ -223,6 +233,8 @@ export const getById = async ({ id }) => { subscriptions, following, followers, + likes, + libraryCids, }; return viewer; diff --git a/pages/_/index.js b/pages/_/index.js index a4ee53bb..f9423ed4 100644 --- a/pages/_/index.js +++ b/pages/_/index.js @@ -14,7 +14,7 @@ export const getServerSideProps = async ({ query }) => { // }, // }; return { - props: { ...JSON.parse(JSON.stringify(query)) }, + props: JSON.parse(JSON.stringify({ ...query })), }; }; diff --git a/pages/api/data/like.js b/pages/api/data/like.js index 6cf94570..f21efb0b 100644 --- a/pages/api/data/like.js +++ b/pages/api/data/like.js @@ -1,6 +1,7 @@ import * as Data from "~/node_common/data"; import * as Utilities from "~/node_common/utilities"; import * as RequestUtilities from "~/node_common/request-utilities"; +import * as ViewerManager from "~/node_common/managers/viewer"; export default async (req, res) => { const userInfo = await RequestUtilities.checkAuthorizationInternal(req, res); @@ -11,7 +12,9 @@ export default async (req, res) => { return res.status(500).send({ decorator: "SERVER_LIKE_FILE_NO_FILE_PROVIDED", error: true }); } - const existingResponse = await Data.getLikeByFile({ userId: user.id, fileId: file.id }); + const fileId = req.body.data.id; + + const existingResponse = await Data.getLikeByFile({ userId: user.id, fileId }); if (existingResponse && existingResponse.error) { return res.status(500).send({ @@ -26,7 +29,7 @@ export default async (req, res) => { if (existingResponse) { response = await Data.deleteLikeByFile({ userId: user.id, - fileId: file.id, + fileId, }); if (!response) { @@ -37,7 +40,7 @@ export default async (req, res) => { return res.status(500).send({ decorator: "SERVER_UNLIKE_FILE_FAILED", error: true }); } } else { - response = await Data.createLike({ userId: user.id, fileId: file.id }); + response = await Data.createLike({ userId: user.id, fileId }); if (!response) { return res.status(404).send({ decorator: "SERVER_LIKE_FILE_FAILED", error: true }); @@ -48,6 +51,8 @@ export default async (req, res) => { } } + ViewerManager.hydratePartial(id, { likes: true }); + return res.status(200).send({ decorator: "SERVER_LIKE_FILE", data: response, diff --git a/pages/api/data/save-copy.js b/pages/api/data/save-copy.js index 0f181301..0ba4d6b8 100644 --- a/pages/api/data/save-copy.js +++ b/pages/api/data/save-copy.js @@ -17,7 +17,7 @@ export default async (req, res) => { let decorator = "SERVER_SAVE_COPY"; - let { buckets, bucketKey, bucketRoot, bucketName } = await Utilities.getBucketAPIFromUserToken({ + let { buckets, bucketKey, bucketRoot } = await Utilities.getBucketAPIFromUserToken({ user, }); @@ -28,7 +28,7 @@ export default async (req, res) => { }); } - const files = req.body.data.files; + const { files } = req.body.data; if (!files?.length) { return res.status(400).send({ decorator: "SERVER_SAVE_COPY_NO_CIDS", @@ -57,7 +57,7 @@ export default async (req, res) => { filteredFiles = filteredFiles .filter((file) => foundCids.includes(file.cid)) //NOTE(martina): make sure the file being copied exists - .map(({ createdAt, id, likeCount, downloadCount, saveCount, ...keepAttrs }) => { + .map(({ createdAt, likeCount, downloadCount, saveCount, ...keepAttrs }) => { //NOTE(martina): remove the old file's id, ownerId, createdAt, and privacy so new fields can be used return { ...keepAttrs, isPublic: slate?.isPublic || false }; }); @@ -70,15 +70,16 @@ export default async (req, res) => { continue; } + const { id, ...rest } = file; let response = await Utilities.addExistingCIDToData({ buckets, key: bucketKey, path: bucketRoot.path, - cid: file.cid, + cid: rest.cid, }); - + Data.incrementFileSavecount({ id }); if (response && !response.error) { - copiedFiles.push(file); + copiedFiles.push(rest); } } diff --git a/scenes/SceneActivity.js b/scenes/DeprecatedSceneActivity.js similarity index 71% rename from scenes/SceneActivity.js rename to scenes/DeprecatedSceneActivity.js index 82a80ed5..a9965bc4 100644 --- a/scenes/SceneActivity.js +++ b/scenes/DeprecatedSceneActivity.js @@ -5,17 +5,20 @@ 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 Styles from "~/common/styles"; +import * as ActivityUtilities from "~/common/activity-utilities"; import { GlobalCarousel } from "~/components/system/components/GlobalCarousel"; import { css } from "@emotion/react"; -import { TabGroup, PrimaryTabGroup, SecondaryTabGroup } from "~/components/core/TabGroup"; +import { SecondaryTabGroup } from "~/components/core/TabGroup"; import { LoaderSpinner } from "~/components/system/components/Loaders"; import { Link } from "~/components/core/Link"; import EmptyState from "~/components/core/EmptyState"; import ScenePage from "~/components/core/ScenePage"; -import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview"; +import ObjectPreview from "~/components/core/ObjectPreview"; import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper"; +import ActivityObjectPreview from "~/components/core/ActivityObjectPreview"; const STYLES_LOADER = css` display: flex; @@ -30,11 +33,9 @@ const STYLES_IMAGE_BOX = css` position: relative; box-shadow: ${Constants.shadow.lightSmall}; margin: 10px; - :hover { box-shadow: ${Constants.shadow.lightMedium}; } - @media (max-width: ${Constants.sizes.mobile}px) { overflow: hidden; border-radius: 8px; @@ -78,48 +79,23 @@ const STYLES_GRADIENT = css` position: absolute; top: 0px; left: 0px; - @media (max-width: ${Constants.sizes.mobile}px) { overflow: hidden; border-radius: 0px 0px 8px 8px; } `; -const STYLES_ACTIVITY_GRID = css` - margin: -10px; - display: flex; - flex-direction: row; - flex-wrap: wrap; - - @media (max-width: ${Constants.sizes.mobile}px) { - margin-top: 24px; - } -`; - 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.data.type) || !!item.file.data.coverImage; + const { item } = this.props; + return ( -
this.setState({ showText: true })} - onMouseLeave={() => this.setState({ showText: false })} - > - +
+
); } @@ -154,15 +130,7 @@ const ActivityRectangle = ({ item, width, height }) => { let numObjects = item.slate?.objects?.length || 0; return (
- {file ? ( - - ) : null} + {file ? : null}
{ + if (!this.props.viewer) { + return "explore"; + } + return this.props.page.params?.tab || "explore"; + }; + fetchActivityItems = async (update = false) => { if (this.state.loading === "loading") return; let tab = this.getTab(); @@ -282,13 +257,11 @@ export default class SceneActivity extends React.Component { } let newItems = response.data || []; + newItems = ActivityUtilities.processActivity(newItems); if (update) { activity.unshift(...newItems); - this.counter = 0; - activity = this.formatActivity(activity); } else { - newItems = this.formatActivity(newItems); activity.push(...newItems); } @@ -304,52 +277,11 @@ export default class SceneActivity extends React.Component { } }; - formatActivity = (userActivity) => { - let activity = []; - for (let item of userActivity) { - // if (item.slate && !item.slate.isPublic) { - // continue; - // } - if (item.type === "CREATE_SLATE_OBJECT") { - //&& item.slate && item.file - activity.push(item); - } else if (item.type === "CREATE_SLATE" && item.slate) { - activity.push(item); - } - } - return activity; //NOTE(martina): because now it's only things of CREATE_SLATE_OBJECT type, so all square and don't need reordering - //NOTE(martina): rearrange order to always get an even row of 6 squares - //TODO(martina): improve this. will fail if there are no more squares left to "swap" with at the end, and you'll end up wtih an empty space - // let activity = userActivity || []; - // for (let i = 0; i < activity.length; i++) { - // let item = activity[i]; - // if (item.type === "CREATE_SLATE") { - // this.counter += 2; - // } else if (item.type === "CREATE_SLATE_OBJECT") { - // this.counter += 1; - // } - // if (this.counter === 6) { - // this.counter = 0; - // } else if (this.counter > 6) { - // let j = i - 1; - // while (activity[j].type !== "CREATE_SLATE_OBJECT") { - // j -= 1; - // } - // let temp = activity[j]; - // activity[j] = activity[i]; - // activity[i] = temp; - // this.counter = 0; - // i -= 1; - // } - // } - // return activity; - }; - calculateWidth = () => { let windowWidth = window.innerWidth; let imageSize; if (windowWidth < Constants.sizes.mobile) { - imageSize = windowWidth - 2 * 24; //(windowWidth - 2 * 24 - 20) / 2; + imageSize = windowWidth - 2 * 24; } else { imageSize = (windowWidth - 2 * 56 - 5 * 20) / 6; } @@ -364,7 +296,6 @@ export default class SceneActivity extends React.Component { render() { let tab = this.getTab(); let activity; - if (this.props.viewer) { activity = tab === "activity" ? this.props.viewer?.activity || [] : this.props.viewer?.explore || []; @@ -372,23 +303,12 @@ export default class SceneActivity extends React.Component { activity = this.state.explore || []; } - let items = activity - .filter((item) => item.type === "CREATE_SLATE_OBJECT") - .map((item) => { - return { - ...item.file, - slateId: item.slateId, - // slate: item.slate, - // owner: item.owner?.username, - }; - }); - return ( - + {this.props.viewer && ( {activity.length ? (
-
+
{activity.map((item, i) => { if (item.type === "CREATE_SLATE") { return ( @@ -426,25 +345,10 @@ export default class SceneActivity extends React.Component { redirect key={i} disabled={this.props.isMobile ? false : true} - // params={ - // this.props.isMobile - // ? null - // : { ...this.props.page.params, cid: item.file.cid } - // } href={`/$/slate/${item.slateId}`} onAction={this.props.onAction} onClick={() => this.setState({ index: i })} > - {/* - this.props.onAction({ - type: "NAVIGATE", - value: "NAV_SLATE", - data: item.slate, - }) - } - > */} - {/* */} ); } else if (item.type === "CREATE_SLATE_OBJECT") { @@ -463,23 +366,9 @@ export default class SceneActivity extends React.Component { redirect key={i} disabled={this.props.isMobile ? false : true} - // params={ - // this.props.isMobile - // ? null - // : { ...this.props.page.params, cid: item.file.cid } - // } href={`/$/slate/${item.slateId}?cid=${item.file.cid}`} onAction={this.props.onAction} - onClick={() => this.setState({ index: i })} - // onClick={ - // this.props.isMobile - // ? () => {} - // : () => - // Events.dispatchCustomEvent({ - // name: "slate-global-open-carousel", - // detail: { index: this.getItemIndexById(items, item) }, - // }) - // } + onClick={() => this.setState({ carouselIndex: i })} > { + const currentItems = viewer?.explore?.items || state?.explore?.items || []; + const response = await ActivityUtilities.fetchExploreItems({ currentItems, update }); + if (Events.hasError(response)) return; + + const newItems = response.data; + + const currentFeed = viewer?.explore?.feed || state?.explore?.feed || []; + const newFeed = await ActivityUtilities.processActivity(newItems); + + const newState = { + items: currentItems.concat(newItems), + feed: currentFeed.concat(newFeed), + shouldFetchMore: newItems.length > 0, + }; + + if (viewer) { + onAction({ type: "UPDATE_VIEWER", viewer: { explore: newState } }); + return; + } + + setState((prev) => ({ ...prev, explore: newState })); +}; + +const updateActivityFeed = async ({ viewer, onAction, update }) => { + const currentItems = viewer?.activity?.items || []; + const response = await ActivityUtilities.fetchActivityItems({ currentItems, viewer, update }); + if (Events.hasError(response)) return; + + const newItems = response.data; + + const currentFeed = viewer?.activity?.feed || []; + const newFeed = ActivityUtilities.processActivity(newItems); + + onAction({ + type: "UPDATE_VIEWER", + viewer: { + activity: { + feed: currentFeed.concat(newFeed), + items: currentItems.concat(newItems), + shouldFetchMore: newItems.length > 0, + }, + }, + }); +}; + +// NOTE(amine): get the state for the selected tab. +const getState = (viewer, state, tab) => { + if (!viewer) return state.explore || []; + + if (tab === "explore") { + return viewer?.explore || {}; + } + return viewer?.activity || {}; +}; + +const getTab = (page, viewer) => { + if (page.params?.tab) return page.params?.tab; + + if (viewer?.subscriptions?.length || viewer?.following?.length) { + return "activity"; + } + return "explore"; +}; + +export function useActivity({ page, viewer, onAction }) { + const [state, setState] = React.useState({ + explore: { + feed: [], + items: [], + shouldFetchMore: true, + }, + loading: { + explore: false, + activity: false, + }, + }); + + const tab = getTab(page, viewer); + + const updateFeed = React.useCallback( + async (update) => { + const currentState = getState(viewer, state, tab); + const { shouldFetchMore } = currentState || {}; + if (typeof shouldFetchMore === "boolean" && !shouldFetchMore) { + return; + } + + if (state.loading[tab]) return; + setState((prev) => ({ ...prev, loading: { ...prev.loading, [tab]: true } })); + if (viewer && tab === "activity") { + await updateActivityFeed({ viewer, onAction, update }); + } else { + await updateExploreFeed({ viewer, onAction, state, setState, update }); + } + setState((prev) => ({ ...prev, loading: { ...prev.loading, [tab]: false } })); + }, + [tab, onAction, state, viewer] + ); + + const { feed = [] } = getState(viewer, state, tab); + React.useEffect(() => { + if (feed && feed?.length !== 0) return; + updateFeed(true); + }, [tab]); + + return { updateFeed, feed, tab, isLoading: state.loading }; +} diff --git a/scenes/SceneActivity/index.js b/scenes/SceneActivity/index.js new file mode 100644 index 00000000..c56dfa05 --- /dev/null +++ b/scenes/SceneActivity/index.js @@ -0,0 +1,86 @@ +import * as React from "react"; +import * as Constants from "~/common/constants"; +import * as Styles from "~/common/styles"; + +import { css } from "@emotion/react"; +import { SecondaryTabGroup } from "~/components/core/TabGroup"; +import { LoaderSpinner } from "~/components/system/components/Loaders"; +import { useIntersection } from "common/hooks"; +import { useActivity } from "./hooks"; + +import ScenePage from "~/components/core/ScenePage"; +import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper"; +import ActivityGroup from "~/components/core/ActivityGroup"; + +const STYLES_GROUPS_CONTAINER = css` + margin-top: 32px; + & > * + * { + margin-top: 32px; + } +`; + +const STYLES_LOADING_CONTAINER = css` + height: 48px; + margin-top: 32px; + ${Styles.CONTAINER_CENTERED} +`; + +const STYLES_LOADER = css` + display: flex; + align-items: center; + justify-content: center; + height: calc(100vh - 400px); + width: 100%; +`; + +export default function SceneActivity({ page, viewer, external, onAction }) { + const { feed, tab, isLoading, updateFeed } = useActivity({ + page, + viewer, + onAction, + }); + + const divRef = React.useRef(); + useIntersection({ + ref: divRef, + onIntersect: () => { + if (feed?.length === 0 || isLoading[tab]) return; + updateFeed(); + }, + }); + + return ( + + + {viewer && ( + + )} +
+ {feed?.map((group) => ( + + ))} +
+
+ {isLoading[tab] && } +
+
+
+ ); +} diff --git a/scenes/SceneFilesFolder.js b/scenes/SceneFilesFolder.js index 5c6e222c..f621db7f 100644 --- a/scenes/SceneFilesFolder.js +++ b/scenes/SceneFilesFolder.js @@ -469,6 +469,7 @@ export default class SceneFilesFolder extends React.Component { key="scene-files-folder" onAction={this.props.onAction} viewer={this.props.viewer} + user={this.props.viewer} items={files} view={tab} resources={this.props.resources} diff --git a/scenes/SceneSlates.js b/scenes/SceneSlates.js index 744818e8..4a4cbf40 100644 --- a/scenes/SceneSlates.js +++ b/scenes/SceneSlates.js @@ -2,15 +2,17 @@ import * as React from "react"; import * as SVG from "~/common/svg"; import * as Events from "~/common/custom-events"; import * as Constants from "~/common/constants"; +import * as Styles from "~/common/styles"; import { css } from "@emotion/react"; import { TabGroup, PrimaryTabGroup, SecondaryTabGroup } from "~/components/core/TabGroup"; import { ButtonSecondary } from "~/components/system/components/Buttons"; import { FileTypeGroup } from "~/components/core/FileTypeIcon"; +import { Link } from "~/components/core/Link"; import ScenePage from "~/components/core/ScenePage"; import ScenePageHeader from "~/components/core/ScenePageHeader"; -import SlatePreviewBlocks from "~/components/core/SlatePreviewBlock"; +import CollectionPreviewBlock from "~/components/core/CollectionPreviewBlock"; import SquareButtonGray from "~/components/core/SquareButtonGray"; import EmptyState from "~/components/core/EmptyState"; import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper"; @@ -42,57 +44,78 @@ export default class SceneSlates extends React.Component { url={`${Constants.hostname}${this.props.page.pathname}`} > -
- - - - -
- {tab === "collections" ? ( - this.props.viewer.slates?.length ? ( - +
+ - ) : ( - - -
- Use collections to create mood boards, share files, and organize research. + + + +
+ {tab === "collections" ? ( + this.props.viewer.slates?.length ? ( +
+ {this.props.viewer.slates.map((slate) => ( + + + + ))}
- - Create collection - -
- ) - ) : null} + ) : ( + + +
+ Use collections to create mood boards, share files, and organize research. +
+ + Create collection + +
+ ) + ) : null} - {tab === "subscribed" ? ( - subscriptions && subscriptions.length ? ( - - ) : ( - - You can follow any public collections on the network. - - Browse collections - - - ) - ) : null} + {tab === "subscribed" ? ( + subscriptions && subscriptions.length ? ( +
+ {subscriptions.map((slate) => ( + + + + ))} +
+ ) : ( + + You can follow any public collections on the network. + + Browse collections + + + ) + ) : null} +
); diff --git a/server.js b/server.js index 75dceec5..13f153e8 100644 --- a/server.js +++ b/server.js @@ -176,7 +176,7 @@ app.prepare().then(async () => { // if (viewer) { // return res.redirect("/_/data"); // } else { - // return res.redirect("/_/explore"); + // return res.redirect("/_/activity"); // } // let page = NavigationData.getById(null, viewer); From dadaca6eab4e47bfacdff0e2fb12c6ec03dd22e3 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Wed, 14 Jul 2021 11:14:33 +0100 Subject: [PATCH 02/27] feat(ObjectPreview): fix typo premitive-> primitive --- components/core/ObjectPreview/3dObjectPreview.js | 6 +++--- components/core/ObjectPreview/AudioObjectPreview.js | 6 +++--- components/core/ObjectPreview/CodeObjectPreview.js | 6 +++--- components/core/ObjectPreview/DefaultObjectPreview.js | 6 +++--- components/core/ObjectPreview/EpubObjectPreview.js | 6 +++--- components/core/ObjectPreview/FontObjectPreview.js | 6 +++--- components/core/ObjectPreview/ImageObjectPreview.js | 6 +++--- components/core/ObjectPreview/KeynoteObjectPreview.js | 6 +++--- ...{ObjectPreviewPremitive.js => ObjectPreviewPrimitive.js} | 0 components/core/ObjectPreview/PdfObjectPreview.js | 6 +++--- components/core/ObjectPreview/TextObjectPreview.js | 6 +++--- components/core/ObjectPreview/VideoObjectPreview.js | 6 +++--- components/core/ObjectPreview/placeholders/index.js | 4 ++-- 13 files changed, 35 insertions(+), 35 deletions(-) rename components/core/ObjectPreview/{ObjectPreviewPremitive.js => ObjectPreviewPrimitive.js} (100%) diff --git a/components/core/ObjectPreview/3dObjectPreview.js b/components/core/ObjectPreview/3dObjectPreview.js index 675c2709..9fca4233 100644 --- a/components/core/ObjectPreview/3dObjectPreview.js +++ b/components/core/ObjectPreview/3dObjectPreview.js @@ -6,7 +6,7 @@ import * as Styles from "~/common/styles"; import { P3 } from "~/components/system"; import { css } from "@emotion/react"; -import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; import ObjectPlaceholder from "./placeholders/3D"; const STYLES_CONTAINER = css` @@ -26,13 +26,13 @@ const STYLES_TAG = (theme) => css` export default function Object3DPreview(props) { return ( - +
3D
-
+ ); } diff --git a/components/core/ObjectPreview/AudioObjectPreview.js b/components/core/ObjectPreview/AudioObjectPreview.js index b31fe52f..55ce809d 100644 --- a/components/core/ObjectPreview/AudioObjectPreview.js +++ b/components/core/ObjectPreview/AudioObjectPreview.js @@ -8,7 +8,7 @@ import { P3 } from "~/components/system"; import { css } from "@emotion/react"; -import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; import AudioPlaceholder from "./placeholders/Audio"; const STYLES_CONTAINER = css` @@ -29,13 +29,13 @@ const STYLES_TAG = (theme) => css` export default function AudioObjectPreview({ file, ...props }) { const tag = Utilities.getFileExtension(file.filename) || "audio"; return ( - +
{tag}
-
+ ); } diff --git a/components/core/ObjectPreview/CodeObjectPreview.js b/components/core/ObjectPreview/CodeObjectPreview.js index ff18fc27..fa6b421b 100644 --- a/components/core/ObjectPreview/CodeObjectPreview.js +++ b/components/core/ObjectPreview/CodeObjectPreview.js @@ -7,7 +7,7 @@ import * as Utilities from "~/common/utilities"; import { P3 } from "~/components/system"; import { css } from "@emotion/react"; -import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; import CodePlaceholder from "./placeholders/Code"; const STYLES_CONTAINER = css` @@ -28,13 +28,13 @@ const STYLES_TAG = (theme) => css` export default function CodeObjectPreview({ file, ...props }) { const tag = Utilities.getFileExtension(file.filename) || "code"; return ( - +
{tag}
-
+ ); } diff --git a/components/core/ObjectPreview/DefaultObjectPreview.js b/components/core/ObjectPreview/DefaultObjectPreview.js index 7632751f..3a12968a 100644 --- a/components/core/ObjectPreview/DefaultObjectPreview.js +++ b/components/core/ObjectPreview/DefaultObjectPreview.js @@ -4,7 +4,7 @@ import * as Styles from "~/common/styles"; import { P3 } from "~/components/system"; import { css } from "@emotion/react"; -import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; import FilePlaceholder from "./placeholders/File"; const STYLES_CONTAINER = css` @@ -24,13 +24,13 @@ const STYLES_TAG = (theme) => css` export default function DefaultObjectPreview(props) { return ( - +
FILE
-
+ ); } diff --git a/components/core/ObjectPreview/EpubObjectPreview.js b/components/core/ObjectPreview/EpubObjectPreview.js index c935378d..72d1218e 100644 --- a/components/core/ObjectPreview/EpubObjectPreview.js +++ b/components/core/ObjectPreview/EpubObjectPreview.js @@ -6,7 +6,7 @@ import * as Styles from "~/common/styles"; import { P3 } from "~/components/system"; import { css } from "@emotion/react"; -import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; import EpubPlaceholder from "./placeholders/EPUB"; const STYLES_CONTAINER = css` @@ -26,13 +26,13 @@ const STYLES_TAG = (theme) => css` export default function EpubObjectPreview(props) { return ( - +
EPUB
-
+ ); } diff --git a/components/core/ObjectPreview/FontObjectPreview.js b/components/core/ObjectPreview/FontObjectPreview.js index 0c03ba89..94418d73 100644 --- a/components/core/ObjectPreview/FontObjectPreview.js +++ b/components/core/ObjectPreview/FontObjectPreview.js @@ -6,7 +6,7 @@ import * as Utilities from "~/common/utilities"; import { css } from "@emotion/react"; -import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; import { useFont } from "~/components/core/FontFrame/hooks"; const STYLES_TEXT_PREVIEW = (theme) => css` @@ -29,12 +29,12 @@ export default function FontObjectPreview({ file, ...props }) { const tag = Utilities.getFileExtension(file.filename) || "font"; return ( - +
Aa
-
+ ); } diff --git a/components/core/ObjectPreview/ImageObjectPreview.js b/components/core/ObjectPreview/ImageObjectPreview.js index 5692dacc..5a96b6ee 100644 --- a/components/core/ObjectPreview/ImageObjectPreview.js +++ b/components/core/ObjectPreview/ImageObjectPreview.js @@ -8,7 +8,7 @@ import { Blurhash } from "react-blurhash"; import { isBlurhashValid } from "blurhash"; import { css } from "@emotion/react"; -import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; const STYLES_PLACEHOLDER_ABSOLUTE = css` position: absolute; @@ -71,7 +71,7 @@ export default function ImageObjectPreview({ url, file, ...props }) { const imageUrl = coverImage ? Strings.getURLfromCID(coverImage?.cid) : url; return ( - +
{isInView && ( @@ -86,6 +86,6 @@ export default function ImageObjectPreview({ url, file, ...props }) { )} {shouldShowPlaceholder && }
-
+ ); } diff --git a/components/core/ObjectPreview/KeynoteObjectPreview.js b/components/core/ObjectPreview/KeynoteObjectPreview.js index aaad4fa2..b89cb5c7 100644 --- a/components/core/ObjectPreview/KeynoteObjectPreview.js +++ b/components/core/ObjectPreview/KeynoteObjectPreview.js @@ -6,7 +6,7 @@ import * as Styles from "~/common/styles"; import { P3 } from "~/components/system"; import { css } from "@emotion/react"; -import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; import KeynotePlaceholder from "./placeholders/Keynote"; const STYLES_CONTAINER = css` @@ -26,13 +26,13 @@ const STYLES_TAG = (theme) => css` export default function KeynoteObjectPreview(props) { return ( - +
KEYNOTE
-
+ ); } diff --git a/components/core/ObjectPreview/ObjectPreviewPremitive.js b/components/core/ObjectPreview/ObjectPreviewPrimitive.js similarity index 100% rename from components/core/ObjectPreview/ObjectPreviewPremitive.js rename to components/core/ObjectPreview/ObjectPreviewPrimitive.js diff --git a/components/core/ObjectPreview/PdfObjectPreview.js b/components/core/ObjectPreview/PdfObjectPreview.js index 27a65888..2ae20fdf 100644 --- a/components/core/ObjectPreview/PdfObjectPreview.js +++ b/components/core/ObjectPreview/PdfObjectPreview.js @@ -7,7 +7,7 @@ import { P3 } from "~/components/system"; import { css } from "@emotion/react"; import PdfPlaceholder from "./placeholders/PDF"; -import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; const STYLES_CONTAINER = css` position: relative; @@ -27,13 +27,13 @@ const STYLES_TAG = (theme) => css` export default function PDFObjectPreview(props) { return ( - +
PDF
-
+ ); } diff --git a/components/core/ObjectPreview/TextObjectPreview.js b/components/core/ObjectPreview/TextObjectPreview.js index b391db01..e85aa374 100644 --- a/components/core/ObjectPreview/TextObjectPreview.js +++ b/components/core/ObjectPreview/TextObjectPreview.js @@ -7,7 +7,7 @@ import * as Utilities from "~/common/utilities"; import { P3 } from "~/components/system"; import { css } from "@emotion/react"; -import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; import TextPlaceholder from "./placeholders/Text"; const STYLES_CONTAINER = css` @@ -56,7 +56,7 @@ export default function TextObjectPreview({ url, file, ...props }) { const tag = Utilities.getFileExtension(file.filename) || "text"; return ( - +
{error ? ( <> @@ -71,6 +71,6 @@ export default function TextObjectPreview({ url, file, ...props }) {
)}
- + ); } diff --git a/components/core/ObjectPreview/VideoObjectPreview.js b/components/core/ObjectPreview/VideoObjectPreview.js index 0f1d4aff..38905f66 100644 --- a/components/core/ObjectPreview/VideoObjectPreview.js +++ b/components/core/ObjectPreview/VideoObjectPreview.js @@ -6,7 +6,7 @@ import * as Styles from "~/common/styles"; import { P3 } from "~/components/system"; import { css } from "@emotion/react"; -import ObjectPreviewPremitive from "./ObjectPreviewPremitive"; +import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; import VideoPlaceholder from "./placeholders/Video"; const STYLES_CONTAINER = css` @@ -28,13 +28,13 @@ export default function VideoObjectPreview({ file, ...props }) { const { type } = file.data; const tag = type.split("/")[1]; return ( - +
{tag}
-
+ ); } diff --git a/components/core/ObjectPreview/placeholders/index.js b/components/core/ObjectPreview/placeholders/index.js index 8295c212..ee020824 100644 --- a/components/core/ObjectPreview/placeholders/index.js +++ b/components/core/ObjectPreview/placeholders/index.js @@ -39,7 +39,7 @@ const STYLES_TAG = (theme) => css` border-radius: 4px; `; -const PlaceholderPremitive = ({ file, ratio }) => { +const PlaceholderPrimitive = ({ file, ratio }) => { const { type } = file.data; if (type.startsWith("video/")) { @@ -104,7 +104,7 @@ export default function Placeholder({ file, containerCss, ratio, showTag }) { {tag}
)} - +
); } From d1a8ea2172834302ea989c8787a9fbf9cbb5b4f4 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Wed, 14 Jul 2021 11:14:52 +0100 Subject: [PATCH 03/27] fix(TextObjectPreview): correct text preview instead of html --- components/core/ObjectPreview/index.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/components/core/ObjectPreview/index.js b/components/core/ObjectPreview/index.js index 85f9a0d8..1fd37ba9 100644 --- a/components/core/ObjectPreview/index.js +++ b/components/core/ObjectPreview/index.js @@ -15,11 +15,9 @@ import CodeObjectPreview from "./CodeObjectPreview"; import FontObjectPreview from "./FontObjectPreview"; const ObjectPreview = ({ file, ...props }) => { - const { type, coverImage } = file.data; + const { type } = file.data; - const url = Validations.isPreviewableImage(type) - ? Strings.getURLfromCID(file.cid) - : Strings.getURLfromCID(coverImage?.cid); + const url = Strings.getURLfromCID(file.cid); if (Validations.isPreviewableImage(type)) { return ; From 2570a2b126bd39b07eedb8589975d02667f130ba Mon Sep 17 00:00:00 2001 From: Aminejv Date: Wed, 14 Jul 2021 11:16:52 +0100 Subject: [PATCH 04/27] feat(Activity): show the correct number of cards per row --- common/constants.js | 21 ++++++++ common/styles.js | 32 ++++++------ .../components/ActivityCollectionGroup.js | 13 ++++- .../components/ActivityFileGroup.js | 23 +++------ .../components/ActivityProfileGroup.js | 13 ++++- components/core/ActivityGroup/index.js | 28 ++++++++-- components/system/ThemeProvider.js | 1 + scenes/SceneActivity/index.js | 51 +++++++++++++++++++ 8 files changed, 142 insertions(+), 40 deletions(-) diff --git a/common/constants.js b/common/constants.js index 6843f56f..6c1b86f1 100644 --- a/common/constants.js +++ b/common/constants.js @@ -195,3 +195,24 @@ export const filetypes = { }; export const linkPreviewSizeLimit = 5000000; //NOTE(martina): 5mb limit for twitter preview images + +// NOTE(amine): used to calculate how many cards will fit into a row in sceneActivity +export const grids = { + activity: { + profileInfo: { + width: 260, + }, + }, + object: { + desktop: { width: 248, rowGap: 16 }, + mobile: { width: 166, rowGap: 8 }, + }, + collection: { + desktop: { width: 432, rowGap: 16 }, + mobile: { width: 300, rowGap: 8 }, + }, + profile: { + desktop: { width: 432, rowGap: 16 }, + mobile: { width: 344, rowGap: 8 }, + }, +}; diff --git a/common/styles.js b/common/styles.js index c380bcca..0b7dac06 100644 --- a/common/styles.js +++ b/common/styles.js @@ -235,14 +235,14 @@ export const IMAGE_FIT = css` `; /* COMMON GRIDS */ -export const OBJECTS_PREVIEW_GRID = css` +export const OBJECTS_PREVIEW_GRID = (theme) => css` display: grid; - grid-template-columns: repeat(auto-fill, minmax(248px, 1fr)); - grid-gap: 24px 16px; + grid-template-columns: repeat(auto-fill, minmax(${theme.grids.object.desktop.width}px, 1fr)); + grid-gap: 24px ${theme.grids.object.desktop.rowGap}px; @media (max-width: ${Constants.sizes.mobile}px) { - grid-gap: 20px 8px; - grid-template-columns: repeat(auto-fill, minmax(166px, 1fr)); + grid-gap: 20px ${theme.grids.object.mobile.rowGap}px; + grid-template-columns: repeat(auto-fill, minmax(${theme.grids.object.mobile.width}px, 1fr)); } `; @@ -254,26 +254,24 @@ export const BUTTON_RESET = css` ${HOVERABLE} `; -export const COLLECTIONS_PREVIEW_GRID = css` +export const COLLECTIONS_PREVIEW_GRID = (theme) => css` display: grid; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(432px, 1fr)); - grid-gap: 24px 16px; + grid-template-columns: repeat(auto-fill, minmax(${theme.grids.collection.desktop.width}px, 1fr)); + grid-gap: 24px ${theme.grids.collection.desktop.rowGap}px; @media (max-width: ${Constants.sizes.desktop}px) { - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - grid-gap: 20px 8px; + grid-gap: 20px ${theme.grids.collection.mobile.rowGap}px; + grid-template-columns: repeat(auto-fill, minmax(${theme.grids.collection.mobile.width}px, 1fr)); } `; -export const PROFILE_PREVIEW_GRID = css` +export const PROFILE_PREVIEW_GRID = (theme) => css` display: grid; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(432px, 1fr)); - grid-gap: 24px 16px; + grid-template-columns: repeat(auto-fill, minmax(${theme.grids.profile.desktop.width}px, 1fr)); + grid-gap: 24px ${theme.grids.profile.desktop.rowGap}px; @media (max-width: ${Constants.sizes.mobile}px) { - grid-gap: 20px 8px; - grid-template-columns: repeat(auto-fill, minmax(344px, 1fr)); + grid-gap: 20px ${theme.grids.profile.mobile.rowGap}px; + grid-template-columns: repeat(auto-fill, minmax(${theme.grids.profile.mobile.width}px, 1fr)); } `; diff --git a/components/core/ActivityGroup/components/ActivityCollectionGroup.js b/components/core/ActivityGroup/components/ActivityCollectionGroup.js index 39d110f8..ac0c8c2d 100644 --- a/components/core/ActivityGroup/components/ActivityCollectionGroup.js +++ b/components/core/ActivityGroup/components/ActivityCollectionGroup.js @@ -29,13 +29,22 @@ const STYLES_VIEWMORE_CONTAINER = (theme) => css` } `; -export default function ActivityCollectionGroup({ onAction, viewer, group, ...props }) { +export default function ActivityCollectionGroup({ + onAction, + viewer, + group, + nbrOfCollectionsPerRow = 2, + ...props +}) { const { owner, slate, type, createdAt } = group; const { elements, restElements } = React.useMemo(() => { if (!Array.isArray(slate)) { return { elements: [slate] }; } - return { elements: slate.slice(0, 2), restElements: slate.slice(2) }; + return { + elements: slate.slice(0, nbrOfCollectionsPerRow), + restElements: slate.slice(nbrOfCollectionsPerRow), + }; }, [slate]); const [showMore, setShowMore] = React.useState(false); diff --git a/components/core/ActivityGroup/components/ActivityFileGroup.js b/components/core/ActivityGroup/components/ActivityFileGroup.js index ca5e5c29..b21942d2 100644 --- a/components/core/ActivityGroup/components/ActivityFileGroup.js +++ b/components/core/ActivityGroup/components/ActivityFileGroup.js @@ -1,6 +1,7 @@ import * as React from "react"; import * as Strings from "~/common/strings"; import * as Utilities from "~/common/utilities"; +import * as Styles from "~/common/styles"; import { css } from "@emotion/react"; import { motion } from "framer-motion"; @@ -8,20 +9,9 @@ import { ViewMoreContent, ProfileInfo } from "~/components/core/ActivityGroup/co import ObjectPreview from "~/components/core/ObjectPreview"; -const STYLES_OBJECT_GRID = (theme) => css` - display: grid; - grid-template-columns: repeat(auto-fill, minmax(248px, 1fr)); - grid-gap: 20px 12px; - - @media (max-width: ${theme.sizes.mobile}px) { - grid-row-gap: 24px; - grid-template-columns: repeat(auto-fill, minmax(169px, 1fr)); - } -`; - const STYLES_GROUP_GRID = (theme) => css` display: grid; - grid-template-columns: 260px 1fr; + grid-template-columns: ${theme.grids.activity.profileInfo.width}px 1fr; grid-row-gap: 32px; border-bottom: 1px solid ${theme.semantic.bgLight}; padding-bottom: 24px; @@ -38,14 +28,17 @@ const STYLES_VIEWMORE_CONTAINER = (theme) => css` } `; -export default function ActivityFileGroup({ viewer, group, onAction }) { +export default function ActivityFileGroup({ viewer, group, onAction, nbrOfObjectsPerRow = 4 }) { const { file, owner, slate, type, createdAt } = group; const { elements, restElements } = React.useMemo(() => { if (!Array.isArray(file)) { return { elements: [file] }; } - return { elements: file.slice(0, 4), restElements: file.slice(4) }; + return { + elements: file.slice(0, nbrOfObjectsPerRow), + restElements: file.slice(nbrOfObjectsPerRow), + }; }, [file]); const [showMore, setShowMore] = React.useState(false); @@ -78,7 +71,7 @@ export default function ActivityFileGroup({ viewer, group, onAction }) { onAction={onAction} />
-
+
{elements.map((file) => ( ))} diff --git a/components/core/ActivityGroup/components/ActivityProfileGroup.js b/components/core/ActivityGroup/components/ActivityProfileGroup.js index a070cd2a..12631173 100644 --- a/components/core/ActivityGroup/components/ActivityProfileGroup.js +++ b/components/core/ActivityGroup/components/ActivityProfileGroup.js @@ -30,14 +30,23 @@ const STYLES_VIEWMORE_CONTAINER = (theme) => css` } `; -export default function ActivityProfileGroup({ viewer, external, group, onAction }) { +export default function ActivityProfileGroup({ + viewer, + external, + group, + nbrOfProfilesPerRow = 2, + onAction, +}) { const { owner, user, createdAt } = group; const { elements, restElements } = React.useMemo(() => { if (!Array.isArray(user)) { return { elements: [user] }; } - return { elements: user.slice(0, 3), restElements: user.slice(3) }; + return { + elements: user.slice(0, nbrOfProfilesPerRow), + restElements: user.slice(nbrOfProfilesPerRow), + }; }, [user]); const [showMore, setShowMore] = React.useState(false); diff --git a/components/core/ActivityGroup/index.js b/components/core/ActivityGroup/index.js index fb4d9357..230adb4a 100644 --- a/components/core/ActivityGroup/index.js +++ b/components/core/ActivityGroup/index.js @@ -16,7 +16,7 @@ const STYLES_GROUP_GRID = (theme) => css` padding-bottom: 24px; `; -export default function ActivityGroup({ onAction, viewer, external, group }) { +export default function ActivityGroup({ onAction, viewer, external, group, nbrOfCardsPerRow }) { const { type } = group; if ( type === "CREATE_FILE" || @@ -24,16 +24,36 @@ export default function ActivityGroup({ onAction, viewer, external, group }) { type === "LIKE_FILE" || type === "SAVE_COPY" ) { - return ; + return ( + + ); } if (type === "CREATE_SLATE" || type === "SUBSCRIBE_SLATE") { - return ; + return ( + + ); } if (type === "SUBSCRIBE_USER") { return ( - + ); } diff --git a/components/system/ThemeProvider.js b/components/system/ThemeProvider.js index 512236b7..bf6e3f75 100644 --- a/components/system/ThemeProvider.js +++ b/components/system/ThemeProvider.js @@ -28,6 +28,7 @@ export default function ThemeProvider({ children }) { font: Constants.font, typescale: Constants.typescale, semantic: Constants.semantic, + grids: Constants.grids, ...theme, }), [theme] diff --git a/scenes/SceneActivity/index.js b/scenes/SceneActivity/index.js index c56dfa05..c05a7012 100644 --- a/scenes/SceneActivity/index.js +++ b/scenes/SceneActivity/index.js @@ -41,6 +41,9 @@ export default function SceneActivity({ page, viewer, external, onAction }) { }); const divRef = React.useRef(); + + const nbrOfCardsInRow = useNbrOfCardsPerRow(divRef); + useIntersection({ ref: divRef, onIntersect: () => { @@ -69,6 +72,7 @@ export default function SceneActivity({ page, viewer, external, onAction }) {
{feed?.map((group) => ( ); } + +let NbrOfCardsInRow = {}; + +function useNbrOfCardsPerRow(ref) { + const calculateNbrOfCards = (card) => { + const isMobile = window.matchMedia(`(max-width: ${Constants.sizes.mobile}px)`).matches; + + const profileInfoWidth = isMobile ? 0 : Constants.grids.activity.profileInfo.width; + const containerWidth = ref.current.offsetWidth - profileInfoWidth; + + const nbrOfCardsWithoutGap = Math.floor(containerWidth / card.width); + const gapsWidth = (nbrOfCardsWithoutGap - 1) * card.gap; + return Math.floor((containerWidth - gapsWidth) / card.width) || 1; + }; + + React.useEffect(() => { + if (JSON.stringify(NbrOfCardsInRow) !== "{}") return; + + const isMobile = window.matchMedia(`(max-width: ${Constants.sizes.mobile}px)`).matches; + const responsiveKey = isMobile ? "mobile" : "desktop"; + + const { width: objectPreviewWidth, rowGap: objectPreviewGridRowGap } = + Constants.grids.object[responsiveKey]; + + NbrOfCardsInRow.object = calculateNbrOfCards({ + width: objectPreviewWidth, + gap: objectPreviewGridRowGap, + }); + + const { width: collectionPreviewWidth, rowGap: collectionPreviewGridRowGap } = + Constants.grids.collection[responsiveKey]; + + NbrOfCardsInRow.collection = calculateNbrOfCards({ + width: collectionPreviewWidth, + gap: collectionPreviewGridRowGap, + }); + + const { width: profilePreviewWidth, rowGap: profilePreviewGridRowGap } = + Constants.grids.profile[responsiveKey]; + NbrOfCardsInRow.profile = calculateNbrOfCards({ + width: profilePreviewWidth, + gap: profilePreviewGridRowGap, + }); + }, []); + + return NbrOfCardsInRow; +} From cf16d774f13ac184f073bf04234d1f4b4e66ef34 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Wed, 14 Jul 2021 11:59:22 +0100 Subject: [PATCH 05/27] feat(Activity): order groups by date --- scenes/SceneActivity/hooks.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scenes/SceneActivity/hooks.js b/scenes/SceneActivity/hooks.js index 9a0d29e1..175188d8 100644 --- a/scenes/SceneActivity/hooks.js +++ b/scenes/SceneActivity/hooks.js @@ -10,7 +10,9 @@ const updateExploreFeed = async ({ viewer, state, onAction, setState, update }) const newItems = response.data; const currentFeed = viewer?.explore?.feed || state?.explore?.feed || []; - const newFeed = await ActivityUtilities.processActivity(newItems); + const newFeed = await ActivityUtilities.processActivity(newItems).sort( + (a, b) => new Date(b.createdAt) - new Date(a.createdAt) + ); const newState = { items: currentItems.concat(newItems), @@ -34,7 +36,9 @@ const updateActivityFeed = async ({ viewer, onAction, update }) => { const newItems = response.data; const currentFeed = viewer?.activity?.feed || []; - const newFeed = ActivityUtilities.processActivity(newItems); + const newFeed = ActivityUtilities.processActivity(newItems).sort( + (a, b) => new Date(b.createdAt) - new Date(a.createdAt) + ); onAction({ type: "UPDATE_VIEWER", From 1cfabf7f5ed76db33488928c3191492497676e10 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Wed, 14 Jul 2021 13:27:38 +0100 Subject: [PATCH 06/27] feat(Activity): change how we format date in ProfileInfo --- common/utilities.js | 27 +++++++++++++------ .../components/ActivityCollectionGroup.js | 4 +-- .../components/ActivityFileGroup.js | 4 +-- .../components/ActivityProfileGroup.js | 5 +--- .../ActivityGroup/components/ProfileInfo.js | 13 ++++++++- 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/common/utilities.js b/common/utilities.js index 0882f0c0..03351cba 100644 --- a/common/utilities.js +++ b/common/utilities.js @@ -86,39 +86,50 @@ export const coerceToArray = (input) => { export const getFileExtension = (filename) => filename?.split(".").pop(); -export const getTimeDifferenceFromNow = (date) => { +export const getTimeDifferenceFromNow = (date, format = {}) => { + const defaultFormats = { + seconds: (time) => time + "s", + minutes: (time) => time + "m", + hours: (time) => time + "h", + days: (time) => time + "d", + currentYear: (month, day) => `${month} ${day}`, + default: (month, day, year) => `${month} ${day}, ${year}`, + }; + + const formatDate = { ...defaultFormats, ...format }; + const pastDate = new Date(date); const now = new Date(); const differenceInSeconds = Math.floor((now - pastDate) / 1000); if (differenceInSeconds < 60) { - return differenceInSeconds + "s"; + return formatDate.seconds(differenceInSeconds); } const differenceInMinutes = Math.floor(differenceInSeconds / 60); if (differenceInMinutes < 60) { - return differenceInMinutes + "m"; + return formatDate.minutes(differenceInMinutes); } const differenceInHours = Math.floor(differenceInMinutes / 60); if (differenceInHours < 24) { - return differenceInHours + "h"; + return formatDate.hours(differenceInHours); } const differenceInDays = Math.floor(differenceInHours / 24); if (differenceInDays < 24) { - return differenceInDays + "d"; + return formatDate.days(differenceInDays); } const currentYear = now.getFullYear(); const day = pastDate.getDay(); - const month = pastDate.toLocaleString("default", { month: "long" }); + const month = pastDate.toLocaleString("default", { month: "short" }); const year = pastDate.getFullYear(); if (year === currentYear) { - return `${day} ${month}`; + return formatDate.currentYear(month, day); } - return `${day} ${month} ${year}`; + return formatDate.default(month, day, year); }; diff --git a/components/core/ActivityGroup/components/ActivityCollectionGroup.js b/components/core/ActivityGroup/components/ActivityCollectionGroup.js index ac0c8c2d..4bbb070e 100644 --- a/components/core/ActivityGroup/components/ActivityCollectionGroup.js +++ b/components/core/ActivityGroup/components/ActivityCollectionGroup.js @@ -1,7 +1,6 @@ import * as React from "react"; import * as Styles from "~/common/styles"; import * as Strings from "~/common/strings"; -import * as Utilities from "~/common/utilities"; import { css } from "@emotion/react"; import { Link } from "~/components/core/Link"; @@ -50,7 +49,6 @@ export default function ActivityCollectionGroup({ const [showMore, setShowMore] = React.useState(false); const viewMoreFiles = () => setShowMore(true); - const timeSinceUploaded = Utilities.getTimeDifferenceFromNow(createdAt); // const timeSinceUploaded = Utilities.getTimeDifferenceFromNow(elements[0].createdAt); const nbrOfFilesUploaded = elements.length + (restElements?.length || 0); const action = React.useMemo(() => { @@ -63,7 +61,7 @@ export default function ActivityCollectionGroup({ return (
setShowMore(true); - const timeSinceAction = Utilities.getTimeDifferenceFromNow(createdAt); const nbrOfFiles = elements.length + (restElements?.length || 0); const action = React.useMemo(() => { if (type === "CREATE_FILE") @@ -64,7 +62,7 @@ export default function ActivityFileGroup({ viewer, group, onAction, nbrOfObject return (
setShowMore(true); - const timeSinceUploaded = Utilities.getTimeDifferenceFromNow(createdAt); - return (
`${time} ${Strings.pluralize("second", time)} ago`, + minutes: (time) => `${time} ${Strings.pluralize("minute", time)} ago`, + hours: (time) => `${time} ${Strings.pluralize("hour", time)} ago`, + days: (time) => `${time} ${Strings.pluralize("day", time)} ago`, + }); + const isOwner = viewer?.id === owner.id; return ( @@ -61,7 +71,8 @@ export default function ProfileInfo({ owner, viewer, time, action, onAction }) {  •  - {time} + {formattedDate} + {mobileFormattedDate} From 5eac0f1a9fc03575df0084854fd7d133481f867d Mon Sep 17 00:00:00 2001 From: Aminejv Date: Thu, 15 Jul 2021 17:32:18 +0100 Subject: [PATCH 07/27] feat(activity): show owner info in CollectionPreviewBlock --- common/constants.js | 3 ++ .../components/ActivityCollectionGroup.js | 21 ++++++-- .../ActivityGroup/components/ProfileInfo.js | 8 ++- .../FilesCollectionPreview.js | 42 +++++++++++---- .../ImageCollectionPreview.js | 51 ++++++++++++------- .../core/CollectionPreviewBlock/index.js | 20 ++++++-- .../ObjectPreview/ObjectPreviewPrimitive.js | 1 + components/core/Profile.js | 2 +- components/core/ProfilePreviewBlock.js | 8 ++- node_common/data/methods/get-activity.js | 18 ++++++- node_common/data/methods/get-explore.js | 18 ++++++- .../methods/get-subscriptions-by-user-id.js | 28 +++++++--- node_common/serializers.js | 1 + scenes/SceneSlates.js | 4 ++ 14 files changed, 174 insertions(+), 51 deletions(-) diff --git a/common/constants.js b/common/constants.js index 6c1b86f1..c0d101c2 100644 --- a/common/constants.js +++ b/common/constants.js @@ -216,3 +216,6 @@ export const grids = { mobile: { width: 344, rowGap: 8 }, }, }; + +export const profileDefaultPicture = + "https://slate.textile.io/ipfs/bafkreick3nscgixwfpq736forz7kzxvvhuej6kszevpsgmcubyhsx2pf7i"; diff --git a/components/core/ActivityGroup/components/ActivityCollectionGroup.js b/components/core/ActivityGroup/components/ActivityCollectionGroup.js index 4bbb070e..fe94eea8 100644 --- a/components/core/ActivityGroup/components/ActivityCollectionGroup.js +++ b/components/core/ActivityGroup/components/ActivityCollectionGroup.js @@ -71,7 +71,12 @@ export default function ActivityCollectionGroup({
{elements.map((collection) => ( - + ))} {showMore && @@ -84,12 +89,22 @@ export default function ActivityCollectionGroup({ key={collection.id} > - + ) : ( - + ) )} diff --git a/components/core/ActivityGroup/components/ProfileInfo.js b/components/core/ActivityGroup/components/ProfileInfo.js index 6844816a..b4136e30 100644 --- a/components/core/ActivityGroup/components/ProfileInfo.js +++ b/components/core/ActivityGroup/components/ProfileInfo.js @@ -2,6 +2,7 @@ import * as React from "react"; import * as Styles from "~/common/styles"; import * as Utilities from "~/common/utilities"; import * as Strings from "~/common/strings"; +import * as Constants from "~/common/constants"; import { Link } from "~/components/core/Link"; import { css } from "@emotion/react"; @@ -58,7 +59,12 @@ export default function ProfileInfo({ owner, viewer, time, action, onAction }) { return (
- {`${username} + {`${username} (e.target.src = Constants.profileDefaultPicture)} + />

diff --git a/components/core/CollectionPreviewBlock/FilesCollectionPreview.js b/components/core/CollectionPreviewBlock/FilesCollectionPreview.js index 27273dc1..32a43a59 100644 --- a/components/core/CollectionPreviewBlock/FilesCollectionPreview.js +++ b/components/core/CollectionPreviewBlock/FilesCollectionPreview.js @@ -2,6 +2,7 @@ import * as React from "react"; import * as Strings from "~/common/strings"; import * as Typography from "~/components/system/components/Typography"; import * as Styles from "~/common/styles"; +import * as Constants from "~/common/constants"; import { Divider } from "~/components/system/components/Divider"; import { Logo } from "~/common/logo"; @@ -10,6 +11,7 @@ import { LikeButton, SaveButton } from "~/components/core/ObjectPreview/componen import { useLikeHandler, useSaveHandler } from "~/common/hooks"; import { FollowButton } from "~/components/core/CollectionPreviewBlock/components"; import { useFollowHandler } from "~/components/core/CollectionPreviewBlock/hooks"; +import { Link } from "~/components/core/Link"; import ObjectPlaceholder from "~/components/core/ObjectPreview/placeholders"; @@ -152,7 +154,7 @@ const useCollectionCarrousel = ({ objects }) => { return { selectBatchIdx, selectedBatch, selectedIdx }; }; -export default function CollectionPreview({ collection, viewer }) { +export default function CollectionPreview({ collection, viewer, owner, onAction }) { const { follow, followCount, isFollowed } = useFollowHandler({ collection, viewer }); const filePreviews = React.useMemo(() => { const files = collection?.objects || []; @@ -242,16 +244,34 @@ export default function CollectionPreview({ collection, viewer }) { {followCount}

-
- owner profile - - Wes Anderson - -
+ + {owner && ( +
+ + {`${owner.username} (e.target.src = Constants.profileDefaultPicture)} + /> + + + + {owner.username} + + +
+ )}
diff --git a/components/core/CollectionPreviewBlock/ImageCollectionPreview.js b/components/core/CollectionPreviewBlock/ImageCollectionPreview.js index 47ceff44..8cd2a22e 100644 --- a/components/core/CollectionPreviewBlock/ImageCollectionPreview.js +++ b/components/core/CollectionPreviewBlock/ImageCollectionPreview.js @@ -2,6 +2,7 @@ import * as React from "react"; import * as Strings from "~/common/strings"; import * as Typography from "~/components/system/components/Typography"; import * as Styles from "~/common/styles"; +import * as Constants from "~/common/constants"; import { useInView } from "~/common/hooks"; import { isBlurhashValid } from "blurhash"; @@ -9,6 +10,7 @@ import { Blurhash } from "react-blurhash"; import { css } from "@emotion/react"; import { FollowButton } from "~/components/core/CollectionPreviewBlock/components"; import { useFollowHandler } from "~/components/core/CollectionPreviewBlock/hooks"; +import { Link } from "~/components/core/Link"; const STYLES_CONTAINER = (theme) => css` display: flex; @@ -129,7 +131,7 @@ const useCollectionCarrousel = ({ objects }) => { }; }; -export default function ImageCollectionPreview({ collection, viewer }) { +export default function ImageCollectionPreview({ collection, viewer, owner, onAction }) { const { follow, followCount, isFollowed } = useFollowHandler({ collection, viewer }); const filePreviews = React.useMemo(() => { @@ -147,14 +149,8 @@ export default function ImageCollectionPreview({ collection, viewer }) { ref: previewerRef, }); - const { - isLoaded, - blurhash, - selectedImage, - handleLoading, - selectedIdx, - selectImageByIdx, - } = useCollectionCarrousel({ objects: filePreviews }); + const { isLoaded, blurhash, selectedImage, handleLoading, selectedIdx, selectImageByIdx } = + useCollectionCarrousel({ objects: filePreviews }); const nbrOfFiles = collection?.objects?.length || 0; @@ -218,16 +214,33 @@ export default function ImageCollectionPreview({ collection, viewer }) { {followCount}
-
- owner profile - - Wes Anderson - -
+ {owner && ( +
+ + {`${owner.username} (e.target.src = Constants.profileDefaultPicture)} + /> + + + + {owner.username} + + +
+ )}
diff --git a/components/core/CollectionPreviewBlock/index.js b/components/core/CollectionPreviewBlock/index.js index 475711b9..5d025122 100644 --- a/components/core/CollectionPreviewBlock/index.js +++ b/components/core/CollectionPreviewBlock/index.js @@ -4,14 +4,28 @@ import * as Validations from "~/common/validations"; import ImageCollectionPreview from "./ImageCollectionPreview"; import FilesCollectionPreview from "./FilesCollectionPreview"; -export default function CollectionPreview({ collection, viewer }) { +export default function CollectionPreview({ collection, viewer, owner, onAction }) { const objects = collection.objects.filter((file) => Validations.isPreviewableImage(file.data.type) ); if (objects.length > 0) { - return ; + return ( + + ); } - return ; + return ( + + ); } diff --git a/components/core/ObjectPreview/ObjectPreviewPrimitive.js b/components/core/ObjectPreview/ObjectPreviewPrimitive.js index e1c5b802..abffbfb0 100644 --- a/components/core/ObjectPreview/ObjectPreviewPrimitive.js +++ b/components/core/ObjectPreview/ObjectPreviewPrimitive.js @@ -159,6 +159,7 @@ export default function ObjectPreviewPremitive({ css={STYLES_PROFILE_IMAGE} src={owner.data.photo} alt={`${owner.username} profile`} + onError={(e) => (e.target.src = Constants.profileDefaultPicture)} /> )} diff --git a/components/core/Profile.js b/components/core/Profile.js index 83855b5b..a39bb2ad 100644 --- a/components/core/Profile.js +++ b/components/core/Profile.js @@ -354,7 +354,7 @@ function CollectionsPage({ onAction={onAction} collection={collection} viewer={viewer} - owner={user} + owner={tab === "collections" ? user : collection.owner} /> ))} diff --git a/components/core/ProfilePreviewBlock.js b/components/core/ProfilePreviewBlock.js index 6b731286..fb4cb3fc 100644 --- a/components/core/ProfilePreviewBlock.js +++ b/components/core/ProfilePreviewBlock.js @@ -2,6 +2,7 @@ import * as React from "react"; import * as Styles from "~/common/styles"; import * as Typography from "~/components/system/components/Typography"; import * as Strings from "~/common/strings"; +import * as Constants from "~/common/constants"; import { Divider } from "~/components/system/components/Divider"; import { Logo } from "~/common/logo"; @@ -142,7 +143,12 @@ export default function ProfilePreviewBlock({ onAction, viewer, profile }) { return (
- {`${profile.username}`} + {`${profile.username}`} (e.target.src = Constants.profileDefaultPicture)} + />
{profile.username} diff --git a/node_common/data/methods/get-activity.js b/node_common/data/methods/get-activity.js index 2cb1437c..6e2c4ad3 100644 --- a/node_common/data/methods/get-activity.js +++ b/node_common/data/methods/get-activity.js @@ -30,8 +30,20 @@ export default async ({ const slateFilesFields = ["files", "slate_files.createdAt", "files.id", "objects"]; const slateFilesQuery = `coalesce(json_agg(?? order by ?? asc) filter (where ?? is not null), '[]') as ??`; - const slateFields = [ + const slateOwnerFields = [ "slate_table", + "slate_with_objects.*", + ...Constants.userPreviewProperties, + "owner", + "slate_with_objects", + "users", + "slate_with_objects.ownerId", + "users.id", + ]; + const slateOwnerQuery = `?? as (SELECT ??, json_build_object('id', ??, 'data', ??, 'username', ??) as ?? FROM ?? LEFT JOIN ?? ON ?? = ?? ) `; + + const slateFields = [ + "slate_with_objects", "slates.id", "slates.slatename", "slates.data", @@ -48,11 +60,13 @@ export default async ({ "files.id", "slate_files.fileId", "slates.id", + ...slateOwnerFields, ]; - const slateQuery = `WITH ?? as (SELECT ??, ??, ??, ??, ??, ??, ??, ${slateFilesQuery} FROM ?? LEFT JOIN ?? on ?? = ?? LEFT JOIN ?? on ?? = ?? GROUP BY ??)`; + const slateQuery = `WITH ?? as (SELECT ??, ??, ??, ??, ??, ??, ??, ${slateFilesQuery} FROM ?? LEFT JOIN ?? on ?? = ?? LEFT JOIN ?? on ?? = ?? GROUP BY ??), ${slateOwnerQuery}`; const userFilesFields = ["files", "files.createdAt", "files.id", "objects"]; const userFilesQuery = `coalesce(json_agg(?? order by ?? asc) filter (where ?? is not null), '[]') as ??`; + const userFields = [ "user_table", "users.id", diff --git a/node_common/data/methods/get-explore.js b/node_common/data/methods/get-explore.js index e57b88c7..e866a318 100644 --- a/node_common/data/methods/get-explore.js +++ b/node_common/data/methods/get-explore.js @@ -6,8 +6,20 @@ export default async ({ earliestTimestamp, latestTimestamp }) => { const slateFilesFields = ["files", "slate_files.createdAt", "files.id", "objects"]; const slateFilesQuery = `coalesce(json_agg(?? order by ?? asc) filter (where ?? is not null), '[]') as ??`; - const slateFields = [ + const slateOwnerFields = [ "slate_table", + "slate_with_objects.*", + ...Constants.userPreviewProperties, + "owner", + "slate_with_objects", + "users", + "slate_with_objects.ownerId", + "users.id", + ]; + const slateOwnerQuery = `?? as (SELECT ??, json_build_object('id', ??, 'data', ??, 'username', ??) as ?? FROM ?? LEFT JOIN ?? ON ?? = ?? ) `; + + const slateFields = [ + "slate_with_objects", "slates.id", "slates.slatename", "slates.data", @@ -24,11 +36,13 @@ export default async ({ earliestTimestamp, latestTimestamp }) => { "files.id", "slate_files.fileId", "slates.id", + ...slateOwnerFields, ]; - const slateQuery = `WITH ?? as (SELECT ??, ??, ??, ??, ??, ??, ??, ${slateFilesQuery} FROM ?? LEFT JOIN ?? on ?? = ?? LEFT JOIN ?? on ?? = ?? GROUP BY ??)`; + const slateQuery = `WITH ?? as (SELECT ??, ??, ??, ??, ??, ??, ??, ${slateFilesQuery} FROM ?? LEFT JOIN ?? on ?? = ?? LEFT JOIN ?? on ?? = ?? GROUP BY ??), ${slateOwnerQuery}`; const userFilesFields = ["files", "files.createdAt", "files.id", "objects"]; const userFilesQuery = `coalesce(json_agg(?? order by ?? asc) filter (where ?? is not null), '[]') as ??`; + const userFields = [ "user_table", "users.id", diff --git a/node_common/data/methods/get-subscriptions-by-user-id.js b/node_common/data/methods/get-subscriptions-by-user-id.js index b3c26d2a..7760e3c5 100644 --- a/node_common/data/methods/get-subscriptions-by-user-id.js +++ b/node_common/data/methods/get-subscriptions-by-user-id.js @@ -11,6 +11,12 @@ export default async ({ ownerId }) => { // const slateFiles = () => // DB.raw("json_agg(?? order by ?? asc) as ??", ["files", "slate_files.createdAt", "objects"]); + const ownerQueryFields = ["*", ...Constants.userPreviewProperties, "owner"]; + const ownerQuery = DB.raw( + `??, json_build_object('id', ??, 'data', ??, 'username', ??) as ??`, + ownerQueryFields + ); + const slateFiles = () => DB.raw("coalesce(json_agg(?? order by ?? asc) filter (where ?? is not null), '[]') as ??", [ "files", @@ -19,14 +25,20 @@ export default async ({ ownerId }) => { "objects", ]); - const query = await DB.select(...Serializers.slateProperties, slateFiles()) + const query = await DB.with("slates", (db) => + db + .select(...Serializers.slateProperties, slateFiles()) + .from("slates") + .join("subscriptions", "subscriptions.slateId", "=", "slates.id") + .join("slate_files", "slate_files.slateId", "=", "slates.id") + .join("files", "slate_files.fileId", "=", "files.id") + .where({ "subscriptions.ownerId": ownerId, "slates.isPublic": true }) + // .orderBy("subscriptions.createdAt", "desc"); + .groupBy("slates.id") + ) + .select(ownerQuery) .from("slates") - .join("subscriptions", "subscriptions.slateId", "=", "slates.id") - .join("slate_files", "slate_files.slateId", "=", "slates.id") - .join("files", "slate_files.fileId", "=", "files.id") - .where({ "subscriptions.ownerId": ownerId, "slates.isPublic": true }) - // .orderBy("subscriptions.createdAt", "desc"); - .groupBy("slates.id"); + .join("users", "slates.ownerId", "users.id"); if (!query || query.error) { return []; @@ -39,7 +51,7 @@ export default async ({ ownerId }) => { return JSON.parse(JSON.stringify(serialized)); }, - errorFn: async (e) => { + errorFn: async () => { Logging.error({ error: true, decorator: "GET_SUBSCRIPTIONS_BY_USER_ID", diff --git a/node_common/serializers.js b/node_common/serializers.js index 7c13fa8a..17ae2133 100644 --- a/node_common/serializers.js +++ b/node_common/serializers.js @@ -31,6 +31,7 @@ export const sanitizeSlate = (entity) => { ownerId: entity.ownerId, isPublic: entity.isPublic, objects: entity.objects, + owner: entity.owner, user: entity.user, //NOTE(martina): this is not in the database. It is added after data: { name: entity.data?.name, diff --git a/scenes/SceneSlates.js b/scenes/SceneSlates.js index 4a4cbf40..3fa890e5 100644 --- a/scenes/SceneSlates.js +++ b/scenes/SceneSlates.js @@ -72,6 +72,8 @@ export default class SceneSlates extends React.Component { key={slate.id} collection={slate} viewer={this.props.viewer} + owner={this.props.viewer} + onAction={this.props.onAction} /> ))} @@ -101,7 +103,9 @@ export default class SceneSlates extends React.Component { ))} From 4a4c0ad594ba08062f607f3c90331a247df05f7f Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 20 Jul 2021 11:13:21 +0100 Subject: [PATCH 08/27] feat(theme): update grayLight6,bgBlurLight6,bgBlurLight6OP,bgBlurLight6TRN colors --- common/constants.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/constants.js b/common/constants.js index c0d101c2..430e4812 100644 --- a/common/constants.js +++ b/common/constants.js @@ -16,7 +16,7 @@ export const sizes = { export const system = { //system color white: "#FFFFFF", - grayLight6: "#F2F5F7", + grayLight6: "#F7F8F9", grayLight5: "#E5E8EA", grayLight4: "#D1D4D6", grayLight3: "#C7CACC", @@ -96,9 +96,9 @@ export const semantic = { bgBlurWhite: "rgba(255, 255, 255, 0.7)", bgBlurWhiteOP: "rgba(255, 255, 255, 0.85)", bgBlurWhiteTRN: "rgba(255, 255, 255, 0.3)", - bgBlurLight6: "rgba(242, 245, 247, 0.7)", - bgBlurLight6OP: "rgba(242, 245, 247, 0.85)", - bgBlurLight6TRN: "rgba(242, 245, 247, 0.3)", + bgBlurLight6: "rgba(247, 248, 249, 0.7)", + bgBlurLight6OP: "rgba(247, 248, 249, 0.85)", + bgBlurLight6TRN: "rgba(247, 248, 249, 0.3)", bgDark: system.grayDark6, bgLightDark: system.grayDark5, From 587b2fe90cfd19afc03d250c2cf52339541f5b92 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 20 Jul 2021 11:15:56 +0100 Subject: [PATCH 09/27] chore(objectPreviews): remove unnecessary object for placeholders --- .../core/ObjectPreview/3dObjectPreview.js | 38 -------- .../core/ObjectPreview/AudioObjectPreview.js | 41 --------- .../core/ObjectPreview/CodeObjectPreview.js | 40 --------- .../ObjectPreview/DefaultObjectPreview.js | 36 -------- .../core/ObjectPreview/EpubObjectPreview.js | 38 -------- .../ObjectPreview/KeynoteObjectPreview.js | 38 -------- .../core/ObjectPreview/PdfObjectPreview.js | 39 -------- .../core/ObjectPreview/VideoObjectPreview.js | 40 --------- .../core/ObjectPreview/placeholders/Text.js | 90 ------------------- 9 files changed, 400 deletions(-) delete mode 100644 components/core/ObjectPreview/3dObjectPreview.js delete mode 100644 components/core/ObjectPreview/AudioObjectPreview.js delete mode 100644 components/core/ObjectPreview/CodeObjectPreview.js delete mode 100644 components/core/ObjectPreview/DefaultObjectPreview.js delete mode 100644 components/core/ObjectPreview/EpubObjectPreview.js delete mode 100644 components/core/ObjectPreview/KeynoteObjectPreview.js delete mode 100644 components/core/ObjectPreview/PdfObjectPreview.js delete mode 100644 components/core/ObjectPreview/VideoObjectPreview.js delete mode 100644 components/core/ObjectPreview/placeholders/Text.js diff --git a/components/core/ObjectPreview/3dObjectPreview.js b/components/core/ObjectPreview/3dObjectPreview.js deleted file mode 100644 index 9fca4233..00000000 --- a/components/core/ObjectPreview/3dObjectPreview.js +++ /dev/null @@ -1,38 +0,0 @@ -import "isomorphic-fetch"; - -import * as React from "react"; -import * as Styles from "~/common/styles"; - -import { P3 } from "~/components/system"; -import { css } from "@emotion/react"; - -import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; -import ObjectPlaceholder from "./placeholders/3D"; - -const STYLES_CONTAINER = css` - height: 100%; -`; - -const STYLES_TAG = (theme) => css` - position: absolute; - text-transform: uppercase; - background-color: ${theme.semantic.bgLight}; - bottom: 25%; - left: 50%; - transform: translateX(-50%); - padding: 2px 8px; - border-radius: 4px; -`; - -export default function Object3DPreview(props) { - return ( - -
- -
- 3D -
-
-
- ); -} diff --git a/components/core/ObjectPreview/AudioObjectPreview.js b/components/core/ObjectPreview/AudioObjectPreview.js deleted file mode 100644 index 55ce809d..00000000 --- a/components/core/ObjectPreview/AudioObjectPreview.js +++ /dev/null @@ -1,41 +0,0 @@ -import "isomorphic-fetch"; - -import * as React from "react"; -import * as Styles from "~/common/styles"; -import * as Utilities from "~/common/utilities"; - -import { P3 } from "~/components/system"; - -import { css } from "@emotion/react"; - -import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; -import AudioPlaceholder from "./placeholders/Audio"; - -const STYLES_CONTAINER = css` - height: 100%; -`; - -const STYLES_TAG = (theme) => css` - position: absolute; - text-transform: uppercase; - background-color: ${theme.semantic.bgLight}; - bottom: 23.7%; - left: 50%; - transform: translateX(-50%); - padding: 2px 8px; - border-radius: 4px; -`; - -export default function AudioObjectPreview({ file, ...props }) { - const tag = Utilities.getFileExtension(file.filename) || "audio"; - return ( - -
- -
- {tag} -
-
-
- ); -} diff --git a/components/core/ObjectPreview/CodeObjectPreview.js b/components/core/ObjectPreview/CodeObjectPreview.js deleted file mode 100644 index fa6b421b..00000000 --- a/components/core/ObjectPreview/CodeObjectPreview.js +++ /dev/null @@ -1,40 +0,0 @@ -import "isomorphic-fetch"; - -import * as React from "react"; -import * as Styles from "~/common/styles"; -import * as Utilities from "~/common/utilities"; - -import { P3 } from "~/components/system"; -import { css } from "@emotion/react"; - -import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; -import CodePlaceholder from "./placeholders/Code"; - -const STYLES_CONTAINER = css` - height: 100%; -`; - -const STYLES_TAG = (theme) => css` - position: absolute; - text-transform: uppercase; - background-color: ${theme.semantic.bgLight}; - bottom: 27%; - left: 50%; - transform: translateX(-50%); - padding: 2px 8px; - border-radius: 4px; -`; - -export default function CodeObjectPreview({ file, ...props }) { - const tag = Utilities.getFileExtension(file.filename) || "code"; - return ( - -
- -
- {tag} -
-
-
- ); -} diff --git a/components/core/ObjectPreview/DefaultObjectPreview.js b/components/core/ObjectPreview/DefaultObjectPreview.js deleted file mode 100644 index 3a12968a..00000000 --- a/components/core/ObjectPreview/DefaultObjectPreview.js +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react"; -import * as Styles from "~/common/styles"; - -import { P3 } from "~/components/system"; -import { css } from "@emotion/react"; - -import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; -import FilePlaceholder from "./placeholders/File"; - -const STYLES_CONTAINER = css` - height: 100%; -`; - -const STYLES_TAG = (theme) => css` - position: absolute; - text-transform: uppercase; - background-color: ${theme.semantic.bgLight}; - bottom: 26%; - left: 50%; - transform: translateX(-50%); - padding: 2px 8px; - border-radius: 4px; -`; - -export default function DefaultObjectPreview(props) { - return ( - -
- -
- FILE -
-
-
- ); -} diff --git a/components/core/ObjectPreview/EpubObjectPreview.js b/components/core/ObjectPreview/EpubObjectPreview.js deleted file mode 100644 index 72d1218e..00000000 --- a/components/core/ObjectPreview/EpubObjectPreview.js +++ /dev/null @@ -1,38 +0,0 @@ -import "isomorphic-fetch"; - -import * as React from "react"; -import * as Styles from "~/common/styles"; - -import { P3 } from "~/components/system"; -import { css } from "@emotion/react"; - -import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; -import EpubPlaceholder from "./placeholders/EPUB"; - -const STYLES_CONTAINER = css` - height: 100%; -`; - -const STYLES_TAG = (theme) => css` - position: absolute; - text-transform: uppercase; - background-color: ${theme.semantic.bgLight}; - bottom: 32%; - left: 50%; - transform: translateX(-50%); - padding: 2px 8px; - border-radius: 4px; -`; - -export default function EpubObjectPreview(props) { - return ( - -
- -
- EPUB -
-
-
- ); -} diff --git a/components/core/ObjectPreview/KeynoteObjectPreview.js b/components/core/ObjectPreview/KeynoteObjectPreview.js deleted file mode 100644 index b89cb5c7..00000000 --- a/components/core/ObjectPreview/KeynoteObjectPreview.js +++ /dev/null @@ -1,38 +0,0 @@ -import "isomorphic-fetch"; - -import * as React from "react"; -import * as Styles from "~/common/styles"; - -import { P3 } from "~/components/system"; -import { css } from "@emotion/react"; - -import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; -import KeynotePlaceholder from "./placeholders/Keynote"; - -const STYLES_CONTAINER = css` - height: 100%; -`; - -const STYLES_TAG = (theme) => css` - position: absolute; - text-transform: uppercase; - background-color: ${theme.semantic.bgLight}; - bottom: 36%; - left: 50%; - transform: translateX(-50%); - padding: 2px 8px; - border-radius: 4px; -`; - -export default function KeynoteObjectPreview(props) { - return ( - -
- -
- KEYNOTE -
-
-
- ); -} diff --git a/components/core/ObjectPreview/PdfObjectPreview.js b/components/core/ObjectPreview/PdfObjectPreview.js deleted file mode 100644 index 2ae20fdf..00000000 --- a/components/core/ObjectPreview/PdfObjectPreview.js +++ /dev/null @@ -1,39 +0,0 @@ -import "isomorphic-fetch"; - -import * as React from "react"; -import * as Styles from "~/common/styles"; - -import { P3 } from "~/components/system"; -import { css } from "@emotion/react"; - -import PdfPlaceholder from "./placeholders/PDF"; -import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; - -const STYLES_CONTAINER = css` - position: relative; - height: 100%; -`; - -const STYLES_TAG = (theme) => css` - position: absolute; - text-transform: uppercase; - background-color: ${theme.semantic.bgLight}; - bottom: 27%; - left: 50%; - transform: translateX(-50%); - padding: 2px 8px; - border-radius: 4px; -`; - -export default function PDFObjectPreview(props) { - return ( - -
- -
- PDF -
-
-
- ); -} diff --git a/components/core/ObjectPreview/VideoObjectPreview.js b/components/core/ObjectPreview/VideoObjectPreview.js deleted file mode 100644 index 38905f66..00000000 --- a/components/core/ObjectPreview/VideoObjectPreview.js +++ /dev/null @@ -1,40 +0,0 @@ -import "isomorphic-fetch"; - -import * as React from "react"; -import * as Styles from "~/common/styles"; - -import { P3 } from "~/components/system"; -import { css } from "@emotion/react"; - -import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; -import VideoPlaceholder from "./placeholders/Video"; - -const STYLES_CONTAINER = css` - height: 100%; -`; - -const STYLES_TAG = (theme) => css` - position: absolute; - text-transform: uppercase; - background-color: ${theme.semantic.bgLight}; - bottom: 31.5%; - left: 50%; - transform: translateX(-50%); - padding: 2px 8px; - border-radius: 4px; -`; - -export default function VideoObjectPreview({ file, ...props }) { - const { type } = file.data; - const tag = type.split("/")[1]; - return ( - -
- -
- {tag} -
-
-
- ); -} diff --git a/components/core/ObjectPreview/placeholders/Text.js b/components/core/ObjectPreview/placeholders/Text.js deleted file mode 100644 index 2b399413..00000000 --- a/components/core/ObjectPreview/placeholders/Text.js +++ /dev/null @@ -1,90 +0,0 @@ -import * as React from "react"; - -import { css } from "@emotion/react"; - -export default function TextPlaceholder({ ratio = 1, ...props }) { - const STYLES_PLACEHOLDER = React.useMemo( - () => css` - overflow: visible !important; - width: ${(123 / 248) * 100 * ratio}%; - height: ${(151 / 248) * 100 * ratio}%; - `, - [ratio] - ); - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} From 9701023d5796bdeaee377cbb603271f792e8f056 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 20 Jul 2021 11:19:28 +0100 Subject: [PATCH 10/27] feat(objectPreview): update placeholders svg --- .../core/ObjectPreview/placeholders/3D.js | 183 +++++------------- .../core/ObjectPreview/placeholders/Audio.js | 37 ++-- .../core/ObjectPreview/placeholders/Code.js | 65 ++++--- .../core/ObjectPreview/placeholders/EPUB.js | 68 ++++--- .../core/ObjectPreview/placeholders/File.js | 87 +++++---- .../ObjectPreview/placeholders/Keynote.js | 61 +++--- .../core/ObjectPreview/placeholders/PDF.js | 63 +++--- .../core/ObjectPreview/placeholders/Video.js | 59 +++--- 8 files changed, 276 insertions(+), 347 deletions(-) diff --git a/components/core/ObjectPreview/placeholders/3D.js b/components/core/ObjectPreview/placeholders/3D.js index 308cc448..ec57175d 100644 --- a/components/core/ObjectPreview/placeholders/3D.js +++ b/components/core/ObjectPreview/placeholders/3D.js @@ -6,169 +6,84 @@ export default function Object3DPlaceholder({ ratio = 1, ...props }) { const STYLES_PLACEHOLDER = React.useMemo( () => css` overflow: visible !important; - width: ${(69 / 248) * 100 * ratio}%; - height: ${(76.65 / 248) * 100 * ratio}%; + width: ${(64 / 248) * 100 * ratio}%; + height: ${(71.25 / 248) * 100 * ratio}%; `, [ratio] ); return ( - + - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + ); diff --git a/components/core/ObjectPreview/placeholders/Audio.js b/components/core/ObjectPreview/placeholders/Audio.js index 94209845..37eb3904 100644 --- a/components/core/ObjectPreview/placeholders/Audio.js +++ b/components/core/ObjectPreview/placeholders/Audio.js @@ -6,39 +6,28 @@ export default function AudioPlaceholder({ ratio = 1, ...props }) { const STYLES_PLACEHOLDER = React.useMemo( () => css` overflow: visible !important; - width: ${(163 / 248) * 100 * ratio}%; - height: ${(163 / 248) * 100 * ratio}%; + width: ${(102 / 248) * 100 * ratio}%; + height: ${(102 / 248) * 100 * ratio}%; `, [ratio] ); return ( - + - + - - + + diff --git a/components/core/ObjectPreview/placeholders/Code.js b/components/core/ObjectPreview/placeholders/Code.js index 7dcb350f..9e25092b 100644 --- a/components/core/ObjectPreview/placeholders/Code.js +++ b/components/core/ObjectPreview/placeholders/Code.js @@ -1,60 +1,77 @@ import * as React from "react"; import { css } from "@emotion/react"; - export default function CodePlaceholder({ ratio = 1, ...props }) { const STYLES_PLACEHOLDER = React.useMemo( () => css` overflow: visible !important; - width: ${(121 / 248) * 100 * ratio}%; - height: ${(151 / 248) * 100 * ratio}%; + width: ${(64 / 248) * 100 * ratio}%; + height: ${(80 / 248) * 100 * ratio}%; `, [ratio] ); return ( - - - + + + + + - - - + + + + + + + + + + + + diff --git a/components/core/ObjectPreview/placeholders/EPUB.js b/components/core/ObjectPreview/placeholders/EPUB.js index 3a778236..e99b8bbb 100644 --- a/components/core/ObjectPreview/placeholders/EPUB.js +++ b/components/core/ObjectPreview/placeholders/EPUB.js @@ -2,52 +2,52 @@ import * as React from "react"; import { css } from "@emotion/react"; -function EpubPlaceholder({ ratio = 1, ...props }) { +export default function EbookPlaceholder({ ratio, ...props }) { const STYLES_PLACEHOLDER = React.useMemo( () => css` overflow: visible !important; - width: ${(199 / 248) * 100 * ratio}%; - height: ${(123 / 248) * 100 * ratio}%; + width: ${(100 / 248) * 100 * ratio}%; + height: ${(64 / 248) * 100 * ratio}%; `, [ratio] ); return ( - + - + - + @@ -55,10 +55,10 @@ function EpubPlaceholder({ ratio = 1, ...props }) { @@ -66,35 +66,35 @@ function EpubPlaceholder({ ratio = 1, ...props }) { - - + + - - - + + + @@ -102,5 +102,3 @@ function EpubPlaceholder({ ratio = 1, ...props }) { ); } - -export default EpubPlaceholder; diff --git a/components/core/ObjectPreview/placeholders/File.js b/components/core/ObjectPreview/placeholders/File.js index b76743e0..652374ef 100644 --- a/components/core/ObjectPreview/placeholders/File.js +++ b/components/core/ObjectPreview/placeholders/File.js @@ -6,59 +6,78 @@ export default function FilePlaceholder({ ratio = 1, ...props }) { const STYLES_PLACEHOLDER = React.useMemo( () => css` overflow: visible !important; - width: ${(121 / 248) * 100 * ratio}%; - height: ${(151 / 248) * 100 * ratio}%; + width: ${(64 / 248) * 100 * ratio}%; + height: ${(80 / 248) * 100 * ratio}%; `, [ratio] ); return ( - - - - - + + + + + + + - - - + + + + + + + + + + + + diff --git a/components/core/ObjectPreview/placeholders/Keynote.js b/components/core/ObjectPreview/placeholders/Keynote.js index 702133b5..4db96ec6 100644 --- a/components/core/ObjectPreview/placeholders/Keynote.js +++ b/components/core/ObjectPreview/placeholders/Keynote.js @@ -6,45 +6,38 @@ export default function KeynotePlaceholder({ ratio = 1, ...props }) { const STYLES_PLACEHOLDER = React.useMemo( () => css` overflow: visible !important; - width: ${(183 / 248) * 100 * ratio}%; - height: ${(115 / 248) * 100 * ratio}%; + width: ${(96 / 248) * 100 * ratio}%; + height: ${(64 / 248) * 100 * ratio}%; `, [ratio] ); return ( - - + - + - - + + @@ -71,33 +64,33 @@ export default function KeynotePlaceholder({ ratio = 1, ...props }) { id="prefix__filter1_d_keynote" x={0} y={0} - width={278.5} - height={210.5} + width={224} + height={188} filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB" > - - + + - - - + + + diff --git a/components/core/ObjectPreview/placeholders/PDF.js b/components/core/ObjectPreview/placeholders/PDF.js index 0eb3649c..0c075a65 100644 --- a/components/core/ObjectPreview/placeholders/PDF.js +++ b/components/core/ObjectPreview/placeholders/PDF.js @@ -2,82 +2,81 @@ import * as React from "react"; import { css } from "@emotion/react"; -export default function PdfPlaceholder({ ratio = 1, ...props }) { +function PdfPlaceholder({ ratio = 1, ...props }) { const STYLES_PLACEHOLDER = React.useMemo( () => css` overflow: visible !important; - width: ${(123 / 248) * 100 * ratio}%; - height: ${(151 / 248) * 100 * ratio}%; + width: ${(64 / 248) * 100 * ratio}%; + height: ${(80 / 248) * 100 * ratio}%; `, [ratio] ); - return ( - - + + - - + + - + - - + + - - - + + + @@ -87,3 +86,5 @@ export default function PdfPlaceholder({ ratio = 1, ...props }) { ); } + +export default PdfPlaceholder; diff --git a/components/core/ObjectPreview/placeholders/Video.js b/components/core/ObjectPreview/placeholders/Video.js index cc68793d..126335df 100644 --- a/components/core/ObjectPreview/placeholders/Video.js +++ b/components/core/ObjectPreview/placeholders/Video.js @@ -5,51 +5,48 @@ export default function VideoPlaceholder({ ratio = 1, ...props }) { const STYLES_PLACEHOLDER = React.useMemo( () => css` overflow: visible !important; - width: ${(188 / 248) * 100 * ratio}%; - height: ${(125 / 248) * 100 * ratio}%; + width: ${(96 / 248) * 100 * ratio}%; + height: ${(64 / 248) * 100 * ratio}%; `, [ratio] ); return ( - + + + - + - - - - + + + + + + + + ); From c8755adc38fdce56ed0cbfb0b940cba3af2b39e9 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 20 Jul 2021 11:19:40 +0100 Subject: [PATCH 11/27] feat(objectPreview): update ImageObjectPreview --- .../core/ObjectPreview/ImageObjectPreview.js | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/components/core/ObjectPreview/ImageObjectPreview.js b/components/core/ObjectPreview/ImageObjectPreview.js index 5a96b6ee..9c943d78 100644 --- a/components/core/ObjectPreview/ImageObjectPreview.js +++ b/components/core/ObjectPreview/ImageObjectPreview.js @@ -25,12 +25,14 @@ const STYLES_FLUID_CONTAINER = css` const STYLES_IMAGE = css` object-fit: cover; + height: 100%; + width: 100%; `; const ImagePlaceholder = ({ blurhash }) => (
- +
(
); -export default function ImageObjectPreview({ url, file, ...props }) { +export default function ImageObjectPreview({ + url, + file, + //NOTE(amine): ImageObjectPreview is used to display cover image for other objects, so we need to pass the tag down + tag, + ...props +}) { const previewerRef = React.useRef(); const [isLoading, setLoading] = React.useState(true); const handleOnLoaded = () => setLoading(false); @@ -56,7 +64,7 @@ export default function ImageObjectPreview({ url, file, ...props }) { }); const { type, coverImage } = file.data; - const tag = type.split("/")[1]; + const imgTag = type.split("/")[1]; const blurhash = React.useMemo(() => { return file.data.blurhash && isBlurhashValid(file.data.blurhash) @@ -71,18 +79,15 @@ export default function ImageObjectPreview({ url, file, ...props }) { const imageUrl = coverImage ? Strings.getURLfromCID(coverImage?.cid) : url; return ( - +
{isInView && ( - - {/** NOTE(amine): if it's loaded */} - {`${file.name} - + {`${file.name} )} {shouldShowPlaceholder && }
From b16ad5d587b6be01ec54c230d18867efa0185936 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 20 Jul 2021 11:19:49 +0100 Subject: [PATCH 12/27] feat(objectPreview): update FontObjectPreview --- components/core/ObjectPreview/FontObjectPreview.js | 1 - 1 file changed, 1 deletion(-) diff --git a/components/core/ObjectPreview/FontObjectPreview.js b/components/core/ObjectPreview/FontObjectPreview.js index 94418d73..c835847e 100644 --- a/components/core/ObjectPreview/FontObjectPreview.js +++ b/components/core/ObjectPreview/FontObjectPreview.js @@ -19,7 +19,6 @@ const STYLES_TEXT_PREVIEW = (theme) => css` `; const STYLES_LETTER = (theme) => css` - transform: translateY(-25%); overflow: hidden; font-size: ${theme.typescale.lvl8}; `; From e288c3daa1e2ee0884d2fbcfe78bdb8ab4a79461 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 20 Jul 2021 11:20:00 +0100 Subject: [PATCH 13/27] feat(objectPreview): update TextObjectPreview --- .../core/ObjectPreview/TextObjectPreview.js | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/components/core/ObjectPreview/TextObjectPreview.js b/components/core/ObjectPreview/TextObjectPreview.js index e85aa374..b64c71f4 100644 --- a/components/core/ObjectPreview/TextObjectPreview.js +++ b/components/core/ObjectPreview/TextObjectPreview.js @@ -7,8 +7,8 @@ import * as Utilities from "~/common/utilities"; import { P3 } from "~/components/system"; import { css } from "@emotion/react"; +import FilePlaceholder from "~/components/core/ObjectPreview/placeholders/File"; import ObjectPreviewPrimitive from "./ObjectPreviewPrimitive"; -import TextPlaceholder from "./placeholders/Text"; const STYLES_CONTAINER = css` position: relative; @@ -16,18 +16,6 @@ const STYLES_CONTAINER = css` height: 100%; justify-content: center; `; - -const STYLES_TAG = (theme) => css` - position: absolute; - text-transform: uppercase; - background-color: ${theme.semantic.bgLight}; - bottom: 26%; - left: 50%; - transform: translateX(-50%); - padding: 2px 8px; - border-radius: 4px; -`; - const STYLES_TEXT_PREVIEW = (theme) => css({ height: "100%", @@ -60,10 +48,7 @@ export default function TextObjectPreview({ url, file, ...props }) {
{error ? ( <> - -
- {tag} -
+ ) : (
From 12355442b07413f18fb6efeec80f81645855a069 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 20 Jul 2021 11:20:49 +0100 Subject: [PATCH 14/27] feat(ObjectPreviewPrimitive): update imports --- .../ObjectPreview/ObjectPreviewPrimitive.js | 184 +++++++----------- .../core/ObjectPreview/placeholders/index.js | 5 - 2 files changed, 70 insertions(+), 119 deletions(-) diff --git a/components/core/ObjectPreview/ObjectPreviewPrimitive.js b/components/core/ObjectPreview/ObjectPreviewPrimitive.js index abffbfb0..616077cf 100644 --- a/components/core/ObjectPreview/ObjectPreviewPrimitive.js +++ b/components/core/ObjectPreview/ObjectPreviewPrimitive.js @@ -1,94 +1,55 @@ import * as React from "react"; -import * as Constants from "~/common/constants"; -import * as Styles from "~/common/styles"; import { css } from "@emotion/react"; -import { H4, P2, P3 } from "~/components/system/components/Typography"; +import { H5, P3 } from "~/components/system/components/Typography"; import { AspectRatio } from "~/components/system"; import { LikeButton, SaveButton } from "./components"; import { useLikeHandler, useSaveHandler } from "~/common/hooks"; -import { Link } from "~/components/core/Link"; +import { motion, AnimatePresence } from "framer-motion"; import ImageObjectPreview from "./ImageObjectPreview"; -const STYLES_BACKGROUND_LIGHT = (theme) => css` +const STYLES_WRAPPER = (theme) => css` + position: relative; background-color: ${theme.semantic.bgLight}; - box-shadow: 0 0 0 1px ${theme.semantic.bgLight}; - border-radius: 8px; -`; - -const STYLES_WRAPPER = css` - border-radius: 8px; + transition: box-shadow 0.2s; + box-shadow: 0 0 0 0.5px ${theme.semantic.bgGrayLight}, ${theme.shadow.lightSmall}; + border-radius: 16px; overflow: hidden; `; const STYLES_DESCRIPTION = (theme) => css` + box-shadow: 0 -0.5px 0.5px ${theme.semantic.bgGrayLight}; + border-radius: 0px 0px 16px 16px; box-sizing: border-box; width: 100%; - padding: 12px 16px 12px; - position: absolute; - bottom: 0; - left: 0; - background-color: ${theme.system.white}; - - @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { - background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #ffffff 100%); - backdrop-filter: blur(75px); - -webkit-backdrop-filter: blur(75px); - } + padding: 9px 16px 8px; @media (max-width: ${theme.sizes.mobile}px) { padding: 8px; } `; - -const STYLES_DESCRIPTION_META = css` - justify-content: space-between; - margin-top: 12px; -`; - -const STYLES_REACTIONS_CONTAINER = css` - display: flex; - & > * + * { - margin-left: 32px; - } -`; - -const STYLES_REACTION = css` - display: flex; - & > * + * { - margin-left: 8px; - } -`; - -const STYLES_PROFILE_IMAGE = css` - display: block; - background-color: ${Constants.semantic.bgLight}; - flex-shrink: 0; - object-fit: cover; - height: 20px; - width: 20px; - border-radius: 2px; -`; - -const STYLES_DESCRIPTION_TAG = (theme) => css` - position: absolute; - top: -32px; - left: 12px; - text-transform: uppercase; - border: 1px solid ${theme.system.grayLight5}; - background-color: ${theme.semantic.bgLight}; - padding: 2px 8px; - border-radius: 4px; +const STYLES_PREVIEW = css` + overflow: hidden; `; const STYLES_SELECTED_RING = (theme) => css` box-shadow: 0 0 0 2px ${theme.system.blue}; `; -export default function ObjectPreviewPremitive({ +const STYLES_CONTROLS = css` + position: absolute; + z-index: 1; + right: 16px; + top: 16px; + & > * + * { + margin-top: 8px !important; + } +`; + +export default function ObjectPreviewPrimitive({ children, - tag, + tag = "FILE", file, isSelected, viewer, @@ -100,73 +61,68 @@ export default function ObjectPreviewPremitive({ const { like, isLiked, likeCount } = useLikeHandler({ file, viewer }); const { save, isSaved, saveCount } = useSaveHandler({ file, viewer }); + const [showControls, setShowControls] = React.useState(false); + const showControlsVisibility = () => setShowControls(true); + const hideControlsVisibility = () => setShowControls(false); + const title = file.data.name || file.filename; if (file?.data?.coverImage && !isImage) { return ( - + ); } const showSaveButton = viewer?.id !== file?.ownerId; return (
- -
- -
{children}
-
- -
- {tag && ( -
- {tag} -
- )} -

- {title} -

- -
-
-
- - {likeCount} -
- {showSaveButton && ( -
- - {saveCount} -
- )} -
- {owner && ( - - {`${owner.username} (e.target.src = Constants.profileDefaultPicture)} - /> - - )} -
-
-
+ + {showControls && ( + + + {showSaveButton && } + + )} + + +
{children}
+
+
+
+ {title} +
+ + {tag} + +
+
); } diff --git a/components/core/ObjectPreview/placeholders/index.js b/components/core/ObjectPreview/placeholders/index.js index ee020824..a38e29bc 100644 --- a/components/core/ObjectPreview/placeholders/index.js +++ b/components/core/ObjectPreview/placeholders/index.js @@ -9,7 +9,6 @@ import PdfPlaceholder from "./PDF"; import AudioPlaceholder from "./Audio"; import CodePlaceholder from "./Code"; import EpubPlaceholder from "./EPUB"; -import TextPlaceholder from "./Text"; import KeynotePlaceholder from "./Keynote"; import Object3DPlaceholder from "./3D"; import FilePlaceholder from "./File"; @@ -66,10 +65,6 @@ const PlaceholderPrimitive = ({ file, ratio }) => { return ; } - if (Validations.isMarkdown(file.filename, type)) { - return ; - } - if (Validations.is3dFile(file.filename)) { return ; } From b4003249b1680cb76fcff4cdf4da57bd054d7ca3 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 20 Jul 2021 11:21:14 +0100 Subject: [PATCH 15/27] feat(typography): add support for 'as' prop --- components/system/components/Typography.js | 148 +++++++++++---------- 1 file changed, 78 insertions(+), 70 deletions(-) diff --git a/components/system/components/Typography.js b/components/system/components/Typography.js index 09dcc1f8..4a0c707c 100644 --- a/components/system/components/Typography.js +++ b/components/system/components/Typography.js @@ -3,7 +3,7 @@ import * as Constants from "~/common/constants"; import * as Styles from "~/common/styles"; import * as Strings from "~/common/strings"; -import { css } from "@emotion/react"; +import { css, jsx } from "@emotion/react"; const LINK_STYLES = ` font-family: ${Constants.font.text}; @@ -137,123 +137,134 @@ export const A = ({ href, children, dark }) => { // ${ANCHOR} // `; -export const H1 = ({ nbrOflines, children, color, ...props }) => { +export const H1 = ({ as = "h1", nbrOflines, children, color, ...props }) => { const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); const COLOR_STYLES = useColorProp(color); - return ( -

- {children} -

+ + return jsx( + as, + { ...props, css: [Styles.H1, TRUNCATE_STYLE, COLOR_STYLES, props?.css] }, + children ); }; -export const H2 = ({ nbrOflines, children, color, ...props }) => { +export const H2 = ({ as = "h2", nbrOflines, children, color, ...props }) => { const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); const COLOR_STYLES = useColorProp(color); - return ( -

- {children} -

+ + return jsx( + as, + { ...props, css: [Styles.H2, TRUNCATE_STYLE, COLOR_STYLES, props?.css] }, + children ); }; -export const H3 = ({ nbrOflines, children, color, ...props }) => { +export const H3 = ({ as = "h3", nbrOflines, children, color, ...props }) => { const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); const COLOR_STYLES = useColorProp(color); - return ( -

- {children} -

+ + return jsx( + as, + { ...props, css: [Styles.H3, TRUNCATE_STYLE, COLOR_STYLES, props?.css] }, + children ); }; -export const H4 = ({ nbrOflines, children, color, ...props }) => { +export const H4 = ({ as = "h4", nbrOflines, children, color, ...props }) => { const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); const COLOR_STYLES = useColorProp(color); - return ( -

- {children} -

+ + return jsx( + as, + { ...props, css: [Styles.H4, TRUNCATE_STYLE, COLOR_STYLES, props?.css] }, + children ); }; -export const H5 = ({ nbrOflines, children, color, ...props }) => { +export const H5 = ({ as = "h5", nbrOflines, children, color, ...props }) => { const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); const COLOR_STYLES = useColorProp(color); - return ( -
- {children} -
+ + return jsx( + as, + { ...props, css: [Styles.H5, TRUNCATE_STYLE, COLOR_STYLES, props?.css] }, + children ); }; -export const P1 = ({ nbrOflines, children, color, ...props }) => { +export const P1 = ({ as = "p", nbrOflines, children, color, ...props }) => { const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); const COLOR_STYLES = useColorProp(color); - return ( -

- {children} -

+ + return jsx( + as, + { ...props, css: [Styles.P1, TRUNCATE_STYLE, COLOR_STYLES, props?.css] }, + children ); }; -export const P2 = ({ nbrOflines, children, color, ...props }) => { +export const P2 = ({ as = "p", nbrOflines, children, color, ...props }) => { const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); const COLOR_STYLES = useColorProp(color); - return ( -

- {children} -

+ + return jsx( + as, + { ...props, css: [Styles.P2, TRUNCATE_STYLE, COLOR_STYLES, props?.css] }, + children ); }; -export const P3 = ({ nbrOflines, children, color, ...props }) => { +export const P3 = ({ as = "p", nbrOflines, children, color, ...props }) => { const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); const COLOR_STYLES = useColorProp(color); - return ( -

- {children} -

+ return jsx( + as, + { ...props, css: [Styles.P3, TRUNCATE_STYLE, COLOR_STYLES, props?.css] }, + children ); }; -export const C1 = ({ nbrOflines, children, color, ...props }) => { +export const C1 = ({ as = "p", nbrOflines, children, color, ...props }) => { const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); const COLOR_STYLES = useColorProp(color); - return ( -

- {children} -

+ + return jsx( + as, + { ...props, css: [Styles.C1, TRUNCATE_STYLE, COLOR_STYLES, props?.css] }, + children ); }; -export const C2 = ({ nbrOflines, children, color, ...props }) => { +export const C2 = ({ as = "p", nbrOflines, children, color, ...props }) => { const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); const COLOR_STYLES = useColorProp(color); - return ( -

- {children} -

+ + return jsx( + as, + { ...props, css: [Styles.C2, TRUNCATE_STYLE, COLOR_STYLES, props?.css] }, + children ); }; -export const C3 = ({ nbrOflines, children, color, ...props }) => { +export const C3 = ({ as = "p", nbrOflines, children, color, ...props }) => { const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); const COLOR_STYLES = useColorProp(color); - return ( -

- {children} -

+ + return jsx( + as, + { ...props, css: [Styles.C3, TRUNCATE_STYLE, COLOR_STYLES, props?.css] }, + children ); }; -export const B1 = ({ nbrOflines, children, color, ...props }) => { +export const B1 = ({ as = "p", nbrOflines, children, color, ...props }) => { const TRUNCATE_STYLE = React.useMemo(() => truncateElements(nbrOflines), [nbrOflines]); const COLOR_STYLES = useColorProp(color); - return ( -

- {children} -

+ + return jsx( + as, + { ...props, css: [Styles.B1, TRUNCATE_STYLE, COLOR_STYLES, props?.css] }, + children ); }; @@ -262,18 +273,16 @@ const STYLES_UL = css` padding-left: 24px; `; -export const UL = (props) => { - return
    ; -}; +export const UL = ({ as = "ul", children, props }) => + jsx(as, { ...props, css: [STYLES_UL, props?.css] }, children); const STYLES_OL = css` box-sizing: border-box; padding-left: 24px; `; -export const OL = (props) => { - return
      ; -}; +export const OL = ({ as = "ol", children, props }) => + jsx(as, { ...props, css: [STYLES_OL, props?.css] }, children); const STYLES_LI = css` box-sizing: border-box; @@ -284,6 +293,5 @@ const STYLES_LI = css` } `; -export const LI = (props) => { - return
    1. ; -}; +export const LI = ({ as = "li", children, props }) => + jsx(as, { ...props, css: [STYLES_LI, props?.css] }, children); From c40fb9887faa52f59d88d4902d6499d416ef3300 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 20 Jul 2021 11:22:09 +0100 Subject: [PATCH 16/27] feat(objectPreview): move placeholders to objectPreview index --- components/core/ObjectPreview/index.js | 87 +++++++++++++++++++++----- 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/components/core/ObjectPreview/index.js b/components/core/ObjectPreview/index.js index 1fd37ba9..f0a9e872 100644 --- a/components/core/ObjectPreview/index.js +++ b/components/core/ObjectPreview/index.js @@ -1,19 +1,28 @@ import * as React from "react"; import * as Validations from "~/common/validations"; import * as Strings from "~/common/strings"; +import * as Styles from "~/common/styles"; +import * as Utilities from "~/common/utilities"; + +import { css } from "@emotion/react"; import ImageObjectPreview from "./ImageObjectPreview"; -import VideoObjectPreview from "./VideoObjectPreview"; import TextObjectPreview from "./TextObjectPreview"; -import PdfObjectPreview from "./PdfObjectPreview"; -import EpubObjectPreview from "./EpubObjectPreview"; -import AudioObjectPreview from "./AudioObjectPreview"; -import KeynoteObjectPreview from "./KeynoteObjectPreview"; -import DefaultObjectPreview from "./DefaultObjectPreview"; -import Object3DPreview from "./3dObjectPreview"; -import CodeObjectPreview from "./CodeObjectPreview"; import FontObjectPreview from "./FontObjectPreview"; +// Note(amine): placeholders +import PdfPlaceholder from "~/components/core/ObjectPreview/placeholders/PDF"; +import VideoPlaceholder from "~/components/core/ObjectPreview/placeholders/Video"; +import AudioPlaceholder from "~/components/core/ObjectPreview/placeholders/Audio"; +import EbookPlaceholder from "~/components/core/ObjectPreview/placeholders/EPUB"; +import KeynotePlaceholder from "~/components/core/ObjectPreview/placeholders/Keynote"; +import CodePlaceholder from "~/components/core/ObjectPreview/placeholders/Code"; +import Object3DPlaceholder from "~/components/core/ObjectPreview/placeholders/3D"; +import FilePlaceholder from "~/components/core/ObjectPreview/placeholders/File"; + +// NOTE(amine): previews +import ObjectPreviewPrimitive from "~/components/core/ObjectPreview/ObjectPreviewPrimitive"; + const ObjectPreview = ({ file, ...props }) => { const { type } = file.data; @@ -24,27 +33,54 @@ const ObjectPreview = ({ file, ...props }) => { } if (type.startsWith("video/")) { - return ; + const tag = type.split("/")[1]; + return ( + + + + ); } if (Validations.isPdfType(type)) { - return ; + return ( + + + + ); } if (type.startsWith("audio/")) { - return ; + const tag = Utilities.getFileExtension(file.filename) || "audio"; + return ( + + + + ); } if (type === "application/epub+zip") { - return ; + return ( + + + + ); } if (file.filename.endsWith(".key")) { - return ; + return ( + + + + ); } if (Validations.isCodeFile(file.filename)) { - return ; + const tag = Utilities.getFileExtension(file.filename) || "code"; + return ( + + + + ); } if (Validations.isFontFile(file.filename)) { @@ -56,10 +92,29 @@ const ObjectPreview = ({ file, ...props }) => { } if (Validations.is3dFile(file.filename)) { - return ; + return ( + + + + ); } - return ; + return ( + + + + ); }; export default React.memo(ObjectPreview); + +const STYLES_CONTAINER = css` + height: 100%; +`; +const PlaceholderWrapper = ({ children, ...props }) => { + return ( + +
      {children}
      +
      + ); +}; From 7dad0cad06313872fa90cbafafba2b4c7e4ae145 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 20 Jul 2021 12:31:47 +0100 Subject: [PATCH 17/27] feat(objectPreview): add hover to see description --- .../ObjectPreview/ObjectPreviewPrimitive.js | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/components/core/ObjectPreview/ObjectPreviewPrimitive.js b/components/core/ObjectPreview/ObjectPreviewPrimitive.js index 616077cf..b8b1ad75 100644 --- a/components/core/ObjectPreview/ObjectPreviewPrimitive.js +++ b/components/core/ObjectPreview/ObjectPreviewPrimitive.js @@ -23,12 +23,20 @@ const STYLES_DESCRIPTION = (theme) => css` border-radius: 0px 0px 16px 16px; box-sizing: border-box; width: 100%; - padding: 9px 16px 8px; + max-height: 61px; @media (max-width: ${theme.sizes.mobile}px) { padding: 8px; } `; + +const STYLES_DESCRIPTION_INNER = (theme) => css` + background-color: ${theme.semantic.bgLight}; + padding: 9px 16px 8px; + border-radius: 16px; + height: calc(170px + 61px); +`; + const STYLES_PREVIEW = css` overflow: hidden; `; @@ -60,10 +68,16 @@ export default function ObjectPreviewPrimitive({ }) { const { like, isLiked, likeCount } = useLikeHandler({ file, viewer }); const { save, isSaved, saveCount } = useSaveHandler({ file, viewer }); + const showSaveButton = viewer?.id !== file?.ownerId; - const [showControls, setShowControls] = React.useState(false); - const showControlsVisibility = () => setShowControls(true); - const hideControlsVisibility = () => setShowControls(false); + const [areControlsVisible, setShowControls] = React.useState(false); + const showControls = () => setShowControls(true); + const hideControls = () => setShowControls(false); + + const [isBodyVisible, setShowBody] = React.useState(false); + const showBody = () => setShowBody(true); + const hideBody = () => setShowBody(false); + const body = file?.data?.body; const title = file.data.name || file.filename; @@ -78,11 +92,9 @@ export default function ObjectPreviewPrimitive({ /> ); } - const showSaveButton = viewer?.id !== file?.ownerId; + return (
      - {showControls && ( + {areControlsVisible && ( -
      {children}
      +
      + {children} +
      -
      -
      +
      +
      {title}
      - + {tag} -
      +
      + {body ? body : "sorry, no description available."} +
      +
      ); From 382a021a386b16af236b2179d64d8045d124b051 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Mon, 26 Jul 2021 17:42:53 +0100 Subject: [PATCH 18/27] feat(ProfilePreview): update to new design --- common/constants.js | 4 +- common/styles.js | 2 +- common/svg.js | 19 ++ components/core/ProfilePreviewBlock.js | 263 +++++++------------------ 4 files changed, 89 insertions(+), 199 deletions(-) diff --git a/common/constants.js b/common/constants.js index 430e4812..f7fefd80 100644 --- a/common/constants.js +++ b/common/constants.js @@ -212,8 +212,8 @@ export const grids = { mobile: { width: 300, rowGap: 8 }, }, profile: { - desktop: { width: 432, rowGap: 16 }, - mobile: { width: 344, rowGap: 8 }, + desktop: { width: 248, rowGap: 16 }, + mobile: { width: 248, rowGap: 8 }, }, }; diff --git a/common/styles.js b/common/styles.js index 0b7dac06..3c02e361 100644 --- a/common/styles.js +++ b/common/styles.js @@ -111,7 +111,7 @@ export const P3 = css` font-family: ${Constants.font.text}; font-size: 0.75rem; font-weight: normal; - line-height: 1.3; + line-height: 1.33; letter-spacing: 0px; ${TEXT} diff --git a/common/svg.js b/common/svg.js index 9123a768..38352112 100644 --- a/common/svg.js +++ b/common/svg.js @@ -1969,3 +1969,22 @@ export const RSS = (props) => ( /> ); + +export const Box = (props) => ( + + + + +); diff --git a/components/core/ProfilePreviewBlock.js b/components/core/ProfilePreviewBlock.js index fb4cb3fc..9c7239e2 100644 --- a/components/core/ProfilePreviewBlock.js +++ b/components/core/ProfilePreviewBlock.js @@ -3,95 +3,20 @@ import * as Styles from "~/common/styles"; import * as Typography from "~/components/system/components/Typography"; import * as Strings from "~/common/strings"; import * as Constants from "~/common/constants"; +import * as SVG from "~/common/svg"; -import { Divider } from "~/components/system/components/Divider"; -import { Logo } from "~/common/logo"; import { ButtonPrimary, ButtonTertiary } from "~/components/system/components/Buttons"; import { css } from "@emotion/react"; -import { LikeButton, SaveButton } from "~/components/core/ObjectPreview/components"; -import { useLikeHandler, useSaveHandler } from "~/common/hooks"; import { useFollowProfileHandler } from "~/common/hooks"; -import ObjectPlaceholder from "~/components/core/ObjectPreview/placeholders"; - -const STYLES_CONTROLLS = css` - display: flex; - flex-direction: column; - align-items: flex-end; -`; - -const useProfileCarrousel = ({ objects }) => { - const [selectedIdx, setSelectedIdx] = React.useState(0); - const selectBatchIdx = (idx) => setSelectedIdx(idx); - const selectedBatch = objects[selectedIdx]; - return { selectBatchIdx, selectedBatch, selectedIdx }; -}; - -const STYLES_HIGHLIGHT_BUTTON = (theme) => css` - box-sizing: border-box; - display: block; - padding: 4px 16px 4px 12px; - border: none; - background-color: unset; - div { - width: 8px; - height: 8px; - background-color: ${theme.system.gray}; - border-radius: 50%; - } -`; - -const STYLES_PLACEHOLDER = css` - height: 64px; - min-width: 86px; - width: 86px; -`; - -const ProfilePreviewFile = ({ file, viewer }) => { - const { like, isLiked, likeCount } = useLikeHandler({ file, viewer }); - const { save, isSaved, saveCount } = useSaveHandler({ file, viewer }); - - const title = file.data.name || file.filename; - const { body } = file.data; - return ( -
      - -
      - - {title} - - - {body} - -
      -
      - - - {likeCount} - -
      -
      - - - {saveCount} - -
      -
      -
      -
      - ); -}; - const STYLES_CONTAINER = (theme) => css` - border-radius: 8px; - overflow: hidden; - box-shadow: 0 0 0 1px ${theme.semantic.bgGrayLight}; - background-color: ${theme.semantic.bgGrayLight}; -`; - -const STYLES_PROFILE_DESCRIPTION = (theme) => css` - background-color: ${theme.system.white}; - padding: 16px; + width: 100%; + position: relative; + background-color: ${theme.semantic.bgLight}; + box-shadow: 0 0 0 0.5px ${theme.semantic.bgGrayLight}, ${theme.shadow.lightSmall}; + border-radius: 16px; + padding: 24px 16px 16px; + ${Styles.VERTICAL_CONTAINER_CENTERED} `; const STYLES_PROFILE_PREVIEW = (theme) => css` @@ -105,30 +30,7 @@ const STYLES_PROFILE_PREVIEW = (theme) => css` } `; -const STYLES_FILES_PREVIEWS = css` - padding: 16px; - padding-right: 0px; - height: 176px; -`; - export default function ProfilePreviewBlock({ onAction, viewer, profile }) { - const filePreviews = React.useMemo(() => { - const files = profile?.objects || []; - let previews = []; - for (let i = 0; i < files.length; i++) { - const batch = []; - if (files[i * 2]) batch.push(files[i * 2]); - if (files[i * 2 + 1]) batch.push(files[i * 2 + 1]); - if (batch.length > 0) previews.push(batch); - if (previews.length === 3 || batch.length < 2) break; - } - return previews; - }, [profile]); - - const { selectBatchIdx, selectedBatch, selectedIdx } = useProfileCarrousel({ - objects: filePreviews, - }); - const { handleFollow, isFollowing } = useFollowProfileHandler({ onAction, viewer, @@ -137,101 +39,70 @@ export default function ProfilePreviewBlock({ onAction, viewer, profile }) { const isOwner = viewer?.id === profile.id; - const nbrOfFiles = profile?.objects?.length || 0; - const doesProfileHaveFiles = nbrOfFiles === 0; - return (
      -
      - {`${profile.username}`} (e.target.src = Constants.profileDefaultPicture)} - /> -
      -
      - {profile.username} - {profile?.data?.body && ( - - {profile.data.body} - - )} -
      -
      - - {profile.fileCount} {Strings.pluralize("file", profile.fileCount)} - - - {profile.slateCount} {Strings.pluralize("collection", profile.slateCount)} - -
      + {`${profile.username}`} (e.target.src = Constants.profileDefaultPicture)} + /> +
      + {profile.username} +
      - {!isOwner && - (isFollowing ? ( - { - e.stopPropagation(); - e.preventDefault(); - handleFollow(profile.id); - }} - > - Following - - ) : ( - { - e.stopPropagation(); - e.preventDefault(); - handleFollow(profile.id); - }} - > - Follow - - ))} +
      +
      + + + {profile.fileCount} {Strings.pluralize("file", profile.fileCount)} + +
      +
      + + + {profile.slateCount} {Strings.pluralize("collection", profile.slateCount)} +
      -
      -
      - {!doesProfileHaveFiles ? ( - selectedBatch.map((file, i) => ( - - {i === 1 && } - - - )) - ) : ( -
      - - No files in this collection -
      - )} -
      - { -
      - {filePreviews.map((preview, i) => ( - - ))} -
      - } -
      + + + {profile?.data?.body || "No Description"} + + + {!isOwner && + (isFollowing ? ( + { + e.stopPropagation(); + e.preventDefault(); + handleFollow(profile.id); + }} + full + > + Following + + ) : ( + { + e.stopPropagation(); + e.preventDefault(); + handleFollow(profile.id); + }} + full + > + Follow + + ))}
      ); } From 499c57d93c1d79e8eff30395701f0ceff86f14bf Mon Sep 17 00:00:00 2001 From: Aminejv Date: Mon, 26 Jul 2021 17:43:54 +0100 Subject: [PATCH 19/27] feat(CollectionPreview): update collection preview to new design --- .../FilesCollectionPreview.js | 279 ---------------- .../ImageCollectionPreview.js | 248 -------------- .../components/FollowButton.js | 127 +++++--- .../core/CollectionPreviewBlock/index.js | 305 ++++++++++++++++-- 4 files changed, 363 insertions(+), 596 deletions(-) delete mode 100644 components/core/CollectionPreviewBlock/FilesCollectionPreview.js delete mode 100644 components/core/CollectionPreviewBlock/ImageCollectionPreview.js diff --git a/components/core/CollectionPreviewBlock/FilesCollectionPreview.js b/components/core/CollectionPreviewBlock/FilesCollectionPreview.js deleted file mode 100644 index 32a43a59..00000000 --- a/components/core/CollectionPreviewBlock/FilesCollectionPreview.js +++ /dev/null @@ -1,279 +0,0 @@ -import * as React from "react"; -import * as Strings from "~/common/strings"; -import * as Typography from "~/components/system/components/Typography"; -import * as Styles from "~/common/styles"; -import * as Constants from "~/common/constants"; - -import { Divider } from "~/components/system/components/Divider"; -import { Logo } from "~/common/logo"; -import { css } from "@emotion/react"; -import { LikeButton, SaveButton } from "~/components/core/ObjectPreview/components"; -import { useLikeHandler, useSaveHandler } from "~/common/hooks"; -import { FollowButton } from "~/components/core/CollectionPreviewBlock/components"; -import { useFollowHandler } from "~/components/core/CollectionPreviewBlock/hooks"; -import { Link } from "~/components/core/Link"; - -import ObjectPlaceholder from "~/components/core/ObjectPreview/placeholders"; - -const STYLES_CONTAINER = (theme) => css` - display: flex; - flex-direction: column; - position: relative; - border-radius: 8px; - width: 100%; - border-radius: 8px; - overflow: hidden; - background-color: ${theme.system.white}; - height: 311px; - box-shadow: 0 0 0 1px ${theme.semantic.bgGrayLight}; - @media (max-width: ${theme.sizes.mobile}px) { - height: 281px; - } -`; - -const STYLES_DESCRIPTION_CONTAINER = (theme) => css` - display: flex; - flex-direction: column; - position: relative; - padding: 12px 16px; - border-radius: 0px 0px 8px 8px; - background-color: ${theme.system.white}; - @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { - background-color: ${theme.semantic.bgGrayLight}; - -webkit-backdrop-filter: blur(75px); - backdrop-filter: blur(75px); - } - width: 100%; - margin-top: auto; - @media (max-width: ${theme.sizes.mobile}px) { - padding: 12px; - } -`; - -const STYLES_SPACE_BETWEEN = css` - justify-content: space-between; -`; - -const STYLES_CONTROLLS = css` - display: flex; - flex-direction: column; - align-items: flex-end; -`; - -const STYLES_TEXT_BLACK = (theme) => css` - color: ${theme.semantic.textBlack}; -`; - -const STYLES_PROFILE_IMAGE = (theme) => css` - background-color: ${theme.semantic.bgLight}; - height: 20px; - width: 20px; - border-radius: 4px; - object-fit: cover; -`; - -const STYLES_HIGHLIGHT_BUTTON = (theme) => css` - box-sizing: border-box; - display: block; - padding: 4px 16px; - border: none; - background-color: unset; - div { - width: 8px; - height: 8px; - background-color: ${theme.system.gray}; - border-radius: 50%; - } -`; - -const STYLES_METRICS = (theme) => css` - margin-top: 16px; - @media (max-width: ${theme.sizes.mobile}px) { - margin-top: 12px; - } - ${Styles.CONTAINER_CENTERED}; - ${STYLES_SPACE_BETWEEN}; -`; - -const STYLES_FILES_PREVIEWS = (theme) => css` - flex-grow: 1; - padding: 16px; - padding-right: 0px; - @media (max-width: ${theme.sizes.mobile}px) { - padding: 12px; - padding-right: 0px; - } -`; - -const STYLES_PLACEHOLDER = css` - height: 64px; - min-width: 86px; - width: 86px; -`; - -const CollectionPreviewFile = ({ file, viewer }) => { - const { like, isLiked, likeCount } = useLikeHandler({ file, viewer }); - const { save, isSaved, saveCount } = useSaveHandler({ file, viewer }); - - const title = file.data.name || file.filename; - const { body } = file.data; - - return ( -
      - -
      - - {title} - - - {body} - -
      -
      - - - {likeCount} - -
      -
      - - - {saveCount} - -
      -
      -
      -
      - ); -}; - -const useCollectionCarrousel = ({ objects }) => { - const [selectedIdx, setSelectedIdx] = React.useState(0); - const selectBatchIdx = (idx) => setSelectedIdx(idx); - const selectedBatch = objects[selectedIdx]; - return { selectBatchIdx, selectedBatch, selectedIdx }; -}; - -export default function CollectionPreview({ collection, viewer, owner, onAction }) { - const { follow, followCount, isFollowed } = useFollowHandler({ collection, viewer }); - const filePreviews = React.useMemo(() => { - const files = collection?.objects || []; - let previews = []; - for (let i = 0; i < files.length; i++) { - const batch = []; - if (files[i * 2]) batch.push(files[i * 2]); - if (files[i * 2 + 1]) batch.push(files[i * 2 + 1]); - if (batch.length > 0) previews.push(batch); - if (previews.length === 3 || batch.length < 2) break; - } - return previews; - }, [collection]); - - const { selectBatchIdx, selectedBatch, selectedIdx } = useCollectionCarrousel({ - objects: filePreviews, - }); - - const nbrOfFiles = collection?.objects?.length || 0; - const isCollectionEmpty = nbrOfFiles === 0; - - const showFollowButton = collection.ownerId !== viewer?.id; - - return ( -
      -
      -
      - {!isCollectionEmpty ? ( - selectedBatch.map((file, i) => ( - - {i === 1 && } - - - )) - ) : ( -
      - - No files in this collection -
      - )} -
      - { -
      - {filePreviews.map((preview, i) => ( - - ))} -
      - } -
      -
      -
      -
      - - {collection.slatename} - - - {nbrOfFiles} {Strings.pluralize("Object", nbrOfFiles)} - -
      - - {collection?.data?.body && ( - - {collection?.data?.body} - - )} -
      - -
      -
      - - - {followCount} - -
      - - {owner && ( -
      - - {`${owner.username} (e.target.src = Constants.profileDefaultPicture)} - /> - - - - {owner.username} - - -
      - )} -
      -
      -
      - ); -} diff --git a/components/core/CollectionPreviewBlock/ImageCollectionPreview.js b/components/core/CollectionPreviewBlock/ImageCollectionPreview.js deleted file mode 100644 index 8cd2a22e..00000000 --- a/components/core/CollectionPreviewBlock/ImageCollectionPreview.js +++ /dev/null @@ -1,248 +0,0 @@ -import * as React from "react"; -import * as Strings from "~/common/strings"; -import * as Typography from "~/components/system/components/Typography"; -import * as Styles from "~/common/styles"; -import * as Constants from "~/common/constants"; - -import { useInView } from "~/common/hooks"; -import { isBlurhashValid } from "blurhash"; -import { Blurhash } from "react-blurhash"; -import { css } from "@emotion/react"; -import { FollowButton } from "~/components/core/CollectionPreviewBlock/components"; -import { useFollowHandler } from "~/components/core/CollectionPreviewBlock/hooks"; -import { Link } from "~/components/core/Link"; - -const STYLES_CONTAINER = (theme) => css` - display: flex; - flex-direction: column; - position: relative; - border-radius: 8px; - width: 100%; - border-radius: 8px; - overflow: hidden; - - height: 311px; - @media (max-width: ${theme.sizes.mobile}px) { - height: 281px; - } -`; - -const STYLES_PREVIEW = (theme) => css` - position: absolute; - left: 0; - top: 0; - height: 100%; - width: 100%; - background-color: ${theme.system.white}; - background-size: cover; - img { - width: 100%; - height: 100%; - object-fit: cover; - } -`; - -const STYLES_DESCRIPTION_CONTAINER = (theme) => css` - display: flex; - flex-direction: column; - position: relative; - padding: 12px 16px; - border-radius: 8px; - background-color: ${theme.system.white}; - @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { - background-color: ${theme.semantic.bgBlurLight6OP}; - -webkit-backdrop-filter: blur(75px); - backdrop-filter: blur(75px); - } - width: 100%; - margin-top: auto; -`; - -const STYLES_SPACE_BETWEEN = css` - justify-content: space-between; -`; - -const STYLES_CONTROLLS = css` - display: flex; - flex-direction: column; - align-items: flex-end; - position: relative; - padding: 16px 0px 16px 16px; -`; - -const STYLES_PROFILE_IMAGE = (theme) => css` - background-color: ${theme.semantic.bgLight}; - height: 20px; - width: 20px; - border-radius: 4px; - object-fit: cover; -`; - -const STYLES_HIGHLIGHT_BUTTON = (theme) => css` - box-sizing: border-box; - display: block; - padding: 4px 16px; - border: none; - background-color: unset; - div { - width: 8px; - height: 8px; - background-color: ${theme.system.gray}; - border-radius: 50%; - } -`; - -const STYLES_METRICS = (theme) => css` - margin-top: 16px; - @media (max-width: ${theme.sizes.mobile}px) { - margin-top: 12px; - } - ${Styles.CONTAINER_CENTERED}; - ${STYLES_SPACE_BETWEEN} -`; - -const getFileBlurHash = (file) => { - const coverImage = file?.data?.coverImage; - const coverImageBlurHash = coverImage?.data?.blurhash; - if (coverImage && isBlurhashValid(coverImageBlurHash)) return coverImageBlurHash; - - const blurhash = file?.data?.blurhash; - if (isBlurhashValid(blurhash)) return blurhash; - - return null; -}; - -const useCollectionCarrousel = ({ objects }) => { - const [selectedIdx, setSelectedIdx] = React.useState(0); - const selectImageByIdx = (idx) => setSelectedIdx(idx); - - const [imagesLoadedIdx, setImagesLoadedIdx] = React.useState({}); - const handleLoading = () => setImagesLoadedIdx((prev) => ({ ...prev, [selectedIdx]: true })); - - const isCurrentImageLoaded = imagesLoadedIdx[selectedIdx]; - const { image: selectedImage, blurhash } = objects[selectedIdx]; - return { - selectedImage, - selectedIdx, - isLoaded: isCurrentImageLoaded, - blurhash, - handleLoading, - selectImageByIdx, - }; -}; - -export default function ImageCollectionPreview({ collection, viewer, owner, onAction }) { - const { follow, followCount, isFollowed } = useFollowHandler({ collection, viewer }); - - const filePreviews = React.useMemo(() => { - const previews = collection.objects.map((object) => { - const coverImage = object?.data?.coverImage; - const image = coverImage || Strings.getURLfromCID(object.cid); - const blurhash = getFileBlurHash(object); - return { image, blurhash }; - }); - return previews.slice(0, 3); - }, [collection]); - - const previewerRef = React.useRef(); - const { isInView } = useInView({ - ref: previewerRef, - }); - - const { isLoaded, blurhash, selectedImage, handleLoading, selectedIdx, selectImageByIdx } = - useCollectionCarrousel({ objects: filePreviews }); - - const nbrOfFiles = collection?.objects?.length || 0; - - const showFollowButton = collection.ownerId !== viewer?.id; - return ( -
      - {isInView && ( -
      - {!isLoaded && blurhash && ( - - )} - Collection preview -
      - )} -
      - {filePreviews.map((preview, i) => ( - - ))} -
      -
      -
      -
      - - {collection.slatename} - - - {nbrOfFiles} {Strings.pluralize("Object", nbrOfFiles)} - -
      - {collection?.data?.body && ( - - {collection?.data?.body} - - )} -
      - -
      -
      - - - {followCount} - -
      - {owner && ( -
      - - {`${owner.username} (e.target.src = Constants.profileDefaultPicture)} - /> - - - - {owner.username} - - -
      - )} -
      -
      -
      - ); -} diff --git a/components/core/CollectionPreviewBlock/components/FollowButton.js b/components/core/CollectionPreviewBlock/components/FollowButton.js index c59baf39..a6974489 100644 --- a/components/core/CollectionPreviewBlock/components/FollowButton.js +++ b/components/core/CollectionPreviewBlock/components/FollowButton.js @@ -4,15 +4,31 @@ import * as Styles from "~/common/styles"; import { css } from "@emotion/react"; import { motion, useAnimation } from "framer-motion"; +import { P3 } from "~/components/system"; +import { useMounted } from "~/common/hooks"; -const STYLES_BUTTON_HOVER = (theme) => css` - :hover .button_path { - stroke: ${theme.system.blue}; +const STYLES_BUTTON = (theme) => css` + display: flex; + background-color: ${theme.semantic.bgBlurWhite}; + padding: 8px; + border-radius: 8px; + box-shadow: 0 0 0 1px ${theme.system.grayLight5}, ${theme.shadow.lightLarge}; + transition: box-shadow 0.3s; + :hover { + box-shadow: 0 0 0 1px ${theme.system.pink}, ${theme.shadow.lightLarge}; + } + + path { + transition: stroke 0.3s; + } + + :hover path { + stroke: ${theme.system.pink}; } `; -const STYLES_INLINE = css` - display: flex; +const STYLES_DISABLED = css` + cursor: not-allowed; `; const animate = async (controls) => { @@ -20,17 +36,7 @@ const animate = async (controls) => { await controls.start({ x: 0, y: 0 }); }; -const useMounted = (callback, depedencies) => { - const mountedRef = React.useRef(false); - React.useLayoutEffect(() => { - if (mountedRef.current && callback) { - callback(); - } - mountedRef.current = true; - }, depedencies); -}; - -export default function FollowButton({ onFollow, isFollowed, disabled, ...props }) { +export default function FollowButton({ onFollow, isFollowed, disabled, followCount, ...props }) { const controls = useAnimation(); useMounted(() => { @@ -41,45 +47,66 @@ export default function FollowButton({ onFollow, isFollowed, disabled, ...props }, [isFollowed]); return ( - + + + + + + + + {followCount} + + + ); } diff --git a/components/core/CollectionPreviewBlock/index.js b/components/core/CollectionPreviewBlock/index.js index 5d025122..3b5c9772 100644 --- a/components/core/CollectionPreviewBlock/index.js +++ b/components/core/CollectionPreviewBlock/index.js @@ -1,31 +1,298 @@ import * as React from "react"; import * as Validations from "~/common/validations"; +import * as Typography from "~/components/system/components/Typography"; +import * as Styles from "~/common/styles"; +import * as Strings from "~/common/strings"; +import * as Constants from "~/common/constants"; +import * as SVG from "~/common/svg"; -import ImageCollectionPreview from "./ImageCollectionPreview"; -import FilesCollectionPreview from "./FilesCollectionPreview"; +import { Logo } from "~/common/logo"; +import { useInView } from "~/common/hooks"; +import { isBlurhashValid } from "blurhash"; +import { Blurhash } from "react-blurhash"; +import { css } from "@emotion/react"; +import { FollowButton } from "~/components/core/CollectionPreviewBlock/components"; +import { useFollowHandler } from "~/components/core/CollectionPreviewBlock/hooks"; +import { Link } from "~/components/core/Link"; +import { AnimatePresence, motion } from "framer-motion"; -export default function CollectionPreview({ collection, viewer, owner, onAction }) { - const objects = collection.objects.filter((file) => - Validations.isPreviewableImage(file.data.type) - ); +import ObjectPlaceholder from "~/components/core/ObjectPreview/placeholders"; - if (objects.length > 0) { +const STYLES_CONTAINER = (theme) => css` + position: relative; + display: flex; + flex-direction: column; + background-color: ${theme.semantic.bgLight}; + box-shadow: 0 0 0 0.5px ${theme.semantic.bgGrayLight}, ${theme.shadow.lightSmall}; + border-radius: 16px; + width: 100%; + overflow: hidden; + + height: 304px; + @media (max-width: ${theme.sizes.mobile}px) { + height: 281px; + } +`; + +const STYLES_PREVIEW = css` + flex-grow: 1; + background-size: cover; + overflow: hidden; + img { + height: 100%; + width: 100%; + object-fit: cover; + } +`; + +const STYLES_DESCRIPTION_CONTAINER = (theme) => css` + display: flex; + flex-direction: column; + position: relative; + padding: 9px 16px 12px; + border-radius: 0px 0px 16px 16px; + box-shadow: 0 -0.5px 0.5px ${theme.semantic.bgGrayLight}; + width: 100%; + margin-top: auto; +`; + +const STYLES_SPACE_BETWEEN = css` + justify-content: space-between; +`; + +const STYLES_PROFILE_IMAGE = (theme) => css` + background-color: ${theme.semantic.bgLight}; + height: 16px; + width: 16px; + border-radius: 4px; + object-fit: cover; +`; + +const STYLES_METRICS = (theme) => css` + margin-top: 7px; + @media (max-width: ${theme.sizes.mobile}px) { + margin-top: 12px; + } + ${Styles.CONTAINER_CENTERED}; + ${STYLES_SPACE_BETWEEN} +`; + +const STYLES_PLACEHOLDER_CONTAINER = css` + height: 100%; + width: 100%; +`; + +const STYLES_EMPTY_CONTAINER = css` + display: flex; + overflow: hidden; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; +`; + +const STYLES_CONTROLS = css` + position: absolute; + z-index: 1; + right: 16px; + top: 16px; + & > * + * { + margin-top: 8px !important; + } +`; +const STYLES_TEXT_GRAY = (theme) => css` + color: ${theme.semantic.textGray}; +`; + +const getFileBlurHash = (file) => { + const coverImage = file?.data?.coverImage; + const coverImageBlurHash = coverImage?.data?.blurhash; + if (coverImage && isBlurhashValid(coverImageBlurHash)) return coverImageBlurHash; + + const blurhash = file?.data?.blurhash; + if (isBlurhashValid(blurhash)) return blurhash; + + return null; +}; + +const getObjectToPreview = (objects = []) => { + let objectIdx = 0; + let isImage = false; + + objects.some((object, i) => { + const isPreviewableImage = Validations.isPreviewableImage(object.data.type); + if (isPreviewableImage) (objectIdx = i), (isImage = true); + return isPreviewableImage; + }); + + return { ...objects[objectIdx], isImage }; +}; + +const STYLES_DESCRIPTION_INNER = (theme) => css` + background-color: ${theme.semantic.bgLight}; + border-radius: 16px; +`; + +const Preview = ({ collection, children, ...props }) => { + const [isLoading, setLoading] = React.useState(true); + const handleOnLoaded = () => setLoading(false); + + const previewerRef = React.useRef(); + const { isInView } = useInView({ + ref: previewerRef, + }); + + const isCollectionEmpty = collection.fileCount === 0; + if (isCollectionEmpty) { return ( - +
      + {children} + + No files in this collection +
      + ); + } + + const object = getObjectToPreview(collection.objects); + if (object.isImage) { + const { coverImage } = object.data; + const blurhash = getFileBlurHash(object); + const previewImage = coverImage + ? Strings.getURLfromCID(coverImage?.cid) + : Strings.getURLfromCID(object.cid); + + return ( +
      + {children} + {isInView && ( + <> + {isLoading && blurhash && ( + + )} + Collection preview + + )} +
      ); } return ( - +
      + {children} + +
      + ); +}; + +export default function CollectionPreview({ collection, viewer, owner, onAction }) { + const [areControlsVisible, setShowControls] = React.useState(false); + const showControls = () => setShowControls(true); + const hideControls = () => setShowControls(false); + + // const [isBodyVisible, setShowBody] = React.useState(false); + // const showBody = () => setShowBody(true); + // const hideBody = () => setShowBody(false); + // const body = collection?.data?.body; + + const { follow, followCount, isFollowed } = useFollowHandler({ collection, viewer }); + + const { fileCount } = collection; + + return ( +
      + + + {areControlsVisible && ( + + + + )} + + +
      +
      +
      + + {collection.slatename} + +
      + + {/* {isBodyVisible && ( +
      + + {body || "sorry, no description available."} + +
      + )} */} +
      + +
      +
      + + + {fileCount} + +
      + {owner && ( +
      + + {`${owner.username} (e.target.src = Constants.profileDefaultPicture)} + /> + + + + {owner.username} + + +
      + )} +
      +
      +
      ); } From 913d40c8f716b3787b155b603cd3eba26ebf6cd8 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Mon, 26 Jul 2021 17:44:50 +0100 Subject: [PATCH 20/27] feat(ObjectPreview): update object preview to new design --- .../components/ViewMoreContent.js | 2 +- .../ObjectPreview/ObjectPreviewPrimitive.js | 62 ++++---- .../ObjectPreview/components/LikeButton.jsx | 140 ++++++++++------ .../ObjectPreview/components/SaveButton.jsx | 150 +++++++++++------- 4 files changed, 212 insertions(+), 142 deletions(-) diff --git a/components/core/ActivityGroup/components/ViewMoreContent.js b/components/core/ActivityGroup/components/ViewMoreContent.js index 47556f36..2cd4acda 100644 --- a/components/core/ActivityGroup/components/ViewMoreContent.js +++ b/components/core/ActivityGroup/components/ViewMoreContent.js @@ -52,7 +52,7 @@ export default function ViewMoreContent({ items, children, ...props }) { {isImageFile ? ( File Preview ) : ( - + )}
      ); diff --git a/components/core/ObjectPreview/ObjectPreviewPrimitive.js b/components/core/ObjectPreview/ObjectPreviewPrimitive.js index b8b1ad75..1a551fd8 100644 --- a/components/core/ObjectPreview/ObjectPreviewPrimitive.js +++ b/components/core/ObjectPreview/ObjectPreviewPrimitive.js @@ -16,6 +16,7 @@ const STYLES_WRAPPER = (theme) => css` box-shadow: 0 0 0 0.5px ${theme.semantic.bgGrayLight}, ${theme.shadow.lightSmall}; border-radius: 16px; overflow: hidden; + cursor: pointer; `; const STYLES_DESCRIPTION = (theme) => css` @@ -39,6 +40,7 @@ const STYLES_DESCRIPTION_INNER = (theme) => css` const STYLES_PREVIEW = css` overflow: hidden; + position: relative; `; const STYLES_SELECTED_RING = (theme) => css` @@ -55,6 +57,10 @@ const STYLES_CONTROLS = css` } `; +const STYLES_UPPERCASE = css` + text-transform: uppercase; +`; + export default function ObjectPreviewPrimitive({ children, tag = "FILE", @@ -94,33 +100,27 @@ export default function ObjectPreviewPrimitive({ } return ( -
      - - {areControlsVisible && ( - - - {showSaveButton && } - - )} - - -
      - {children} -
      -
      +
      +
      + + {areControlsVisible && ( + + + {showSaveButton && ( + + )} + + )} + + +
      {children}
      +
      +
      -
      +
      {title}
      - + {tag}
      - {body ? body : "sorry, no description available."} + {body || "sorry, no description available."}
      diff --git a/components/core/ObjectPreview/components/LikeButton.jsx b/components/core/ObjectPreview/components/LikeButton.jsx index 27c5ef26..9dfcd3c0 100644 --- a/components/core/ObjectPreview/components/LikeButton.jsx +++ b/components/core/ObjectPreview/components/LikeButton.jsx @@ -4,69 +4,107 @@ import * as Styles from "~/common/styles"; import { css } from "@emotion/react"; import { motion, useAnimation } from "framer-motion"; +import { P3 } from "~/components/system"; +import { useMounted } from "~/common/hooks"; -const STYLES_BUTTON_HOVER = (theme) => css` +const STYLES_BUTTON = (theme) => css` display: flex; + background-color: ${theme.semantic.bgBlurWhite}; + padding: 8px; + border-radius: 8px; + box-shadow: 0 0 0 1px ${theme.system.grayLight5}, ${theme.shadow.lightLarge}; + transition: box-shadow 0.3s; + :hover { + box-shadow: 0 0 0 1px ${theme.system.pink}, ${theme.shadow.lightLarge}; + } + + svg { + transition: fill 0.1s; + } + :hover svg { + fill: ${theme.system.pink}; + } + + path { + transition: stroke 0.3s; + } :hover path { - stroke: ${theme.system.blue}; + stroke: ${theme.system.pink}; } `; -const animate = async (controls) => { - await controls.start({ scale: 1.3, rotateY: 180, fill: "rgba(0, 132, 255, 1)" }); - await controls.start({ scale: 1 }); - controls.set({ rotateY: 0 }); +export default function LikeButton({ onClick, isLiked, likeCount }) { + const { heartAnimation, backgroundAnimation } = useAnimations({ isLiked }); + + const handleClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + if (onClick) onClick(); + }; + + return ( + + + + + + + {likeCount} + + + + ); +} + +const animateButton = async (heartAnimation, backgroundAnimation) => { + await heartAnimation.start({ scale: 1.3, rotateY: 180, fill: Constants.system.pink }); + heartAnimation.start({ scale: 1, transition: { duration: 0.2 } }); + backgroundAnimation.start({ backgroundColor: Constants.system.redLight6 }); + heartAnimation.set({ rotateY: 0 }); }; -const useMounted = (callback, depedencies) => { - const mountedRef = React.useRef(false); - React.useLayoutEffect(() => { - if (mountedRef.current && callback) { - callback(); - } - mountedRef.current = true; - }, depedencies); -}; - -export default function LikeButton({ onClick, isLiked, ...props }) { - const controls = useAnimation(); +const useAnimations = ({ isLiked }) => { + const backgroundAnimation = useAnimation(); + const heartAnimation = useAnimation(); useMounted(() => { if (isLiked) { - animate(controls); + animateButton(heartAnimation, backgroundAnimation); return; } - controls.start({ fill: "#fff", scale: 1 }); + // NOTE(amine): reset values to default + heartAnimation.start({ fill: Constants.semantic.bgBlurWhite, scale: 1 }); + backgroundAnimation.start({ backgroundColor: Constants.semantic.bgBlurWhite }); }, [isLiked]); - return ( - - ); -} + return { heartAnimation, backgroundAnimation }; +}; diff --git a/components/core/ObjectPreview/components/SaveButton.jsx b/components/core/ObjectPreview/components/SaveButton.jsx index c0d1ff98..755b7a42 100644 --- a/components/core/ObjectPreview/components/SaveButton.jsx +++ b/components/core/ObjectPreview/components/SaveButton.jsx @@ -3,79 +3,111 @@ import * as Styles from "~/common/styles"; import * as Constants from "~/common/constants"; import { css } from "@emotion/react"; -import { motion } from "framer-motion"; +import { motion, useTransform, useMotionValue } from "framer-motion"; +import { P3 } from "~/components/system"; const STYLES_BUTTON_HOVER = (theme) => css` display: flex; + background-color: ${theme.semantic.bgBlurWhite}; + padding: 8px; + border-radius: 8px; + box-shadow: 0 0 0 1px ${theme.system.grayLight5}, ${theme.shadow.lightLarge}; + transition: box-shadow 0.3s; + :hover { + box-shadow: 0 0 0 1px ${theme.system.pink}, ${theme.shadow.lightLarge}; + } + + .button_path { + transition: stroke 0.3s; + } :hover .button_path { - stroke: ${theme.system.blue}; + stroke: ${theme.system.pink}; } `; -export default function SaveButton({ onSave, isSaved, ...props }) { +export default function SaveButton({ onSave, isSaved, saveCount, ...props }) { + const pathLength = useMotionValue(0); + const opacity = useTransform(pathLength, [0, 1], [0, 1]); + return ( - + + + + + + {/** NOTE(amine): checkmark path */} + + + + {saveCount} + + + ); } From e91041f18cdd8e005fd8530bd54d5c097b3b3e64 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Mon, 26 Jul 2021 17:50:01 +0100 Subject: [PATCH 21/27] fix(hooks): adjust useMounted --- common/hooks.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/common/hooks.js b/common/hooks.js index 3589756f..927acfc3 100644 --- a/common/hooks.js +++ b/common/hooks.js @@ -3,16 +3,15 @@ import * as Logging from "~/common/logging"; import * as Actions from "~/common/actions"; import * as Events from "~/common/custom-events"; -export const useMounted = () => { - const isMounted = React.useRef(true); +export const useMounted = (callback, depedencies) => { + const mountedRef = React.useRef(false); React.useLayoutEffect(() => { - return () => { - isMounted.current = false; - }; - }, []); - return isMounted.current; + if (mountedRef.current && callback) { + callback(); + } + mountedRef.current = true; + }, depedencies); }; - /** NOTE(amine): * useForm handles three main responsabilities * - control inputs From bc6256403168326acdf304d89abcd166580a28db Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 27 Jul 2021 13:01:44 +0100 Subject: [PATCH 22/27] feat(activity): add feature flag --- common/environment.js | 2 ++ common/navigation-data.js | 30 +++++++++++++++------------- components/core/Application.js | 6 +++--- components/core/ApplicationHeader.js | 10 +++------- components/core/SignIn.js | 2 +- server.js | 2 +- 6 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 common/environment.js diff --git a/common/environment.js b/common/environment.js new file mode 100644 index 00000000..a0bef998 --- /dev/null +++ b/common/environment.js @@ -0,0 +1,2 @@ +//NOTE(amine): feature flags +export const ACTIVITY_FEATURE_FLAG = !!process.env.NEXT_PUBLIC_ACTIVITY_FEATURE_FLAG; diff --git a/common/navigation-data.js b/common/navigation-data.js index f759e834..0aa248c3 100644 --- a/common/navigation-data.js +++ b/common/navigation-data.js @@ -1,5 +1,5 @@ -import * as Actions from "~/common/actions"; import * as Strings from "~/common/strings"; +import * as Environment from "~/common/environment"; export const getById = (id, viewer) => { let target; @@ -12,7 +12,7 @@ export const getById = (id, viewer) => { } if (viewer && target.id === authPage.id) { - return { ...activityPage }; //NOTE(martina): authenticated users should be redirected to the home page rather than the + return { ...dataPage }; //NOTE(martina): authenticated users should be redirected to the home page rather than the } if (!viewer && !target.externalAllowed) { @@ -31,7 +31,7 @@ export const getByHref = (href, viewer) => { return { page: { ...errorPage } }; } if (pathname === "/_") { - return { page: { ...activityPage } }; + return { page: { ...dataPage } }; } let page = navigation.find((each) => pathname.startsWith(each.pathname)); @@ -65,7 +65,7 @@ export const getByHref = (href, viewer) => { if (viewer && page.id === authPage.id) { redirected = true; - page = { ...activityPage }; + page = { ...dataPage }; } if (!viewer && !page.externalAllowed) { @@ -105,6 +105,15 @@ const dataPage = { mainNav: true, }; +const collectionsPage = { + id: "NAV_SLATES", + name: "Collections", + pageTitle: "Your Collections", + ignore: true, + pathname: "/_/collections", + mainNav: true, +}; + const activityPage = { id: "NAV_ACTIVITY", name: "Activity", @@ -112,7 +121,7 @@ const activityPage = { ignore: true, externalAllowed: true, pathname: "/_/activity", - mainNav: true, + mainNav: Environment.ACTIVITY_FEATURE_FLAG, }; const slatePage = { @@ -145,16 +154,9 @@ const errorPage = { export const navigation = [ errorPage, authPage, - activityPage, + ...(Environment.ACTIVITY_FEATURE_FLAG ? [activityPage] : []), + collectionsPage, dataPage, - { - id: "NAV_SLATES", - name: "Collections", - pageTitle: "Your Collections", - ignore: true, - pathname: "/_/collections", - mainNav: true, - }, // { // id: "NAV_SEARCH", // name: "Search", diff --git a/components/core/Application.js b/components/core/Application.js index 9934b702..720fcf7a 100644 --- a/components/core/Application.js +++ b/components/core/Application.js @@ -13,6 +13,7 @@ import * as Websockets from "~/common/browser-websockets"; import * as UserBehaviors from "~/common/user-behaviors"; import * as Events from "~/common/custom-events"; import * as Logging from "~/common/logging"; +import * as Environment from "~/common/environment"; // NOTE(jim): // Scenes each have an ID and can be navigated to with _handleAction @@ -79,7 +80,7 @@ const SIDEBARS = { const SCENES = { NAV_ERROR: , NAV_SIGN_IN: , - NAV_ACTIVITY: , + ...(Environment.ACTIVITY_FEATURE_FLAG ? { NAV_ACTIVITY: } : {}), NAV_DIRECTORY: , NAV_PROFILE: , NAV_DATA: , @@ -88,7 +89,6 @@ const SCENES = { NAV_API: , NAV_SETTINGS: , NAV_SLATES: , - NAV_DIRECTORY: , NAV_FILECOIN: , NAV_STORAGE_DEAL: , }; @@ -433,7 +433,7 @@ export default class ApplicationPage extends React.Component { // if (!redirected) { // this._handleAction({ type: "NAVIGATE", value: "NAV_DATA" }); // } - this._handleNavigateTo({ href: "/_/activity", redirect: true }); + this._handleNavigateTo({ href: "/_/data", redirect: true }); return response; }; diff --git a/components/core/ApplicationHeader.js b/components/core/ApplicationHeader.js index cede71a4..3fd83cb1 100644 --- a/components/core/ApplicationHeader.js +++ b/components/core/ApplicationHeader.js @@ -202,11 +202,7 @@ export default class ApplicationHeader extends React.Component {
      - +
      {searchComponent}
      @@ -331,7 +327,7 @@ export default class ApplicationHeader extends React.Component {
      @@ -396,7 +392,7 @@ export default class ApplicationHeader extends React.Component {
      diff --git a/components/core/SignIn.js b/components/core/SignIn.js index acd9a103..1ceb077c 100644 --- a/components/core/SignIn.js +++ b/components/core/SignIn.js @@ -147,7 +147,7 @@ export class SignIn extends React.Component { }); } if (response && !response.error) { - window.location.replace("/_/activity"); + window.location.replace("/_/data"); } this.setState({ loading: false }); }; diff --git a/server.js b/server.js index 13f153e8..d19f8d96 100644 --- a/server.js +++ b/server.js @@ -154,7 +154,7 @@ app.prepare().then(async () => { }); server.get("/_", async (req, res) => { - return res.redirect("/_/activity"); + return res.redirect("/_/data"); // let isMobile = Window.isMobileBrowser(req.headers["user-agent"]); // let isMac = Window.isMac(req.headers["user-agent"]); From ef3b0bab787e29c416b801e5e5a74970b4b7829b Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 27 Jul 2021 13:02:21 +0100 Subject: [PATCH 23/27] feat(ObjectPreview): disable like and save buttons --- .../ObjectPreview/ObjectPreviewPrimitive.js | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/components/core/ObjectPreview/ObjectPreviewPrimitive.js b/components/core/ObjectPreview/ObjectPreviewPrimitive.js index 1a551fd8..640a1e75 100644 --- a/components/core/ObjectPreview/ObjectPreviewPrimitive.js +++ b/components/core/ObjectPreview/ObjectPreviewPrimitive.js @@ -3,9 +3,9 @@ import * as React from "react"; import { css } from "@emotion/react"; import { H5, P3 } from "~/components/system/components/Typography"; import { AspectRatio } from "~/components/system"; -import { LikeButton, SaveButton } from "./components"; -import { useLikeHandler, useSaveHandler } from "~/common/hooks"; -import { motion, AnimatePresence } from "framer-motion"; +// import { LikeButton, SaveButton } from "./components"; +// import { useLikeHandler, useSaveHandler } from "~/common/hooks"; +import { motion } from "framer-motion"; import ImageObjectPreview from "./ImageObjectPreview"; @@ -47,15 +47,15 @@ const STYLES_SELECTED_RING = (theme) => css` box-shadow: 0 0 0 2px ${theme.system.blue}; `; -const STYLES_CONTROLS = css` - position: absolute; - z-index: 1; - right: 16px; - top: 16px; - & > * + * { - margin-top: 8px !important; - } -`; +// const STYLES_CONTROLS = css` +// position: absolute; +// z-index: 1; +// right: 16px; +// top: 16px; +// & > * + * { +// margin-top: 8px !important; +// } +// `; const STYLES_UPPERCASE = css` text-transform: uppercase; @@ -66,19 +66,19 @@ export default function ObjectPreviewPrimitive({ tag = "FILE", file, isSelected, - viewer, + // viewer, owner, // NOTE(amine): internal prop used to display isImage, onAction, }) { - const { like, isLiked, likeCount } = useLikeHandler({ file, viewer }); - const { save, isSaved, saveCount } = useSaveHandler({ file, viewer }); - const showSaveButton = viewer?.id !== file?.ownerId; + // const { like, isLiked, likeCount } = useLikeHandler({ file, viewer }); + // const { save, isSaved, saveCount } = useSaveHandler({ file, viewer }); + // const showSaveButton = viewer?.id !== file?.ownerId; - const [areControlsVisible, setShowControls] = React.useState(false); - const showControls = () => setShowControls(true); - const hideControls = () => setShowControls(false); + // const [areControlsVisible, setShowControls] = React.useState(false); + // const showControls = () => setShowControls(true); + // const hideControls = () => setShowControls(false); const [isBodyVisible, setShowBody] = React.useState(false); const showBody = () => setShowBody(true); @@ -101,8 +101,11 @@ export default function ObjectPreviewPrimitive({ return (
      -
      - +
      + {/* {areControlsVisible && ( )} - + */}
      {children}
      From 6c9a67e05a4ab43aab75cf069cf3b7b187b68fe1 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 27 Jul 2021 13:14:50 +0100 Subject: [PATCH 24/27] feat(activity): disable my network feed --- scenes/SceneActivity/hooks.js | 3 ++- scenes/SceneActivity/index.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/scenes/SceneActivity/hooks.js b/scenes/SceneActivity/hooks.js index 175188d8..8a8c6784 100644 --- a/scenes/SceneActivity/hooks.js +++ b/scenes/SceneActivity/hooks.js @@ -84,7 +84,8 @@ export function useActivity({ page, viewer, onAction }) { }, }); - const tab = getTab(page, viewer); + // const tab = getTab(page, viewer); + const tab = "explore"; const updateFeed = React.useCallback( async (update) => { diff --git a/scenes/SceneActivity/index.js b/scenes/SceneActivity/index.js index c05a7012..ff575424 100644 --- a/scenes/SceneActivity/index.js +++ b/scenes/SceneActivity/index.js @@ -3,7 +3,7 @@ import * as Constants from "~/common/constants"; import * as Styles from "~/common/styles"; import { css } from "@emotion/react"; -import { SecondaryTabGroup } from "~/components/core/TabGroup"; +// import { SecondaryTabGroup } from "~/components/core/TabGroup"; import { LoaderSpinner } from "~/components/system/components/Loaders"; import { useIntersection } from "common/hooks"; import { useActivity } from "./hooks"; @@ -58,7 +58,7 @@ export default function SceneActivity({ page, viewer, external, onAction }) { url={`${Constants.hostname}${page.pathname}`} > - {viewer && ( + {/* {viewer && ( - )} + )} */}
      {feed?.map((group) => ( Date: Wed, 28 Jul 2021 14:56:05 +0100 Subject: [PATCH 25/27] feat(Activity): add GlobalCarousel --- .../components/ActivityFileGroup.js | 52 +++++++++++++++---- components/core/ActivityGroup/index.js | 10 +++- scenes/SceneActivity/index.js | 23 +++++++- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/components/core/ActivityGroup/components/ActivityFileGroup.js b/components/core/ActivityGroup/components/ActivityFileGroup.js index dba1ca09..5abe0e8e 100644 --- a/components/core/ActivityGroup/components/ActivityFileGroup.js +++ b/components/core/ActivityGroup/components/ActivityFileGroup.js @@ -5,6 +5,7 @@ import * as Styles from "~/common/styles"; import { css } from "@emotion/react"; import { motion } from "framer-motion"; import { ViewMoreContent, ProfileInfo } from "~/components/core/ActivityGroup/components"; +import { Link } from "~/components/core/Link"; import ObjectPreview from "~/components/core/ObjectPreview"; @@ -27,18 +28,24 @@ const STYLES_VIEWMORE_CONTAINER = (theme) => css` } `; -export default function ActivityFileGroup({ viewer, group, onAction, nbrOfObjectsPerRow = 4 }) { - const { file, owner, slate, type, createdAt } = group; +export default function ActivityFileGroup({ + viewer, + group, + onFileClick, + onAction, + nbrOfObjectsPerRow = 4, +}) { + const { file: files, owner, slate, type, createdAt } = group; const { elements, restElements } = React.useMemo(() => { - if (!Array.isArray(file)) { - return { elements: [file] }; + if (!Array.isArray(files)) { + return { elements: [files] }; } return { - elements: file.slice(0, nbrOfObjectsPerRow), - restElements: file.slice(nbrOfObjectsPerRow), + elements: files.slice(0, nbrOfObjectsPerRow), + restElements: files.slice(nbrOfObjectsPerRow), }; - }, [file]); + }, [files]); const [showMore, setShowMore] = React.useState(false); const viewMoreFiles = () => setShowMore(true); @@ -70,8 +77,17 @@ export default function ActivityFileGroup({ viewer, group, onAction, nbrOfObject />
      - {elements.map((file) => ( - + {elements.map((file, i) => ( + <> + + ))} {showMore && restElements.map((file, i) => @@ -82,10 +98,24 @@ export default function ActivityFileGroup({ viewer, group, onAction, nbrOfObject animate={{ opacity: 1, y: 0 }} key={file.id} > - + ) : ( - + ) )}
      diff --git a/components/core/ActivityGroup/index.js b/components/core/ActivityGroup/index.js index 230adb4a..8ffedccb 100644 --- a/components/core/ActivityGroup/index.js +++ b/components/core/ActivityGroup/index.js @@ -16,7 +16,14 @@ const STYLES_GROUP_GRID = (theme) => css` padding-bottom: 24px; `; -export default function ActivityGroup({ onAction, viewer, external, group, nbrOfCardsPerRow }) { +export default function ActivityGroup({ + onAction, + viewer, + onFileClick, + external, + group, + nbrOfCardsPerRow, +}) { const { type } = group; if ( type === "CREATE_FILE" || @@ -30,6 +37,7 @@ export default function ActivityGroup({ onAction, viewer, external, group, nbrOf viewer={viewer} onAction={onAction} group={group} + onFileClick={onFileClick} /> ); } diff --git a/scenes/SceneActivity/index.js b/scenes/SceneActivity/index.js index ff575424..0165cbd8 100644 --- a/scenes/SceneActivity/index.js +++ b/scenes/SceneActivity/index.js @@ -7,6 +7,7 @@ import { css } from "@emotion/react"; import { LoaderSpinner } from "~/components/system/components/Loaders"; import { useIntersection } from "common/hooks"; import { useActivity } from "./hooks"; +import { GlobalCarousel } from "~/components/system/components/GlobalCarousel"; import ScenePage from "~/components/core/ScenePage"; import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper"; @@ -33,7 +34,7 @@ const STYLES_LOADER = css` width: 100%; `; -export default function SceneActivity({ page, viewer, external, onAction }) { +export default function SceneActivity({ page, viewer, external, onAction, ...props }) { const { feed, tab, isLoading, updateFeed } = useActivity({ page, viewer, @@ -44,6 +45,14 @@ export default function SceneActivity({ page, viewer, external, onAction }) { const nbrOfCardsInRow = useNbrOfCardsPerRow(divRef); + const [globalCarouselState, setGlobalCarouselState] = React.useState({ + currentCarrousel: -1, + currentObjects: [], + }); + const handleFileClick = (fileIdx, groupFiles) => + setGlobalCarouselState({ currentCarrousel: fileIdx, currentObjects: groupFiles }); + console.log(globalCarouselState.currentCarrousel, globalCarouselState.currentObjects.length); + useIntersection({ ref: divRef, onIntersect: () => { @@ -78,6 +87,7 @@ export default function SceneActivity({ page, viewer, external, onAction }) { external={external} onAction={onAction} group={group} + onFileClick={handleFileClick} /> ))}
      @@ -85,6 +95,17 @@ export default function SceneActivity({ page, viewer, external, onAction }) { {isLoading[tab] && }
      + + setGlobalCarouselState((prev) => ({ ...prev, currentCarrousel: idx }))} + isOwner={false} + onAction={() => {}} + /> ); } From dfa07242f3d987dd4f6697b8c71b2ec86c533b5f Mon Sep 17 00:00:00 2001 From: Aminejv Date: Wed, 28 Jul 2021 14:56:34 +0100 Subject: [PATCH 26/27] feat(Activity): delete old scene --- scenes/DeprecatedSceneActivity.js | 408 ------------------------------ 1 file changed, 408 deletions(-) delete mode 100644 scenes/DeprecatedSceneActivity.js diff --git a/scenes/DeprecatedSceneActivity.js b/scenes/DeprecatedSceneActivity.js deleted file mode 100644 index a9965bc4..00000000 --- a/scenes/DeprecatedSceneActivity.js +++ /dev/null @@ -1,408 +0,0 @@ -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 Styles from "~/common/styles"; -import * as ActivityUtilities from "~/common/activity-utilities"; - -import { GlobalCarousel } from "~/components/system/components/GlobalCarousel"; -import { css } from "@emotion/react"; -import { SecondaryTabGroup } from "~/components/core/TabGroup"; -import { LoaderSpinner } from "~/components/system/components/Loaders"; -import { Link } from "~/components/core/Link"; - -import EmptyState from "~/components/core/EmptyState"; -import ScenePage from "~/components/core/ScenePage"; -import ObjectPreview from "~/components/core/ObjectPreview"; -import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper"; -import ActivityObjectPreview from "~/components/core/ActivityObjectPreview"; - -const STYLES_LOADER = css` - display: flex; - align-items: center; - justify-content: center; - height: calc(100vh - 400px); - width: 100%; -`; - -const STYLES_IMAGE_BOX = css` - cursor: pointer; - position: relative; - box-shadow: ${Constants.shadow.lightSmall}; - margin: 10px; - :hover { - box-shadow: ${Constants.shadow.lightMedium}; - } - @media (max-width: ${Constants.sizes.mobile}px) { - overflow: hidden; - border-radius: 8px; - } -`; - -const STYLES_TEXT_AREA = css` - position: absolute; - top: 16px; - 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}; - width: 100%; -`; - -const STYLES_GRADIENT = css` - background: linear-gradient( - 180deg, - rgba(0, 0, 0, 0.3) 0%, - rgba(0, 0, 0, 0.2) 26.56%, - rgba(0, 0, 0, 0) 100% - ); - backdrop-filter: blur(2px); - width: 100%; - height: 72px; - position: absolute; - top: 0px; - left: 0px; - @media (max-width: ${Constants.sizes.mobile}px) { - overflow: hidden; - border-radius: 0px 0px 8px 8px; - } -`; - -class ActivitySquare extends React.Component { - state = { - showText: false, - }; - - render() { - const { item } = this.props; - - return ( -
      - -
      - ); - } -} - -// {this.state.showText || this.props.isMobile ?
      : null} -// {this.state.showText || this.props.isMobile ? ( -//
      -// -// -// {item.slate.data.name || item.slate.slatename} -// -//
      -// ) : null} - -const ActivityRectangle = ({ item, width, height }) => { - let file; - for (let obj of item.slate?.objects || []) { - if (Validations.isPreviewableImage(obj.type) || obj.coverImage) { - file = obj; - } - } - let numObjects = item.slate?.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, - loading: false, - carouselIndex: -1, - }; - - async componentDidMount() { - this.fetchActivityItems(true); - 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); - } - - componentDidUpdate(prevProps) { - if (prevProps.page.params?.tab !== this.props.page.params?.tab) { - this.fetchActivityItems(true); - } - } - - componentWillUnmount() { - window.removeEventListener("resize", this.debounceInstance); - window.removeEventListener("scroll", this.scrollDebounceInstance); - } - - getTab = () => { - if (this.props.page.params?.tab) { - return this.props.page.params?.tab; - } - if (this.props.viewer?.subscriptions?.length || this.props.viewer?.following?.length) { - return "activity"; - } - return "explore"; - }; - - _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(); - } - }; - - getTab = () => { - if (!this.props.viewer) { - return "explore"; - } - return this.props.page.params?.tab || "explore"; - }; - - fetchActivityItems = async (update = false) => { - if (this.state.loading === "loading") return; - let tab = this.getTab(); - const isExplore = tab === "explore"; - this.setState({ loading: "loading" }); - let activity; - if (this.props.viewer) { - activity = isExplore ? this.props.viewer?.explore || [] : this.props.viewer?.activity || []; - } else { - activity = this.state.explore || []; - } - let requestObject = {}; - if (activity.length) { - if (update) { - requestObject.latestTimestamp = activity[0].createdAt; - } else { - requestObject.earliestTimestamp = activity[activity.length - 1].createdAt; - } - } - - let response; - if (isExplore) { - response = await Actions.getExplore(requestObject); - } else { - requestObject.following = this.props.viewer.following.map((item) => item.id); - requestObject.subscriptions = this.props.viewer.subscriptions.map((item) => item.id); - - response = await Actions.getActivity(requestObject); - } - if (Events.hasError(response)) { - this.setState({ loading: false }); - return; - } - - let newItems = response.data || []; - newItems = ActivityUtilities.processActivity(newItems); - - if (update) { - activity.unshift(...newItems); - } else { - activity.push(...newItems); - } - - if (this.props.viewer) { - if (!isExplore) { - this.props.onAction({ type: "UPDATE_VIEWER", viewer: { activity: activity } }); - } else { - this.props.onAction({ type: "UPDATE_VIEWER", viewer: { explore: activity } }); - } - this.setState({ loading: false }); - } else { - this.setState({ explore: activity, loading: false }); - } - }; - - calculateWidth = () => { - let windowWidth = window.innerWidth; - let imageSize; - if (windowWidth < Constants.sizes.mobile) { - imageSize = windowWidth - 2 * 24; - } else { - imageSize = (windowWidth - 2 * 56 - 5 * 20) / 6; - } - this.setState({ imageSize }); - }; - - getItemIndexById = (items, item) => { - const id = item.file?.id; - return items.findIndex((i) => i.id === id); - }; - - render() { - let tab = this.getTab(); - let activity; - if (this.props.viewer) { - activity = - tab === "activity" ? this.props.viewer?.activity || [] : this.props.viewer?.explore || []; - } else { - activity = this.state.explore || []; - } - - return ( - - - {this.props.viewer && ( - - )} - {}} - index={this.state.index} - onChange={(index) => { - this.setState({ index }); - if (index >= items.length - 4) { - this.fetchActivityItems(); - } - }} - isMobile={this.props.isMobile} - isOwner={false} - /> - {activity.length ? ( -
      -
      - {activity.map((item, i) => { - if (item.type === "CREATE_SLATE") { - return ( - this.setState({ index: i })} - > - - - ); - } else if (item.type === "CREATE_SLATE_OBJECT") { - return ( - this.setState({ carouselIndex: i })} - > - - - ); - } else { - return null; - } - })} -
      -
      - {this.state.loading === "loading" ? ( - - ) : null} -
      -
      - ) : this.state.loading === "loading" ? ( -
      - -
      - ) : ( - - -
      - Start following people and collections to see their activity here -
      -
      - )} -
      -
      - ); - } -} From 5512a6e1f6a3619d7e90554083e7b187efdc6e3e Mon Sep 17 00:00:00 2001 From: Aminejv Date: Wed, 28 Jul 2021 18:50:49 +0100 Subject: [PATCH 27/27] feat(ObjectPreview): add LinkObjectPreview --- .../core/ObjectPreview/ImageObjectPreview.js | 4 +- .../core/ObjectPreview/LinkObjectPreview.js | 41 +++++++++++++++++++ .../ObjectPreview/ObjectPreviewPrimitive.js | 12 ++++-- components/core/ObjectPreview/index.js | 14 ++++--- 4 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 components/core/ObjectPreview/LinkObjectPreview.js diff --git a/components/core/ObjectPreview/ImageObjectPreview.js b/components/core/ObjectPreview/ImageObjectPreview.js index 9c943d78..8b705e8c 100644 --- a/components/core/ObjectPreview/ImageObjectPreview.js +++ b/components/core/ObjectPreview/ImageObjectPreview.js @@ -76,7 +76,9 @@ export default function ImageObjectPreview({ const shouldShowPlaceholder = isLoading && blurhash; - const imageUrl = coverImage ? Strings.getURLfromCID(coverImage?.cid) : url; + const imageUrl = coverImage + ? coverImage?.data?.url || Strings.getURLfromCID(coverImage?.cid) + : url; return ( diff --git a/components/core/ObjectPreview/LinkObjectPreview.js b/components/core/ObjectPreview/LinkObjectPreview.js new file mode 100644 index 00000000..7b1a3c02 --- /dev/null +++ b/components/core/ObjectPreview/LinkObjectPreview.js @@ -0,0 +1,41 @@ +import * as React from "react"; +import * as Styles from "~/common/styles"; + +import { H5, P3 } from "~/components/system/components/Typography"; +import { css } from "@emotion/react"; + +import ObjectPreviewPrimitive from "~/components/core/ObjectPreview/ObjectPreviewPrimitive"; + +const STYLES_SOURCE_LOGO = css` + height: 14px; + width: 14px; + border-radius: 4px; +`; + +export default function LinkObjectPreview({ file }) { + const { + data: { link }, + } = file; + + const tag = ( +
      + {link.logo && ( + Link source logo + )} + + {link.source} + +
      + ); + + return ( + + link preview + + ); +} diff --git a/components/core/ObjectPreview/ObjectPreviewPrimitive.js b/components/core/ObjectPreview/ObjectPreviewPrimitive.js index 640a1e75..243673e9 100644 --- a/components/core/ObjectPreview/ObjectPreviewPrimitive.js +++ b/components/core/ObjectPreview/ObjectPreviewPrimitive.js @@ -134,9 +134,15 @@ export default function ObjectPreviewPrimitive({
      {title}
      - - {tag} - +
      + {typeof tag === "string" ? ( + + {tag} + + ) : ( + tag + )} +
      { - const { type } = file.data; + const { type, link } = file.data; const url = Strings.getURLfromCID(file.cid); + if (link) { + return ; + } + if (Validations.isPreviewableImage(type)) { return ; }