Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@
const currentInstance = getCurrentInstance().proxy;
const store = currentInstance.$store;
const router = currentInstance.$router;
const { tourActive, isTourActive, startTour, endTour, resumeTour } = useTour();
const { isUserLoggedIn, isCoach, isAdmin, isSuperuser, isLearner, user_id } = useUser();
const { tourActive, isTourActive, startTour, endTour } = useTour();
const { isUserLoggedIn, isCoach, isAdmin, isSuperuser, isLearner, currentUserId } = useUser();

const { allowDownloadOnMeteredConnection } = useDeviceSettings();
const {
Expand Down Expand Up @@ -433,8 +433,7 @@
isTourActive,
startTour,
endTour,
resumeTour,
userId: user_id,
userId: currentUserId,
};
},
props: {
Expand Down Expand Up @@ -543,11 +542,10 @@
},
loading(newVal, oldVal) {
if (oldVal && !newVal) {
const isTourStarted = this.resumeTour(this.userId, 'LibraryPage');
if (isTourStarted) {
if (!this.welcomeModalVisible) {
setTimeout(() => {
this.startTour('LibraryPage');
}, 3000);
}, 300);
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,11 @@
},
},
mounted() {
if (this.topics.length && this.windowIsLarge) {
this.startTour('ExploreLibraries');
}
this.$nextTick(() => {
if (this.topics.length && this.windowIsLarge) {
this.startTour('ExploreLibraries');
}
});
},
};

Expand Down
9 changes: 3 additions & 6 deletions packages/kolibri/components/onboarding/TooltipTour.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,16 @@
import tippy from 'tippy.js/umd';
import { onboardingSteps } from 'kolibri/utils/onboardingSteps';
import useTour from 'kolibri/composables/useTour';
import useUser from '../../../kolibri/composables/useUser';
import TooltipContent from './TooltipContent.vue';

export default {
name: 'TooltipTour',
setup() {
const { saveTourProgress, completeTour, currentStepIndex } = useTour();
const { user_id } = useUser();
return {
saveTourProgress,
completeTour,
currentStepIndex,
userId: user_id,
};
},
props: {
Expand Down Expand Up @@ -164,11 +161,11 @@
nextStep() {
if (this.currentStepIndex < this.steps.length - 1) {
this.currentStepIndex++;
this.saveTourProgress(this.userId, this.page, this.currentStepIndex, true);
this.saveTourProgress(this.page, this.currentStepIndex, true);
this.showTooltip();
} else {
// Check if current page is the last key in onboardingSteps
this.saveTourProgress(this.userId, this.page, this.currentStepIndex, false);
this.saveTourProgress(this.page, this.currentStepIndex, false);
const pageKeys = Object.keys(onboardingSteps);
const isLastPage = this.page === pageKeys[pageKeys.length - 1];
if (isLastPage) {
Expand All @@ -180,7 +177,7 @@
prevStep() {
if (this.currentStepIndex > 0) {
this.currentStepIndex--;
this.saveTourProgress(this.userId, this.page, this.currentStepIndex, true);
this.saveTourProgress(this.page, this.currentStepIndex, true);
this.showTooltip();
}
},
Expand Down
156 changes: 91 additions & 65 deletions packages/kolibri/composables/useTour.js
Original file line number Diff line number Diff line change
@@ -1,87 +1,114 @@
import { reactive, ref } from 'vue';
import { onboardingSteps } from 'kolibri/utils/onboardingSteps';
import useUser from 'kolibri/composables/useUser';
import Lockr from 'lockr';

const TOUR_PROGRESS_KEY = 'kolibri_onboarding_tour_progress';
const TOUR_COMPLETE_KEY = 'kolibri_onboarding_tour_complete';
const TOUR_ACTIVE = 'kolibri_onboarding_tour_active';

const tourActive = ref(false);
const currentStepIndex = ref(0);
const tourActiveMap = reactive({});

// Temporarily disabling tour; uncomment lines 15-22 to re-enable
// eslint-disable-next-line no-unused-vars
function startTour(pageName) {
// Small delay to let users see the page before tour darkens it
// setTimeout(() => {
// localStorage.setItem(TOUR_ACTIVE, 'true');
// tourActive.value = true;
// Object.keys(tourActiveMap).forEach(key => {
// tourActiveMap[key] = false;
// });
// tourActiveMap[pageName] = true;
// }, 400);
return;
}
export default function useTour() {
const { currentUserId } = useUser();

function endTour(pageName) {
localStorage.setItem(TOUR_ACTIVE, 'false');
tourActive.value = false;
tourActiveMap[pageName] = false;
}
function isTourActive(pageName) {
return !!tourActiveMap[pageName];
}
function getProgressKey() {
return `${TOUR_PROGRESS_KEY}_${currentUserId.value}`;
}

function getTourProgress(userId) {
const progress = JSON.parse(localStorage.getItem(TOUR_PROGRESS_KEY));
return progress?.userId === userId ? progress : null;
}
/**
* Saving the tour progress of all pages to easily resume if needed
* Example stored value (per user):
* {
* "pages": {
* "LibraryPage": { "stepIndex": 3, "isTourActive": false },
* "ExploreLibraries": { "stepIndex": 1, "isTourActive": true }
* }
* }
*/
function saveTourProgress(page, stepIndex, isTourActive) {
const key = getProgressKey();
const progress = Lockr.get(key, { pages: {} });

function saveTourProgress(userId, page, stepIndex, isTourActive) {
localStorage.setItem(
TOUR_PROGRESS_KEY,
JSON.stringify({ userId, page, stepIndex, isTourActive }),
);
}
if (!progress.pages) {
progress.pages = {};
}

function completeTour() {
localStorage.setItem(TOUR_COMPLETE_KEY, 'true');
localStorage.removeItem(TOUR_PROGRESS_KEY);
}
progress.pages[page] = {
stepIndex,
isTourActive,
};

function isTourCompleted() {
return localStorage.getItem(TOUR_COMPLETE_KEY) === 'true';
}
function resumeTour(userId, page) {
const progress = getTourProgress(userId);
if ((progress && progress.isTourActive === false) || !progress) {
return false;
Lockr.set(key, progress);
}
const pageKeys = Object.keys(onboardingSteps);
const currentPageIndex = pageKeys.indexOf(page);
const welcomeDismissed = localStorage.getItem('DEVICE_WELCOME_MODAL_DISMISSED');
const prevPage = currentPageIndex === 0 ? null : pageKeys[currentPageIndex - 1];
const prevPageSteps = prevPage ? onboardingSteps[prevPage].steps : [];
const isLastStepOfPrevPage = prevPageSteps && progress.stepIndex === prevPageSteps.length - 1;
const steps = onboardingSteps[page].steps || [];
const isSamePage = progress.page === page;
if (welcomeDismissed && progress && (isSamePage || isLastStepOfPrevPage)) {
if (progress.stepIndex + 1 < steps.length) {
// Still steps left on current page
currentStepIndex.value = progress.stepIndex + 1;
return true;
} else if (progress.stepIndex + 1 === steps.length) {
currentStepIndex.value = progress.stepIndex;
return true;

function getTourProgress() {
const key = getProgressKey();
return Lockr.get(key, null);
}

function completeTour() {
const userId = currentUserId.value;

// Marking the tour as complete for the current user
// Completion map: { "userId1": true, "userId2": true, ... }
const completedMap = Lockr.get(TOUR_COMPLETE_KEY, {});
completedMap[userId] = true;
Lockr.set(TOUR_COMPLETE_KEY, completedMap);

// Clear the tour progress for the current user
Lockr.rm(getProgressKey());
}

function isTourCompleted() {
const userId = currentUserId.value;
const completedMap = Lockr.get(TOUR_COMPLETE_KEY, {});
return !!completedMap[userId];
}

function startTour(pageName) {
if (isTourCompleted()) return;

const stepsForPage = onboardingSteps[pageName]?.steps || [];
// Check if tour progress exists for current user for pageName
const existingProgress = getTourProgress();
const currentPageProgress = existingProgress?.pages?.[pageName];

if (currentPageProgress) {
const isLastStep = currentPageProgress.stepIndex >= stepsForPage.length - 1;
// If isTourActive is false or user completed all of the steps for current page,
// do not start the tour
if (currentPageProgress.isTourActive === false || isLastStep) return;
// Otherwise, user progress exists for pageName but steps are incomplete;
// set currentStepIndex to the saved stepIndex
currentStepIndex.value = currentPageProgress.stepIndex;
} else {
endTour();
return false;
currentStepIndex.value = 0;
}

// Small delay to let users see the page before tour darkens it
setTimeout(() => {
Lockr.set(TOUR_ACTIVE, true);
tourActive.value = true;
Object.keys(tourActiveMap).forEach(key => {
tourActiveMap[key] = false;
});
tourActiveMap[pageName] = true;
}, 400);
}

function endTour(pageName) {
Lockr.set(TOUR_ACTIVE, false);
tourActive.value = false;
tourActiveMap[pageName] = false;
}

function isTourActive(pageName) {
return !!tourActiveMap[pageName];
}
return false;
}

export default function useTour() {
return {
tourActive,
startTour,
Expand All @@ -92,7 +119,6 @@ export default function useTour() {
isTourCompleted,
isTourActive,
tourActiveMap,
resumeTour,
currentStepIndex,
};
}
Loading