From bf2360bcf2a23982e154b720a7104040aa0589db Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Fri, 10 May 2024 19:07:43 +0530 Subject: [PATCH] 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. --- api_docs/changelog.md | 10 + tools/test-js-with-node | 1 - version.py | 2 +- web/images/hotspots/fish.svg | Bin 5016 -> 0 bytes web/images/hotspots/kelp.svg | Bin 8706 -> 0 bytes web/images/hotspots/speech-bubble.svg | Bin 2386 -> 0 bytes web/images/hotspots/whale-bubble.svg | Bin 8354 -> 0 bytes web/images/hotspots/whale.svg | Bin 7567 -> 0 bytes web/src/bundles/app.ts | 1 - web/src/hotkey.js | 10 - web/src/hotspots.ts | 403 -------------------------- web/src/onboarding_steps.ts | 8 +- web/src/server_events_dispatch.js | 2 - web/src/state_data.ts | 44 +-- web/src/ui_init.js | 2 - web/styles/dark_theme.css | 49 ---- web/styles/hotspots.css | 245 ---------------- web/templates/hotspot_icon.hbs | 5 - web/templates/hotspot_overlay.hbs | 14 - web/tests/dispatch.test.js | 4 +- web/tests/hotkey.test.js | 4 - web/tests/lib/events.js | 16 +- zerver/lib/event_schema.py | 8 +- zerver/lib/hotspots.py | 100 +------ zerver/openapi/zulip.yaml | 44 +-- zerver/tests/test_events.py | 5 +- zerver/tests/test_hotspots.py | 77 ++--- zerver/tests/test_import_export.py | 17 +- zerver/tests/test_users.py | 10 +- zproject/default_settings.py | 9 +- zproject/dev_settings.py | 3 - zproject/prod_settings_template.py | 4 +- 32 files changed, 82 insertions(+), 1015 deletions(-) delete mode 100644 web/images/hotspots/fish.svg delete mode 100644 web/images/hotspots/kelp.svg delete mode 100644 web/images/hotspots/speech-bubble.svg delete mode 100644 web/images/hotspots/whale-bubble.svg delete mode 100644 web/images/hotspots/whale.svg delete mode 100644 web/src/hotspots.ts delete mode 100644 web/styles/hotspots.css delete mode 100644 web/templates/hotspot_icon.hbs delete mode 100644 web/templates/hotspot_overlay.hbs diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 595425f51c..7928d4e50d 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,16 @@ format used by the Zulip server that they are interacting with. ## 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**: * [`GET /user_groups`](/api/get-user-groups), [`POST diff --git a/tools/test-js-with-node b/tools/test-js-with-node index b2df22c002..84f2d127e2 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -117,7 +117,6 @@ EXEMPT_FILES = make_set( "web/src/hashchange.js", "web/src/hbs.d.ts", "web/src/hotkey.js", - "web/src/hotspots.ts", "web/src/inbox_ui.js", "web/src/inbox_util.ts", "web/src/info_overlay.ts", diff --git a/version.py b/version.py index e2c25610f7..e812d0a121 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # Changes should be accompanied by documentation explaining what the # new level means in api_docs/changelog.md, as well as "**Changes**" # 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 # only when going from an old version of the code to a newer version. Bump diff --git a/web/images/hotspots/fish.svg b/web/images/hotspots/fish.svg deleted file mode 100644 index 051aa24f57a3aa9bada99eb47d462adfac0bbbd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5016 zcmb7|TW?#}5rv->AphZ%eJXIy;l5u4tAR~Bh5FXbSM%%h<<-T>6XVYj#(|3RW`EB>+)6Mnmx_iI6 zzZl+Kbz^(@^JbIdj)kUsb-!Glcjhl$Hw?}G>mO%7{pNRPT$SC$uOP% z)6b^U*FU{(ChTaZ(>Fg%y2<5weRDjWK0G{(4}QG9y_mkeJ-fMF&hMr;Fx5gnnqth_ z@qB$g(H=6r7pk3f%kz`T>Dh0K+kbNd5?6O8Uoh>AF*3DXa{Otvy!!P^3vI1++TODC zWMXbUO}gJU|9<<;QTMPsUta>UYcDPr%Ztl3x2D~GTrM77UF)mPbS&w3u3VgbSS^MR zXY*e#Zm;jJ@b_x*z>2*!_2B_pKNd0z2x5Ut?@nH`w)Bl%{KLQOs z*Ti)wu{WE&hQOQ93F_ST~QO!zLLwKtd9s1OO#A@*q?_CVc^bOl%4Z zurXG^Rk)7@EO?OYc01Us#6zm;2JCH+275^0AyTu38?H1`HLM)>$p`Fwc1t%RB+)~7 zD5`wLA&#j!IBAXBCC>vcyj0TK|9GsQ#LBlx)u1RtpEi|6g{08xjJz9MsDRD3 zEnAd;FXD&`av}%895)03A<`AL4hT{&%9Pr;k+eRABG(`JL*aN&7zLu?h0hiu1lG;)x0AJWE3X)T3SdnEH8YzXOfvhR;3!@T(Ziw zAWz~K?F;0Uzdw|gUE%`CCPFJq1))OyDng;+pk_n0?r8*Jlq>Uzwr zyHYdomgzt;quNSQc}mLbp3D@Y^jfH03~@NoUlY z(YSr4@2cbZ(oyJ&h;~89s&AneQou~%=uetR;4AOwAuf$ooMLz5x7tAy(?p}eBO)5e zg~|-62;fRe1-d2}gE|Q#X*)!eI##QjDpo)fx*C#A7l7ui9z2G?j5ujXND1V2WyNQx zRnw+^Iavt*;i#}?G5t4+ZUqe>| zTCRRS!az+bO{`LnLpB@MS#t=Ug&;|8LX~1CzNpGGNW=hD8=1BVC{N_e7vtI|h z+IhadWR15JF&-48RXp*Ef(`J7h$s`ERIHV;fyJ)OD&(b_b?gA2LR4^9&lr~X&3)x0 z;$ObrokTLCfJEQ{8md92pql*Bz$q5q~*~J{OQju&Y&&5Z$giSMb}4>F5jqLNYdtNQ zLx2d$)JNNwYCuuVpE~uxyiEh*j}g3%94jGR8%#OAlMsDMJKSuC^-N75W9^R|k>-Oj qS|cIq9N~PJap0uOlc>OSM{-~}KyfTRi1Bnk;4iUf%HNqdI{goKTM!xm diff --git a/web/images/hotspots/kelp.svg b/web/images/hotspots/kelp.svg deleted file mode 100644 index aeee86a697833a33bd5d187d6b99b1174f597dae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8706 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 diff --git a/web/images/hotspots/speech-bubble.svg b/web/images/hotspots/speech-bubble.svg deleted file mode 100644 index 3eadb0b39a9dae3b9daab810e93eba9c6f2cfd0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2386 zcmZve+io015QeW4iFX)=E2Np8J|8SEQL+wEqyUi!L0lncJh4|=H^;fOWpFI4rZtkbuezn~!TBCH^Oq+3gwc1=S+PlO1KDST4`|9!e*L~l- zm^Rby^03{VHBYa$@22MEdVRM))T72krIqfQpWnV{oT1!x zy8JlperE#`oBiSormfamOr2ED9@eYPhc79bQp!+oQC+n9_MvS)&g)l?A2r9-)!_z^ zomVNy-OaSRzBw$~;FXC&NwKarhE9*YSJ|*HDq0rfm3QpNKB;6y(FEm#XjsS7)M=GSc}qu zb?l9@$&0osw&2Z%f(xCwRL;~ua=@l|U8@kiAgQB-Bud~M1F8v>$SD%GBfl!o`B`O1Ue6{f zl(?9YH`Z}LEd(C0lng4mAX#gX39zbQBp{3w!VAVd{gFZ+l#S*j*OPm!iQ6TKkadxJ zzA@X*cl3HC$_+-xlcBCY>#CuXJR358X&lL8#YJVQzOg~YSb2u#KcxG!9V$U9O+V+U zcTX=0J^C3>0STan~lPz6F&qGn;1p+qJEaLy1TzzNj(TFDP)N<{@|=x3;i zRFE_Uf19C1h(>gRh$abA&>R{E(`VjzWE(uCwqA%8ihO;kq~HsUycJTElRp^iDtnC( zE?FvJibeDug9%f@2os=F<{ROsaM(gHpmMV)?G~khYC>2de|Vk{PFc~8(yzTq$~ozo zT;P->=%fjwlcXZVlg6wZB_SS>IebM1S0fZgp+wc7ha{mL)x4L@D#FV^g||Hk0?XIl<&Z&DdMH5{`yIxulBO zQCC1XRne#Gft{qZ0I{^OFJZQXSut z7f5-KHC|B5GLrt=7KB@y)EXgAa>Dt@;K!qtQJG>MaY})t->EP2(s3RbeS&jDcBK%I zHZnpD!xNr+dI09sADcNzLXTER#T2r_zs^ww8M{kE`GO6dr`74-PdlN`=S2E6sRkKU L=ch0KA+Y-k=nw+- diff --git a/web/images/hotspots/whale-bubble.svg b/web/images/hotspots/whale-bubble.svg deleted file mode 100644 index 0faccc0ffe80cac543c7b26e0fa52ca0ac7d5213..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8354 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[ - [ - "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(); - }); -} diff --git a/web/src/onboarding_steps.ts b/web/src/onboarding_steps.ts index d1b094f441..933dfe5cdc 100644 --- a/web/src/onboarding_steps.ts +++ b/web/src/onboarding_steps.ts @@ -1,7 +1,7 @@ import * as blueslip from "./blueslip"; import * as channel from "./channel"; 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(); @@ -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 { ONE_TIME_NOTICES_TO_DISPLAY.clear(); diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index b1be13bb47..666f45f697 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -20,7 +20,6 @@ import * as emoji from "./emoji"; import * as emoji_picker from "./emoji_picker"; import * as gear_menu from "./gear_menu"; import * as giphy from "./giphy"; -import * as hotspots from "./hotspots"; import * as information_density from "./information_density"; import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area"; import * as linkifiers from "./linkifiers"; @@ -147,7 +146,6 @@ export function dispatch_normal_event(event) { break; case "onboarding_steps": - hotspots.load_new(onboarding_steps.filter_new_hotspots(event.onboarding_steps)); onboarding_steps.update_notice_to_display(event.onboarding_steps); current_user.onboarding_steps = current_user.onboarding_steps ? [...current_user.onboarding_steps, ...event.onboarding_steps] diff --git a/web/src/state_data.ts b/web/src/state_data.ts index d0d6bd6b1f..53cc6cc2fd 100644 --- a/web/src/state_data.ts +++ b/web/src/state_data.ts @@ -21,49 +21,17 @@ export const narrow_term_schema = z.object({ export type NarrowTerm = z.output; // 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; - -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; - -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; - -const hotspot_schema = raw_hotspot_schema.extend({ - location: hotspot_location_schema, -}); - -export type Hotspot = z.output; - const one_time_notice_schema = z.object({ name: z.string(), 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; diff --git a/web/src/ui_init.js b/web/src/ui_init.js index 391cfa9347..d6e415efbf 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -48,7 +48,6 @@ import * as gear_menu from "./gear_menu"; import * as giphy from "./giphy"; import * as hashchange from "./hashchange"; import * as hotkey from "./hotkey"; -import * as hotspots from "./hotspots"; import * as i18n from "./i18n"; import * as inbox_ui from "./inbox_ui"; import * as information_density from "./information_density"; @@ -877,7 +876,6 @@ export function initialize_everything(state_data) { drafts.initialize_ui(); drafts_overlay_ui.initialize(); onboarding_steps.initialize(); - hotspots.initialize(); typing.initialize(); starred_messages_ui.initialize(); user_status_ui.initialize(); diff --git a/web/styles/dark_theme.css b/web/styles/dark_theme.css index 56f5e4f1f9..ba55ff750e 100644 --- a/web/styles/dark_theme.css +++ b/web/styles/dark_theme.css @@ -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 { #default-section { .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 { opacity: 0.7; } diff --git a/web/styles/hotspots.css b/web/styles/hotspots.css deleted file mode 100644 index e38b074f65..0000000000 --- a/web/styles/hotspots.css +++ /dev/null @@ -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; -} diff --git a/web/templates/hotspot_icon.hbs b/web/templates/hotspot_icon.hbs deleted file mode 100644 index 87deca5e52..0000000000 --- a/web/templates/hotspot_icon.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
- - -
?
-
diff --git a/web/templates/hotspot_overlay.hbs b/web/templates/hotspot_overlay.hbs deleted file mode 100644 index f8a2d1fe89..0000000000 --- a/web/templates/hotspot_overlay.hbs +++ /dev/null @@ -1,14 +0,0 @@ -
-
-
-

{{title}}

-
-
-

{{description}}

-
-
- - -
-
-
diff --git a/web/tests/dispatch.test.js b/web/tests/dispatch.test.js index ef04b161fb..706436e0ba 100644 --- a/web/tests/dispatch.test.js +++ b/web/tests/dispatch.test.js @@ -32,7 +32,6 @@ const compose_pm_pill = mock_esm("../src/compose_pm_pill"); const dark_theme = mock_esm("../src/dark_theme"); const emoji_picker = mock_esm("../src/emoji_picker"); const gear_menu = mock_esm("../src/gear_menu"); -const hotspots = mock_esm("../src/hotspots"); const information_density = mock_esm("../src/information_density"); const linkifiers = mock_esm("../src/linkifiers"); 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); }); -run_test("onboarding_steps", ({override}) => { +run_test("onboarding_steps", () => { current_user.onboarding_steps = []; const event = event_fixtures.onboarding_steps; - override(hotspots, "load_new", noop); dispatch(event); assert_same(current_user.onboarding_steps, event.onboarding_steps); }); diff --git a/web/tests/hotkey.test.js b/web/tests/hotkey.test.js index b83b8b508e..440fd3958d 100644 --- a/web/tests/hotkey.test.js +++ b/web/tests/hotkey.test.js @@ -88,10 +88,6 @@ const settings_data = mock_esm("../src/settings_data"); const stream_list = mock_esm("../src/stream_list"); const stream_settings_ui = mock_esm("../src/stream_settings_ui"); -mock_esm("../src/hotspots", { - is_open: () => false, -}); - mock_esm("../src/recent_view_ui", { is_in_focus: () => false, }); diff --git a/web/tests/lib/events.js b/web/tests/lib/events.js index 5779347625..a1feb5e726 100644 --- a/web/tests/lib/events.js +++ b/web/tests/lib/events.js @@ -195,20 +195,12 @@ exports.fixtures = { type: "onboarding_steps", onboarding_steps: [ { - type: "hotspot", - name: "topics", - title: "About topics", - description: "Topics are good.", - delay: 1.5, - has_trigger: false, + type: "one_time_notice", + name: "intro_inbox_view_modal", }, { - type: "hotspot", - name: "compose", - title: "Compose box", - description: "This is where you compose messages.", - delay: 3.14159, - has_trigger: false, + type: "one_time_notice", + name: "intro_recent_view_modal", }, ], }, diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index 7ce886d658..b3e49b7c60 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -338,13 +338,7 @@ _onboarding_steps = DictType( required_keys=[ ("type", str), ("name", str), - ], - optional_keys=[ - ("title", str), - ("description", str), - ("delay", NumberType()), - ("has_trigger", bool), - ], + ] ) onboarding_steps_event = event_dict_type( diff --git a/zerver/lib/hotspots.py b/zerver/lib/hotspots.py index 4fcbe8a4cf..df749a209b 100644 --- a/zerver/lib/hotspots.py +++ b/zerver/lib/hotspots.py @@ -1,73 +1,13 @@ # See https://zulip.readthedocs.io/en/latest/subsystems/hotspots.html # for documentation on this subsystem. 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.utils.translation import gettext_lazy -from django_stubs_ext import StrPromise 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 class OneTimeNotice: name: str @@ -94,24 +34,17 @@ ONE_TIME_NOTICES: List[OneTimeNotice] = [ ), ] -# We would most likely implement new hotspots in the future that aren't -# a part of the initial tutorial. To that end, classifying them into -# categories which are aggregated in ALL_HOTSPOTS, seems like a good start. -ALL_HOTSPOTS = [*INTRO_HOTSPOTS, *NON_INTRO_HOTSPOTS] -ALL_ONBOARDING_STEPS: List[Union[Hotspot, OneTimeNotice]] = [*ALL_HOTSPOTS, *ONE_TIME_NOTICES] +# 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: +# ALL_ONBOARDING_STEPS: List[Union[OneTimeNotice, OtherType]] +# 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]]: - # For manual testing, it can be convenient to set - # ALWAYS_SEND_ALL_HOTSPOTS=True in `zproject/dev_settings.py` to - # 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 a Zulip server has disabled the tutorial, never send any + # onboarding steps. if not settings.TUTORIAL_ENABLED: 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) ) - 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: if one_time_notice.name in seen_onboarding_steps: continue 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 diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index ef65e3c9dd..1f91bf0c73 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -2325,14 +2325,6 @@ paths: "type": "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", "name": "visibility_policy_banner", @@ -19854,43 +19846,15 @@ components: type: type: string description: | - The type of the onboarding step. Valid values are either - `"hotspot"` or `"one_time_notice"`. + The type of the onboarding step. Valid value is `"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: type: string description: | 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: type: object properties: diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index ce20c4ec45..250cf7e2f5 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -2930,11 +2930,8 @@ class NormalActionsTest(BaseAction): check_realm_deactivated("events[0]", events[0]) 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: - 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]) def test_rename_stream(self) -> None: diff --git a/zerver/tests/test_hotspots.py b/zerver/tests/test_hotspots.py index 0da8ec6e37..2ad8c1ba3c 100644 --- a/zerver/tests/test_hotspots.py +++ b/zerver/tests/test_hotspots.py @@ -2,15 +2,9 @@ from typing_extensions import override from zerver.actions.create_user import do_create_user from zerver.actions.hotspots import do_mark_onboarding_step_as_read -from zerver.lib.hotspots import ( - ALL_HOTSPOTS, - INTRO_HOTSPOTS, - NON_INTRO_HOTSPOTS, - ONE_TIME_NOTICES, - get_next_onboarding_steps, -) +from zerver.lib.hotspots import ONE_TIME_NOTICES, get_next_onboarding_steps 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 @@ -24,77 +18,50 @@ class TestGetNextOnboardingSteps(ZulipTestCase): "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) + def test_some_done_some_not(self) -> None: + do_mark_onboarding_step_as_read(self.user, "visibility_policy_banner") + do_mark_onboarding_step_as_read(self.user, "intro_inbox_view_modal") + onboarding_steps = get_next_onboarding_steps(self.user) + self.assert_length(onboarding_steps, 2) + self.assertEqual(onboarding_steps[0]["name"], "intro_recent_view_modal") + self.assertEqual(onboarding_steps[1]["name"], "first_stream_created_banner") + + with self.settings(TUTORIAL_ENABLED=False): + onboarding_steps = get_next_onboarding_steps(self.user) + self.assert_length(onboarding_steps, 0) + + def test_all_onboarding_steps_done(self) -> None: + self.assertNotEqual(get_next_onboarding_steps(self.user), []) 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: - do_mark_onboarding_step_as_read(self.user, "intro_streams") - do_mark_onboarding_step_as_read(self.user, "intro_compose") - onboarding_steps = get_next_onboarding_steps(self.user) - self.assert_length(onboarding_steps, 5) - self.assertEqual(onboarding_steps[0]["name"], "visibility_policy_banner") - self.assertEqual(onboarding_steps[1]["name"], "intro_inbox_view_modal") - self.assertEqual(onboarding_steps[2]["name"], "intro_recent_view_modal") - self.assertEqual(onboarding_steps[3]["name"], "first_stream_created_banner") - self.assertEqual(onboarding_steps[4]["name"], "intro_topics") - - def test_all_onboarding_steps_done(self) -> None: - with self.settings(TUTORIAL_ENABLED=True): - 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 - 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): def test_do_mark_onboarding_step_as_read(self) -> None: 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( list( 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: user = self.example_user("hamlet") self.login_user(user) 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.assertEqual( list( 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"}) @@ -103,5 +70,5 @@ class TestOnboardingSteps(ZulipTestCase): list( OnboardingStep.objects.filter(user=user).values_list("onboarding_step", flat=True) ), - ["intro_streams"], + ["intro_recent_view_modal"], ) diff --git a/zerver/tests/test_import_export.py b/zerver/tests/test_import_export.py index b94b6077bb..9e685272f5 100644 --- a/zerver/tests/test_import_export.py +++ b/zerver/tests/test_import_export.py @@ -836,10 +836,10 @@ class RealmImportExportTest(ExportFile): # Verify strange invariant for Reaction/RealmEmoji. 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( user=sample_user, - onboarding_step="intro_streams", + onboarding_step="intro_inbox_view_modal", ) # data to test import of muted topic @@ -1232,13 +1232,16 @@ class RealmImportExportTest(ExportFile): self.assertEqual(tups, {("hawaii", cordelia.full_name)}) return tups - # test userhotspot + # test onboarding step @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") - hotspots = OnboardingStep.objects.filter(user_id=user_id) - user_hotspots = {hotspot.onboarding_step for hotspot in hotspots} - return user_hotspots + onboarding_steps = set( + OnboardingStep.objects.filter(user_id=user_id).values_list( + "onboarding_step", flat=True + ) + ) + return onboarding_steps # test muted topics @getter diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py index 3485ec8b6e..a0d79e3beb 100644 --- a/zerver/tests/test_users.py +++ b/zerver/tests/test_users.py @@ -1313,9 +1313,9 @@ class UserProfileTest(ZulipTestCase): OnboardingStep.objects.filter(user=cordelia).delete() OnboardingStep.objects.filter(user=iago).delete() - hotspots_completed = {"intro_streams", "intro_topics"} - for hotspot in hotspots_completed: - OnboardingStep.objects.create(user=cordelia, onboarding_step=hotspot) + onboarding_steps_completed = {"intro_inbox_view_modal", "intro_recent_view_modal"} + for onboarding_step in onboarding_steps_completed: + OnboardingStep.objects.create(user=cordelia, onboarding_step=onboarding_step) # Check that we didn't send an realm_user update events to # 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(hamlet.enter_sends, True) - hotspots = set( + onboarding_steps = set( 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: cordelia = self.example_user("cordelia") diff --git a/zproject/default_settings.py b/zproject/default_settings.py index 84f5a987bb..f69f7f5fc2 100644 --- a/zproject/default_settings.py +++ b/zproject/default_settings.py @@ -310,12 +310,9 @@ RATE_LIMITING_RULES: Dict[str, List[Tuple[int, int]]] = {} # Two factor authentication is not yet implementation-complete TWO_FACTOR_AUTHENTICATION_ENABLED = False -# This is used to send all hotspots for convenient manual testing -# in development mode. -ALWAYS_SEND_ALL_HOTSPOTS = False - -# 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. +# The new user tutorial can be disabled for self-hosters who want to +# disable the tutorial entirely on their system. Primarily useful for +# products embedding Zulip as their chat feature. TUTORIAL_ENABLED = True # We log emails in development environment for accessing diff --git a/zproject/dev_settings.py b/zproject/dev_settings.py index 57c0268541..5c132426ca 100644 --- a/zproject/dev_settings.py +++ b/zproject/dev_settings.py @@ -121,9 +121,6 @@ PASSWORD_MIN_GUESSES = 0 TWO_FACTOR_CALL_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 # development environment, without needing an LDAP server! # diff --git a/zproject/prod_settings_template.py b/zproject/prod_settings_template.py index adc18a92cd..289630c8ed 100644 --- a/zproject/prod_settings_template.py +++ b/zproject/prod_settings_template.py @@ -824,7 +824,9 @@ ENABLE_GRAVATAR = True ## to "" will disable the Camo integration. 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 ## Controls whether Zulip will rate-limit user requests.