diff --git a/pkg/interface/src/logic/lib/tutorialModal.ts b/pkg/interface/src/logic/lib/tutorialModal.ts new file mode 100644 index 000000000..ce5820ed9 --- /dev/null +++ b/pkg/interface/src/logic/lib/tutorialModal.ts @@ -0,0 +1,120 @@ +import { TutorialProgress, Associations } from "~/types"; +import { + AlignX, + AlignY, +} from "~/logic/lib/relativePosition"; + +export const MODAL_WIDTH = 256; +export const MODAL_HEIGHT = 180; +export const MODAL_WIDTH_PX = `${MODAL_WIDTH}px`; +export const MODAL_HEIGHT_PX = `${MODAL_HEIGHT}px`; + +interface StepDetail { + title: string; + description: string; + url: string; + alignX: AlignX | AlignX[]; + alignY: AlignY | AlignY[]; + offsetX: number; + offsetY: number; +} + +export function hasTutorialGroup(props: { associations: Associations }) { + return '/ship/~hastuc-dibtux/beginner-island' in props.associations.groups; +} + +export const progressDetails: Record = { + hidden: {} as any, + done: { + title: "End", + description: "This tutorial is finished. Would you like to leave Beginner Island?", + url: "/", + alignX: "right", + alignY: "top", + offsetX: MODAL_WIDTH + 8, + offsetY: 0 + }, + start: { + title: "New group added", + description: + "We just added you to the Beginner island group to show you around. This group is public, but other groups can be private", + url: "/", + alignX: "right", + alignY: "top", + offsetX: MODAL_WIDTH + 8, + offsetY: 0 + }, + 'group-desc': { + title: "What's a group", + description: "A group contains members and tends to be centered around a topic or multiple topics.", + url: "/~landscape/ship/~hastuc-dibtux/beginner-island", + alignX: "left", + alignY: "top", + offsetX: MODAL_WIDTH + 8, + offsetY: (MODAL_HEIGHT / 2) - 8, + }, + channels: { + title: "Channels", + description: "Inside a group you have three types of Channels: Chat, Collection, or Notebook. Mix and match these depending on your group context!", + url: "/~landscape/ship/~hastuc-dibtux/beginner-island", + alignY: "top", + alignX: "right", + offsetX: MODAL_WIDTH + 8, + offsetY: -8, + }, + chat: { + title: "Chat", + description: "Chat channels are for messaging within your group. Direct Messages are also supported, and are accessible from the “DMs” tile on the homescreen", + url: "/~landscape/ship/~hastuc-dibtux/beginner-island/resource/chat/ship/~hastuc-dibtux/chat-8401", + alignY: "top", + alignX: "right", + offsetX: 0, + offsetY: -32, + }, + link: { + title: "Collection", + description: "A collection is where you can share and view links, images, and other media within your group. Every item in a Collection can have it’s own comment thread.", + url: "/~landscape/ship/~hastuc-dibtux/beginner-island/resource/link/ship/~hastuc-dibtux/link-4353", + alignY: "top", + alignX: "right", + offsetX: 0, + offsetY: -32, + }, + publish: { + title: "Notebook", + description: "Notebooks are for creating long-form content within your group. Use markdown to create rich posts with headers, lists and images.", + url: "/~landscape/ship/~hastuc-dibtux/beginner-island/resource/publish/ship/~hastuc-dibtux/notebook-9148", + alignY: "top", + alignX: "right", + offsetX: 0, + offsetY: -32, + }, + notifications: { + title: "Notifications", + description: "Subscribing to a channel will send you notifications when there are new updates. You will also receive a notification when someone mentions your name in a channel.", + url: "/~landscape/ship/~hastuc-dibtux/beginner-island/resource/publish/ship/~hastuc-dibtux/notebook-9148/settings#notifications", + alignY: "top", + alignX: "right", + offsetX: 0, + offsetY: -32, + }, + profile: { + title: "Profile", + description: "Your profile is customizable and can be shared with other ships. Enter as much or as little information as you’d like.", + url: `/~profile/~${window.ship}`, + alignY: "top", + alignX: "right", + offsetX: -300 + (MODAL_WIDTH / 2), + offsetY: -120 + (MODAL_HEIGHT / 2), + }, + leap: { + title: "Leap", + description: "Leap allows you to go to a specific channel, message, collection, profile or group simply by typing in a command or selecting a shortcut from the dropdown menu.", + url: `/~profile/~${window.ship}`, + alignY: "top", + alignX: "left", + offsetX: 0, + offsetY: -32, + } +}; + diff --git a/pkg/interface/src/logic/state/local.tsx b/pkg/interface/src/logic/state/local.tsx index 8b3260904..64e68a0ca 100644 --- a/pkg/interface/src/logic/state/local.tsx +++ b/pkg/interface/src/logic/state/local.tsx @@ -1,13 +1,21 @@ import React, { ReactNode } from "react"; +import f from 'lodash/fp'; import create, { State } from 'zustand'; import { persist } from 'zustand/middleware'; import produce from 'immer'; -import { BackgroundConfig, RemoteContentPolicy } from "~/types/local-update"; +import { BackgroundConfig, RemoteContentPolicy, TutorialProgress, tutorialProgress } from "~/types/local-update"; + export interface LocalState extends State { hideAvatars: boolean; hideNicknames: boolean; remoteContentPolicy: RemoteContentPolicy; + tutorialProgress: TutorialProgress; + tutorialRef: HTMLElement | null, + hideTutorial: () => void; + nextTutStep: () => void; + prevTutStep: () => void; + setTutorialRef: (el: HTMLElement | null) => void; dark: boolean; background: BackgroundConfig; omniboxShown: boolean; @@ -15,12 +23,35 @@ export interface LocalState extends State { toggleOmnibox: () => void; set: (fn: (state: LocalState) => void) => void }; +export const selectLocalState = + (keys: K[]) => f.pick(keys); const useLocalState = create(persist((set, get) => ({ dark: false, background: undefined, hideAvatars: false, hideNicknames: false, + tutorialProgress: 'hidden', + tutorialRef: null, + setTutorialRef: (el: HTMLElement | null) => set(produce(state => { + state.tutorialRef = el; + })), + hideTutorial: () => set(produce(state => { + state.tutorialProgress = 'hidden'; + state.tutorialRef = null; + })), + nextTutStep: () => set(produce(state => { + const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress) + if(currIdx < tutorialProgress.length) { + state.tutorialProgress = tutorialProgress[currIdx + 1]; + } + })), + prevTutStep: () => set(produce(state => { + const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress) + if(currIdx > 0) { + state.tutorialProgress = tutorialProgress[currIdx - 1]; + } + })), remoteContentPolicy: { imageShown: true, audioShown: true, @@ -41,7 +72,10 @@ const useLocalState = create(persist((set, get) => ({ })), set: fn => set(produce(fn)) }), { - blacklist: ['suspendedFocus', 'toggleOmnibox', 'omniboxShown'], + blacklist: [ + 'suspendedFocus', 'toggleOmnibox', 'omniboxShown', 'tutorialProgress', + 'prevTutStep', 'nextTutStep', 'tutorialRef', 'setTutorialRef' + ], name: 'localReducer' })); diff --git a/pkg/interface/src/views/components/useTutorialModal.tsx b/pkg/interface/src/views/components/useTutorialModal.tsx new file mode 100644 index 000000000..7457e102a --- /dev/null +++ b/pkg/interface/src/views/components/useTutorialModal.tsx @@ -0,0 +1,21 @@ +import { useEffect } from "react"; +import { TutorialProgress } from "~/types"; +import useLocalState, { selectLocalState } from "~/logic/state/local"; + +const localSelector = selectLocalState(["tutorialProgress", "setTutorialRef"]); + +export function useTutorialModal( + onProgress: TutorialProgress, + show: boolean, + anchorRef: HTMLElement | null +) { + const { tutorialProgress, setTutorialRef } = useLocalState(localSelector); + + useEffect(() => { + if (show && onProgress === tutorialProgress && anchorRef) { + setTutorialRef(anchorRef); + } + }, [onProgress, tutorialProgress, show, anchorRef]); + + return show && onProgress === tutorialProgress; +}