mirror of https://github.com/zulip/zulip.git
hotspots: Add backend support for tutorial hotspots.
This commit adds the backend support for a new style of tutorial which allows for highlighting of multiple areas of the page with hotspots that disappear when clicked by the user.
This commit is contained in:
parent
33c130a603
commit
6f061beb46
|
@ -119,7 +119,8 @@
|
||||||
"emoji_codes": false,
|
"emoji_codes": false,
|
||||||
"drafts": false,
|
"drafts": false,
|
||||||
"katex": false,
|
"katex": false,
|
||||||
"Clipboard": false
|
"Clipboard": false,
|
||||||
|
"hotspots": false
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-restricted-syntax": 0,
|
"no-restricted-syntax": 0,
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
var hotspots = (function () {
|
||||||
|
|
||||||
|
var exports = {};
|
||||||
|
|
||||||
|
exports.show = function (hotspot_list) {
|
||||||
|
$('.hotspot').hide();
|
||||||
|
for (var i = 0; i < hotspot_list.length; i += 1) {
|
||||||
|
$("#hotspot_".concat(hotspot_list[i])).show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.initialize = function () {
|
||||||
|
exports.show(page_params.hotspots);
|
||||||
|
};
|
||||||
|
|
||||||
|
function mark_hotspot_as_read(hotspot) {
|
||||||
|
channel.post({
|
||||||
|
url: '/json/users/me/hotspots',
|
||||||
|
data: {hotspot: JSON.stringify(hotspot)},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
$("#hotspot_welcome").on('click', function (e) {
|
||||||
|
mark_hotspot_as_read("welcome");
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
$("#hotspot_streams").on('click', function (e) {
|
||||||
|
mark_hotspot_as_read("streams");
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
$("#hotspot_topics").on('click', function (e) {
|
||||||
|
mark_hotspot_as_read("topics");
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
$("#hotspot_narrowing").on('click', function (e) {
|
||||||
|
mark_hotspot_as_read("narrowing");
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
$("#hotspot_replying").on('click', function (e) {
|
||||||
|
mark_hotspot_as_read("replying");
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
$("#hotspot_get_started").on('click', function (e) {
|
||||||
|
mark_hotspot_as_read("get_started");
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return exports;
|
||||||
|
}());
|
||||||
|
if (typeof module !== 'undefined') {
|
||||||
|
module.exports = hotspots;
|
||||||
|
}
|
|
@ -23,6 +23,11 @@ function dispatch_normal_event(event) {
|
||||||
admin.update_default_streams_table();
|
admin.update_default_streams_table();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'hotspots':
|
||||||
|
hotspots.show(event.hotspots);
|
||||||
|
page_params.hotspots = event.hotspots;
|
||||||
|
break;
|
||||||
|
|
||||||
case 'muted_topics':
|
case 'muted_topics':
|
||||||
muting_ui.handle_updates(event.muted_topics);
|
muting_ui.handle_updates(event.muted_topics);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -256,6 +256,7 @@ $(function () {
|
||||||
unread_ui.initialize();
|
unread_ui.initialize();
|
||||||
activity.initialize();
|
activity.initialize();
|
||||||
emoji.initialize();
|
emoji.initialize();
|
||||||
|
hotspots.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ from zerver.lib.cache import (
|
||||||
to_dict_cache_key_id,
|
to_dict_cache_key_id,
|
||||||
)
|
)
|
||||||
from zerver.lib.context_managers import lockfile
|
from zerver.lib.context_managers import lockfile
|
||||||
|
from zerver.lib.hotspots import get_next_hotspots
|
||||||
from zerver.lib.message import (
|
from zerver.lib.message import (
|
||||||
access_message,
|
access_message,
|
||||||
MessageDict,
|
MessageDict,
|
||||||
|
@ -28,7 +29,7 @@ from zerver.lib.message import (
|
||||||
)
|
)
|
||||||
from zerver.lib.realm_icon import realm_icon_url
|
from zerver.lib.realm_icon import realm_icon_url
|
||||||
from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity, RealmAlias, \
|
from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity, RealmAlias, \
|
||||||
Subscription, Recipient, Message, Attachment, UserMessage, RealmAuditLog, \
|
Subscription, Recipient, Message, Attachment, UserMessage, RealmAuditLog, UserHotspot, \
|
||||||
Client, DefaultStream, UserPresence, Referral, PushDeviceToken, MAX_SUBJECT_LENGTH, \
|
Client, DefaultStream, UserPresence, Referral, PushDeviceToken, MAX_SUBJECT_LENGTH, \
|
||||||
MAX_MESSAGE_LENGTH, get_client, get_stream, get_recipient, get_huddle, \
|
MAX_MESSAGE_LENGTH, get_client, get_stream, get_recipient, get_huddle, \
|
||||||
get_user_profile_by_id, PreregistrationUser, get_display_recipient, \
|
get_user_profile_by_id, PreregistrationUser, get_display_recipient, \
|
||||||
|
@ -3174,6 +3175,12 @@ def do_update_muted_topic(user_profile, stream, topic, op):
|
||||||
event = dict(type="muted_topics", muted_topics=muted_topics)
|
event = dict(type="muted_topics", muted_topics=muted_topics)
|
||||||
send_event(event, [user_profile.id])
|
send_event(event, [user_profile.id])
|
||||||
|
|
||||||
|
def do_mark_hotspot_as_read(user, hotspot):
|
||||||
|
# type: (UserProfile, str) -> None
|
||||||
|
UserHotspot.objects.get_or_create(user=user, hotspot=hotspot)
|
||||||
|
event = dict(type="hotspots", hotspots=get_next_hotspots(user))
|
||||||
|
send_event(event, [user.id])
|
||||||
|
|
||||||
def notify_realm_filters(realm):
|
def notify_realm_filters(realm):
|
||||||
# type: (Realm) -> None
|
# type: (Realm) -> None
|
||||||
realm_filters = realm_filters_for_realm(realm.id)
|
realm_filters = realm_filters_for_realm(realm.id)
|
||||||
|
|
|
@ -20,6 +20,7 @@ session_engine = import_module(settings.SESSION_ENGINE)
|
||||||
from zerver.lib.alert_words import user_alert_words
|
from zerver.lib.alert_words import user_alert_words
|
||||||
from zerver.lib.attachments import user_attachments
|
from zerver.lib.attachments import user_attachments
|
||||||
from zerver.lib.avatar import get_avatar_url
|
from zerver.lib.avatar import get_avatar_url
|
||||||
|
from zerver.lib.hotspots import get_next_hotspots
|
||||||
from zerver.lib.narrow import check_supported_events_narrow_filter
|
from zerver.lib.narrow import check_supported_events_narrow_filter
|
||||||
from zerver.lib.realm_icon import realm_icon_url
|
from zerver.lib.realm_icon import realm_icon_url
|
||||||
from zerver.lib.request import JsonableError
|
from zerver.lib.request import JsonableError
|
||||||
|
@ -72,6 +73,9 @@ def fetch_initial_state_data(user_profile, event_types, queue_id,
|
||||||
if want('attachments'):
|
if want('attachments'):
|
||||||
state['attachments'] = user_attachments(user_profile)
|
state['attachments'] = user_attachments(user_profile)
|
||||||
|
|
||||||
|
if want('hotspots'):
|
||||||
|
state['hotspots'] = get_next_hotspots(user_profile)
|
||||||
|
|
||||||
if want('message'):
|
if want('message'):
|
||||||
# The client should use get_messages() to fetch messages
|
# The client should use get_messages() to fetch messages
|
||||||
# starting with the max_message_id. They will get messages
|
# starting with the max_message_id. They will get messages
|
||||||
|
@ -181,6 +185,8 @@ def apply_event(state, event, user_profile, include_subscribers):
|
||||||
# type: (Dict[str, Any], Dict[str, Any], UserProfile, bool) -> None
|
# type: (Dict[str, Any], Dict[str, Any], UserProfile, bool) -> None
|
||||||
if event['type'] == "message":
|
if event['type'] == "message":
|
||||||
state['max_message_id'] = max(state['max_message_id'], event['message']['id'])
|
state['max_message_id'] = max(state['max_message_id'], event['message']['id'])
|
||||||
|
elif event['type'] == "hotspots":
|
||||||
|
state['hotspots'] = event['hotspots']
|
||||||
elif event['type'] == "pointer":
|
elif event['type'] == "pointer":
|
||||||
state['pointer'] = max(state['pointer'], event['pointer'])
|
state['pointer'] = max(state['pointer'], event['pointer'])
|
||||||
elif event['type'] == "realm_user":
|
elif event['type'] == "realm_user":
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
from zerver.models import UserProfile, UserHotspot
|
||||||
|
|
||||||
|
from typing import List, Text
|
||||||
|
|
||||||
|
ALL_HOTSPOTS = ['welcome', 'streams', 'topics', 'narrowing', 'replying', 'get_started']
|
||||||
|
def get_next_hotspots(user):
|
||||||
|
# type: (UserProfile) -> List[Text]
|
||||||
|
seen_hotspots = frozenset(UserHotspot.objects.filter(user=user).values_list('hotspot', flat=True))
|
||||||
|
for hotspot in ALL_HOTSPOTS:
|
||||||
|
if hotspot not in seen_hotspots:
|
||||||
|
return [hotspot]
|
||||||
|
return []
|
|
@ -0,0 +1,31 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-03-28 00:22
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('zerver', '0069_realmauditlog_extra_data'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserHotspot',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('hotspot', models.CharField(max_length=30)),
|
||||||
|
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='userhotspot',
|
||||||
|
unique_together=set([('user', 'hotspot')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1553,3 +1553,11 @@ class RealmAuditLog(models.Model):
|
||||||
event_time = models.DateTimeField() # type: datetime.datetime
|
event_time = models.DateTimeField() # type: datetime.datetime
|
||||||
backfilled = models.BooleanField(default=False) # type: bool
|
backfilled = models.BooleanField(default=False) # type: bool
|
||||||
extra_data = models.TextField(null=True) # type: Text
|
extra_data = models.TextField(null=True) # type: Text
|
||||||
|
|
||||||
|
class UserHotspot(models.Model):
|
||||||
|
user = models.ForeignKey(UserProfile) # type: UserProfile
|
||||||
|
hotspot = models.CharField(max_length=30) # type: Text
|
||||||
|
timestamp = models.DateTimeField(default=timezone.now) # type: datetime.datetime
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
unique_together = ("user", "hotspot")
|
||||||
|
|
|
@ -38,6 +38,7 @@ from zerver.lib.actions import (
|
||||||
do_create_user,
|
do_create_user,
|
||||||
do_deactivate_stream,
|
do_deactivate_stream,
|
||||||
do_deactivate_user,
|
do_deactivate_user,
|
||||||
|
do_mark_hotspot_as_read,
|
||||||
do_reactivate_user,
|
do_reactivate_user,
|
||||||
do_refer_friend,
|
do_refer_friend,
|
||||||
do_regenerate_api_key,
|
do_regenerate_api_key,
|
||||||
|
@ -1451,6 +1452,16 @@ class EventsRegisterTest(ZulipTestCase):
|
||||||
error = bot_reactivate_checker('events[1]', events[1])
|
error = bot_reactivate_checker('events[1]', events[1])
|
||||||
self.assert_on_error(error)
|
self.assert_on_error(error)
|
||||||
|
|
||||||
|
def test_do_mark_hotspot_as_read(self):
|
||||||
|
# type: () -> None
|
||||||
|
schema_checker = check_dict([
|
||||||
|
('type', equals('hotspots')),
|
||||||
|
('hotspots', check_list(check_string)),
|
||||||
|
])
|
||||||
|
events = self.do_test(lambda: do_mark_hotspot_as_read(self.user_profile, 'welcome'))
|
||||||
|
error = schema_checker('events[0]', events[0])
|
||||||
|
self.assert_on_error(error)
|
||||||
|
|
||||||
def test_rename_stream(self):
|
def test_rename_stream(self):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
stream = self.make_stream('old_name')
|
stream = self.make_stream('old_name')
|
||||||
|
|
|
@ -68,6 +68,7 @@ class HomeTest(ZulipTestCase):
|
||||||
"furthest_read_time",
|
"furthest_read_time",
|
||||||
"has_mobile_devices",
|
"has_mobile_devices",
|
||||||
"have_initial_messages",
|
"have_initial_messages",
|
||||||
|
"hotspots",
|
||||||
"initial_pointer",
|
"initial_pointer",
|
||||||
"initial_presences",
|
"initial_presences",
|
||||||
"initial_servertime",
|
"initial_servertime",
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from zerver.lib.actions import do_mark_hotspot_as_read
|
||||||
|
from zerver.lib.hotspots import ALL_HOTSPOTS, get_next_hotspots
|
||||||
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
|
from zerver.models import UserProfile, UserHotspot
|
||||||
|
from zerver.views.hotspots import mark_hotspot_as_read
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
import ujson
|
||||||
|
|
||||||
|
# Splitting this out, since I imagine this will eventually have most of the
|
||||||
|
# complicated hotspots logic.
|
||||||
|
class TestGetNextHotspots(ZulipTestCase):
|
||||||
|
def test_first_hotspot(self):
|
||||||
|
# type: () -> None
|
||||||
|
user = UserProfile.objects.get(email='hamlet@zulip.com')
|
||||||
|
self.assertEqual(get_next_hotspots(user), ['welcome'])
|
||||||
|
|
||||||
|
def test_some_done_some_not(self):
|
||||||
|
# type: () -> None
|
||||||
|
user = UserProfile.objects.get(email='hamlet@zulip.com')
|
||||||
|
do_mark_hotspot_as_read(user, 'welcome')
|
||||||
|
do_mark_hotspot_as_read(user, 'topics')
|
||||||
|
self.assertEqual(get_next_hotspots(user), ['streams'])
|
||||||
|
|
||||||
|
def test_all_done(self):
|
||||||
|
# type: () -> None
|
||||||
|
user = UserProfile.objects.get(email='hamlet@zulip.com')
|
||||||
|
for hotspot in ALL_HOTSPOTS:
|
||||||
|
do_mark_hotspot_as_read(user, hotspot)
|
||||||
|
self.assertEqual(get_next_hotspots(user), [])
|
||||||
|
|
||||||
|
class TestHotspots(ZulipTestCase):
|
||||||
|
def test_do_mark_hotspot_as_read(self):
|
||||||
|
# type: () -> None
|
||||||
|
user = UserProfile.objects.get(email='hamlet@zulip.com')
|
||||||
|
do_mark_hotspot_as_read(user, 'streams')
|
||||||
|
self.assertEqual(list(UserHotspot.objects.filter(user=user)
|
||||||
|
.values_list('hotspot', flat=True)), ['streams'])
|
||||||
|
|
||||||
|
def test_hotspots_url_endpoint(self):
|
||||||
|
# type: () -> None
|
||||||
|
email = 'hamlet@zulip.com'
|
||||||
|
user = UserProfile.objects.get(email=email)
|
||||||
|
self.login(email)
|
||||||
|
result = self.client_post('/json/users/me/hotspots',
|
||||||
|
{'hotspot': ujson.dumps('welcome')})
|
||||||
|
self.assert_json_success(result)
|
||||||
|
self.assertEqual(list(UserHotspot.objects.filter(user=user)
|
||||||
|
.values_list('hotspot', flat=True)), ['welcome'])
|
|
@ -283,6 +283,7 @@ def home_real(request):
|
||||||
'attachments',
|
'attachments',
|
||||||
'default_language',
|
'default_language',
|
||||||
'emoji_alt_code',
|
'emoji_alt_code',
|
||||||
|
'hotspots',
|
||||||
'last_event_id',
|
'last_event_id',
|
||||||
'left_side_userlist',
|
'left_side_userlist',
|
||||||
'max_icon_file_size',
|
'max_icon_file_size',
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from zerver.decorator import has_request_variables, REQ
|
||||||
|
from zerver.lib.actions import do_mark_hotspot_as_read
|
||||||
|
from zerver.lib.hotspots import ALL_HOTSPOTS
|
||||||
|
from zerver.lib.response import json_error, json_success
|
||||||
|
from zerver.lib.validator import check_string
|
||||||
|
from zerver.models import UserProfile
|
||||||
|
|
||||||
|
@has_request_variables
|
||||||
|
def mark_hotspot_as_read(request, user, hotspot=REQ(validator=check_string)):
|
||||||
|
# type: (HttpRequest, UserProfile, str) -> HttpResponse
|
||||||
|
if hotspot not in ALL_HOTSPOTS:
|
||||||
|
return json_error(_('Unknown hotspot: %s') % (hotspot,))
|
||||||
|
do_mark_hotspot_as_read(user, hotspot)
|
||||||
|
return json_success()
|
|
@ -895,6 +895,7 @@ JS_SPECS = {
|
||||||
'js/colorspace.js',
|
'js/colorspace.js',
|
||||||
'js/timerender.js',
|
'js/timerender.js',
|
||||||
'js/tutorial.js',
|
'js/tutorial.js',
|
||||||
|
'js/hotspots.js',
|
||||||
'js/templates.js',
|
'js/templates.js',
|
||||||
'js/upload_widget.js',
|
'js/upload_widget.js',
|
||||||
'js/avatar.js',
|
'js/avatar.js',
|
||||||
|
|
|
@ -298,6 +298,10 @@ v1_api_and_json_patterns = [
|
||||||
{'PUT': 'zerver.views.user_settings.set_avatar_backend',
|
{'PUT': 'zerver.views.user_settings.set_avatar_backend',
|
||||||
'DELETE': 'zerver.views.user_settings.delete_avatar_backend'}),
|
'DELETE': 'zerver.views.user_settings.delete_avatar_backend'}),
|
||||||
|
|
||||||
|
# users/me/hotspots -> zerver.views.hotspots
|
||||||
|
url(r'^users/me/hotspots$', rest_dispatch,
|
||||||
|
{'POST': 'zerver.views.hotspots.mark_hotspot_as_read'}),
|
||||||
|
|
||||||
# settings -> zerver.views.user_settings
|
# settings -> zerver.views.user_settings
|
||||||
url(r'^settings/display$', rest_dispatch,
|
url(r'^settings/display$', rest_dispatch,
|
||||||
{'PATCH': 'zerver.views.user_settings.update_display_settings_backend'}),
|
{'PATCH': 'zerver.views.user_settings.update_display_settings_backend'}),
|
||||||
|
|
Loading…
Reference in New Issue