Merge pull request #2638 from mozilla/hello-fte

[fix bug 1109132] Implement Firefox Hello FTUE
This commit is contained in:
Jon Petto 2015-01-12 10:52:52 -06:00
Родитель 023ddcb7b4 75e0337dc5
Коммит bf549cc18f
10 изменённых файлов: 682 добавлений и 3 удалений

Просмотреть файл

@ -0,0 +1,83 @@
{# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/. -#}
{% extends "/firefox/base-resp.html" %}
{% set_lang_files "firefox/hello" %}
{% block extra_meta %}<meta name="robots" content="noindex">{% endblock %}
{% block site_css %}
{{ css('firefox_hello_start') }}
{% endblock %}
{% block page_title_prefix %}{% endblock %}
{% block page_title %}{{ _('Firefox Hello') }}{% endblock %}
{% block page_title_suffix %}{% endblock %}
{% block body_id %}firefox-hello-start{% endblock %}
{% block body_class %}sky{% endblock %}
{% block string_data %}
data-get-started-title="{{ _('Welcome to Hello') }}"
data-get-started-text="{{ _('Open a video conversation with a single click, then invite a friend to join.') }}"
data-invite-title="{{ _('Share your link with a friend') }}"
data-invite-text="{{ _('They dont even need Firefox to join. They just need to click the link.') }}"
data-shared-title="{{ _('No need to wait around') }}"
data-shared-text="{{ _('Once your friend clicks the link, the Hello icon will change to blue, letting you know theyre there.') }}"
data-room-list-title="{{ _('You have a conversation waiting') }}"
data-room-list-text="{{ _('Click now to join and say hello.') }}"
{% endblock %}
{% block site_header %}{% endblock %}
{% block content %}
<div class="spacer"></div>
<main role="main" data-incoming-conversation="{{ incoming_conversation }}">
<h1>{{ high_res_img('img/firefox/hello/firefox-hello-logo.png?01-2015', {'alt': _('Firefox Hello'), 'width': '535', 'height': '88'}) }}</h1>
<section class="default">
<h2>{{ _('The easiest way to have a free video conversation with anyone, anywhere') }}</h2>
<ol>
<li>{{ _('Start a conversation right from Firefox') }}</li>
<li>{{ _('Invite a friend by sending them a link') }}</li>
<li>{{ _('The Hello icon will change to blue once theyve joined') }}</li>
</ol>
<aside>
<ul>
<li><a rel="external" href="https://support.mozilla.org/kb/firefox-hello-video-and-voice-conversations-online" class="more">{{ _('Need help? Get support here') }}</a></li>
<li><a href="{{ url('firefox.hello') }}" class="more">{{ _('Learn all about Hello') }}</a></li>
</ul>
<ul>
<li><a rel="external" href="https://support.mozilla.org/kb/create-and-manage-your-contacts-list-firefox-hello" class="more">{{ _('Do more with a Firefox Account') }}</a></li>
{# Removing link for now, as per Bug 1109132#c14
<li><a href="#" class="more">{{ _('Tell a friend about Hello') }}</a></li>
#}
</ul>
</aside>
</section>
<section class="start">
<h2>{{ _('The easiest way to connect for free over video') }}</h2>
<p>{{ _('Start right from Firefox and invite anyone, anywhere to have a conversation. All they have to do is click a link to join. Theres no account or sign-in required.') }}</p>
</section>
<section class="end">
<h2>{{ _('Youve started your first conversation. Wasnt that easy?') }}</h2>
<ul>
{# Removing link for now, as per Bug 1109132#c14
<li><a href="#" class="more">{{ _('Tell a friend about Hello') }}</a></li>
#}
<li><a rel="external" href="https://support.mozilla.org/kb/firefox-hello-video-and-voice-conversations-online" class="more">{{ _('Need help? Get support here') }}</a></li>
<li><a href="{{ url('firefox.hello') }}" class="more">{{ _('Learn all about Hello') }}</a></li>
<li><a rel="external" href="https://support.mozilla.org/kb/create-and-manage-your-contacts-list-firefox-hello" class="more">{{ _('Do more with a Firefox Account') }}</a></li>
</ul>
</section>
</main>
{% endblock %}
{% block email_form %}{% endblock %}
{% block site_footer %}{% endblock %}
{% block js %}
{{ js('firefox_hello_start') }}
{% endblock %}

Просмотреть файл

@ -1065,3 +1065,30 @@ class TestWhatsnewRedirect(FxVersionRedirectsMixin, TestCase):
# if there's no oldversion parameter, show no tour
response = self.client.get(self.url, HTTP_USER_AGENT=self.user_agent)
self.assertNotIn(self.expected, response.content)
class TestHelloStartView(TestCase):
def setUp(self):
self.user_agent = ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:35.0) '
'Gecko/20100101 Firefox/35.0')
self.url = reverse('firefox.hello.start', args=['35.0'])
def test_fx_hello_no_conversation(self):
"""Should identify when there is no conversation"""
response = self.client.get(self.url, HTTP_USER_AGENT=self.user_agent)
self.assertIn('data-incoming-conversation="none"', response.content)
def test_fx_hello_conversation_open(self):
"""Should identify when a conversation is open"""
response = self.client.get(self.url + '?incomingConversation=open',
HTTP_USER_AGENT=self.user_agent)
self.assertIn('data-incoming-conversation="open"', response.content)
def test_fx_hello_conversation_waiting(self):
"""Should identify when a conversation is waiting"""
response = self.client.get(self.url + '?incomingConversation=waiting',
HTTP_USER_AGENT=self.user_agent)
self.assertIn('data-incoming-conversation="waiting"', response.content)

Просмотреть файл

@ -18,6 +18,7 @@ latest_re = r'^firefox(?:/(?P<version>%s))?/%s/$'
firstrun_re = latest_re % (version_re, 'firstrun')
whatsnew_re = latest_re % (version_re, 'whatsnew')
tour_re = latest_re % (version_re, 'tour')
hello_start_re = latest_re % (version_re, 'hello/start')
product_re = '(?P<product>firefox|mobile)'
channel_re = '(?P<channel>beta|aurora|developer|organizations)'
releasenotes_re = latest_re % (version_re, r'(aurora|release)notes')
@ -65,6 +66,7 @@ urlpatterns = patterns('',
url(firstrun_re, views.FirstrunView.as_view(), name='firefox.firstrun'),
url(whatsnew_re, views.WhatsnewView.as_view(), name='firefox.whatsnew'),
url(tour_re, views.TourView.as_view(), name='firefox.tour'),
url(hello_start_re, views.HelloStartView.as_view(), name='firefox.hello.start'),
url(r'^firefox/partners/$', views.firefox_partners,
name='firefox.partners.index'),

Просмотреть файл

@ -495,3 +495,14 @@ def hello(request):
}
return l10n_utils.render(request, 'firefox/hello.html', {'video_url': videos.get(request.locale, '')})
class HelloStartView(LatestFxView):
template_name = 'firefox/hello/start.html'
def get_context_data(self, **kwargs):
ctx = super(HelloStartView, self).get_context_data(**kwargs)
incoming = self.request.GET.get('incomingConversation') or 'none'
ctx['incoming_conversation'] = incoming
return ctx

Просмотреть файл

@ -304,6 +304,10 @@ MINIFY_BUNDLES = {
'css/firefox/menu-resp.less',
'css/firefox/developer.less',
),
'firefox_hello_start': (
'css/sandstone/sandstone-resp.less',
'css/firefox/hello/start.less',
),
'firefox_new': (
'css/sandstone/sandstone-resp.less',
'css/firefox/template-resp.less',
@ -742,6 +746,10 @@ MINIFY_BUNDLES = {
'js/base/mozilla-modal.js',
'js/firefox/dev-firstrun.js',
),
'firefox_hello_start': (
'js/firefox/australis/australis-uitour.js',
'js/firefox/hello/start.js',
),
'firefox_new': (
'js/libs/socialshare.min.js',
'js/libs/modernizr.custom.csstransitions.js',

Просмотреть файл

@ -871,9 +871,8 @@ RewriteRule ^/hacking/notification/acceptance-email.txt$ https://static.mozilla.
# bug 1071959
RewriteRule ^/(\w{2,3}(?:-\w{2})?/)?firefox/independent(/?.*)$ /b/$1firefox/independent$2 [PT]
# bug 1093985, 1105664, remove this once the Firefox Hello product page is ready
RewriteRule ^/(\w{2,3}(?:-\w{2})?/)?firefox/hello(/?.*)$ https://support.mozilla.org/kb/respond-firefox-hello-invitation-guest-mode [L,R=temp]
RewriteRule ^/(\w{2,3}(?:-\w{2})?/)?firefox/([3-9]\d\.\d(?:a1|a2|beta|\.\d)?)/hello/start/?$ https://support.mozilla.org/kb/firefox-hello-make-and-receive-calls-guest-mode [L,R=temp]
# bug 1109132
RewriteRule ^/(\w{2,3}(?:-\w{2})?/)?firefox(/(?:\d+\.\d+\.?(?:\d+)?\.?(?:\d+)?(?:[a|b]?)(?:\d*)(?:pre)?(?:\d)?))?/hello/start(/?)$ /b/$1firefox$2/hello/start$3 [PT]
# bug 1088752
RewriteRule ^/(\w{2,3}(?:-\w{2})?/)?shapeoftheweb(/?.*)$ /b/$1shapeoftheweb$2 [PT]

Просмотреть файл

@ -0,0 +1,185 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
@import "../../sandstone/lib.less";
html,
body {
height: 100%;
}
h1 {
margin: (@baseLine * 14) @baseLine @baseLine;
img {
max-width: 100%;
height: auto;
}
}
h2 {
margin: @baseLine;
.font-size(48px);
}
p {
margin: @baseLine;
.open-sans-light;
.font-size(20px);
}
ol {
list-style-type: none;
margin: @baseLine;
padding: 0;
.open-sans-light;
.font-size(24px);
font-style: italic;
counter-reset: steps-counter;
li {
position: relative;
margin: (@baseLine / 2) 0;
line-height: 1.5;
padding-left: 2em;
&:before {
position: absolute;
top: 0;
left: 0;
content: counter(steps-counter);
counter-increment: steps-counter;
float: left;
width: 1.5em;
height: 1.5em;
color: #fff;
background: #00A9DC;
border-radius: 100%;
text-align: center;
font-style: normal;
.open-sans;
}
}
}
ul {
list-style-type: none;
margin: @baseLine;
padding: 0;
.font-size(@largeFontSize);
li {
margin: (@baseLine / 2) 0;
padding: 0;
}
}
main {
width: 70%;
margin-bottom: @baseLine * 4;
}
.js main {
visibility: hidden;
}
.spacer {
width: 30%;
height: 700px;
float: right;
}
aside {
position: relative;
padding: @baseLine;
.clearfix();
ul {
float: left;
width: 50%;
margin: 0;
}
}
#outer-wrapper {
min-height: 100%;
border: none;
padding: 0;
margin: 0;
}
#wrapper {
height: 100%;
overflow: hidden;
padding: 0;
}
.start,
.end {
display: none;
}
.html-rtl {
.spacer {
float: left;
}
ol li {
padding-left: 0;
padding-right: 2em;
&:before {
left: auto;
right: 0;
}
}
aside ul {
float: right;
}
}
@media only screen and (min-width: @breakTablet) and (max-width: @breakDesktop) {
main {
width: 65%;
}
.spacer {
width: 35%;
}
}
@media only screen and (max-width: @breakTablet) {
main {
width: 100%;
}
.spacer {
display: none;
float: none;
}
h2 {
.font-size(32px);
}
p {
.font-size(16px);
}
aside ul {
float: none;
width: 100%;
}
}
@media only screen and (max-height: 600px) {
h2 {
.font-size(32px);
}
p {
.font-size(18px);
}
}

Двоичные данные
media/img/firefox/hello/firefox-hello-logo-high-res.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 18 KiB

Двоичные данные
media/img/firefox/hello/firefox-hello-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 8.7 KiB

Просмотреть файл

@ -0,0 +1,364 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
;(function($, Mozilla) {
'use strict';
var $main = $('main');
var $defaultCopy = $('.default');
var $startCopy = $('.start');
var $endCopy = $('.end');
var highlightTimeout;
var highlightsSupressed = false;
var tourStep = 'default';
/*
* Strips HTML from string to make sure markup
* does not get injected in any UITour door-hangers.
* @param string (data attribute string)
*/
function _getText(string) {
return $('<div/>').html(window.trans(string)).text();
}
// GA event for successful Hello conversations
function trackGAConversationConnect() {
gaTrack(['_trackEvent', '/hello/start interactions', 'tour', 'TourConnectConversation']);
}
/*
* Tells Firefox to re-connect to the FTE tour when user has their first conversation.
* We do this only once the user has interacted with the FTE.
*/
function resumeTourOnFirstJoin() {
Mozilla.UITour.setConfiguration('Loop:ResumeTourOnFirstJoin', true);
}
// Close any open menus and hide info panels
function hideUITourHighlights() {
Mozilla.UITour.hideInfo();
Mozilla.UITour.hideMenu('loop');
}
// hide and reshow tour highlights on tab visibility
function handleVisibilityChange() {
if (document.hidden) {
hideUITourHighlights();
unbindHelloEvents();
} else {
clearTimeout(highlightTimeout);
bindHelloEvents();
highlightTimeout = setTimeout(showTourStep, 900);
}
}
// hide and reshow tour highlights on page resize
function handleResize() {
clearTimeout(highlightTimeout);
if (!highlightsSupressed) {
hideUITourHighlights();
highlightsSupressed = true;
}
highlightTimeout = setTimeout(function() {
highlightsSupressed = false;
showTourStep();
}, 300);
}
// Shows the current step of the FTE tour
function showTourStep() {
switch(tourStep) {
case 'get-started':
showHelloPanel(highlightNewRoomButton);
break;
case 'invite':
targetConversationView(highlightSelectedRoomButtons);
break;
case 'shared':
targetConversationView(showSharedInfoPanel);
break;
case 'conversation-waiting':
showHelloPanel(highlightRoomList);
break;
case 'conversation-open':
showPageState('end', true);
break;
}
}
/*
* Show FTE web page copy state
* @param state (string)
* @param anim (boolean)
*/
function showPageState(state, anim) {
$defaultCopy.hide();
if (state && state === 'start') {
$startCopy.show();
$endCopy.hide();
} else if (state && state === 'end') {
if (anim) {
$startCopy.stop().fadeOut('fast', function () {
$endCopy.stop().fadeIn('fast');
});
} else {
$startCopy.hide();
$endCopy.show();
}
} else {
$startCopy.hide();
$endCopy.hide();
$defaultCopy.show();
}
}
/*
* Show the Hello panel and trigger callback once opening animation finishes
* @param callback (function)
*/
function showHelloPanel(callback) {
// Only open the info panel if tab is visible
if (document.hidden) {
return;
}
// Make sure loop icon is available before opening Hello panel (bug 1111828)
Mozilla.UITour.getConfiguration('availableTargets', function (config) {
if (config.targets && $.inArray('loop', config.targets) !== -1) {
Mozilla.UITour.showMenu('loop', function() {
if (typeof callback === 'function') {
callback();
}
});
}
});
}
// Highlights Hello panel target.
function highlightNewRoomButton() {
Mozilla.UITour.showInfo(
'loop-newRoom',
_getText('getStartedTitle'),
_getText('getStartedText')
);
}
// Highlights Hello room list when a conversation is waiting.
function highlightRoomList() {
Mozilla.UITour.showInfo(
'loop-roomList',
_getText('roomListTitle'),
_getText('roomListText')
);
}
/*
* Determine if conversation view and room button targets are visible
* @param callback (function) to excecute if target is available
*/
function targetConversationView(callback) {
// Only open the info panel if tab is visible
if (document.hidden) {
return;
}
Mozilla.UITour.getConfiguration('availableTargets', function (config) {
if (config.targets && $.inArray('loop-selectedRoomButtons', config.targets) !== -1) {
if (typeof callback === 'function') {
callback();
}
}
});
}
// Add door-hanger to selected room buttons in conversation view
function highlightSelectedRoomButtons() {
Mozilla.UITour.showInfo(
'loop-selectedRoomButtons',
_getText('inviteTitle'),
_getText('inviteText')
);
}
// Add door-hanger to conversation view once room has been copied/shared
function showSharedInfoPanel() {
Mozilla.UITour.showInfo(
'loop-selectedRoomButtons',
_getText('sharedTitle'),
_getText('sharedText')
);
}
// register for Hello UITour events
function bindHelloEvents() {
Mozilla.UITour.observe(function(event, data) {
switch(event) {
case 'Loop:ChatWindowOpened':
// only show invite step if conversation is not waiting
if (tourStep === 'get-started') {
tourStep = 'invite';
showTourStep();
// User has interacted with FTE, so tell Firefox to resume
// when they have their first conversation.
resumeTourOnFirstJoin();
// track user has clicked "Start a conversation" button
gaTrack(['_trackEvent', '/hello/start interactions', 'tour', 'StartConversation-Tour']);
} if (tourStep === 'invite') {
showTourStep();
} else if (tourStep === 'conversation-waiting') {
tourStep = 'conversation-open';
showTourStep();
// set param to done, so if the user shares the URL
// the next person can still take the tour from the start.
replaceURLState('waiting', 'done');
// track conversation connect
trackGAConversationConnect();
}
break;
case 'Loop:ChatWindowClosed':
hideUITourHighlights();
break;
case 'Loop:ChatWindowHidden':
hideUITourHighlights();
break;
case 'Loop:ChatWindowDetached':
hideUITourHighlights();
break;
case 'Loop:IncomingConversation':
if (data && data.conversationOpen === true) {
tourStep = 'conversation-open';
hideUITourHighlights();
showPageState('end', true);
// track conversation connect
trackGAConversationConnect();
} else if (data && data.conversationOpen === false) {
tourStep = 'conversation-waiting';
showHelloPanel(highlightRoomList);
}
break;
case 'Loop:RoomURLCopied':
tourStep = 'shared';
showTourStep();
// track user has clicked copy button
gaTrack(['_trackEvent', '/hello/start interactions', 'tour', 'URLCopied-Tour']);
break;
case 'Loop:RoomURLEmailed':
tourStep = 'shared';
showTourStep();
// track user has clicked email button
gaTrack(['_trackEvent', '/hello/start interactions', 'tour', 'URLEmailed-Tour']);
}
}, function () {
// ping callback, nothing to actually do here!
});
}
// Stop listening for UITour Hello events
function unbindHelloEvents() {
Mozilla.UITour.observe(null);
}
/*
* Sets incomingConversation URL param to 'done' using replaceState
* @param currentValue to be replaced (string)
* @param newValue (string)
*/
function replaceURLState(currentValue, newValue) {
var url = window.location.href;
var currentParam;
if (!currentValue || !newValue) {
return;
}
currentParam = 'incomingConversation=' + currentValue;
if (url.indexOf(currentParam) !== -1) {
url = url.replace(currentParam, 'incomingConversation=' + newValue);
window.history.replaceState({}, '', url);
}
}
function init() {
// URL query param populated at template level in the view
var incomingConversation = $main.data('incomingConversation');
// set the tour state based on incomingConversation status
switch(incomingConversation) {
case 'none':
tourStep = 'get-started';
break;
case 'done':
tourStep = 'get-started';
break;
case 'waiting':
tourStep = 'conversation-waiting';
break;
case 'open':
tourStep = 'conversation-open';
break;
}
// Make sure that the Hello icon is an available target in the UI.
// Hello is still referred to as 'loop' internally for legacy reasons.
Mozilla.UITour.getConfiguration('availableTargets', function (config) {
if (config.targets && $.inArray('loop', config.targets) !== -1) {
// hide and reshow uitour highlights on resize
$(window).on('resize', handleResize);
// hide and reshow uitour highlights page visibility
$(document).on('visibilitychange', handleVisibilityChange);
if (tourStep === 'conversation-open') {
// conversation is open and rooms view is visible,
// so show the page end state. Job done! \o/
showPageState('end');
// set incomingConversation URL param to 'done'
replaceURLState('open', 'done');
// track conversation connect
trackGAConversationConnect();
} else {
// show the first page copy state
showPageState('start');
// track start of tour in GA
if (tourStep === 'get-started') {
gaTrack(['_trackEvent', '/hello/start interactions', 'tour', 'GetStarted']);
}
}
// register for Hello events
bindHelloEvents();
// show tour step highlight
showTourStep();
}
});
}
// use a slight delay for showing the main page content
// to allow for initial page state to be determined
setTimeout(function () {
$main.css('visibility', 'visible');
}, 500);
// FTE will only run on Firefox Desktop 35 and above
if (window.isFirefox() && !window.isFirefoxMobile() && window.getFirefoxMasterVersion() >= 35) {
init();
}
})(window.jQuery, window.Mozilla);