events: Add 'onboarding_steps' event deprecating 'hotspots'.

Earlier, the event sent when an onboarding step (hotspot till now)
is marked as read generated an event with type='hotspots' and
'hotspots' named array in it.

This commit renames the type to 'onboarding_steps' and the array
to 'onboarding_steps' to reflect the fact that it'll also contain
data for elements other than hotspots.
This commit is contained in:
Prakhar Pratyush 2023-12-02 16:00:35 +05:30 committed by Tim Abbott
parent dde3d72100
commit 83bd9955e3
13 changed files with 90 additions and 60 deletions

View File

@ -22,6 +22,14 @@ format used by the Zulip server that they are interacting with.
**Feature level 233** **Feature level 233**
* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events):
Renamed the event type `hotspots` and the `hotspots` array field in it
to `onboarding_steps` as this event is sent to clients with remaining
onboarding steps data that includes hotspots and one-time notices to display.
Earlier, we had hotspots only. Added a `type` field to the objects in
the renamed `onboarding_steps` array to distinguish between the two type
of onboarding steps.
* `POST /users/me/onboarding_steps`: Added a new endpoint that * `POST /users/me/onboarding_steps`: Added a new endpoint that
deprecates the `/users/me/hotspots` endpoint. Added support for deprecates the `/users/me/hotspots` endpoint. Added support for
displaying one-time notices in addition to existing hotspots. displaying one-time notices in addition to existing hotspots.

View File

@ -336,7 +336,7 @@ export function load_new(new_hotspots) {
} }
export function initialize() { export function initialize() {
load_new(page_params.hotspots); load_new(onboarding_steps.filter_new_hotspots(page_params.onboarding_steps));
// open // open
$("body").on("click", ".hotspot-icon", function (e) { $("body").on("click", ".hotspot-icon", function (e) {

View File

@ -16,3 +16,7 @@ export function post_onboarding_step_as_read(onboarding_step_name) {
}, },
}); });
} }
export function filter_new_hotspots(onboarding_steps) {
return onboarding_steps.filter((onboarding_step) => onboarding_step.type === "hotspot");
}

View File

@ -30,6 +30,7 @@ import * as muted_users_ui from "./muted_users_ui";
import * as narrow_state from "./narrow_state"; import * as narrow_state from "./narrow_state";
import * as narrow_title from "./narrow_title"; import * as narrow_title from "./narrow_title";
import * as navbar_alerts from "./navbar_alerts"; import * as navbar_alerts from "./navbar_alerts";
import * as onboarding_steps from "./onboarding_steps";
import * as overlays from "./overlays"; import * as overlays from "./overlays";
import {page_params} from "./page_params"; import {page_params} from "./page_params";
import * as peer_data from "./peer_data"; import * as peer_data from "./peer_data";
@ -142,11 +143,11 @@ export function dispatch_normal_event(event) {
} }
break; break;
case "hotspots": case "onboarding_steps":
hotspots.load_new(event.hotspots); hotspots.load_new(onboarding_steps.filter_new_hotspots(event.onboarding_steps));
page_params.hotspots = page_params.hotspots page_params.onboarding_steps = page_params.onboarding_steps
? [...page_params.hotspots, ...event.hotspots] ? [...page_params.onboarding_steps, ...event.onboarding_steps]
: event.hotspots; : event.onboarding_steps;
break; break;
case "invites_changed": case "invites_changed":

View File

@ -306,12 +306,12 @@ 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("hotspots", ({override}) => { run_test("onboarding_steps", ({override}) => {
page_params.hotspots = []; page_params.onboarding_steps = [];
const event = event_fixtures.hotspots; const event = event_fixtures.onboarding_steps;
override(hotspots, "load_new", noop); override(hotspots, "load_new", noop);
dispatch(event); dispatch(event);
assert_same(page_params.hotspots, event.hotspots); assert_same(page_params.onboarding_steps, event.onboarding_steps);
}); });
run_test("invites_changed", ({override}) => { run_test("invites_changed", ({override}) => {

View File

@ -169,26 +169,6 @@ exports.fixtures = {
value: true, value: true,
}, },
hotspots: {
type: "hotspots",
hotspots: [
{
name: "topics",
title: "About topics",
description: "Topics are good.",
delay: 1.5,
has_trigger: false,
},
{
name: "compose",
title: "Compose box",
description: "This is where you compose messages.",
delay: 3.14159,
has_trigger: false,
},
],
},
invites_changed: { invites_changed: {
type: "invites_changed", type: "invites_changed",
}, },
@ -207,6 +187,28 @@ exports.fixtures = {
], ],
}, },
onboarding_steps: {
type: "onboarding_steps",
onboarding_steps: [
{
type: "hotspot",
name: "topics",
title: "About topics",
description: "Topics are good.",
delay: 1.5,
has_trigger: false,
},
{
type: "hotspot",
name: "compose",
title: "Compose box",
description: "This is where you compose messages.",
delay: 3.14159,
has_trigger: false,
},
],
},
presence: { presence: {
type: "presence", type: "presence",
email: "alice@example.com", email: "alice@example.com",

View File

@ -5,5 +5,5 @@ from zerver.tornado.django_api import send_event
def do_mark_onboarding_step_as_read(user: UserProfile, onboarding_step: str) -> None: def do_mark_onboarding_step_as_read(user: UserProfile, onboarding_step: str) -> None:
OnboardingStep.objects.get_or_create(user=user, onboarding_step=onboarding_step) OnboardingStep.objects.get_or_create(user=user, onboarding_step=onboarding_step)
event = dict(type="hotspots", hotspots=get_next_onboarding_steps(user)) event = dict(type="onboarding_steps", onboarding_steps=get_next_onboarding_steps(user))
send_event(user.realm, event, [user.id]) send_event(user.realm, event, [user.id])

View File

@ -332,27 +332,29 @@ def check_heartbeat(
_check_heartbeat(var_name, event) _check_heartbeat(var_name, event)
_hotspot = DictType( _onboarding_steps = DictType(
required_keys=[ required_keys=[
("type", str), ("type", str),
("name", str), ("name", str),
],
optional_keys=[
("title", str), ("title", str),
("description", str), ("description", str),
("delay", NumberType()), ("delay", NumberType()),
("has_trigger", bool), ("has_trigger", bool),
] ],
) )
hotspots_event = event_dict_type( onboarding_steps_event = event_dict_type(
required_keys=[ required_keys=[
("type", Equals("hotspots")), ("type", Equals("onboarding_steps")),
( (
"hotspots", "onboarding_steps",
ListType(_hotspot), ListType(_onboarding_steps),
), ),
] ]
) )
check_hotspots = make_checker(hotspots_event) check_onboarding_steps = make_checker(onboarding_steps_event)
invites_changed_event = event_dict_type( invites_changed_event = event_dict_type(
required_keys=[ required_keys=[

View File

@ -184,11 +184,13 @@ def fetch_initial_state_data(
del state["custom_profile_field_types"]["PRONOUNS"] del state["custom_profile_field_types"]["PRONOUNS"]
if want("hotspots"): if want("onboarding_steps"):
# Even if we offered special hotspots for guests without an # Even if we offered special onboarding steps for guests without an
# account, we'd maybe need to store their state using cookies # account, we'd maybe need to store their state using cookies
# or local storage, rather than in the database. # or local storage, rather than in the database.
state["hotspots"] = [] if user_profile is None else get_next_onboarding_steps(user_profile) state["onboarding_steps"] = (
[] if user_profile is None else get_next_onboarding_steps(user_profile)
)
if want("message"): if want("message"):
# Since the introduction of `anchor="latest"` in the API, # Since the introduction of `anchor="latest"` in the API,
@ -854,8 +856,8 @@ def apply_event(
if scheduled_message["scheduled_message_id"] == event["scheduled_message_id"]: if scheduled_message["scheduled_message_id"] == event["scheduled_message_id"]:
del state["scheduled_messages"][idx] del state["scheduled_messages"][idx]
elif event["type"] == "hotspots": elif event["type"] == "onboarding_steps":
state["hotspots"] = event["hotspots"] state["onboarding_steps"] = event["onboarding_steps"]
elif event["type"] == "custom_profile_fields": elif event["type"] == "custom_profile_fields":
state["custom_profile_fields"] = event["fields"] state["custom_profile_fields"] = event["fields"]
custom_profile_field_ids = {field["id"] for field in state["custom_profile_fields"]} custom_profile_field_ids = {field["id"] for field in state["custom_profile_fields"]}

View File

@ -2272,11 +2272,14 @@ paths:
- type: object - type: object
additionalProperties: false additionalProperties: false
description: | description: |
Event sent when the set of onboarding "hotspots" to show for Event sent when the set of onboarding steps to show for
the current user have changed (E.g. because the user dismissed one). the current user have changed (E.g. because the user dismissed one).
Clients that feature a similar tutorial experience to the Zulip Clients that feature a similar tutorial experience to the Zulip
web app may want to handle these events. web app may want to handle these events.
**Changes**: Before Zulip 8.0 (feature level 233), this was named
as `Hotspots`. One-time notice wasn't available earlier.
properties: properties:
id: id:
$ref: "#/components/schemas/EventIdSchema" $ref: "#/components/schemas/EventIdSchema"
@ -2284,18 +2287,21 @@ paths:
allOf: allOf:
- $ref: "#/components/schemas/EventTypeSchema" - $ref: "#/components/schemas/EventTypeSchema"
- enum: - enum:
- hotspots - onboarding_steps
hotspots: onboarding_steps:
type: array type: array
description: | description: |
An array of dictionaries where each An array of dictionaries where each
dictionary contains details about a single hotspot. dictionary contains details about a single onboarding step.
**Changes**: Before Zulip 8.0 (feature level 233), this array
was named as `hotspots`. One-time notice wasn't available earlier.
items: items:
$ref: "#/components/schemas/Hotspot" $ref: "#/components/schemas/OnboardingStep"
example: example:
{ {
"type": "hotspots", "type": "onboarding_steps",
"hotspots": "onboarding_steps":
[ [
{ {
"type": "hotspot", "type": "hotspot",
@ -11815,17 +11821,17 @@ paths:
array will be empty if `enable_drafts_synchronization` is set to `false`. array will be empty if `enable_drafts_synchronization` is set to `false`.
items: items:
$ref: "#/components/schemas/Draft" $ref: "#/components/schemas/Draft"
hotspots: onboarding_steps:
type: array type: array
description: | description: |
Present if `hotspots` is present in `fetch_event_types`. Present if `onboarding_steps` is present in `fetch_event_types`.
An array of dictionaries, where each dictionary contains details about An array of dictionaries, where each dictionary contains details about
a single onboarding hotspot that should be shown to new users. a single onboarding step that should be shown to new users.
We expect that only official Zulip clients will interact with these data. We expect that only official Zulip clients will interact with these data.
items: items:
$ref: "#/components/schemas/Hotspot" $ref: "#/components/schemas/OnboardingStep"
max_message_id: max_message_id:
type: integer type: integer
deprecated: true deprecated: true
@ -18552,17 +18558,22 @@ components:
[profile field types](/help/custom-profile-fields#profile-field-types). [profile field types](/help/custom-profile-fields#profile-field-types).
**Changes**: New in Zulip 6.0 (feature level 146). **Changes**: New in Zulip 6.0 (feature level 146).
Hotspot: OnboardingStep:
type: object type: object
additionalProperties: false additionalProperties: false
description: | description: |
Dictionary containing details of a single hotspot. Dictionary containing details of a single onboarding step.
**Changes**: Before Zulip 8.0 (feature level 233), this was
named as `Hotspot`. One-time notice wasn't available earlier.
properties: properties:
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 values are either
'hotspot' or 'one_time_notice'. 'hotspot' or 'one_time_notice'.
**Changes**: New in Zulip 8.0 (feature level 233).
name: name:
type: string type: string
description: | description: |

View File

@ -1259,10 +1259,10 @@ class FetchQueriesTest(ZulipTestCase):
default_streams=1, default_streams=1,
default_stream_groups=1, default_stream_groups=1,
drafts=1, drafts=1,
hotspots=1,
message=1, message=1,
muted_topics=1, muted_topics=1,
muted_users=1, muted_users=1,
onboarding_steps=1,
presence=1, presence=1,
realm=1, realm=1,
realm_bot=1, realm_bot=1,

View File

@ -145,11 +145,11 @@ from zerver.lib.event_schema import (
check_draft_update, check_draft_update,
check_has_zoom_token, check_has_zoom_token,
check_heartbeat, check_heartbeat,
check_hotspots,
check_invites_changed, check_invites_changed,
check_message, check_message,
check_muted_topics, check_muted_topics,
check_muted_users, check_muted_users,
check_onboarding_steps,
check_presence, check_presence,
check_reaction_add, check_reaction_add,
check_reaction_remove, check_reaction_remove,
@ -3055,7 +3055,7 @@ class NormalActionsTest(BaseAction):
events = self.verify_action( events = self.verify_action(
lambda: do_mark_onboarding_step_as_read(self.user_profile, "intro_streams") lambda: do_mark_onboarding_step_as_read(self.user_profile, "intro_streams")
) )
check_hotspots("events[0]", events[0]) check_onboarding_steps("events[0]", events[0])
def test_rename_stream(self) -> None: def test_rename_stream(self) -> None:
for i, include_streams in enumerate([True, False]): for i, include_streams in enumerate([True, False]):

View File

@ -73,7 +73,6 @@ class HomeTest(ZulipTestCase):
"giphy_api_key", "giphy_api_key",
"giphy_rating_options", "giphy_rating_options",
"has_zoom_token", "has_zoom_token",
"hotspots",
"insecure_desktop_app", "insecure_desktop_app",
"is_admin", "is_admin",
"is_billing_admin", "is_billing_admin",
@ -101,6 +100,7 @@ class HomeTest(ZulipTestCase):
"needs_tutorial", "needs_tutorial",
"never_subscribed", "never_subscribed",
"no_event_queue", "no_event_queue",
"onboarding_steps",
"password_min_guesses", "password_min_guesses",
"password_min_length", "password_min_length",
"presences", "presences",