mirror of https://github.com/zulip/zulip.git
onboarding_steps: Remove hotspot as an onboarding_step.
Earlier, hotspots and one-time notices were the valid type of onboarding step. Now, one-time notice is the only valid type. Fixes #29296.
This commit is contained in:
parent
e8769f80a6
commit
bf2360bcf2
|
@ -20,6 +20,16 @@ format used by the Zulip server that they are interacting with.
|
||||||
|
|
||||||
## Changes in Zulip 9.0
|
## Changes in Zulip 9.0
|
||||||
|
|
||||||
|
**Feature level 259**:
|
||||||
|
|
||||||
|
* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events):
|
||||||
|
For the `onboarding_steps` event type, an array of onboarding steps
|
||||||
|
to be displayed to clients is sent. Onboarding step now has one-time
|
||||||
|
notices as the only valid type. Prior to this, both hotspots and
|
||||||
|
one-time notices were valid types of onboarding steps. There is no compatibility
|
||||||
|
support, as we expect that only official Zulip clients will interact with
|
||||||
|
this data. Currently, no client other than the Zulip web app uses this.
|
||||||
|
|
||||||
**Feature level 258**:
|
**Feature level 258**:
|
||||||
|
|
||||||
* [`GET /user_groups`](/api/get-user-groups), [`POST
|
* [`GET /user_groups`](/api/get-user-groups), [`POST
|
||||||
|
|
|
@ -117,7 +117,6 @@ EXEMPT_FILES = make_set(
|
||||||
"web/src/hashchange.js",
|
"web/src/hashchange.js",
|
||||||
"web/src/hbs.d.ts",
|
"web/src/hbs.d.ts",
|
||||||
"web/src/hotkey.js",
|
"web/src/hotkey.js",
|
||||||
"web/src/hotspots.ts",
|
|
||||||
"web/src/inbox_ui.js",
|
"web/src/inbox_ui.js",
|
||||||
"web/src/inbox_util.ts",
|
"web/src/inbox_util.ts",
|
||||||
"web/src/info_overlay.ts",
|
"web/src/info_overlay.ts",
|
||||||
|
|
|
@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
|
||||||
# Changes should be accompanied by documentation explaining what the
|
# Changes should be accompanied by documentation explaining what the
|
||||||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||||
# entries in the endpoint's documentation in `zulip.yaml`.
|
# entries in the endpoint's documentation in `zulip.yaml`.
|
||||||
API_FEATURE_LEVEL = 258
|
API_FEATURE_LEVEL = 259
|
||||||
|
|
||||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
||||||
# only when going from an old version of the code to a newer version. Bump
|
# only when going from an old version of the code to a newer version. Bump
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.4 KiB |
|
@ -53,7 +53,6 @@ import "../../styles/lightbox.css";
|
||||||
import "../../styles/popovers.css";
|
import "../../styles/popovers.css";
|
||||||
import "../../styles/recent_view.css";
|
import "../../styles/recent_view.css";
|
||||||
import "../../styles/typing_notifications.css";
|
import "../../styles/typing_notifications.css";
|
||||||
import "../../styles/hotspots.css";
|
|
||||||
import "../../styles/dark_theme.css";
|
import "../../styles/dark_theme.css";
|
||||||
import "../../styles/user_status.css";
|
import "../../styles/user_status.css";
|
||||||
import "../../styles/widgets.css";
|
import "../../styles/widgets.css";
|
||||||
|
|
|
@ -24,7 +24,6 @@ import * as gear_menu from "./gear_menu";
|
||||||
import * as giphy from "./giphy";
|
import * as giphy from "./giphy";
|
||||||
import * as hash_util from "./hash_util";
|
import * as hash_util from "./hash_util";
|
||||||
import * as hashchange from "./hashchange";
|
import * as hashchange from "./hashchange";
|
||||||
import * as hotspots from "./hotspots";
|
|
||||||
import * as inbox_ui from "./inbox_ui";
|
import * as inbox_ui from "./inbox_ui";
|
||||||
import * as lightbox from "./lightbox";
|
import * as lightbox from "./lightbox";
|
||||||
import * as list_util from "./list_util";
|
import * as list_util from "./list_util";
|
||||||
|
@ -454,11 +453,6 @@ export function process_enter_key(e) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hotspots.is_open()) {
|
|
||||||
$(e.target).find(".hotspot.overlay.show .hotspot-confirm").trigger("click");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emoji_picker.is_open()) {
|
if (emoji_picker.is_open()) {
|
||||||
return emoji_picker.navigate("enter", e);
|
return emoji_picker.navigate("enter", e);
|
||||||
}
|
}
|
||||||
|
@ -786,10 +780,6 @@ export function process_hotkey(e, hotkey) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hotspots.is_open()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overlays.info_overlay_open()) {
|
if (overlays.info_overlay_open()) {
|
||||||
if (event_name === "show_shortcuts") {
|
if (event_name === "show_shortcuts") {
|
||||||
overlays.close_active();
|
overlays.close_active();
|
||||||
|
|
|
@ -1,403 +0,0 @@
|
||||||
import $ from "jquery";
|
|
||||||
import _ from "lodash";
|
|
||||||
import assert from "minimalistic-assert";
|
|
||||||
|
|
||||||
import whale_image from "../images/hotspots/whale.svg";
|
|
||||||
import render_hotspot_icon from "../templates/hotspot_icon.hbs";
|
|
||||||
import render_hotspot_overlay from "../templates/hotspot_overlay.hbs";
|
|
||||||
|
|
||||||
import * as blueslip from "./blueslip";
|
|
||||||
import * as message_viewport from "./message_viewport";
|
|
||||||
import * as onboarding_steps from "./onboarding_steps";
|
|
||||||
import * as overlays from "./overlays";
|
|
||||||
import {current_user} from "./state_data";
|
|
||||||
import type {Hotspot, HotspotLocation, Placement, RawHotspot} from "./state_data";
|
|
||||||
|
|
||||||
// popover orientations
|
|
||||||
const TOP = "top";
|
|
||||||
const LEFT = "left";
|
|
||||||
const RIGHT = "right";
|
|
||||||
const BOTTOM = "bottom";
|
|
||||||
const LEFT_BOTTOM = "left_bottom";
|
|
||||||
const VIEWPORT_CENTER = "viewport_center";
|
|
||||||
|
|
||||||
// popover orientation can optionally be fixed here to override the
|
|
||||||
// defaults calculated by compute_placement.
|
|
||||||
const HOTSPOT_LOCATIONS = new Map<string, HotspotLocation>([
|
|
||||||
[
|
|
||||||
"intro_streams",
|
|
||||||
{
|
|
||||||
element: "#streams_header .left-sidebar-title .streams-tooltip-target",
|
|
||||||
offset_x: 1.3,
|
|
||||||
offset_y: 0.44,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"intro_topics",
|
|
||||||
{
|
|
||||||
element: ".topic-name",
|
|
||||||
offset_x: 1,
|
|
||||||
offset_y: 0.4,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"intro_gear",
|
|
||||||
{
|
|
||||||
element: "#personal-menu",
|
|
||||||
offset_x: 0.45,
|
|
||||||
offset_y: 1.15,
|
|
||||||
popover: LEFT_BOTTOM,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"intro_compose",
|
|
||||||
{
|
|
||||||
element: "#new_conversation_button",
|
|
||||||
offset_x: 0.5,
|
|
||||||
offset_y: -0.7,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
const meta: {
|
|
||||||
opened_hotspot_name: null | string;
|
|
||||||
} = {
|
|
||||||
opened_hotspot_name: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
function compute_placement(
|
|
||||||
$elt: JQuery,
|
|
||||||
popover_height: number,
|
|
||||||
popover_width: number,
|
|
||||||
prefer_vertical_positioning: boolean,
|
|
||||||
): Placement {
|
|
||||||
const client_rect = $elt.get(0)!.getBoundingClientRect();
|
|
||||||
const distance_from_top = client_rect.top;
|
|
||||||
const distance_from_bottom = message_viewport.height() - client_rect.bottom;
|
|
||||||
const distance_from_left = client_rect.left;
|
|
||||||
const distance_from_right = message_viewport.width() - client_rect.right;
|
|
||||||
|
|
||||||
const element_width = $elt.width()!;
|
|
||||||
const element_height = $elt.height()!;
|
|
||||||
|
|
||||||
const elt_will_fit_horizontally =
|
|
||||||
distance_from_left + element_width / 2 > popover_width / 2 &&
|
|
||||||
distance_from_right + element_width / 2 > popover_width / 2;
|
|
||||||
|
|
||||||
const elt_will_fit_vertically =
|
|
||||||
distance_from_bottom + element_height / 2 > popover_height / 2 &&
|
|
||||||
distance_from_top + element_height / 2 > popover_height / 2;
|
|
||||||
|
|
||||||
// default to placing the popover in the center of the screen
|
|
||||||
let placement: Placement = "viewport_center";
|
|
||||||
|
|
||||||
// prioritize left/right over top/bottom
|
|
||||||
if (distance_from_top > popover_height && elt_will_fit_horizontally) {
|
|
||||||
placement = "top";
|
|
||||||
}
|
|
||||||
if (distance_from_bottom > popover_height && elt_will_fit_horizontally) {
|
|
||||||
placement = "bottom";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prefer_vertical_positioning && placement !== "viewport_center") {
|
|
||||||
// If vertical positioning is preferred and the popover fits in
|
|
||||||
// either top or bottom position then return.
|
|
||||||
return placement;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (distance_from_left > popover_width && elt_will_fit_vertically) {
|
|
||||||
placement = "left";
|
|
||||||
}
|
|
||||||
if (distance_from_right > popover_width && elt_will_fit_vertically) {
|
|
||||||
placement = "right";
|
|
||||||
}
|
|
||||||
|
|
||||||
return placement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function post_hotspot_as_read(hotspot_name: string): void {
|
|
||||||
onboarding_steps.post_onboarding_step_as_read(hotspot_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function place_icon(hotspot: Hotspot): boolean {
|
|
||||||
const $element = $(hotspot.location.element);
|
|
||||||
const $icon = $(`#hotspot_${CSS.escape(hotspot.name)}_icon`);
|
|
||||||
|
|
||||||
if (
|
|
||||||
$element.length === 0 ||
|
|
||||||
$element.css("display") === "none" ||
|
|
||||||
!$element.is(":visible") ||
|
|
||||||
$element.is(":hidden")
|
|
||||||
) {
|
|
||||||
$icon.css("display", "none");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = {
|
|
||||||
top: $element.outerHeight()! * hotspot.location.offset_y,
|
|
||||||
left: $element.outerWidth()! * hotspot.location.offset_x,
|
|
||||||
};
|
|
||||||
const client_rect = $element.get(0)!.getBoundingClientRect();
|
|
||||||
const placement = {
|
|
||||||
top: client_rect.top + offset.top,
|
|
||||||
left: client_rect.left + offset.left,
|
|
||||||
};
|
|
||||||
$icon.css("display", "block");
|
|
||||||
$icon.css(placement);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function place_popover(hotspot: Hotspot): void {
|
|
||||||
const popover_width = $(
|
|
||||||
`#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`,
|
|
||||||
).outerWidth()!;
|
|
||||||
const popover_height = $(
|
|
||||||
`#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`,
|
|
||||||
).outerHeight()!;
|
|
||||||
const el_width = $(hotspot.location.element).outerWidth()!;
|
|
||||||
const el_height = $(hotspot.location.element).outerHeight()!;
|
|
||||||
|
|
||||||
const arrow_offset = 20;
|
|
||||||
|
|
||||||
let popover_offset;
|
|
||||||
let arrow_placement;
|
|
||||||
const orientation =
|
|
||||||
hotspot.location.popover ??
|
|
||||||
compute_placement($(hotspot.location.element), popover_height, popover_width, false);
|
|
||||||
|
|
||||||
switch (orientation) {
|
|
||||||
case TOP:
|
|
||||||
popover_offset = {
|
|
||||||
top: -(popover_height + arrow_offset),
|
|
||||||
left: el_width / 2 - popover_width / 2,
|
|
||||||
};
|
|
||||||
arrow_placement = "bottom";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LEFT:
|
|
||||||
popover_offset = {
|
|
||||||
top: el_height / 2 - popover_height / 2,
|
|
||||||
left: -(popover_width + arrow_offset),
|
|
||||||
};
|
|
||||||
arrow_placement = "right";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BOTTOM:
|
|
||||||
popover_offset = {
|
|
||||||
top: el_height + arrow_offset,
|
|
||||||
left: el_width / 2 - popover_width / 2,
|
|
||||||
};
|
|
||||||
arrow_placement = "top";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case RIGHT:
|
|
||||||
popover_offset = {
|
|
||||||
top: el_height / 2 - popover_height / 2,
|
|
||||||
left: el_width + arrow_offset,
|
|
||||||
};
|
|
||||||
arrow_placement = "left";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LEFT_BOTTOM:
|
|
||||||
popover_offset = {
|
|
||||||
top: 0,
|
|
||||||
left: -(popover_width + arrow_offset / 2),
|
|
||||||
};
|
|
||||||
arrow_placement = "";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case VIEWPORT_CENTER:
|
|
||||||
popover_offset = {
|
|
||||||
top: el_height / 2,
|
|
||||||
left: el_width / 2,
|
|
||||||
};
|
|
||||||
arrow_placement = "";
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
blueslip.error("Invalid popover placement value for hotspot", {name: hotspot.name});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// position arrow
|
|
||||||
arrow_placement = "arrow-" + arrow_placement;
|
|
||||||
$(`#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`)
|
|
||||||
.removeClass("arrow-top arrow-left arrow-bottom arrow-right")
|
|
||||||
.addClass(arrow_placement);
|
|
||||||
|
|
||||||
// position popover
|
|
||||||
let popover_placement;
|
|
||||||
if (orientation === VIEWPORT_CENTER) {
|
|
||||||
popover_placement = {
|
|
||||||
top: "45%",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const client_rect = $(hotspot.location.element).get(0)!.getBoundingClientRect();
|
|
||||||
popover_placement = {
|
|
||||||
top: client_rect.top + popover_offset.top,
|
|
||||||
left: client_rect.left + popover_offset.left,
|
|
||||||
transform: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
$(`#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`).css(popover_placement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function insert_hotspot_into_DOM(hotspot: Hotspot): void {
|
|
||||||
const hotspot_overlay_HTML = render_hotspot_overlay({
|
|
||||||
name: hotspot.name,
|
|
||||||
title: hotspot.title,
|
|
||||||
description: hotspot.description,
|
|
||||||
img: whale_image,
|
|
||||||
});
|
|
||||||
|
|
||||||
const hotspot_icon_HTML = render_hotspot_icon({
|
|
||||||
name: hotspot.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!hotspot.has_trigger) {
|
|
||||||
$("body").prepend($(hotspot_icon_HTML));
|
|
||||||
}
|
|
||||||
$("body").prepend($(hotspot_overlay_HTML));
|
|
||||||
if (hotspot.has_trigger || place_icon(hotspot)) {
|
|
||||||
place_popover(hotspot);
|
|
||||||
}
|
|
||||||
|
|
||||||
// reposition on any event that might update the UI
|
|
||||||
for (const event_name of ["resize", "scroll", "onkeydown", "click"]) {
|
|
||||||
window.addEventListener(
|
|
||||||
event_name,
|
|
||||||
_.debounce(() => {
|
|
||||||
if (hotspot.has_trigger || place_icon(hotspot)) {
|
|
||||||
place_popover(hotspot);
|
|
||||||
}
|
|
||||||
}, 10),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, hotspot.delay * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function is_open(): boolean {
|
|
||||||
return meta.opened_hotspot_name !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function is_hotspot_displayed(hotspot_name: string): number {
|
|
||||||
return $(`#hotspot_${hotspot_name}_overlay`).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function close_hotspot_icon($elem: JQuery): void {
|
|
||||||
$elem.animate(
|
|
||||||
{opacity: 0},
|
|
||||||
{
|
|
||||||
duration: 300,
|
|
||||||
done() {
|
|
||||||
$elem.css({display: "none"});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function close_read_hotspots(new_hotspots: RawHotspot[]): void {
|
|
||||||
const unwanted_hotspots = _.difference(
|
|
||||||
[...HOTSPOT_LOCATIONS.keys()],
|
|
||||||
new_hotspots.map((hotspot) => hotspot.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const hotspot_name of unwanted_hotspots) {
|
|
||||||
close_hotspot_icon($(`#hotspot_${CSS.escape(hotspot_name)}_icon`));
|
|
||||||
$(`#hotspot_${CSS.escape(hotspot_name)}_overlay`).remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function open_popover_if_hotspot_exist(
|
|
||||||
hotspot_name: string,
|
|
||||||
bind_element: HTMLElement,
|
|
||||||
): void {
|
|
||||||
const overlay_name = "hotspot_" + hotspot_name + "_overlay";
|
|
||||||
|
|
||||||
if (is_hotspot_displayed(hotspot_name)) {
|
|
||||||
overlays.open_overlay({
|
|
||||||
name: overlay_name,
|
|
||||||
$overlay: $(`#${CSS.escape(overlay_name)}`),
|
|
||||||
on_close: function (this: HTMLElement) {
|
|
||||||
// close popover
|
|
||||||
$(this).css({display: "block"});
|
|
||||||
$(this).animate(
|
|
||||||
{opacity: 1},
|
|
||||||
{
|
|
||||||
duration: 300,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}.bind(bind_element),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function load_new(new_hotspots: RawHotspot[]): void {
|
|
||||||
close_read_hotspots(new_hotspots);
|
|
||||||
|
|
||||||
let hotspot_with_location: Hotspot;
|
|
||||||
for (const hotspot of new_hotspots) {
|
|
||||||
hotspot_with_location = {
|
|
||||||
...hotspot,
|
|
||||||
location: HOTSPOT_LOCATIONS.get(hotspot.name)!,
|
|
||||||
};
|
|
||||||
if (!is_hotspot_displayed(hotspot.name)) {
|
|
||||||
insert_hotspot_into_DOM(hotspot_with_location);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initialize(): void {
|
|
||||||
load_new(onboarding_steps.filter_new_hotspots(current_user.onboarding_steps));
|
|
||||||
|
|
||||||
// open
|
|
||||||
$("body").on("click", ".hotspot-icon", function (this: HTMLElement, e) {
|
|
||||||
// hide icon
|
|
||||||
close_hotspot_icon($(this));
|
|
||||||
|
|
||||||
// show popover
|
|
||||||
const match_array = /^hotspot_(.*)_icon$/.exec(
|
|
||||||
$(e.target).closest(".hotspot-icon").attr("id")!,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(match_array !== null);
|
|
||||||
const [, hotspot_name] = match_array;
|
|
||||||
open_popover_if_hotspot_exist(hotspot_name, this);
|
|
||||||
|
|
||||||
meta.opened_hotspot_name = hotspot_name;
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
// confirm
|
|
||||||
$("body").on("click", ".hotspot.overlay .hotspot-confirm", function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const overlay_name = $(this).closest(".hotspot.overlay").attr("id")!;
|
|
||||||
|
|
||||||
const match_array = /^hotspot_(.*)_overlay$/.exec(overlay_name);
|
|
||||||
assert(match_array !== null);
|
|
||||||
const [, hotspot_name] = match_array;
|
|
||||||
|
|
||||||
// Comment below to disable marking hotspots as read in production
|
|
||||||
post_hotspot_as_read(hotspot_name);
|
|
||||||
|
|
||||||
overlays.close_overlay(overlay_name);
|
|
||||||
$(`#hotspot_${CSS.escape(hotspot_name)}_icon`).remove();
|
|
||||||
|
|
||||||
// We are removing the hotspot overlay after it's read as it will help us avoid
|
|
||||||
// multiple copies of the hotspots when ALWAYS_SEND_ALL_HOTSPOTS is set to true.
|
|
||||||
$(`#${overlay_name}`).remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
// stop propagation
|
|
||||||
$("body").on("click", ".hotspot.overlay .hotspot-popover", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as blueslip from "./blueslip";
|
import * as blueslip from "./blueslip";
|
||||||
import * as channel from "./channel";
|
import * as channel from "./channel";
|
||||||
import {current_user} from "./state_data";
|
import {current_user} from "./state_data";
|
||||||
import type {OnboardingStep, RawHotspot} from "./state_data";
|
import type {OnboardingStep} from "./state_data";
|
||||||
|
|
||||||
export const ONE_TIME_NOTICES_TO_DISPLAY = new Set<string>();
|
export const ONE_TIME_NOTICES_TO_DISPLAY = new Set<string>();
|
||||||
|
|
||||||
|
@ -21,12 +21,6 @@ export function post_onboarding_step_as_read(onboarding_step_name: string): void
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filter_new_hotspots(onboarding_steps: OnboardingStep[]): RawHotspot[] {
|
|
||||||
return onboarding_steps.flatMap((onboarding_step) =>
|
|
||||||
onboarding_step.type === "hotspot" ? [onboarding_step] : [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function update_notice_to_display(onboarding_steps: OnboardingStep[]): void {
|
export function update_notice_to_display(onboarding_steps: OnboardingStep[]): void {
|
||||||
ONE_TIME_NOTICES_TO_DISPLAY.clear();
|
ONE_TIME_NOTICES_TO_DISPLAY.clear();
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ import * as emoji from "./emoji";
|
||||||
import * as emoji_picker from "./emoji_picker";
|
import * as emoji_picker from "./emoji_picker";
|
||||||
import * as gear_menu from "./gear_menu";
|
import * as gear_menu from "./gear_menu";
|
||||||
import * as giphy from "./giphy";
|
import * as giphy from "./giphy";
|
||||||
import * as hotspots from "./hotspots";
|
|
||||||
import * as information_density from "./information_density";
|
import * as information_density from "./information_density";
|
||||||
import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area";
|
import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area";
|
||||||
import * as linkifiers from "./linkifiers";
|
import * as linkifiers from "./linkifiers";
|
||||||
|
@ -147,7 +146,6 @@ export function dispatch_normal_event(event) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "onboarding_steps":
|
case "onboarding_steps":
|
||||||
hotspots.load_new(onboarding_steps.filter_new_hotspots(event.onboarding_steps));
|
|
||||||
onboarding_steps.update_notice_to_display(event.onboarding_steps);
|
onboarding_steps.update_notice_to_display(event.onboarding_steps);
|
||||||
current_user.onboarding_steps = current_user.onboarding_steps
|
current_user.onboarding_steps = current_user.onboarding_steps
|
||||||
? [...current_user.onboarding_steps, ...event.onboarding_steps]
|
? [...current_user.onboarding_steps, ...event.onboarding_steps]
|
||||||
|
|
|
@ -21,49 +21,17 @@ export const narrow_term_schema = z.object({
|
||||||
export type NarrowTerm = z.output<typeof narrow_term_schema>;
|
export type NarrowTerm = z.output<typeof narrow_term_schema>;
|
||||||
// Sync this with zerver.lib.events.do_events_register.
|
// Sync this with zerver.lib.events.do_events_register.
|
||||||
|
|
||||||
const placement_schema = z.enum([
|
|
||||||
"top",
|
|
||||||
"left",
|
|
||||||
"right",
|
|
||||||
"bottom",
|
|
||||||
"left_bottom",
|
|
||||||
"viewport_center",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export type Placement = z.infer<typeof placement_schema>;
|
|
||||||
|
|
||||||
const hotspot_location_schema = z.object({
|
|
||||||
element: z.string(),
|
|
||||||
offset_x: z.number(),
|
|
||||||
offset_y: z.number(),
|
|
||||||
popover: z.optional(placement_schema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type HotspotLocation = z.output<typeof hotspot_location_schema>;
|
|
||||||
|
|
||||||
const raw_hotspot_schema = z.object({
|
|
||||||
delay: z.number(),
|
|
||||||
description: z.string(),
|
|
||||||
has_trigger: z.boolean(),
|
|
||||||
name: z.string(),
|
|
||||||
title: z.string(),
|
|
||||||
type: z.literal("hotspot"),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type RawHotspot = z.output<typeof raw_hotspot_schema>;
|
|
||||||
|
|
||||||
const hotspot_schema = raw_hotspot_schema.extend({
|
|
||||||
location: hotspot_location_schema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Hotspot = z.output<typeof hotspot_schema>;
|
|
||||||
|
|
||||||
const one_time_notice_schema = z.object({
|
const one_time_notice_schema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
type: z.literal("one_time_notice"),
|
type: z.literal("one_time_notice"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onboarding_step_schema = z.union([one_time_notice_schema, raw_hotspot_schema]);
|
/* We may introduce onboarding step of types other than 'one time notice'
|
||||||
|
in future. Earlier, we had 'hotspot' and 'one time notice' as the two
|
||||||
|
types. We can simply do:
|
||||||
|
const onboarding_step_schema = z.union([one_time_notice_schema, other_type_schema]);
|
||||||
|
to avoid major refactoring when new type is introduced in the future. */
|
||||||
|
const onboarding_step_schema = one_time_notice_schema;
|
||||||
|
|
||||||
export type OnboardingStep = z.output<typeof onboarding_step_schema>;
|
export type OnboardingStep = z.output<typeof onboarding_step_schema>;
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,6 @@ import * as gear_menu from "./gear_menu";
|
||||||
import * as giphy from "./giphy";
|
import * as giphy from "./giphy";
|
||||||
import * as hashchange from "./hashchange";
|
import * as hashchange from "./hashchange";
|
||||||
import * as hotkey from "./hotkey";
|
import * as hotkey from "./hotkey";
|
||||||
import * as hotspots from "./hotspots";
|
|
||||||
import * as i18n from "./i18n";
|
import * as i18n from "./i18n";
|
||||||
import * as inbox_ui from "./inbox_ui";
|
import * as inbox_ui from "./inbox_ui";
|
||||||
import * as information_density from "./information_density";
|
import * as information_density from "./information_density";
|
||||||
|
@ -877,7 +876,6 @@ export function initialize_everything(state_data) {
|
||||||
drafts.initialize_ui();
|
drafts.initialize_ui();
|
||||||
drafts_overlay_ui.initialize();
|
drafts_overlay_ui.initialize();
|
||||||
onboarding_steps.initialize();
|
onboarding_steps.initialize();
|
||||||
hotspots.initialize();
|
|
||||||
typing.initialize();
|
typing.initialize();
|
||||||
starred_messages_ui.initialize();
|
starred_messages_ui.initialize();
|
||||||
user_status_ui.initialize();
|
user_status_ui.initialize();
|
||||||
|
|
|
@ -923,14 +923,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Popover: */
|
|
||||||
.hotspot.overlay .hotspot-popover {
|
|
||||||
border-color: hsl(0deg 0% 0% / 20%) !important;
|
|
||||||
/* Based on the `.hotspot-popover` shadow in `hotspots.css`, but with a new
|
|
||||||
color. */
|
|
||||||
box-shadow: 0 5px 10px hsl(0deg 0% 0% / 40%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#user-profile-modal {
|
#user-profile-modal {
|
||||||
#default-section {
|
#default-section {
|
||||||
.default-field {
|
.default-field {
|
||||||
|
@ -962,47 +954,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Arrows: */
|
|
||||||
.hotspot.overlay {
|
|
||||||
.hotspot-popover.arrow-right::before {
|
|
||||||
border-left-color: hsl(0deg 0% 0% / 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-popover.arrow-right::after {
|
|
||||||
border-left-color: hsl(212deg 28% 18%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-popover.arrow-bottom::before {
|
|
||||||
border-top-color: hsl(0deg 0% 0% / 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-popover.arrow-bottom::after {
|
|
||||||
border-top-color: hsl(212deg 28% 18%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-popover.arrow-left::before {
|
|
||||||
border-right-color: hsl(0deg 0% 0% / 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-popover.arrow-left::after {
|
|
||||||
border-right-color: hsl(212deg 28% 18%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-popover.arrow-top::before {
|
|
||||||
border-bottom-color: hsl(0deg 0% 0% / 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-popover.arrow-top::after {
|
|
||||||
border-bottom-color: hsl(212deg 28% 18%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content: */
|
|
||||||
.hotspot.overlay .hotspot-popover .hotspot-popover-content,
|
|
||||||
.hotspot.overlay .hotspot-popover .hotspot-popover-bottom {
|
|
||||||
background-color: var(--color-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-messages-logo {
|
.top-messages-logo {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,245 +0,0 @@
|
||||||
/* icon */
|
|
||||||
.hotspot-icon {
|
|
||||||
position: fixed;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 100;
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
width: 25px;
|
|
||||||
height: 25px;
|
|
||||||
margin: -12.5px 0 0 -12.5px;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
background-color: hsl(196deg 100% 82% / 30%);
|
|
||||||
border: 2px solid var(--color-outline-focus);
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulse {
|
|
||||||
width: 25px;
|
|
||||||
height: 25px;
|
|
||||||
margin: -11.5px 0 0 -11.5px;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
background-color: hsl(0deg 0% 100%);
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid hsl(205deg 100% 70%);
|
|
||||||
transform: scale(2.2);
|
|
||||||
opacity: 0;
|
|
||||||
animation: pulsate 5s ease-out 0.375s 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bounce {
|
|
||||||
animation: bounce 5s 5;
|
|
||||||
|
|
||||||
.bounce-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: -5px;
|
|
||||||
bottom: 3px;
|
|
||||||
transform: rotate(7deg);
|
|
||||||
color: var(--color-outline-focus);
|
|
||||||
font-size: 2.75em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulsate {
|
|
||||||
0% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
30%,
|
|
||||||
100% {
|
|
||||||
transform: scale(2.2);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce {
|
|
||||||
0%,
|
|
||||||
15%,
|
|
||||||
100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
7.5% {
|
|
||||||
transform: translateY(4px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* popover */
|
|
||||||
.hotspot.overlay {
|
|
||||||
z-index: 104;
|
|
||||||
background-color: hsl(191deg 7% 20% / 15%);
|
|
||||||
|
|
||||||
.hotspot-popover {
|
|
||||||
position: fixed;
|
|
||||||
width: 250px;
|
|
||||||
text-align: left;
|
|
||||||
box-shadow: 0 5px 10px hsl(223deg 4% 54% / 20%);
|
|
||||||
border: 1px solid hsl(0deg 0% 80%);
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
/* arrows */
|
|
||||||
&::after,
|
|
||||||
&::before {
|
|
||||||
border: solid transparent;
|
|
||||||
content: "";
|
|
||||||
height: 0;
|
|
||||||
width: 0;
|
|
||||||
position: absolute;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-width: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
border-width: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.arrow-top {
|
|
||||||
&::before,
|
|
||||||
&::after {
|
|
||||||
bottom: 100%;
|
|
||||||
right: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-bottom-color: hsl(164deg 44% 47%);
|
|
||||||
margin-right: -12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
border-bottom-color: hsl(0deg 0% 80%);
|
|
||||||
margin-right: -13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.arrow-left {
|
|
||||||
&::before,
|
|
||||||
&::after {
|
|
||||||
right: 100%;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-right-color: hsl(0deg 0% 100%);
|
|
||||||
margin-top: -12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
border-right-color: hsl(0deg 0% 80%);
|
|
||||||
margin-top: -13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.arrow-bottom {
|
|
||||||
&::before,
|
|
||||||
&::after {
|
|
||||||
top: 100%;
|
|
||||||
right: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-top-color: hsl(0deg 0% 100%);
|
|
||||||
margin-right: -12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
border-top-color: hsl(0deg 0% 80%);
|
|
||||||
margin-right: -13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.arrow-right {
|
|
||||||
&::before,
|
|
||||||
&::after {
|
|
||||||
left: 100%;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-left-color: hsl(0deg 0% 100%);
|
|
||||||
margin-top: -12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
border-left-color: hsl(0deg 0% 80%);
|
|
||||||
margin-top: -13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-popover-top {
|
|
||||||
padding: 0 15px;
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
background-color: hsl(164deg 44% 47%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.15em;
|
|
||||||
font-weight: 600;
|
|
||||||
color: hsl(0deg 0% 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-popover-content {
|
|
||||||
background-color: hsl(0deg 0% 100%);
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-popover-bottom {
|
|
||||||
background-color: hsl(0deg 0% 100%);
|
|
||||||
height: 90px;
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-img {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-confirm {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 15px;
|
|
||||||
right: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-img {
|
|
||||||
height: 83px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotspot-confirm {
|
|
||||||
max-width: 125px;
|
|
||||||
max-height: 70px;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.15em;
|
|
||||||
font-weight: 600;
|
|
||||||
color: hsl(0deg 0% 100%);
|
|
||||||
background-color: hsl(164deg 44% 47%);
|
|
||||||
border-radius: 4px;
|
|
||||||
white-space: normal;
|
|
||||||
padding: 7px 20px;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: hsl(164deg 44% 56%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* individual icon z-indexing */
|
|
||||||
#hotspot_intro_streams_icon,
|
|
||||||
#hotspot_intro_topics_icon,
|
|
||||||
#hotspot_intro_gear_icon {
|
|
||||||
z-index: 103;
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
<div class="hotspot-icon" id="hotspot_{{name}}_icon">
|
|
||||||
<span class="dot"></span>
|
|
||||||
<span class="pulse"></span>
|
|
||||||
<div class="bounce"><span class="bounce-icon">?</span></div>
|
|
||||||
</div>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<div id="hotspot_{{name}}_overlay" class="hotspot overlay" data-overlay="hotspot_{{name}}_overlay">
|
|
||||||
<div class="hotspot-popover">
|
|
||||||
<div class="hotspot-popover-top">
|
|
||||||
<h1 class="hotspot-title">{{title}}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="hotspot-popover-content">
|
|
||||||
<p class="hotspot-description">{{description}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="hotspot-popover-bottom">
|
|
||||||
<img class="hotspot-img" src="{{img}}" />
|
|
||||||
<button class="hotspot-confirm">{{t 'Got it!' }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -32,7 +32,6 @@ const compose_pm_pill = mock_esm("../src/compose_pm_pill");
|
||||||
const dark_theme = mock_esm("../src/dark_theme");
|
const dark_theme = mock_esm("../src/dark_theme");
|
||||||
const emoji_picker = mock_esm("../src/emoji_picker");
|
const emoji_picker = mock_esm("../src/emoji_picker");
|
||||||
const gear_menu = mock_esm("../src/gear_menu");
|
const gear_menu = mock_esm("../src/gear_menu");
|
||||||
const hotspots = mock_esm("../src/hotspots");
|
|
||||||
const information_density = mock_esm("../src/information_density");
|
const information_density = mock_esm("../src/information_density");
|
||||||
const linkifiers = mock_esm("../src/linkifiers");
|
const linkifiers = mock_esm("../src/linkifiers");
|
||||||
const message_events = mock_esm("../src/message_events");
|
const message_events = mock_esm("../src/message_events");
|
||||||
|
@ -320,10 +319,9 @@ run_test("default_streams", ({override}) => {
|
||||||
assert_same(args.realm_default_streams, event.default_streams);
|
assert_same(args.realm_default_streams, event.default_streams);
|
||||||
});
|
});
|
||||||
|
|
||||||
run_test("onboarding_steps", ({override}) => {
|
run_test("onboarding_steps", () => {
|
||||||
current_user.onboarding_steps = [];
|
current_user.onboarding_steps = [];
|
||||||
const event = event_fixtures.onboarding_steps;
|
const event = event_fixtures.onboarding_steps;
|
||||||
override(hotspots, "load_new", noop);
|
|
||||||
dispatch(event);
|
dispatch(event);
|
||||||
assert_same(current_user.onboarding_steps, event.onboarding_steps);
|
assert_same(current_user.onboarding_steps, event.onboarding_steps);
|
||||||
});
|
});
|
||||||
|
|
|
@ -88,10 +88,6 @@ const settings_data = mock_esm("../src/settings_data");
|
||||||
const stream_list = mock_esm("../src/stream_list");
|
const stream_list = mock_esm("../src/stream_list");
|
||||||
const stream_settings_ui = mock_esm("../src/stream_settings_ui");
|
const stream_settings_ui = mock_esm("../src/stream_settings_ui");
|
||||||
|
|
||||||
mock_esm("../src/hotspots", {
|
|
||||||
is_open: () => false,
|
|
||||||
});
|
|
||||||
|
|
||||||
mock_esm("../src/recent_view_ui", {
|
mock_esm("../src/recent_view_ui", {
|
||||||
is_in_focus: () => false,
|
is_in_focus: () => false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -195,20 +195,12 @@ exports.fixtures = {
|
||||||
type: "onboarding_steps",
|
type: "onboarding_steps",
|
||||||
onboarding_steps: [
|
onboarding_steps: [
|
||||||
{
|
{
|
||||||
type: "hotspot",
|
type: "one_time_notice",
|
||||||
name: "topics",
|
name: "intro_inbox_view_modal",
|
||||||
title: "About topics",
|
|
||||||
description: "Topics are good.",
|
|
||||||
delay: 1.5,
|
|
||||||
has_trigger: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "hotspot",
|
type: "one_time_notice",
|
||||||
name: "compose",
|
name: "intro_recent_view_modal",
|
||||||
title: "Compose box",
|
|
||||||
description: "This is where you compose messages.",
|
|
||||||
delay: 3.14159,
|
|
||||||
has_trigger: false,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -338,13 +338,7 @@ _onboarding_steps = DictType(
|
||||||
required_keys=[
|
required_keys=[
|
||||||
("type", str),
|
("type", str),
|
||||||
("name", str),
|
("name", str),
|
||||||
],
|
]
|
||||||
optional_keys=[
|
|
||||||
("title", str),
|
|
||||||
("description", str),
|
|
||||||
("delay", NumberType()),
|
|
||||||
("has_trigger", bool),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
onboarding_steps_event = event_dict_type(
|
onboarding_steps_event = event_dict_type(
|
||||||
|
|
|
@ -1,73 +1,13 @@
|
||||||
# See https://zulip.readthedocs.io/en/latest/subsystems/hotspots.html
|
# See https://zulip.readthedocs.io/en/latest/subsystems/hotspots.html
|
||||||
# for documentation on this subsystem.
|
# for documentation on this subsystem.
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext_lazy
|
|
||||||
from django_stubs_ext import StrPromise
|
|
||||||
|
|
||||||
from zerver.models import OnboardingStep, UserProfile
|
from zerver.models import OnboardingStep, UserProfile
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Hotspot:
|
|
||||||
name: str
|
|
||||||
title: Optional[StrPromise]
|
|
||||||
description: Optional[StrPromise]
|
|
||||||
has_trigger: bool = False
|
|
||||||
|
|
||||||
def to_dict(self, delay: float = 0) -> Dict[str, Union[str, float, bool]]:
|
|
||||||
return {
|
|
||||||
"type": "hotspot",
|
|
||||||
"name": self.name,
|
|
||||||
"title": str(self.title),
|
|
||||||
"description": str(self.description),
|
|
||||||
"delay": delay,
|
|
||||||
"has_trigger": self.has_trigger,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
INTRO_HOTSPOTS: List[Hotspot] = [
|
|
||||||
Hotspot(
|
|
||||||
name="intro_streams",
|
|
||||||
title=gettext_lazy("Catch up on a channel"),
|
|
||||||
description=gettext_lazy(
|
|
||||||
"Messages sent to a channel are seen by everyone subscribed "
|
|
||||||
"to that channel. Try clicking on one of the channel links below."
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Hotspot(
|
|
||||||
name="intro_topics",
|
|
||||||
title=gettext_lazy("Topics"),
|
|
||||||
description=gettext_lazy(
|
|
||||||
"Every message has a topic. Topics keep conversations "
|
|
||||||
"easy to follow, and make it easy to reply to conversations that start "
|
|
||||||
"while you are offline."
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Hotspot(
|
|
||||||
# In theory, this should be renamed to intro_personal, since
|
|
||||||
# it's no longer attached to the gear menu, but renaming these
|
|
||||||
# requires a migration that is not worth doing at this time.
|
|
||||||
name="intro_gear",
|
|
||||||
title=gettext_lazy("Settings"),
|
|
||||||
description=gettext_lazy("Go to Settings to configure your notifications and preferences."),
|
|
||||||
),
|
|
||||||
Hotspot(
|
|
||||||
name="intro_compose",
|
|
||||||
title=gettext_lazy("Compose"),
|
|
||||||
description=gettext_lazy(
|
|
||||||
"Click here to start a new conversation. Pick a topic "
|
|
||||||
"(2-3 words is best), and give it a go!"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
NON_INTRO_HOTSPOTS: List[Hotspot] = []
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class OneTimeNotice:
|
class OneTimeNotice:
|
||||||
name: str
|
name: str
|
||||||
|
@ -94,24 +34,17 @@ ONE_TIME_NOTICES: List[OneTimeNotice] = [
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# We would most likely implement new hotspots in the future that aren't
|
# We may introduce onboarding step of types other than 'one time notice'
|
||||||
# a part of the initial tutorial. To that end, classifying them into
|
# in future. Earlier, we had 'hotspot' and 'one time notice' as the two
|
||||||
# categories which are aggregated in ALL_HOTSPOTS, seems like a good start.
|
# types. We can simply do:
|
||||||
ALL_HOTSPOTS = [*INTRO_HOTSPOTS, *NON_INTRO_HOTSPOTS]
|
# ALL_ONBOARDING_STEPS: List[Union[OneTimeNotice, OtherType]]
|
||||||
ALL_ONBOARDING_STEPS: List[Union[Hotspot, OneTimeNotice]] = [*ALL_HOTSPOTS, *ONE_TIME_NOTICES]
|
# to avoid API changes when new type is introduced in the future.
|
||||||
|
ALL_ONBOARDING_STEPS: List[OneTimeNotice] = ONE_TIME_NOTICES
|
||||||
|
|
||||||
|
|
||||||
def get_next_onboarding_steps(user: UserProfile) -> List[Dict[str, Any]]:
|
def get_next_onboarding_steps(user: UserProfile) -> List[Dict[str, Any]]:
|
||||||
# For manual testing, it can be convenient to set
|
# If a Zulip server has disabled the tutorial, never send any
|
||||||
# ALWAYS_SEND_ALL_HOTSPOTS=True in `zproject/dev_settings.py` to
|
# onboarding steps.
|
||||||
# make it easy to click on all of the hotspots.
|
|
||||||
#
|
|
||||||
# Since this is just for development purposes, it's convenient for us to send
|
|
||||||
# all the hotspots rather than any specific category.
|
|
||||||
if settings.ALWAYS_SEND_ALL_HOTSPOTS:
|
|
||||||
return [hotspot.to_dict() for hotspot in ALL_HOTSPOTS]
|
|
||||||
|
|
||||||
# If a Zulip server has disabled the tutorial, never send hotspots.
|
|
||||||
if not settings.TUTORIAL_ENABLED:
|
if not settings.TUTORIAL_ENABLED:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -119,25 +52,12 @@ def get_next_onboarding_steps(user: UserProfile) -> List[Dict[str, Any]]:
|
||||||
OnboardingStep.objects.filter(user=user).values_list("onboarding_step", flat=True)
|
OnboardingStep.objects.filter(user=user).values_list("onboarding_step", flat=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
onboarding_steps: List[Dict[str, Any]] = [hotspot.to_dict() for hotspot in NON_INTRO_HOTSPOTS]
|
onboarding_steps: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
for one_time_notice in ONE_TIME_NOTICES:
|
for one_time_notice in ONE_TIME_NOTICES:
|
||||||
if one_time_notice.name in seen_onboarding_steps:
|
if one_time_notice.name in seen_onboarding_steps:
|
||||||
continue
|
continue
|
||||||
onboarding_steps.append(one_time_notice.to_dict())
|
onboarding_steps.append(one_time_notice.to_dict())
|
||||||
|
|
||||||
if user.tutorial_status == UserProfile.TUTORIAL_FINISHED:
|
|
||||||
return onboarding_steps
|
|
||||||
|
|
||||||
for hotspot in INTRO_HOTSPOTS:
|
|
||||||
if hotspot.name in seen_onboarding_steps:
|
|
||||||
continue
|
|
||||||
|
|
||||||
onboarding_steps.append(hotspot.to_dict(delay=0.5))
|
|
||||||
return onboarding_steps
|
|
||||||
|
|
||||||
user.tutorial_status = UserProfile.TUTORIAL_FINISHED
|
|
||||||
user.save(update_fields=["tutorial_status"])
|
|
||||||
return onboarding_steps
|
return onboarding_steps
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2325,14 +2325,6 @@ paths:
|
||||||
"type": "onboarding_steps",
|
"type": "onboarding_steps",
|
||||||
"onboarding_steps":
|
"onboarding_steps":
|
||||||
[
|
[
|
||||||
{
|
|
||||||
"type": "hotspot",
|
|
||||||
"name": "intro_compose",
|
|
||||||
"title": "Compose",
|
|
||||||
"description": "Click here to start a new conversation. Pick a topic (2-3 words is best), and give it a go!",
|
|
||||||
"delay": 0.5,
|
|
||||||
"has_trigger": false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "one_time_notice",
|
"type": "one_time_notice",
|
||||||
"name": "visibility_policy_banner",
|
"name": "visibility_policy_banner",
|
||||||
|
@ -19854,43 +19846,15 @@ components:
|
||||||
type:
|
type:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
The type of the onboarding step. Valid values are either
|
The type of the onboarding step. Valid value is `"one_time_notice"`.
|
||||||
`"hotspot"` or `"one_time_notice"`.
|
|
||||||
|
|
||||||
**Changes**: New in Zulip 8.0 (feature level 233).
|
**Changes**: Removed type `"hotspot"` in Zulip 9.0 (feature level 259).
|
||||||
|
|
||||||
|
New in Zulip 8.0 (feature level 233).
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
The name of the onboarding step.
|
The name of the onboarding step.
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
The title of the onboarding step, as displayed to the user.
|
|
||||||
|
|
||||||
Only present for onboarding steps with type `"hotspot"`.
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
The description of the onboarding step, as displayed to the
|
|
||||||
user.
|
|
||||||
|
|
||||||
Only present for onboarding steps with type `"hotspot"`.
|
|
||||||
delay:
|
|
||||||
type: number
|
|
||||||
description: |
|
|
||||||
The delay, in seconds, after which the user should be shown
|
|
||||||
the onboarding step.
|
|
||||||
|
|
||||||
Only present for onboarding steps with type `"hotspot"`.
|
|
||||||
has_trigger:
|
|
||||||
type: boolean
|
|
||||||
description: |
|
|
||||||
Identifies if the onboarding step will activate only when some
|
|
||||||
specific event occurs.
|
|
||||||
|
|
||||||
Only present for onboarding steps with type `"hotspot"`.
|
|
||||||
|
|
||||||
**Changes**: New in Zulip 8.0 (feature level 230).
|
|
||||||
RealmAuthenticationMethod:
|
RealmAuthenticationMethod:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
|
@ -2930,11 +2930,8 @@ class NormalActionsTest(BaseAction):
|
||||||
check_realm_deactivated("events[0]", events[0])
|
check_realm_deactivated("events[0]", events[0])
|
||||||
|
|
||||||
def test_do_mark_onboarding_step_as_read(self) -> None:
|
def test_do_mark_onboarding_step_as_read(self) -> None:
|
||||||
self.user_profile.tutorial_status = UserProfile.TUTORIAL_WAITING
|
|
||||||
self.user_profile.save(update_fields=["tutorial_status"])
|
|
||||||
|
|
||||||
with self.verify_action() as events:
|
with self.verify_action() as events:
|
||||||
do_mark_onboarding_step_as_read(self.user_profile, "intro_streams")
|
do_mark_onboarding_step_as_read(self.user_profile, "intro_inbox_view_modal")
|
||||||
check_onboarding_steps("events[0]", events[0])
|
check_onboarding_steps("events[0]", events[0])
|
||||||
|
|
||||||
def test_rename_stream(self) -> None:
|
def test_rename_stream(self) -> None:
|
||||||
|
|
|
@ -2,15 +2,9 @@ from typing_extensions import override
|
||||||
|
|
||||||
from zerver.actions.create_user import do_create_user
|
from zerver.actions.create_user import do_create_user
|
||||||
from zerver.actions.hotspots import do_mark_onboarding_step_as_read
|
from zerver.actions.hotspots import do_mark_onboarding_step_as_read
|
||||||
from zerver.lib.hotspots import (
|
from zerver.lib.hotspots import ONE_TIME_NOTICES, get_next_onboarding_steps
|
||||||
ALL_HOTSPOTS,
|
|
||||||
INTRO_HOTSPOTS,
|
|
||||||
NON_INTRO_HOTSPOTS,
|
|
||||||
ONE_TIME_NOTICES,
|
|
||||||
get_next_onboarding_steps,
|
|
||||||
)
|
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.models import OnboardingStep, UserProfile
|
from zerver.models import OnboardingStep
|
||||||
from zerver.models.realms import get_realm
|
from zerver.models.realms import get_realm
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,77 +18,50 @@ class TestGetNextOnboardingSteps(ZulipTestCase):
|
||||||
"user@zulip.com", "password", get_realm("zulip"), "user", acting_user=None
|
"user@zulip.com", "password", get_realm("zulip"), "user", acting_user=None
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_first_hotspot(self) -> None:
|
|
||||||
for hotspot in NON_INTRO_HOTSPOTS: # nocoverage
|
|
||||||
do_mark_onboarding_step_as_read(self.user, hotspot.name)
|
|
||||||
|
|
||||||
for one_time_notice in ONE_TIME_NOTICES: # nocoverage
|
|
||||||
do_mark_onboarding_step_as_read(self.user, one_time_notice.name)
|
|
||||||
|
|
||||||
hotspots = get_next_onboarding_steps(self.user)
|
|
||||||
self.assert_length(hotspots, 1)
|
|
||||||
self.assertEqual(hotspots[0]["name"], "intro_streams")
|
|
||||||
|
|
||||||
def test_some_done_some_not(self) -> None:
|
def test_some_done_some_not(self) -> None:
|
||||||
do_mark_onboarding_step_as_read(self.user, "intro_streams")
|
do_mark_onboarding_step_as_read(self.user, "visibility_policy_banner")
|
||||||
do_mark_onboarding_step_as_read(self.user, "intro_compose")
|
do_mark_onboarding_step_as_read(self.user, "intro_inbox_view_modal")
|
||||||
onboarding_steps = get_next_onboarding_steps(self.user)
|
onboarding_steps = get_next_onboarding_steps(self.user)
|
||||||
self.assert_length(onboarding_steps, 5)
|
self.assert_length(onboarding_steps, 2)
|
||||||
self.assertEqual(onboarding_steps[0]["name"], "visibility_policy_banner")
|
self.assertEqual(onboarding_steps[0]["name"], "intro_recent_view_modal")
|
||||||
self.assertEqual(onboarding_steps[1]["name"], "intro_inbox_view_modal")
|
self.assertEqual(onboarding_steps[1]["name"], "first_stream_created_banner")
|
||||||
self.assertEqual(onboarding_steps[2]["name"], "intro_recent_view_modal")
|
|
||||||
self.assertEqual(onboarding_steps[3]["name"], "first_stream_created_banner")
|
with self.settings(TUTORIAL_ENABLED=False):
|
||||||
self.assertEqual(onboarding_steps[4]["name"], "intro_topics")
|
onboarding_steps = get_next_onboarding_steps(self.user)
|
||||||
|
self.assert_length(onboarding_steps, 0)
|
||||||
|
|
||||||
def test_all_onboarding_steps_done(self) -> None:
|
def test_all_onboarding_steps_done(self) -> None:
|
||||||
with self.settings(TUTORIAL_ENABLED=True):
|
self.assertNotEqual(get_next_onboarding_steps(self.user), [])
|
||||||
self.assertNotEqual(self.user.tutorial_status, UserProfile.TUTORIAL_FINISHED)
|
|
||||||
for hotspot in NON_INTRO_HOTSPOTS: # nocoverage
|
|
||||||
do_mark_onboarding_step_as_read(self.user, hotspot.name)
|
|
||||||
|
|
||||||
self.assertNotEqual(self.user.tutorial_status, UserProfile.TUTORIAL_FINISHED)
|
|
||||||
for one_time_notice in ONE_TIME_NOTICES: # nocoverage
|
for one_time_notice in ONE_TIME_NOTICES: # nocoverage
|
||||||
do_mark_onboarding_step_as_read(self.user, one_time_notice.name)
|
do_mark_onboarding_step_as_read(self.user, one_time_notice.name)
|
||||||
|
|
||||||
self.assertNotEqual(self.user.tutorial_status, UserProfile.TUTORIAL_FINISHED)
|
|
||||||
for hotspot in INTRO_HOTSPOTS:
|
|
||||||
do_mark_onboarding_step_as_read(self.user, hotspot.name)
|
|
||||||
|
|
||||||
self.assertEqual(self.user.tutorial_status, UserProfile.TUTORIAL_FINISHED)
|
|
||||||
self.assertEqual(get_next_onboarding_steps(self.user), [])
|
|
||||||
|
|
||||||
def test_send_all_hotspots(self) -> None:
|
|
||||||
with self.settings(DEVELOPMENT=True, ALWAYS_SEND_ALL_HOTSPOTS=True):
|
|
||||||
self.assert_length(ALL_HOTSPOTS, len(get_next_onboarding_steps(self.user)))
|
|
||||||
|
|
||||||
def test_tutorial_disabled(self) -> None:
|
|
||||||
with self.settings(TUTORIAL_ENABLED=False):
|
|
||||||
self.assertEqual(get_next_onboarding_steps(self.user), [])
|
self.assertEqual(get_next_onboarding_steps(self.user), [])
|
||||||
|
|
||||||
|
|
||||||
class TestOnboardingSteps(ZulipTestCase):
|
class TestOnboardingSteps(ZulipTestCase):
|
||||||
def test_do_mark_onboarding_step_as_read(self) -> None:
|
def test_do_mark_onboarding_step_as_read(self) -> None:
|
||||||
user = self.example_user("hamlet")
|
user = self.example_user("hamlet")
|
||||||
do_mark_onboarding_step_as_read(user, "intro_compose")
|
do_mark_onboarding_step_as_read(user, "intro_inbox_view_modal")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
list(
|
list(
|
||||||
OnboardingStep.objects.filter(user=user).values_list("onboarding_step", flat=True)
|
OnboardingStep.objects.filter(user=user).values_list("onboarding_step", flat=True)
|
||||||
),
|
),
|
||||||
["intro_compose"],
|
["intro_inbox_view_modal"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_onboarding_steps_url_endpoint(self) -> None:
|
def test_onboarding_steps_url_endpoint(self) -> None:
|
||||||
user = self.example_user("hamlet")
|
user = self.example_user("hamlet")
|
||||||
self.login_user(user)
|
self.login_user(user)
|
||||||
result = self.client_post(
|
result = self.client_post(
|
||||||
"/json/users/me/onboarding_steps", {"onboarding_step": "intro_streams"}
|
"/json/users/me/onboarding_steps", {"onboarding_step": "intro_recent_view_modal"}
|
||||||
)
|
)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
list(
|
list(
|
||||||
OnboardingStep.objects.filter(user=user).values_list("onboarding_step", flat=True)
|
OnboardingStep.objects.filter(user=user).values_list("onboarding_step", flat=True)
|
||||||
),
|
),
|
||||||
["intro_streams"],
|
["intro_recent_view_modal"],
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.client_post("/json/users/me/onboarding_steps", {"onboarding_step": "invalid"})
|
result = self.client_post("/json/users/me/onboarding_steps", {"onboarding_step": "invalid"})
|
||||||
|
@ -103,5 +70,5 @@ class TestOnboardingSteps(ZulipTestCase):
|
||||||
list(
|
list(
|
||||||
OnboardingStep.objects.filter(user=user).values_list("onboarding_step", flat=True)
|
OnboardingStep.objects.filter(user=user).values_list("onboarding_step", flat=True)
|
||||||
),
|
),
|
||||||
["intro_streams"],
|
["intro_recent_view_modal"],
|
||||||
)
|
)
|
||||||
|
|
|
@ -836,10 +836,10 @@ class RealmImportExportTest(ExportFile):
|
||||||
# Verify strange invariant for Reaction/RealmEmoji.
|
# Verify strange invariant for Reaction/RealmEmoji.
|
||||||
self.assertEqual(reaction.emoji_code, str(realm_emoji.id))
|
self.assertEqual(reaction.emoji_code, str(realm_emoji.id))
|
||||||
|
|
||||||
# data to test import of hotspots
|
# data to test import of onboaring step
|
||||||
OnboardingStep.objects.create(
|
OnboardingStep.objects.create(
|
||||||
user=sample_user,
|
user=sample_user,
|
||||||
onboarding_step="intro_streams",
|
onboarding_step="intro_inbox_view_modal",
|
||||||
)
|
)
|
||||||
|
|
||||||
# data to test import of muted topic
|
# data to test import of muted topic
|
||||||
|
@ -1232,13 +1232,16 @@ class RealmImportExportTest(ExportFile):
|
||||||
self.assertEqual(tups, {("hawaii", cordelia.full_name)})
|
self.assertEqual(tups, {("hawaii", cordelia.full_name)})
|
||||||
return tups
|
return tups
|
||||||
|
|
||||||
# test userhotspot
|
# test onboarding step
|
||||||
@getter
|
@getter
|
||||||
def get_user_hotspots(r: Realm) -> Set[str]:
|
def get_onboarding_steps(r: Realm) -> Set[str]:
|
||||||
user_id = get_user_id(r, "King Hamlet")
|
user_id = get_user_id(r, "King Hamlet")
|
||||||
hotspots = OnboardingStep.objects.filter(user_id=user_id)
|
onboarding_steps = set(
|
||||||
user_hotspots = {hotspot.onboarding_step for hotspot in hotspots}
|
OnboardingStep.objects.filter(user_id=user_id).values_list(
|
||||||
return user_hotspots
|
"onboarding_step", flat=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return onboarding_steps
|
||||||
|
|
||||||
# test muted topics
|
# test muted topics
|
||||||
@getter
|
@getter
|
||||||
|
|
|
@ -1313,9 +1313,9 @@ class UserProfileTest(ZulipTestCase):
|
||||||
|
|
||||||
OnboardingStep.objects.filter(user=cordelia).delete()
|
OnboardingStep.objects.filter(user=cordelia).delete()
|
||||||
OnboardingStep.objects.filter(user=iago).delete()
|
OnboardingStep.objects.filter(user=iago).delete()
|
||||||
hotspots_completed = {"intro_streams", "intro_topics"}
|
onboarding_steps_completed = {"intro_inbox_view_modal", "intro_recent_view_modal"}
|
||||||
for hotspot in hotspots_completed:
|
for onboarding_step in onboarding_steps_completed:
|
||||||
OnboardingStep.objects.create(user=cordelia, onboarding_step=hotspot)
|
OnboardingStep.objects.create(user=cordelia, onboarding_step=onboarding_step)
|
||||||
|
|
||||||
# Check that we didn't send an realm_user update events to
|
# Check that we didn't send an realm_user update events to
|
||||||
# users; this work is happening before the user account is
|
# users; this work is happening before the user account is
|
||||||
|
@ -1357,10 +1357,10 @@ class UserProfileTest(ZulipTestCase):
|
||||||
self.assertEqual(cordelia.enter_sends, False)
|
self.assertEqual(cordelia.enter_sends, False)
|
||||||
self.assertEqual(hamlet.enter_sends, True)
|
self.assertEqual(hamlet.enter_sends, True)
|
||||||
|
|
||||||
hotspots = set(
|
onboarding_steps = set(
|
||||||
OnboardingStep.objects.filter(user=iago).values_list("onboarding_step", flat=True)
|
OnboardingStep.objects.filter(user=iago).values_list("onboarding_step", flat=True)
|
||||||
)
|
)
|
||||||
self.assertEqual(hotspots, hotspots_completed)
|
self.assertEqual(onboarding_steps, onboarding_steps_completed)
|
||||||
|
|
||||||
def test_copy_default_settings_from_realm_user_default(self) -> None:
|
def test_copy_default_settings_from_realm_user_default(self) -> None:
|
||||||
cordelia = self.example_user("cordelia")
|
cordelia = self.example_user("cordelia")
|
||||||
|
|
|
@ -310,12 +310,9 @@ RATE_LIMITING_RULES: Dict[str, List[Tuple[int, int]]] = {}
|
||||||
# Two factor authentication is not yet implementation-complete
|
# Two factor authentication is not yet implementation-complete
|
||||||
TWO_FACTOR_AUTHENTICATION_ENABLED = False
|
TWO_FACTOR_AUTHENTICATION_ENABLED = False
|
||||||
|
|
||||||
# This is used to send all hotspots for convenient manual testing
|
# The new user tutorial can be disabled for self-hosters who want to
|
||||||
# in development mode.
|
# disable the tutorial entirely on their system. Primarily useful for
|
||||||
ALWAYS_SEND_ALL_HOTSPOTS = False
|
# products embedding Zulip as their chat feature.
|
||||||
|
|
||||||
# The new user tutorial is enabled by default, but can be disabled for
|
|
||||||
# self-hosters who want to disable the tutorial entirely on their system.
|
|
||||||
TUTORIAL_ENABLED = True
|
TUTORIAL_ENABLED = True
|
||||||
|
|
||||||
# We log emails in development environment for accessing
|
# We log emails in development environment for accessing
|
||||||
|
|
|
@ -121,9 +121,6 @@ PASSWORD_MIN_GUESSES = 0
|
||||||
TWO_FACTOR_CALL_GATEWAY = "two_factor.gateways.fake.Fake"
|
TWO_FACTOR_CALL_GATEWAY = "two_factor.gateways.fake.Fake"
|
||||||
TWO_FACTOR_SMS_GATEWAY = "two_factor.gateways.fake.Fake"
|
TWO_FACTOR_SMS_GATEWAY = "two_factor.gateways.fake.Fake"
|
||||||
|
|
||||||
# Set this True to send all hotspots in development
|
|
||||||
ALWAYS_SEND_ALL_HOTSPOTS = False
|
|
||||||
|
|
||||||
# FAKE_LDAP_MODE supports using a fake LDAP database in the
|
# FAKE_LDAP_MODE supports using a fake LDAP database in the
|
||||||
# development environment, without needing an LDAP server!
|
# development environment, without needing an LDAP server!
|
||||||
#
|
#
|
||||||
|
|
|
@ -824,7 +824,9 @@ ENABLE_GRAVATAR = True
|
||||||
## to "" will disable the Camo integration.
|
## to "" will disable the Camo integration.
|
||||||
CAMO_URI = "/external_content/"
|
CAMO_URI = "/external_content/"
|
||||||
|
|
||||||
## Controls the tutorial popups for new users.
|
## Controls various features explaining Zulip to new users. Disabling
|
||||||
|
## this is only recommended for installations that are using a limited
|
||||||
|
## subset of the Zulip UI, such as embedding it in a larger app.
|
||||||
# TUTORIAL_ENABLED = True
|
# TUTORIAL_ENABLED = True
|
||||||
|
|
||||||
## Controls whether Zulip will rate-limit user requests.
|
## Controls whether Zulip will rate-limit user requests.
|
||||||
|
|
Loading…
Reference in New Issue