1
Fork 0

Add the tour logic, introduction and homepage tours.

This commit is contained in:
Bauke 2023-06-15 14:53:00 +02:00
parent 8c62714b9b
commit 97cb3a5603
Signed by: Bauke
GPG Key ID: C1C0F29952BCF558
6 changed files with 750 additions and 2 deletions

11
source/tours/exports.ts Normal file
View File

@ -0,0 +1,11 @@
import {homepageSteps, homepageEventHandlers} from "./interface/exports.js";
import {introductionSteps} from "./introduction.js";
export const tourIds = ["introduction", "interface-homepage"] as const;
export const tourIdsAndSteps: Array<
[TourId, TourStepOptions[], TourStepEventHandler[]]
> = [
["introduction", introductionSteps, []],
["interface-homepage", homepageSteps, homepageEventHandlers],
];

View File

@ -0,0 +1,4 @@
export {
eventHandlers as homepageEventHandlers,
steps as homepageSteps,
} from "./homepage.js";

View File

@ -0,0 +1,499 @@
import type Shepherd from "shepherd.js";
import {
addDatasetCounter,
encapsulateElements,
removeAllDatasetCounters,
renderInContainer,
} from "../utilities.js";
const step01 = renderInContainer(
<>
<h1>The Homepage</h1>
<p>
If you plan on staying a while, this is likely the place you'll see the
most. So let's work our way top to bottom, left to right.
</p>
<p>Starting with...</p>
</>,
);
const step02 = renderInContainer(
<>
<h1>The Main Header</h1>
<p>
On the left there is the Tildes logo and title, which you can click to get
back to the homepage, or refresh it if you're already there. If you are in
a group, the group name will also be shown.
</p>
<p>
On the right is the notifications section and your username. When you have
unread messages and/or are mentioned, they will show up next to your
username. If you don't have any notifications right now, you can{" "}
<a href="#" onClick={toggleMockNotifications}>
click here
</a>{" "}
to show a preview of what they look like.
</p>
<p>
At the moment mentions only work in comments, so if you get mentioned in a
topic's text body and don't get a notification,{" "}
<a target="_blank" href="https://gitlab.com/tildes/tildes/-/issues/195">
that's why
</a>
.
</p>
</>,
);
// Toggle mock notifications, based on the code in the following link.
// If the logic for this needs to be updated, also update the permalink to the
// actual Tildes HTML code.
// https://gitlab.com/tildes/tildes/-/blob/0dbb031562cd9297968fec0049af4b833ef77301/tildes/tildes/templates/macros/user.jinja2#L9-20
function toggleMockNotifications(event: Event): void {
// Prevent the click from going through so the anchor isn't removed.
event.preventDefault();
const root = document.querySelector("#site-header .logged-in-user-info");
function toggle(existingSelector: string, href: string, text: string): void {
const existing =
root?.querySelector<HTMLElement>(existingSelector) ?? undefined;
if (existing === undefined) {
// If no notifiaction exists, render our mock.
const messageNotification = document.createElement("a");
const count = 1 + Math.ceil(Math.random() * 10);
messageNotification.classList.add("logged-in-user-alert");
messageNotification.dataset.tildesShepherdMock = "true";
messageNotification.href = href;
messageNotification.textContent = `${count} ${text}`;
root?.insertAdjacentElement("beforeend", messageNotification);
} else if (existing.dataset.tildesShepherdMock === "true") {
// If a notification exists and it has our mock attribute, remove it.
existing.remove();
} else {
// A real message notification already exists, so we do nothing.
}
}
toggle('[href="/messages/unread"]', "/messages/unread", "new messages");
toggle(
'[href="/notifications/unread"]',
"/notifications/unread",
"new comments",
);
}
const step03 = renderInContainer(
<>
<h1>The Listing Options</h1>
<p>
Right below the main header are the topic listing options. These determine
how the topics in the listing are sorted and{" "}
<em>
<b>from</b> what period
</em>{" "}
topics should be shown.
</p>
<p>
The non-activity sorts are the easiest to explain, so let's start with
them:
</p>
<ul>
<li>
<b>Votes</b> sorts topics with the most votes first.
</li>
<li>
<b>Comments</b> sorts them with the most comments first.
</li>
<li>
And <b>New</b> sorts them by their date, so newest topics first.
</li>
</ul>
<p>
Then the first <b>Activity</b> will sort topics by bumping topics up as
they receive comments, however this sort makes use of comment labels.
</p>
<p>
If you don't know what comment labels are yet, don't worry, there is a
tour that explains them. But in short they are a community tool to
emphasize and de-emphasize comments, similar to votes but with more
specific intention. If you'd like to read more, check out{" "}
<a
target="_blank"
href="https://docs.tildes.net/instructions/commenting-on-tildes#labelling-comments"
>
the documentation
</a>
.
</p>
<p>
And finally the <b>All activity</b> sorts topics the same as Activity, but
without taking into account any comment labels.
</p>
<p>
If you only want to see topics from a certain period, click on the "from"
dropdown and select your period. Aside from a set of predefined options,
you can also set a custom one by clicking the "other period" option, after
which you'll be prompted for it.
</p>
</>,
);
const step04 = renderInContainer(
<>
<h1>The Topic Listing</h1>
<p>
Next up, just below the listing options is the topic listing itself. We've
only highlighted the first topic here, but you can probably see it
continues all the way down the page.
</p>
<p>Here, we've marked the main elements of the topic:</p>
<ol>
<li>
The topic title, when the topic is a "text topic" links to the same
place as the comments link does. Otherwise when it's a "link topic", it
will link to an external site.
</li>
<li>
The topic metadata, which by default includes the group it's in. If a
topic has specific notable tags and you have the "show tags" setting
enabled in your user settings, they will be shown after the group name.
A "topic type" may also be shown to indicate whether the topic is a text
topic, a video, an "ask" topic for recommendations or a survey, etc.
</li>
<li>
The amount of comments the topic has received. If any new comments have
been posted since you last viewed the topic, in orange a "(X new)" will
be added.
</li>
<li>
The topic source. For text topics this is always the poster of the
topic, but for link topics in certain groups it will be the domain name
together with that website's icon. For topics posted automatically by
Tildes itself, it will be "Scheduled topic".
</li>
</ol>
<p>The list continues on the next page.</p>
</>,
);
const step05 = renderInContainer(
<>
<h1>The Topic Listing continued</h1>
<ol start={5}>
<li>
The date when the topic was posted. For dates that are pretty recent it
will show something like "X days ago" while for longer dates it will be
the exact date, like "April 26th, 2021".
</li>
<li>
The voting button, clicking it will add your vote to the topic. On
Tildes, once the topic is older than 30 days you can no longer vote on
it and the individual voting data for is removed, with only the total
count kept. You can read more about it in{" "}
<a target="_blank" href="https://tild.es/jhm">
this announcement topic
</a>
.
</li>
<li>
And finally the topic actions. Clicking on the "Actions" button will
show a dropdown including "Bookmark", "Ignore this topic" and if you
have been granted the permission for it, "Edit title". Ignoring the
topic will remove it from the topic listing and prevent any future
notifications from happening. Both your bookmarked and ignored topics
can be found in their respective sections on your profile.
</li>
</ol>
</>,
);
const step06 = renderInContainer(
<>
<h1>The Sidebar</h1>
<p>
Let's take a look at the sidebar, where the first thing we're greeted with
is the search bar.
</p>
<p>
A quick note for mobile use, the sidebar can opened via a button that
appears in the very top-right of the page.
</p>
<p>
When searching from the homepage, topics will be included from any group.
However, searching when inside a group will only include topics from that
group and any sub-groups.
</p>
<p>
Below the search bar you will also find the title of the page, or name of
the group, and a small description. The sidebar has more elements when in
you're in a group page, but those are touched upon in the groups tour.
</p>
</>,
);
const step07 = renderInContainer(
<>
<h1>The Sidebar Subscriptions</h1>
<p>
Moving on, you'll find the list of groups you are subscribed to and a
button below it to go to the groups listing, where you can find all the
that are available.
</p>
<p>
To post a topic, subscribe or unsubscribe from any group, go to the group
in question and in the sidebar there will be buttons to do so.
</p>
</>,
);
const step08 = renderInContainer(
<>
<h1>The Sidebar User Settings</h1>
<p>
At the bottom of the sidebar you will find some user settings.
Specifically the filtered topics tags you have set and a link to the
settings page.
</p>
<p>
When you have filtered topics tags set, any topics with any of the tags
present will be removed from your listing. You can read more about it by
going to the dedicated page for it by clicking the "Edit filtered tags"
button.
</p>
</>,
);
const step09 = renderInContainer(
<>
<h1>The Footer</h1>
<p>
And finally, the very last part of the homepage is the footer. Here you
will find a theme selector (more on that in a moment) and various links to
places, such as the Tildes Documentation, Contact page, the source code,
etc. We highly recommend reading through the documentation as it explains
a lot of the how and why Tildes does certain things.
</p>
<p>
As for the theme selector, you can change your theme with a number of
options. When you change it here, it will only change for the current
device, if you'd like to set an account default for all devices, head to
your account settings.
</p>
</>,
);
const step10 = renderInContainer(
<>
<h1>This is the end, beautiful friend</h1>
<p>And that's the end of the Homepage tour!</p>
<p>
As always, if you find any bugs, have feature requests or simply want to
ask a question. Please send us a message at{" "}
<a target="_blank" href="https://tildes.net/user/Community">
@Community
</a>
.
</p>
<p>Happy Tildying!~</p>
</>,
);
export const steps: Shepherd.Step.StepOptions[] = [
{
id: "homepage-01",
text: step01,
},
{
attachTo: {
element: "#site-header",
on: "bottom",
},
canClickTarget: false,
id: "homepage-02",
scrollTo: true,
text: step02,
},
{
attachTo: {
element: "main > .listing-options",
on: "bottom",
},
canClickTarget: false,
id: "homepage-03",
text: step03,
},
{
attachTo: {
element: ".topic-listing > li:first-child",
on: "bottom",
},
canClickTarget: false,
id: "homepage-04",
text: step04,
},
{
attachTo: {
element: ".topic-listing > li:first-child",
on: "bottom",
},
canClickTarget: false,
id: "homepage-05",
text: step05,
},
{
attachTo: {
element: '#sidebar [data-tildes-shepherd-encapsulated="homepage-06"]',
on: "left-start",
},
canClickTarget: false,
id: "homepage-06",
text: step06,
},
{
attachTo: {
element: '#sidebar [data-tildes-shepherd-encapsulated="homepage-07"]',
on: "left-start",
},
canClickTarget: false,
id: "homepage-07",
scrollTo: true,
text: step07,
},
{
attachTo: {
element: "#sidebar .divider + .nav",
on: "left-start",
},
canClickTarget: false,
id: "homepage-08",
scrollTo: true,
text: step08,
},
{
attachTo: {
element: "#site-footer",
on: "top",
},
canClickTarget: false,
id: "homepage-09",
scrollTo: true,
text: step09,
},
{
canClickTarget: false,
id: "homepage-10",
text: step10,
},
];
export const eventHandlers: TourStepEventHandler[] = [
[
"homepage-04",
[
"show",
() => {
const topic = ".topic-listing > li:first-child";
const counters = [
".topic-title",
".topic-metadata",
".topic-info-comments",
".topic-info-source",
"time",
".topic-voting",
".topic-actions",
];
for (const [count, selector] of counters.entries()) {
addDatasetCounter(`${topic} ${selector}`, count + 1);
}
},
],
],
[
"homepage-05",
[
"destroy",
() => {
removeAllDatasetCounters();
},
],
],
[
"homepage-05",
[
"show",
() => {
encapsulateElements(
"homepage-06",
"#sidebar .sidebar-controls",
"afterend",
["#sidebar .form-search", "#sidebar h2", "#sidebar p"],
);
},
],
],
[
"homepage-06",
[
"show",
() => {
encapsulateElements("homepage-07", "#sidebar .divider", "beforebegin", [
"#sidebar .nav",
'#sidebar [href="/groups"',
]);
},
],
],
[
"homepage-08",
[
"show",
() => {
const filteredTags =
document.querySelector<HTMLDetailsElement>("#sidebar details") ??
undefined;
if (filteredTags === undefined) {
console.warn("Element is unexpectedly undefined");
return;
}
filteredTags.open = true;
},
],
],
];

View File

@ -0,0 +1,122 @@
import type Shepherd from "shepherd.js";
import {createIntroductionUnderstood} from "../storage/common.js";
import {openOptionsPageFromBackground, renderInContainer} from "./utilities.js";
const stepOne = renderInContainer(
<>
<h1>Thank you for installing Tildes Shepherd!</h1>
<p>
Tildes Shepherd is a set of interactive guided tours for Tildes to show
you around and introduce you to the concepts that make this website great.
</p>
<p>
To see and start the tours that are available, click on the extension icon
to go to the options page or{" "}
<a href="#" onClick={openOptionsPageFromBackground}>
click here now
</a>
.
</p>
<p>
Each tour will start with a pop-up like this one and have "Continue",
"Back" and "Exit" buttons at the bottom. They will progress the tour
forward, backward or exit it.
</p>
</>,
);
const stepTwo = renderInContainer(
<>
<h1>Tour Mechanics</h1>
<p>
During the tours, at various points we will want to highlight specific
areas. When that happens you won't be able to click anything inside them,
mainly to prevent you from accidentally going to a different page and
interrupting the tour. But also to prevent you from taking actions you
maybe don't want to take, like voting on something you don't necessarily
want to vote on.
</p>
<p>
Depending on your selected theme, the highlighted areas will have a yellow
or orange border and some extra added whitespace. As you can see in this
example where the main site header has been highlighted.
</p>
</>,
);
const stepThree = renderInContainer(
<>
<p>
If you find any bugs, have feature requests or simply want to ask a
question. Please send us a message at{" "}
<a target="_blank" href="https://tildes.net/user/Community">
@Community
</a>
.
</p>
<p>
Also, big shoutout to the people at{" "}
<a target="_blank" href="https://shipshape.io">
Ship Shape
</a>{" "}
for making{" "}
<a target="_blank" href="https://shepherdjs.dev">
Shepherd.js
</a>
, the software library making this entire project so much easier to
create. And the namesake of it, too.
</p>
<p>
Once you click the "I understand" button below, this message won't pop up
again, so remember the extension icon is how you get to the tours.
</p>
<p>Happy Tildying!~</p>
</>,
);
export const introductionSteps: Shepherd.Step.StepOptions[] = [
{
id: "introduction-1",
text: stepOne,
},
{
attachTo: {
element: "#site-header",
on: "bottom",
},
canClickTarget: false,
id: "introduction-2",
text: stepTwo,
},
{
buttons: [
{
classes: "btn",
text: "I understand",
async action() {
const introductionUnderstood = await createIntroductionUnderstood();
introductionUnderstood.value = true;
await introductionUnderstood.save();
this.complete();
},
},
{
classes: "btn",
text: "Back",
action() {
this.back();
},
},
],
id: "introduction-3",
text: stepThree,
},
];

106
source/tours/utilities.ts Normal file
View File

@ -0,0 +1,106 @@
import {type JSX, render} from "preact";
import browser from "webextension-polyfill";
/**
* Adds a `[data-tildes-shepherd-counter]` attribute to a specified element. For
* the associated CSS, see `source/content-scripts/scss/main.scss`.
*
* @param selector The selector of element to apply the counter to, if the
* target element can't be selected an error will be thrown.
* @param count The number to display in the counter.
*/
export function addDatasetCounter(selector: string, count: number) {
const element = document.querySelector<HTMLElement>(selector) ?? undefined;
if (element === undefined) {
throw new Error(`Target element for "${selector}" is undefined`);
}
element.dataset.tildesShepherdCounter = count.toString();
}
/**
* Create a new `div` element and put all the specified elements inside it. Used
* for highlighting groups of elements that have sibling elements we may not
* want to highlight. Like highlighting only the search bar, title and
* description in the homepage sidebar, but not all the rest of the sidebar.
* @param stepId The unique step ID to set as
* `data-tildes-shepherd-encapsulated="<id>"`. Used for attaching the Shepherd
* step to the container.
* @param rootSelector The selector for the root element to insert the container
* near. This doesn't need to be the parent element necessarily, since
* {@link Element.insertAdjacentElement} is used to do the inserting it can also
* be inserted before or after the root element.
* @param position The insert position for {@link Element.insertAdjacentElement}.
* @param selectors The list of element selectors to include in the new
* container. Note that you should make sure any elements included don't break
* their CSS when contained by a new `div` element and ideally they should all
* be adjacent to one another and specified in the order they appear in.
*/
export function encapsulateElements(
stepId: string,
rootSelector: string,
position: InsertPosition,
selectors: string[],
) {
const container = document.createElement("div");
container.dataset.tildesShepherdEncapsulated = stepId;
for (const selector of selectors) {
const element = document.querySelector<HTMLElement>(selector) ?? undefined;
if (element === undefined) {
console.warn(`Unexpected undefined element: ${selector}`);
continue;
}
// Don't encapsulate anything if the parent is already one of our elements.
if (
element.parentElement?.dataset.tildesShepherdEncapsulated !== undefined
) {
return;
}
container.append(element);
}
const root = document.querySelector(rootSelector) ?? undefined;
if (root === undefined) {
throw new Error(`Root selector returned undefined: ${rootSelector}`);
}
root.insertAdjacentElement(position, container);
}
/**
* Content scripts can't open the options page themselves, so send a message to
* the background script and open it there.
* @param event The mouse click event to prevent from happening.
*/
export async function openOptionsPageFromBackground(
event: MouseEvent,
): Promise<void> {
event.preventDefault();
await browser.runtime.sendMessage("open-options-page");
}
/**
* Removes all elements with a `data-tildes-shepherd-counter` set.
*/
export function removeAllDatasetCounters() {
const elements = document.querySelectorAll<HTMLElement>(
"[data-tildes-shepherd-counter]",
);
for (const element of Array.from(elements)) {
delete element.dataset.tildesShepherdCounter;
}
}
/**
* Render JSX inside a `div` and return the div. This is mostly used so we can
* pass JSX more easily to Shepherd, which doesn't accept JSX directly but does
* accept {@link HTMLElement}. So this bit of indirection gets us there.
*/
export function renderInContainer(root: JSX.Element): HTMLElement {
const container = document.createElement("div");
render(root, container);
return container;
}

10
source/types.d.ts vendored
View File

@ -1,8 +1,14 @@
// Export something so TypeScript doesn't see this file as an ambient module.
export {};
import type Shepherd from "shepherd.js";
import type {tourIds} from "./tours/exports.js";
declare global {
const $browser: "chromium" | "firefox";
const $dev: boolean;
const $test: boolean;
type TourId = (typeof tourIds)[number];
type TourStepEvent = "show" | "destroy";
type TourStepEventFunction = Parameters<Shepherd.Step["on"]>[1];
type TourStepEventHandler = [string, [TourStepEvent, TourStepEventFunction]];
type TourStepOptions = Shepherd.Step.StepOptions;
}