mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
ui_init: Use OnboardingStep for narrowing on first web app load.
We plan to remove the 'tutorial_status' field from UserProfile table as it is no longer used to show tutorial. The field is also used to narrow a new user in DM with welcome bot on the first load. This prep commit updates the logic to use a new OnboardingStep for the narrowing behaviour on the first load. This will help in removing the 'tutorial_status' field.
This commit is contained in:
committed by
Tim Abbott
parent
13c0571183
commit
ee806c49b9
@@ -2,7 +2,8 @@ import type {z} from "zod";
|
||||
|
||||
import * as blueslip from "./blueslip";
|
||||
import * as channel from "./channel";
|
||||
import type {StateData, onboarding_step_schema} from "./state_data";
|
||||
import * as people from "./people";
|
||||
import type {NarrowTerm, StateData, onboarding_step_schema} from "./state_data";
|
||||
|
||||
export type OnboardingStep = z.output<typeof onboarding_step_schema>;
|
||||
|
||||
@@ -34,6 +35,32 @@ export function update_onboarding_steps_to_display(onboarding_steps: OnboardingS
|
||||
}
|
||||
}
|
||||
|
||||
export function initialize(params: StateData["onboarding_steps"]): void {
|
||||
update_onboarding_steps_to_display(params.onboarding_steps);
|
||||
function narrow_to_dm_with_welcome_bot_new_user(
|
||||
onboarding_steps: OnboardingStep[],
|
||||
show_message_view: (raw_terms: NarrowTerm[], opts: {trigger: string}) => void,
|
||||
): void {
|
||||
if (
|
||||
onboarding_steps.some(
|
||||
(onboarding_step) => onboarding_step.name === "narrow_to_dm_with_welcome_bot_new_user",
|
||||
)
|
||||
) {
|
||||
show_message_view(
|
||||
[
|
||||
{
|
||||
operator: "dm",
|
||||
operand: people.WELCOME_BOT.email,
|
||||
},
|
||||
],
|
||||
{trigger: "sidebar"},
|
||||
);
|
||||
post_onboarding_step_as_read("narrow_to_dm_with_welcome_bot_new_user");
|
||||
}
|
||||
}
|
||||
|
||||
export function initialize(
|
||||
params: StateData["onboarding_steps"],
|
||||
show_message_view: (raw_terms: NarrowTerm[], opts: {trigger: string}) => void,
|
||||
): void {
|
||||
update_onboarding_steps_to_display(params.onboarding_steps);
|
||||
narrow_to_dm_with_welcome_bot_new_user(params.onboarding_steps, show_message_view);
|
||||
}
|
||||
|
@@ -174,6 +174,11 @@ const one_time_notice_schema = z.object({
|
||||
type: z.literal("one_time_notice"),
|
||||
});
|
||||
|
||||
const one_time_action_schema = z.object({
|
||||
name: z.string(),
|
||||
type: z.literal("one_time_action"),
|
||||
});
|
||||
|
||||
export const thumbnail_format_schema = z.object({
|
||||
name: z.string(),
|
||||
max_width: z.number(),
|
||||
@@ -182,12 +187,7 @@ export const thumbnail_format_schema = z.object({
|
||||
animated: z.boolean(),
|
||||
});
|
||||
|
||||
/* 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. */
|
||||
export const onboarding_step_schema = one_time_notice_schema;
|
||||
export const onboarding_step_schema = z.union([one_time_notice_schema, one_time_action_schema]);
|
||||
|
||||
// Sync this with zerver.lib.events.do_events_register.
|
||||
const current_user_schema = z.object({
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import * as channel from "./channel";
|
||||
import * as message_view from "./message_view";
|
||||
import {page_params} from "./page_params";
|
||||
import * as people from "./people";
|
||||
|
||||
function set_tutorial_status(status, callback) {
|
||||
return channel.post({
|
||||
@@ -14,14 +12,5 @@ function set_tutorial_status(status, callback) {
|
||||
export function initialize() {
|
||||
if (page_params.needs_tutorial) {
|
||||
set_tutorial_status("started");
|
||||
message_view.show(
|
||||
[
|
||||
{
|
||||
operator: "dm",
|
||||
operand: people.WELCOME_BOT.email,
|
||||
},
|
||||
],
|
||||
{trigger: "sidebar"},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -669,7 +669,9 @@ export function initialize_everything(state_data) {
|
||||
});
|
||||
drafts.initialize_ui();
|
||||
drafts_overlay_ui.initialize();
|
||||
onboarding_steps.initialize(state_data.onboarding_steps);
|
||||
// This needs to happen after activity_ui.initialize, so that user_filter
|
||||
// is defined. Also, must happen after people.initialize()
|
||||
onboarding_steps.initialize(state_data.onboarding_steps, message_view.show);
|
||||
typing.initialize();
|
||||
starred_messages_ui.initialize();
|
||||
user_status_ui.initialize();
|
||||
|
@@ -325,7 +325,7 @@ run_test("default_streams", ({override}) => {
|
||||
});
|
||||
|
||||
run_test("onboarding_steps", () => {
|
||||
onboarding_steps.initialize({onboarding_steps: []});
|
||||
onboarding_steps.initialize({onboarding_steps: []}, () => {});
|
||||
const event = event_fixtures.onboarding_steps;
|
||||
const one_time_notices = new Set();
|
||||
for (const onboarding_step of event.onboarding_steps) {
|
||||
|
@@ -19,6 +19,17 @@ class OneTimeNotice:
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class OneTimeAction:
|
||||
name: str
|
||||
|
||||
def to_dict(self) -> dict[str, str]:
|
||||
return {
|
||||
"type": "one_time_action",
|
||||
"name": self.name,
|
||||
}
|
||||
|
||||
|
||||
ONE_TIME_NOTICES: list[OneTimeNotice] = [
|
||||
OneTimeNotice(
|
||||
name="visibility_policy_banner",
|
||||
@@ -43,12 +54,9 @@ ONE_TIME_NOTICES: list[OneTimeNotice] = [
|
||||
),
|
||||
]
|
||||
|
||||
# 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
|
||||
ONE_TIME_ACTIONS = [OneTimeAction(name="narrow_to_dm_with_welcome_bot_new_user")]
|
||||
|
||||
ALL_ONBOARDING_STEPS: list[OneTimeNotice | OneTimeAction] = ONE_TIME_NOTICES + ONE_TIME_ACTIONS
|
||||
|
||||
|
||||
def get_next_onboarding_steps(user: UserProfile) -> list[dict[str, Any]]:
|
||||
@@ -62,10 +70,10 @@ def get_next_onboarding_steps(user: UserProfile) -> list[dict[str, Any]]:
|
||||
)
|
||||
|
||||
onboarding_steps: list[dict[str, Any]] = []
|
||||
for one_time_notice in ONE_TIME_NOTICES:
|
||||
if one_time_notice.name in seen_onboarding_steps:
|
||||
for onboarding_step in ALL_ONBOARDING_STEPS:
|
||||
if onboarding_step.name in seen_onboarding_steps:
|
||||
continue
|
||||
onboarding_steps.append(one_time_notice.to_dict())
|
||||
onboarding_steps.append(onboarding_step.to_dict())
|
||||
|
||||
return onboarding_steps
|
||||
|
||||
|
@@ -0,0 +1,69 @@
|
||||
# Generated by Django 5.0.6 on 2024-07-25 13:24
|
||||
|
||||
from django.db import connection, migrations
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.migrations.state import StateApps
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from psycopg2.sql import SQL
|
||||
|
||||
|
||||
def mark_narrow_to_dm_with_welcome_bot_new_user_as_read(
|
||||
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
|
||||
) -> None:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(SQL("SELECT MAX(id) FROM zerver_userprofile;"))
|
||||
(max_id,) = cursor.fetchone()
|
||||
|
||||
if max_id is None:
|
||||
return
|
||||
|
||||
BATCH_SIZE = 10000
|
||||
max_id += BATCH_SIZE / 2
|
||||
lower_id_bound = 0
|
||||
timestamp_value = timezone_now()
|
||||
while lower_id_bound < max_id:
|
||||
upper_id_bound = min(lower_id_bound + BATCH_SIZE, max_id)
|
||||
with connection.cursor() as cursor:
|
||||
query = SQL("""
|
||||
INSERT INTO zerver_onboardingstep (user_id, onboarding_step, timestamp)
|
||||
SELECT id, 'narrow_to_dm_with_welcome_bot_new_user', %(timestamp_value)s
|
||||
FROM zerver_userprofile
|
||||
WHERE is_bot = False
|
||||
AND is_mirror_dummy = False
|
||||
AND tutorial_status != "W"
|
||||
AND id > %(lower_id_bound)s AND id <= %(upper_id_bound)s;
|
||||
""")
|
||||
cursor.execute(
|
||||
query,
|
||||
{
|
||||
"timestamp_value": timestamp_value,
|
||||
"lower_id_bound": lower_id_bound,
|
||||
"upper_id_bound": upper_id_bound,
|
||||
},
|
||||
)
|
||||
|
||||
print(f"Processed {upper_id_bound} / {max_id}")
|
||||
lower_id_bound += BATCH_SIZE
|
||||
|
||||
|
||||
def mark_narrow_to_dm_with_welcome_bot_new_user_as_unread(
|
||||
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
|
||||
) -> None:
|
||||
OnboardingStep = apps.get_model("zerver", "OnboardingStep")
|
||||
|
||||
OnboardingStep.objects.filter(onboarding_step="narrow_to_dm_with_welcome_bot_new_user").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
dependencies = [
|
||||
("zerver", "0567_alter_realm_can_delete_any_message_group"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
mark_narrow_to_dm_with_welcome_bot_new_user_as_read,
|
||||
reverse_code=mark_narrow_to_dm_with_welcome_bot_new_user_as_unread,
|
||||
elidable=True,
|
||||
),
|
||||
]
|
@@ -2,7 +2,7 @@ from typing_extensions import override
|
||||
|
||||
from zerver.actions.create_user import do_create_user
|
||||
from zerver.actions.onboarding_steps import do_mark_onboarding_step_as_read
|
||||
from zerver.lib.onboarding_steps import ONE_TIME_NOTICES, get_next_onboarding_steps
|
||||
from zerver.lib.onboarding_steps import ALL_ONBOARDING_STEPS, get_next_onboarding_steps
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.models import OnboardingStep
|
||||
from zerver.models.realms import get_realm
|
||||
@@ -25,12 +25,13 @@ class TestGetNextOnboardingSteps(ZulipTestCase):
|
||||
|
||||
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, 5)
|
||||
self.assert_length(onboarding_steps, 6)
|
||||
self.assertEqual(onboarding_steps[0]["name"], "intro_recent_view_modal")
|
||||
self.assertEqual(onboarding_steps[1]["name"], "first_stream_created_banner")
|
||||
self.assertEqual(onboarding_steps[2]["name"], "jump_to_conversation_banner")
|
||||
self.assertEqual(onboarding_steps[3]["name"], "non_interleaved_view_messages_fading")
|
||||
self.assertEqual(onboarding_steps[4]["name"], "interleaved_view_messages_fading")
|
||||
self.assertEqual(onboarding_steps[5]["name"], "narrow_to_dm_with_welcome_bot_new_user")
|
||||
|
||||
with self.settings(TUTORIAL_ENABLED=False):
|
||||
onboarding_steps = get_next_onboarding_steps(self.user)
|
||||
@@ -39,13 +40,18 @@ class TestGetNextOnboardingSteps(ZulipTestCase):
|
||||
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)
|
||||
for onboarding_step in ALL_ONBOARDING_STEPS: # nocoverage
|
||||
do_mark_onboarding_step_as_read(self.user, onboarding_step.name)
|
||||
|
||||
self.assertEqual(get_next_onboarding_steps(self.user), [])
|
||||
|
||||
|
||||
class TestOnboardingSteps(ZulipTestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
OnboardingStep.objects.filter(user=self.example_user("hamlet")).delete()
|
||||
|
||||
def test_do_mark_onboarding_step_as_read(self) -> None:
|
||||
user = self.example_user("hamlet")
|
||||
do_mark_onboarding_step_as_read(user, "intro_inbox_view_modal")
|
||||
|
@@ -19,6 +19,7 @@ from urllib3.fields import RequestField
|
||||
import zerver.lib.upload
|
||||
from analytics.models import RealmCount
|
||||
from zerver.actions.create_realm import do_create_realm
|
||||
from zerver.actions.create_user import do_create_user
|
||||
from zerver.actions.message_send import internal_send_private_message
|
||||
from zerver.actions.realm_icon import do_change_icon_source
|
||||
from zerver.actions.realm_logo import do_change_logo_source
|
||||
@@ -1305,7 +1306,9 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase):
|
||||
self.client_post("/json/users/me/avatar", {"file": image_file})
|
||||
|
||||
source_user_profile = self.example_user("hamlet")
|
||||
target_user_profile = self.example_user("iago")
|
||||
target_user_profile = do_create_user(
|
||||
"user@zulip.com", "password", get_realm("zulip"), "user", acting_user=None
|
||||
)
|
||||
|
||||
copy_default_settings(source_user_profile, target_user_profile)
|
||||
|
||||
|
@@ -9,6 +9,7 @@ import pyvips
|
||||
from django.conf import settings
|
||||
|
||||
import zerver.lib.upload
|
||||
from zerver.actions.create_user import do_create_user
|
||||
from zerver.actions.user_settings import do_delete_avatar_image
|
||||
from zerver.lib.avatar_hash import user_avatar_path
|
||||
from zerver.lib.create_user import copy_default_settings
|
||||
@@ -321,7 +322,9 @@ class S3Test(ZulipTestCase):
|
||||
self.client_post("/json/users/me/avatar", {"file": image_file})
|
||||
|
||||
source_user_profile = self.example_user("hamlet")
|
||||
target_user_profile = self.example_user("othello")
|
||||
target_user_profile = do_create_user(
|
||||
"user@zulip.com", "password", get_realm("zulip"), "user", acting_user=None
|
||||
)
|
||||
|
||||
copy_default_settings(source_user_profile, target_user_profile)
|
||||
|
||||
|
@@ -70,6 +70,7 @@ from zerver.models import (
|
||||
)
|
||||
from zerver.models.alert_words import flush_alert_word
|
||||
from zerver.models.clients import get_client
|
||||
from zerver.models.onboarding_steps import OnboardingStep
|
||||
from zerver.models.realms import WildcardMentionPolicyEnum, get_realm
|
||||
from zerver.models.recipients import get_or_create_direct_message_group
|
||||
from zerver.models.streams import get_stream
|
||||
@@ -935,7 +936,18 @@ class Command(ZulipBaseCommand):
|
||||
defaults={"last_active_time": date, "last_connected_time": date},
|
||||
)
|
||||
|
||||
user_profiles_ids = [user_profile.id for user_profile in user_profiles]
|
||||
user_profiles_ids = []
|
||||
onboarding_steps = []
|
||||
for user_profile in user_profiles:
|
||||
user_profiles_ids.append(user_profile.id)
|
||||
onboarding_steps.append(
|
||||
OnboardingStep(
|
||||
user=user_profile, onboarding_step="narrow_to_dm_with_welcome_bot_new_user"
|
||||
)
|
||||
)
|
||||
|
||||
# Existing users shouldn't narrow to DM with welcome bot on first login.
|
||||
OnboardingStep.objects.bulk_create(onboarding_steps)
|
||||
|
||||
# Create several initial direct message groups
|
||||
for i in range(options["num_direct_message_groups"]):
|
||||
|
Reference in New Issue
Block a user