diff --git a/pkg/interface/src/logic/lib/relativePosition.tsx b/pkg/interface/src/logic/lib/relativePosition.tsx new file mode 100644 index 000000000..8ad1618e5 --- /dev/null +++ b/pkg/interface/src/logic/lib/relativePosition.tsx @@ -0,0 +1,55 @@ +import _ from "lodash"; + +export const alignY = ["top", "bottom"] as const; +export type AlignY = typeof alignY[number]; +export const alignX = ["left", "right"] as const; +export type AlignX = typeof alignX[number]; + +export function getRelativePosition( + relativeTo: HTMLElement | null, + alignX: AlignX | AlignX[], + alignY: AlignY | AlignY[], + offsetX: number = 0, + offsetY: number = 0 +) { + const rect = relativeTo?.getBoundingClientRect(); + if (!rect) { + return {}; + } + const bounds = { + top: rect.top - offsetY, + left: rect.left - offsetX, + bottom: document.documentElement.clientHeight - rect.bottom - offsetY, + right: document.documentElement.clientWidth - rect.right - offsetX, + }; + const alignXArr = _.isArray(alignX) ? alignX : [alignX]; + const alignYArr = _.isArray(alignY) ? alignY : [alignY]; + + return { + ..._.reduce( + alignXArr, + (acc, a, idx) => ({ + ...acc, + [a]: _.zipWith( + [...Array(idx), `${bounds[a]}px`], + acc[a] || [], + (a, b) => a || b || null + ), + }), + {} + ), + ..._.reduce( + alignYArr, + (acc, a, idx) => ({ + ...acc, + [a]: _.zipWith( + [...Array(idx), `${bounds[a]}px`], + acc[a] || [], + (a, b) => a || b || null + ), + }), + {} + ), + } as Record; +} + diff --git a/pkg/interface/src/types/local-update.ts b/pkg/interface/src/types/local-update.ts index 8438c8434..814832e96 100644 --- a/pkg/interface/src/types/local-update.ts +++ b/pkg/interface/src/types/local-update.ts @@ -1,3 +1,6 @@ +export const tutorialProgress = ['hidden', 'start', 'group-desc', 'channels', 'chat', 'link', 'publish', 'notifications', 'profile', 'leap', 'done'] as const; + +export type TutorialProgress = typeof tutorialProgress[number]; interface LocalUpdateSetDark { setDark: boolean; } @@ -48,4 +51,4 @@ export type LocalUpdate = | LocalUpdateHideAvatars | LocalUpdateHideNicknames | LocalUpdateSetOmniboxShown -| RemoteContentPolicy; \ No newline at end of file +| RemoteContentPolicy; diff --git a/pkg/interface/src/views/App.js b/pkg/interface/src/views/App.js index 689704ad4..3e1650dc3 100644 --- a/pkg/interface/src/views/App.js +++ b/pkg/interface/src/views/App.js @@ -20,6 +20,7 @@ import { Content } from './landscape/components/Content'; import StatusBar from './components/StatusBar'; import Omnibox from './components/leap/Omnibox'; import ErrorBoundary from '~/views/components/ErrorBoundary'; +import { TutorialModal } from '~/views/landscape/components/TutorialModal'; import GlobalStore from '~/logic/store/store'; import GlobalSubscription from '~/logic/subscription/global'; @@ -149,6 +150,7 @@ class App extends React.Component { + ) => { + e.stopPropagation(); + const idx = progress.findIndex((p) => p === tutorialProgress); + const { url } = progressDetails[progress[idx + 1]]; + history.push(url); + nextTutStep(); + }, + [nextTutStep, history, tutorialProgress, setCoords] + ); + const prev = useCallback(() => { + const idx = progress.findIndex((p) => p === tutorialProgress); + history.push(progressDetails[progress[idx - 1]].url); + prevTutStep(); + }, [prevTutStep, history, tutorialProgress]); + + const updatePos = useCallback(() => { + const newCoords = getRelativePosition( + tutorialRef, + alignX, + alignY, + offsetX, + offsetY + ); + if (newCoords) { + setCoords(newCoords); + } + }, [tutorialRef]); + + const dismiss = useCallback(() => { + hideTutorial(); + props.api.settings.putEntry("tutorial", "seen", true); + }, [hideTutorial, props.api]); + + const leaveGroup = useCallback(async () => { + await props.api.groups.leaveGroup("~hastuc-dibtux", "beginner-island"); + }, [props.api]); + + const progressIdx = progress.findIndex((p) => p === tutorialProgress); + + useEffect(() => { + if ( + tutorialProgress !== "hidden" && + tutorialProgress !== "done" && + tutorialRef + ) { + const interval = setInterval(updatePos, 100); + return () => { + clearInterval(interval); + }; + } + return () => {}; + }, [tutorialRef, tutorialProgress, updatePos]); + + // manually center final window + useEffect(() => { + if (tutorialProgress === "done") { + const { innerWidth, innerHeight } = window; + const left = `${(innerWidth - MODAL_WIDTH) / 2}px`; + const top = `${(innerHeight - MODAL_HEIGHT) / 2}px`; + console.log(`resetting ${top} ${left}`); + setCoords({ top, left }); + } + }, [tutorialProgress]); + + if (tutorialProgress === "hidden") { + return null; + } + + return ( + + + + + + + + {title} + + {description} + {tutorialProgress !== "done" ? ( + + + + + + {progressIdx}/{progress.length - 1} + + + + + + ) : ( + + + Leave Group + + + + )} + + + + ); +}