diff --git a/components/system/components/GlobalCarousel/index.js b/components/system/components/GlobalCarousel/index.js
new file mode 100644
index 00000000..43da8410
--- /dev/null
+++ b/components/system/components/GlobalCarousel/index.js
@@ -0,0 +1,792 @@
+import * as React from "react";
+import * as Constants from "~/common/constants";
+import * as SVG from "~/common/svg";
+import * as System from "~/components/system";
+import * as Styles from "~/common/styles";
+import * as Jumpers from "~/components/system/components/GlobalCarousel/jumpers";
+
+import { css } from "@emotion/react";
+import { Alert } from "~/components/core/Alert";
+import { motion } from "framer-motion";
+import {
+ useDetectTextOverflow,
+ useEscapeKey,
+ useEventListener,
+ useLockScroll,
+} from "~/common/hooks";
+import { Show } from "~/components/utility/Show";
+import { ModalPortal } from "~/components/core/ModalPortal";
+
+import SlateMediaObject from "~/components/core/SlateMediaObject";
+import LinkIcon from "~/components/core/LinkIcon";
+
+/* -------------------------------------------------------------------------------------------------
+ * Carousel Header
+ * -----------------------------------------------------------------------------------------------*/
+
+const VisitLinkButton = ({ file }) => {
+ return (
+ e.stopPropagation()}
+ style={{
+ color: Constants.semantic.textGrayDark,
+ padding: "4px 8px 7px",
+ marginLeft: 4,
+ minHeight: 30,
+ }}
+ href={file.url}
+ target="_blank"
+ rel="noreferrer"
+ type="link"
+ >
+
+ Visit site
+
+ );
+};
+
+/* -----------------------------------------------------------------------------------------------*/
+
+const useCarouselJumperControls = () => {
+ const [isControlVisible, setControlVisibility] = React.useState(false);
+ const showControl = () => setControlVisibility(true);
+ const hideControl = () => setControlVisibility(false);
+ return [isControlVisible, { showControl, hideControl }];
+};
+
+const STYLES_HEADER_WRAPPER = (theme) => css`
+ ${Styles.HORIZONTAL_CONTAINER_CENTERED};
+ position: absolute;
+ width: 100%;
+ min-height: 64px;
+ padding: 13px 24px 10px;
+ color: ${theme.semantic.textGrayDark};
+ border-bottom: 1px solid ${theme.semantic.borderGrayLight};
+ box-shadow: ${theme.shadow.lightSmall};
+ z-index: 1;
+
+ background-color: ${theme.semantic.bgWhite};
+ @supports ((-webkit-backdrop-filter: blur(15px)) or (backdrop-filter: blur(15px))) {
+ background-color: ${theme.semantic.bgBlurWhiteOP};
+ -webkit-backdrop-filter: blur(15px);
+ backdrop-filter: blur(15px);
+ }
+`;
+
+const STYLES_ACTION_BUTTON = css`
+ ${Styles.BUTTON_RESET};
+ height: 32px;
+ width: 32px;
+`;
+
+function CarouselHeader({
+ viewer,
+ data,
+ external,
+ isOwner,
+ file,
+ current,
+ total,
+ onAction,
+ onClose,
+ ...props
+}) {
+ const [isHeaderVisible, setHeaderVisibility] = React.useState(true);
+ const timeoutRef = React.useRef();
+
+ const showHeader = () => {
+ clearTimeout(timeoutRef.current);
+ setHeaderVisibility(true);
+ };
+
+ const hideHeader = () => {
+ timeoutRef.current = setTimeout(() => {
+ setHeaderVisibility(false);
+ }, 500);
+ };
+
+ React.useEffect(() => {
+ timeoutRef.current = setTimeout(hideHeader, 3000);
+ return () => clearTimeout(timeoutRef.current);
+ }, []);
+
+ // NOTE(amine): Detect if the text is overflowing to show the MORE button
+ const elementRef = React.useRef();
+ const isBodyOverflowing = useDetectTextOverflow({ ref: elementRef }, [file]);
+
+ // NOTE(amine): jumpers handlers
+ const [
+ isFileDescriptionVisible,
+ { showControl: showFileDescription, hideControl: hideFileDescription },
+ ] = useCarouselJumperControls();
+
+ const [isMoreInfoVisible, { showControl: showMoreInfo, hideControl: hideMoreInfo }] =
+ useCarouselJumperControls();
+
+ const [isEditInfoVisible, { showControl: showEditInfo, hideControl: hideEditInfo }] =
+ useCarouselJumperControls();
+
+ const [isShareFileVisible, { showControl: showShareFile, hideControl: hideShareFile }] =
+ useCarouselJumperControls();
+
+ const [isEditChannelsVisible, { showControl: showEditChannels, hideControl: hideEditChannels }] =
+ useCarouselJumperControls();
+
+ const isJumperOpen =
+ isFileDescriptionVisible ||
+ isMoreInfoVisible ||
+ isEditInfoVisible ||
+ isShareFileVisible ||
+ isEditChannelsVisible;
+
+ return (
+ <>
+
+ {isOwner && (
+
+ )}
+ {isOwner && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {file?.name || file?.filename}
+
+
+ {current} / {total}
+
+
+
+
+
+ {file.body}
+
+
+
+ MORE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {file.isLink ? : null}
+
+
+
+
+
+
+ >
+ );
+}
+
+const STYLES_CAROUSEL_MOBILE_HEADER = (theme) => css`
+ position: relative;
+ ${Styles.HORIZONTAL_CONTAINER_CENTERED};
+ padding: 7px 8px 3px;
+ color: ${theme.semantic.textGrayDark};
+ border-bottom: 1px solid ${theme.semantic.borderGrayLight};
+
+ background-color: ${theme.semantic.bgWhite};
+ @supports ((-webkit-backdrop-filter: blur(15px)) or (backdrop-filter: blur(15px))) {
+ background-color: ${theme.semantic.bgBlurWhiteOP};
+ -webkit-backdrop-filter: blur(15px);
+ backdrop-filter: blur(15px);
+ }
+`;
+
+const STYLES_CAROUSEL_MOBILE_FOOTER = (theme) => css`
+ ${Styles.HORIZONTAL_CONTAINER_CENTERED};
+ justify-content: space-between;
+ z-index: 1;
+ width: 100%;
+ padding: 8px 16px;
+ border-top: 1px solid ${theme.semantic.borderGrayLight};
+ color: ${theme.semantic.textGrayDark};
+
+ background-color: ${theme.semantic.bgWhite};
+ @supports ((-webkit-backdrop-filter: blur(15px)) or (backdrop-filter: blur(15px))) {
+ background-color: ${theme.semantic.bgBlurWhite};
+ -webkit-backdrop-filter: blur(15px);
+ backdrop-filter: blur(15px);
+ }
+`;
+
+const STYLES_CAROUSEL_MOBILE_SLIDE_COUNT = css`
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+`;
+
+function CarouselHeaderMobile({ current, total, onClose, onNextSlide, onPreviousSlide }) {
+ return (
+
+ );
+}
+
+function CarouselFooterMobile({ file, onAction, external, isOwner, data, viewer }) {
+ const [isEditInfoVisible, { showControl: showEditInfo, hideControl: hideEditInfo }] =
+ useCarouselJumperControls();
+
+ const [isShareFileVisible, { showControl: showShareFile, hideControl: hideShareFile }] =
+ useCarouselJumperControls();
+
+ const [isMoreInfoVisible, { showControl: showMoreInfo, hideControl: hideMoreInfo }] =
+ useCarouselJumperControls();
+
+ const [isEditChannelsVisible, { showControl: showEditChannels, hideControl: hideEditChannels }] =
+ useCarouselJumperControls();
+ return (
+ <>
+
+ {isOwner && (
+
+ )}
+ {isOwner && (
+
+ )}
+
+
+
+
+ >
+ );
+}
+
+/* -------------------------------------------------------------------------------------------------
+ * Carousel Controls
+ * -----------------------------------------------------------------------------------------------*/
+
+const useCarouselKeyCommands = ({ handleNext, handlePrevious, handleClose }) => {
+ const handleKeyDown = (e) => {
+ const inputs = document.querySelectorAll("input");
+ for (let elem of inputs) {
+ if (document.activeElement === elem) {
+ return;
+ }
+ }
+
+ const textareas = document.querySelectorAll("textarea");
+ for (let elem of textareas) {
+ if (document.activeElement === elem) {
+ return;
+ }
+ }
+
+ switch (e.key) {
+ case "Right":
+ case "ArrowRight":
+ handleNext();
+ break;
+ case "Left":
+ case "ArrowLeft":
+ handlePrevious();
+ break;
+ }
+ };
+
+ useEscapeKey(handleClose);
+
+ useEventListener({ type: "keydown", handler: handleKeyDown });
+};
+
+const STYLES_CONTROLS_BUTTON = (theme) => css`
+ ${Styles.BUTTON_RESET};
+ background-color: ${theme.semantic.bgGrayLight};
+ border-radius: 8px;
+ border: 1px solid ${theme.semantic.borderGrayLight};
+ padding: 10px;
+ box-shadow: ${theme.shadow.lightMedium};
+ svg {
+ display: block;
+ }
+`;
+
+const STYLES_CONTROLS_WRAPPER = css`
+ ${Styles.CONTAINER_CENTERED};
+ position: absolute;
+ width: 122px;
+ height: 100%;
+ z-index: 1;
+ top: 0;
+ padding-left: 24px;
+ padding-right: 24px;
+`;
+
+function CarouselControls({
+ enableNextSlide,
+ enablePreviousSlide,
+ onNextSlide,
+ onPreviousSlide,
+ onClose,
+}) {
+ useCarouselKeyCommands({
+ handleNext: onNextSlide,
+ handlePrevious: onPreviousSlide,
+ handleClose: onClose,
+ });
+
+ const [areControlsVisible, setControlsVisibility] = React.useState(true);
+ const timeoutRef = React.useRef();
+
+ const showControls = () => {
+ clearTimeout(timeoutRef.current);
+ setControlsVisibility(true);
+ };
+
+ const hideControls = () => {
+ timeoutRef.current = setTimeout(() => {
+ setControlsVisibility(false);
+ }, 500);
+ };
+
+ React.useEffect(() => {
+ timeoutRef.current = setTimeout(hideControls, 3000);
+ return () => clearTimeout(timeoutRef.current);
+ }, []);
+
+ return (
+ <>
+
+ {enablePreviousSlide ? (
+
+
+
+ ) : null}
+
+
+ {enableNextSlide ? (
+
+
+
+ ) : null}
+
+ >
+ );
+}
+
+/* -------------------------------------------------------------------------------------------------
+ * Carousel Content
+ * -----------------------------------------------------------------------------------------------*/
+
+const STYLES_CONTENT = (theme) => css`
+ position: relative;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ @media (max-width: ${theme.sizes.mobile}px) {
+ flex-direction: column;
+ overflow-y: auto;
+ justify-content: flex-start;
+ }
+`;
+
+const STYLES_PREVIEW_WRAPPER = (theme) => css`
+ ${Styles.CONTAINER_CENTERED};
+ position: relative;
+ width: 100%;
+ height: 100%;
+
+ @media (max-width: ${theme.sizes.mobile}px) {
+ min-height: 75vh;
+ height: 75vh;
+ overflow: hidden;
+ }
+`;
+
+export function CarouselContent({
+ carouselType,
+ objects,
+ index,
+ data,
+ isMobile,
+ viewer,
+ sidebar,
+ style,
+ onClose,
+}) {
+ const file = objects?.[index];
+
+ let isRepost = false;
+ if (carouselType === "SLATE") isRepost = data?.ownerId !== file.ownerId;
+
+ useLockScroll();
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {file?.name || file?.filename}
+
+
+
+ {file?.body}
+
+
+
+
+
+
+ {file.url}
+
+
+
+
+
+ >
+ );
+}
+
+/* -------------------------------------------------------------------------------------------------
+ * Global Carousel
+ * -----------------------------------------------------------------------------------------------*/
+const useCarouselViaParams = ({ index, params, objects, onChange }) => {
+ const findSelectedIndex = () => {
+ const cid = params?.cid;
+ if (!cid) return -1;
+
+ let index = objects.findIndex((elem) => elem.cid === cid);
+ return index;
+ };
+
+ React.useEffect(() => {
+ if (index !== -1) return;
+
+ const selectedIndex = findSelectedIndex();
+ if (selectedIndex !== index) onChange(index);
+ }, [params?.cid]);
+
+ React.useEffect(() => {
+ if (params?.cid) {
+ const index = findSelectedIndex();
+ onChange(index);
+ }
+ }, [params]);
+};
+
+const getCarouselHandlers = ({ index, objects, params, onChange, onAction }) => {
+ const handleNext = (e) => {
+ if (e) e.stopPropagation();
+
+ let nextIndex = index + 1;
+ if (nextIndex >= objects.length) return;
+
+ let { cid } = objects[nextIndex];
+ onChange(nextIndex);
+ onAction({ type: "UPDATE_PARAMS", params: { ...params, cid }, redirect: true });
+ };
+
+ const handlePrevious = (e) => {
+ if (e) e.stopPropagation();
+
+ let prevIndex = index - 1;
+ if (prevIndex < 0) return;
+
+ let { cid } = objects[prevIndex];
+ onChange(prevIndex);
+ onAction({ type: "UPDATE_PARAMS", params: { params, cid }, redirect: true });
+ };
+
+ const handleClose = (e) => {
+ if (e) e.stopPropagation(), e.preventDefault();
+
+ let params = { ...params };
+ delete params.cid;
+ onAction({ type: "UPDATE_PARAMS", params, redirect: true });
+ onChange(-1);
+ };
+ return { handleNext, handlePrevious, handleClose };
+};
+
+const STYLES_ROOT = (theme) => css`
+ ${Styles.VERTICAL_CONTAINER};
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ height: 100vh;
+ color: ${Constants.system.white};
+ z-index: ${Constants.zindex.modal};
+ background-color: rgba(0, 0, 0, 0.8);
+
+ // Note(Amine): we're using the blur filter to fix a weird backdrop-filter's bug in chrome
+
+ filter: blur(0px);
+ @supports ((-webkit-backdrop-filter: blur(15px)) or (backdrop-filter: blur(15px))) {
+ -webkit-backdrop-filter: blur(15px);
+ backdrop-filter: blur(15px);
+ }
+
+ @media (max-width: ${Constants.sizes.mobile}px) {
+ background-color: ${theme.semantic.bgWhite};
+ }
+
+ @keyframes global-carousel-fade-in {
+ from {
+ transform: translateX(8px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0px);
+ opacity: 1;
+ }
+ }
+ animation: global-carousel-fade-in 400ms ease;
+`;
+
+export function GlobalCarousel({
+ carouselType,
+ objects,
+ index,
+ params,
+ data,
+ isMobile,
+ onChange,
+ onAction,
+ viewer,
+ external,
+ isOwner,
+ sidebar,
+ style,
+}) {
+ const file = objects?.[index];
+ const isCarouselOpen = (carouselType || index > 0 || index <= objects.length) && !!file;
+
+ useCarouselViaParams({ index, params, objects, onChange });
+
+ if (!isCarouselOpen) return null;
+
+ const { handleNext, handlePrevious, handleClose } = getCarouselHandlers({
+ index,
+ objects,
+ params,
+ onChange,
+ onAction,
+ });
+
+ return (
+
+ {!isMobile ? (
+ 0}
+ onNextSlide={handleNext}
+ onPreviousSlide={handlePrevious}
+ onClose={handleClose}
+ />
+ ) : null}
+
+ {isMobile ? (
+
+ ) : (
+
+ )}
+
+
+ {isMobile ? (
+
+ ) : null}
+
+ );
+}