Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
564
gaia/apps/sms/views/conversation/index.html
Executable file
|
|
@ -0,0 +1,564 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<meta charset="utf-8">
|
||||
<meta name="theme-group" content="theme-communications">
|
||||
<meta name="theme-color" content="var(--header-background)">
|
||||
<title>Messages</title>
|
||||
<!--App Style root.css-->
|
||||
<link rel="stylesheet" type="text/css" href="/views/shared/style/root.css">
|
||||
<!-- Stable Building blocks -->
|
||||
<link rel="stylesheet" type="text/css" href="/shared/style/animations.css">
|
||||
<link rel="stylesheet" type="text/css" href="/shared/style/action_menu.css">
|
||||
<link rel="stylesheet" type="text/css" href="/shared/style/option_menu.css">
|
||||
<link rel="stylesheet" type="text/css" href="/shared/style/buttons.css">
|
||||
<link rel="stylesheet" type="text/css" href="/shared/style/confirm.css">
|
||||
<link rel="stylesheet" type="text/css" href="/shared/style/edit_mode.css">
|
||||
<link rel="stylesheet" type="text/css" href="/shared/style/input_areas.css">
|
||||
<link rel="stylesheet" type="text/css" href="/shared/style/switches.css">
|
||||
<link rel="stylesheet" type="text/css" href="/shared/style/lists.css">
|
||||
<link rel="stylesheet" type="text/css" href="/shared/style/progress_activity.css">
|
||||
<link rel="stylesheet" type="text/css" href="/shared/style/status.css">
|
||||
<link rel="stylesheet" type="text/css" href="/shared/style/tabs.css" >
|
||||
<link rel="stylesheet" type="text/css" href="/shared/elements/gaia-icons/gaia-icons.css">
|
||||
<!-- App styles -->
|
||||
<link rel="stylesheet" type="text/css" href="/views/shared/style/sms.css">
|
||||
<link rel="stylesheet" type="text/css" href="/views/shared/style/panel.css">
|
||||
<link rel="stylesheet" type="text/css" href="/views/shared/style/edit-mode.css">
|
||||
<link rel="stylesheet" type="text/css" href="/views/shared/style/notification.css">
|
||||
<link rel="stylesheet" type="text/css" href="/views/shared/style/common.css">
|
||||
<link rel="stylesheet" type="text/css" href="/views/shared/style/composer.css">
|
||||
<link rel="stylesheet" type="text/css" href="/views/conversation/style/message.css">
|
||||
<link rel="stylesheet" type="text/css" href="/views/conversation/style/report_view.css">
|
||||
<link rel="stylesheet" type="text/css" href="/views/conversation/style/attachment.css">
|
||||
<link rel="stylesheet" type="text/css" href="/views/conversation/style/conversation.css">
|
||||
<link rel="stylesheet" type="text/css" href="/views/conversation/style/edit-mode.css">
|
||||
<link rel="stylesheet" type="text/css" href="/views/new_message/style/new_message.css">
|
||||
<link rel="stylesheet" type="text/css" href="/views/new_message/style/recipients.css">
|
||||
<!-- Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/locales/sms.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/date/date.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/intl/unitformat.{locale}.properties">
|
||||
<script defer src="/mocks/navigator_moz_icc_manager.js"></script>
|
||||
<!-- Firefox Desktop shims -->
|
||||
<script defer src="/desktop-mock/navigator_moz_system_messages.js"></script>
|
||||
<script defer src="/desktop-mock/navigator_moz_settings.js"></script>
|
||||
<script defer src="/desktop-mock/navigator_moz_mobilemessage.js"></script>
|
||||
<script defer src="/desktop-mock/navigator_moz_contacts.js"></script>
|
||||
<!-- Web Components -->
|
||||
<script defer src="/shared/elements/config.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css" />
|
||||
<script async src="/shared/elements/gaia-header/dist/gaia-header.js"></script>
|
||||
<link rel="localization" href="/shared/elements/gaia_menu/locales/gaia_menu.{locale}.properties">
|
||||
<link rel="localization" href="/shared/elements/gaia_sim_picker/locales/gaia_sim_picker.{locale}.properties">
|
||||
<!--
|
||||
<script defer src="/shared/js/component_utils.js"></script>
|
||||
<script defer src="/shared/elements/gaia_menu/script.js"></script>
|
||||
<script defer src="/shared/elements/gaia_sim_picker/script.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/shared/elements/gaia_menu/style.css">
|
||||
<link rel="stylesheet" type="text/css" href="/shared/elements/gaia_sim_picker/style.css">
|
||||
-->
|
||||
<!-- Shared code -->
|
||||
<script defer src="/shared/js/intl/l20n.js"></script>
|
||||
<!-- Should be removed while gecko support navigator.mozHour12 API -->
|
||||
<script defer src="/shared/js/date_time_helper.js"></script>
|
||||
<script defer src="/shared/js/template.js"></script>
|
||||
<script defer src="/shared/js/async_storage.js"></script>
|
||||
<script defer src="/shared/js/lazy_loader.js"></script>
|
||||
<script defer src="/shared/js/contact_photo_helper.js"></script>
|
||||
<script defer src="/shared/js/fb/fb_request.js"></script>
|
||||
<script defer src="/shared/js/fb/fb_data_reader.js"></script>
|
||||
<script defer src="/shared/js/fb/fb_reader_utils.js"></script>
|
||||
<script defer src="/shared/js/event_dispatcher.js"></script>
|
||||
<script defer src="/views/shared/js/utils.js"></script>
|
||||
<script defer src="/views/shared/js/navigation.js"></script>
|
||||
<script defer src="/views/shared/js/inter_instance_event_dispatcher.js"></script>
|
||||
<script defer src="/views/shared/js/time_headers.js"></script>
|
||||
<script defer src="/views/shared/js/contacts.js"></script>
|
||||
<script defer src="/services/js/drafts.js"></script>
|
||||
<script defer src="/services/js/threads.js"></script>
|
||||
<script defer src="/views/shared/js/settings.js"></script>
|
||||
<script defer src="/services/js/message_manager.js"></script>
|
||||
<script defer src="/views/shared/js/app.js"></script>
|
||||
<script defer src="/shared/js/multi_sim_action_button.js"></script>
|
||||
<script defer src="/views/shared/js/task_runner.js"></script>
|
||||
<script defer src="/views/shared/js/wbmp.js"></script>
|
||||
<script defer src="/views/shared/js/smil.js"></script>
|
||||
<script defer src="/views/shared/js/shared_components.js"></script>
|
||||
<script defer src="/views/conversation/js/link_helper.js"></script>
|
||||
<script defer src="/shared/js/image_utils.js"></script>
|
||||
<script defer src="/views/conversation/js/attachment.js"></script>
|
||||
<script defer src="/views/conversation/js/attachment_renderer.js"></script>
|
||||
<script defer src="/views/conversation/js/subject_composer.js"></script>
|
||||
<script defer src="/views/conversation/js/compose.js"></script>
|
||||
<script defer src="/shared/js/gesture_detector.js"></script>
|
||||
<script defer src="/lib/bridge/client.js"></script>
|
||||
<script defer src="/services/js/activity/activity_client.js"></script>
|
||||
<script defer src="/views/conversation/js/recipients.js"></script>
|
||||
<script defer src="/views/conversation/js/conversation.js"></script>
|
||||
<script defer src="/views/conversation/js/startup.js"></script>
|
||||
<script defer src="/views/shared/js/bootstrap.js"></script>
|
||||
<!-- Lazy load scripts
|
||||
<script defer src="/shared/js/settings_url.js"></script>
|
||||
<script defer src="/shared/js/mime_mapper.js"></script>
|
||||
<script defer src="/shared/js/mobile_operator.js"></script>
|
||||
<script defer src="/shared/js/settings_listener.js"></script>
|
||||
<script defer src="/shared/js/option_menu.js"></script>
|
||||
<script defer src="/views/shared/js/errors.js"></script>
|
||||
<script defer src="/views/shared/js/shared_components.js"></script>
|
||||
<script defer src="/views/shared/js/dialog.js"></script>
|
||||
<script defer src="/views/shared/js/error_dialog.js"></script>
|
||||
<script defer src="/views/shared/js/waiting_screen.js"></script>
|
||||
<script defer src="/views/shared/js/activity_picker.js"></script>
|
||||
<script defer src="/views/conversation/js/link_action_handler.js"></script>
|
||||
<script defer src="/views/shared/js/contact_renderer.js"></script>
|
||||
<script defer src="/views/shared/js/localization_helper.js"></script>
|
||||
<script defer src="/views/conversation/js/information.js"></script>
|
||||
<script defer src="/services/js/messaging/messaging_client.js"></script>
|
||||
<script defer src="/services/js/moz_mobile_connections/moz_mobile_connections_client.js"></script>
|
||||
-->
|
||||
</head>
|
||||
|
||||
<body role="application" class="skin-comms theme-communications">
|
||||
<iframe src="/views/shared/shim_host.html" class="shim-host" hidden></iframe>
|
||||
<article id="main-wrapper" class="wrapper">
|
||||
<section class="panel panel-ConversationView editable-select-mode" role="region" aria-hidden="true">
|
||||
<gaia-header id="messages-header" class="view-header regular-header" action="back" no-font-fit>
|
||||
<h1 id="messages-header-text"></h1>
|
||||
<button id="messages-call-number-button" data-icon="call" class="hide" data-l10n-id="callButton"></button>
|
||||
<button id="messages-options-button" data-icon="more" data-l10n-id="actionsButton"></button>
|
||||
</gaia-header>
|
||||
<div id="messages-subheader" class="subheader">
|
||||
<section id="messages-to-field">
|
||||
<section data-l10n-id="to" id="to-label">
|
||||
To:
|
||||
</section>
|
||||
<!-- When overflow occurs in the recipients list, scrolling is
|
||||
commonly triggered automatically through focus and keyboard events.
|
||||
In these cases, white space described by CSS `padding-bottom` will
|
||||
not be automatically scrolled into view (because the element will
|
||||
only scroll far enough to bring the cursor into view). A container
|
||||
element is necessary to consistently render this vertical space
|
||||
(instead realized with CSS `margin-bottom`). -->
|
||||
<div id="messages-recipients-list-container" class="bb-editable"
|
||||
role="group" aria-labelledby="to-label">
|
||||
<section id="messages-recipients-list" class="singleline">
|
||||
</section>
|
||||
</div>
|
||||
<menu type="toolbar">
|
||||
<a id="messages-contact-pick-button">
|
||||
<span class="icon icon-user"></span>
|
||||
</a>
|
||||
</menu>
|
||||
</section>
|
||||
<div id="contact-carrier" class="carrier-wrapper"></div>
|
||||
<!-- Notifications in this container will be rendered on top of the
|
||||
Compose field. These notifications should be hidden automatically so
|
||||
they do not prevent the user from inputting text. -->
|
||||
<div class="overlay">
|
||||
<section id="messages-subject-max-length-notice"
|
||||
role="notification"
|
||||
class="hide">
|
||||
<p data-l10n-id='messages-max-subject-length-text'></p>
|
||||
</section>
|
||||
<section id="messages-max-length-notice"
|
||||
role="notification"
|
||||
class="hide">
|
||||
<p></p>
|
||||
</section>
|
||||
<section id="messages-sms-counter-notice"
|
||||
role="notification"
|
||||
class="hide">
|
||||
<p></p>
|
||||
</section>
|
||||
<section id="messages-convert-notice"
|
||||
role="notification"
|
||||
class="hide">
|
||||
<p></p>
|
||||
</section>
|
||||
<section id="messages-resize-notice"
|
||||
role="notification"
|
||||
class="hide">
|
||||
<p data-l10n-id="resize-image">Resizing the attached image</p>
|
||||
</section>
|
||||
<section id="messages-new-message-notice"
|
||||
role="notification"
|
||||
class="hide">
|
||||
<div class="text-container">
|
||||
<div data-l10n-id="new-message-notice-text">You have new messages</div>
|
||||
<div dir="auto" class="contact">Contact</div>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button class="notice-button" data-l10n-id="new-message-notice-a-text">read it</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<article id="composer-container" class="view-body">
|
||||
<div class="flex-composer">
|
||||
<div id="messages-container" data-type="list" class="article-list"></div>
|
||||
<form id="messages-compose-form" role="search" class="bottom" data-message-type="sms">
|
||||
<section class="mms-label-line">
|
||||
<section class="subject-composer js-subject-composer hide">
|
||||
<div contentEditable="true" class="subject-composer-input"
|
||||
role="textbox"
|
||||
aria-multiline="false">
|
||||
</div>
|
||||
<div class="subject-composer-placeholder"
|
||||
data-l10n-id="messagesSubjectInput_placeholder">
|
||||
</div>
|
||||
</section>
|
||||
<span class="mms-label"
|
||||
data-l10n-id="mms-label">MMS</span>
|
||||
</section>
|
||||
<section class="content-composer">
|
||||
<div class="messages-attach-container">
|
||||
<button id="messages-attach-button" aria-label="Add attachment" data-l10n-id="attach-button" type="button"></button>
|
||||
</div>
|
||||
<div class="composer-message-input-container">
|
||||
<div id="messages-input" contentEditable="true" name="message"
|
||||
x-inputmode="-moz-sms" data-l10n-id="messagesInput"
|
||||
role="textbox" aria-multiline="true">
|
||||
</div>
|
||||
</div>
|
||||
<div class="composer-button-container">
|
||||
<span class="message-counter js-message-counter"></span>
|
||||
<button id="messages-send-button" class="icon" aria-label="Send" data-l10n-id="send-button" type="submit">
|
||||
<div class="js-sim-indication sim-indication" data-l10n-id="send-button-sim-indication"></div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<form role="dialog" id="messages-edit-form" data-type="edit" >
|
||||
<section role="region" class="theme-settings">
|
||||
<gaia-header id="messages-edit-header" action="close" no-font-fit>
|
||||
<h1 id="messages-edit-mode" data-l10n-id="deleteMessages-title"></h1>
|
||||
<button id="messages-check-uncheck-all-button" class="edit-button" data-l10n-id="select-all"></button>
|
||||
</gaia-header>
|
||||
</section>
|
||||
<div class="bottom-tab" role="region" >
|
||||
<ul class="bb-tablist">
|
||||
<li role="presentation">
|
||||
<button id="messages-delete-button" class="disable-when-dialog-animates" data-action="delete-button"></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
<section id="messages-recipient-suggestions" class="article-list hide" data-type="list">
|
||||
<ul class="contact-list recipient-suggestions-list" role="listbox"></ul>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-GroupView" role="region" aria-hidden="true">
|
||||
<gaia-header id="information-group-header" class="header" action="back" no-font-fit>
|
||||
<h1 data-l10n-id="participant" class="header-text"></h1>
|
||||
</gaia-header>
|
||||
<article class="view-body article-list" data-type="list">
|
||||
<ul class="contact-list"></ul>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-ReportView" role="region" aria-hidden="true">
|
||||
<gaia-header id="information-report-header" class="header" action="close" no-font-fit>
|
||||
<h1 data-l10n-id="message-report"></h1>
|
||||
</gaia-header>
|
||||
<article class="view-body article-list report-view container" data-type="list">
|
||||
<div class="description">
|
||||
<p class="type"></p>
|
||||
<p class="subject">
|
||||
<span data-l10n-id="message-subject"></span>
|
||||
<span class="detail"></span>
|
||||
</p>
|
||||
<p class="sent-block">
|
||||
<span class="sent-title"></span>
|
||||
<span class="sent-timestamp l10n-contains-date detail"></span>
|
||||
</p>
|
||||
<p class="received-block">
|
||||
<span data-l10n-id="message-received"></span>
|
||||
<span class="received-timestamp l10n-contains-date detail"></span>
|
||||
</p>
|
||||
<p class="lateness-block">
|
||||
<span data-l10n-id="message-lateness"></span>
|
||||
<span class="lateness">
|
||||
<span class="lateness-month detail"></span>
|
||||
<span class="lateness-day detail"></span>
|
||||
<span class="lateness-hour detail"></span>
|
||||
<span class="lateness-minute detail"></span>
|
||||
<span class="lateness-second detail"></span>
|
||||
</span>
|
||||
</p>
|
||||
<p class="size-block">
|
||||
<span data-l10n-id="message-size-was"></span>
|
||||
<span class="size detail"></span>
|
||||
</p>
|
||||
<p class="sim-info hide">
|
||||
<span data-l10n-id="message-via"></span>
|
||||
<span class="sim-detail detail"></span>
|
||||
</p>
|
||||
</div>
|
||||
<span class="contact-title"></span>
|
||||
<ul class="contact-list"></ul>
|
||||
<button class="resend-btn" data-l10n-id="message-resend">
|
||||
</button>
|
||||
</article>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<article id="loading">
|
||||
<form role="dialog" data-type="confirm" class="loading-container">
|
||||
<section>
|
||||
<p class="loading-header" data-l10n-id="deleting-messages">Deleting messages...</p>
|
||||
<div role="status">
|
||||
<progress class="small"></progress>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<!--
|
||||
- Templates
|
||||
-->
|
||||
|
||||
<div id="contact-suggestion-tmpl" class="hide">
|
||||
<!--
|
||||
<li role="presentation">
|
||||
<a class="suggestion js-contact-info" role="option"
|
||||
data-number="${number}" data-source="contacts" data-name="${name}"
|
||||
data-type="${type}" data-carrier="${carrier}">
|
||||
${photoHTML}
|
||||
<p class="name"><bdi class="ellipsis-dir-fix">${nameHTML}</bdi></p>
|
||||
<p class="number">${phoneDetailsHTML}</p>
|
||||
</a>
|
||||
</li>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="contact-report-tmpl" class="hide">
|
||||
<!--
|
||||
<li>
|
||||
<div class="js-contact-info">
|
||||
${photoHTML}
|
||||
<p class="name"><bdi class="ellipsis-dir-fix">${nameHTML}</bdi></p>
|
||||
<p class="number">${phoneDetailsHTML}</p>
|
||||
</div>
|
||||
</li>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="contact-suggestion-unknown-tmpl" class="hide">
|
||||
<!--
|
||||
<li role="presentation">
|
||||
<a class="suggestion" role="option"
|
||||
data-number="${number}" data-source="contacts" data-name="${name}"
|
||||
data-type="${type}" data-carrier="${carrier}">
|
||||
${photoHTML}
|
||||
<p class="name">${phoneDetailsHTML}</p>
|
||||
<p class="number">
|
||||
<span data-l10n-id="unknown-contact-title">Unknown Contact</span>
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="contact-prompt-tmpl" class="hide">
|
||||
<!--
|
||||
<div class="contact-header">
|
||||
${photoHTML}
|
||||
<p class="name"><bdi class="ellipsis-dir-fix">${name}</bdi></p>
|
||||
<p class="details">${phoneDetailsHTML}</p>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="contact-photo-tmpl" class="hide">
|
||||
<!--
|
||||
<aside role="img" data-l10n-id="contact-photo" aria-label="Contact photo" class="pack-end">
|
||||
<span class="contact-photo" data-type="img"></span>
|
||||
</aside>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="contact-highlight-tmpl" class="hide">
|
||||
<!--
|
||||
<span class="highlight">${str}</span>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="messages-number-tmpl" class="hide">
|
||||
<!--
|
||||
<a class="suggestion js-contact-info" data-number="${number}" data-source="manual" data-name="${number}">
|
||||
<p class="name"><bdi class="ellipsis-dir-fix">${number}</bdi></p>
|
||||
</a>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="information-report-tmpl" class="hide">
|
||||
<!--
|
||||
<span class="icon"></span>
|
||||
<span data-l10n-id="${titleL10n}"></span>
|
||||
<span class="timestamp l10n-contains-date"
|
||||
data-l10n-date="${timestamp}"
|
||||
data-l10n-date-format="${messageL10nDateFormat}"
|
||||
>${reportDateL10n}</span>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="messages-not-downloaded-tmpl" class="hide">
|
||||
<!--
|
||||
<span class="not-downloaded-message l10n-contains-date"
|
||||
data-l10n-id="${messageL10nId}"
|
||||
data-l10n-args="${messageL10nArgs}"
|
||||
data-l10n-date="${messageL10nDate}"
|
||||
data-l10n-date-format="${messageL10nDateFormat}"></span>
|
||||
<button type="button" class="download" data-l10n-id="${downloadL10nId}"></button>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="messages-date-group-tmpl">
|
||||
<!--
|
||||
<div id="${id}" class="messages-date-group" data-timestamp="${timestamp}">
|
||||
<header
|
||||
class="messages-date-group-header"
|
||||
data-time-update="${headerTimeUpdate}"
|
||||
data-time="${headerTime}"
|
||||
data-date-only="${headerDateOnly}">
|
||||
</header>
|
||||
<ul class="message-list"></ul>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="messages-message-tmpl" class="hide">
|
||||
<!--
|
||||
<label class="pack-checkbox danger">
|
||||
<input type="checkbox" value="${id}">
|
||||
<span></span>
|
||||
</label>
|
||||
<section class="bubble">
|
||||
<h1 class="message-subject">
|
||||
${subject}
|
||||
</h1>
|
||||
<div class="message-content">
|
||||
<p class="message-content-body" tabindex="-1">${bodyHTML}</p>
|
||||
<div class="message-details">
|
||||
${simInformationHTML}
|
||||
<time data-time-update="true" data-time-only="true" data-time="${timestamp}"></time>
|
||||
${messageStatusHTML}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="messages-message-sim-information-tmpl" class="hide">
|
||||
<!--
|
||||
<span class="message-sim-information"
|
||||
data-l10n-id="sim-id-label"
|
||||
data-l10n-args="${simNumberL10nArgs}">
|
||||
</span>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="messages-message-status-tmpl" class="hide">
|
||||
<!--
|
||||
<div class="message-status" data-l10n-id="${statusL10nId}"
|
||||
aria-label="Message status"
|
||||
role="status">
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="messages-late-arrival-notice-tmpl" class="hide">
|
||||
<!--
|
||||
<div class="late-arrival-icon" data-l10n-id="late-notice-label" aria-label="Late arrival"></div>
|
||||
<div class="late-arrival-notice"></div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="messages-recipient-tmpl" class="hide">
|
||||
<!--
|
||||
<span class="${className}" x-inputmode="verbatim"
|
||||
contenteditable="${editable}" role="${role}"
|
||||
data-number="${number}" data-source="${source}" data-name="${name}"
|
||||
data-carrier="${carrier}" data-type="${type}" dir="auto">
|
||||
${name}
|
||||
</span>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="attachment-nopreview-tmpl" class="hide">
|
||||
<!--
|
||||
<div class="attachment ${errorClass}" data-attachment-type="${type}">
|
||||
<div class="thumbnail-placeholder ${type}-placeholder">
|
||||
</div>
|
||||
<div class="js-size-indicator size-indicator" data-l10n-id="${sizeL10nId}" data-l10n-args="${sizeL10nArgs}"></div>
|
||||
</div>
|
||||
<div class="file-name" dir="auto"></div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="attachment-preview-tmpl" class="hide">
|
||||
<!--
|
||||
<div class="attachment">
|
||||
<div class="thumbnail"></div>
|
||||
<div class="js-size-indicator size-indicator" data-l10n-id="${sizeL10nId}" data-l10n-args="${sizeL10nArgs}"></div>
|
||||
</div>
|
||||
<div class="file-name" dir="auto"></div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="attachment-draft-tmpl" class="hide">
|
||||
<!--
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<meta charset="utf-8">
|
||||
<base href="${baseURL}">
|
||||
<link rel="stylesheet" type="text/css" href="views/conversation/style/attachment_draft.css">
|
||||
<link rel="stylesheet" type="text/css" href="views/conversation/style/attachment.css">
|
||||
</head>
|
||||
<body class="attachment-draft ${cssClass}">${baseHTML}</body>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Please, don't change template markup alignment, it's done this way to
|
||||
remove any whitespace symbols between inline elements.
|
||||
-->
|
||||
<div id="phone-details-tmpl" class="hide">
|
||||
<!--
|
||||
<span class="${hasTypeClass} ${hasCarrierClass}"><span
|
||||
dir="auto" class="phone-type"
|
||||
data-l10n-id="${type}">${type}</span><span
|
||||
class="phone-type-separator"
|
||||
data-l10n-id="phone-type-separator"></span><span
|
||||
class="phone-number" dir="ltr">${number}</span><span
|
||||
class="phone-carrier-separator"
|
||||
data-l10n-id="phone-carrier-separator"></span><span
|
||||
class="phone-carrier" dir="auto">${carrier}</span>
|
||||
</span>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="messages-group-header-tmpl" class="hide">
|
||||
<!--
|
||||
<span class="group-header-title"><bdi class="ellipsis-dir-fix">${name}</bdi> <bdi dir="ltr">(+${participantCount})</bdi></span>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="messages-header-tmpl" class="hide">
|
||||
<!--
|
||||
<bdi class="ellipsis-dir-fix">${name}</bdi>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<gaia-sim-picker id="sim-picker" tabindex="-1"></gaia-sim-picker>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
122
gaia/apps/sms/views/conversation/js/attachment.js
Executable file
|
|
@ -0,0 +1,122 @@
|
|||
/** Creates an Attachment object
|
||||
* @param {String} type of attachment (image, video, etc).
|
||||
* @param {String} uri Location or datauri of image to show.
|
||||
* @param {Number} size Size of attachment in bytes.
|
||||
* @return {Attachment} new attachment object.
|
||||
*/
|
||||
|
||||
/*global AttachmentRenderer, MimeMapper, MozActivity, Utils*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
// Path to the folder that stores all saved attachments
|
||||
const ATTACHMENT_FOLDER_PATH = 'sms-attachments/';
|
||||
|
||||
/**
|
||||
* Gets actual base file name (name.extension) from its path.
|
||||
* @param {String} filepath
|
||||
* @return {String} actual base file name i.e name.extension
|
||||
*/
|
||||
function getBaseName(filePath) {
|
||||
if (!filePath) {
|
||||
throw new Error('Filepath is not defined!');
|
||||
}
|
||||
return filePath.substring(filePath.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* set Property of Attachment from given blob and options object.
|
||||
* @param {Object, Object} (blob object, options object)
|
||||
*/
|
||||
function Attachment(blob, options) {
|
||||
options = options || {};
|
||||
this.blob = blob;
|
||||
this.isDraft = !!options.isDraft;
|
||||
this.name = blob.name || options.name || '';
|
||||
|
||||
if (typeof this.name !== 'string') {
|
||||
this.name = '';
|
||||
}
|
||||
|
||||
// force the _renderer property to be non enumerable so that we don't try to
|
||||
// store it in IndexedDB
|
||||
Object.defineProperty(this, '_renderer', { writable: true });
|
||||
}
|
||||
|
||||
Attachment.prototype = {
|
||||
/* private methods */
|
||||
_getAttachmentRenderer: function() {
|
||||
this._renderer = this._renderer || AttachmentRenderer.for(this);
|
||||
return this._renderer;
|
||||
},
|
||||
|
||||
/* public properties */
|
||||
get size() {
|
||||
return this.blob.size;
|
||||
},
|
||||
|
||||
get type() {
|
||||
return Utils.typeFromMimeType(this.blob.type);
|
||||
},
|
||||
|
||||
/* public methods */
|
||||
render: function(readyCallback) {
|
||||
var attachmentRenderer = this._getAttachmentRenderer();
|
||||
|
||||
attachmentRenderer.render().catch(function(e) {
|
||||
console.error('Error occurred while rendering attachment.', e);
|
||||
}).then(readyCallback);
|
||||
|
||||
// We still need this for the case where we render a list of attachments
|
||||
// and right order is important.
|
||||
return attachmentRenderer.getAttachmentContainer();
|
||||
},
|
||||
|
||||
updateFileSize: function() {
|
||||
var attachmentRenderer = this._getAttachmentRenderer();
|
||||
attachmentRenderer.updateFileSize();
|
||||
},
|
||||
|
||||
view: function(options) {
|
||||
// Make sure media is openable and savable even if
|
||||
// file extenion is missing or invalid but mimetype is supported.
|
||||
|
||||
var l10nPromise = this.name ? Promise.resolve(this.name) :
|
||||
document.l10n.formatValue('unnamed-attachment');
|
||||
|
||||
return l10nPromise.then(name => {
|
||||
// Make sure media is openable and savable even if
|
||||
// blob mimetype is unsupported but file extension is valid.
|
||||
var mimetype =
|
||||
MimeMapper.guessTypeFromFileProperties(
|
||||
name,
|
||||
this.blob.type.toLowerCase()
|
||||
);
|
||||
var filename = MimeMapper.ensureFilenameMatchesType(name, mimetype);
|
||||
|
||||
// Override filename, so that every attachment that is saved via "open"
|
||||
// activity will be placed in the single location.
|
||||
filename = ATTACHMENT_FOLDER_PATH + getBaseName(filename);
|
||||
|
||||
var activity = new MozActivity({
|
||||
name: 'open',
|
||||
data: {
|
||||
type: mimetype,
|
||||
filename: filename,
|
||||
blob: this.blob,
|
||||
allowSave: options && options.allowSave
|
||||
}
|
||||
});
|
||||
activity.onerror = function() {
|
||||
console.error('error with open activity', this.error.name);
|
||||
if (this.error.name === 'NO_PROVIDER') {
|
||||
Utils.alert('attachmentOpenError');
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.Attachment = Attachment;
|
||||
}(this));
|
||||
300
gaia/apps/sms/views/conversation/js/attachment_renderer.js
Executable file
|
|
@ -0,0 +1,300 @@
|
|||
/*global Promise, Template, Utils,
|
||||
ImageUtils
|
||||
*/
|
||||
|
||||
/* exported AttachmentRenderer */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
// do not create thumbnails for too big attachments
|
||||
// (see bug 805114 for a similar issue in Gallery)
|
||||
const MAX_THUMBNAIL_GENERATION_SIZE = 10 * 1024 * 1024; // 5MB
|
||||
|
||||
// Actual thumbnails size should be 10 rem (100px) * devicePixelRatio
|
||||
const THUMBNAIL_SIZE = 100 * window.devicePixelRatio;
|
||||
|
||||
/**
|
||||
* A <div> container suits most of the cases where we want to display an
|
||||
* MMS attachment (= icon + file name + file size). However, drafts are a
|
||||
* specific case because they are inside an editable area.
|
||||
*
|
||||
* A <div contenteditable="false"> container would be fine for drafts but
|
||||
* Gecko does not support it at the moment, see bug 685445:
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=685445
|
||||
*
|
||||
* Instead of a bunch of JavaScript to manage where the caret is and what
|
||||
* to delete on backspace, the contentEditable region treats the iframe as
|
||||
* a simple block. Outside of the Compose area, a <div> container is still
|
||||
* fine -- and it's *way* faster.
|
||||
*/
|
||||
const RENDERERS = {
|
||||
draft: {
|
||||
createAttachmentContainer: function() {
|
||||
var container = document.createElement('iframe');
|
||||
|
||||
// The attachment's iFrame requires access to the parent document's
|
||||
// context so that URIs for Blobs created in the parent may resolve as
|
||||
// expected.
|
||||
container.setAttribute('sandbox', 'allow-same-origin');
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders baseMarkup into container node (in this case iframe).
|
||||
* @param baseMarkup Base attachment HTML markup. It should be safely
|
||||
* escaped in advance!
|
||||
* @param attachmentContainer Attachment container node.
|
||||
* @returns {Promise.<Node>} Content container node is the container node
|
||||
* for the attachment base HTML markup that allows consumer code to
|
||||
* perform post processing DOM operations.
|
||||
*/
|
||||
renderTo: function(data, attachmentContainer) {
|
||||
attachmentContainer.classList.add(data.cssClass);
|
||||
|
||||
var template = Template('attachment-draft-tmpl');
|
||||
var markup = template.interpolate({
|
||||
baseURL: location.protocol + '//' + location.host + '/',
|
||||
baseHTML: data.markup,
|
||||
cssClass: data.cssClass
|
||||
}, { safe: ['baseHTML'] });
|
||||
|
||||
var blob = new Blob([markup], { type: 'text/html' });
|
||||
var blobUrl = window.URL.createObjectURL(blob);
|
||||
|
||||
attachmentContainer.src = blobUrl;
|
||||
|
||||
return this._whenLoaded(attachmentContainer).then(function onload() {
|
||||
// do some postprocessing after it's loaded
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
|
||||
var contentDocument = attachmentContainer.contentDocument;
|
||||
var documentElement = contentDocument.documentElement;
|
||||
documentElement.lang = document.documentElement.getAttribute('lang');
|
||||
documentElement.dir = document.documentElement.getAttribute('dir');
|
||||
document.l10n.translateFragment(documentElement);
|
||||
|
||||
// Attach click listeners and fire the callback when rendering is
|
||||
// complete: we can't bind `readyCallback' to the `load' event
|
||||
// listener because it would break our unit tests.
|
||||
// Bubble click events from inside the iframe.
|
||||
contentDocument.addEventListener(
|
||||
'click',
|
||||
() => attachmentContainer.click()
|
||||
);
|
||||
|
||||
// Return actual content container node to allow postprocessing of
|
||||
// DOM content without dealing with iframe structure.
|
||||
return contentDocument.body;
|
||||
});
|
||||
},
|
||||
|
||||
_whenLoaded: function(iframe) {
|
||||
var innerDocument = iframe.contentDocument;
|
||||
// checking for the class is a way to check that the correct content is
|
||||
// loaded. If the IFrame is inserted to the DOM before we change the
|
||||
// src, its default src about:blank is loaded and we have a body, albeit
|
||||
// a bad one.
|
||||
if (innerDocument && innerDocument.body &&
|
||||
innerDocument.body.classList.contains('attachment-draft')) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
iframe.addEventListener('load', function onload() {
|
||||
this.removeEventListener('load', onload);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the inner DOM node.
|
||||
*/
|
||||
getContentNode: function(attachmentContainer) {
|
||||
return this._whenLoaded(attachmentContainer).then(
|
||||
() => attachmentContainer.contentDocument.documentElement
|
||||
);
|
||||
},
|
||||
|
||||
setL10nAttributes: function(element, l10nId, l10nArgs) {
|
||||
document.l10n.setAttributes(
|
||||
element, l10nId, l10nArgs
|
||||
);
|
||||
// l10n library can't see changes inside iframes yet
|
||||
document.l10n.translateFragment(element);
|
||||
}
|
||||
},
|
||||
|
||||
base: {
|
||||
createAttachmentContainer: function() {
|
||||
return document.createElement('div');
|
||||
},
|
||||
|
||||
renderTo: function(data, attachmentContainer) {
|
||||
attachmentContainer.classList.add(data.cssClass);
|
||||
attachmentContainer.innerHTML = data.markup;
|
||||
|
||||
return Promise.resolve(attachmentContainer);
|
||||
},
|
||||
|
||||
getContentNode: function(attachmentContainer) {
|
||||
return Promise.resolve(attachmentContainer);
|
||||
},
|
||||
|
||||
setL10nAttributes: function(element, l10nId, l10nArgs) {
|
||||
document.l10n.setAttributes(
|
||||
element, l10nId, l10nArgs
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var AttachmentRenderer = function(attachment) {
|
||||
this._attachment = attachment;
|
||||
this._renderer = attachment.isDraft ? RENDERERS.draft : RENDERERS.base;
|
||||
this._attachmentContainer = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets DOM node that will be container for the attachment markup. This method
|
||||
* will create new container if it wasn't before.
|
||||
* @returns {Node}
|
||||
*/
|
||||
AttachmentRenderer.prototype.getAttachmentContainer = function() {
|
||||
if (!this._attachmentContainer) {
|
||||
this._attachmentContainer = this._createAttachmentContainer();
|
||||
}
|
||||
|
||||
return this._attachmentContainer;
|
||||
};
|
||||
|
||||
AttachmentRenderer.prototype.render = function() {
|
||||
var attachmentContainer = this.getAttachmentContainer();
|
||||
|
||||
// Currently we try to extract thumbnail for image only, for the rest of
|
||||
// types (audio, video and etc.) we display default attachment placeholder.
|
||||
// Video type should be revisited with:
|
||||
// Bug 924609 - Video thumbnails previews are not showing in MMS when
|
||||
// attaching or receiving a video.
|
||||
var thumbnailPromise = this._attachment.type === 'img' &&
|
||||
this._attachment.size < MAX_THUMBNAIL_GENERATION_SIZE ?
|
||||
this.getThumbnail() : Promise.reject();
|
||||
|
||||
return thumbnailPromise.
|
||||
then((thumbnail) => {
|
||||
return {
|
||||
markup: this._getBaseMarkup('attachment-preview-tmpl'),
|
||||
cssClass: 'preview',
|
||||
thumbnail: thumbnail
|
||||
};
|
||||
}).
|
||||
catch((error) => {
|
||||
return {
|
||||
markup: this._getBaseMarkup('attachment-nopreview-tmpl', !!error),
|
||||
cssClass: 'nopreview'
|
||||
};
|
||||
}).
|
||||
then((data) => {
|
||||
return this._renderer.renderTo(data, attachmentContainer).
|
||||
then((contentContainer) => {
|
||||
var thumbnailNode = contentContainer.querySelector('.thumbnail');
|
||||
if (thumbnailNode) {
|
||||
thumbnailNode.style.backgroundImage =
|
||||
'url("' + data.thumbnail.url + data.thumbnail.fragment + '")';
|
||||
|
||||
// It's essential to remember real image URL to revoke it later
|
||||
attachmentContainer.dataset.thumbnail = data.thumbnail.url;
|
||||
}
|
||||
|
||||
var fileNode = contentContainer.querySelector('.file-name');
|
||||
if (this._attachment.name) {
|
||||
fileNode.removeAttribute('data-l10n-id');
|
||||
fileNode.textContent = this._attachment.name.slice(
|
||||
this._attachment.name.lastIndexOf('/') + 1
|
||||
);
|
||||
} else {
|
||||
fileNode.setAttribute('data-l10n-id', 'unnamed-attachment');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
AttachmentRenderer.prototype.updateFileSize = function() {
|
||||
var attachmentContainer = this.getAttachmentContainer();
|
||||
return this._renderer.getContentNode(attachmentContainer).then(
|
||||
(contentNode) => {
|
||||
var sizeIndicator = contentNode.querySelector('.js-size-indicator');
|
||||
if (!sizeIndicator) {
|
||||
throw new Error('updateFileSize should be called after a render().');
|
||||
}
|
||||
|
||||
var sizeL10n = Utils.getSizeForL10n(this._attachment.size);
|
||||
this._renderer.setL10nAttributes(
|
||||
sizeIndicator, sizeL10n.l10nId, sizeL10n.l10nArgs
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts thumbnail for the image.
|
||||
* TODO: As we started to use mozSampleSize for thumbnail generation we need
|
||||
* to check if we still want to store this thumbnail data (indexedDB)
|
||||
* Bug 876467 - [MMS] generate, store, and reuse image thumbnails
|
||||
* @returns {Promise}
|
||||
*/
|
||||
AttachmentRenderer.prototype.getThumbnail = function() {
|
||||
// The thumbnail format matches the blob format.
|
||||
var blob = this._attachment.blob;
|
||||
|
||||
return ImageUtils.getSizeAndType(blob).then(
|
||||
function getSizeResolve(data) {
|
||||
var fragment = ImageUtils.Downsample.sizeNoMoreThan(
|
||||
THUMBNAIL_SIZE / Math.min(data.width, data.height)
|
||||
);
|
||||
|
||||
return {
|
||||
url: window.URL.createObjectURL(blob),
|
||||
fragment: fragment
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates new attachment container.
|
||||
* @returns {Node}
|
||||
*/
|
||||
AttachmentRenderer.prototype._createAttachmentContainer = function() {
|
||||
var attachmentContainer = this._renderer.createAttachmentContainer();
|
||||
|
||||
attachmentContainer.classList.add('attachment-container');
|
||||
attachmentContainer.dataset.attachmentType = this._attachment.type;
|
||||
|
||||
return attachmentContainer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns HTML string that represents base attachment markup.
|
||||
* @param templateId Id of the template to use.
|
||||
* @param hasError Indicates whether something is wrong with attachment.
|
||||
* @returns {string}
|
||||
*/
|
||||
AttachmentRenderer.prototype._getBaseMarkup = function(templateId, hasError) {
|
||||
// interpolate the #attachment-[no]preview-tmpl template
|
||||
var sizeL10n = Utils.getSizeForL10n(this._attachment.size);
|
||||
return Template(templateId).interpolate({
|
||||
type: this._attachment.type,
|
||||
errorClass: hasError ? 'corrupted' : '',
|
||||
sizeL10nId: sizeL10n.l10nId,
|
||||
sizeL10nArgs: JSON.stringify(sizeL10n.l10nArgs)
|
||||
});
|
||||
};
|
||||
|
||||
exports.AttachmentRenderer = {
|
||||
for: function(attachment) {
|
||||
return new AttachmentRenderer(attachment);
|
||||
}
|
||||
};
|
||||
}(this));
|
||||
848
gaia/apps/sms/views/conversation/js/compose.js
Executable file
|
|
@ -0,0 +1,848 @@
|
|||
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/*global Settings, Utils, Attachment, MozActivity,
|
||||
MessageManager,
|
||||
SubjectComposer,
|
||||
Promise,
|
||||
EventDispatcher,
|
||||
DOMError,
|
||||
OptionMenu
|
||||
*/
|
||||
/*exported Compose */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Handle UI specifics of message composition. Namely,
|
||||
* resetting (auto manages placeholder text), getting
|
||||
* message content, and message size
|
||||
*/
|
||||
var Compose = (function() {
|
||||
const TYPES = ['image', 'audio', 'video'];
|
||||
|
||||
// delay between 2 counter updates while composing a message
|
||||
const UPDATE_DELAY = 500;
|
||||
|
||||
// Min available chars count that triggers available chars counter
|
||||
const MIN_AVAILABLE_CHARS_COUNT = 20;
|
||||
|
||||
var placeholderClass = 'placeholder';
|
||||
var attachmentClass = 'attachment-container';
|
||||
|
||||
var attachments = new Map();
|
||||
|
||||
// will be defined in init
|
||||
var dom = {
|
||||
form: null,
|
||||
message: null,
|
||||
sendButton: null,
|
||||
attachButton: null,
|
||||
counter: null
|
||||
};
|
||||
|
||||
var state = {
|
||||
empty: true,
|
||||
maxLength: null,
|
||||
size: null,
|
||||
lastScrollPosition: 0,
|
||||
resizing: false,
|
||||
|
||||
// 'sms' or 'mms'
|
||||
type: 'sms',
|
||||
|
||||
lock: {
|
||||
canSend: null,
|
||||
canEdit: null,
|
||||
forceType: null
|
||||
},
|
||||
|
||||
segmentInfo: {
|
||||
segments: 0,
|
||||
charsAvailableInLastSegment: 0
|
||||
}
|
||||
};
|
||||
|
||||
var subject = null;
|
||||
|
||||
// Given a DOM element, we will extract an array of the
|
||||
// relevant nodes as attachment or text
|
||||
function getContentFromDOM(domElement) {
|
||||
var content = [];
|
||||
var node;
|
||||
|
||||
for (node = domElement.firstChild; node; node = node.nextSibling) {
|
||||
// hunt for an attachment in the Map and append it
|
||||
var attachment = attachments.get(node);
|
||||
if (attachment) {
|
||||
content.push(attachment);
|
||||
continue;
|
||||
}
|
||||
|
||||
var last = content.length - 1;
|
||||
var text = node.textContent;
|
||||
|
||||
// Bug 877141 - contenteditable wil insert non-break spaces when
|
||||
// multiple consecutive spaces are entered, we don't want them.
|
||||
if (text) {
|
||||
text = text.replace(/\u00A0/g, ' ');
|
||||
}
|
||||
|
||||
if (node.nodeName == 'BR') {
|
||||
if (node === domElement.lastChild) {
|
||||
continue;
|
||||
}
|
||||
text = '\n';
|
||||
}
|
||||
|
||||
// append (if possible) text to the last entry
|
||||
if (text.length) {
|
||||
if (typeof content[last] === 'string') {
|
||||
content[last] += text;
|
||||
} else {
|
||||
content.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// anytime content changes - takes a parameter to check for image resizing
|
||||
function onContentChanged(duck) {
|
||||
// if the duck is an image attachment or object, handle resizes
|
||||
if ((duck instanceof Attachment && duck.type === 'img') ||
|
||||
(duck && duck.containsImage)) {
|
||||
return imageAttachmentsHandling();
|
||||
}
|
||||
|
||||
var isEmptyMessage = !dom.message.textContent.length && !hasAttachment();
|
||||
|
||||
if (isEmptyMessage) {
|
||||
var brs = dom.message.getElementsByTagName('br');
|
||||
// firefox will keep an extra <br> in there
|
||||
if (brs.length > 1) {
|
||||
isEmptyMessage = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder management
|
||||
var placeholding = dom.message.classList.contains(placeholderClass);
|
||||
if (placeholding && !isEmptyMessage) {
|
||||
dom.message.classList.remove(placeholderClass);
|
||||
}
|
||||
if (!placeholding && isEmptyMessage) {
|
||||
dom.message.classList.add(placeholderClass);
|
||||
}
|
||||
|
||||
state.emptyMessage = isEmptyMessage;
|
||||
|
||||
Compose.updateEmptyState();
|
||||
Compose.updateType();
|
||||
Compose.updateSendButton();
|
||||
Compose.updateAttachButton();
|
||||
updateSegmentInfoThrottled();
|
||||
|
||||
Compose.emit('input');
|
||||
}
|
||||
|
||||
function onSubjectChanged() {
|
||||
Compose.updateEmptyState();
|
||||
Compose.updateType();
|
||||
Compose.updateSendButton();
|
||||
Compose.updateAttachButton();
|
||||
|
||||
Compose.emit('subject-change');
|
||||
}
|
||||
|
||||
function onSubjectVisibilityChanged() {
|
||||
if (subject.isVisible()) {
|
||||
dom.form.classList.add('subject-composer-visible');
|
||||
} else {
|
||||
dom.message.focus();
|
||||
dom.form.classList.remove('subject-composer-visible');
|
||||
}
|
||||
}
|
||||
|
||||
function hasAttachment() {
|
||||
return !!dom.message.querySelector('iframe');
|
||||
}
|
||||
|
||||
function hasSubject() {
|
||||
return subject.isVisible() && !!subject.getValue();
|
||||
}
|
||||
|
||||
function composeKeyEvents(e) {
|
||||
// If message can't be edited anymore and no-backspace pressed, cancel.
|
||||
if (!compose.canEdit() && e.which !== 8) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
// Trigger a recompute of size on the keypress events.
|
||||
state.size = null;
|
||||
}
|
||||
}
|
||||
|
||||
function insert(item) {
|
||||
var fragment = document.createDocumentFragment();
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// trigger recalc on insert
|
||||
state.size = null;
|
||||
|
||||
if (item.render) { // it's an Attachment
|
||||
var node = item.render();
|
||||
attachments.set(node, item);
|
||||
fragment.appendChild(node);
|
||||
} else if (item.nodeName === 'IFRAME') {
|
||||
// this iframe is generated by us
|
||||
fragment.appendChild(item);
|
||||
} else if (typeof item === 'string') {
|
||||
item.split('\n').forEach(function(line) {
|
||||
fragment.appendChild(document.createTextNode(line));
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
});
|
||||
fragment.lastElementChild.remove();
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function imageAttachmentsHandling() {
|
||||
// There is need to resize image attachment if total compose
|
||||
// size doen't exceed mms size limitation.
|
||||
if (Compose.size < Settings.mmsSizeLimitation) {
|
||||
onContentChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
var imgNodes = [];
|
||||
attachments.forEach((attachment, node) => {
|
||||
if (attachment.type === 'img') {
|
||||
imgNodes.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
var done = 0;
|
||||
|
||||
// Total number of images < 3
|
||||
// => Set max image size to 2/5 message size limitation.
|
||||
// Total number of images >= 3
|
||||
// => Set max image size to 1/5 message size limitation.
|
||||
var images = imgNodes.length;
|
||||
var limit = images > 2 ? Settings.mmsSizeLimitation * 0.2 :
|
||||
Settings.mmsSizeLimitation * 0.4;
|
||||
|
||||
function imageSized() {
|
||||
if (++done === images) {
|
||||
state.resizing = false;
|
||||
onContentChanged();
|
||||
} else {
|
||||
resizedImg(imgNodes[done]);
|
||||
}
|
||||
}
|
||||
|
||||
function resizedImg(node) {
|
||||
var item = attachments.get(node);
|
||||
if (item.blob.size < limit) {
|
||||
imageSized();
|
||||
} else {
|
||||
Utils.getResizedImgBlob(item.blob, limit, function(resizedBlob) {
|
||||
// trigger recalc when resized
|
||||
state.size = null;
|
||||
|
||||
item.blob = resizedBlob;
|
||||
item.updateFileSize();
|
||||
|
||||
imageSized();
|
||||
});
|
||||
}
|
||||
}
|
||||
state.resizing = true;
|
||||
resizedImg(imgNodes[0]);
|
||||
onContentChanged();
|
||||
}
|
||||
|
||||
var segmentInfoTimeout = null;
|
||||
function updateSegmentInfoThrottled() {
|
||||
// we need to call updateSegmentInfo even in MMS mode if we have only text:
|
||||
// if we're in MMS mode because we have a long message, then we need to
|
||||
// check when we go back to SMS mode by having a shorter message.
|
||||
// A possible solution is to do it only when the user deletes characters in
|
||||
// MMS mode.
|
||||
if (hasAttachment()) {
|
||||
return resetSegmentInfo();
|
||||
}
|
||||
|
||||
if (segmentInfoTimeout === null) {
|
||||
segmentInfoTimeout = setTimeout(updateSegmentInfo, UPDATE_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSegmentInfo() {
|
||||
segmentInfoTimeout = null;
|
||||
|
||||
var value = Compose.getText();
|
||||
|
||||
// saving one IPC call when we clear the composer
|
||||
var segmentInfoPromise = value ?
|
||||
MessageManager.getSegmentInfo(value) :
|
||||
Promise.reject();
|
||||
|
||||
segmentInfoPromise.then(
|
||||
function(segmentInfo) {
|
||||
state.segmentInfo = segmentInfo;
|
||||
}, resetSegmentInfo
|
||||
).then(compose.updateType.bind(Compose))
|
||||
.then(compose.emit.bind(compose, 'segmentinfochange'));
|
||||
}
|
||||
|
||||
function resetSegmentInfo() {
|
||||
state.segmentInfo = {
|
||||
segments: 0,
|
||||
charsAvailableInLastSegment: 0
|
||||
};
|
||||
}
|
||||
|
||||
function disposeAttachmentNode(attachmentNode) {
|
||||
var thumbnailURL = attachmentNode.dataset.thumbnail;
|
||||
if (thumbnailURL) {
|
||||
window.URL.revokeObjectURL(thumbnailURL);
|
||||
}
|
||||
attachments.delete(attachmentNode);
|
||||
}
|
||||
|
||||
var compose = {
|
||||
init: function composeInit(formId) {
|
||||
dom.form = document.getElementById(formId);
|
||||
dom.message = document.getElementById('messages-input');
|
||||
dom.sendButton = document.getElementById('messages-send-button');
|
||||
dom.attachButton = document.getElementById('messages-attach-button');
|
||||
dom.counter = dom.form.querySelector('.js-message-counter');
|
||||
dom.messagesAttach = dom.form.querySelector('.messages-attach-container');
|
||||
dom.composerButton = dom.form.querySelector('.composer-button-container');
|
||||
|
||||
subject = new SubjectComposer(
|
||||
dom.form.querySelector('.js-subject-composer')
|
||||
);
|
||||
|
||||
subject.on('change', onSubjectChanged);
|
||||
subject.on('visibility-change', onSubjectChanged);
|
||||
subject.on('visibility-change', onSubjectVisibilityChanged);
|
||||
|
||||
// update the placeholder, send button and Compose.type
|
||||
dom.message.addEventListener('input', onContentChanged);
|
||||
|
||||
// we need to bind to keydown & keypress because of #870120
|
||||
dom.message.addEventListener('keydown', composeKeyEvents);
|
||||
dom.message.addEventListener('keypress', composeKeyEvents);
|
||||
|
||||
dom.message.addEventListener('click',
|
||||
this.onAttachmentClick.bind(this));
|
||||
|
||||
dom.attachButton.addEventListener('click',
|
||||
this.onAttachClick.bind(this));
|
||||
|
||||
this.offAll();
|
||||
this.clear();
|
||||
|
||||
this.on('type', this.onTypeChange.bind(this));
|
||||
this.on('type', this.updateMessageCounter.bind(this));
|
||||
this.on('segmentinfochange', this.updateMessageCounter.bind(this));
|
||||
|
||||
var onInteracted = this.emit.bind(this, 'interact');
|
||||
|
||||
dom.message.addEventListener('focus', onInteracted);
|
||||
dom.message.addEventListener('click', onInteracted);
|
||||
dom.messagesAttach.addEventListener('click', onInteracted);
|
||||
dom.composerButton.addEventListener('click', onInteracted);
|
||||
subject.on('focus', onInteracted);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getContent: function() {
|
||||
return getContentFromDOM(dom.message);
|
||||
},
|
||||
|
||||
getSubject: function() {
|
||||
return subject.isVisible() ? subject.getValue() : null;
|
||||
},
|
||||
|
||||
setSubject: function(value) {
|
||||
return subject.setValue(value);
|
||||
},
|
||||
|
||||
isSubjectMaxLength: function() {
|
||||
return subject.getValue().length >= subject.getMaxLength();
|
||||
},
|
||||
|
||||
showSubject: function() {
|
||||
subject.show();
|
||||
subject.focus();
|
||||
},
|
||||
|
||||
hideSubject: function() {
|
||||
subject.hide();
|
||||
},
|
||||
|
||||
/** Render draft
|
||||
*
|
||||
* @param {Draft} draft Draft to be loaded into the composer.
|
||||
*
|
||||
*/
|
||||
fromDraft: function(draft) {
|
||||
// Clear out the composer
|
||||
this.clear();
|
||||
|
||||
// If we don't have a draft, return only having cleared the composer
|
||||
if (!draft) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (draft.subject) {
|
||||
this.setSubject(draft.subject);
|
||||
subject.show();
|
||||
}
|
||||
|
||||
if (!draft.content) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(draft.content)) {
|
||||
throw new Error('If present, the `content` property must be an array.');
|
||||
}
|
||||
|
||||
// draft content is an array
|
||||
draft.content.forEach(function(fragment) {
|
||||
// If the fragment is an attachment
|
||||
// use the stored content to instantiate a new Attachment object
|
||||
// to be properly rendered after a cold start for the app
|
||||
if (fragment.blob) {
|
||||
fragment = new Attachment(fragment.blob, {
|
||||
name: fragment.name,
|
||||
isDraft: true
|
||||
});
|
||||
}
|
||||
// Append each fragment in order to the composer
|
||||
Compose.append(fragment);
|
||||
}, Compose);
|
||||
},
|
||||
|
||||
getText: function() {
|
||||
var out = this.getContent().filter(function(elem) {
|
||||
return (typeof elem === 'string');
|
||||
});
|
||||
return out.join('');
|
||||
},
|
||||
|
||||
isEmpty: function() {
|
||||
return state.empty;
|
||||
},
|
||||
|
||||
setupLock({ canSend, canEdit, forceType }) {
|
||||
state.lock.canSend = canSend;
|
||||
state.lock.canEdit = canEdit;
|
||||
state.lock.forceType = forceType;
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.updateType();
|
||||
this.updateSendButton();
|
||||
this.updateAttachButton();
|
||||
},
|
||||
|
||||
scrollToTarget: function(target) {
|
||||
// target can be an element or a selection range
|
||||
var targetRect = target.getBoundingClientRect();
|
||||
|
||||
// put the middle of the target at the middle of the container box
|
||||
var containerRect = dom.message.getBoundingClientRect();
|
||||
var offset = (targetRect.top + targetRect.height / 2) -
|
||||
(containerRect.top + containerRect.height / 2);
|
||||
|
||||
// we += because the scrollTop that was set is already compensated
|
||||
// with the getBoundingClientRect()
|
||||
dom.message.scrollTop += offset;
|
||||
},
|
||||
|
||||
scrollMessageContent: function() {
|
||||
if (document.activeElement === dom.message) {
|
||||
// we just got the focus: ensure the caret is visible, and clone
|
||||
// the range to avoid any unnecessary selectionchange event fired
|
||||
// by modifying the range associated with current selection
|
||||
var range = window.getSelection().getRangeAt(0).cloneRange();
|
||||
if (range.collapsed) {
|
||||
// We can't get the bounding client rect of a collapsed range,
|
||||
// so let's insert a temporary node to get the caret position.
|
||||
range.insertNode(document.createElement('span'));
|
||||
this.scrollToTarget(range);
|
||||
range.deleteContents();
|
||||
} else {
|
||||
this.scrollToTarget(range);
|
||||
}
|
||||
state.lastScrollPosition = dom.message.scrollTop;
|
||||
} else {
|
||||
// we just lost the focus: restore the last scroll position
|
||||
dom.message.scrollTop = state.lastScrollPosition;
|
||||
}
|
||||
},
|
||||
|
||||
/** Writes node to composition element
|
||||
* @param {mixed} item Html, DOMNode, or attachment to add
|
||||
* to composition element.
|
||||
* @param {Boolean} position True to append, false to prepend or
|
||||
* undefined/null for auto (at cursor).
|
||||
*/
|
||||
prepend: function(item) {
|
||||
var fragment = insert(item);
|
||||
if (!fragment) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// If the first element is a <br>, it needs to stay first
|
||||
// insert after it but before everyting else
|
||||
if (dom.message.firstChild && dom.message.firstChild.nodeName === 'BR') {
|
||||
dom.message.insertBefore(fragment, dom.message.childNodes[1]);
|
||||
} else {
|
||||
dom.message.insertBefore(fragment, dom.message.childNodes[0]);
|
||||
}
|
||||
|
||||
onContentChanged(item);
|
||||
return this;
|
||||
},
|
||||
|
||||
// if item is an array,ignore calling onContentChanged for each item
|
||||
append: function(item, options) {
|
||||
options = options || {};
|
||||
if (Array.isArray(item)) {
|
||||
var containsImage = false;
|
||||
item.forEach(function(content) {
|
||||
if (content.type === 'img') {
|
||||
containsImage = true;
|
||||
}
|
||||
this.append(content, {ignoreChange: true});
|
||||
}, this);
|
||||
onContentChanged({containsImage: containsImage});
|
||||
} else {
|
||||
var fragment = insert(item);
|
||||
if (!fragment) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (document.activeElement === dom.message) {
|
||||
// insert element at caret position
|
||||
var range = window.getSelection().getRangeAt(0);
|
||||
var firstNodes = fragment.firstChild;
|
||||
range.deleteContents();
|
||||
range.insertNode(fragment);
|
||||
this.scrollToTarget(range);
|
||||
dom.message.focus();
|
||||
range.setStartAfter(firstNodes);
|
||||
} else {
|
||||
// insert element at the end of the Compose area
|
||||
dom.message.insertBefore(fragment, dom.message.lastChild);
|
||||
this.scrollToTarget(dom.message.lastChild);
|
||||
}
|
||||
if (!options.ignoreChange) {
|
||||
onContentChanged(item);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
// changing the type here prevents the "type" event from being fired
|
||||
state.type = 'sms';
|
||||
this.onTypeChange();
|
||||
|
||||
// Dispose attachments
|
||||
attachments.forEach((attachment, node) => disposeAttachmentNode(node));
|
||||
|
||||
dom.message.innerHTML = '<br>';
|
||||
|
||||
subject.reset();
|
||||
|
||||
state.resizing = false;
|
||||
state.size = 0;
|
||||
|
||||
resetSegmentInfo();
|
||||
segmentInfoTimeout = null;
|
||||
|
||||
onContentChanged();
|
||||
return this;
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
dom.message.focus();
|
||||
|
||||
// Put the cursor at the end of the message
|
||||
var selection = window.getSelection();
|
||||
var range = document.createRange();
|
||||
var lastChild = dom.message.lastChild;
|
||||
if (lastChild.tagName === 'BR') {
|
||||
range.setStartBefore(lastChild);
|
||||
} else {
|
||||
range.setStartAfter(lastChild);
|
||||
}
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
updateType: function() {
|
||||
var newType = this.getType();
|
||||
if (newType !== state.type) {
|
||||
state.type = newType;
|
||||
this.emit('type');
|
||||
}
|
||||
},
|
||||
|
||||
getType() {
|
||||
var typeForcedByLock = state.lock.forceType && state.lock.forceType();
|
||||
if (typeForcedByLock) {
|
||||
return typeForcedByLock;
|
||||
}
|
||||
|
||||
var isTextTooLong =
|
||||
state.segmentInfo.segments > Settings.maxConcatenatedMessages;
|
||||
|
||||
return isTextTooLong || hasSubject() || hasAttachment() ? 'mms' : 'sms';
|
||||
},
|
||||
|
||||
updateEmptyState: function() {
|
||||
state.empty = state.emptyMessage && !hasSubject();
|
||||
},
|
||||
|
||||
updateSendButton: function() {
|
||||
dom.sendButton.disabled = !this.canSend();
|
||||
},
|
||||
|
||||
updateAttachButton: function() {
|
||||
dom.attachButton.disabled = !this.canEdit();
|
||||
},
|
||||
|
||||
canSend() {
|
||||
var isSendAllowedByLock = !state.lock.canSend || state.lock.canSend();
|
||||
|
||||
var sizeExceedsLimit = this.type === 'mms' &&
|
||||
Settings.mmsSizeLimitation && this.size > Settings.mmsSizeLimitation;
|
||||
|
||||
// We can't send if message is empty, we're in the process of resizing or
|
||||
// message is already too big.
|
||||
return isSendAllowedByLock && !sizeExceedsLimit && !this.isEmpty() &&
|
||||
!this.isResizing;
|
||||
},
|
||||
|
||||
canEdit() {
|
||||
var isEditAllowedByLock = !state.lock.canEdit || state.lock.canEdit();
|
||||
|
||||
var sizeExceedsOrEqualToLimit = this.type === 'mms' &&
|
||||
Settings.mmsSizeLimitation && this.size >= Settings.mmsSizeLimitation;
|
||||
|
||||
// User can continue to type message if it's either SMS, MMS which size
|
||||
// doesn't exceed maximum allowed size or maximum allowed size is not set.
|
||||
return isEditAllowedByLock && !sizeExceedsOrEqualToLimit;
|
||||
},
|
||||
|
||||
_onAttachmentRequestError: function c_onAttachmentRequestError(err) {
|
||||
var errId = err instanceof DOMError ? err.name : err.message;
|
||||
if (errId === 'file too large') {
|
||||
Utils.alert({
|
||||
id: 'attached-files-too-large',
|
||||
args: {
|
||||
n: 1,
|
||||
mmsSize: (Settings.mmsSizeLimitation / 1024).toFixed(0)
|
||||
}
|
||||
});
|
||||
|
||||
//'pick cancelled' is the error thrown when the pick activity app is
|
||||
// canceled normally
|
||||
} else if (errId !== 'ActivityCanceled' && errId !== 'pick cancelled') {
|
||||
console.warn('Unhandled error: ', err);
|
||||
}
|
||||
},
|
||||
|
||||
onAttachClick: function c_onAttachClick(event) {
|
||||
this.requestAttachment().then(
|
||||
this.append.bind(this),
|
||||
this._onAttachmentRequestError
|
||||
);
|
||||
},
|
||||
|
||||
onAttachmentClick: function c_onAttachmentClick(event) {
|
||||
if (!event.target.classList.contains(attachmentClass) || state.resizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentAttachmentDOM = event.target;
|
||||
var currentAttachment = attachments.get(event.target);
|
||||
var name = currentAttachment.name;
|
||||
var blob = currentAttachment.blob;
|
||||
var options = {
|
||||
header: name.substr(name.lastIndexOf('/') + 1)
|
||||
};
|
||||
|
||||
// Localize the name of the file type
|
||||
var mimeFirstPart = blob.type.substr(0, blob.type.indexOf('/'));
|
||||
var fileType = (TYPES.indexOf(mimeFirstPart) > -1) ?
|
||||
mimeFirstPart: 'other';
|
||||
|
||||
var onView = () => { currentAttachment.view(); };
|
||||
|
||||
function onRemove() {
|
||||
dom.message.removeChild(currentAttachmentDOM);
|
||||
disposeAttachmentNode(currentAttachmentDOM);
|
||||
state.size = null;
|
||||
onContentChanged();
|
||||
}
|
||||
|
||||
var onReplace = () => {
|
||||
this.requestAttachment().then((newAttachment) => {
|
||||
var fragment = insert(newAttachment);
|
||||
|
||||
dom.message.insertBefore(fragment, currentAttachmentDOM);
|
||||
dom.message.removeChild(currentAttachmentDOM);
|
||||
disposeAttachmentNode(currentAttachmentDOM);
|
||||
onContentChanged(newAttachment);
|
||||
}, this._onAttachmentRequestError);
|
||||
};
|
||||
|
||||
options.items = [
|
||||
{ l10nId: `view-attachment-${fileType}`, method: onView },
|
||||
{ l10nId: `remove-attachment-${fileType}`, method: onRemove },
|
||||
{ l10nId: `replace-attachment-${fileType}`, method: onReplace },
|
||||
{ l10nId: 'cancel' }
|
||||
];
|
||||
|
||||
new OptionMenu(options).show();
|
||||
},
|
||||
|
||||
onTypeChange: function c_onTypeChange() {
|
||||
if (this.type === 'sms') {
|
||||
dom.message.setAttribute('x-inputmode', '-moz-sms');
|
||||
} else {
|
||||
dom.message.removeAttribute('x-inputmode');
|
||||
}
|
||||
|
||||
dom.form.dataset.messageType = this.type;
|
||||
},
|
||||
|
||||
updateMessageCounter: function c_updateMessageCounter() {
|
||||
var counterValue = '';
|
||||
|
||||
if (this.type === 'sms') {
|
||||
var segments = state.segmentInfo.segments;
|
||||
var availableChars = state.segmentInfo.charsAvailableInLastSegment;
|
||||
|
||||
if (segments && (segments > 1 ||
|
||||
availableChars <= MIN_AVAILABLE_CHARS_COUNT)) {
|
||||
counterValue = availableChars + '/' + segments;
|
||||
}
|
||||
}
|
||||
|
||||
if (counterValue !== dom.counter.textContent) {
|
||||
dom.counter.textContent = counterValue;
|
||||
}
|
||||
},
|
||||
|
||||
/** Initiates a 'pick' MozActivity allowing the user to create an
|
||||
* attachment
|
||||
* @return {Promise} A Promise for the underlying DOMRequest API.
|
||||
* Resolve will be triggered when activity returns
|
||||
* successfully with result blob size smaller than limit.
|
||||
* Reject will be triggered when activity returns error
|
||||
* or blob size exceeds limit.
|
||||
*/
|
||||
requestAttachment: function() {
|
||||
var activityData = {
|
||||
type: ['image/*', 'audio/*', 'video/*', 'text/vcard']
|
||||
};
|
||||
var activity;
|
||||
var defer = Utils.Promise.defer();
|
||||
|
||||
if (Settings.mmsSizeLimitation) {
|
||||
activityData.maxFileSizeBytes = Settings.mmsSizeLimitation;
|
||||
}
|
||||
|
||||
activity = new MozActivity({
|
||||
name: 'pick',
|
||||
data: activityData
|
||||
});
|
||||
|
||||
activity.onsuccess = function() {
|
||||
var result = activity.result;
|
||||
|
||||
if (Settings.mmsSizeLimitation &&
|
||||
result.blob.size > Settings.mmsSizeLimitation &&
|
||||
Utils.typeFromMimeType(result.blob.type) !== 'img') {
|
||||
|
||||
defer.reject(new Error('file too large'));
|
||||
}
|
||||
|
||||
defer.resolve(new Attachment(result.blob, {
|
||||
name: result.name,
|
||||
isDraft: true
|
||||
}));
|
||||
};
|
||||
|
||||
// Re-throw Gecko-level errors
|
||||
activity.onerror = () => {
|
||||
defer.reject(activity.error);
|
||||
};
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Object.defineProperty(compose, 'type', {
|
||||
get: function composeGetType() {
|
||||
return state.type;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(compose, 'size', {
|
||||
get: function composeGetSize() {
|
||||
if (state.size === null) {
|
||||
state.size = this.getContent().reduce(function(sum, content) {
|
||||
if (typeof content === 'string') {
|
||||
return sum + content.length;
|
||||
} else {
|
||||
return sum + content.size;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return state.size;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(compose, 'segmentInfo', {
|
||||
get: function composeGetSegmentInfo() {
|
||||
return state.segmentInfo;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(compose, 'isResizing', {
|
||||
get: function composeGetResizeState() {
|
||||
return state.resizing;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(compose, 'isSubjectVisible', {
|
||||
get: function composeIsSubjectVisible() {
|
||||
return subject.isVisible();
|
||||
}
|
||||
});
|
||||
|
||||
return EventDispatcher.mixin(compose, [
|
||||
'input',
|
||||
'type',
|
||||
'segmentinfochange',
|
||||
'interact',
|
||||
'subject-change'
|
||||
]);
|
||||
}());
|
||||
3167
gaia/apps/sms/views/conversation/js/conversation.js
Executable file
582
gaia/apps/sms/views/conversation/js/information.js
Executable file
|
|
@ -0,0 +1,582 @@
|
|||
/*global ContactRenderer,
|
||||
Contacts,
|
||||
ConversationView,
|
||||
MessageManager,
|
||||
MobileOperator,
|
||||
Navigation,
|
||||
Settings,
|
||||
Template,
|
||||
Threads,
|
||||
Utils
|
||||
*/
|
||||
|
||||
/*exported Information */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 60 * SECOND;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const DAY = 24 * HOUR;
|
||||
const MONTH = 30 * DAY;
|
||||
const LATENESS_DISPLAY_LOWER_LIMIT = 5 * MINUTE;
|
||||
|
||||
/*
|
||||
Information view module is a subpanel belongs to TheadUI panel. This
|
||||
module provide some default method for thiw view module:
|
||||
|
||||
show: Reuse the conversation view for information view. Hiding unrelated icon,
|
||||
reset the header, and render container for showing the view.
|
||||
|
||||
reset: Hide the view container, clean up the contact list and resume the
|
||||
icons/header for ConversationView.
|
||||
|
||||
renderContactList(participants, options): Rendering group-view style contact
|
||||
list inside the page. Participants is an array of contact number and option
|
||||
is for setting additional information for rendering other lock for contact
|
||||
information.
|
||||
*/
|
||||
var TMPL = function createTemplate(tmpls) {
|
||||
for (var key in tmpls) {
|
||||
tmpls[key] = Template(tmpls[key]);
|
||||
}
|
||||
return tmpls;
|
||||
}({
|
||||
number: 'messages-number-tmpl',
|
||||
report: 'information-report-tmpl'
|
||||
});
|
||||
|
||||
/*
|
||||
Summarized single status based on delivery and read status for mms.
|
||||
The 1st level properties represents delivery status and 2nd level properties
|
||||
represents read status.
|
||||
*/
|
||||
const REPORT_MAP = {
|
||||
'not-applicable': {
|
||||
'not-applicable': 'not-applicable',
|
||||
'pending' : 'pending',
|
||||
'success' : 'read',
|
||||
'error' : 'error'
|
||||
},
|
||||
'pending' : {
|
||||
'not-applicable': 'pending',
|
||||
'pending': 'pending',
|
||||
'success' : 'read', // should not possible
|
||||
'error' : 'error'
|
||||
},
|
||||
'success' : {
|
||||
'not-applicable': 'delivered',
|
||||
'pending': 'delivered',
|
||||
'success' : 'read',
|
||||
'error' : 'error'
|
||||
},
|
||||
'error' : {
|
||||
'not-applicable': 'error',
|
||||
'pending': 'error',
|
||||
'success' : 'error', // should not possible
|
||||
'error' : 'error' // should not possible
|
||||
}
|
||||
};
|
||||
|
||||
// Register the message events we wanted for report view refresh
|
||||
const MESSAGE_EVENTS = [
|
||||
'message-failed-to-send',
|
||||
'message-delivered',
|
||||
'message-read',
|
||||
'message-sent',
|
||||
'message-sending'
|
||||
];
|
||||
|
||||
const reportDateTimeFormatOptions = {
|
||||
month: 'long',
|
||||
day: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
};
|
||||
|
||||
function completeLocaleFormat(timestamp) {
|
||||
var options = Object.assign(
|
||||
{ hour12: navigator.mozHour12 },
|
||||
reportDateTimeFormatOptions);
|
||||
|
||||
return new Date(+timestamp).toLocaleString(navigator.languages, options);
|
||||
}
|
||||
|
||||
function l10nContainsDateSetup(element, timestamp) {
|
||||
element.dataset.l10nDate = timestamp;
|
||||
element.dataset.l10nDateFormat = JSON.stringify(reportDateTimeFormatOptions);
|
||||
element.textContent = completeLocaleFormat(timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate report Div contains delivery report and read report for showing
|
||||
* report information within contact list
|
||||
*/
|
||||
function createReportDiv(reports) {
|
||||
var reportDiv = document.createElement('div');
|
||||
reportDiv.className = 'network-status';
|
||||
var data = {
|
||||
titleL10n: '',
|
||||
reportDateL10n: '',
|
||||
messageL10nDateFormat: JSON.stringify(reportDateTimeFormatOptions),
|
||||
timestamp: ''
|
||||
};
|
||||
var status;
|
||||
var deliveryStatus = reports.deliveryStatus;
|
||||
var readStatus = reports.readStatus;
|
||||
|
||||
if (!readStatus) { // sms
|
||||
status = deliveryStatus === 'success' ?
|
||||
'delivered' :
|
||||
deliveryStatus;
|
||||
} else if (deliveryStatus === 'rejected') {
|
||||
// Status = 'rejected' when receiver is not allowed to download any mms
|
||||
status = 'rejected';
|
||||
} else if (deliveryStatus in REPORT_MAP) {
|
||||
status = REPORT_MAP[deliveryStatus][readStatus];
|
||||
} else {
|
||||
console.error('Invalid message report status: ' + deliveryStatus);
|
||||
return reportDiv;
|
||||
}
|
||||
reportDiv.dataset.deliveryStatus = status;
|
||||
|
||||
switch (status) {
|
||||
case 'not-applicable':
|
||||
return reportDiv;
|
||||
case 'delivered':
|
||||
data.timestamp = '' + reports.deliveryTimestamp;
|
||||
data.reportDateL10n = completeLocaleFormat(reports.deliveryTimestamp);
|
||||
break;
|
||||
case 'read':
|
||||
data.timestamp = '' + reports.readTimestamp;
|
||||
data.reportDateL10n = completeLocaleFormat(reports.readTimestamp);
|
||||
break;
|
||||
}
|
||||
data.titleL10n = 'report-status-' + status;
|
||||
reportDiv.innerHTML = TMPL.report.interpolate(data);
|
||||
|
||||
return reportDiv;
|
||||
}
|
||||
|
||||
function showSimInfo(element, iccId) {
|
||||
var iccManager = navigator.mozIccManager;
|
||||
// Hide the element when single SIM or no iccManager/mobileConnections
|
||||
if (!(Settings.hasSeveralSim() && iccId && iccManager)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var simInfoElement = element.querySelector('.sim-detail');
|
||||
var simId = Settings.getServiceIdByIccId(iccId);
|
||||
|
||||
if (simId === null) {
|
||||
document.l10n.setAttributes(
|
||||
simInfoElement,
|
||||
'dsds-unknown-sim'
|
||||
);
|
||||
} else {
|
||||
var operator = getOperatorByServiceId(simId);
|
||||
var icc = iccManager.getIccById(iccId);
|
||||
var number = icc && icc.iccInfo.msisdn;
|
||||
|
||||
var data = {};
|
||||
var l10nId;
|
||||
|
||||
var info = [operator, number].filter(function(value) {
|
||||
return value;
|
||||
});
|
||||
|
||||
var detailString = info.join(', ');
|
||||
l10nId = info.length ? 'sim-detail' : 'sim-id-label';
|
||||
data = { id: simId + 1, detailString: detailString };
|
||||
document.l10n.setAttributes(
|
||||
simInfoElement,
|
||||
l10nId,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
element.classList.remove('hide');
|
||||
}
|
||||
|
||||
/**
|
||||
* Incoming message: return array of sender number string;
|
||||
* Outgoing message: return array of object(number and report div block).
|
||||
*/
|
||||
function createListWithMsgInfo(message) {
|
||||
var list = [];
|
||||
if (message.delivery === 'received' ||
|
||||
message.delivery === 'not-downloaded') { // received message
|
||||
if (message.type === 'mms') {
|
||||
if (message.receivers) {
|
||||
list.push(...message.receivers);
|
||||
}
|
||||
if (!list.includes(message.sender)) {
|
||||
list.push(message.sender);
|
||||
}
|
||||
} else {
|
||||
list.push(message.sender);
|
||||
}
|
||||
} else if (message.type === 'mms') { // sent mms message
|
||||
message.deliveryInfo.forEach(function(info) {
|
||||
list.push({number: info.receiver,
|
||||
infoBlock: createReportDiv(info)});
|
||||
});
|
||||
} else { // sent sms message
|
||||
var info = {};
|
||||
info.deliveryStatus = message.deliveryStatus;
|
||||
info.deliveryTimestamp = message.deliveryTimestamp;
|
||||
list.push({number: message.receiver,
|
||||
infoBlock: createReportDiv(info)});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return operator name depending on the iccId.
|
||||
* Will return the empty string in a single SIM scenario.
|
||||
*/
|
||||
function getOperatorByServiceId(index) {
|
||||
var conn = navigator.mozMobileConnections[index];
|
||||
return MobileOperator.userFacingInfo(conn).operator;
|
||||
}
|
||||
|
||||
var VIEWS = {
|
||||
group: {
|
||||
name: 'group-view',
|
||||
selector: '.panel-GroupView',
|
||||
contactFlavor: 'group-view',
|
||||
|
||||
render: function renderGroup() {
|
||||
var participants = Threads.get(this.id).participants;
|
||||
this.renderContactList(participants);
|
||||
document.l10n.setAttributes(
|
||||
this.headerText, 'participant', { n:participants.length }
|
||||
);
|
||||
},
|
||||
|
||||
setEventListener: function setEventListener() {
|
||||
this.contactList.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
var target = event.target;
|
||||
|
||||
ConversationView.promptContact({
|
||||
number: target.dataset.number
|
||||
}).then(
|
||||
() => Navigation.toPanel('thread', { id: this.id })
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
elements: ['contact-list', 'header', 'header-text']
|
||||
},
|
||||
report: {
|
||||
name: 'report-view',
|
||||
selector: '.panel-ReportView',
|
||||
contactFlavor: 'report-view',
|
||||
|
||||
init: function() {
|
||||
this.onStatusChanged = this.onStatusChanged.bind(this);
|
||||
},
|
||||
|
||||
beforeEnter: function() {
|
||||
this.constructor.prototype.beforeEnter.apply(this, arguments);
|
||||
|
||||
MESSAGE_EVENTS.forEach((event) => {
|
||||
MessageManager.on(event, this.onStatusChanged);
|
||||
});
|
||||
},
|
||||
|
||||
afterLeave: function() {
|
||||
this.constructor.prototype.afterLeave.apply(this, arguments);
|
||||
|
||||
MESSAGE_EVENTS.forEach((event) => {
|
||||
MessageManager.off(event, this.onStatusChanged);
|
||||
});
|
||||
},
|
||||
|
||||
render: function renderReport() {
|
||||
var setL10nAttributes = document.l10n.setAttributes;
|
||||
var request = MessageManager.getMessage(this.id);
|
||||
|
||||
// Hide these dynamic fields to avoid incorrect info displayed at first
|
||||
this.subject.classList.add('hide');
|
||||
this.sizeBlock.classList.add('hide');
|
||||
|
||||
request.onsuccess = (function() {
|
||||
var message = request.result;
|
||||
var type = message.type;
|
||||
var delivery = message.delivery;
|
||||
|
||||
var isIncoming = delivery === 'received' ||
|
||||
delivery === 'not-downloaded';
|
||||
|
||||
// Fill in the description/status/size
|
||||
if (type === 'sms') {
|
||||
setL10nAttributes(this.type, 'message-type-sms');
|
||||
} else { //mms
|
||||
setL10nAttributes(this.type, 'message-type-mms');
|
||||
// subject text content
|
||||
var subject = message.subject;
|
||||
|
||||
this.subject.classList.toggle('hide', !subject);
|
||||
if (subject) {
|
||||
this.subject.querySelector('.detail').textContent = subject;
|
||||
}
|
||||
|
||||
// Message total size show/hide
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
var size = message.attachments.reduce(function(size, attachment) {
|
||||
return (size += attachment.content.size);
|
||||
}, 0);
|
||||
var params = Utils.getSizeForL10n(size);
|
||||
setL10nAttributes(this.size, params.l10nId, params.l10nArgs);
|
||||
this.sizeBlock.classList.remove('hide');
|
||||
}
|
||||
}
|
||||
this.container.dataset.delivery = delivery;
|
||||
this.container.classList.toggle('incoming-message', isIncoming);
|
||||
this.container.classList.toggle('outgoing-message', !isIncoming);
|
||||
|
||||
// If incoming message is migrated from the database where sentTimestamp
|
||||
// hadn't been supported yet then we won't have valid value for it.
|
||||
this.container.classList.toggle(
|
||||
'no-valid-sent-timestamp',
|
||||
isIncoming && !message.sentTimestamp
|
||||
);
|
||||
|
||||
setL10nAttributes(
|
||||
this.contactTitle,
|
||||
isIncoming ? 'report-from-title' : 'report-to-title'
|
||||
);
|
||||
|
||||
if (isIncoming) {
|
||||
l10nContainsDateSetup(this.receivedTimestamp, message.timestamp);
|
||||
l10nContainsDateSetup(this.sentTimestamp, message.sentTimestamp);
|
||||
setL10nAttributes(this.sentTitle, 'message-sent');
|
||||
this.renderLateArrivalMsg(message);
|
||||
|
||||
} else {
|
||||
if (delivery === 'sending' || delivery === 'sent') {
|
||||
setL10nAttributes(this.sentTitle, 'message-' + delivery);
|
||||
}
|
||||
if (delivery === 'error' || delivery === 'sent') {
|
||||
l10nContainsDateSetup(this.sentTimestamp, message.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
//show sim information for dual sim device
|
||||
showSimInfo(this.simInfo, message.iccId);
|
||||
|
||||
// Filled in the contact list. Only outgoing message contains detailed
|
||||
// report information.
|
||||
this.renderContactList(createListWithMsgInfo(message));
|
||||
}).bind(this);
|
||||
},
|
||||
|
||||
renderLateArrivalMsg: function report_renderLateArrivalMsg(message) {
|
||||
var latenessBlock = this.panel.querySelector('.lateness-block');
|
||||
var lateness = message.timestamp - message.sentTimestamp;
|
||||
var showLatenessNotice = message.timestamp &&
|
||||
message.sentTimestamp &&
|
||||
lateness >= LATENESS_DISPLAY_LOWER_LIMIT;
|
||||
|
||||
if (showLatenessNotice) {
|
||||
var units = [
|
||||
{ unit: 'month', unitVal: MONTH },
|
||||
{ unit: 'day', unitVal: DAY },
|
||||
{ unit: 'hour', unitVal: HOUR },
|
||||
{ unit: 'minute', unitVal: MINUTE },
|
||||
{ unit: 'second', unitVal: SECOND }
|
||||
];
|
||||
|
||||
units.forEach(({unit, unitVal}) => {
|
||||
var output;
|
||||
var unitElement = this.panel.querySelector(`.lateness-${unit}`);
|
||||
|
||||
if (lateness >= unitVal) {
|
||||
// Compute integer value for the current unit output
|
||||
// and get the rest for the next smaller unit
|
||||
output = Math.floor(lateness / unitVal);
|
||||
lateness = lateness % unitVal;
|
||||
|
||||
document.l10n.setAttributes(
|
||||
unitElement,
|
||||
`duration-${unit}-narrow`,
|
||||
{ value: output }
|
||||
);
|
||||
unitElement.classList.remove('hide');
|
||||
} else {
|
||||
unitElement.classList.add('hide');
|
||||
}
|
||||
});
|
||||
latenessBlock.classList.remove('hide');
|
||||
} else {
|
||||
latenessBlock.classList.add('hide');
|
||||
}
|
||||
},
|
||||
|
||||
// Set this flag to true only when resend is triggered.
|
||||
messageResending: false,
|
||||
|
||||
setEventListener: function report_setEventListener() {
|
||||
this.resendBtn.addEventListener('click', () => {
|
||||
this.messageResending = true;
|
||||
ConversationView.resendMessage(this.id);
|
||||
});
|
||||
},
|
||||
|
||||
isReportForMessage: function report_isReportForMessage(id) {
|
||||
return Navigation.isCurrentPanel('report-view', { id: id }) ||
|
||||
(Navigation.isCurrentPanel('report-view') &&
|
||||
this.id === id);
|
||||
},
|
||||
|
||||
onStatusChanged: function report_onStatusChanged(e) {
|
||||
// If we got sending status change in report view after resend clicked
|
||||
// (messageResending is true), we should change report panel id, reset
|
||||
// messageResending flag and refresh for new message report.
|
||||
if (e.message.delivery === 'sending' && this.messageResending) {
|
||||
this.id = e.message.id;
|
||||
this.messageResending = false;
|
||||
}
|
||||
|
||||
this.isReportForMessage(e.message.id) && this.refresh();
|
||||
},
|
||||
|
||||
elements: ['contact-list', 'size', 'size-block', 'type', 'sent-title',
|
||||
'sent-timestamp', 'received-timestamp', 'subject', 'sim-info',
|
||||
'contact-title', 'resend-btn', 'header', 'container'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var Information = function(type) {
|
||||
Object.assign(this, VIEWS[type]);
|
||||
|
||||
if (this.init) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
this.panel = document.querySelector(this.selector);
|
||||
|
||||
this.elements.forEach(function(name) {
|
||||
this[Utils.camelCase(name)] = this.panel.querySelector('.' + name);
|
||||
}, this);
|
||||
|
||||
this.header.addEventListener(
|
||||
'action', this.backOrClose.bind(this)
|
||||
);
|
||||
|
||||
this.setEventListener && this.setEventListener();
|
||||
this.reset();
|
||||
};
|
||||
|
||||
Information.prototype = {
|
||||
constructor: Information,
|
||||
|
||||
beforeEnter: function(args) {
|
||||
this.id = +args.id;
|
||||
this.header.removeAttribute('no-font-fit');
|
||||
this.show();
|
||||
},
|
||||
|
||||
afterLeave: function() {
|
||||
this.reset();
|
||||
this.id = null;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
if (Navigation.isCurrentPanel(this.name)) {
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
// Remove all LIs
|
||||
if (this.contactList) {
|
||||
this.contactList.textContent = '';
|
||||
}
|
||||
},
|
||||
|
||||
backOrClose: function() {
|
||||
Navigation.back();
|
||||
},
|
||||
|
||||
// Incrementing ID for each rendering request to avoid possible race when next
|
||||
// renderContactList request earlier than list item appended to contact list
|
||||
// ul. Ignoring the rendering/appending request if the rendering ID doesn't
|
||||
// match the latest rendering ID.
|
||||
renderingId: 0,
|
||||
|
||||
// Param participants could be:
|
||||
// - Array of contact number string or
|
||||
// - Array of object({ number: number, infoBlock: infoBlock })
|
||||
// for rendering the contact list.
|
||||
renderContactList: function(participants) {
|
||||
var ul = this.contactList;
|
||||
var renderer = ContactRenderer.flavor(this.contactFlavor);
|
||||
var currentRenderingId = ++this.renderingId;
|
||||
|
||||
ul.textContent = '';
|
||||
participants.forEach((participant) => {
|
||||
var number, infoBlock, selector;
|
||||
|
||||
if (typeof participant === 'object') {
|
||||
number = participant.number;
|
||||
infoBlock = participant.infoBlock;
|
||||
selector = '.js-contact-info';
|
||||
} else {
|
||||
number = participant;
|
||||
}
|
||||
Contacts.findByAddress(number).then((contacts) => {
|
||||
// If the current rendering ID doesn't match the latest ID, skip current
|
||||
// one and only render for ID which matches latest request ID.
|
||||
if (currentRenderingId !== this.renderingId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (contacts.length > 0) {
|
||||
renderer.render({
|
||||
contact: contacts[0],
|
||||
input: number,
|
||||
infoBlock: infoBlock,
|
||||
infoBlockParentSelector: selector,
|
||||
target: ul
|
||||
});
|
||||
} else {
|
||||
var li = document.createElement('li');
|
||||
li.role = 'presentation';
|
||||
li.innerHTML = TMPL.number.interpolate({
|
||||
number: number
|
||||
});
|
||||
|
||||
var parentBlock = li.querySelector(selector);
|
||||
if (parentBlock && infoBlock) {
|
||||
parentBlock.appendChild(infoBlock);
|
||||
}
|
||||
ul.appendChild(li);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Information.initDefaultViews = function() {
|
||||
// Create group / report information view
|
||||
exports.GroupView = new Information('group');
|
||||
exports.ReportView = new Information('report');
|
||||
};
|
||||
|
||||
exports.Information = Information;
|
||||
|
||||
// end global closure
|
||||
}(this));
|
||||
|
||||
49
gaia/apps/sms/views/conversation/js/link_action_handler.js
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
/*global ActivityPicker, ConversationView */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
/*
|
||||
Centralized event handling for various
|
||||
data-actions url, email, phone in a message
|
||||
*/
|
||||
|
||||
var LinkActionHandler = {
|
||||
onClick: function lah_onClick(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
var dataset = event.target.dataset;
|
||||
var action = dataset.action;
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'email-link') {
|
||||
ConversationView.prompt({
|
||||
email: dataset.email,
|
||||
inMessage: true
|
||||
});
|
||||
}
|
||||
|
||||
if (action === 'dial-link') {
|
||||
ConversationView.promptContact({
|
||||
number: dataset.dial,
|
||||
inMessage: true
|
||||
});
|
||||
}
|
||||
|
||||
if (action === 'url-link') {
|
||||
|
||||
var type = action.replace('-link', '');
|
||||
|
||||
ActivityPicker[type](
|
||||
dataset[type], this.reset, this.reset
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
exports.LinkActionHandler = LinkActionHandler;
|
||||
}(this));
|
||||
301
gaia/apps/sms/views/conversation/js/link_helper.js
Executable file
|
|
@ -0,0 +1,301 @@
|
|||
/*global Utils */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
/*
|
||||
The LinkHelper exposes functionality to apply the regexp
|
||||
on the input strings and replace them with corresponding
|
||||
anchor links for url, phone, email.
|
||||
*/
|
||||
|
||||
var KNOWN_TLDS = [
|
||||
'aero', 'asia', 'biz', 'cat', 'com', 'coop', 'info',
|
||||
'int', 'jobs', 'mobi', 'museum', 'name', 'net', 'org', 'post', 'pro',
|
||||
'tel', 'travel', 'xxx', 'edu', 'gov', 'mil', 'nyc', 'ac',
|
||||
'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq', 'ar',
|
||||
'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf',
|
||||
'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv',
|
||||
'no', 'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci',
|
||||
'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cs', 'cu', 'cv', 'cx', 'cy',
|
||||
'cz', 'dd', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg',
|
||||
'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr',
|
||||
'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn',
|
||||
'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn',
|
||||
'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir',
|
||||
'is', 'it', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km',
|
||||
'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk',
|
||||
'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg',
|
||||
'mh', 'mk', 'ml', 'mm', 'mn', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms',
|
||||
'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf',
|
||||
'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe',
|
||||
'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt', 'pw',
|
||||
'py', 'qa', 're', 'ro', 'rs', 'ru', 'su', 'рф', 'rw', 'sa', 'sb',
|
||||
'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'no', 'sk', 'sl', 'sm',
|
||||
'sn', 'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc',
|
||||
'td', 'tf', 'tg', 'th', 'tj', 'tk', 'tl', 'tp', 'tm', 'tn', 'to',
|
||||
'tp', 'tl', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'uk', 'us',
|
||||
'gov', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf',
|
||||
'ws', 'ye', 'yt', 'yu', 'za', 'zm', 'zw'
|
||||
];
|
||||
|
||||
var ipv4part = '(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)';
|
||||
var ipv4RegExp = new RegExp('^(?:' + ipv4part + '\\.){3}' + ipv4part + '$');
|
||||
var leadingDigitsRegExp = /^[+0-9]+/;
|
||||
|
||||
// ensure that each part of the domain is long enough
|
||||
function checkDomain(domain) {
|
||||
// Check for a specific IPv4 address
|
||||
if (ipv4RegExp.test(domain)) {
|
||||
return true;
|
||||
} else {
|
||||
// Don't add many restrictions,
|
||||
// just the tld to be non numeric and length > 1
|
||||
var parts = domain.split('.');
|
||||
var lastPart = parts[parts.length - 1];
|
||||
// We want the last part not to be a number
|
||||
return lastPart.length > 1 && !isFinite(lastPart);
|
||||
}
|
||||
}
|
||||
|
||||
// defines things that can match right before to be a "safe" link
|
||||
var safeStart = /[\s.,:;\(>\u0080-\uFFFF]/;
|
||||
|
||||
const MINIMUM_DIGITS_IN_PHONE_NUMBER = 5;
|
||||
const LEADING_DIGIT_BREAKPOINT = 7;
|
||||
|
||||
/**
|
||||
* For each category of links:
|
||||
* The key is the link type
|
||||
* order matters - defined first means higher priority if two matches equal
|
||||
*
|
||||
* regexp: The regular expression to match potential links
|
||||
* matchFilter: (optional) A function that is passed the matched part, and
|
||||
* the linkSpec.
|
||||
* Should return the linkSpec, or false if not to be matched
|
||||
* transform: A function that converts the match data to a html link string
|
||||
* Is passed the matched part and the linkSpec.
|
||||
*/
|
||||
var LINK_TYPES = {
|
||||
phone: {
|
||||
regexp: new RegExp([
|
||||
// sddp: space, dot, dash or parens
|
||||
'(?:\\+\\d{1,4}[ \\t.()-]{0,3}|\\()?' + // (\+<digits><sddp>|\()?
|
||||
'(?:\\d{1,4}[ \\t.()-]{0,3})?' + // <digits><sdd>*
|
||||
'(?:\\d[\\d \\t.()-]{0,12}\\d)' + // <digit><digit|sddp>*<digit>
|
||||
'\\b' // must end on a word boundary
|
||||
].join(''), 'g'),
|
||||
matchFilter: function phoneMatchFilter(phone, link) {
|
||||
var onlyDigits = Utils.removeNonDialables(phone);
|
||||
|
||||
if (onlyDigits.length < MINIMUM_DIGITS_IN_PHONE_NUMBER) {
|
||||
return false;
|
||||
}
|
||||
if (onlyDigits.length === MINIMUM_DIGITS_IN_PHONE_NUMBER &&
|
||||
phone.length !== onlyDigits.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var leadingDigits = leadingDigitsRegExp.exec(phone);
|
||||
if (leadingDigits &&
|
||||
leadingDigits[0].length >= LEADING_DIGIT_BREAKPOINT) {
|
||||
link.end = link.start + leadingDigits[0].length;
|
||||
}
|
||||
return link;
|
||||
},
|
||||
transform: function phoneTransform(phone, link) {
|
||||
return '<a dir="ltr" data-dial="' + phone +
|
||||
'" data-action="dial-link">' + phone + '</a>';
|
||||
}
|
||||
},
|
||||
|
||||
url: {
|
||||
regexp: new RegExp([
|
||||
// must begin at start of string, after whitespace,
|
||||
// {1} match the protocol https?:// or rtsp:// (optional)
|
||||
'(https?://|rtsp://)?',
|
||||
// {2} match "server name": . must be followed by at least one letter
|
||||
'(\\w(?:\\.?[-\\w]){0,255})',
|
||||
// {3} match a . followed by one or more domain valid chars
|
||||
'(\\.\\w{1,10})',
|
||||
// optional :port
|
||||
'(?::[0-9]{1,5})?',
|
||||
// start the "query" capture group by matching an optional dot then /
|
||||
'(?:\\.?/',
|
||||
// anything other than a sentence ender
|
||||
// a sentence ender not followed by whitespace
|
||||
// match 0 - 2048 characters in the url
|
||||
'(?:[^\\s.,?:;!]|[.,?:;!](?!\\s|$)){0,2048}',
|
||||
// end the "query" capture group (optional)
|
||||
')?'
|
||||
].join(''), 'mgi'),
|
||||
matchFilter: function urlMatchFilter(url, linkSpec) {
|
||||
var match = linkSpec.match;
|
||||
var scheme, tld;
|
||||
|
||||
if (!checkDomain(match[2] + match[3])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
scheme = match[1];
|
||||
tld = match[3] && match[3].slice(1);
|
||||
|
||||
// For Cases where:
|
||||
//
|
||||
// 1. There was no scheme (eg, "http", "https", "rtsp")
|
||||
// 2. The matched tld is not a number
|
||||
// 3. The matched tld is not a known tld
|
||||
//
|
||||
// ... Do not create a clickable link
|
||||
if (!scheme && (!isFinite(tld) && KNOWN_TLDS.indexOf(tld) === -1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// strip a trailing ) if there isn't a ( in the url
|
||||
if (url.slice(-1) === ')' && url.indexOf('(') === -1) {
|
||||
linkSpec.end--;
|
||||
}
|
||||
return linkSpec;
|
||||
},
|
||||
transform: function urlTransform(url, linkSpec) {
|
||||
var href = url;
|
||||
if (!linkSpec.match[1]) {
|
||||
href = 'http://' + href;
|
||||
}
|
||||
return '<a data-url="' + href + '" data-action="url-link" >' + url +
|
||||
'</a>';
|
||||
}
|
||||
},
|
||||
|
||||
email: {
|
||||
regexp: /[\w.+-]+@[\w.-]+\.[a-z.]{2,6}/mgi,
|
||||
transform: function emailTransform(email) {
|
||||
return [
|
||||
'<a data-email="',
|
||||
'" data-action="email-link">',
|
||||
'</a>'
|
||||
].join(email);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var LINK_TYPES_KEYS = Object.keys(LINK_TYPES);
|
||||
|
||||
function searchForLinks(type, string) {
|
||||
var linkSpecs = [];
|
||||
var spec = LINK_TYPES[type];
|
||||
|
||||
if (!spec) {
|
||||
return linkSpecs;
|
||||
}
|
||||
|
||||
var regexp = spec.regexp;
|
||||
var matchFilter = spec.matchFilter;
|
||||
var match, linkSpec;
|
||||
|
||||
// while we match stuff...
|
||||
while ((match = regexp.exec(string))) {
|
||||
|
||||
// if the match isn't at the begining of the string, check for a safe
|
||||
// character before the match
|
||||
|
||||
var rest = string.slice(match.index - 1);
|
||||
if (match.index && !safeStart.test(rest.charAt(0))) {
|
||||
|
||||
// we should only advance the regexp to the next safeStart
|
||||
var nextSafe = safeStart.exec(rest);
|
||||
if (nextSafe) {
|
||||
regexp.lastIndex = match.index + nextSafe.index;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
linkSpec = {
|
||||
type: type,
|
||||
start: match.index,
|
||||
length: match[0].length,
|
||||
end: match.index + match[0].length,
|
||||
match: match
|
||||
};
|
||||
|
||||
if (matchFilter) {
|
||||
linkSpec = matchFilter(match[0], linkSpec);
|
||||
}
|
||||
|
||||
if (linkSpec) {
|
||||
linkSpecs.push(linkSpec);
|
||||
regexp.lastIndex = linkSpec.end;
|
||||
}
|
||||
}
|
||||
|
||||
return linkSpecs;
|
||||
}
|
||||
|
||||
function linkSort(a, b) {
|
||||
// sort by starting position first
|
||||
// then by ending position reversed
|
||||
// then by the order of the type in the LINK_TYPES
|
||||
return (a.start - b.start) ||
|
||||
(b.end - a.end) ||
|
||||
(LINK_TYPES_KEYS.indexOf(a.type) - LINK_TYPES_KEYS.indexOf(b.type));
|
||||
}
|
||||
|
||||
function removeOverlapping(linkSpecs) {
|
||||
linkSpecs.sort(linkSort);
|
||||
for (var index = 0; index < linkSpecs.length - 1; index++) {
|
||||
var end = linkSpecs[index].end;
|
||||
// while there are more linkSpecs, and they start before we end remove the
|
||||
// overlapping matches
|
||||
while (linkSpecs[index + 1] && linkSpecs[index + 1].start < end) {
|
||||
linkSpecs.splice(index + 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var LinkHelper = window.LinkHelper = {
|
||||
searchAndLinkUrl: function lh_searchAndLinkUrl(inputText) {
|
||||
return LinkHelper.searchAndLinkClickableData(inputText, { url: true });
|
||||
},
|
||||
searchAndLinkEmail: function lh_searchAndLinkEmail(inputText) {
|
||||
return LinkHelper.searchAndLinkClickableData(inputText, { email: true });
|
||||
},
|
||||
searchAndLinkPhone: function lh_searchAndLinkPhone(inputText) {
|
||||
return LinkHelper.searchAndLinkClickableData(inputText, { phone: true });
|
||||
},
|
||||
// Invokes resepective functions to change url, phone and email strings
|
||||
// and make them active links
|
||||
// |inputText| should be already html escaped
|
||||
searchAndLinkClickableData:
|
||||
function lh_searchAndLinkClickableData(inputText, mode) {
|
||||
// default is everything enabled
|
||||
mode = mode || LINK_TYPES;
|
||||
|
||||
var linkSpecs = [];
|
||||
var type;
|
||||
|
||||
for (type in mode) {
|
||||
if (mode[type] && LINK_TYPES[type]) {
|
||||
linkSpecs = linkSpecs.concat(searchForLinks(type, inputText));
|
||||
}
|
||||
}
|
||||
|
||||
removeOverlapping(linkSpecs);
|
||||
|
||||
var result = '';
|
||||
var lastEnd = 0;
|
||||
|
||||
linkSpecs.forEach(function replaceLink(link) {
|
||||
result += inputText.slice(lastEnd, link.start);
|
||||
|
||||
var replacing = inputText.slice(link.start, link.end);
|
||||
var replaceWith = LINK_TYPES[link.type].transform(replacing, link);
|
||||
result += replaceWith;
|
||||
lastEnd = link.end;
|
||||
});
|
||||
|
||||
result += inputText.slice(lastEnd);
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
1076
gaia/apps/sms/views/conversation/js/recipients.js
Executable file
87
gaia/apps/sms/views/conversation/js/startup.js
Executable file
|
|
@ -0,0 +1,87 @@
|
|||
/* global App,
|
||||
ConversationView,
|
||||
Information,
|
||||
InterInstanceEventDispatcher,
|
||||
LazyLoader,
|
||||
LocalizationHelper,
|
||||
MessageManager,
|
||||
MessagingClient,
|
||||
MozMobileConnectionsClient,
|
||||
Navigation,
|
||||
Settings,
|
||||
Threads,
|
||||
TimeHeaders,
|
||||
Utils
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
const LAZY_DEPENDENCIES = [
|
||||
'/services/js/messaging/messaging_client.js',
|
||||
'/services/js/moz_mobile_connections/moz_mobile_connections_client.js',
|
||||
'/shared/js/settings_listener.js',
|
||||
'/shared/js/mime_mapper.js',
|
||||
'/shared/js/option_menu.js',
|
||||
'/shared/js/mobile_operator.js',
|
||||
'/shared/elements/gaia_sim_picker/script.js',
|
||||
'/views/shared/js/waiting_screen.js',
|
||||
'/views/shared/js/errors.js',
|
||||
'/views/shared/js/dialog.js',
|
||||
'/views/shared/js/error_dialog.js',
|
||||
'/views/conversation/js/link_action_handler.js',
|
||||
'/views/shared/js/contact_renderer.js',
|
||||
'/views/shared/js/activity_picker.js',
|
||||
'/views/conversation/js/information.js',
|
||||
'/views/shared/js/localization_helper.js'
|
||||
];
|
||||
|
||||
function initLazyDependencies() {
|
||||
return LazyLoader.load(LAZY_DEPENDENCIES).then(() => {
|
||||
LocalizationHelper.init();
|
||||
TimeHeaders.init();
|
||||
Information.initDefaultViews();
|
||||
Settings.init();
|
||||
MessagingClient.init(App.instanceId);
|
||||
MozMobileConnectionsClient.init(App.instanceId);
|
||||
Navigation.setReady();
|
||||
|
||||
InterInstanceEventDispatcher.connect();
|
||||
});
|
||||
}
|
||||
|
||||
function initShims() {
|
||||
exports.InboxView = Object.freeze({
|
||||
markReadUnread: () => {},
|
||||
updateThread: () => {}
|
||||
});
|
||||
|
||||
var deferred = Utils.Promise.defer();
|
||||
|
||||
MessageManager.getThreads({
|
||||
each: (thread) => Threads.set(thread.id, thread),
|
||||
done: deferred.resolve
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
exports.Startup = {
|
||||
init() {
|
||||
Utils.initializeShimHost(App.instanceId);
|
||||
|
||||
MessageManager.init();
|
||||
ConversationView.init();
|
||||
if (Navigation.isDefaultPanel()) {
|
||||
ConversationView.once('visually-loaded', initLazyDependencies);
|
||||
} else {
|
||||
initLazyDependencies();
|
||||
}
|
||||
|
||||
// Note that we won't be able to access report and group views directly
|
||||
// because information.js is not loaded before ConversationView finishes
|
||||
// loading.
|
||||
initShims().then(() => Navigation.init());
|
||||
}
|
||||
};
|
||||
})(window);
|
||||
178
gaia/apps/sms/views/conversation/js/subject_composer.js
Executable file
|
|
@ -0,0 +1,178 @@
|
|||
/* global EventDispatcher */
|
||||
|
||||
/* exported SubjectComposer */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
const MAX_LENGTH = 40;
|
||||
const privateMembers = new WeakMap();
|
||||
|
||||
function onKeyDown(e) {
|
||||
/* jshint validthis: true */
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
if (e.keyCode === e.DOM_VK_BACK_SPACE) {
|
||||
if (!priv.isHoldingBackspace) {
|
||||
priv.isEmptyOnBackspace = !this.getValue();
|
||||
priv.isHoldingBackspace = true;
|
||||
}
|
||||
} else {
|
||||
priv.isHoldingBackspace = false;
|
||||
// Input char will be ignored when:
|
||||
// - Reaching the maximum subject length. Any char input is not allowed
|
||||
// - Return key(new line) input. Since new line won't work in subject
|
||||
if (this.getValue().length >= MAX_LENGTH ||
|
||||
e.keyCode === e.DOM_VK_RETURN) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyUp(e) {
|
||||
/* jshint validthis: true */
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
// User removes subject field by either:
|
||||
// - Selecting options menu in top right hand corner, or
|
||||
// - Selecting the delete button in the keyboard. if all
|
||||
// the text is removed from the subject field and the user
|
||||
// selects delete on the keyboard the subject field is removed
|
||||
if (e.keyCode === e.DOM_VK_BACK_SPACE && priv.isHoldingBackspace &&
|
||||
priv.isEmptyOnBackspace) {
|
||||
this.hide();
|
||||
}
|
||||
priv.isEmptyOnBackspace = false;
|
||||
priv.isHoldingBackspace = false;
|
||||
}
|
||||
|
||||
function onInput(e) {
|
||||
/* jshint validthis: true */
|
||||
privateMembers.get(this).updateValue(e.target.textContent);
|
||||
}
|
||||
|
||||
function updatePlaceholder() {
|
||||
/* jshint validthis: true */
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
if (priv.value) {
|
||||
priv.placeholder.remove();
|
||||
} else {
|
||||
priv.node.appendChild(priv.placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
function updateValue(newValue) {
|
||||
/* jshint validthis: true */
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
// Bug 877141 - contenteditable will insert non-break spaces when
|
||||
// multiple consecutive spaces are entered, we don't want them.
|
||||
if (newValue) {
|
||||
newValue = newValue.replace(/\n\s*|\u00A0/g, ' ');
|
||||
}
|
||||
|
||||
if (priv.value !== newValue) {
|
||||
priv.value = newValue;
|
||||
priv.updatePlaceholder();
|
||||
|
||||
this.emit('change');
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
var SubjectComposer = function(node) {
|
||||
EventDispatcher.mixin(this, [
|
||||
'focus',
|
||||
'change',
|
||||
'visibility-change'
|
||||
]);
|
||||
|
||||
if (!node) {
|
||||
throw new Error('Subject node is required');
|
||||
}
|
||||
|
||||
var priv = {
|
||||
isHoldingBackspace: false,
|
||||
isEmptyOnBackspace: false,
|
||||
isVisible: false,
|
||||
value: '',
|
||||
|
||||
// nodes
|
||||
node: node,
|
||||
input: null,
|
||||
placeholder: null,
|
||||
|
||||
// methods
|
||||
updateValue: updateValue.bind(this),
|
||||
updatePlaceholder: updatePlaceholder.bind(this),
|
||||
};
|
||||
privateMembers.set(this, priv);
|
||||
|
||||
priv.input = priv.node.querySelector('.subject-composer-input');
|
||||
priv.placeholder = priv.node.querySelector(
|
||||
'.subject-composer-placeholder'
|
||||
);
|
||||
|
||||
priv.input.addEventListener('keydown', onKeyDown.bind(this));
|
||||
priv.input.addEventListener('keyup', onKeyUp.bind(this));
|
||||
priv.input.addEventListener('input', onInput.bind(this));
|
||||
priv.input.addEventListener('focus', this.emit.bind(this, 'focus'));
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.show = function ms_show() {
|
||||
this.toggle(true);
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.hide = function ms_hide() {
|
||||
this.toggle(false);
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.toggle = function ms_toggle(toggle) {
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
priv.node.classList.toggle('hide', !toggle);
|
||||
priv.isVisible = toggle;
|
||||
|
||||
this.emit('visibility-change');
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.isVisible = function ms_is_visible() {
|
||||
return privateMembers.get(this).isVisible;
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.focus = function ms_focus() {
|
||||
privateMembers.get(this).input.focus();
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.getMaxLength = function ms_get_maxLength() {
|
||||
return MAX_LENGTH;
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.getValue = function ms_get_value() {
|
||||
return privateMembers.get(this).value;
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.setValue = function ms_set_value(value) {
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('Value should be a valid string!');
|
||||
}
|
||||
|
||||
priv.input.textContent = priv.updateValue(value);
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.reset = function ms_reset() {
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
priv.value = priv.input.textContent = '';
|
||||
priv.node.classList.add('hide');
|
||||
priv.isVisible = false;
|
||||
|
||||
priv.updatePlaceholder();
|
||||
};
|
||||
|
||||
exports.SubjectComposer = SubjectComposer;
|
||||
})(window);
|
||||
214
gaia/apps/sms/views/conversation/style/attachment.css
Executable file
|
|
@ -0,0 +1,214 @@
|
|||
/**
|
||||
* Attachment containers are either:
|
||||
* - <div> elements, in the message thread;
|
||||
* - <iframe> elements, in the Compose area (draft attachments).
|
||||
* Both containers have an `attachment-container' class, but only <iframe>
|
||||
* containers have an `attachment-draft' class.
|
||||
*/
|
||||
.attachment-container {
|
||||
position: relative;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
display: block;
|
||||
border: none;
|
||||
height: 10rem;
|
||||
width: 10rem;
|
||||
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.attachment-draft {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
/* make sure all pointer events are sent to the main container */
|
||||
.attachment-draft *,
|
||||
.attachment-container * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.attachment-container.nopreview { /* 100x100 px placeholder + filename below */
|
||||
height: 12.7rem; /* font-size = 1.7rem + 1rem margin */
|
||||
}
|
||||
|
||||
/* Internal height of the iframe that contains the attachment draft */
|
||||
.attachment-container.nopreview[data-attachment-type="vcard"] {
|
||||
/* 40x40 px placeholder + filename below */
|
||||
height: 6.7rem; /* font-size = 1.7rem + 1rem margin */
|
||||
}
|
||||
|
||||
/* The internal height of the attachment itself */
|
||||
.attachment[data-attachment-type="vcard"] {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Allow to display the filename properly */
|
||||
#messages-input > [data-attachment-type="vcard"] {
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
.article-list[data-type="list"] .message .attachment-container {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attachments that occur at the bounds of an MMS message do not need
|
||||
* additional spacing.
|
||||
*/
|
||||
#messages-container[data-type="list"] .message .attachment-container:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
#messages-container[data-type="list"] .message .attachment-container:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.attachment {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
offset-inline-start: 0;
|
||||
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
.outgoing .attachment {
|
||||
offset-inline-start: auto;
|
||||
offset-inline-end: 0;
|
||||
}
|
||||
|
||||
.preview .attachment {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: #ececec;
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
.preview .attachment:active,
|
||||
.preview .attachment:hover {
|
||||
background-color: #b2f2ff;
|
||||
}
|
||||
|
||||
.size-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
|
||||
padding: 0 0.4rem;
|
||||
display: none;
|
||||
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #333;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.attachment-draft > :not([data-attachment-type="vcard"]) .size-indicator {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
|
||||
height: 2rem;
|
||||
display: none;
|
||||
|
||||
line-height: 2rem;
|
||||
font-size: 1.6rem;
|
||||
font-style: italic;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
/* .file-name has a dir="auto". Moreover, sms.css in not included in attachment
|
||||
* iframe, so we cannot leverage the general alignment rules. */
|
||||
html[dir="ltr"] .file-name {
|
||||
text-align: left;
|
||||
left: 1px; /* Avoid issues related to being too much on the left */
|
||||
right: 0;
|
||||
}
|
||||
html[dir="rtl"] .file-name {
|
||||
text-align: right;
|
||||
left: 0;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
.attachment-draft .file-name {
|
||||
color: #4a4a4a;
|
||||
}
|
||||
|
||||
.nopreview .file-name {
|
||||
display: block;
|
||||
|
||||
unicode-bidi: -moz-plaintext;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: none no-repeat center center / cover;
|
||||
}
|
||||
|
||||
.thumbnail-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/* BiDi-proof: CSS sprites */
|
||||
background: url('images/attachments.png') no-repeat left top / 10rem;
|
||||
}
|
||||
|
||||
.attachment-container .thumbnail,
|
||||
.attachment-container .thumbnail-placeholder {
|
||||
position: absolute;
|
||||
top: 0
|
||||
}
|
||||
|
||||
.attachment-draft:hover .thumbnail-placeholder:not(.vcard-placeholder),
|
||||
.attachment-draft:active .thumbnail-placeholder:not(.vcard-placeholder),
|
||||
.attachment-container:not([data-attachment-type="vcard"]):hover .thumbnail-placeholder,
|
||||
.attachment-container:not([data-attachment-type="vcard"]):active .thumbnail-placeholder {
|
||||
background-color: #33cccc;
|
||||
background-blend-mode: luminosity;
|
||||
}
|
||||
|
||||
.img-placeholder {
|
||||
background-position: left -10rem; /* BiDi proof: CSS sprites */
|
||||
}
|
||||
|
||||
.audio-placeholder {
|
||||
background-position: left -20rem; /* BiDi proof: CSS sprites */
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
background-position: left -30rem; /* BiDi proof: CSS sprites */
|
||||
}
|
||||
|
||||
.vcard-placeholder {
|
||||
background: url('images/attachment-vcard.png') no-repeat;
|
||||
background-size: 4rem;
|
||||
}
|
||||
html[dir="ltr"] .vcard-placeholder { background-position: left top; }
|
||||
html[dir="rtl"] .vcard-placeholder { background-position: right top; }
|
||||
|
||||
.incoming.received .vcard-placeholder {
|
||||
background-image: url('images/attachment-vcard-incoming.png');
|
||||
}
|
||||
|
||||
.attachment.corrupted .thumbnail-placeholder:after {
|
||||
display: block;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
offset-inline-start: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
content: '';
|
||||
background: url('icons/corrupted.png') no-repeat center center / 10rem;
|
||||
}
|
||||
6
gaia/apps/sms/views/conversation/style/attachment_draft.css
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
font-size: 10px;
|
||||
}
|
||||
159
gaia/apps/sms/views/conversation/style/conversation.css
Executable file
|
|
@ -0,0 +1,159 @@
|
|||
#messages-container {
|
||||
/* We'll set flex-basis with a shrink factor of 0 to do min-height.
|
||||
* formula: flex-basis = a % + b)
|
||||
* a = (gap2 - gap1) / keyboard_height
|
||||
* b = gap2 - (gap2 - gap1) * (buri_height - header) / keyboard_height + subheader + time_header
|
||||
*
|
||||
* with :
|
||||
* gap1 = 4rem
|
||||
* minimal visible part of a thread when the keyboard is displayed, on buri
|
||||
* gap2 = 8rem
|
||||
* minimal visible part of a thread when the keyboard is hidden, on a buri
|
||||
* keyboard_height = 26rem
|
||||
* measured on a buri, standard english keyboard with suggestions
|
||||
* header = 5rem
|
||||
* subheader = 2rem
|
||||
* buri_height = 48rem
|
||||
* time_header = 2rem
|
||||
*
|
||||
* See https://bugzilla.mozilla.org/show_bug.cgi?id=1015867#c12 for more
|
||||
* information
|
||||
*/
|
||||
flex: 1 0 calc(15.4% + 5.38rem);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#messages-new-message-notice {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
padding: 1rem;
|
||||
padding-inline-start: 2rem;
|
||||
padding-inline-end: 1.5rem;
|
||||
overflow: hidden;
|
||||
|
||||
white-space: nowrap;
|
||||
line-height: 2rem;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
#messages-new-message-notice a {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#messages-new-message-notice .contact {
|
||||
overflow: hidden;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#messages-new-message-notice > .text-container {
|
||||
flex: auto;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#messages-new-message-notice > .button-container {
|
||||
flex: none;
|
||||
|
||||
padding: 0.2rem;
|
||||
padding-inline-start: 1.5rem;
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
|
||||
/* remove margin for notice-button class of button */
|
||||
.notice-button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.group-header-title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.carrier-wrapper {
|
||||
display: none;
|
||||
|
||||
box-sizing: border-box;
|
||||
height: 2.2rem;
|
||||
padding: 0 1.5rem;
|
||||
|
||||
font-size: 1.4rem;
|
||||
line-height: 2.2rem;
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.carrier-wrapper .phone-carrier {
|
||||
color: #008787;
|
||||
}
|
||||
|
||||
.has-carrier .carrier-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#messages-container .messages-date-group-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
|
||||
height: 2.2rem;
|
||||
margin: 0;
|
||||
padding: 0.6rem 3rem;
|
||||
|
||||
font-size: 1.4rem;
|
||||
line-height: 2.2rem;
|
||||
text-transform: uppercase;
|
||||
|
||||
background: rgba(225, 240, 236, 0.85);
|
||||
color: #008787;
|
||||
}
|
||||
|
||||
/* If has carrier change top position */
|
||||
.has-carrier #messages-container .messages-date-group-header {
|
||||
top: 2.2rem;
|
||||
}
|
||||
|
||||
.has-carrier #messages-container > .messages-date-group:first-child > .messages-date-group-header {
|
||||
margin-top: 2.2rem;
|
||||
}
|
||||
|
||||
/* TODO: check whether we need this rule */
|
||||
#messages-container .messages-date-group-header.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.article-list[data-type="list"] .message {
|
||||
z-index: 0; /* bug 970422 */
|
||||
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.article-list[data-type="list"] .message.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#messages-container[data-type="list"] .message.mms .bubble img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* transition in and out edit mode, both for LTR and RTL contexts */
|
||||
.message .bubble {
|
||||
transition: transform var(--transition-duration) ease 0s;
|
||||
}
|
||||
|
||||
/* RTL View */
|
||||
#messages-header-text .ellipsis-dir-fix {
|
||||
width: auto;
|
||||
}
|
||||
33
gaia/apps/sms/views/conversation/style/edit-mode.css
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* This file contains styles for Conversation view edit mode. They complement
|
||||
* styles from views/shared/style/edit-mode.css that are applied as well.
|
||||
* View edit mode is activated via Options -> Select messages.
|
||||
*/
|
||||
|
||||
|
||||
.edit #messages-container {
|
||||
margin-bottom: 5.5rem;
|
||||
}
|
||||
|
||||
.edit .article-list[data-type="list"] .message {
|
||||
border-color: #e7e7e7;
|
||||
}
|
||||
|
||||
.edit [data-type="list"] .message .bubble {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.edit .message .bubble {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .edit .message.incoming .bubble {
|
||||
transform: translateX(2.5rem);
|
||||
}
|
||||
html[dir="rtl"] .edit .message.incoming .bubble {
|
||||
transform: translateX(-2.5rem);
|
||||
}
|
||||
|
||||
.edit #messages-compose-form {
|
||||
display: none;
|
||||
}
|
||||
BIN
gaia/apps/sms/views/conversation/style/icons/corrupted.png
Executable file
|
After Width: | Height: | Size: 584 B |
BIN
gaia/apps/sms/views/conversation/style/icons/corrupted@1.5x.png
Executable file
|
After Width: | Height: | Size: 649 B |
BIN
gaia/apps/sms/views/conversation/style/icons/corrupted@2.25x.png
Executable file
|
After Width: | Height: | Size: 843 B |
BIN
gaia/apps/sms/views/conversation/style/icons/corrupted@2x.png
Executable file
|
After Width: | Height: | Size: 769 B |
BIN
gaia/apps/sms/views/conversation/style/images/attachment-vcard-incoming.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
gaia/apps/sms/views/conversation/style/images/attachment-vcard-incoming@1.5x.png
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
BIN
gaia/apps/sms/views/conversation/style/images/attachment-vcard-incoming@2x.png
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
gaia/apps/sms/views/conversation/style/images/attachment-vcard.png
Executable file
|
After Width: | Height: | Size: 454 B |
BIN
gaia/apps/sms/views/conversation/style/images/attachment-vcard@1.5x.png
Executable file
|
After Width: | Height: | Size: 671 B |
BIN
gaia/apps/sms/views/conversation/style/images/attachment-vcard@2.25x.png
Executable file
|
After Width: | Height: | Size: 1 KiB |
BIN
gaia/apps/sms/views/conversation/style/images/attachment-vcard@2x.png
Executable file
|
After Width: | Height: | Size: 840 B |
BIN
gaia/apps/sms/views/conversation/style/images/attachments.png
Executable file
|
After Width: | Height: | Size: 11 KiB |
BIN
gaia/apps/sms/views/conversation/style/images/attachments@1.5x.png
Executable file
|
After Width: | Height: | Size: 17 KiB |
BIN
gaia/apps/sms/views/conversation/style/images/attachments@2.25x.png
Executable file
|
After Width: | Height: | Size: 28 KiB |
BIN
gaia/apps/sms/views/conversation/style/images/attachments@2x.png
Executable file
|
After Width: | Height: | Size: 22 KiB |
BIN
gaia/apps/sms/views/conversation/style/images/icons/deliveredtick.png
Executable file
|
After Width: | Height: | Size: 411 B |
BIN
gaia/apps/sms/views/conversation/style/images/icons/deliveredtick@1.5x.png
Executable file
|
After Width: | Height: | Size: 575 B |
BIN
gaia/apps/sms/views/conversation/style/images/icons/deliveredtick@2.25x.png
Executable file
|
After Width: | Height: | Size: 778 B |
BIN
gaia/apps/sms/views/conversation/style/images/icons/deliveredtick@2x.png
Executable file
|
After Width: | Height: | Size: 695 B |
BIN
gaia/apps/sms/views/conversation/style/images/icons/exclamation.png
Executable file
|
After Width: | Height: | Size: 354 B |
BIN
gaia/apps/sms/views/conversation/style/images/icons/exclamation@1.5x.png
Executable file
|
After Width: | Height: | Size: 499 B |
BIN
gaia/apps/sms/views/conversation/style/images/icons/exclamation@2.25x.png
Executable file
|
After Width: | Height: | Size: 705 B |
BIN
gaia/apps/sms/views/conversation/style/images/icons/exclamation@2x.png
Executable file
|
After Width: | Height: | Size: 601 B |
BIN
gaia/apps/sms/views/conversation/style/images/icons/late_clock.png
Executable file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
gaia/apps/sms/views/conversation/style/images/icons/late_clock@1.5x.png
Executable file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
gaia/apps/sms/views/conversation/style/images/icons/late_clock@2.25x.png
Executable file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
gaia/apps/sms/views/conversation/style/images/icons/late_clock@2x.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
gaia/apps/sms/views/conversation/style/images/icons/message_read.png
Executable file
|
After Width: | Height: | Size: 398 B |
BIN
gaia/apps/sms/views/conversation/style/images/icons/message_read@1.5x.png
Executable file
|
After Width: | Height: | Size: 560 B |
BIN
gaia/apps/sms/views/conversation/style/images/icons/message_read@2.25x.png
Executable file
|
After Width: | Height: | Size: 793 B |
BIN
gaia/apps/sms/views/conversation/style/images/icons/message_read@2x.png
Executable file
|
After Width: | Height: | Size: 666 B |
238
gaia/apps/sms/views/conversation/style/message.css
Executable file
|
|
@ -0,0 +1,238 @@
|
|||
.message.incoming {
|
||||
padding-inline-start: 1.5rem;
|
||||
padding-inline-end: 4rem;
|
||||
}
|
||||
|
||||
.message.outgoing {
|
||||
padding-inline-start: 4rem;
|
||||
padding-inline-end: 1.5rem;
|
||||
}
|
||||
|
||||
.message .bubble {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
padding: 1rem 1.5rem;
|
||||
overflow: visible;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.bubble .message-content-body:focus {
|
||||
-moz-user-select: text;
|
||||
}
|
||||
|
||||
.message.outgoing .bubble {
|
||||
background: #fff;
|
||||
}
|
||||
html[dir="ltr"] .message.outgoing .bubble {
|
||||
float: right;
|
||||
border-radius: 1.5rem 0 1.5rem 1.5rem;
|
||||
}
|
||||
html[dir="rtl"] .message.outgoing .bubble {
|
||||
float: left;
|
||||
border-radius: 0 1.5rem 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.message.outgoing .bubble,
|
||||
.message.outgoing a:active {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.message.incoming .bubble {
|
||||
background: #39c3c2;
|
||||
}
|
||||
html[dir="ltr"] .message.incoming .bubble {
|
||||
float: left;
|
||||
border-radius: 0 1.5rem 1.5rem 1.5rem;
|
||||
}
|
||||
html[dir="rtl"] .message.incoming .bubble {
|
||||
float: right;
|
||||
border-radius: 1.5rem 0 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.message.incoming .bubble,
|
||||
.message.incoming a:active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.message p.message-content-body {
|
||||
white-space: pre-wrap;
|
||||
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* BB overrides */
|
||||
.message.outgoing p:first-of-type:last-of-type,
|
||||
.message.incoming p:first-of-type:last-of-type {
|
||||
padding-top: 0.5rem;
|
||||
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.message.incoming p:first-of-type {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.message.sending p:first-of-type {
|
||||
color: #6e6e6e;
|
||||
}
|
||||
|
||||
/* End of BB overrides */
|
||||
|
||||
.outgoing.error .bubble {
|
||||
background: #fae1df;
|
||||
}
|
||||
|
||||
.incoming.error .bubble {
|
||||
background: #ff9898;
|
||||
}
|
||||
|
||||
.message.sending .bubble,
|
||||
.message.sending a,
|
||||
.message.sending a:active {
|
||||
color: #6e6e6e;
|
||||
}
|
||||
|
||||
.message.sending .message-subject,
|
||||
.message.sending .message-content-body {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Message Subject */
|
||||
.message .message-subject {
|
||||
display: none;
|
||||
|
||||
margin-bottom: 1rem;
|
||||
|
||||
font-size: 1.4rem;
|
||||
font-style: italic;
|
||||
line-height: 1.7rem;
|
||||
|
||||
unicode-bidi: -moz-plaintext;
|
||||
}
|
||||
|
||||
.message.has-subject .message-subject {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Message content styles */
|
||||
.message-content a {
|
||||
color: #004eff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message-content > p {
|
||||
unicode-bidi: -moz-plaintext;
|
||||
}
|
||||
|
||||
.message-details {
|
||||
display: flex;
|
||||
|
||||
margin-top: 0.5rem;
|
||||
|
||||
font-size: 1.4rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.message.sending time,
|
||||
.message.sending .message-sim-information {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message.outgoing .message-details {
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.message.outgoing.error .message-details {
|
||||
color: #e97f79;
|
||||
}
|
||||
|
||||
.message.incoming .message-details {
|
||||
color: #d1eeee;
|
||||
}
|
||||
|
||||
.message.incoming .late-arrival-notice {
|
||||
color: #026a69;
|
||||
}
|
||||
|
||||
.message.incoming .late-arrival-icon {
|
||||
margin-inline-start: 1rem;
|
||||
margin-inline-end: 0.1rem;
|
||||
min-width: 1.5rem;
|
||||
height: 2rem;
|
||||
|
||||
background: no-repeat center center / 1.5rem;
|
||||
background-image: url('images/icons/late_clock.png');
|
||||
}
|
||||
|
||||
.message.incoming.error .message-details {
|
||||
color: #ffe0dc;
|
||||
}
|
||||
|
||||
.message-sim-information {
|
||||
margin-inline-end: 0.4rem;
|
||||
}
|
||||
|
||||
.message-sim-information::after {
|
||||
content: ' ‧ ';
|
||||
}
|
||||
|
||||
/* Message status aside panel */
|
||||
.message .message-status {
|
||||
height: 2rem;
|
||||
min-width: 3rem;
|
||||
|
||||
background: no-repeat center center / 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.message.error .message-status {
|
||||
background-image: url('images/icons/exclamation.png');
|
||||
}
|
||||
|
||||
.message.delivered .message-status {
|
||||
background-image: url('images/icons/deliveredtick.png');
|
||||
}
|
||||
|
||||
.message.read .message-status {
|
||||
background-image: url('images/icons/message_read.png');
|
||||
}
|
||||
|
||||
.message.pending .message-status,
|
||||
.message.sending .message-status {
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
||||
/* Message download button */
|
||||
.message .download {
|
||||
display: block;
|
||||
|
||||
min-height: initial;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
/* Eliminating container paragraph line-height */
|
||||
margin: -1.5rem 0 0;
|
||||
|
||||
background: none;
|
||||
color: #fff;
|
||||
border: none;
|
||||
/* We're using border-bottom instead of just underline style as line is too
|
||||
close to the text and doesn't look similar to visual spec. */
|
||||
border-bottom: 1px solid #fff;
|
||||
}
|
||||
|
||||
.message .download:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* hide download button via css on expired messages */
|
||||
.message.expired .download {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message.pending .download {
|
||||
border-color: transparent;
|
||||
opacity: 0.8;
|
||||
}
|
||||
167
gaia/apps/sms/views/conversation/style/report_view.css
Executable file
|
|
@ -0,0 +1,167 @@
|
|||
/* Message information page for report details*/
|
||||
.report-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.report-view > .description {
|
||||
padding-top: 2rem;
|
||||
padding-inline-end: 1.5rem;
|
||||
padding-bottom: 3rem;
|
||||
padding-inline-start: 3rem;
|
||||
}
|
||||
|
||||
.report-view p,
|
||||
.report-view span {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1rem;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.report-view p {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.report-view .description > .type {
|
||||
padding-top: 0;
|
||||
|
||||
font-size: 1.6rem;
|
||||
color: #ff4e00;
|
||||
}
|
||||
|
||||
.report-view .description .detail {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.report-view .description > .type[data-l10n-id="message-type-sms"] {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Hiding some elements for some states */
|
||||
.report-view[data-delivery="error"] .sent-title,
|
||||
.report-view[data-delivery="sending"] .sent-timestamp,
|
||||
.outgoing-message .received-block,
|
||||
.outgoing-message .lateness-block {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Hide sent timestamp label if we don't have valid value to display */
|
||||
.report-view.no-valid-sent-timestamp .sent-block {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.report-view .contact-title {
|
||||
color: #008787;
|
||||
font-size: 1.6rem;
|
||||
padding-inline-start: 3rem;
|
||||
}
|
||||
|
||||
.report-view > ul.contact-list {
|
||||
/* Showing 4 complete contacts and part of the contact name if list
|
||||
contains more than 4 contacts */
|
||||
position: static;
|
||||
|
||||
margin-top: 0.5rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.report-view > .contact-list > li {
|
||||
height: auto;
|
||||
min-height: 9rem;
|
||||
padding-top: 0.7rem;
|
||||
padding-bottom: 1rem;
|
||||
padding-inline-end: 0;
|
||||
padding-inline-start: 1.5rem;
|
||||
|
||||
border-top: solid 0.1rem #e7e7e7;
|
||||
border-bottom: medium none;
|
||||
}
|
||||
|
||||
.report-view > .contact-list p,
|
||||
.report-view > .contact-list span {
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.6rem;
|
||||
}
|
||||
|
||||
.report-view > .contact-list .name {
|
||||
padding-top: 0;
|
||||
|
||||
font-size: 1.8rem;
|
||||
font-weight: 500;
|
||||
line-height: 2.6rem;
|
||||
}
|
||||
|
||||
.report-view > .contact-list aside {
|
||||
/* margin-top = (li - padding - aside) / 2 = (9rem - 2rem - 5.5rem) / 2 */
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.report-view > .contact-list .number {
|
||||
padding-top: 0.6rem;
|
||||
}
|
||||
|
||||
.report-view .network-status {
|
||||
padding-top: 0.7rem;
|
||||
}
|
||||
|
||||
.report-view .name + .network-status {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.report-view .network-status,
|
||||
.report-view .network-status span {
|
||||
line-height: 1.4rem;
|
||||
}
|
||||
|
||||
.contact-list p.number .phone-carrier {
|
||||
color: #008787;
|
||||
}
|
||||
|
||||
.report-view > .contact-list .icon {
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
margin: -0.1rem 0 0.1rem;
|
||||
margin-inline-end: 0.7rem;
|
||||
|
||||
background: no-repeat center center / 1.6rem transparent;
|
||||
}
|
||||
html[dir="ltr"] .report-view > .contact-list .icon { float: left; }
|
||||
html[dir="rtl"] .report-view > .contact-list .icon { float: right; }
|
||||
|
||||
.contact-list [data-delivery-status="delivered"] .icon {
|
||||
background-image: url('images/icons/deliveredtick.png');
|
||||
}
|
||||
|
||||
.contact-list [data-delivery-status="read"] .icon {
|
||||
background-image: url('images/icons/message_read.png');
|
||||
}
|
||||
|
||||
.contact-list [data-delivery-status="error"] .icon {
|
||||
background-image: url('images/icons/exclamation.png');
|
||||
}
|
||||
|
||||
.contact-list [data-delivery-status="not-applicable"],
|
||||
.contact-list [data-delivery-status="pending"] .icon,
|
||||
.contact-list [data-delivery-status="pending"] .timestamp,
|
||||
.contact-list [data-delivery-status="error"] .timestamp,
|
||||
.contact-list [data-delivery-status="rejected"] .icon,
|
||||
.contact-list [data-delivery-status="rejected"] .timestamp{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.report-view .resend-btn {
|
||||
display: none;
|
||||
width: calc(100% - 3rem);
|
||||
|
||||
flex-shrink: 0;
|
||||
margin: 0 1.5rem 2rem;
|
||||
|
||||
background-color: #00caf2;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.report-view[data-delivery="error"] .resend-btn {
|
||||
display: block;
|
||||
}
|
||||
3
gaia/apps/sms/views/conversation/test/unit/.jshintrc
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../../../../test/unit/.jshintrc"
|
||||
}
|
||||
511
gaia/apps/sms/views/conversation/test/unit/attachment_renderer_test.js
Executable file
|
|
@ -0,0 +1,511 @@
|
|||
/*global MocksHelper, MockL10n, loadBodyHTML, Attachment, AttachmentRenderer,
|
||||
AssetsHelper,
|
||||
ImageUtils,
|
||||
Promise
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/views/conversation/js/attachment_renderer.js');
|
||||
require('/views/shared/js/utils.js');
|
||||
|
||||
require('/shared/js/image_utils.js');
|
||||
require('/shared/test/unit/mocks/mock_l20n.js');
|
||||
require('/views/shared/test/unit/mock_attachment.js');
|
||||
require('/views/shared/test/unit/mock_utils.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'Attachment',
|
||||
'Utils'
|
||||
]).init();
|
||||
|
||||
suite('AttachmentRenderer >', function() {
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
|
||||
var testImageBlob;
|
||||
var testImageBlob_small;
|
||||
var testImageBlob_bogus;
|
||||
var testAudioBlob;
|
||||
var testVideoBlob;
|
||||
var testVcardBlob;
|
||||
|
||||
function assertThumbnailPreview(el) {
|
||||
assert.ok(el.classList.contains('preview'));
|
||||
assert.isFalse(el.classList.contains('nopreview'));
|
||||
assert.isNull(el.querySelector('div.placeholder'));
|
||||
var thumbnail = el.querySelector('div.thumbnail');
|
||||
assert.ok(thumbnail);
|
||||
assert.include(thumbnail.style.backgroundImage, 'blob:');
|
||||
}
|
||||
|
||||
function assertThumbnailPlaceholder(el, type) {
|
||||
assert.ok(el.classList.contains('nopreview'));
|
||||
assert.isFalse(el.classList.contains('preview'));
|
||||
assert.isNull(el.querySelector('div.thumbnail'));
|
||||
assert.isFalse(el.hasAttribute('data-thumbnail'));
|
||||
var placeholder = el.querySelector('div.thumbnail-placeholder');
|
||||
assert.ok(placeholder);
|
||||
assert.ok(placeholder.classList.contains(type + '-placeholder'));
|
||||
}
|
||||
|
||||
suiteSetup(function(done) {
|
||||
this.realMozL10n = document.l10n;
|
||||
document.l10n = MockL10n;
|
||||
|
||||
// create a bogus image blob (should be rendered with a `corrupted' class)
|
||||
testImageBlob_bogus = new Blob(['This is an image message'], {
|
||||
type: 'image/jpeg'
|
||||
});
|
||||
|
||||
var mediaFolder = '/views/shared/test/unit/media';
|
||||
var blobPromises = [
|
||||
AssetsHelper.generateImageBlob(300, 300, 'image/jpeg', 0.25).then(
|
||||
(blob) => testImageBlob_small = blob
|
||||
),
|
||||
AssetsHelper.generateImageBlob(1400, 1400, 'image/jpeg', 1).then(
|
||||
(blob) => testImageBlob = blob
|
||||
),
|
||||
AssetsHelper.loadFileBlob(`${mediaFolder}/audio.oga`).then(
|
||||
(blob) => testAudioBlob = blob
|
||||
),
|
||||
AssetsHelper.loadFileBlob(`${mediaFolder}/video.ogv`).then(
|
||||
(blob) => testVideoBlob = blob
|
||||
),
|
||||
AssetsHelper.loadFileBlob(`${mediaFolder}/contacts.vcf`).then(
|
||||
(blob) => testVcardBlob = blob
|
||||
)
|
||||
];
|
||||
|
||||
Promise.all(blobPromises).then(() => {
|
||||
done(() => {
|
||||
var mmsSizeLimit = 300 * 1024;
|
||||
|
||||
assert.isTrue(
|
||||
testImageBlob_small.size < mmsSizeLimit,
|
||||
'Image blob should be greater than MMS size limit'
|
||||
);
|
||||
|
||||
assert.isTrue(
|
||||
testImageBlob.size > mmsSizeLimit,
|
||||
'Image blob should be greater than MMS size limit'
|
||||
);
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
document.l10n = this.realMozL10n;
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
loadBodyHTML('/index.html');
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
document.body.textContent = '';
|
||||
});
|
||||
|
||||
suite('render >', function() {
|
||||
test('bogus image attachment', function(done) {
|
||||
var attachment = new Attachment(testImageBlob_bogus, {
|
||||
name: 'Image attachment'
|
||||
});
|
||||
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment),
|
||||
attachmentContainer = attachmentRenderer.getAttachmentContainer();
|
||||
|
||||
attachmentRenderer.render().then(() => {
|
||||
assert.ok(
|
||||
attachmentContainer.classList.contains('attachment-container')
|
||||
);
|
||||
assert.equal(attachmentContainer.dataset.attachmentType, 'img');
|
||||
// broken image => no thumbnail and `corrupted' class
|
||||
assertThumbnailPlaceholder(attachmentContainer, 'img');
|
||||
assert.ok(attachmentContainer.querySelector('div.corrupted'));
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('small image attachment (thumbnail)', function(done) {
|
||||
var attachment = new Attachment(testImageBlob_small, {
|
||||
name: 'Image attachment'
|
||||
});
|
||||
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment),
|
||||
attachmentContainer = attachmentRenderer.getAttachmentContainer();
|
||||
|
||||
attachmentRenderer.render().then(() => {
|
||||
assert.ok(
|
||||
attachmentContainer.classList.contains('attachment-container')
|
||||
);
|
||||
assert.equal(attachmentContainer.dataset.attachmentType, 'img');
|
||||
// image < message limit => thumbnail blob
|
||||
assertThumbnailPreview(attachmentContainer);
|
||||
assert.isNull(attachmentContainer.querySelector('div.corrupted'));
|
||||
assert.isDefined(attachmentContainer.dataset.thumbnail);
|
||||
assert.include(attachmentContainer.dataset.thumbnail, 'blob:');
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('HUGE (fake) image attachment', function(done) {
|
||||
var attachment = new Attachment({
|
||||
size: 10 * 1024 * 1024,
|
||||
type: 'image/jpeg'
|
||||
}, {
|
||||
name: 'Image attachment'
|
||||
});
|
||||
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment),
|
||||
attachmentContainer = attachmentRenderer.getAttachmentContainer();
|
||||
|
||||
attachmentRenderer.render().then(() => {
|
||||
assert.ok(
|
||||
attachmentContainer.classList.contains('attachment-container')
|
||||
);
|
||||
assert.equal(attachmentContainer.dataset.attachmentType, 'img');
|
||||
// image > message limit => no thumbnail preview
|
||||
assertThumbnailPlaceholder(attachmentContainer, 'img');
|
||||
assert.isNull(attachmentContainer.querySelector('div.corrupted'));
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('big image attachment', function(done) {
|
||||
var attachment = new Attachment(testImageBlob, {
|
||||
name: 'Image attachment'
|
||||
});
|
||||
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment),
|
||||
attachmentContainer = attachmentRenderer.getAttachmentContainer();
|
||||
|
||||
attachmentRenderer.render().then(() => {
|
||||
assert.ok(
|
||||
attachmentContainer.classList.contains('attachment-container')
|
||||
);
|
||||
assert.equal(attachmentContainer.dataset.attachmentType, 'img');
|
||||
// image < message limit => thumbnail blob
|
||||
assertThumbnailPreview(attachmentContainer);
|
||||
assert.isNull(attachmentContainer.querySelector('div.corrupted'));
|
||||
assert.isDefined(attachmentContainer.dataset.thumbnail);
|
||||
assert.include(attachmentContainer.dataset.thumbnail, 'blob:');
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('audio attachment', function(done) {
|
||||
var attachment = new Attachment(testAudioBlob, {
|
||||
name: 'Audio attachment'
|
||||
});
|
||||
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment),
|
||||
attachmentContainer = attachmentRenderer.getAttachmentContainer();
|
||||
|
||||
attachmentRenderer.render().then(() => {
|
||||
assert.ok(
|
||||
attachmentContainer.classList.contains('attachment-container')
|
||||
);
|
||||
assert.equal(attachmentContainer.dataset.attachmentType, 'audio');
|
||||
// not an image => no thumbnail preview
|
||||
assertThumbnailPlaceholder(attachmentContainer, 'audio');
|
||||
assert.isNull(attachmentContainer.querySelector('div.corrupted'));
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('video attachment', function(done) {
|
||||
var attachment = new Attachment(testVideoBlob, {
|
||||
name: 'Video attachment'
|
||||
});
|
||||
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment),
|
||||
attachmentContainer = attachmentRenderer.getAttachmentContainer();
|
||||
|
||||
attachmentRenderer.render().then(() => {
|
||||
assert.ok(
|
||||
attachmentContainer.classList.contains('attachment-container')
|
||||
);
|
||||
assert.equal(attachmentContainer.dataset.attachmentType, 'video');
|
||||
// not an image => no thumbnail preview
|
||||
assertThumbnailPlaceholder(attachmentContainer, 'video');
|
||||
assert.isNull(attachmentContainer.querySelector('div.corrupted'));
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('vcard attachment', function(done) {
|
||||
var attachment = new Attachment(testVcardBlob, {
|
||||
name: 'vcard.vcf'
|
||||
});
|
||||
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment),
|
||||
attachmentContainer = attachmentRenderer.getAttachmentContainer();
|
||||
|
||||
attachmentRenderer.render().then(() => {
|
||||
assert.ok(
|
||||
attachmentContainer.classList.contains('attachment-container')
|
||||
);
|
||||
assert.equal(attachmentContainer.dataset.attachmentType, 'vcard');
|
||||
// not an image => no thumbnail preview
|
||||
assertThumbnailPlaceholder(attachmentContainer, 'vcard');
|
||||
assert.isNull(attachmentContainer.querySelector('div.corrupted'));
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
suite('draft attachments > ', function() {
|
||||
test('> vcard', function(done) {
|
||||
var attachment = new Attachment(testVcardBlob, {
|
||||
name: 'vcard.vcf',
|
||||
isDraft: true
|
||||
});
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment),
|
||||
attachmentContainer = attachmentRenderer.getAttachmentContainer();
|
||||
|
||||
attachmentRenderer.render().then(() => {
|
||||
assert.equal(attachmentContainer.tagName, 'IFRAME');
|
||||
assert.equal(attachmentContainer.dataset.attachmentType, 'vcard');
|
||||
var doc = attachmentContainer.contentDocument;
|
||||
var attachmentNode = doc.querySelector('.attachment');
|
||||
assert.ok(attachmentNode);
|
||||
|
||||
assert.isNull(attachmentNode.querySelector('div.corrupted'));
|
||||
assert.ok(attachmentNode.querySelector('.vcard-placeholder'));
|
||||
var fileNameNode = doc.querySelector('.file-name');
|
||||
assert.ok(fileNameNode);
|
||||
assert.isNull(fileNameNode.firstElementChild);
|
||||
assert.equal(fileNameNode.textContent, attachment.name);
|
||||
}).then(done, done);
|
||||
|
||||
document.body.appendChild(attachmentContainer);
|
||||
});
|
||||
|
||||
suite('> image', function() {
|
||||
function testDraftImage(testName, attachmentName) {
|
||||
test(testName + ' attachment', function(done) {
|
||||
var attachment = new Attachment(testImageBlob, {
|
||||
name: attachmentName,
|
||||
isDraft: true
|
||||
});
|
||||
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment),
|
||||
attachmentContainer =
|
||||
attachmentRenderer.getAttachmentContainer();
|
||||
|
||||
attachmentRenderer.render().then(() => {
|
||||
assert.equal(attachmentContainer.tagName, 'IFRAME');
|
||||
var doc = attachmentContainer.contentDocument;
|
||||
var attachmentNode = doc.querySelector('.attachment');
|
||||
assert.ok(attachmentNode);
|
||||
assert.isNull(attachmentNode.querySelector('div.corrupted'));
|
||||
assert.ok(attachmentNode.querySelector('.thumbnail'));
|
||||
assert.isDefined(attachmentContainer.dataset.thumbnail);
|
||||
assert.include(attachmentContainer.dataset.thumbnail, 'blob:');
|
||||
var fileNameNode = doc.querySelector('.file-name');
|
||||
assert.ok(fileNameNode);
|
||||
assert.isNull(fileNameNode.firstElementChild);
|
||||
assert.equal(fileNameNode.textContent, attachment.name);
|
||||
}).then(done, done);
|
||||
|
||||
document.body.appendChild(attachmentContainer);
|
||||
});
|
||||
}
|
||||
|
||||
testDraftImage('normal', 'Image attachment');
|
||||
testDraftImage(
|
||||
'malicious script',
|
||||
'%3Cscript%3Ealert(%22I%20am%20dangerous%22)%3C%2Fscript%3E'
|
||||
);
|
||||
testDraftImage(
|
||||
'malicious image',
|
||||
'%3Cimg%20src%3D%22http%3A%2F%2Fmalicious.server.ru%2Fpingback%22%3E'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('getThumbnail >', function() {
|
||||
|
||||
setup(function() {
|
||||
this.sinon.stub(ImageUtils, 'getSizeAndType');
|
||||
this.sinon.spy(ImageUtils.Downsample, 'sizeNoMoreThan');
|
||||
});
|
||||
|
||||
test('Use ImageUtils for no resize case', function(done) {
|
||||
var attachmentRenderer = AttachmentRenderer.for(new Attachment(
|
||||
testImageBlob, {
|
||||
name: 'auto_generated'
|
||||
}
|
||||
));
|
||||
|
||||
ImageUtils.getSizeAndType.returns(
|
||||
Promise.resolve({
|
||||
width: 100,
|
||||
height: 100
|
||||
})
|
||||
);
|
||||
|
||||
attachmentRenderer.getThumbnail().then(function(thumbnail) {
|
||||
assert.include(thumbnail.url, 'blob:');
|
||||
assert.equal(thumbnail.fragment.toString(), '');
|
||||
sinon.assert.calledWith(
|
||||
ImageUtils.Downsample.sizeNoMoreThan,
|
||||
sinon.match.number
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Use ImageUtils for resize needed case', function(done) {
|
||||
var attachmentRenderer = AttachmentRenderer.for(new Attachment(
|
||||
testImageBlob, {
|
||||
name: 'auto_generated'
|
||||
}
|
||||
));
|
||||
|
||||
ImageUtils.getSizeAndType.returns(
|
||||
Promise.resolve({
|
||||
width: 300,
|
||||
height: 300
|
||||
})
|
||||
);
|
||||
|
||||
attachmentRenderer.getThumbnail().then(function(thumbnails) {
|
||||
assert.include(thumbnails.url, 'blob:');
|
||||
assert.include(thumbnails.fragment.toString(), '#-moz-samplesize=');
|
||||
sinon.assert.calledWith(
|
||||
ImageUtils.Downsample.sizeNoMoreThan,
|
||||
sinon.match.number
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('throw error if image is corrupted', function(done) {
|
||||
var attachmentRenderer = AttachmentRenderer.for(
|
||||
new Attachment(testImageBlob_bogus, {
|
||||
name: 'auto_generated'
|
||||
})
|
||||
);
|
||||
var imgError = 'image error';
|
||||
|
||||
ImageUtils.getSizeAndType.returns(
|
||||
Promise.reject(imgError)
|
||||
);
|
||||
|
||||
attachmentRenderer.getThumbnail().catch(function(error) {
|
||||
sinon.assert.calledWith(ImageUtils.getSizeAndType, testImageBlob_bogus);
|
||||
assert.equal(imgError, error);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getAttachmentContainer >', function() {
|
||||
test('uses only one container per attachment renderer', function() {
|
||||
var attachment = new Attachment(testImageBlob_bogus, {
|
||||
name: 'Image attachment'
|
||||
});
|
||||
|
||||
var attachmentRenderer1 = AttachmentRenderer.for(attachment),
|
||||
attachmentRenderer2 = AttachmentRenderer.for(attachment);
|
||||
|
||||
var attachmentContainer1 = attachmentRenderer1.getAttachmentContainer(),
|
||||
attachmentContainer2 = attachmentRenderer2.getAttachmentContainer();
|
||||
|
||||
assert.isNotNull(attachmentContainer1);
|
||||
assert.isNotNull(attachmentContainer2);
|
||||
|
||||
assert.notEqual(attachmentContainer1, attachmentContainer2);
|
||||
|
||||
var theSameAttachmentContainer1 =
|
||||
attachmentRenderer1.getAttachmentContainer();
|
||||
var theSameAttachmentContainer2 =
|
||||
attachmentRenderer2.getAttachmentContainer();
|
||||
|
||||
assert.equal(attachmentContainer1, theSameAttachmentContainer1);
|
||||
assert.equal(attachmentContainer2, theSameAttachmentContainer2);
|
||||
});
|
||||
|
||||
test('returns correct container for non-draft attachment', function() {
|
||||
var attachment = new Attachment(testImageBlob_bogus, {
|
||||
name: 'Image attachment'
|
||||
});
|
||||
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment);
|
||||
|
||||
var attachmentContainer = attachmentRenderer.getAttachmentContainer();
|
||||
|
||||
assert.isNotNull(attachmentContainer);
|
||||
assert.equal(attachmentContainer.nodeName.toLowerCase(), 'div');
|
||||
assert.isTrue(
|
||||
attachmentContainer.classList.contains('attachment-container')
|
||||
);
|
||||
assert.equal(attachmentContainer.dataset.attachmentType, attachment.type);
|
||||
});
|
||||
|
||||
test('returns correct container for draft attachment', function() {
|
||||
var attachment = new Attachment(testImageBlob_bogus, {
|
||||
name: 'Image attachment',
|
||||
isDraft: true
|
||||
});
|
||||
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment);
|
||||
|
||||
var attachmentContainer = attachmentRenderer.getAttachmentContainer();
|
||||
|
||||
assert.isNotNull(attachmentContainer);
|
||||
assert.equal(attachmentContainer.nodeName.toLowerCase(), 'iframe');
|
||||
assert.isTrue(
|
||||
attachmentContainer.classList.contains('attachment-container')
|
||||
);
|
||||
assert.equal(attachmentContainer.dataset.attachmentType, attachment.type);
|
||||
assert.equal(
|
||||
attachmentContainer.getAttribute('sandbox'),
|
||||
'allow-same-origin'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('updateFileSize >', function() {
|
||||
test('updates size in the same container', function(done) {
|
||||
var attachment = new Attachment(testImageBlob, {
|
||||
name: 'Image attachment'
|
||||
});
|
||||
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment);
|
||||
var attachmentContainer = attachmentRenderer.getAttachmentContainer();
|
||||
|
||||
var currentFileSize, sizeInfo;
|
||||
attachmentRenderer.render().then(() => {
|
||||
sizeInfo = attachmentContainer.querySelector('.size-indicator');
|
||||
currentFileSize = sizeInfo.dataset.l10nArgs;
|
||||
|
||||
attachment.blob = testImageBlob_small;
|
||||
return attachmentRenderer.updateFileSize();
|
||||
}).then(() => {
|
||||
var newFileSize = sizeInfo.dataset.l10nArgs;
|
||||
assert.notEqual(newFileSize, currentFileSize);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('updates size in iframes too', function(done) {
|
||||
this.sinon.spy(document.l10n, 'translateFragment');
|
||||
|
||||
var attachment = new Attachment(testImageBlob, {
|
||||
name: 'Image attachment',
|
||||
isDraft: true
|
||||
});
|
||||
|
||||
var attachmentRenderer = AttachmentRenderer.for(attachment);
|
||||
var attachmentContainer = attachmentRenderer.getAttachmentContainer();
|
||||
document.body.appendChild(attachmentContainer);
|
||||
|
||||
var currentFileSize, sizeInfo, node;
|
||||
attachmentRenderer.render().then(() => {
|
||||
node = attachmentContainer.contentDocument.documentElement;
|
||||
sizeInfo = node.querySelector('.size-indicator');
|
||||
currentFileSize = sizeInfo.dataset.l10nArgs;
|
||||
|
||||
attachment.blob = testImageBlob_small;
|
||||
return attachmentRenderer.updateFileSize();
|
||||
}).then(() => {
|
||||
var newFileSize = sizeInfo.dataset.l10nArgs;
|
||||
assert.notEqual(newFileSize, currentFileSize);
|
||||
|
||||
sinon.assert.calledWith(document.l10n.translateFragment, node);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
267
gaia/apps/sms/views/conversation/test/unit/attachment_test.js
Executable file
|
|
@ -0,0 +1,267 @@
|
|||
/*global MocksHelper, MockL10n, Attachment,
|
||||
AttachmentRenderer, MimeMapper, MockMozActivity, Promise, Utils,
|
||||
AssetsHelper
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/views/conversation/js/attachment.js');
|
||||
require('/views/shared/js/utils.js');
|
||||
|
||||
require('/views/shared/test/unit/mock_attachment_renderer.js');
|
||||
require('/shared/test/unit/mocks/mock_l20n.js');
|
||||
require('/views/shared/test/unit/mock_utils.js');
|
||||
require('/views/shared/test/unit/mock_moz_activity.js');
|
||||
require('/views/shared/test/unit/mock_mime_mapper.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'Utils',
|
||||
'MozActivity',
|
||||
'MimeMapper',
|
||||
'AttachmentRenderer'
|
||||
]).init();
|
||||
|
||||
suite('attachment_test.js', function() {
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
|
||||
var testImageBlob;
|
||||
|
||||
suiteSetup(function(done) {
|
||||
// this sometimes takes longer because we fetch 4 assets via XHR
|
||||
this.realMozL10n = document.l10n;
|
||||
document.l10n = MockL10n;
|
||||
|
||||
AssetsHelper.generateImageBlob(1400, 1400, 'image/jpeg', 1).then((blob) => {
|
||||
done(() => {
|
||||
assert.isTrue(
|
||||
blob.size > 300 * 1024,
|
||||
'Image blob should be greater than MMS size limit'
|
||||
);
|
||||
testImageBlob = blob;
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
document.l10n = this.realMozL10n;
|
||||
});
|
||||
|
||||
test('Name property defaults to a string value', function() {
|
||||
var attachment = new Attachment(new Blob());
|
||||
assert.typeOf(attachment.name, 'string');
|
||||
});
|
||||
|
||||
test('Filename is not a string', function() {
|
||||
var attachment = new Attachment(testImageBlob, {});
|
||||
|
||||
assert.equal(attachment.name, '');
|
||||
});
|
||||
|
||||
test('correct enumerable properties', function() {
|
||||
var expectedProperties = ['name', 'blob', 'isDraft'];
|
||||
var attachment = new Attachment(new Blob());
|
||||
attachment.render();
|
||||
|
||||
expectedProperties.forEach((prop) => assert.ok(prop in attachment));
|
||||
|
||||
var keys = Object.keys(attachment);
|
||||
assert.equal(keys.length, expectedProperties.length);
|
||||
});
|
||||
|
||||
suite('render attachment', function() {
|
||||
var attachmentRendererMock;
|
||||
|
||||
var attachmentContainer;
|
||||
|
||||
setup(function() {
|
||||
attachmentContainer = document.createElement('x-container');
|
||||
this.sinon.stub(AttachmentRenderer.prototype, 'getAttachmentContainer');
|
||||
AttachmentRenderer.prototype.getAttachmentContainer.returns(
|
||||
attachmentContainer
|
||||
);
|
||||
|
||||
attachmentRendererMock = new AttachmentRenderer();
|
||||
this.sinon.stub(AttachmentRenderer, 'for').returns(
|
||||
attachmentRendererMock
|
||||
);
|
||||
});
|
||||
|
||||
test('returns attachment container', function() {
|
||||
var attachment = new Attachment(new Blob(), {
|
||||
name: 'Image attachment'
|
||||
});
|
||||
|
||||
assert.equal(attachment.render(), attachmentContainer);
|
||||
});
|
||||
|
||||
test('calls ready callback in fail and success cases', function(done) {
|
||||
var attachmentRendererResult = Promise.resolve();
|
||||
|
||||
this.sinon.stub(attachmentRendererMock, 'render', function() {
|
||||
return attachmentRendererResult;
|
||||
});
|
||||
|
||||
var attachment = new Attachment(new Blob(), {
|
||||
name: 'Image attachment'
|
||||
});
|
||||
|
||||
var successfulDeferred = Utils.Promise.defer();
|
||||
attachment.render(function() {
|
||||
successfulDeferred.resolve();
|
||||
});
|
||||
|
||||
attachmentRendererResult = Promise.reject();
|
||||
|
||||
var failedDeferred = Utils.Promise.defer();
|
||||
attachment.render(function() {
|
||||
failedDeferred.resolve();
|
||||
});
|
||||
|
||||
successfulDeferred.promise.then(() => failedDeferred.promise).
|
||||
then(done, done);
|
||||
});
|
||||
|
||||
test('updateFileSize delegates to the renderer', function() {
|
||||
var firstAttachmentRenderer = new AttachmentRenderer();
|
||||
AttachmentRenderer.for.onCall(0).returns(
|
||||
firstAttachmentRenderer
|
||||
);
|
||||
|
||||
this.sinon.spy(AttachmentRenderer.prototype, 'render');
|
||||
this.sinon.spy(AttachmentRenderer.prototype, 'updateFileSize');
|
||||
|
||||
var attachment = new Attachment(new Blob());
|
||||
|
||||
attachment.render();
|
||||
attachment.updateFileSize();
|
||||
|
||||
sinon.assert.calledOnce(AttachmentRenderer.for);
|
||||
sinon.assert.alwaysCalledOn(
|
||||
firstAttachmentRenderer.render, firstAttachmentRenderer
|
||||
);
|
||||
sinon.assert.alwaysCalledOn(
|
||||
firstAttachmentRenderer.updateFileSize, firstAttachmentRenderer
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('view attachment with open activity', function() {
|
||||
setup(function() {
|
||||
this.sinon.spy(MimeMapper, 'guessTypeFromFileProperties');
|
||||
this.sinon.spy(MimeMapper, 'ensureFilenameMatchesType');
|
||||
});
|
||||
|
||||
test('Open normal image attachment', function(done) {
|
||||
var fileName = 'jpeg_image.jpg';
|
||||
var attachment = new Attachment(testImageBlob, {
|
||||
name: fileName
|
||||
});
|
||||
var typeSpy = MimeMapper.guessTypeFromFileProperties;
|
||||
var matchSpy = MimeMapper.ensureFilenameMatchesType;
|
||||
attachment.view().then(() => {
|
||||
assert.ok(typeSpy.calledWith(fileName, 'image/jpeg'));
|
||||
assert.ok(matchSpy.calledWith(fileName, typeSpy.returnValues[0]));
|
||||
assert.equal(MockMozActivity.calls.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Filename has no extension', function(done) {
|
||||
var fileName = 'jpeg_image';
|
||||
var attachment = new Attachment(testImageBlob, {
|
||||
name: fileName
|
||||
});
|
||||
var typeSpy = MimeMapper.guessTypeFromFileProperties;
|
||||
var matchSpy = MimeMapper.ensureFilenameMatchesType;
|
||||
attachment.view().then(() => {
|
||||
assert.ok(typeSpy.calledWith(fileName, 'image/jpeg'));
|
||||
assert.ok(matchSpy.calledWith(fileName, typeSpy.returnValues[0]));
|
||||
assert.equal(MockMozActivity.calls.length, 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Filename is overridden using single attachment folder',
|
||||
function(done) {
|
||||
|
||||
var attachment1 = new Attachment(testImageBlob, {
|
||||
name: '/some_path/.hidden_folder/attachment1.jpg'
|
||||
});
|
||||
|
||||
attachment1.view().then(() => {
|
||||
|
||||
assert.equal(MockMozActivity.calls.length, 1);
|
||||
assert.equal(
|
||||
MockMozActivity.calls[0].data.filename,
|
||||
'sms-attachments/attachment1.jpg'
|
||||
);
|
||||
|
||||
var attachment2 = new Attachment(testImageBlob, {
|
||||
name: 'attachment2.jpg'
|
||||
});
|
||||
|
||||
return attachment2.view();
|
||||
}).then(() => {
|
||||
assert.equal(MockMozActivity.calls.length, 2);
|
||||
assert.equal(
|
||||
MockMozActivity.calls[1].data.filename,
|
||||
'sms-attachments/attachment2.jpg'
|
||||
);
|
||||
}).then(done, done);
|
||||
|
||||
});
|
||||
|
||||
test('Filename is undefined',
|
||||
function(done) {
|
||||
|
||||
var attachment1 = new Attachment(testImageBlob, {
|
||||
name: undefined
|
||||
});
|
||||
|
||||
attachment1.view().then(() => {
|
||||
assert.equal(MockMozActivity.calls.length, 1);
|
||||
assert.equal(
|
||||
MockMozActivity.calls[0].data.filename,
|
||||
'sms-attachments/unnamed-attachment.jpg'
|
||||
);
|
||||
}).then(done, done);
|
||||
|
||||
});
|
||||
|
||||
suite('Activity errors >', function() {
|
||||
var activity;
|
||||
setup(function(done) {
|
||||
this.sinon.spy(window, 'MozActivity');
|
||||
this.sinon.stub(Utils, 'alert').returns(Promise.resolve());
|
||||
|
||||
var attachment = new Attachment(testImageBlob, {
|
||||
name: 'jpeg_image.jpg'
|
||||
});
|
||||
|
||||
attachment.view().then(() => {
|
||||
activity = window.MozActivity.firstCall.thisValue;
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('No handler for this image', function() {
|
||||
activity.onerror.call({
|
||||
error: { name: 'NO_PROVIDER' }
|
||||
});
|
||||
sinon.assert.calledWith(Utils.alert, 'attachmentOpenError');
|
||||
});
|
||||
|
||||
test('Activity is canceled', function() {
|
||||
activity.onerror.call({
|
||||
error: { name: 'ActivityCanceled' }
|
||||
});
|
||||
sinon.assert.notCalled(Utils.alert);
|
||||
});
|
||||
|
||||
test('Activity is canceled (on some other environment)', function() {
|
||||
activity.onerror.call({
|
||||
error: { name: 'USER_ABORT' }
|
||||
});
|
||||
sinon.assert.notCalled(Utils.alert);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
1955
gaia/apps/sms/views/conversation/test/unit/compose_test.js
Executable file
555
gaia/apps/sms/views/conversation/test/unit/conversation_integration_test.js
Executable file
|
|
@ -0,0 +1,555 @@
|
|||
/*global MocksHelper, MockL10n, ConversationView,
|
||||
loadBodyHTML, Compose, MessageManager, Navigation */
|
||||
|
||||
'use strict';
|
||||
|
||||
if (typeof GestureDetector === 'undefined') {
|
||||
require('/shared/js/gesture_detector.js');
|
||||
}
|
||||
require('/shared/js/event_dispatcher.js');
|
||||
require('/shared/test/unit/mocks/mock_gesture_detector.js');
|
||||
require('/shared/test/unit/mocks/mock_l20n.js');
|
||||
|
||||
require('/views/shared/test/unit/mock_contact.js');
|
||||
require('/services/test/unit/mock_message_manager.js');
|
||||
require('/views/shared/test/unit/mock_moz_activity.js');
|
||||
require('/views/shared/test/unit/mock_information.js');
|
||||
require('/views/shared/test/unit/mock_navigation.js');
|
||||
require('/views/shared/js/utils.js');
|
||||
require('/views/shared/js/settings.js');
|
||||
require('/views/conversation/js/subject_composer.js');
|
||||
require('/views/conversation/js/compose.js');
|
||||
require('/views/shared/js/contacts.js');
|
||||
require('/views/conversation/js/recipients.js');
|
||||
require('/views/inbox/js/inbox.js');
|
||||
require('/views/conversation/js/conversation.js');
|
||||
require('/views/conversation/js/attachment.js');
|
||||
require('/views/shared/js/contact_renderer.js');
|
||||
require('/views/shared/js/navigation.js');
|
||||
require('/services/test/unit/mock_threads.js');
|
||||
require('/services/test/unit/activity/mock_activity_client.js');
|
||||
|
||||
var mHelperIntegration = new MocksHelper([
|
||||
'MessageManager',
|
||||
'MozActivity',
|
||||
'Information',
|
||||
'Navigation',
|
||||
'Threads',
|
||||
'ActivityClient'
|
||||
]).init();
|
||||
|
||||
suite('ConversationView Integration', function() {
|
||||
var realMozL10n;
|
||||
var recipients;
|
||||
var children;
|
||||
var fixture;
|
||||
var sendButton;
|
||||
var input;
|
||||
|
||||
mHelperIntegration.attachTestHelpers();
|
||||
|
||||
if (typeof loadBodyHTML === 'undefined') {
|
||||
require('/shared/test/unit/load_body_html_helper.js');
|
||||
}
|
||||
|
||||
suiteSetup(function() {
|
||||
realMozL10n = document.l10n;
|
||||
document.l10n = MockL10n;
|
||||
|
||||
loadBodyHTML('/index.html');
|
||||
Navigation.init();
|
||||
ConversationView.init();
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
document.l10n = realMozL10n;
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
|
||||
sendButton = document.getElementById('messages-send-button');
|
||||
input = document.getElementById('messages-input');
|
||||
|
||||
fixture = {
|
||||
name: 'foo',
|
||||
number: '999',
|
||||
email: 'a@b.com',
|
||||
source: 'none',
|
||||
// Mapped to node attr, not true boolean
|
||||
editable: 'true'
|
||||
};
|
||||
|
||||
|
||||
ConversationView.recipients = null;
|
||||
ConversationView.initRecipients();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
recipients = null;
|
||||
fixture = null;
|
||||
});
|
||||
|
||||
suite('Search Contact Events', function() {
|
||||
|
||||
setup(function() {
|
||||
this.sinon.spy(ConversationView, 'searchContact');
|
||||
});
|
||||
|
||||
test('toFieldInput handler, successful', function() {
|
||||
var fakeEvent = {
|
||||
target: {
|
||||
isPlaceholder: true,
|
||||
textContent: 'abc'
|
||||
}
|
||||
};
|
||||
|
||||
ConversationView.toFieldInput.call(ConversationView, fakeEvent);
|
||||
|
||||
sinon.assert.calledOnce(ConversationView.searchContact);
|
||||
sinon.assert.calledWith(ConversationView.searchContact, 'abc');
|
||||
});
|
||||
|
||||
test('toFieldInput handler, unsuccessful', function() {
|
||||
var fakeEvent = {
|
||||
target: {
|
||||
isPlaceholder: true,
|
||||
textContent: ''
|
||||
}
|
||||
};
|
||||
|
||||
ConversationView.toFieldInput.call(ConversationView, fakeEvent);
|
||||
|
||||
sinon.assert.called(ConversationView.searchContact);
|
||||
sinon.assert.calledWith(ConversationView.searchContact, '');
|
||||
|
||||
fakeEvent.target.textContent = 'abd';
|
||||
fakeEvent.target.isPlaceholder = false;
|
||||
|
||||
ConversationView.toFieldInput.call(ConversationView, fakeEvent);
|
||||
|
||||
assert.isFalse(ConversationView.searchContact.calledTwice);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Recipient List Display', function() {
|
||||
test('Always begins in singleline mode', function() {
|
||||
|
||||
// Assert initial state: #messages-recipients-list is singleline
|
||||
assert.isFalse(
|
||||
ConversationView.recipientsList.classList.contains('multiline')
|
||||
);
|
||||
assert.isTrue(
|
||||
ConversationView.recipientsList.classList.contains('singleline')
|
||||
);
|
||||
|
||||
// Modify state
|
||||
ConversationView.recipients.visible('multiline');
|
||||
|
||||
// Assert modified state: #messages-recipients-list is multiline
|
||||
assert.isTrue(
|
||||
ConversationView.recipientsList.classList.contains('multiline')
|
||||
);
|
||||
assert.isFalse(
|
||||
ConversationView.recipientsList.classList.contains('singleline')
|
||||
);
|
||||
|
||||
// Reset state
|
||||
ConversationView.initRecipients();
|
||||
|
||||
// Assert initial/reset state: #messages-recipients-list is singleline
|
||||
assert.isFalse(
|
||||
ConversationView.recipientsList.classList.contains('multiline')
|
||||
);
|
||||
assert.isTrue(
|
||||
ConversationView.recipientsList.classList.contains('singleline')
|
||||
);
|
||||
});
|
||||
|
||||
test('Typing in list will switch to singline mode', function(done) {
|
||||
var toField = ConversationView.toField;
|
||||
var recipientsList = ConversationView.recipientsList;
|
||||
|
||||
children = recipientsList.children;
|
||||
recipients = ConversationView.recipients;
|
||||
|
||||
// Fill the recipients list with enough recipients to
|
||||
// enable the "pan" event
|
||||
recipients.add({
|
||||
number: '999999999999999999999999999999'
|
||||
});
|
||||
|
||||
recipients.add({
|
||||
number: '888888888888888888888888888888'
|
||||
});
|
||||
|
||||
// Since the offsetHeight is lazily captured during any
|
||||
// first event, trigger an event to ensure that the
|
||||
// "pan" event will have a height to calculate with.
|
||||
//
|
||||
toField.click();
|
||||
|
||||
// Simulate list growth
|
||||
recipientsList.style.height = '100px';
|
||||
|
||||
// Listen for the pan event, simulated immediately following.
|
||||
toField.addEventListener('pan', function panTest() {
|
||||
done(function() {
|
||||
assert.isTrue(toField.classList.contains('multiline'));
|
||||
|
||||
toField.removeEventListener('pan', panTest, false);
|
||||
|
||||
// Once the pan event has occured, the following steps
|
||||
// will simulate the user actions necessary for testing
|
||||
// the return to single line mode
|
||||
|
||||
// 1. Enter text into the editable "placeholder"
|
||||
children[2].textContent = '0';
|
||||
|
||||
// 2. Trigger a "keyup" event
|
||||
children[2].dispatchEvent(
|
||||
new CustomEvent('keyup', {
|
||||
bubbles: true
|
||||
})
|
||||
);
|
||||
|
||||
// toField should now be "singleline" again
|
||||
assert.isTrue(toField.classList.contains('singleline'));
|
||||
});
|
||||
});
|
||||
|
||||
// Simulate pan event
|
||||
toField.dispatchEvent(
|
||||
new CustomEvent('pan', {
|
||||
detail: {
|
||||
absolute: {
|
||||
dy: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('Clicking in list will switch to singline mode', function(done) {
|
||||
var toField = ConversationView.toField;
|
||||
var recipientsList = ConversationView.recipientsList;
|
||||
|
||||
children = recipientsList.children;
|
||||
recipients = ConversationView.recipients;
|
||||
|
||||
// Fill the recipients list with enough recipients to
|
||||
// enable the "multiline" view
|
||||
recipients.add({
|
||||
number: '999999999999999999999999999999'
|
||||
});
|
||||
|
||||
recipients.add({
|
||||
number: '888888888888888888888888888888'
|
||||
});
|
||||
|
||||
// Simulate list growth
|
||||
recipientsList.style.height = '100px';
|
||||
|
||||
// Listen for the pan event, simulated immediately following.
|
||||
toField.addEventListener('click', function clickTest() {
|
||||
done(function() {
|
||||
toField.removeEventListener('click', clickTest, false);
|
||||
|
||||
// toField should now be "singleline" again
|
||||
assert.isTrue(toField.classList.contains('singleline'));
|
||||
});
|
||||
});
|
||||
|
||||
// Artificially set the list to multiline view
|
||||
recipients.visible('multiline');
|
||||
|
||||
// Click "anywhere"
|
||||
toField.click();
|
||||
});
|
||||
});
|
||||
|
||||
suite('Recipient Input Behaviours', function() {
|
||||
var is = {
|
||||
corresponding: function(recipient, avatar, value) {
|
||||
return is.recipient(recipient, value) &&
|
||||
is.avatar(avatar, value);
|
||||
},
|
||||
recipient: function(candidate, value) {
|
||||
return (candidate.name === value || candidate.number === value);
|
||||
},
|
||||
avatar: function(candidate, value) {
|
||||
return !is.placeholder(candidate) &&
|
||||
!is.editable(candidate) && candidate.textContent.trim() === value;
|
||||
},
|
||||
placeholder: function(candidate, opts) {
|
||||
opts = opts || { isEmpty: true };
|
||||
return candidate.isPlaceholder &&
|
||||
((opts.isEmpty && candidate.textContent.trim() === '') || true);
|
||||
},
|
||||
editable: function(candidate) {
|
||||
return candidate.contentEditable === false;
|
||||
}
|
||||
};
|
||||
|
||||
setup(function() {
|
||||
this.sinon.stub(Navigation, 'isCurrentPanel').returns(false);
|
||||
Navigation.isCurrentPanel.withArgs('composer').returns(true);
|
||||
|
||||
this.sinon.stub(MessageManager, 'sendSMS');
|
||||
});
|
||||
|
||||
test('Assimilate stranded recipients (message input)', function() {
|
||||
|
||||
ConversationView.recipients.add({
|
||||
number: '999'
|
||||
});
|
||||
|
||||
children = ConversationView.recipientsList.children;
|
||||
recipients = ConversationView.recipients;
|
||||
|
||||
// There are one recipients...
|
||||
assert.equal(recipients.length, 1);
|
||||
// And two displayed children,
|
||||
// (the recipient "avatars" and a
|
||||
// placeholder for the next entry)
|
||||
assert.equal(children.length, 2);
|
||||
|
||||
assert.ok(is.corresponding(recipients.list[0], children[0], '999'));
|
||||
assert.ok(is.placeholder(children[1]));
|
||||
|
||||
// Set text in the placeholder, as if the user has typed
|
||||
// something before jumping to the input field
|
||||
children[1].textContent = '000';
|
||||
|
||||
// Simulate input field focus/entry
|
||||
input.dispatchEvent(new CustomEvent('focus'));
|
||||
|
||||
|
||||
// There are now two recipients...
|
||||
assert.equal(recipients.length, 2);
|
||||
// And three displayed children,
|
||||
// (the recipient "avatars" and a
|
||||
// placeholder for the next entry)
|
||||
assert.equal(children.length, 3);
|
||||
|
||||
assert.ok(is.corresponding(recipients.list[0], children[0], '999'));
|
||||
assert.ok(is.corresponding(recipients.list[1], children[1], '000'));
|
||||
assert.ok(is.placeholder(children[2]));
|
||||
});
|
||||
|
||||
test('Assimilate stranded recipients (attachButton)', function() {
|
||||
|
||||
ConversationView.recipients.add({
|
||||
number: '777'
|
||||
});
|
||||
|
||||
children = ConversationView.recipientsList.children;
|
||||
recipients = ConversationView.recipients;
|
||||
|
||||
// There are one recipients...
|
||||
assert.equal(recipients.length, 1);
|
||||
// And two displayed children,
|
||||
// (the recipient "avatars" and a
|
||||
// placeholder for the next entry)
|
||||
assert.equal(children.length, 2);
|
||||
|
||||
assert.ok(is.corresponding(recipients.list[0], children[0], '777'));
|
||||
assert.ok(is.placeholder(children[1]));
|
||||
|
||||
// Set text in the placeholder, as if the user has typed
|
||||
// something before jumping to the input field
|
||||
children[1].textContent = '555';
|
||||
|
||||
// Simulate input field focus/entry
|
||||
document.getElementById('messages-attach-button').click();
|
||||
|
||||
// There are now two recipients...
|
||||
assert.equal(recipients.length, 2);
|
||||
// And three displayed children,
|
||||
// (the recipient "avatars" and a
|
||||
// placeholder for the next entry)
|
||||
assert.equal(children.length, 3);
|
||||
|
||||
assert.ok(is.corresponding(recipients.list[0], children[0], '777'));
|
||||
assert.ok(is.corresponding(recipients.list[1], children[1], '555'));
|
||||
assert.ok(is.placeholder(children[2]));
|
||||
});
|
||||
|
||||
test('Assimilate stranded recipients (sendButton)', function() {
|
||||
// To ensure the onSendClick handler will succeed:
|
||||
|
||||
// 1. Add some content to the message
|
||||
Compose.append('foo');
|
||||
|
||||
// 2. Create a recipient
|
||||
ConversationView.recipients.add({
|
||||
number: '999'
|
||||
});
|
||||
|
||||
|
||||
children = ConversationView.recipientsList.children;
|
||||
recipients = ConversationView.recipients;
|
||||
|
||||
// There are one recipients...
|
||||
assert.equal(recipients.length, 1);
|
||||
// And two displayed children,
|
||||
// (the recipient "avatars" and a
|
||||
// placeholder for the next entry)
|
||||
assert.equal(children.length, 2);
|
||||
|
||||
assert.ok(is.corresponding(recipients.list[0], children[0], '999'));
|
||||
assert.ok(is.placeholder(children[1]));
|
||||
|
||||
// Set text in the placeholder, as if the user has typed
|
||||
// something before jumping to the input field
|
||||
children[1].textContent = '000';
|
||||
|
||||
// Simulate sendButton click
|
||||
ConversationView.onSendClick();
|
||||
ConversationView.simSelectedCallback(undefined, 0);
|
||||
|
||||
// Ensure that the "unaccepted" recipient was assimilated
|
||||
// and included in the recipients list when message was sent
|
||||
sinon.assert.calledWithMatch(MessageManager.sendSMS, {
|
||||
recipients: ['999', '000'],
|
||||
content: 'foo'
|
||||
});
|
||||
});
|
||||
|
||||
/* Bug:909641 test fails on ci
|
||||
test('Assimilate stranded recipients (contactPickButton)', function(done) {
|
||||
// To ensure the recipient wrapped before picker return:
|
||||
|
||||
// 1. Add some content to the message
|
||||
Compose.append('foo');
|
||||
|
||||
// 2. Create a recipient
|
||||
ConversationView.recipients.add({
|
||||
number: '111'
|
||||
});
|
||||
|
||||
children = ConversationView.recipientsList.children;
|
||||
recipients = ConversationView.recipients;
|
||||
|
||||
// Set text in the placeholder, as if the user has typed
|
||||
// something before jumping to the input field
|
||||
children[1].textContent = '222';
|
||||
|
||||
// Simulate contact pick
|
||||
ConversationView.requestContact();
|
||||
|
||||
// Simulate the picker activity success
|
||||
setTimeout(function onsuccess() {
|
||||
ConversationView.recipients.add({
|
||||
number: '333'
|
||||
});
|
||||
|
||||
// There are now three recipients after picker activity success
|
||||
assert.equal(recipients.length, 3);
|
||||
// And four displayed children,
|
||||
// (the recipient "avatars" and a
|
||||
// placeholder for the next entry)
|
||||
assert.equal(children.length, 4);
|
||||
assert.ok(is.corresponding(recipients.list[0], children[0], '111'));
|
||||
assert.ok(is.corresponding(recipients.list[1], children[1], '222'));
|
||||
assert.ok(is.corresponding(recipients.list[2], children[2], '333'));
|
||||
assert.ok(is.placeholder(children[3]));
|
||||
done();
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
test('Lone ";" are not recipients', function() {
|
||||
|
||||
|
||||
children = ConversationView.recipientsList.children;
|
||||
recipients = ConversationView.recipients;
|
||||
|
||||
// Set ";" in the placeholder, as if the user has typed
|
||||
children[0].textContent = ';';
|
||||
|
||||
// Simulate input field focus/entry
|
||||
input.click();
|
||||
|
||||
// There are no recipients...
|
||||
assert.equal(recipients.length, 0);
|
||||
// And one displayed child...
|
||||
assert.equal(children.length, 1);
|
||||
|
||||
// The the ";" has been removed from the
|
||||
// recipients list
|
||||
assert.equal(
|
||||
ConversationView.recipientsList.children[0].textContent, ''
|
||||
);
|
||||
});
|
||||
|
||||
test('Taps on in-progress recipients do nothing special', function() {
|
||||
|
||||
children = ConversationView.recipientsList.children;
|
||||
recipients = ConversationView.recipients;
|
||||
|
||||
// Set text in the placeholder, as if the user has typed
|
||||
children[0].textContent = '9999999999';
|
||||
|
||||
// Simulate a tap on the placeholder
|
||||
children[0].click();
|
||||
|
||||
// There should be no recipients created
|
||||
assert.equal(recipients.length, 0);
|
||||
|
||||
// And only the placeholder is displayed
|
||||
assert.equal(children.length, 1);
|
||||
assert.isTrue(children[0].isPlaceholder);
|
||||
});
|
||||
|
||||
/**
|
||||
* The code in this test works exactly as it should
|
||||
* when run as "standalone".
|
||||
*
|
||||
* The test runner appears to cause a loss of
|
||||
* node references that are expected to exist
|
||||
|
||||
test('<delete> removes "known" contact to the left', function() {
|
||||
function backspace(target) {
|
||||
var doc = target.ownerDocument;
|
||||
var view = doc.defaultView;
|
||||
|
||||
var event = doc.createEvent('KeyboardEvent');
|
||||
|
||||
event.initKeyEvent(
|
||||
'keypress', true, true, view,
|
||||
false, false, false, false,
|
||||
KeyEvent.DOM_VK_BACK_SPACE, 0
|
||||
);
|
||||
|
||||
target.dispatchEvent(event);
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
ConversationView.recipients = new Recipients({
|
||||
outer: 'messages-to-field',
|
||||
inner: 'messages-recipients-list',
|
||||
template: new Template('messages-recipient-tmpl')
|
||||
});
|
||||
|
||||
ConversationView.recipients.add({
|
||||
source: 'contacts',
|
||||
name: 'Rick',
|
||||
number: '99999'
|
||||
});
|
||||
|
||||
children = ConversationView.recipientsList.children;
|
||||
recipients = ConversationView.recipients;
|
||||
|
||||
// Simulate backspace on the current placeholder
|
||||
backspace(children[1]);
|
||||
|
||||
// There are no recipients...
|
||||
assert.equal(recipients.length, 0);
|
||||
// And one displayed placeholder
|
||||
assert.equal(children.length, 1);
|
||||
});
|
||||
*/
|
||||
|
||||
});
|
||||
});
|
||||
7655
gaia/apps/sms/views/conversation/test/unit/conversation_test.js
Executable file
1408
gaia/apps/sms/views/conversation/test/unit/information_test.js
Executable file
154
gaia/apps/sms/views/conversation/test/unit/link_action_handler_test.js
Executable file
|
|
@ -0,0 +1,154 @@
|
|||
/*global MocksHelper, MockL10n, LinkActionHandler, ActivityPicker,
|
||||
ConversationView,
|
||||
Contacts
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/shared/test/unit/mocks/mock_l20n.js');
|
||||
require('/shared/test/unit/mocks/mock_option_menu.js');
|
||||
|
||||
require('/views/conversation/js/link_action_handler.js');
|
||||
require('/views/shared/js/utils.js');
|
||||
|
||||
require('/views/shared/test/unit/mock_activity_picker.js');
|
||||
require('/views/shared/test/unit/mock_contacts.js');
|
||||
require('/views/shared/test/unit/mock_moz_activity.js');
|
||||
require('/views/shared/test/unit/mock_conversation.js');
|
||||
require('/views/shared/test/unit/mock_utils.js');
|
||||
|
||||
var mocksHelperLAH = new MocksHelper([
|
||||
'ActivityPicker',
|
||||
'Contacts',
|
||||
'MozActivity',
|
||||
'OptionMenu',
|
||||
'ConversationView',
|
||||
'Utils'
|
||||
]).init();
|
||||
|
||||
suite('LinkActionHandler', function() {
|
||||
var realMozL10n, events;
|
||||
|
||||
suiteSetup(function() {
|
||||
realMozL10n = document.l10n;
|
||||
document.l10n = MockL10n;
|
||||
mocksHelperLAH.suiteSetup();
|
||||
|
||||
events = {
|
||||
phone: {
|
||||
target: {
|
||||
dataset: {
|
||||
action: 'dial-link',
|
||||
dial: '999'
|
||||
}
|
||||
}
|
||||
},
|
||||
email: {
|
||||
target: {
|
||||
dataset: {
|
||||
action: 'email-link',
|
||||
email: 'a@b.com'
|
||||
}
|
||||
}
|
||||
},
|
||||
url: {
|
||||
target: {
|
||||
dataset: {
|
||||
action: 'url-link',
|
||||
url: 'http://mozilla.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
document.l10n = realMozL10n;
|
||||
mocksHelperLAH.suiteTeardown();
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
|
||||
Object.keys(events).forEach(function(type) {
|
||||
events[type].preventDefault = this.sinon.spy();
|
||||
events[type].stopPropagation = this.sinon.spy();
|
||||
}, this);
|
||||
|
||||
mocksHelperLAH.setup();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
mocksHelperLAH.teardown();
|
||||
});
|
||||
|
||||
|
||||
suite('onClick', function() {
|
||||
|
||||
setup(function() {
|
||||
mocksHelperLAH.setup();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
mocksHelperLAH.teardown();
|
||||
});
|
||||
|
||||
test('dial-link: (known) delegates to promptContact ', function() {
|
||||
this.sinon.stub(ConversationView, 'promptContact');
|
||||
this.sinon.stub(Contacts, 'findByPhoneNumber')
|
||||
.callsArgWith(1, [{
|
||||
name: ['Huey'],
|
||||
tel: {
|
||||
value: '999'
|
||||
}
|
||||
}]);
|
||||
|
||||
LinkActionHandler.onClick(events.phone);
|
||||
|
||||
assert.deepEqual(ConversationView.promptContact.args[0][0], {
|
||||
number: '999',
|
||||
inMessage: true
|
||||
});
|
||||
|
||||
assert.ok(events.phone.preventDefault.called);
|
||||
assert.ok(events.phone.stopPropagation.called);
|
||||
});
|
||||
|
||||
test('dial-link: (unknown) delegates to promptContact ', function() {
|
||||
this.sinon.stub(ConversationView, 'promptContact');
|
||||
this.sinon.stub(Contacts, 'findByPhoneNumber')
|
||||
.callsArgWith(1, []);
|
||||
|
||||
LinkActionHandler.onClick(events.phone);
|
||||
|
||||
assert.deepEqual(ConversationView.promptContact.args[0][0], {
|
||||
number: '999',
|
||||
inMessage: true
|
||||
});
|
||||
|
||||
assert.ok(events.phone.preventDefault.called);
|
||||
assert.ok(events.phone.stopPropagation.called);
|
||||
});
|
||||
|
||||
test('email-link: delegates to prompt ', function() {
|
||||
this.sinon.stub(ConversationView, 'prompt');
|
||||
|
||||
LinkActionHandler.onClick(events.email);
|
||||
|
||||
assert.ok(ConversationView.prompt.called);
|
||||
assert.deepEqual(ConversationView.prompt.args[0][0], {
|
||||
email: 'a@b.com',
|
||||
inMessage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('url-link: go directly to the action ', function() {
|
||||
this.sinon.stub(ActivityPicker, 'url');
|
||||
|
||||
LinkActionHandler.onClick(events.url);
|
||||
|
||||
assert.isTrue(ActivityPicker.url.called);
|
||||
assert.ok(events.url.preventDefault.called);
|
||||
assert.ok(events.url.stopPropagation.called);
|
||||
});
|
||||
});
|
||||
});
|
||||
377
gaia/apps/sms/views/conversation/test/unit/link_helper_test.js
Executable file
|
|
@ -0,0 +1,377 @@
|
|||
/*global LinkHelper, FixturePhones */
|
||||
|
||||
/*
|
||||
Link Helper Tests
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
require('/views/conversation/js/link_helper.js');
|
||||
|
||||
require('/views/shared/js/utils.js');
|
||||
require('/views/shared/test/unit/mock_utils.js');
|
||||
|
||||
suite('link_helper_test.js', function() {
|
||||
|
||||
suite('LinkHelper URL replacements', function() {
|
||||
|
||||
function url2msg(url, addprefix) {
|
||||
var link = url;
|
||||
|
||||
if (addprefix) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
|
||||
return '<a data-url="' + url + '"' +
|
||||
' data-action="url-link" >' + link + '</a>';
|
||||
}
|
||||
|
||||
function testURLMatch(url, match, addprefix) {
|
||||
var expected = url.replace(match, url2msg(match, addprefix));
|
||||
var result = LinkHelper.searchAndLinkClickableData(url);
|
||||
assert.equal(result, expected);
|
||||
}
|
||||
|
||||
function testURLOK(url, addprefix) {
|
||||
var expected = url2msg(url, addprefix);
|
||||
var result = LinkHelper.searchAndLinkClickableData(url);
|
||||
assert.equal(result, expected);
|
||||
}
|
||||
|
||||
function testURLNOK(url) {
|
||||
// we only test url here to make sure it doesn't match
|
||||
var result = LinkHelper.searchAndLinkUrl(url);
|
||||
assert.equal(result, url);
|
||||
}
|
||||
|
||||
suite('Matches', function() {
|
||||
test('Simple URL', function() {
|
||||
testURLOK('http://www.mozilla.org/');
|
||||
});
|
||||
test('Simple URL .co.uk', function() {
|
||||
testURLOK('http://www.mozilla.co.uk/');
|
||||
});
|
||||
test('Simple short URL', function() {
|
||||
testURLOK('mzl.la', true);
|
||||
});
|
||||
test('Simple short URL with parens', function() {
|
||||
testURLMatch('(mzl.la)', 'mzl.la', true);
|
||||
});
|
||||
test('Simple short URL with trailing dot', function() {
|
||||
testURLMatch('mzl.la.', 'mzl.la', true);
|
||||
});
|
||||
test('Simple short URL with trailing dot and port', function() {
|
||||
testURLMatch('mzl.la.:527', 'mzl.la', true);
|
||||
});
|
||||
test('Simple short URL with value', function() {
|
||||
testURLOK('mzl.la/ac', true);
|
||||
});
|
||||
test('Simple URL https', function() {
|
||||
testURLOK('https://www.mozilla.org');
|
||||
});
|
||||
test('Simple URL https without www.', function() {
|
||||
testURLOK('https://mozilla.org');
|
||||
});
|
||||
test('Simple URL https with trailing dot on domain.', function() {
|
||||
testURLOK('https://mozilla.org./blah');
|
||||
});
|
||||
test('Simple URL http with port', function() {
|
||||
testURLOK('http://www.mozilla.org:8080/');
|
||||
});
|
||||
test('Simple URL http without www. and with port', function() {
|
||||
testURLOK('http://mozilla.org:8080/');
|
||||
});
|
||||
test('Simple URL with IPv4', function() {
|
||||
testURLOK('http://8.8.8.8/');
|
||||
});
|
||||
test('Simple URL with IPv4 with zero', function() {
|
||||
testURLOK('http://8.0.8.8/');
|
||||
});
|
||||
test('Simple URL rtsp', function() {
|
||||
testURLOK('rtsp://www.mozilla.org/video.mov');
|
||||
});
|
||||
test('<br>prefix', function() {
|
||||
testURLMatch('<br>mozilla.org', 'mozilla.org', true);
|
||||
});
|
||||
test('Trailing question mark', function() {
|
||||
testURLMatch('OMG mozilla.org? it is cool!', 'mozilla.org', true);
|
||||
});
|
||||
test('Trailing colon', function() {
|
||||
testURLMatch('OMG mozilla.org: it is cool!', 'mozilla.org', true);
|
||||
});
|
||||
test('Trailing semi-colon', function() {
|
||||
testURLMatch('OMG mozilla.org; it is cool!', 'mozilla.org', true);
|
||||
});
|
||||
test('Trailing bang', function() {
|
||||
testURLMatch('OMG mozilla.org! it is cool!', 'mozilla.org', true);
|
||||
});
|
||||
test('Trailing period', function() {
|
||||
testURLMatch('Check out mozilla.org.', 'mozilla.org', true);
|
||||
});
|
||||
test('Trailing comma', function() {
|
||||
testURLMatch('Check out mozilla.org, or not', 'mozilla.org', true);
|
||||
});
|
||||
test('Trailing parens', function() {
|
||||
testURLMatch('(Check out mozilla.org)', 'mozilla.org', true);
|
||||
});
|
||||
test('Trailing question mark on path', function() {
|
||||
testURLMatch('OMG mzl.la/huh? it is cool!', 'mzl.la/huh', true);
|
||||
});
|
||||
test('Trailing colon on path', function() {
|
||||
testURLMatch('OMG mzl.la/huh: it is cool!', 'mzl.la/huh', true);
|
||||
});
|
||||
test('Trailing semi-colon on path', function() {
|
||||
testURLMatch('OMG mzl.la/huh; it is cool!', 'mzl.la/huh', true);
|
||||
});
|
||||
test('Trailing bang on path', function() {
|
||||
testURLMatch('OMG mzl.la/huh! it is cool!', 'mzl.la/huh', true);
|
||||
});
|
||||
test('Trailing parens on path', function() {
|
||||
testURLMatch('(Check out mzl.la/ac)', 'mzl.la/ac', true);
|
||||
});
|
||||
test('Trailing period on path', function() {
|
||||
testURLMatch('Check out mzl.la/ac.', 'mzl.la/ac', true);
|
||||
});
|
||||
test('Trailing comma on path', function() {
|
||||
testURLMatch('Check out mzl.la/ac, or not', 'mzl.la/ac', true);
|
||||
});
|
||||
test('wiki link that has paren', function() {
|
||||
testURLOK('http://en.wikipedia.org/wiki/Arete_(disambiguation)');
|
||||
});
|
||||
test('Crazy URL from #887146', function() {
|
||||
testURLOK('http://sani.tiz.ed.com/wap/videos/files_mm/' +
|
||||
'3GPP%20MPEG4%20-%20176×144_7fps_15kpbs_AACmono_8KHz_5kbps.3gp');
|
||||
});
|
||||
test('One letter second-level', function() {
|
||||
testURLOK('http://x.com');
|
||||
});
|
||||
test('URL with phone number in the middle', function() {
|
||||
testURLOK('http://somesite.com/q,12288296666/');
|
||||
});
|
||||
test('Full width char prefix', function() {
|
||||
testURLMatch(':mozilla.org', 'mozilla.org', true);
|
||||
});
|
||||
test('non ascii prefix', function() {
|
||||
testURLMatch('àmozilla.org', 'mozilla.org', true);
|
||||
});
|
||||
test('Bug 941763: Add dot as URL prefix', function() {
|
||||
testURLMatch('.website.com', 'website.com', true);
|
||||
testURLMatch('https://.website.com', 'website.com', true);
|
||||
testURLMatch('htt://www.mozilla.org', 'mozilla.org', true);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Failures', function() {
|
||||
test('Simple invalid URL with slashes', function() {
|
||||
testURLNOK('http://www.a/b/d.com');
|
||||
});
|
||||
test('Simple invalid TLD URL', function() {
|
||||
testURLNOK('http://www.mozilla.o');
|
||||
});
|
||||
test('Simple ellipse', function() {
|
||||
testURLNOK('aaa...ccc');
|
||||
});
|
||||
test('Simple URL with username', function() {
|
||||
testURLNOK('http://user@mozilla.org');
|
||||
});
|
||||
test('Simple URL with credentials', function() {
|
||||
testURLNOK('http://user:pass@mozilla.org');
|
||||
});
|
||||
test('Simple URL with IPv6', function() {
|
||||
testURLNOK('http://[::1]/');
|
||||
});
|
||||
test('Bug 890342 1', function() {
|
||||
testURLNOK('2.74');
|
||||
});
|
||||
test('Bug 890342 2', function() {
|
||||
testURLNOK('21.72');
|
||||
});
|
||||
|
||||
test('Bug 923739', function() {
|
||||
testURLNOK('foo.zz');
|
||||
testURLNOK('foo.foo');
|
||||
testURLNOK('foo.quux');
|
||||
|
||||
testURLOK('http://whatever.stuff');
|
||||
testURLOK('https://whatever.stuff');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('LinkHelper Email replacements', function() {
|
||||
|
||||
function email2msg(email) {
|
||||
return '<a data-email="' + email + '"' +
|
||||
' data-action="email-link">' + email + '</a>';
|
||||
}
|
||||
|
||||
function testEmailOK(email) {
|
||||
var exp = email2msg(email);
|
||||
var res = LinkHelper.searchAndLinkClickableData(email);
|
||||
assert.equal(res, exp);
|
||||
}
|
||||
|
||||
function testEmailNOK(email) {
|
||||
var res = LinkHelper.searchAndLinkEmail(email);
|
||||
assert.equal(res, email);
|
||||
}
|
||||
|
||||
test('Simple email', function() {
|
||||
testEmailOK('user@hostname.tld');
|
||||
});
|
||||
test('Simple email with alias', function() {
|
||||
testEmailOK('user+alias@hostname.tld');
|
||||
});
|
||||
test('Simple email with subdomain', function() {
|
||||
testEmailOK('user@cs.subdhostname.tld');
|
||||
});
|
||||
test('Invalid email no hostname', function() {
|
||||
testEmailNOK('user@');
|
||||
});
|
||||
test('Invalid email no user', function() {
|
||||
testEmailNOK('@hostname.tld');
|
||||
});
|
||||
});
|
||||
|
||||
suite('LinkHelper Phone replacements', function() {
|
||||
|
||||
function testPhoneMatch(text, match) {
|
||||
var expected = text.replace(match, phone2msg(match));
|
||||
var result = LinkHelper.searchAndLinkClickableData(text);
|
||||
assert.equal(result, expected);
|
||||
}
|
||||
|
||||
function phone2msg(phone) {
|
||||
return '<a dir="ltr" data-dial="' + phone + '"' +
|
||||
' data-action="dial-link">' + phone + '</a>';
|
||||
}
|
||||
|
||||
function testPhoneOK(phone) {
|
||||
var exp = phone2msg(phone);
|
||||
var res = LinkHelper.searchAndLinkClickableData(phone);
|
||||
assert.equal(res, exp);
|
||||
}
|
||||
|
||||
function testPhoneNOK(phone) {
|
||||
var res = LinkHelper.searchAndLinkPhone(phone);
|
||||
assert.equal(res, phone);
|
||||
}
|
||||
|
||||
suite('Matches', function() {
|
||||
test('Simple french mobile nat.', function() {
|
||||
testPhoneOK('0612345678');
|
||||
});
|
||||
|
||||
test('Simple french mobile intl', function() {
|
||||
testPhoneOK('+33612345678');
|
||||
});
|
||||
|
||||
test('Phone from #887737', function() {
|
||||
testPhoneOK('+5511 98907-6047');
|
||||
});
|
||||
|
||||
test('word before trailing number', function() {
|
||||
testPhoneMatch('Test1 600123123', '600123123');
|
||||
});
|
||||
test('word after preceding number', function() {
|
||||
testPhoneMatch('600123123 1Test', '600123123');
|
||||
});
|
||||
[
|
||||
// Bug 900413 - detect 5 digit short codes
|
||||
'24242',
|
||||
'10086',
|
||||
'+1234',
|
||||
// bug 941976 - 7 digit 4 digit with symbols should still work
|
||||
'123-1234 1234'
|
||||
].forEach(function(phone) {
|
||||
test(phone, testPhoneOK.bind(null, phone));
|
||||
});
|
||||
});
|
||||
|
||||
suite('Failures', function() {
|
||||
[
|
||||
'0.34',
|
||||
'1200',
|
||||
'5000',
|
||||
'0.122',
|
||||
'0921',
|
||||
// Bug 900413 - 5 digit codes with separators not allowed
|
||||
'1-10-13',
|
||||
'12-3-13',
|
||||
'1-800-A',
|
||||
'1(234)5',
|
||||
'1234..5',
|
||||
'1 2 3 45',
|
||||
'1\t23\t45',
|
||||
'12+34'
|
||||
].forEach(function(phone) {
|
||||
test(phone, testPhoneNOK.bind(null, phone));
|
||||
});
|
||||
});
|
||||
|
||||
suite('Varied cases', function() {
|
||||
FixturePhones.forEach(function(fixture) {
|
||||
if (fixture.isTestable) {
|
||||
suite(fixture.title, function() {
|
||||
fixture.values.forEach(function(value) {
|
||||
test(value, function() {
|
||||
testPhoneOK(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
suite('Tricky Problems', function() {
|
||||
test('Two 9 digit numbers separated by space (#892480)', function() {
|
||||
var test = '123456789 987654321';
|
||||
var expected = test.split(' ').map(phone2msg).join(' ');
|
||||
var result = LinkHelper.searchAndLinkClickableData(test);
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
test('Two 6 digit numbers separated by newline (#892480)', function() {
|
||||
var test = '222333\n333222';
|
||||
var expected = test.split('\n').map(phone2msg).join('\n');
|
||||
var result = LinkHelper.searchAndLinkClickableData(test);
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
test('7 digit and 9 digit (#941976)', function() {
|
||||
var test = '1234567 987654321';
|
||||
var expected = test.split(' ').map(phone2msg).join(' ');
|
||||
var result = LinkHelper.searchAndLinkClickableData(test);
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
test('7 digit and 5 digit (#941976)', function() {
|
||||
var test = '1234567 12345';
|
||||
var expected = test.split(' ').map(phone2msg).join(' ');
|
||||
var result = LinkHelper.searchAndLinkClickableData(test);
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
test('7 digit and 5 digit(with plus) (#941976)', function() {
|
||||
var test = '1234567 +1234';
|
||||
var expected = test.split(' ').map(phone2msg).join(' ');
|
||||
var result = LinkHelper.searchAndLinkClickableData(test);
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
test('7 digit and 4 digit (#941976)', function() {
|
||||
testPhoneMatch('1234567 1234', '1234567');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
suite('Multiple in the same string', function() {
|
||||
test('Hits every case', function() {
|
||||
var test = 'Hey, check out http://stackoverflow.com/q/12882966/ and ' +
|
||||
'call me at +18155551212 or (e-mail user@hostname.tld)';
|
||||
var expected = 'Hey, check out ' +
|
||||
'<a data-url="http://stackoverflow.com/q/12882966/" ' +
|
||||
'data-action="url-link" >http://stackoverflow.com/q/12882966/</a>' +
|
||||
' and call me at ' +
|
||||
'<a dir="ltr" data-dial="+18155551212" data-action="dial-link">' +
|
||||
'+18155551212</a> or (e-mail <a data-email="user@hostname.tld"' +
|
||||
' data-action="email-link">user@hostname.tld</a>)';
|
||||
assert.equal(LinkHelper.searchAndLinkClickableData(test), expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
1208
gaia/apps/sms/views/conversation/test/unit/recipients_test.js
Executable file
154
gaia/apps/sms/views/conversation/test/unit/startup_test.js
Executable file
|
|
@ -0,0 +1,154 @@
|
|||
/*global App,
|
||||
ConversationView,
|
||||
Information,
|
||||
InterInstanceEventDispatcher,
|
||||
LazyLoader,
|
||||
LocalizationHelper,
|
||||
MessageManager,
|
||||
MessagingClient,
|
||||
MozMobileConnectionsClient,
|
||||
MocksHelper,
|
||||
Navigation,
|
||||
Settings,
|
||||
Startup,
|
||||
TimeHeaders,
|
||||
Utils
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/views/shared/test/unit/mock_time_headers.js');
|
||||
require('/views/shared/test/unit/mock_navigation.js');
|
||||
require('/views/shared/test/unit/mock_settings.js');
|
||||
require('/views/shared/test/unit/mock_inter_instance_event_dispatcher.js');
|
||||
require('/views/shared/test/unit/mock_conversation.js');
|
||||
require('/views/shared/test/unit/mock_information.js');
|
||||
require('/views/shared/test/unit/mock_localization_helper.js');
|
||||
require('/views/shared/test/unit/mock_app.js');
|
||||
require('/shared/test/unit/mocks/mock_lazy_loader.js');
|
||||
require('/services/test/unit/messaging/mock_messaging_client.js');
|
||||
require('/services/test/unit/moz_mobile_connections/' +
|
||||
'mock_moz_mobile_connections_client.js');
|
||||
require('/services/test/unit/mock_message_manager.js');
|
||||
require('/views/shared/js/utils.js');
|
||||
require('/views/shared/test/unit/mock_utils.js');
|
||||
|
||||
require('/views/conversation/js/startup.js');
|
||||
|
||||
var MocksHelperForInboxStartup = new MocksHelper([
|
||||
'App',
|
||||
'ConversationView',
|
||||
'Information',
|
||||
'InterInstanceEventDispatcher',
|
||||
'LazyLoader',
|
||||
'LocalizationHelper',
|
||||
'MessageManager',
|
||||
'MessagingClient',
|
||||
'MozMobileConnectionsClient',
|
||||
'Navigation',
|
||||
'TimeHeaders',
|
||||
'Settings',
|
||||
'Utils'
|
||||
]).init();
|
||||
|
||||
suite('ConversationView Startup,', function() {
|
||||
MocksHelperForInboxStartup.attachTestHelpers();
|
||||
|
||||
setup(function() {
|
||||
this.sinon.spy(MessageManager, 'init');
|
||||
this.sinon.spy(Navigation, 'init');
|
||||
this.sinon.spy(ConversationView, 'init');
|
||||
this.sinon.spy(LocalizationHelper, 'init');
|
||||
this.sinon.spy(Information, 'initDefaultViews');
|
||||
this.sinon.spy(TimeHeaders, 'init');
|
||||
this.sinon.spy(Settings, 'init');
|
||||
this.sinon.spy(Navigation, 'setReady');
|
||||
this.sinon.spy(InterInstanceEventDispatcher, 'connect');
|
||||
this.sinon.stub(ConversationView, 'once');
|
||||
this.sinon.stub(LazyLoader, 'load').returns(Promise.resolve());
|
||||
this.sinon.stub(MessageManager, 'getThreads');
|
||||
this.sinon.stub(Navigation, 'isDefaultPanel');
|
||||
this.sinon.spy(MessagingClient, 'init');
|
||||
this.sinon.spy(MozMobileConnectionsClient, 'init');
|
||||
this.sinon.spy(Utils, 'initializeShimHost');
|
||||
});
|
||||
|
||||
suite('In default panel,', function() {
|
||||
setup(function() {
|
||||
Navigation.isDefaultPanel.returns(true);
|
||||
|
||||
Startup.init();
|
||||
});
|
||||
|
||||
test('correctly initializes dependencies', function(done) {
|
||||
sinon.assert.calledWith(Utils.initializeShimHost, App.instanceId);
|
||||
sinon.assert.calledOnce(MessageManager.init);
|
||||
sinon.assert.calledOnce(ConversationView.init);
|
||||
|
||||
// Workaround to deal with temporary shim code.
|
||||
MessageManager.getThreads.lastCall.args[0].done();
|
||||
Promise.resolve().then(
|
||||
() => sinon.assert.calledOnce(Navigation.init)
|
||||
).then(done, done);
|
||||
});
|
||||
|
||||
test('starts non-critical initializations only once view is visually ready',
|
||||
function() {
|
||||
// Lazy loading is not started yet and headers are not processed either.
|
||||
sinon.assert.notCalled(LazyLoader.load);
|
||||
|
||||
ConversationView.once.withArgs('visually-loaded').yield();
|
||||
|
||||
sinon.assert.calledOnce(LazyLoader.load);
|
||||
});
|
||||
|
||||
test('correctly initializes lazy dependencies', function(done) {
|
||||
ConversationView.once.withArgs('visually-loaded').yield();
|
||||
|
||||
LazyLoader.load.lastCall.returnValue.then(() => {
|
||||
sinon.assert.calledOnce(LocalizationHelper.init);
|
||||
sinon.assert.calledOnce(TimeHeaders.init);
|
||||
sinon.assert.calledOnce(Settings.init);
|
||||
sinon.assert.calledOnce(Navigation.setReady);
|
||||
sinon.assert.calledOnce(Information.initDefaultViews);
|
||||
sinon.assert.calledOnce(InterInstanceEventDispatcher.connect);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('In a subpanel,', function() {
|
||||
setup(function() {
|
||||
Navigation.isDefaultPanel.returns(false);
|
||||
|
||||
Startup.init();
|
||||
});
|
||||
|
||||
test('correctly initializes dependencies', function(done) {
|
||||
sinon.assert.calledWith(Utils.initializeShimHost, App.instanceId);
|
||||
sinon.assert.calledOnce(MessageManager.init);
|
||||
sinon.assert.calledOnce(ConversationView.init);
|
||||
|
||||
// Workaround to deal with temporary shim code.
|
||||
MessageManager.getThreads.lastCall.args[0].done();
|
||||
Promise.resolve().then(
|
||||
() => sinon.assert.calledOnce(Navigation.init)
|
||||
).then(done, done);
|
||||
});
|
||||
|
||||
test('starts non-critical initializations right away', function() {
|
||||
sinon.assert.notCalled(ConversationView.once);
|
||||
sinon.assert.calledOnce(LazyLoader.load);
|
||||
});
|
||||
|
||||
test('correctly initializes lazy dependencies', function(done) {
|
||||
LazyLoader.load.lastCall.returnValue.then(() => {
|
||||
sinon.assert.calledOnce(LocalizationHelper.init);
|
||||
sinon.assert.calledOnce(TimeHeaders.init);
|
||||
sinon.assert.calledOnce(Settings.init);
|
||||
sinon.assert.calledOnce(Navigation.setReady);
|
||||
sinon.assert.calledOnce(Information.initDefaultViews);
|
||||
sinon.assert.calledOnce(InterInstanceEventDispatcher.connect);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
367
gaia/apps/sms/views/conversation/test/unit/subject_composer_test.js
Executable file
|
|
@ -0,0 +1,367 @@
|
|||
/*global loadBodyHTML,
|
||||
FocusEvent,
|
||||
InputEvent,
|
||||
KeyEvent,
|
||||
KeyboardEvent,
|
||||
SubjectComposer
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/shared/js/event_dispatcher.js');
|
||||
|
||||
require('/views/conversation/js/subject_composer.js');
|
||||
|
||||
suite('SubjectComposer >', function() {
|
||||
var node, subjectComposer, input;
|
||||
|
||||
setup(function() {
|
||||
loadBodyHTML('/index.html');
|
||||
|
||||
node = document.querySelector('.js-subject-composer');
|
||||
subjectComposer = new SubjectComposer(node);
|
||||
input = node.querySelector('.subject-composer-input');
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
subjectComposer.reset();
|
||||
});
|
||||
|
||||
test('correctly instantiate', function() {
|
||||
assert.throws(
|
||||
() => new SubjectComposer(),
|
||||
'Subject node is required'
|
||||
);
|
||||
|
||||
assert.ok(new SubjectComposer(node));
|
||||
assert.isTrue(
|
||||
node.classList.contains('hide'),
|
||||
'Should be hidden by default'
|
||||
);
|
||||
});
|
||||
|
||||
suite('visibility >', function() {
|
||||
teardown(function() {
|
||||
subjectComposer.reset();
|
||||
});
|
||||
|
||||
test('show and hide work correctly', function() {
|
||||
var visibilityChangeStub = sinon.stub();
|
||||
subjectComposer.on('visibility-change', visibilityChangeStub);
|
||||
|
||||
subjectComposer.show();
|
||||
|
||||
assert.isFalse(node.classList.contains('hide'));
|
||||
sinon.assert.called(visibilityChangeStub);
|
||||
|
||||
subjectComposer.hide();
|
||||
|
||||
assert.isTrue(node.classList.contains('hide'));
|
||||
sinon.assert.called(visibilityChangeStub);
|
||||
});
|
||||
|
||||
test('keeps value between show and hide', function() {
|
||||
subjectComposer.show();
|
||||
subjectComposer.setValue('my value');
|
||||
|
||||
assert.equal(subjectComposer.getValue(), 'my value');
|
||||
assert.equal(input.textContent, 'my value');
|
||||
|
||||
// Hide subject and show again
|
||||
subjectComposer.hide();
|
||||
subjectComposer.show();
|
||||
|
||||
assert.equal(subjectComposer.getValue(), 'my value');
|
||||
assert.equal(input.textContent, 'my value');
|
||||
});
|
||||
|
||||
test('isVisible tracks visibility correctly', function() {
|
||||
assert.isFalse(subjectComposer.isVisible());
|
||||
|
||||
subjectComposer.show();
|
||||
assert.isTrue(subjectComposer.isVisible());
|
||||
|
||||
subjectComposer.hide();
|
||||
assert.isFalse(subjectComposer.isVisible());
|
||||
});
|
||||
});
|
||||
|
||||
suite('value management >', function() {
|
||||
setup(function() {
|
||||
subjectComposer.show();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
subjectComposer.reset();
|
||||
});
|
||||
|
||||
test('correctly sets and gets value', function() {
|
||||
assert.throws(
|
||||
() => subjectComposer.setValue(), 'Value should be a valid string'
|
||||
);
|
||||
assert.equal(subjectComposer.getValue(), '');
|
||||
|
||||
subjectComposer.setValue('test');
|
||||
|
||||
assert.equal(subjectComposer.getValue(), 'test');
|
||||
assert.equal(input.textContent, 'test');
|
||||
|
||||
input.innerHTML = 'test#2<br><br>';
|
||||
input.dispatchEvent(new InputEvent('input'));
|
||||
|
||||
assert.equal(subjectComposer.getValue(), 'test#2');
|
||||
assert.equal(input.textContent, 'test#2');
|
||||
|
||||
subjectComposer.setValue('test#3');
|
||||
|
||||
assert.equal(subjectComposer.getValue(), 'test#3');
|
||||
assert.equal(input.textContent, 'test#3');
|
||||
|
||||
input.innerHTML = '<br>test#4<br><br>';
|
||||
input.dispatchEvent(new InputEvent('input'));
|
||||
|
||||
assert.equal(subjectComposer.getValue(), 'test#4');
|
||||
assert.equal(input.textContent, 'test#4');
|
||||
});
|
||||
|
||||
test('correctly pre-process value', function() {
|
||||
var content = 'Line\u00A01<br>\n Line 2<br><br><br><br>\nLine\n 3<br>';
|
||||
|
||||
subjectComposer.setValue(content);
|
||||
|
||||
assert.equal(
|
||||
subjectComposer.getValue(),
|
||||
'Line 1<br> Line 2<br><br><br><br> Line 3<br>'
|
||||
);
|
||||
|
||||
subjectComposer.setValue('');
|
||||
input.innerHTML = content;
|
||||
input.dispatchEvent(new InputEvent('input'));
|
||||
|
||||
assert.equal(subjectComposer.getValue(), 'Line 1 Line 2 Line 3');
|
||||
|
||||
subjectComposer.setValue('');
|
||||
input.textContent = content;
|
||||
input.dispatchEvent(new InputEvent('input'));
|
||||
|
||||
assert.equal(
|
||||
subjectComposer.getValue(),
|
||||
'Line 1<br> Line 2<br><br><br><br> Line 3<br>'
|
||||
);
|
||||
});
|
||||
|
||||
test('fires "change" only if value actually changes via setValue',
|
||||
function() {
|
||||
var onChange = sinon.stub();
|
||||
subjectComposer.on('change', onChange);
|
||||
|
||||
subjectComposer.setValue('test');
|
||||
sinon.assert.calledOnce(onChange);
|
||||
|
||||
subjectComposer.setValue('test');
|
||||
sinon.assert.calledOnce(onChange);
|
||||
|
||||
subjectComposer.setValue('test#2');
|
||||
sinon.assert.calledTwice(onChange);
|
||||
});
|
||||
|
||||
test('fires "change" only if value actually changes via innerHTML',
|
||||
function() {
|
||||
var onChange = sinon.stub();
|
||||
subjectComposer.on('change', onChange);
|
||||
|
||||
input.innerHTML = 'test';
|
||||
input.dispatchEvent(new InputEvent('input'));
|
||||
sinon.assert.calledOnce(onChange);
|
||||
|
||||
input.innerHTML = 'test';
|
||||
input.dispatchEvent(new InputEvent('input'));
|
||||
sinon.assert.calledOnce(onChange);
|
||||
|
||||
input.innerHTML = 'test#2';
|
||||
input.dispatchEvent(new InputEvent('input'));
|
||||
sinon.assert.calledTwice(onChange);
|
||||
});
|
||||
|
||||
test('correctly manages placeholder', function() {
|
||||
var placeholder = node.querySelector(
|
||||
'.subject-composer-placeholder'
|
||||
);
|
||||
|
||||
assert.isNotNull(placeholder, 'Placeholder should be presented');
|
||||
assert.equal(
|
||||
placeholder.getAttribute('data-l10n-id'),
|
||||
'messagesSubjectInput_placeholder'
|
||||
);
|
||||
|
||||
subjectComposer.setValue('a');
|
||||
assert.isNull(
|
||||
node.querySelector('.subject-composer-placeholder'),
|
||||
'Placeholder should be removed'
|
||||
);
|
||||
|
||||
input.textContent = '';
|
||||
input.dispatchEvent(new InputEvent('input'));
|
||||
assert.isNotNull(
|
||||
node.querySelector('.subject-composer-placeholder'),
|
||||
'Placeholder should be presented'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('Backspace key handling >', function() {
|
||||
var downEvent, upEvent, backspace;
|
||||
|
||||
setup(function() {
|
||||
downEvent = new KeyboardEvent('keydown', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
keyCode: KeyEvent.DOM_VK_BACK_SPACE
|
||||
});
|
||||
|
||||
upEvent = new KeyboardEvent('keyup', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
keyCode: KeyEvent.DOM_VK_BACK_SPACE
|
||||
});
|
||||
|
||||
backspace = function() {
|
||||
input.dispatchEvent(downEvent);
|
||||
input.dispatchEvent(upEvent);
|
||||
};
|
||||
|
||||
subjectComposer.show();
|
||||
subjectComposer.setValue('Howdy!');
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
subjectComposer.reset();
|
||||
});
|
||||
|
||||
test('<delete> in empty subject hides field', function() {
|
||||
// 1. Assert the correct state condition updates have occurred,
|
||||
// as described in step 1
|
||||
assert.isTrue(subjectComposer.isVisible());
|
||||
|
||||
// 2. To simulate the user "deleting" the subject,
|
||||
// set the value to an empty string.
|
||||
input.textContent = '';
|
||||
input.dispatchEvent(new InputEvent('input'));
|
||||
|
||||
// 3. Simulate backspace on the subject field
|
||||
backspace();
|
||||
|
||||
// 4. Confirm that the state of the compose
|
||||
// area has updated properly.
|
||||
assert.isFalse(subjectComposer.isVisible());
|
||||
});
|
||||
|
||||
test('<delete> in non-empty subject does not hide field', function() {
|
||||
// 1. Assert the correct state condition updates have occurred,
|
||||
// as described in step 1
|
||||
assert.isTrue(subjectComposer.isVisible());
|
||||
|
||||
// 2. Simulate backspace on the subject field
|
||||
backspace();
|
||||
|
||||
// 3. Confirm that the state of the compose area not changed.
|
||||
assert.isTrue(subjectComposer.isVisible());
|
||||
});
|
||||
|
||||
test('<delete> holding subject does not hide field', function() {
|
||||
// 1. Assert the correct state condition updates have occurred,
|
||||
// as described in step 1
|
||||
assert.isTrue(subjectComposer.isVisible());
|
||||
|
||||
// 2. Simulate holding backspace on the subject field
|
||||
for (var i = 0; i < 5; i++) {
|
||||
input.dispatchEvent(downEvent);
|
||||
}
|
||||
|
||||
// 3. This is the "release" from a holding state
|
||||
input.dispatchEvent(upEvent);
|
||||
|
||||
// 4. Confirm that the state of the compose area not changed.
|
||||
assert.isTrue(subjectComposer.isVisible());
|
||||
});
|
||||
|
||||
test('<delete> holding subject, release and tap hides field', function() {
|
||||
// 1. Assert the correct state condition updates have occurred,
|
||||
// as described in step 1
|
||||
assert.isTrue(subjectComposer.isVisible());
|
||||
|
||||
// 2. Simulate holding backspace on the subject field
|
||||
for (var i = 0; i < 5; i++) {
|
||||
input.dispatchEvent(downEvent);
|
||||
}
|
||||
|
||||
// 3. This is the "release" from a holding state
|
||||
input.dispatchEvent(upEvent);
|
||||
|
||||
// 4. To simulate the user "deleting" the subject,
|
||||
// set the value to an empty string.
|
||||
input.textContent = '';
|
||||
input.dispatchEvent(new InputEvent('input'));
|
||||
|
||||
// 5. Simulate backspace on the subject field.
|
||||
backspace();
|
||||
|
||||
// 6. Confirm that the state of the compose area not changed.
|
||||
assert.isFalse(subjectComposer.isVisible());
|
||||
});
|
||||
});
|
||||
|
||||
suite('Return key handling >', function() {
|
||||
test('Return key should be ignored at key down', function() {
|
||||
subjectComposer.show();
|
||||
subjectComposer.setValue('Howdy!');
|
||||
|
||||
var event = new KeyboardEvent('keydown', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
keyCode: KeyEvent.DOM_VK_RETURN
|
||||
});
|
||||
this.sinon.spy(event, 'stopPropagation');
|
||||
input.dispatchEvent(event);
|
||||
|
||||
assert.ok(event.defaultPrevented);
|
||||
sinon.assert.called(event.stopPropagation);
|
||||
});
|
||||
});
|
||||
|
||||
test('correctly resets subject', function() {
|
||||
var onChangeStub = sinon.stub();
|
||||
|
||||
subjectComposer.show();
|
||||
|
||||
assert.equal(subjectComposer.getValue(), '');
|
||||
|
||||
subjectComposer.setValue('test');
|
||||
|
||||
assert.equal(subjectComposer.getValue(), 'test');
|
||||
assert.equal(input.textContent, 'test');
|
||||
|
||||
subjectComposer.on('visibility-change', onChangeStub);
|
||||
subjectComposer.on('change', onChangeStub);
|
||||
subjectComposer.reset();
|
||||
|
||||
assert.equal(subjectComposer.getValue(), '');
|
||||
assert.equal(input.textContent, '');
|
||||
assert.isFalse(subjectComposer.isVisible());
|
||||
// Visibility change and change events should not be fired on reset
|
||||
sinon.assert.notCalled(onChangeStub);
|
||||
});
|
||||
|
||||
test('fires "focus" event', function() {
|
||||
subjectComposer.show();
|
||||
|
||||
var onFocusStub = sinon.stub();
|
||||
|
||||
subjectComposer.on('focus', onFocusStub);
|
||||
|
||||
input.dispatchEvent(new FocusEvent('focus'));
|
||||
|
||||
sinon.assert.calledOnce(onFocusStub);
|
||||
});
|
||||
});
|
||||