mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-14 05:45:37 +00:00
Bug 1478144 - Add TippyTop RemoteSettings, TaskCluster MochiTests and bug fixes to Activity Stream. r=ursula
MozReview-Commit-ID: CwJ4zU7CvnD --HG-- extra : rebase_source : 88f29fe32245e7b73faa399df0d69a454eb19865
This commit is contained in:
parent
40b40ad653
commit
077dad8094
@ -1,11 +1,12 @@
|
||||
options:
|
||||
merge-default-rules: true
|
||||
max-warnings: 0
|
||||
|
||||
files:
|
||||
include: 'content-src/**/*.scss'
|
||||
|
||||
rules:
|
||||
class-name-format: [{convention: ["hyphenatedlowercase", "camelcase"]}]
|
||||
class-name-format: 0
|
||||
extends-before-declarations: 2
|
||||
extends-before-mixins: 2
|
||||
force-element-nesting: 0
|
||||
|
28
browser/extensions/activity-stream/.taskcluster.yml
Normal file
28
browser/extensions/activity-stream/.taskcluster.yml
Normal file
@ -0,0 +1,28 @@
|
||||
version: 0
|
||||
tasks:
|
||||
- provisionerId: '{{ taskcluster.docker.provisionerId }}'
|
||||
workerType: '{{ taskcluster.docker.workerType }}'
|
||||
extra:
|
||||
github:
|
||||
events:
|
||||
- pull_request.opened
|
||||
- pull_request.reopened
|
||||
- pull_request.synchronize
|
||||
- push
|
||||
payload:
|
||||
maxRunTime: 7200
|
||||
image: piatra/asmochitests
|
||||
command:
|
||||
- /bin/bash
|
||||
- '--login'
|
||||
- '-c'
|
||||
- >-
|
||||
git clone {{event.head.repo.url}} /activity-stream && cd /activity-stream &&
|
||||
git checkout {{event.head.sha}} && bash ./mochitest.sh
|
||||
metadata:
|
||||
name: activitystream
|
||||
description: run mochitests for PRs
|
||||
owner: '{{ event.head.user.email }}'
|
||||
source: '{{ event.head.repo.url }}'
|
||||
allowPullRequests: public
|
||||
|
@ -1,5 +1,7 @@
|
||||
# activity-stream
|
||||
|
||||
[![Task Status](https://github.taskcluster.net/v1/repository/mozilla/activity-stream/master/badge.svg)](https://github.taskcluster.net/v1/repository/mozilla/activity-stream/master/latest)
|
||||
|
||||
This system add-on replaces the new tab page in Firefox with a new design and
|
||||
functionality as part of the Activity Stream project.
|
||||
|
||||
|
@ -137,6 +137,7 @@ export class ASRouterUISurface extends React.PureComponent {
|
||||
}
|
||||
|
||||
sendImpression(extraProps) {
|
||||
ASRouterUtils.sendMessage({type: "IMPRESSION", data: this.state.message});
|
||||
this.sendUserActionTelemetry({event: "IMPRESSION", ...extraProps});
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,9 @@ Field name | Type | Required | Description | Example / Note
|
||||
`campaign` | `string` | No | Campaign id that the message belongs to | `RustWebAssembly`
|
||||
`targeting` | `string` `JEXL` | No | A [JEXL expression](http://normandy.readthedocs.io/en/latest/user/filter_expressions.html#jexl-basics) with all targeting information needed in order to decide if the message is shown | Not yet implemented, [Examples](#targeting-attributes)
|
||||
`trigger` | `string` | No | An event or condition upon which the message will be immediately shown. This can be combined with `targeting`. Messages that define a trigger will not be shown during non-trigger-based passive message rotation.
|
||||
`frequency` | `object` | No | A definition for frequency cap information for the message
|
||||
`frequency.lifetime` | `integer` | No | The maximum number of lifetime impressions for the message.
|
||||
`frequency.custom` | `array` | No | An array of frequency cap definition objects including `period`, a time period in milliseconds, and `cap`, a max number of impressions for that period.
|
||||
|
||||
### Message example
|
||||
```javascript
|
||||
@ -19,6 +22,11 @@ Field name | Type | Required | Description | Example / Note
|
||||
content: {
|
||||
title: "Find it faster",
|
||||
body: "Access all of your favorite search engines with a click. Search the whole Web or just one website from the search box."
|
||||
},
|
||||
targeting: "hasFxAccount && !addonsInfo.addons['activity-stream@mozilla.org']",
|
||||
frequency: {
|
||||
lifetime: 20,
|
||||
custom: [{period: "daily", cap: 5}, {period: 3600000, cap: 1}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -2,6 +2,7 @@
|
||||
"title": "ProviderResponse",
|
||||
"description": "A response object for remote providers of AS Router",
|
||||
"type": "object",
|
||||
"version": "0.1.0",
|
||||
"properties": {
|
||||
"messages": {
|
||||
"type": "array",
|
||||
@ -31,6 +32,49 @@
|
||||
"trigger": {
|
||||
"type": "string",
|
||||
"description": "A string representing what the trigger to show this message is."
|
||||
},
|
||||
"frequency": {
|
||||
"type": "object",
|
||||
"description": "An object containing frequency cap information for a message.",
|
||||
"properties": {
|
||||
"lifetime": {
|
||||
"type": "integer",
|
||||
"description": "The maximum lifetime impressions for a message.",
|
||||
"minimum": 1,
|
||||
"maximum": 100
|
||||
},
|
||||
"custom": {
|
||||
"type": "array",
|
||||
"description": "An array of custom frequency cap definitions.",
|
||||
"items": {
|
||||
"description": "A frequency cap definition containing time and max impression information",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"period": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Period of time in milliseconds (e.g. 86400000 for one day)"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "One of a preset list of short forms for period of time (e.g. 'daily' for one day)",
|
||||
"enum": ["daily"]
|
||||
}
|
||||
]
|
||||
|
||||
},
|
||||
"cap": {
|
||||
"type": "integer",
|
||||
"description": "The maximum impressions for the message within the defined period.",
|
||||
"minimum": 1,
|
||||
"maximum": 100
|
||||
}
|
||||
},
|
||||
"required": ["period", "cap"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["id", "template", "content"]
|
||||
|
@ -27,7 +27,7 @@
|
||||
},
|
||||
"text": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/plainText"},
|
||||
{"$ref": "#/definitions/richText"},
|
||||
{"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}
|
||||
]
|
||||
},
|
||||
|
@ -54,16 +54,18 @@ export class ASRouterAdmin extends React.PureComponent {
|
||||
renderMessageItem(msg) {
|
||||
const isCurrent = msg.id === this.state.lastMessageId;
|
||||
const isBlocked = this.state.blockList.includes(msg.id);
|
||||
const impressions = this.state.impressions[msg.id] ? this.state.impressions[msg.id].length : 0;
|
||||
|
||||
let itemClassName = "message-item";
|
||||
if (isCurrent) { itemClassName += " current"; }
|
||||
if (isBlocked) { itemClassName += " blocked"; }
|
||||
|
||||
return (<tr className={itemClassName} key={msg.id}>
|
||||
<td className="message-id"><span>{msg.id}</span></td>
|
||||
<td className="message-id"><span>{msg.id} <br /></span></td>
|
||||
<td>
|
||||
<button className={`button ${(isBlocked ? "" : " primary")}`} onClick={isBlocked ? this.handleUnblock(msg) : this.handleBlock(msg)}>{isBlocked ? "Unblock" : "Block"}</button>
|
||||
{isBlocked ? null : <button className="button" onClick={this.handleOverride(msg.id)}>Show</button>}
|
||||
<br />({impressions} impressions)
|
||||
</td>
|
||||
<td className="message-summary">
|
||||
<pre>{JSON.stringify(msg, null, 2)}</pre>
|
||||
|
@ -15,10 +15,6 @@
|
||||
}
|
||||
|
||||
main {
|
||||
.hide-main & {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
margin: auto;
|
||||
// Offset the snippets container so things at the bottom of the page are still
|
||||
// visible when snippets / onboarding are visible. Adjust for other spacing.
|
||||
@ -45,6 +41,11 @@ main {
|
||||
margin-bottom: $section-spacing;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hide-main & {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.base-content-fallback {
|
||||
|
@ -247,8 +247,9 @@
|
||||
&:not(.no-description) .card-title {
|
||||
font-size: $card-title-font-size;
|
||||
line-height: $card-title-font-size + 1;
|
||||
max-height: $card-title-font-size + 1;
|
||||
max-height: $card-title-font-size + 5;
|
||||
overflow: hidden;
|
||||
padding: 0 0 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -72,11 +72,11 @@
|
||||
}
|
||||
|
||||
.background,
|
||||
body.hide-main {
|
||||
body.hide-main { // sass-lint:disable-line no-qualifying-elements
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
background-image: url('#{$image-path}fox-tail.png'), linear-gradient(to bottom, $blue-70 40%, #004EC2 60%, $blue-60 80%, #0080FF 90%, #00C7FF 100%);
|
||||
background-image: url('#{$image-path}fox-tail.png'), $about-welcome-gradient;
|
||||
background-position-x: center;
|
||||
background-position-y: -200px, top;
|
||||
background-repeat: no-repeat;
|
||||
@ -104,7 +104,7 @@ body.hide-main {
|
||||
font-size: 12px;
|
||||
max-width: 340px;
|
||||
margin: 17px 50px;
|
||||
color: #676F7E;
|
||||
color: $about-welcome-extra-links;
|
||||
cursor: default;
|
||||
|
||||
a {
|
||||
@ -239,7 +239,7 @@ body.hide-main {
|
||||
padding-bottom: 210px;
|
||||
}
|
||||
|
||||
a.firstrun-link {
|
||||
a.firstrun-link { // sass-lint:disable-line no-qualifying-elements
|
||||
color: $white;
|
||||
display: block;
|
||||
text-decoration: underline;
|
||||
|
@ -18,12 +18,12 @@ body {
|
||||
font-size: 16px;
|
||||
overflow-y: scroll;
|
||||
|
||||
&.hide-onboarding > #onboarding-overlay-button,
|
||||
&.hide-main > #onboarding-overlay-button {
|
||||
display: none !important;
|
||||
&.hide-onboarding > #onboarding-overlay-button, // sass-lint:disable-line no-ids
|
||||
&.hide-main > #onboarding-overlay-button { // sass-lint:disable-line no-ids
|
||||
display: none !important; // sass-lint:disable-line no-important
|
||||
}
|
||||
|
||||
&.hide-main > #onboarding-notification-bar {
|
||||
&.hide-main > #onboarding-notification-bar { // sass-lint:disable-line no-ids
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -74,62 +74,61 @@ body {
|
||||
// Snippets
|
||||
--newtab-snippets-background-color: $white;
|
||||
--newtab-snippets-hairline-color: transparent;
|
||||
}
|
||||
|
||||
// Dark theme
|
||||
body[lwt-newtab-brighttext] {
|
||||
// General styles
|
||||
--newtab-background-color: $grey-80;
|
||||
--newtab-border-primary-color: $grey-10-80;
|
||||
--newtab-border-secondary-color: $grey-10-10;
|
||||
--newtab-button-primary-color: $blue-60;
|
||||
--newtab-button-secondary-color: $grey-70;
|
||||
--newtab-element-active-color: $grey-10-20;
|
||||
--newtab-element-hover-color: $grey-10-10;
|
||||
--newtab-icon-primary-color: $grey-10-80;
|
||||
--newtab-icon-secondary-color: $grey-10-40;
|
||||
--newtab-icon-tertiary-color: $grey-10-40;
|
||||
--newtab-inner-box-shadow-color: $grey-10-20;
|
||||
--newtab-link-primary-color: $blue-40;
|
||||
--newtab-link-secondary-color: $pocket-teal;
|
||||
--newtab-text-conditional-color: $grey-10;
|
||||
--newtab-text-primary-color: $grey-10;
|
||||
--newtab-text-secondary-color: $grey-10-80;
|
||||
--newtab-textbox-background-color: $grey-70;
|
||||
--newtab-textbox-border: $grey-10-20;
|
||||
@include textbox-focus($blue-40); // sass-lint:disable-line mixins-before-declarations
|
||||
|
||||
// Context menu
|
||||
--newtab-contextmenu-background-color: $grey-60;
|
||||
--newtab-contextmenu-button-color: $grey-80;
|
||||
|
||||
// Modal + overlay
|
||||
--newtab-modal-color: $grey-80;
|
||||
--newtab-overlay-color: $grey-90-80;
|
||||
|
||||
// Sections
|
||||
--newtab-section-header-text-color: $grey-10-80;
|
||||
--newtab-section-navigation-text-color: $grey-10-80;
|
||||
--newtab-section-active-contextmenu-color: $white;
|
||||
|
||||
// Search
|
||||
--newtab-search-border-color: $grey-10-20;
|
||||
--newtab-search-dropdown-color: $grey-70;
|
||||
--newtab-search-dropdown-header-color: $grey-60;
|
||||
--newtab-search-icon-color: $grey-10-60;
|
||||
|
||||
// Top Sites
|
||||
--newtab-topsites-background-color: $grey-70;
|
||||
--newtab-topsites-icon-shadow: none;
|
||||
--newtab-topsites-label-color: $grey-10-80;
|
||||
|
||||
// Cards
|
||||
--newtab-card-active-outline-color: $grey-60;
|
||||
--newtab-card-background-color: $grey-70;
|
||||
--newtab-card-hairline-color: $grey-10-10;
|
||||
--newtab-card-shadow: 0 1px 8px 0 $grey-90-20;
|
||||
|
||||
// Snippets
|
||||
--newtab-snippets-background-color: $grey-70;
|
||||
--newtab-snippets-hairline-color: $white-10;
|
||||
|
||||
&[lwt-newtab-brighttext] {
|
||||
// General styles
|
||||
--newtab-background-color: $grey-80;
|
||||
--newtab-border-primary-color: $grey-10-80;
|
||||
--newtab-border-secondary-color: $grey-10-10;
|
||||
--newtab-button-primary-color: $blue-60;
|
||||
--newtab-button-secondary-color: $grey-70;
|
||||
--newtab-element-active-color: $grey-10-20;
|
||||
--newtab-element-hover-color: $grey-10-10;
|
||||
--newtab-icon-primary-color: $grey-10-80;
|
||||
--newtab-icon-secondary-color: $grey-10-40;
|
||||
--newtab-icon-tertiary-color: $grey-10-40;
|
||||
--newtab-inner-box-shadow-color: $grey-10-20;
|
||||
--newtab-link-primary-color: $blue-40;
|
||||
--newtab-link-secondary-color: $pocket-teal;
|
||||
--newtab-text-conditional-color: $grey-10;
|
||||
--newtab-text-primary-color: $grey-10;
|
||||
--newtab-text-secondary-color: $grey-10-80;
|
||||
--newtab-textbox-background-color: $grey-70;
|
||||
--newtab-textbox-border: $grey-10-20;
|
||||
@include textbox-focus($blue-40); // sass-lint:disable-line mixins-before-declarations
|
||||
|
||||
// Context menu
|
||||
--newtab-contextmenu-background-color: $grey-60;
|
||||
--newtab-contextmenu-button-color: $grey-80;
|
||||
|
||||
// Modal + overlay
|
||||
--newtab-modal-color: $grey-80;
|
||||
--newtab-overlay-color: $grey-90-80;
|
||||
|
||||
// Sections
|
||||
--newtab-section-header-text-color: $grey-10-80;
|
||||
--newtab-section-navigation-text-color: $grey-10-80;
|
||||
--newtab-section-active-contextmenu-color: $white;
|
||||
|
||||
// Search
|
||||
--newtab-search-border-color: $grey-10-20;
|
||||
--newtab-search-dropdown-color: $grey-70;
|
||||
--newtab-search-dropdown-header-color: $grey-60;
|
||||
--newtab-search-icon-color: $grey-10-60;
|
||||
|
||||
// Top Sites
|
||||
--newtab-topsites-background-color: $grey-70;
|
||||
--newtab-topsites-icon-shadow: none;
|
||||
--newtab-topsites-label-color: $grey-10-80;
|
||||
|
||||
// Cards
|
||||
--newtab-card-active-outline-color: $grey-60;
|
||||
--newtab-card-background-color: $grey-70;
|
||||
--newtab-card-hairline-color: $grey-10-10;
|
||||
--newtab-card-shadow: 0 1px 8px 0 $grey-90-20;
|
||||
|
||||
// Snippets
|
||||
--newtab-snippets-background-color: $grey-70;
|
||||
--newtab-snippets-hairline-color: $white-10;
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,11 @@ $download-icon-fill: #12BC00;
|
||||
$pocket-icon-fill: #D70022;
|
||||
$email-input-focus: rgba($blue-50, 0.3);
|
||||
$email-input-invalid: rgba($red-60, 0.3);
|
||||
$aw-extra-blue-1: #004EC2;
|
||||
$aw-extra-blue-2: #0080FF;
|
||||
$aw-extra-blue-3: #00C7FF;
|
||||
$about-welcome-gradient: linear-gradient(to bottom, $blue-70 40%, $aw-extra-blue-1 60%, $blue-60 80%, $aw-extra-blue-2 90%, $aw-extra-blue-3 100%);
|
||||
$about-welcome-extra-links: #676F7E;
|
||||
|
||||
// Photon transitions from http://design.firefox.com/photon/motion/duration-and-easing.html
|
||||
$photon-easing: cubic-bezier(0.07, 0.95, 0, 1);
|
||||
|
@ -65,48 +65,47 @@ body {
|
||||
--newtab-card-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
|
||||
--newtab-snippets-background-color: #FFF;
|
||||
--newtab-snippets-hairline-color: transparent; }
|
||||
|
||||
body[lwt-newtab-brighttext] {
|
||||
--newtab-background-color: #2A2A2E;
|
||||
--newtab-border-primary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-button-primary-color: #0060DF;
|
||||
--newtab-button-secondary-color: #38383D;
|
||||
--newtab-element-active-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-element-hover-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-icon-primary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-icon-secondary-color: rgba(249, 249, 250, 0.4);
|
||||
--newtab-icon-tertiary-color: rgba(249, 249, 250, 0.4);
|
||||
--newtab-inner-box-shadow-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-link-primary-color: #45A1FF;
|
||||
--newtab-link-secondary-color: #50BCB6;
|
||||
--newtab-text-conditional-color: #F9F9FA;
|
||||
--newtab-text-primary-color: #F9F9FA;
|
||||
--newtab-text-secondary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-textbox-background-color: #38383D;
|
||||
--newtab-textbox-border: rgba(249, 249, 250, 0.2);
|
||||
--newtab-textbox-focus-color: #45A1FF;
|
||||
--newtab-textbox-focus-boxshadow: 0 0 0 1px #45A1FF, 0 0 0 4px rgba(69, 161, 255, 0.3);
|
||||
--newtab-contextmenu-background-color: #4A4A4F;
|
||||
--newtab-contextmenu-button-color: #2A2A2E;
|
||||
--newtab-modal-color: #2A2A2E;
|
||||
--newtab-overlay-color: rgba(12, 12, 13, 0.8);
|
||||
--newtab-section-header-text-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-section-navigation-text-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-section-active-contextmenu-color: #FFF;
|
||||
--newtab-search-border-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-search-dropdown-color: #38383D;
|
||||
--newtab-search-dropdown-header-color: #4A4A4F;
|
||||
--newtab-search-icon-color: rgba(249, 249, 250, 0.6);
|
||||
--newtab-topsites-background-color: #38383D;
|
||||
--newtab-topsites-icon-shadow: none;
|
||||
--newtab-topsites-label-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-card-active-outline-color: #4A4A4F;
|
||||
--newtab-card-background-color: #38383D;
|
||||
--newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
|
||||
--newtab-snippets-background-color: #38383D;
|
||||
--newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1); }
|
||||
body[lwt-newtab-brighttext] {
|
||||
--newtab-background-color: #2A2A2E;
|
||||
--newtab-border-primary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-button-primary-color: #0060DF;
|
||||
--newtab-button-secondary-color: #38383D;
|
||||
--newtab-element-active-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-element-hover-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-icon-primary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-icon-secondary-color: rgba(249, 249, 250, 0.4);
|
||||
--newtab-icon-tertiary-color: rgba(249, 249, 250, 0.4);
|
||||
--newtab-inner-box-shadow-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-link-primary-color: #45A1FF;
|
||||
--newtab-link-secondary-color: #50BCB6;
|
||||
--newtab-text-conditional-color: #F9F9FA;
|
||||
--newtab-text-primary-color: #F9F9FA;
|
||||
--newtab-text-secondary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-textbox-background-color: #38383D;
|
||||
--newtab-textbox-border: rgba(249, 249, 250, 0.2);
|
||||
--newtab-textbox-focus-color: #45A1FF;
|
||||
--newtab-textbox-focus-boxshadow: 0 0 0 1px #45A1FF, 0 0 0 4px rgba(69, 161, 255, 0.3);
|
||||
--newtab-contextmenu-background-color: #4A4A4F;
|
||||
--newtab-contextmenu-button-color: #2A2A2E;
|
||||
--newtab-modal-color: #2A2A2E;
|
||||
--newtab-overlay-color: rgba(12, 12, 13, 0.8);
|
||||
--newtab-section-header-text-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-section-navigation-text-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-section-active-contextmenu-color: #FFF;
|
||||
--newtab-search-border-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-search-dropdown-color: #38383D;
|
||||
--newtab-search-dropdown-header-color: #4A4A4F;
|
||||
--newtab-search-icon-color: rgba(249, 249, 250, 0.6);
|
||||
--newtab-topsites-background-color: #38383D;
|
||||
--newtab-topsites-icon-shadow: none;
|
||||
--newtab-topsites-label-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-card-active-outline-color: #4A4A4F;
|
||||
--newtab-card-background-color: #38383D;
|
||||
--newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
|
||||
--newtab-snippets-background-color: #38383D;
|
||||
--newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1); }
|
||||
|
||||
.icon {
|
||||
background-position: center center;
|
||||
@ -335,8 +334,6 @@ main {
|
||||
margin: auto;
|
||||
padding-bottom: 68px;
|
||||
width: 274px; }
|
||||
.hide-main main {
|
||||
visibility: hidden; }
|
||||
@media (min-width: 482px) {
|
||||
main {
|
||||
width: 402px; } }
|
||||
@ -352,6 +349,8 @@ main {
|
||||
main section {
|
||||
margin-bottom: 20px;
|
||||
position: relative; }
|
||||
.hide-main main {
|
||||
visibility: hidden; }
|
||||
|
||||
.base-content-fallback {
|
||||
height: 100vh; }
|
||||
@ -1475,8 +1474,9 @@ a.firstrun-link {
|
||||
.compact-cards .card-outer .card-text:not(.no-description) .card-title {
|
||||
font-size: 12px;
|
||||
line-height: 13px;
|
||||
max-height: 13px;
|
||||
max-height: 17px;
|
||||
overflow: hidden;
|
||||
padding: 0 0 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
.compact-cards .card-outer .card-description {
|
||||
|
File diff suppressed because one or more lines are too long
@ -68,48 +68,47 @@ body {
|
||||
--newtab-card-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
|
||||
--newtab-snippets-background-color: #FFF;
|
||||
--newtab-snippets-hairline-color: transparent; }
|
||||
|
||||
body[lwt-newtab-brighttext] {
|
||||
--newtab-background-color: #2A2A2E;
|
||||
--newtab-border-primary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-button-primary-color: #0060DF;
|
||||
--newtab-button-secondary-color: #38383D;
|
||||
--newtab-element-active-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-element-hover-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-icon-primary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-icon-secondary-color: rgba(249, 249, 250, 0.4);
|
||||
--newtab-icon-tertiary-color: rgba(249, 249, 250, 0.4);
|
||||
--newtab-inner-box-shadow-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-link-primary-color: #45A1FF;
|
||||
--newtab-link-secondary-color: #50BCB6;
|
||||
--newtab-text-conditional-color: #F9F9FA;
|
||||
--newtab-text-primary-color: #F9F9FA;
|
||||
--newtab-text-secondary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-textbox-background-color: #38383D;
|
||||
--newtab-textbox-border: rgba(249, 249, 250, 0.2);
|
||||
--newtab-textbox-focus-color: #45A1FF;
|
||||
--newtab-textbox-focus-boxshadow: 0 0 0 1px #45A1FF, 0 0 0 4px rgba(69, 161, 255, 0.3);
|
||||
--newtab-contextmenu-background-color: #4A4A4F;
|
||||
--newtab-contextmenu-button-color: #2A2A2E;
|
||||
--newtab-modal-color: #2A2A2E;
|
||||
--newtab-overlay-color: rgba(12, 12, 13, 0.8);
|
||||
--newtab-section-header-text-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-section-navigation-text-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-section-active-contextmenu-color: #FFF;
|
||||
--newtab-search-border-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-search-dropdown-color: #38383D;
|
||||
--newtab-search-dropdown-header-color: #4A4A4F;
|
||||
--newtab-search-icon-color: rgba(249, 249, 250, 0.6);
|
||||
--newtab-topsites-background-color: #38383D;
|
||||
--newtab-topsites-icon-shadow: none;
|
||||
--newtab-topsites-label-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-card-active-outline-color: #4A4A4F;
|
||||
--newtab-card-background-color: #38383D;
|
||||
--newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
|
||||
--newtab-snippets-background-color: #38383D;
|
||||
--newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1); }
|
||||
body[lwt-newtab-brighttext] {
|
||||
--newtab-background-color: #2A2A2E;
|
||||
--newtab-border-primary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-button-primary-color: #0060DF;
|
||||
--newtab-button-secondary-color: #38383D;
|
||||
--newtab-element-active-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-element-hover-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-icon-primary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-icon-secondary-color: rgba(249, 249, 250, 0.4);
|
||||
--newtab-icon-tertiary-color: rgba(249, 249, 250, 0.4);
|
||||
--newtab-inner-box-shadow-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-link-primary-color: #45A1FF;
|
||||
--newtab-link-secondary-color: #50BCB6;
|
||||
--newtab-text-conditional-color: #F9F9FA;
|
||||
--newtab-text-primary-color: #F9F9FA;
|
||||
--newtab-text-secondary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-textbox-background-color: #38383D;
|
||||
--newtab-textbox-border: rgba(249, 249, 250, 0.2);
|
||||
--newtab-textbox-focus-color: #45A1FF;
|
||||
--newtab-textbox-focus-boxshadow: 0 0 0 1px #45A1FF, 0 0 0 4px rgba(69, 161, 255, 0.3);
|
||||
--newtab-contextmenu-background-color: #4A4A4F;
|
||||
--newtab-contextmenu-button-color: #2A2A2E;
|
||||
--newtab-modal-color: #2A2A2E;
|
||||
--newtab-overlay-color: rgba(12, 12, 13, 0.8);
|
||||
--newtab-section-header-text-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-section-navigation-text-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-section-active-contextmenu-color: #FFF;
|
||||
--newtab-search-border-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-search-dropdown-color: #38383D;
|
||||
--newtab-search-dropdown-header-color: #4A4A4F;
|
||||
--newtab-search-icon-color: rgba(249, 249, 250, 0.6);
|
||||
--newtab-topsites-background-color: #38383D;
|
||||
--newtab-topsites-icon-shadow: none;
|
||||
--newtab-topsites-label-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-card-active-outline-color: #4A4A4F;
|
||||
--newtab-card-background-color: #38383D;
|
||||
--newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
|
||||
--newtab-snippets-background-color: #38383D;
|
||||
--newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1); }
|
||||
|
||||
.icon {
|
||||
background-position: center center;
|
||||
@ -338,8 +337,6 @@ main {
|
||||
margin: auto;
|
||||
padding-bottom: 68px;
|
||||
width: 274px; }
|
||||
.hide-main main {
|
||||
visibility: hidden; }
|
||||
@media (min-width: 482px) {
|
||||
main {
|
||||
width: 402px; } }
|
||||
@ -355,6 +352,8 @@ main {
|
||||
main section {
|
||||
margin-bottom: 20px;
|
||||
position: relative; }
|
||||
.hide-main main {
|
||||
visibility: hidden; }
|
||||
|
||||
.base-content-fallback {
|
||||
height: 100vh; }
|
||||
@ -1478,8 +1477,9 @@ a.firstrun-link {
|
||||
.compact-cards .card-outer .card-text:not(.no-description) .card-title {
|
||||
font-size: 12px;
|
||||
line-height: 13px;
|
||||
max-height: 13px;
|
||||
max-height: 17px;
|
||||
overflow: hidden;
|
||||
padding: 0 0 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
.compact-cards .card-outer .card-description {
|
||||
|
File diff suppressed because one or more lines are too long
@ -65,48 +65,47 @@ body {
|
||||
--newtab-card-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.1);
|
||||
--newtab-snippets-background-color: #FFF;
|
||||
--newtab-snippets-hairline-color: transparent; }
|
||||
|
||||
body[lwt-newtab-brighttext] {
|
||||
--newtab-background-color: #2A2A2E;
|
||||
--newtab-border-primary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-button-primary-color: #0060DF;
|
||||
--newtab-button-secondary-color: #38383D;
|
||||
--newtab-element-active-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-element-hover-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-icon-primary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-icon-secondary-color: rgba(249, 249, 250, 0.4);
|
||||
--newtab-icon-tertiary-color: rgba(249, 249, 250, 0.4);
|
||||
--newtab-inner-box-shadow-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-link-primary-color: #45A1FF;
|
||||
--newtab-link-secondary-color: #50BCB6;
|
||||
--newtab-text-conditional-color: #F9F9FA;
|
||||
--newtab-text-primary-color: #F9F9FA;
|
||||
--newtab-text-secondary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-textbox-background-color: #38383D;
|
||||
--newtab-textbox-border: rgba(249, 249, 250, 0.2);
|
||||
--newtab-textbox-focus-color: #45A1FF;
|
||||
--newtab-textbox-focus-boxshadow: 0 0 0 1px #45A1FF, 0 0 0 4px rgba(69, 161, 255, 0.3);
|
||||
--newtab-contextmenu-background-color: #4A4A4F;
|
||||
--newtab-contextmenu-button-color: #2A2A2E;
|
||||
--newtab-modal-color: #2A2A2E;
|
||||
--newtab-overlay-color: rgba(12, 12, 13, 0.8);
|
||||
--newtab-section-header-text-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-section-navigation-text-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-section-active-contextmenu-color: #FFF;
|
||||
--newtab-search-border-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-search-dropdown-color: #38383D;
|
||||
--newtab-search-dropdown-header-color: #4A4A4F;
|
||||
--newtab-search-icon-color: rgba(249, 249, 250, 0.6);
|
||||
--newtab-topsites-background-color: #38383D;
|
||||
--newtab-topsites-icon-shadow: none;
|
||||
--newtab-topsites-label-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-card-active-outline-color: #4A4A4F;
|
||||
--newtab-card-background-color: #38383D;
|
||||
--newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
|
||||
--newtab-snippets-background-color: #38383D;
|
||||
--newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1); }
|
||||
body[lwt-newtab-brighttext] {
|
||||
--newtab-background-color: #2A2A2E;
|
||||
--newtab-border-primary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-border-secondary-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-button-primary-color: #0060DF;
|
||||
--newtab-button-secondary-color: #38383D;
|
||||
--newtab-element-active-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-element-hover-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-icon-primary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-icon-secondary-color: rgba(249, 249, 250, 0.4);
|
||||
--newtab-icon-tertiary-color: rgba(249, 249, 250, 0.4);
|
||||
--newtab-inner-box-shadow-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-link-primary-color: #45A1FF;
|
||||
--newtab-link-secondary-color: #50BCB6;
|
||||
--newtab-text-conditional-color: #F9F9FA;
|
||||
--newtab-text-primary-color: #F9F9FA;
|
||||
--newtab-text-secondary-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-textbox-background-color: #38383D;
|
||||
--newtab-textbox-border: rgba(249, 249, 250, 0.2);
|
||||
--newtab-textbox-focus-color: #45A1FF;
|
||||
--newtab-textbox-focus-boxshadow: 0 0 0 1px #45A1FF, 0 0 0 4px rgba(69, 161, 255, 0.3);
|
||||
--newtab-contextmenu-background-color: #4A4A4F;
|
||||
--newtab-contextmenu-button-color: #2A2A2E;
|
||||
--newtab-modal-color: #2A2A2E;
|
||||
--newtab-overlay-color: rgba(12, 12, 13, 0.8);
|
||||
--newtab-section-header-text-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-section-navigation-text-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-section-active-contextmenu-color: #FFF;
|
||||
--newtab-search-border-color: rgba(249, 249, 250, 0.2);
|
||||
--newtab-search-dropdown-color: #38383D;
|
||||
--newtab-search-dropdown-header-color: #4A4A4F;
|
||||
--newtab-search-icon-color: rgba(249, 249, 250, 0.6);
|
||||
--newtab-topsites-background-color: #38383D;
|
||||
--newtab-topsites-icon-shadow: none;
|
||||
--newtab-topsites-label-color: rgba(249, 249, 250, 0.8);
|
||||
--newtab-card-active-outline-color: #4A4A4F;
|
||||
--newtab-card-background-color: #38383D;
|
||||
--newtab-card-hairline-color: rgba(249, 249, 250, 0.1);
|
||||
--newtab-card-shadow: 0 1px 8px 0 rgba(12, 12, 13, 0.2);
|
||||
--newtab-snippets-background-color: #38383D;
|
||||
--newtab-snippets-hairline-color: rgba(255, 255, 255, 0.1); }
|
||||
|
||||
.icon {
|
||||
background-position: center center;
|
||||
@ -335,8 +334,6 @@ main {
|
||||
margin: auto;
|
||||
padding-bottom: 68px;
|
||||
width: 274px; }
|
||||
.hide-main main {
|
||||
visibility: hidden; }
|
||||
@media (min-width: 482px) {
|
||||
main {
|
||||
width: 402px; } }
|
||||
@ -352,6 +349,8 @@ main {
|
||||
main section {
|
||||
margin-bottom: 20px;
|
||||
position: relative; }
|
||||
.hide-main main {
|
||||
visibility: hidden; }
|
||||
|
||||
.base-content-fallback {
|
||||
height: 100vh; }
|
||||
@ -1475,8 +1474,9 @@ a.firstrun-link {
|
||||
.compact-cards .card-outer .card-text:not(.no-description) .card-title {
|
||||
font-size: 12px;
|
||||
line-height: 13px;
|
||||
max-height: 13px;
|
||||
max-height: 17px;
|
||||
overflow: hidden;
|
||||
padding: 0 0 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
.compact-cards .card-outer .card-description {
|
||||
|
File diff suppressed because one or more lines are too long
@ -1078,6 +1078,7 @@ class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_6___default.a.Pur
|
||||
}
|
||||
|
||||
sendImpression(extraProps) {
|
||||
ASRouterUtils.sendMessage({ type: "IMPRESSION", data: this.state.message });
|
||||
this.sendUserActionTelemetry(Object.assign({ event: "IMPRESSION" }, extraProps));
|
||||
}
|
||||
|
||||
@ -1749,6 +1750,7 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
|
||||
renderMessageItem(msg) {
|
||||
const isCurrent = msg.id === this.state.lastMessageId;
|
||||
const isBlocked = this.state.blockList.includes(msg.id);
|
||||
const impressions = this.state.impressions[msg.id] ? this.state.impressions[msg.id].length : 0;
|
||||
|
||||
let itemClassName = "message-item";
|
||||
if (isCurrent) {
|
||||
@ -1767,7 +1769,9 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
|
||||
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
||||
"span",
|
||||
null,
|
||||
msg.id
|
||||
msg.id,
|
||||
" ",
|
||||
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("br", null)
|
||||
)
|
||||
),
|
||||
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
||||
@ -1782,7 +1786,11 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
|
||||
"button",
|
||||
{ className: "button", onClick: this.handleOverride(msg.id) },
|
||||
"Show"
|
||||
)
|
||||
),
|
||||
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("br", null),
|
||||
"(",
|
||||
impressions,
|
||||
" impressions)"
|
||||
),
|
||||
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
||||
"td",
|
||||
|
File diff suppressed because one or more lines are too long
@ -8,7 +8,7 @@
|
||||
<em:type>2</em:type>
|
||||
<em:bootstrap>true</em:bootstrap>
|
||||
<em:unpack>false</em:unpack>
|
||||
<em:version>2018.07.18.1179-6b6a3463</em:version>
|
||||
<em:version>2018.07.24.1260-3e33e3e1</em:version>
|
||||
<em:name>Activity Stream</em:name>
|
||||
<em:description>A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.</em:description>
|
||||
<em:multiprocessCompatible>true</em:multiprocessCompatible>
|
||||
|
@ -127,6 +127,7 @@ class _ASRouter {
|
||||
lastMessageId: null,
|
||||
providers: [],
|
||||
blockList: [],
|
||||
impressions: {},
|
||||
messages: [],
|
||||
...initialState
|
||||
};
|
||||
@ -211,6 +212,7 @@ class _ASRouter {
|
||||
}
|
||||
}
|
||||
await this.setState(newState);
|
||||
await this.cleanupImpressions();
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,11 +228,13 @@ class _ASRouter {
|
||||
this.messageChannel = channel;
|
||||
this.messageChannel.addMessageListener(INCOMING_MESSAGE_NAME, this.onMessage);
|
||||
this._addASRouterPrefListener();
|
||||
await this.loadMessagesFromAllProviders();
|
||||
this._storage = storage;
|
||||
|
||||
const blockList = await this._storage.get("blockList") || [];
|
||||
await this.setState({blockList});
|
||||
const impressions = await this._storage.get("impressions") || {};
|
||||
await this.setState({blockList, impressions});
|
||||
await this.loadMessagesFromAllProviders();
|
||||
|
||||
// sets .initialized to true and resolves .waitForInitialized promise
|
||||
this._finishInitializing();
|
||||
}
|
||||
@ -264,18 +268,18 @@ class _ASRouter {
|
||||
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: state});
|
||||
}
|
||||
|
||||
async _findMessage(msgs, target, data = {}) {
|
||||
async _findMessage(messages, target, data = {}) {
|
||||
let message;
|
||||
let {trigger} = data;
|
||||
const {trigger} = data;
|
||||
const {impressions} = this.state;
|
||||
if (trigger) {
|
||||
// Find a message that matches the targeting context as well as the trigger context
|
||||
message = await ASRouterTargeting.findMatchingMessageWithTrigger(msgs, target, trigger);
|
||||
message = await ASRouterTargeting.findMatchingMessageWithTrigger({messages, impressions, target, trigger});
|
||||
}
|
||||
if (!message) {
|
||||
// If there was no messages with this trigger, try finding a regular targeted message
|
||||
message = await ASRouterTargeting.findMatchingMessage(msgs, target);
|
||||
message = await ASRouterTargeting.findMatchingMessage({messages, impressions, target});
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@ -343,6 +347,75 @@ class _ASRouter {
|
||||
}
|
||||
}
|
||||
|
||||
async addImpression(message) {
|
||||
// Don't store impressions for messages that don't include any limits on frequency
|
||||
if (!message.frequency) {
|
||||
return;
|
||||
}
|
||||
await this.setState(state => {
|
||||
// The destructuring here is to avoid mutating existing objects in state as in redux
|
||||
// (see https://redux.js.org/recipes/structuring-reducers/prerequisite-concepts#immutable-data-management)
|
||||
const impressions = {...state.impressions};
|
||||
impressions[message.id] = impressions[message.id] ? [...impressions[message.id]] : [];
|
||||
impressions[message.id].push(Date.now());
|
||||
this._storage.set("impressions", impressions);
|
||||
return {impressions};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* getLongestPeriod
|
||||
*
|
||||
* @param {obj} message An ASRouter message
|
||||
* @returns {int|null} if the message has custom frequency caps, the longest period found in the list of caps.
|
||||
if the message has no custom frequency caps, null
|
||||
* @memberof _ASRouter
|
||||
*/
|
||||
getLongestPeriod(message) {
|
||||
if (!message.frequency || !message.frequency.custom) {
|
||||
return null;
|
||||
}
|
||||
return message.frequency.custom.sort((a, b) => b.period - a.period)[0].period;
|
||||
}
|
||||
|
||||
/**
|
||||
* cleanupImpressions - this function cleans up obsolete impressions whenever
|
||||
* messages are refreshed or fetched. It will likely need to be more sophisticated in the future,
|
||||
* but the current behaviour for when impressions are cleared is as follows:
|
||||
*
|
||||
* 1. If the message id for a list of impressions no longer exists in state.messages, it will be cleared.
|
||||
* 2. If the message has time-bound frequency caps but no lifetime cap, any impressions older
|
||||
* than the longest time period will be cleared.
|
||||
*/
|
||||
async cleanupImpressions() {
|
||||
await this.setState(state => {
|
||||
const impressions = {...state.impressions};
|
||||
let needsUpdate = false;
|
||||
Object.keys(impressions).forEach(id => {
|
||||
const [message] = state.messages.filter(msg => msg.id === id);
|
||||
// Don't keep impressions for messages that no longer exist
|
||||
if (!message || !message.frequency || !Array.isArray(impressions[id])) {
|
||||
delete impressions[id];
|
||||
needsUpdate = true;
|
||||
return;
|
||||
}
|
||||
if (!impressions[id].length) {
|
||||
return;
|
||||
}
|
||||
// If we don't want to store impressions older than the longest period
|
||||
if (message.frequency.custom && !message.frequency.lifetime) {
|
||||
const now = Date.now();
|
||||
impressions[id] = impressions[id].filter(t => (now - t) < this.getLongestPeriod(message));
|
||||
needsUpdate = true;
|
||||
}
|
||||
});
|
||||
if (needsUpdate) {
|
||||
this._storage.set("impressions", impressions);
|
||||
}
|
||||
return {impressions};
|
||||
});
|
||||
}
|
||||
|
||||
async sendNextMessage(target, action = {}) {
|
||||
let {data} = action;
|
||||
const msgs = this._getUnblockedMessages();
|
||||
@ -354,8 +427,8 @@ class _ASRouter {
|
||||
} else {
|
||||
message = await this._findMessage(msgs, target, data);
|
||||
}
|
||||
await this.setState({lastMessageId: message ? message.id : null});
|
||||
|
||||
await this.setState({lastMessageId: message ? message.id : null});
|
||||
await this._sendMessageToTarget(message, target, data);
|
||||
}
|
||||
|
||||
@ -368,10 +441,14 @@ class _ASRouter {
|
||||
|
||||
async blockById(idOrIds) {
|
||||
const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
||||
|
||||
await this.setState(state => {
|
||||
const blockList = [...state.blockList, ...idsToBlock];
|
||||
// When a message is blocked, its impressions should be cleared as well
|
||||
const impressions = {...state.impressions};
|
||||
idsToBlock.forEach(id => delete impressions[id]);
|
||||
this._storage.set("blockList", blockList);
|
||||
return {blockList};
|
||||
return {blockList, impressions};
|
||||
});
|
||||
}
|
||||
|
||||
@ -473,6 +550,9 @@ class _ASRouter {
|
||||
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: this.state});
|
||||
}
|
||||
break;
|
||||
case "IMPRESSION":
|
||||
this.addImpression(action.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,46 @@ ChromeUtils.import("resource://gre/modules/components-utils/FilterExpressions.js
|
||||
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "AddonManager",
|
||||
"resource://gre/modules/AddonManager.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "NewTabUtils",
|
||||
"resource://gre/modules/NewTabUtils.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "ProfileAge",
|
||||
"resource://gre/modules/ProfileAge.jsm");
|
||||
ChromeUtils.import("resource://gre/modules/Console.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "ShellService",
|
||||
"resource:///modules/ShellService.jsm");
|
||||
|
||||
const FXA_USERNAME_PREF = "services.sync.username";
|
||||
const ONBOARDING_EXPERIMENT_PREF = "browser.newtabpage.activity-stream.asrouterOnboardingCohort";
|
||||
// Max possible cap for any message
|
||||
const MAX_LIFETIME_CAP = 100;
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
const {activityStreamProvider: asProvider} = NewTabUtils;
|
||||
|
||||
const FRECENT_SITES_UPDATE_INTERVAL = 6 * 60 * 60 * 1000; // Six hours
|
||||
const FRECENT_SITES_IGNORE_BLOCKED = true;
|
||||
const FRECENT_SITES_NUM_ITEMS = 50;
|
||||
const FRECENT_SITES_MIN_FRECENCY = 100;
|
||||
|
||||
const TopFrecentSitesCache = {
|
||||
_lastUpdated: 0,
|
||||
_topFrecentSites: null,
|
||||
get topFrecentSites() {
|
||||
return new Promise(async resolve => {
|
||||
const now = Date.now();
|
||||
if (now - this._lastUpdated >= FRECENT_SITES_UPDATE_INTERVAL) {
|
||||
this._topFrecentSites = await asProvider.getTopFrecentSites({
|
||||
ignoreBlocked: FRECENT_SITES_IGNORE_BLOCKED,
|
||||
numItems: FRECENT_SITES_NUM_ITEMS,
|
||||
topsiteFrecency: FRECENT_SITES_MIN_FRECENCY,
|
||||
onePerDomain: true,
|
||||
includeFavicon: false
|
||||
});
|
||||
this._lastUpdated = now;
|
||||
}
|
||||
resolve(this._topFrecentSites);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* removeRandomItemFromArray - Removes a random item from the array and returns it.
|
||||
@ -84,6 +116,17 @@ const TargetingGetters = {
|
||||
return Services.prefs.getIntPref("devtools.selfxss.count");
|
||||
},
|
||||
|
||||
get topFrecentSites() {
|
||||
return TopFrecentSitesCache.topFrecentSites.then(sites => sites.map(site => (
|
||||
{
|
||||
url: site.url,
|
||||
host: (new URL(site.url)).hostname,
|
||||
frecency: site.frecency,
|
||||
lastVisitDate: site.lastVisitDate
|
||||
}
|
||||
)));
|
||||
},
|
||||
|
||||
// Temporary targeting function for the purposes of running the simplified onboarding experience
|
||||
get isInExperimentCohort() {
|
||||
return Services.prefs.getIntPref(ONBOARDING_EXPERIMENT_PREF, 0);
|
||||
@ -97,6 +140,35 @@ this.ASRouterTargeting = {
|
||||
return FilterExpressions.eval(filterExpression, context);
|
||||
},
|
||||
|
||||
isBelowFrequencyCap(message, impressionsForMessage) {
|
||||
if (!message.frequency || !impressionsForMessage || !impressionsForMessage.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
message.frequency.lifetime &&
|
||||
impressionsForMessage.length >= Math.min(message.frequency.lifetime, MAX_LIFETIME_CAP)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.frequency.custom) {
|
||||
const now = Date.now();
|
||||
for (const setting of message.frequency.custom) {
|
||||
let {period} = setting;
|
||||
if (period === "daily") {
|
||||
period = ONE_DAY;
|
||||
}
|
||||
const impressionsInPeriod = impressionsForMessage.filter(t => (now - t) < period);
|
||||
if (impressionsInPeriod.length >= setting.cap) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* findMatchingMessage - Given an array of messages, returns one message
|
||||
* whos targeting expression evaluates to true
|
||||
@ -105,26 +177,37 @@ this.ASRouterTargeting = {
|
||||
* @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above.
|
||||
* @returns {obj} an AS router message
|
||||
*/
|
||||
async findMatchingMessage(messages, target, context) {
|
||||
async findMatchingMessage({messages, impressions = {}, target, context}) {
|
||||
const arrayOfItems = [...messages];
|
||||
let match;
|
||||
let candidate;
|
||||
|
||||
while (!match && arrayOfItems.length) {
|
||||
candidate = removeRandomItemFromArray(arrayOfItems);
|
||||
if (candidate && !candidate.trigger && (!candidate.targeting || await this.isMatch(candidate.targeting, target, context))) {
|
||||
if (
|
||||
candidate &&
|
||||
this.isBelowFrequencyCap(candidate, impressions[candidate.id]) &&
|
||||
!candidate.trigger &&
|
||||
(!candidate.targeting || await this.isMatch(candidate.targeting, target, context))
|
||||
) {
|
||||
match = candidate;
|
||||
}
|
||||
}
|
||||
return match;
|
||||
},
|
||||
|
||||
async findMatchingMessageWithTrigger(messages, target, trigger, context) {
|
||||
async findMatchingMessageWithTrigger({messages, impressions = {}, target, trigger, context}) {
|
||||
const arrayOfItems = [...messages];
|
||||
let match;
|
||||
let candidate;
|
||||
while (!match && arrayOfItems.length) {
|
||||
candidate = removeRandomItemFromArray(arrayOfItems);
|
||||
if (candidate && candidate.trigger === trigger && (!candidate.targeting || await this.isMatch(candidate.targeting, target, context))) {
|
||||
if (
|
||||
candidate &&
|
||||
this.isBelowFrequencyCap(candidate, impressions[candidate.id]) &&
|
||||
candidate.trigger === trigger &&
|
||||
(!candidate.targeting || await this.isMatch(candidate.targeting, target, context))
|
||||
) {
|
||||
match = candidate;
|
||||
}
|
||||
}
|
||||
|
@ -149,10 +149,6 @@ const PREFS_CONFIG = new Map([
|
||||
title: "Number of rows of Top Stories to display",
|
||||
value: 1
|
||||
}],
|
||||
["tippyTop.service.endpoint", {
|
||||
title: "Tippy Top service manifest url",
|
||||
value: "https://activity-stream-icons.services.mozilla.com/v1/icons.json.br"
|
||||
}],
|
||||
["sectionOrder", {
|
||||
title: "The rendering order for the sections",
|
||||
value: "topsites,topstories,highlights"
|
||||
|
@ -5,11 +5,9 @@
|
||||
|
||||
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
|
||||
|
||||
const {actionTypes: at} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
const {PersistentCache} = ChromeUtils.import("resource://activity-stream/lib/PersistentCache.jsm", {});
|
||||
const {getDomain} = ChromeUtils.import("resource://activity-stream/lib/TippyTopProvider.jsm", {});
|
||||
const {RemoteSettings} = ChromeUtils.import("resource://services-settings/remote-settings.js", {});
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
@ -18,10 +16,6 @@ ChromeUtils.defineModuleGetter(this, "Services",
|
||||
ChromeUtils.defineModuleGetter(this, "NewTabUtils",
|
||||
"resource://gre/modules/NewTabUtils.jsm");
|
||||
|
||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
const TIPPYTOP_UPDATE_TIME = ONE_DAY;
|
||||
const TIPPYTOP_RETRY_DELAY = FIVE_MINUTES;
|
||||
const MIN_FAVICON_SIZE = 96;
|
||||
|
||||
/**
|
||||
@ -120,97 +114,9 @@ async function fetchIconFromRedirects(url) {
|
||||
|
||||
this.FaviconFeed = class FaviconFeed {
|
||||
constructor() {
|
||||
this.tippyTopNextUpdate = 0;
|
||||
this.cache = new PersistentCache("tippytop", true);
|
||||
this._sitesByDomain = null;
|
||||
this.numRetries = 0;
|
||||
this._queryForRedirects = new Set();
|
||||
}
|
||||
|
||||
get endpoint() {
|
||||
return this.store.getState().Prefs.values["tippyTop.service.endpoint"];
|
||||
}
|
||||
|
||||
async loadCachedData() {
|
||||
const data = await this.cache.get("sites");
|
||||
if (data && "_timestamp" in data) {
|
||||
this._sitesByDomain = data;
|
||||
this.tippyTopNextUpdate = data._timestamp + TIPPYTOP_UPDATE_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
async maybeRefresh() {
|
||||
if (Date.now() >= this.tippyTopNextUpdate) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
let headers = new Headers();
|
||||
if (this._sitesByDomain && this._sitesByDomain._etag) {
|
||||
headers.set("If-None-Match", this._sitesByDomain._etag);
|
||||
}
|
||||
let {data, etag, status} = await this.loadFromURL(this.endpoint, headers);
|
||||
let failedUpdate = false;
|
||||
if (status === 200) {
|
||||
this._sitesByDomain = this._sitesArrayToObjectByDomain(data);
|
||||
this._sitesByDomain._etag = etag;
|
||||
} else if (status !== 304) {
|
||||
failedUpdate = true;
|
||||
}
|
||||
let delay = TIPPYTOP_UPDATE_TIME;
|
||||
if (failedUpdate) {
|
||||
delay = Math.min(TIPPYTOP_UPDATE_TIME, TIPPYTOP_RETRY_DELAY * Math.pow(2, this.numRetries++));
|
||||
} else {
|
||||
this._sitesByDomain._timestamp = Date.now();
|
||||
this.cache.set("sites", this._sitesByDomain);
|
||||
this.numRetries = 0;
|
||||
}
|
||||
this.tippyTopNextUpdate = Date.now() + delay;
|
||||
}
|
||||
|
||||
async loadFromURL(url, headers) {
|
||||
let data = [];
|
||||
let etag;
|
||||
let status;
|
||||
try {
|
||||
let response = await fetch(url, {headers});
|
||||
status = response.status;
|
||||
if (status === 200) {
|
||||
data = await response.json();
|
||||
etag = response.headers.get("ETag");
|
||||
}
|
||||
} catch (error) {
|
||||
Cu.reportError(`Failed to load tippy top manifest from ${url}`);
|
||||
}
|
||||
return {data, etag, status};
|
||||
}
|
||||
|
||||
_sitesArrayToObjectByDomain(sites) {
|
||||
let sitesByDomain = {};
|
||||
for (const site of sites) {
|
||||
// The tippy top manifest can have a url property (string) or a
|
||||
// urls property (array of strings)
|
||||
for (const domain of site.domains || []) {
|
||||
sitesByDomain[domain] = {image_url: site.image_url};
|
||||
}
|
||||
}
|
||||
return sitesByDomain;
|
||||
}
|
||||
|
||||
getSitesByDomain() {
|
||||
// return an already loaded object or a promise for that object
|
||||
return this._sitesByDomain || (this._sitesByDomain = new Promise(async resolve => {
|
||||
await this.loadCachedData();
|
||||
await this.maybeRefresh();
|
||||
if (this._sitesByDomain instanceof Promise) {
|
||||
// If _sitesByDomain is still a Promise, no data was loaded from cache or fetch.
|
||||
this._sitesByDomain = {};
|
||||
}
|
||||
resolve(this._sitesByDomain);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* fetchIcon attempts to fetch a rich icon for the given url from two sources.
|
||||
* First, it looks up the tippy top feed, if it's still missing, then it queries
|
||||
@ -223,44 +129,55 @@ this.FaviconFeed = class FaviconFeed {
|
||||
return;
|
||||
}
|
||||
|
||||
const sitesByDomain = await this.getSitesByDomain();
|
||||
const domain = getDomain(url);
|
||||
if (domain in sitesByDomain) {
|
||||
let iconUri = Services.io.newURI(sitesByDomain[domain].image_url);
|
||||
// The #tippytop is to be able to identify them for telemetry.
|
||||
iconUri = iconUri.mutate().setRef("tippytop").finalize();
|
||||
PlacesUtils.favicons.setAndFetchFaviconForPage(
|
||||
Services.io.newURI(url),
|
||||
iconUri,
|
||||
false,
|
||||
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
|
||||
null,
|
||||
Services.scriptSecurityManager.getSystemPrincipal()
|
||||
);
|
||||
const site = await this.getSite(getDomain(url));
|
||||
if (!site) {
|
||||
if (!this._queryForRedirects.has(url)) {
|
||||
this._queryForRedirects.add(url);
|
||||
Services.tm.idleDispatchToMainThread(() => fetchIconFromRedirects(url));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._queryForRedirects.has(url)) {
|
||||
this._queryForRedirects.add(url);
|
||||
Services.tm.idleDispatchToMainThread(() => fetchIconFromRedirects(url));
|
||||
let iconUri = Services.io.newURI(site.image_url);
|
||||
// The #tippytop is to be able to identify them for telemetry.
|
||||
iconUri = iconUri.mutate().setRef("tippytop").finalize();
|
||||
PlacesUtils.favicons.setAndFetchFaviconForPage(
|
||||
Services.io.newURI(url),
|
||||
iconUri,
|
||||
false,
|
||||
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
|
||||
null,
|
||||
Services.scriptSecurityManager.getSystemPrincipal()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site tippy top data from Remote Settings.
|
||||
*/
|
||||
async getSite(domain) {
|
||||
const sites = await this.tippyTop.get({filters: {domain}});
|
||||
return sites.length ? sites[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tippy top collection from Remote Settings.
|
||||
*/
|
||||
get tippyTop() {
|
||||
if (!this._tippyTop) {
|
||||
this._tippyTop = RemoteSettings("tippytop");
|
||||
}
|
||||
return this._tippyTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we should be fetching and saving icons.
|
||||
*/
|
||||
get shouldFetchIcons() {
|
||||
return this.endpoint && Services.prefs.getBoolPref("browser.chrome.site_icons");
|
||||
return Services.prefs.getBoolPref("browser.chrome.site_icons");
|
||||
}
|
||||
|
||||
onAction(action) {
|
||||
switch (action.type) {
|
||||
case at.SYSTEM_TICK:
|
||||
if (this._sitesByDomain) {
|
||||
// No need to refresh if we haven't been initialized.
|
||||
this.maybeRefresh();
|
||||
}
|
||||
break;
|
||||
case at.RICH_ICON_MISSING:
|
||||
this.fetchIcon(action.data.url);
|
||||
break;
|
||||
|
@ -171,6 +171,7 @@ section_menu_action_remove_section=Abschnitt entfernen
|
||||
section_menu_action_collapse_section=Abschnitt einklappen
|
||||
section_menu_action_expand_section=Abschnitt ausklappen
|
||||
section_menu_action_manage_section=Abschnitt verwalten
|
||||
section_menu_action_manage_webext=Erweiterung verwalten
|
||||
section_menu_action_add_topsite=Wichtige Seite hinzufügen
|
||||
section_menu_action_move_up=Nach oben schieben
|
||||
section_menu_action_move_down=Nach unten schieben
|
||||
@ -178,13 +179,25 @@ section_menu_action_privacy_notice=Datenschutzhinweis
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
|
||||
# firstrun of the browser, they give an introduction to Firefox and Sync.
|
||||
firstrun_title=Firefox für unterwegs
|
||||
firstrun_content=Nehmen Sie Ihre Lesezeichen, Chronik, Passwörter und andere Einstellungen auf allen Geräten mit.
|
||||
firstrun_learn_more_link=Jetzt mehr über Firefox Konten erfahren
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
|
||||
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
|
||||
# firstrun_form_header is displayed more boldly as the call to action.
|
||||
firstrun_form_header=E-Mail-Adresse eingeben
|
||||
firstrun_form_sub_header=um sich bei Firefox Sync anzumelden.
|
||||
|
||||
firstrun_email_input_placeholder=E-Mail
|
||||
|
||||
firstrun_invalid_input=Gültige E-Mail-Adresse erforderlich
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_extra_legal_links=Indem Sie fortfahren, stimmen Sie den {terms} und dem {privacy} zu.
|
||||
firstrun_terms_of_service=Nutzungsbedingungen
|
||||
firstrun_privacy_notice=Datenschutzhinweis
|
||||
|
||||
firstrun_continue_to_login=Weiter
|
||||
firstrun_skip_login=Diesen Schritt überspringen
|
||||
|
@ -191,6 +191,8 @@ firstrun_form_sub_header=para acceder a Firefox Sync.
|
||||
|
||||
firstrun_email_input_placeholder=Correo electrónico
|
||||
|
||||
firstrun_invalid_input=Se requiere un correo válido
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_extra_legal_links=Al proceder, aceptas los {terms} y la {privacy}.
|
||||
|
@ -191,6 +191,8 @@ firstrun_form_sub_header=برای فعال کردن همگامسازی فای
|
||||
|
||||
firstrun_email_input_placeholder=پستالکترونیکی
|
||||
|
||||
firstrun_invalid_input=رایانامهٔ معتبر لازم است
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_extra_legal_links=با ادامه دادن، شما {terms} و {privacy} قبول میکنید.
|
||||
|
@ -191,6 +191,8 @@ firstrun_form_sub_header=jatkaaksesi Firefox Sync -palveluun.
|
||||
|
||||
firstrun_email_input_placeholder=Sähköposti
|
||||
|
||||
firstrun_invalid_input=Sähköpostiosoitteen täytyy olla kelvollinen
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_extra_legal_links=Jatkamalla hyväksyt {terms} ja {privacy}.
|
||||
|
@ -191,6 +191,8 @@ firstrun_form_sub_header=pour continuer avec Firefox Sync.
|
||||
|
||||
firstrun_email_input_placeholder=Adresse électronique
|
||||
|
||||
firstrun_invalid_input=Adresse électronique valide requise
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_extra_legal_links=En continuant, vous acceptez les {terms} et la {privacy}.
|
||||
|
@ -191,6 +191,8 @@ firstrun_form_sub_header=Firefox સમન્વયન ચાલુ રાખવ
|
||||
|
||||
firstrun_email_input_placeholder=ઇમેઇલ
|
||||
|
||||
firstrun_invalid_input=માન્ય ઇમેઇલ આવશ્યક છે
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_extra_legal_links=આગળ વધીને, તમે {terms} અને {privacy} સાથે સંમત થાઓ છો.
|
||||
|
@ -1,7 +1,7 @@
|
||||
newtab_page_title=नया टैब
|
||||
|
||||
header_top_sites=सर्वोच्च साइटें
|
||||
header_highlights=झलकियाँ
|
||||
header_highlights=प्रमुखताएँ
|
||||
# LOCALIZATION NOTE(header_recommended_by): This is followed by the name
|
||||
# of the corresponding content provider.
|
||||
header_recommended_by={provider} द्वारा अनुशंसित
|
||||
@ -107,7 +107,7 @@ prefs_highlights_options_pocket_label=पॉकेट में सहेजे
|
||||
prefs_snippets_description=Mozilla और Firefox से अद्यतन
|
||||
settings_pane_button_label=अपने नए टैब पृष्ठ को अनुकूलित करें
|
||||
settings_pane_topsites_header=सर्वोच्च साइटें
|
||||
settings_pane_highlights_header=झलकियाँ
|
||||
settings_pane_highlights_header=प्रमुखताएँ
|
||||
settings_pane_highlights_options_bookmarks=पुस्तचिह्न
|
||||
# LOCALIZATION NOTE(settings_pane_snippets_header): For the "Snippets" feature
|
||||
# traditionally on about:home. Alternative translation options: "Small Note" or
|
||||
|
@ -144,7 +144,7 @@ pocket_read_more=პოპულარული თემები:
|
||||
# end of the list of popular topic links.
|
||||
pocket_read_even_more=მეტი სიახლის ნახვა
|
||||
|
||||
highlights_empty_state=დაიწყეთ გვერდების დათვალიერება და აქ გამოჩნდება თქვენი რჩეული სტატიები, ვიდეოები და ბოლოს მონახულებული ან ჩანიშნული საიტები.
|
||||
highlights_empty_state=დაიწყეთ გვერდების დათვალიერება და აქ გამოჩნდება თქვენთვის სასურველი სტატიები, ვიდეოები და ბოლოს მონახულებული ან ჩანიშნული საიტები.
|
||||
# LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,
|
||||
# in the space that would have shown a few stories, this is shown instead.
|
||||
# {provider} is replaced by the name of the content provider for this section.
|
||||
|
@ -1,33 +1,24 @@
|
||||
newtab_page_title=ಹೊಸ ಹಾಳೆ
|
||||
default_label_loading=ಲೋಡ್ ಆಗುತ್ತಿದೆ…
|
||||
|
||||
header_top_sites=ಪ್ರಮುಖ ತಾಣಗಳು
|
||||
header_stories=ಪ್ರಮುಖ ಸುದ್ದಿಗಳು
|
||||
header_highlights=ಮುಖ್ಯಾಂಶಗಳು
|
||||
header_visit_again=ಮತ್ತೆ ಭೇಟಿಕೊಡು
|
||||
header_bookmarks=ಇತ್ತೀಚಿಗೆ ಮಾಡಲಾದ ಬುಕ್ಮಾರ್ಕುಗಳು
|
||||
# LOCALIZATION NOTE(header_recommended_by): This is followed by the name
|
||||
# of the corresponding content provider.
|
||||
header_recommended_by={provider} ರಿಂದ ಶಿಫಾರಸುಮಾಡುಲಾಗಿದೆ
|
||||
# LOCALIZATION NOTE(header_bookmarks_placeholder): This message is
|
||||
# meant to inform that section contains no information because
|
||||
# the user hasn't added any bookmarks.
|
||||
header_bookmarks_placeholder=ನಿಮ್ಮ ಹತ್ತಿರ ಇನ್ನೂ ಯಾವುದೇ ಪುಟಗುರುತುಗಳಿಲ್ಲ.
|
||||
# LOCALIZATION NOTE(header_stories_from): This is followed by a logo of the
|
||||
# corresponding content (stories) provider
|
||||
header_stories_from=ಯಿಂದ
|
||||
|
||||
# LOCALIZATION NOTE(context_menu_button_sr): This is for screen readers when
|
||||
# the context menu button is focused/active. Title is the label or hostname of
|
||||
# the site.
|
||||
|
||||
# LOCALIZATION NOTE(section_context_menu_button_sr): This is for screen readers when
|
||||
# the section edit context menu button is focused/active.
|
||||
|
||||
# LOCALIZATION NOTE (type_label_*): These labels are associated to pages to give
|
||||
# context on how the element is related to the user, e.g. type indicates that
|
||||
# the page is bookmarked, or is currently open on another device
|
||||
type_label_visited=ಭೇಟಿ ನೀಡಲಾದ
|
||||
type_label_bookmarked=ಪುಟಗುರುತು ಮಾಡಲಾದ
|
||||
type_label_synced=ಮತ್ತೊಂದು ಸಾಧನದಿಂದ ಸಿಂಕ್ ಮಾಡಲಾಗಿದೆ
|
||||
type_label_recommended=ಪ್ರಚಲಿತ
|
||||
# LOCALIZATION NOTE(type_label_open): Open is an adjective, as in "page is open"
|
||||
type_label_open=ತೆರೆ
|
||||
type_label_topic=ವಿಷಯ
|
||||
type_label_now=ಈಗ
|
||||
|
||||
# LOCALIZATION NOTE (menu_action_*): These strings are displayed in a context
|
||||
# menu and are meant as a call to action for a given page.
|
||||
@ -35,8 +26,6 @@ type_label_now=ಈಗ
|
||||
# bookmarks"
|
||||
menu_action_bookmark=ಪುಟ ಗುರುತು
|
||||
menu_action_remove_bookmark=ಪುಟ ಗುರುತು ತೆಗೆ
|
||||
menu_action_copy_address=ವಿಳಾಸವನ್ನು ನಕಲಿಸು
|
||||
menu_action_email_link=ಇಮೈಲ್ ಕೊಂಡಿ…
|
||||
menu_action_open_new_window=ಹೊಸ ಕಿಟಕಿಯಲ್ಲಿ ತೆರೆ
|
||||
menu_action_open_private_window=ಹೊಸ ಖಾಸಗಿ ಕಿಟಕಿಯಲ್ಲಿ ತೆರೆ
|
||||
menu_action_dismiss=ವಜಾಗೊಳಿಸು
|
||||
@ -49,11 +38,19 @@ menu_action_unpin=ಅನ್ಪಿನ್
|
||||
confirm_history_delete_notice_p2=ಈ ಕಾರ್ಯವನ್ನು ರದ್ದುಗೊಳಿಸಲು ಸಾಧ್ಯವಿರುವುದಿಲ್ಲ.
|
||||
menu_action_save_to_pocket=ಪಾಕೆಟ್ನಲ್ಲಿ ಉಳಿಸಿ
|
||||
|
||||
# LOCALIZATION NOTE (search_for_something_with): {search_term} is a placeholder
|
||||
# for what the user has typed in the search input field, e.g. 'Search for ' +
|
||||
# search_term + 'with:' becomes 'Search for abc with:'
|
||||
# The search engine name is displayed as an icon and does not need a translation
|
||||
search_for_something_with={search_term} ಅನ್ನು ಇದರಿಂದ ಹುಡುಕಿ:
|
||||
# LOCALIZATION NOTE (menu_action_show_file_*): These are platform specific strings
|
||||
# found in the context menu of an item that has been downloaded. The intention behind
|
||||
# "this action" is that it will show where the downloaded file exists on the file system
|
||||
# for each operating system.
|
||||
menu_action_show_file_default=ಕಡತ ತೋರಿಸು
|
||||
menu_action_open_file=ಕಡತವನ್ನು ತೆರೆ
|
||||
|
||||
# LOCALIZATION NOTE (menu_action_copy_download_link, menu_action_go_to_download_page):
|
||||
# "Download" here, in both cases, is not a verb, it is a noun. As in, "Copy the
|
||||
# link that belongs to this downloaded item"
|
||||
menu_action_copy_download_link=ಡೌನ್ಲೋಡ್ ಕೊಂಡಿಯನ್ನು ಪ್ರತಿ ಮಾಡು
|
||||
menu_action_go_to_download_page=ಡೌನ್ಲೋಡ್ ಪುಟಕ್ಕೆ ತೆರಳು
|
||||
menu_action_remove_download=ಇತಿಹಾಸದಿಂದ ತೆಗೆದುಹಾಕು
|
||||
|
||||
# LOCALIZATION NOTE (search_button): This is screenreader only text for the
|
||||
# search button.
|
||||
@ -67,63 +64,46 @@ search_header={search_engine_name} ನಿಂದ ಹುಡುಕಿ
|
||||
# LOCALIZATION NOTE (search_web_placeholder): This is shown in the searchbox when
|
||||
# the user hasn't typed anything yet.
|
||||
search_web_placeholder=ಅಂತರ್ಜಾಲವನ್ನು ಹುಡುಕಿ
|
||||
search_settings=ಹುಡುಕು ಸಿದ್ಧತೆಗಳನ್ನು ಬದಲಾಯಿಸು
|
||||
|
||||
# LOCALIZATION NOTE (section_info_option): This is the screenreader text for the
|
||||
# (?) icon that would show a section's description with optional feedback link.
|
||||
section_info_option=ಮಾಹಿತಿ
|
||||
section_info_send_feedback=ಅಭಿಪ್ರಾಯವನ್ನು ಕಳುಹಿಸಿ
|
||||
section_info_privacy_notice=ಗೌಪ್ಯತಾ ಸೂಚನೆ
|
||||
# LOCALIZATION NOTE (section_disclaimer_topstories): This is shown below
|
||||
# the topstories section title to provide additional information about
|
||||
# how the stories are selected.
|
||||
# LOCALIZATION NOTE (section_disclaimer_topstories_buttontext): The text of
|
||||
# the button used to acknowledge, and hide this disclaimer in the future.
|
||||
|
||||
# LOCALIZATION NOTE (welcome_*): This is shown as a modal dialog, typically on a
|
||||
# first-run experience when there's no data to display yet
|
||||
welcome_title=ಹೊಸ ಹಾಳೆಗೆ ಸುಸ್ವಾಗತ
|
||||
|
||||
# LOCALIZATION NOTE (time_label_*): {number} is a placeholder for a number which
|
||||
# represents a shortened timestamp format, e.g. '10m' means '10 minutes ago'.
|
||||
time_label_less_than_minute=<1ನಿ
|
||||
time_label_minute={number}ನಿ
|
||||
time_label_hour={number}ಗ
|
||||
time_label_day={number}ದಿ
|
||||
|
||||
# LOCALIZATION NOTE (settings_pane_*): This is shown in the Settings Pane sidebar.
|
||||
# LOCALIZATION NOTE (prefs_*, settings_*): These are shown in about:preferences
|
||||
# for a "Firefox Home" section. "Firefox" should be treated as a brand and kept
|
||||
# in English, while "Home" should be localized matching the about:preferences
|
||||
# sidebar mozilla-central string for the panel that has preferences related to
|
||||
# what is shown for the homepage, new windows, and new tabs.
|
||||
# LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
|
||||
# plural forms used in a drop down of multiple row options (1 row, 2 rows).
|
||||
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
|
||||
prefs_search_header=ಜಾಲದ ಹುಡುಕಾಟ
|
||||
settings_pane_button_label=ಹೊಸ ಹಾಳೆಯ ಪುಟವನ್ನು ಅಗತ್ಯಾನುಗುಣಗೊಳಿಸಿ
|
||||
settings_pane_header=ಹೊಸ ಹಾಳೆಯ ಆದ್ಯತೆಗಳು
|
||||
settings_pane_body2=ನೀವು ಈ ಪುಟದಲ್ಲಿ ಏನು ನೋಡಿತ್ತೀರೆಂದು ಆಯ್ಕೆಮಾಡಿ.
|
||||
settings_pane_search_header=ಹುಡುಕು
|
||||
settings_pane_search_body=ಹೊಸ ಹಾಳೆಯಿಂದ ಅಂತರ್ಜಾಲವನ್ನು ಹುಡುಕಿ.
|
||||
settings_pane_topsites_header=ಪ್ರಮುಖ ತಾಣಗಳು
|
||||
settings_pane_topsites_body=ನೀವು ಅತಿ ಹೆಚ್ಚು ನೋಡುವ ಜಾಲತಾಣಗಳಿಗೆ ಪ್ರವೇಶದ್ವಾರ.
|
||||
settings_pane_topsites_options_showmore=ಎರಡು ಸಾಲುಗಳನ್ನು ಪ್ರದರ್ಶಿಸು
|
||||
settings_pane_bookmarks_header=ಇತ್ತೀಚಿನ ಪುಟಗುರುತುಗಳು
|
||||
settings_pane_visit_again_header=ಮತ್ತೆ ಭೇಟಿಕೊಡು
|
||||
settings_pane_highlights_header=ಮುಖ್ಯಾಂಶಗಳು
|
||||
settings_pane_highlights_options_bookmarks=ಪುಟಗುರುತುಗಳು
|
||||
settings_pane_highlights_options_visited=ಭೇಟಿ ನೀಡಿದ ತಾಣಗಳು
|
||||
# LOCALIZATION NOTE(settings_pane_snippets_header): For the "Snippets" feature
|
||||
# traditionally on about:home. Alternative translation options: "Small Note" or
|
||||
# something that expresses the idea of "a small message, shortened from
|
||||
# something else, and non-essential but also not entirely trivial and useless."
|
||||
settings_pane_snippets_header=ಉಲ್ಲೇಖಗಳು
|
||||
settings_pane_done_button=ಆಯಿತು
|
||||
|
||||
# LOCALIZATION NOTE (edit_topsites_*): This is shown in the Edit Top Sites modal
|
||||
# dialog.
|
||||
edit_topsites_button_text=ತಿದ್ದು
|
||||
edit_topsites_showmore_button=ಹೆಚ್ಚು ತೋರಿಸು
|
||||
edit_topsites_showless_button=ಕೆಲವೊಂದು ತೋರಿಸಿ
|
||||
edit_topsites_done_button=ಆಯಿತು
|
||||
edit_topsites_pin_button=ಈ ತಾಣವನ್ನು ಪಿನ್ ಮಾಡು
|
||||
edit_topsites_unpin_button=ಈ ತಾಣವನ್ನು ಹೊರತೆಗೆ
|
||||
edit_topsites_edit_button=ಈ ತಾಣವನ್ನು ಸಂಪಾದಿಸು
|
||||
edit_topsites_dismiss_button=ಈ ತಾಣವನ್ನು ತೆಗೆದುಹಾಕು
|
||||
edit_topsites_add_button=ಸೇರಿಸು
|
||||
|
||||
# LOCALIZATION NOTE (topsites_form_*): This is shown in the New/Edit Topsite modal.
|
||||
topsites_form_add_header=ಹೊಸ ಅಗ್ರ ತಾಣಗಳು
|
||||
topsites_form_edit_header=ಅಗ್ರ ತಾಣಗಳನ್ನು ಸಂಪಾದಿಸಿ
|
||||
topsites_form_title_label=ಶೀರ್ಷಿಕೆ
|
||||
topsites_form_title_placeholder=ಶೀರ್ಷಿಕೆಯನ್ನು ನಮೂದಿಸಿ
|
||||
topsites_form_url_label=URL
|
||||
topsites_form_url_placeholder=ಒಂದು URL ಅನ್ನು ಟೈಪಿಸಿ ಅಥವಾ ನಕಲಿಸಿ
|
||||
# LOCALIZATION NOTE (topsites_form_*_button): These are verbs/actions.
|
||||
topsites_form_preview_button=ಮುನ್ನೋಟ
|
||||
topsites_form_add_button=ಸೇರಿಸು
|
||||
topsites_form_save_button=ಉಳಿಸು
|
||||
topsites_form_cancel_button=ರದ್ದು ಮಾಡು
|
||||
@ -135,10 +115,6 @@ pocket_read_more=ಜನಪ್ರಿಯವಾದ ವಿಷಯಗಳು:
|
||||
# LOCALIZATION NOTE (pocket_read_even_more): This is shown as a link at the
|
||||
# end of the list of popular topic links.
|
||||
pocket_read_even_more=ಹೆಚ್ಚು ಕತೆಗಳನ್ನು ನೋಡಿರಿ
|
||||
# LOCALIZATION NOTE (pocket_feedback_header): This is shown as an introduction
|
||||
# to Pocket as part of the feedback form.
|
||||
# LOCALIZATION NOTE (pocket_description): This is shown in the settings pane and
|
||||
# below (pocket_feedback_header) to provide more information about Pocket.
|
||||
|
||||
highlights_empty_state=ವೀಕ್ಷಣೆ ಮಾಡಲು ಶುರುಮಾಡಿ, ಮತ್ತು ನಾವು ಇತ್ತೀಚೆಗೆ ಭೇಟಿ ನೀಡಿದ ಅಥವಾ ಬುಕ್ಮಾರ್ಕ್ ಮಾಡಲಾದ ಕೆಲವು ಶ್ರೇಷ್ಠ ಲೇಖನಗಳು, ವೀಡಿಯೊಗಳು ಮತ್ತು ಇತರ ಪುಟಗಳನ್ನು ನಾವು ತೋರಿಸುತ್ತೇವೆ.
|
||||
# LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,
|
||||
@ -153,3 +129,29 @@ manual_migration_cancel_button=ಪರವಾಗಿಲ್ಲ
|
||||
# LOCALIZATION NOTE (manual_migration_import_button): This message is shown on a button that starts the process
|
||||
# of importing another browser’s profile profile into Firefox.
|
||||
manual_migration_import_button=ಈಗ ಆಮದು ಮಾಡು
|
||||
|
||||
# LOCALIZATION NOTE (error_fallback_default_*): This message and suggested
|
||||
# action link are shown in each section of UI that fails to render
|
||||
|
||||
# LOCALIZATION NOTE (section_menu_action_*). These strings are displayed in the section
|
||||
# context menu and are meant as a call to action for the given section.
|
||||
section_menu_action_move_up=ಮೇಲೆ ಜರುಗಿಸು
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
|
||||
# firstrun of the browser, they give an introduction to Firefox and Sync.
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
|
||||
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
|
||||
# firstrun_form_header is displayed more boldly as the call to action.
|
||||
|
||||
firstrun_email_input_placeholder=ಇಮೇಲ್
|
||||
|
||||
firstrun_invalid_input=ಸರಿಯಾದ ಇಮೇಲ್ ಬೇಕಾಗಿದೆ
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_terms_of_service=ಸೇವೆಯ ನಿಯಮಗಳು
|
||||
firstrun_privacy_notice=ಗೌಪ್ಯತಾ ಸೂಚನೆ
|
||||
|
||||
firstrun_continue_to_login=ಮುಂದುವರೆ
|
||||
firstrun_skip_login=ಈ ಹಂತವನ್ನು ಹಾರಿಸಿ
|
||||
|
@ -191,6 +191,8 @@ firstrun_form_sub_header=norėdami tęsti su „Firefox Sync“.
|
||||
|
||||
firstrun_email_input_placeholder=El. paštas
|
||||
|
||||
firstrun_invalid_input=Reikalingas galiojantis el. pašto adresas
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_extra_legal_links=Tęsdami sutinkate su {terms} ir {privacy}.
|
||||
|
@ -1,22 +1,28 @@
|
||||
newtab_page_title=नवीन टॅब
|
||||
default_label_loading=दाखल करीत आहे…
|
||||
|
||||
header_top_sites=खास साईट्स
|
||||
header_highlights=ठळक
|
||||
header_stories=महत्वाच्या गोष्टी
|
||||
# LOCALIZATION NOTE(header_stories_from): This is followed by a logo of the
|
||||
# corresponding content (stories) provider
|
||||
header_stories_from=कडून
|
||||
# LOCALIZATION NOTE(header_recommended_by): This is followed by the name
|
||||
# of the corresponding content provider.
|
||||
header_recommended_by={provider} तर्फे शिफारस
|
||||
|
||||
# LOCALIZATION NOTE(context_menu_button_sr): This is for screen readers when
|
||||
# the context menu button is focused/active. Title is the label or hostname of
|
||||
# the site.
|
||||
context_menu_button_sr={title} साठी संदर्भ मेनू उघडा
|
||||
|
||||
# LOCALIZATION NOTE(section_context_menu_button_sr): This is for screen readers when
|
||||
# the section edit context menu button is focused/active.
|
||||
section_context_menu_button_sr=विभाग संदर्भ मेनू उघडा
|
||||
|
||||
# LOCALIZATION NOTE (type_label_*): These labels are associated to pages to give
|
||||
# context on how the element is related to the user, e.g. type indicates that
|
||||
# the page is bookmarked, or is currently open on another device
|
||||
type_label_visited=भेट दिलेले
|
||||
type_label_bookmarked=वाचनखुण लावले
|
||||
type_label_synced=इतर साधनावरुन ताळमेळ केले
|
||||
# LOCALIZATION NOTE(type_label_open): Open is an adjective, as in "page is open"
|
||||
type_label_open=उघडा
|
||||
type_label_topic=विषय
|
||||
type_label_recommended=प्रचलित
|
||||
type_label_pocket=Pocket मध्ये जतन झाले
|
||||
type_label_downloaded=डाउनलोड केलेले
|
||||
|
||||
# LOCALIZATION NOTE (menu_action_*): These strings are displayed in a context
|
||||
# menu and are meant as a call to action for a given page.
|
||||
@ -24,19 +30,30 @@ type_label_topic=विषय
|
||||
# bookmarks"
|
||||
menu_action_bookmark=वाचनखुण
|
||||
menu_action_remove_bookmark=वाचनखुण काढा
|
||||
menu_action_copy_address=पत्त्याची प्रत बनवा
|
||||
menu_action_email_link=दुवा इमेल करा…
|
||||
menu_action_open_new_window=नवीन पटलात उघडा
|
||||
menu_action_open_private_window=नवीन खाजगी पटलात उघडा
|
||||
menu_action_dismiss=रद्द करा
|
||||
menu_action_delete=इतिहासातून नष्ट करा
|
||||
menu_action_pin=पिन लावा
|
||||
menu_action_unpin=पिन काढा
|
||||
confirm_history_delete_p1=आपल्या इतिहासामधून या पृष्ठातील प्रत्येक उदाहरण खात्रीने हटवू इच्छिता?
|
||||
# LOCALIZATION NOTE (confirm_history_delete_notice_p2): this string is displayed in
|
||||
# the same dialog as confirm_history_delete_p1. "This action" refers to deleting a
|
||||
# page from history.
|
||||
confirm_history_delete_notice_p2=ही क्रिया पूर्ववत केली जाऊ शकत नाही.
|
||||
menu_action_save_to_pocket=Pocket मध्ये जतन करा
|
||||
menu_action_delete_pocket=Pocket मधून हटवा
|
||||
menu_action_archive_pocket=Pocket मध्ये संग्रहित करा
|
||||
|
||||
# LOCALIZATION NOTE (search_for_something_with): {search_term} is a placeholder
|
||||
# for what the user has typed in the search input field, e.g. 'Search for ' +
|
||||
# search_term + 'with:' becomes 'Search for abc with:'
|
||||
# The search engine name is displayed as an icon and does not need a translation
|
||||
search_for_something_with=शोधा {search_term} सोबत:
|
||||
# LOCALIZATION NOTE (menu_action_show_file_*): These are platform specific strings
|
||||
# found in the context menu of an item that has been downloaded. The intention behind
|
||||
# "this action" is that it will show where the downloaded file exists on the file system
|
||||
# for each operating system.
|
||||
menu_action_show_file_mac_os=Finder मध्ये दर्शवा
|
||||
|
||||
# LOCALIZATION NOTE (menu_action_copy_download_link, menu_action_go_to_download_page):
|
||||
# "Download" here, in both cases, is not a verb, it is a noun. As in, "Copy the
|
||||
# link that belongs to this downloaded item"
|
||||
|
||||
# LOCALIZATION NOTE (search_button): This is screenreader only text for the
|
||||
# search button.
|
||||
@ -50,36 +67,101 @@ search_header={search_engine_name} शोध
|
||||
# LOCALIZATION NOTE (search_web_placeholder): This is shown in the searchbox when
|
||||
# the user hasn't typed anything yet.
|
||||
search_web_placeholder=वेबवर शोधा
|
||||
search_settings=शोध सेटिंग बदला
|
||||
|
||||
# LOCALIZATION NOTE (welcome_*): This is shown as a modal dialog, typically on a
|
||||
# first-run experience when there's no data to display yet
|
||||
welcome_title=नवीन टॅबवर स्वागत आहे
|
||||
# LOCALIZATION NOTE (section_disclaimer_topstories): This is shown below
|
||||
# the topstories section title to provide additional information about
|
||||
# how the stories are selected.
|
||||
section_disclaimer_topstories=आपण जे वाचतो त्यानुसार निवडलेल्या, वेबवरील सर्वात मनोरंजक कथा. Pocket कडून, आता Mozilla चा भाग.
|
||||
section_disclaimer_topstories_linktext=कसे कार्य करते ते जाणून घ्या.
|
||||
# LOCALIZATION NOTE (section_disclaimer_topstories_buttontext): The text of
|
||||
# the button used to acknowledge, and hide this disclaimer in the future.
|
||||
section_disclaimer_topstories_buttontext=ठीक आहे, समजले
|
||||
|
||||
# LOCALIZATION NOTE (time_label_*): {number} is a placeholder for a number which
|
||||
# represents a shortened timestamp format, e.g. '10m' means '10 minutes ago'.
|
||||
time_label_less_than_minute=<1मि
|
||||
time_label_minute={number}मि
|
||||
time_label_hour={number}ता
|
||||
time_label_day={number}दि
|
||||
|
||||
# LOCALIZATION NOTE (settings_pane_*): This is shown in the Settings Pane sidebar.
|
||||
# LOCALIZATION NOTE (prefs_*, settings_*): These are shown in about:preferences
|
||||
# for a "Firefox Home" section. "Firefox" should be treated as a brand and kept
|
||||
# in English, while "Home" should be localized matching the about:preferences
|
||||
# sidebar mozilla-central string for the panel that has preferences related to
|
||||
# what is shown for the homepage, new windows, and new tabs.
|
||||
prefs_home_header=फायरफॉक्स होम वरील मजकूर
|
||||
prefs_home_description=आपल्या फायरफॉक्सचा मुख्यपृष्ठवर आपल्याला कोणती माहिती पाहिजे ते निवडा.
|
||||
# LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
|
||||
# plural forms used in a drop down of multiple row options (1 row, 2 rows).
|
||||
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
|
||||
prefs_search_header=वेब शोध
|
||||
prefs_topsites_description=आपण सर्वाधिक भेट देता त्या साइट
|
||||
prefs_topstories_description2=आपल्यासाठी वैयक्तिकीकृत केलेल्या वेबवरील छान सामग्री
|
||||
prefs_topstories_sponsored_learn_more=अधिक जाणून घ्या
|
||||
prefs_highlights_description=आपण जतन केलेल्या किंवा भेट दिलेल्या साइट्सचा एक निवडक साठा
|
||||
prefs_snippets_description=Mozilla आणि Firefox कडून अद्यतने
|
||||
settings_pane_button_label=आपले नवीन टॅब पृष्ठ सानुकूलित करा
|
||||
settings_pane_header=नवीन टॅब प्राधान्ये
|
||||
settings_pane_body=नवीन टॅब उघडल्यानंतर काय दिसायला हवे ते निवडा.
|
||||
settings_pane_search_header=शोध
|
||||
settings_pane_search_body=आपल्या नवीन टॅब वरून वेबवर शोधा.
|
||||
settings_pane_topsites_header=शीर्ष साइट्स
|
||||
settings_pane_highlights_header=ठळक
|
||||
settings_pane_highlights_options_bookmarks=वाचनखुणा
|
||||
# LOCALIZATION NOTE(settings_pane_snippets_header): For the "Snippets" feature
|
||||
# traditionally on about:home. Alternative translation options: "Small Note" or
|
||||
# something that expresses the idea of "a small message, shortened from
|
||||
# something else, and non-essential but also not entirely trivial and useless."
|
||||
settings_pane_snippets_header=कात्रणे
|
||||
|
||||
# LOCALIZATION NOTE (edit_topsites_*): This is shown in the Edit Top Sites modal
|
||||
# dialog.
|
||||
edit_topsites_button_text=संपादित करा
|
||||
edit_topsites_edit_button=ही साइट संपादित करा
|
||||
|
||||
# LOCALIZATION NOTE (topsites_form_*): This is shown in the New/Edit Topsite modal.
|
||||
topsites_form_add_header=नवीन खास साइट
|
||||
topsites_form_edit_header=खास साईट संपादित करा
|
||||
topsites_form_title_label=शिर्षक
|
||||
topsites_form_title_placeholder=शिर्षक प्रविष्ट करा
|
||||
topsites_form_url_placeholder=URL चिकटवा किंवा टाईप करा
|
||||
# LOCALIZATION NOTE (topsites_form_*_button): These are verbs/actions.
|
||||
topsites_form_preview_button=पूर्वावलोकन
|
||||
topsites_form_add_button=समाविष्ट करा
|
||||
topsites_form_save_button=जतन करा
|
||||
topsites_form_cancel_button=रद्द करा
|
||||
topsites_form_url_validation=वैध URL आवश्यक
|
||||
|
||||
# LOCALIZATION NOTE (pocket_read_more): This is shown at the bottom of the
|
||||
# trending stories section and precedes a list of links to popular topics.
|
||||
pocket_read_more=लोकप्रिय विषय:
|
||||
# LOCALIZATION NOTE (pocket_read_even_more): This is shown as a link at the
|
||||
# end of the list of popular topic links.
|
||||
# LOCALIZATION NOTE (pocket_feedback_header): This is shown as an introduction
|
||||
# to Pocket as part of the feedback form.
|
||||
# LOCALIZATION NOTE (pocket_feedback_body): This is shown below
|
||||
# (pocket_feedback_header) to provide more information about Pocket.
|
||||
pocket_read_even_more=अधिक कथा पहा
|
||||
|
||||
highlights_empty_state=ब्राउझिंग सुरू करा, आणि आम्ही आपल्याला इथे आपण अलीकडील भेट दिलेले किंवा वाचनखूण लावलेले उत्कृष्ठ लेख, व्हिडिओ, आणि इतर पृष्ठांपैकी काही दाखवू.
|
||||
# LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,
|
||||
# in the space that would have shown a few stories, this is shown instead.
|
||||
# {provider} is replaced by the name of the content provider for this section.
|
||||
topstories_empty_state=तुम्ही सर्व बघितले. {provider} कडून आणखी महत्वाच्या गोष्टी बघण्यासाठी नंतर परत तपासा. प्रतीक्षा करू शकत नाही? वेबवरील छान गोष्टी शोधण्यासाठी लोकप्रिय विषय निवडा.
|
||||
|
||||
# LOCALIZATION NOTE (manual_migration_explanation2): This message is shown to encourage users to
|
||||
# import their browser profile from another browser they might be using.
|
||||
manual_migration_explanation2=दुसऱ्या ब्राऊझरमधील वाचनखूणा, इतिहास आणि पासवर्ड सोबत Firefox ला वापरून पहा.
|
||||
# LOCALIZATION NOTE (manual_migration_cancel_button): This message is shown on a button that cancels the
|
||||
# process of importing another browser’s profile into Firefox.
|
||||
manual_migration_cancel_button=नाही धन्यवाद
|
||||
# LOCALIZATION NOTE (manual_migration_import_button): This message is shown on a button that starts the process
|
||||
# of importing another browser’s profile profile into Firefox.
|
||||
manual_migration_import_button=आता आयात करा
|
||||
|
||||
# LOCALIZATION NOTE (error_fallback_default_*): This message and suggested
|
||||
# action link are shown in each section of UI that fails to render
|
||||
|
||||
# LOCALIZATION NOTE (section_menu_action_*). These strings are displayed in the section
|
||||
# context menu and are meant as a call to action for the given section.
|
||||
section_menu_action_move_up=वर जा
|
||||
section_menu_action_move_down=खाली जा
|
||||
section_menu_action_privacy_notice=गोपनीयता सूचना
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
|
||||
# firstrun of the browser, they give an introduction to Firefox and Sync.
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
|
||||
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
|
||||
# firstrun_form_header is displayed more boldly as the call to action.
|
||||
|
||||
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
|
||||
|
@ -90,7 +90,7 @@ section_disclaimer_topstories_buttontext=Ok, entendi
|
||||
# sidebar mozilla-central string for the panel that has preferences related to
|
||||
# what is shown for the homepage, new windows, and new tabs.
|
||||
prefs_home_header=Conteúdo inicial do Firefox
|
||||
prefs_home_description=Escolha qual conteúdo você quer na sua tela inicial do Firefox.
|
||||
prefs_home_description=Escolha que conteúdo você quer na sua tela inicial do Firefox.
|
||||
# LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
|
||||
# plural forms used in a drop down of multiple row options (1 row, 2 rows).
|
||||
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
|
||||
@ -191,6 +191,8 @@ firstrun_form_sub_header=para continuar com o Firefox Sync.
|
||||
|
||||
firstrun_email_input_placeholder=E-mail
|
||||
|
||||
firstrun_invalid_input=Necessário e-mail válido
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_extra_legal_links=Ao continuar você concorda com os {terms} e {privacy}.
|
||||
|
@ -158,10 +158,14 @@ section_menu_action_privacy_notice=رازداری کا نوٹس
|
||||
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
|
||||
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
|
||||
# firstrun_form_header is displayed more boldly as the call to action.
|
||||
firstrun_form_header=اپنی ای میل داخل کریں
|
||||
|
||||
firstrun_email_input_placeholder=ای میل
|
||||
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_terms_of_service=خدمت کی شرائط
|
||||
firstrun_privacy_notice=رازداری کا نوٹس
|
||||
|
||||
firstrun_continue_to_login=جاری رکھیں
|
||||
|
24
browser/extensions/activity-stream/mochitest.sh
Normal file
24
browser/extensions/activity-stream/mochitest.sh
Normal file
@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
export SHELL=/bin/bash
|
||||
# Display required for `browser_parsable_css` tests
|
||||
export DISPLAY=:99.0
|
||||
/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 -extension RANDR
|
||||
|
||||
# Pull latest m-c and update tip
|
||||
cd /mozilla-central && hg pull && hg update -C
|
||||
|
||||
# Build Activity Stream and copy the output to m-c
|
||||
cd /activity-stream && npm install . && npm run buildmc
|
||||
|
||||
# Build latest m-c with Activity Stream changes
|
||||
cd /mozilla-central && ./mach build \
|
||||
&& ./mach test browser_parsable_css \
|
||||
&& ./mach lint -l eslint -l codespell browser/extensions/activity-stream \
|
||||
&& ./mach test browser/extensions/activity-stream --headless \
|
||||
&& ./mach test browser/components/newtab/tests/browser --headless \
|
||||
&& ./mach test browser/components/newtab/tests/xpcshell \
|
||||
&& ./mach test browser/components/preferences/in-content/tests/browser_hometab_restore_defaults.js --headless \
|
||||
&& ./mach test browser/components/preferences/in-content/tests/browser_newtab_menu.js --headless \
|
||||
&& ./mach test browser/components/enterprisepolicies/tests/browser/browser_policy_set_homepage.js --headless \
|
||||
&& ./mach test browser/components/preferences/in-content/tests/browser_search_subdialogs_within_preferences_1.js --headless
|
@ -90,7 +90,7 @@
|
||||
"mc_dir": "../mozilla-central"
|
||||
},
|
||||
"scripts": {
|
||||
"mochitest": "(cd $npm_package_config_mc_dir && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest )",
|
||||
"mochitest": "(cd $npm_package_config_mc_dir && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest --headless)",
|
||||
"mochitest-debug": "(cd $npm_package_config_mc_dir && ./mach mochitest --jsdebugger browser/extensions/activity-stream/test/functional/mochitest)",
|
||||
"bundle": "npm-run-all bundle:*",
|
||||
"bundle:locales": "pontoon-to-json --src locales --dest data",
|
||||
|
@ -85,21 +85,21 @@ window.gActivityStreamStrings = {
|
||||
"section_menu_action_collapse_section": "Abschnitt einklappen",
|
||||
"section_menu_action_expand_section": "Abschnitt ausklappen",
|
||||
"section_menu_action_manage_section": "Abschnitt verwalten",
|
||||
"section_menu_action_manage_webext": "Manage Extension",
|
||||
"section_menu_action_manage_webext": "Erweiterung verwalten",
|
||||
"section_menu_action_add_topsite": "Wichtige Seite hinzufügen",
|
||||
"section_menu_action_move_up": "Nach oben schieben",
|
||||
"section_menu_action_move_down": "Nach unten schieben",
|
||||
"section_menu_action_privacy_notice": "Datenschutzhinweis",
|
||||
"firstrun_title": "Take Firefox with You",
|
||||
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
|
||||
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
|
||||
"firstrun_title": "Firefox für unterwegs",
|
||||
"firstrun_content": "Nehmen Sie Ihre Lesezeichen, Chronik, Passwörter und andere Einstellungen auf allen Geräten mit.",
|
||||
"firstrun_learn_more_link": "Jetzt mehr über Firefox Konten erfahren",
|
||||
"firstrun_form_header": "E-Mail-Adresse eingeben",
|
||||
"firstrun_form_sub_header": "to continue to Firefox Sync",
|
||||
"firstrun_email_input_placeholder": "Email",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
|
||||
"firstrun_terms_of_service": "Terms of Service",
|
||||
"firstrun_privacy_notice": "Privacy Notice",
|
||||
"firstrun_continue_to_login": "Continue",
|
||||
"firstrun_skip_login": "Skip this step"
|
||||
"firstrun_form_sub_header": "um sich bei Firefox Sync anzumelden.",
|
||||
"firstrun_email_input_placeholder": "E-Mail",
|
||||
"firstrun_invalid_input": "Gültige E-Mail-Adresse erforderlich",
|
||||
"firstrun_extra_legal_links": "Indem Sie fortfahren, stimmen Sie den {terms} und dem {privacy} zu.",
|
||||
"firstrun_terms_of_service": "Nutzungsbedingungen",
|
||||
"firstrun_privacy_notice": "Datenschutzhinweis",
|
||||
"firstrun_continue_to_login": "Weiter",
|
||||
"firstrun_skip_login": "Diesen Schritt überspringen"
|
||||
};
|
||||
|
@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
|
||||
"firstrun_form_header": "پستالکترونیکی خود را وارد کنید",
|
||||
"firstrun_form_sub_header": "برای فعال کردن همگامسازی فایرفاکس.",
|
||||
"firstrun_email_input_placeholder": "پستالکترونیکی",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_invalid_input": "رایانامهٔ معتبر لازم است",
|
||||
"firstrun_extra_legal_links": "با ادامه دادن، شما {terms} و {privacy} قبول میکنید.",
|
||||
"firstrun_terms_of_service": "قوانین سرویس",
|
||||
"firstrun_privacy_notice": "نکات حریمخصوصی",
|
||||
|
@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
|
||||
"firstrun_form_header": "Kirjoita sähköpostisi",
|
||||
"firstrun_form_sub_header": "jatkaaksesi Firefox Sync -palveluun.",
|
||||
"firstrun_email_input_placeholder": "Sähköposti",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_invalid_input": "Sähköpostiosoitteen täytyy olla kelvollinen",
|
||||
"firstrun_extra_legal_links": "Jatkamalla hyväksyt {terms} ja {privacy}.",
|
||||
"firstrun_terms_of_service": "käyttöehdot",
|
||||
"firstrun_privacy_notice": "tietosuojakäytännön",
|
||||
|
@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
|
||||
"firstrun_form_header": "Saisissez votre adresse électronique",
|
||||
"firstrun_form_sub_header": "pour continuer avec Firefox Sync.",
|
||||
"firstrun_email_input_placeholder": "Adresse électronique",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_invalid_input": "Adresse électronique valide requise",
|
||||
"firstrun_extra_legal_links": "En continuant, vous acceptez les {terms} et la {privacy}.",
|
||||
"firstrun_terms_of_service": "Conditions d’utilisation",
|
||||
"firstrun_privacy_notice": "Politique de confidentialité",
|
||||
|
@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
|
||||
"firstrun_form_header": "તમારા ઇમેઇલ દાખલ કરો",
|
||||
"firstrun_form_sub_header": "Firefox સમન્વયન ચાલુ રાખવા માટે.",
|
||||
"firstrun_email_input_placeholder": "ઇમેઇલ",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_invalid_input": "માન્ય ઇમેઇલ આવશ્યક છે",
|
||||
"firstrun_extra_legal_links": "આગળ વધીને, તમે {terms} અને {privacy} સાથે સંમત થાઓ છો.",
|
||||
"firstrun_terms_of_service": "સેવાની શરતો",
|
||||
"firstrun_privacy_notice": "ખાનગી સૂચના",
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -2,7 +2,7 @@
|
||||
window.gActivityStreamStrings = {
|
||||
"newtab_page_title": "नया टैब",
|
||||
"header_top_sites": "सर्वोच्च साइटें",
|
||||
"header_highlights": "झलकियाँ",
|
||||
"header_highlights": "प्रमुखताएँ",
|
||||
"header_recommended_by": "{provider} द्वारा अनुशंसित",
|
||||
"context_menu_button_sr": "{title} के लिए कॉन्टेक्स्ट मेनू खोलें",
|
||||
"section_context_menu_button_sr": "अनुभाग प्रसंग मेनू खोलें",
|
||||
@ -53,7 +53,7 @@ window.gActivityStreamStrings = {
|
||||
"prefs_snippets_description": "Mozilla और Firefox से अद्यतन",
|
||||
"settings_pane_button_label": "अपने नए टैब पृष्ठ को अनुकूलित करें",
|
||||
"settings_pane_topsites_header": "सर्वोच्च साइटें",
|
||||
"settings_pane_highlights_header": "झलकियाँ",
|
||||
"settings_pane_highlights_header": "प्रमुखताएँ",
|
||||
"settings_pane_highlights_options_bookmarks": "पुस्तचिह्न",
|
||||
"settings_pane_snippets_header": "अंश",
|
||||
"edit_topsites_button_text": "संपादित करें",
|
||||
|
@ -74,7 +74,7 @@ window.gActivityStreamStrings = {
|
||||
"topsites_form_image_validation": "სურათი ვერ ჩაიტვირთა. სცადეთ სხვა URL ბმული.",
|
||||
"pocket_read_more": "პოპულარული თემები:",
|
||||
"pocket_read_even_more": "მეტი სიახლის ნახვა",
|
||||
"highlights_empty_state": "დაიწყეთ გვერდების დათვალიერება და აქ გამოჩნდება თქვენი რჩეული სტატიები, ვიდეოები და ბოლოს მონახულებული ან ჩანიშნული საიტები.",
|
||||
"highlights_empty_state": "დაიწყეთ გვერდების დათვალიერება და აქ გამოჩნდება თქვენთვის სასურველი სტატიები, ვიდეოები და ბოლოს მონახულებული ან ჩანიშნული საიტები.",
|
||||
"topstories_empty_state": "უკვე ყველაფერი წაკითხული გაქვთ. {provider}-იდან ახალი რჩეული სტატიების მისაღებად, მოგვიანებით შემოიარეთ. თუ ვერ ითმენთ, აირჩიეთ რომელიმე მოთხოვნადი თემა, ახალი საინტერესო სტატიების მოსაძიებლად.",
|
||||
"manual_migration_explanation2": "გადმოიტანეთ სხვა ბრაუზერებიდან თქვენი სანიშნები, ისტორია და პაროლები Firefox-ში.",
|
||||
"manual_migration_cancel_button": "არა, გმადლობთ",
|
||||
|
@ -27,11 +27,11 @@ window.gActivityStreamStrings = {
|
||||
"menu_action_show_file_mac_os": "Show in Finder",
|
||||
"menu_action_show_file_windows": "Open Containing Folder",
|
||||
"menu_action_show_file_linux": "Open Containing Folder",
|
||||
"menu_action_show_file_default": "Show File",
|
||||
"menu_action_open_file": "Open File",
|
||||
"menu_action_copy_download_link": "Copy Download Link",
|
||||
"menu_action_go_to_download_page": "Go to Download Page",
|
||||
"menu_action_remove_download": "Remove from History",
|
||||
"menu_action_show_file_default": "ಕಡತ ತೋರಿಸು",
|
||||
"menu_action_open_file": "ಕಡತವನ್ನು ತೆರೆ",
|
||||
"menu_action_copy_download_link": "ಡೌನ್ಲೋಡ್ ಕೊಂಡಿಯನ್ನು ಪ್ರತಿ ಮಾಡು",
|
||||
"menu_action_go_to_download_page": "ಡೌನ್ಲೋಡ್ ಪುಟಕ್ಕೆ ತೆರಳು",
|
||||
"menu_action_remove_download": "ಇತಿಹಾಸದಿಂದ ತೆಗೆದುಹಾಕು",
|
||||
"search_button": "ಹುಡುಕು",
|
||||
"search_header": "{search_engine_name} ನಿಂದ ಹುಡುಕಿ",
|
||||
"search_web_placeholder": "ಅಂತರ್ಜಾಲವನ್ನು ಹುಡುಕಿ",
|
||||
@ -41,7 +41,7 @@ window.gActivityStreamStrings = {
|
||||
"prefs_home_header": "Firefox Home Content",
|
||||
"prefs_home_description": "Choose what content you want on your Firefox Home screen.",
|
||||
"prefs_section_rows_option": "{num} row;{num} rows",
|
||||
"prefs_search_header": "Web Search",
|
||||
"prefs_search_header": "ಜಾಲದ ಹುಡುಕಾಟ",
|
||||
"prefs_topsites_description": "The sites you visit most",
|
||||
"prefs_topstories_description2": "Great content from around the web, personalized for you",
|
||||
"prefs_topstories_options_sponsored_label": "Sponsored Stories",
|
||||
@ -60,13 +60,13 @@ window.gActivityStreamStrings = {
|
||||
"edit_topsites_edit_button": "ಈ ತಾಣವನ್ನು ಸಂಪಾದಿಸು",
|
||||
"topsites_form_add_header": "ಹೊಸ ಅಗ್ರ ತಾಣಗಳು",
|
||||
"topsites_form_edit_header": "ಅಗ್ರ ತಾಣಗಳನ್ನು ಸಂಪಾದಿಸಿ",
|
||||
"topsites_form_title_label": "Title",
|
||||
"topsites_form_title_label": "ಶೀರ್ಷಿಕೆ",
|
||||
"topsites_form_title_placeholder": "ಶೀರ್ಷಿಕೆಯನ್ನು ನಮೂದಿಸಿ",
|
||||
"topsites_form_url_label": "URL",
|
||||
"topsites_form_image_url_label": "Custom Image URL",
|
||||
"topsites_form_url_placeholder": "ಒಂದು URL ಅನ್ನು ಟೈಪಿಸಿ ಅಥವಾ ನಕಲಿಸಿ",
|
||||
"topsites_form_use_image_link": "Use a custom image…",
|
||||
"topsites_form_preview_button": "Preview",
|
||||
"topsites_form_preview_button": "ಮುನ್ನೋಟ",
|
||||
"topsites_form_add_button": "ಸೇರಿಸು",
|
||||
"topsites_form_save_button": "ಉಳಿಸು",
|
||||
"topsites_form_cancel_button": "ರದ್ದು ಮಾಡು",
|
||||
@ -87,7 +87,7 @@ window.gActivityStreamStrings = {
|
||||
"section_menu_action_manage_section": "Manage Section",
|
||||
"section_menu_action_manage_webext": "Manage Extension",
|
||||
"section_menu_action_add_topsite": "Add Top Site",
|
||||
"section_menu_action_move_up": "Move Up",
|
||||
"section_menu_action_move_up": "ಮೇಲೆ ಜರುಗಿಸು",
|
||||
"section_menu_action_move_down": "Move Down",
|
||||
"section_menu_action_privacy_notice": "Privacy Notice",
|
||||
"firstrun_title": "Take Firefox with You",
|
||||
@ -95,50 +95,11 @@ window.gActivityStreamStrings = {
|
||||
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
|
||||
"firstrun_form_header": "Enter your email",
|
||||
"firstrun_form_sub_header": "to continue to Firefox Sync",
|
||||
"firstrun_email_input_placeholder": "Email",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_email_input_placeholder": "ಇಮೇಲ್",
|
||||
"firstrun_invalid_input": "ಸರಿಯಾದ ಇಮೇಲ್ ಬೇಕಾಗಿದೆ",
|
||||
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
|
||||
"firstrun_terms_of_service": "Terms of Service",
|
||||
"firstrun_privacy_notice": "Privacy Notice",
|
||||
"firstrun_continue_to_login": "Continue",
|
||||
"firstrun_skip_login": "Skip this step",
|
||||
"default_label_loading": "ಲೋಡ್ ಆಗುತ್ತಿದೆ…",
|
||||
"header_stories": "ಪ್ರಮುಖ ಸುದ್ದಿಗಳು",
|
||||
"header_visit_again": "ಮತ್ತೆ ಭೇಟಿಕೊಡು",
|
||||
"header_bookmarks": "ಇತ್ತೀಚಿಗೆ ಮಾಡಲಾದ ಬುಕ್ಮಾರ್ಕುಗಳು",
|
||||
"header_bookmarks_placeholder": "ನಿಮ್ಮ ಹತ್ತಿರ ಇನ್ನೂ ಯಾವುದೇ ಪುಟಗುರುತುಗಳಿಲ್ಲ.",
|
||||
"header_stories_from": "ಯಿಂದ",
|
||||
"type_label_synced": "ಮತ್ತೊಂದು ಸಾಧನದಿಂದ ಸಿಂಕ್ ಮಾಡಲಾಗಿದೆ",
|
||||
"type_label_open": "ತೆರೆ",
|
||||
"type_label_topic": "ವಿಷಯ",
|
||||
"type_label_now": "ಈಗ",
|
||||
"menu_action_copy_address": "ವಿಳಾಸವನ್ನು ನಕಲಿಸು",
|
||||
"menu_action_email_link": "ಇಮೈಲ್ ಕೊಂಡಿ…",
|
||||
"search_for_something_with": "{search_term} ಅನ್ನು ಇದರಿಂದ ಹುಡುಕಿ:",
|
||||
"search_settings": "ಹುಡುಕು ಸಿದ್ಧತೆಗಳನ್ನು ಬದಲಾಯಿಸು",
|
||||
"section_info_option": "ಮಾಹಿತಿ",
|
||||
"section_info_send_feedback": "ಅಭಿಪ್ರಾಯವನ್ನು ಕಳುಹಿಸಿ",
|
||||
"section_info_privacy_notice": "ಗೌಪ್ಯತಾ ಸೂಚನೆ",
|
||||
"welcome_title": "ಹೊಸ ಹಾಳೆಗೆ ಸುಸ್ವಾಗತ",
|
||||
"time_label_less_than_minute": "<1ನಿ",
|
||||
"time_label_minute": "{number}ನಿ",
|
||||
"time_label_hour": "{number}ಗ",
|
||||
"time_label_day": "{number}ದಿ",
|
||||
"settings_pane_header": "ಹೊಸ ಹಾಳೆಯ ಆದ್ಯತೆಗಳು",
|
||||
"settings_pane_body2": "ನೀವು ಈ ಪುಟದಲ್ಲಿ ಏನು ನೋಡಿತ್ತೀರೆಂದು ಆಯ್ಕೆಮಾಡಿ.",
|
||||
"settings_pane_search_header": "ಹುಡುಕು",
|
||||
"settings_pane_search_body": "ಹೊಸ ಹಾಳೆಯಿಂದ ಅಂತರ್ಜಾಲವನ್ನು ಹುಡುಕಿ.",
|
||||
"settings_pane_topsites_body": "ನೀವು ಅತಿ ಹೆಚ್ಚು ನೋಡುವ ಜಾಲತಾಣಗಳಿಗೆ ಪ್ರವೇಶದ್ವಾರ.",
|
||||
"settings_pane_topsites_options_showmore": "ಎರಡು ಸಾಲುಗಳನ್ನು ಪ್ರದರ್ಶಿಸು",
|
||||
"settings_pane_bookmarks_header": "ಇತ್ತೀಚಿನ ಪುಟಗುರುತುಗಳು",
|
||||
"settings_pane_visit_again_header": "ಮತ್ತೆ ಭೇಟಿಕೊಡು",
|
||||
"settings_pane_highlights_options_visited": "ಭೇಟಿ ನೀಡಿದ ತಾಣಗಳು",
|
||||
"settings_pane_done_button": "ಆಯಿತು",
|
||||
"edit_topsites_showmore_button": "ಹೆಚ್ಚು ತೋರಿಸು",
|
||||
"edit_topsites_showless_button": "ಕೆಲವೊಂದು ತೋರಿಸಿ",
|
||||
"edit_topsites_done_button": "ಆಯಿತು",
|
||||
"edit_topsites_pin_button": "ಈ ತಾಣವನ್ನು ಪಿನ್ ಮಾಡು",
|
||||
"edit_topsites_unpin_button": "ಈ ತಾಣವನ್ನು ಹೊರತೆಗೆ",
|
||||
"edit_topsites_dismiss_button": "ಈ ತಾಣವನ್ನು ತೆಗೆದುಹಾಕು",
|
||||
"edit_topsites_add_button": "ಸೇರಿಸು"
|
||||
"firstrun_terms_of_service": "ಸೇವೆಯ ನಿಯಮಗಳು",
|
||||
"firstrun_privacy_notice": "ಗೌಪ್ಯತಾ ಸೂಚನೆ",
|
||||
"firstrun_continue_to_login": "ಮುಂದುವರೆ",
|
||||
"firstrun_skip_login": "ಈ ಹಂತವನ್ನು ಹಾರಿಸಿ"
|
||||
};
|
||||
|
@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
|
||||
"firstrun_form_header": "Įveskite savo el. paštą",
|
||||
"firstrun_form_sub_header": "norėdami tęsti su „Firefox Sync“.",
|
||||
"firstrun_email_input_placeholder": "El. paštas",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_invalid_input": "Reikalingas galiojantis el. pašto adresas",
|
||||
"firstrun_extra_legal_links": "Tęsdami sutinkate su {terms} ir {privacy}.",
|
||||
"firstrun_terms_of_service": "paslaugos teikimo nuostatais",
|
||||
"firstrun_privacy_notice": "privatumo nuostatais",
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -3,28 +3,28 @@ window.gActivityStreamStrings = {
|
||||
"newtab_page_title": "नवीन टॅब",
|
||||
"header_top_sites": "खास साईट्स",
|
||||
"header_highlights": "ठळक",
|
||||
"header_recommended_by": "Recommended by {provider}",
|
||||
"context_menu_button_sr": "Open context menu for {title}",
|
||||
"section_context_menu_button_sr": "Open the section context menu",
|
||||
"header_recommended_by": "{provider} तर्फे शिफारस",
|
||||
"context_menu_button_sr": "{title} साठी संदर्भ मेनू उघडा",
|
||||
"section_context_menu_button_sr": "विभाग संदर्भ मेनू उघडा",
|
||||
"type_label_visited": "भेट दिलेले",
|
||||
"type_label_bookmarked": "वाचनखुण लावले",
|
||||
"type_label_recommended": "Trending",
|
||||
"type_label_pocket": "Saved to Pocket",
|
||||
"type_label_downloaded": "Downloaded",
|
||||
"type_label_recommended": "प्रचलित",
|
||||
"type_label_pocket": "Pocket मध्ये जतन झाले",
|
||||
"type_label_downloaded": "डाउनलोड केलेले",
|
||||
"menu_action_bookmark": "वाचनखुण",
|
||||
"menu_action_remove_bookmark": "वाचनखुण काढा",
|
||||
"menu_action_open_new_window": "नवीन पटलात उघडा",
|
||||
"menu_action_open_private_window": "नवीन खाजगी पटलात उघडा",
|
||||
"menu_action_dismiss": "रद्द करा",
|
||||
"menu_action_delete": "इतिहासातून नष्ट करा",
|
||||
"menu_action_pin": "Pin",
|
||||
"menu_action_unpin": "Unpin",
|
||||
"confirm_history_delete_p1": "Are you sure you want to delete every instance of this page from your history?",
|
||||
"confirm_history_delete_notice_p2": "This action cannot be undone.",
|
||||
"menu_action_pin": "पिन लावा",
|
||||
"menu_action_unpin": "पिन काढा",
|
||||
"confirm_history_delete_p1": "आपल्या इतिहासामधून या पृष्ठातील प्रत्येक उदाहरण खात्रीने हटवू इच्छिता?",
|
||||
"confirm_history_delete_notice_p2": "ही क्रिया पूर्ववत केली जाऊ शकत नाही.",
|
||||
"menu_action_save_to_pocket": "Pocket मध्ये जतन करा",
|
||||
"menu_action_delete_pocket": "Delete from Pocket",
|
||||
"menu_action_archive_pocket": "Archive in Pocket",
|
||||
"menu_action_show_file_mac_os": "Show in Finder",
|
||||
"menu_action_delete_pocket": "Pocket मधून हटवा",
|
||||
"menu_action_archive_pocket": "Pocket मध्ये संग्रहित करा",
|
||||
"menu_action_show_file_mac_os": "Finder मध्ये दर्शवा",
|
||||
"menu_action_show_file_windows": "Open Containing Folder",
|
||||
"menu_action_show_file_linux": "Open Containing Folder",
|
||||
"menu_action_show_file_default": "Show File",
|
||||
@ -35,50 +35,50 @@ window.gActivityStreamStrings = {
|
||||
"search_button": "शोधा",
|
||||
"search_header": "{search_engine_name} शोध",
|
||||
"search_web_placeholder": "वेबवर शोधा",
|
||||
"section_disclaimer_topstories": "The most interesting stories on the web, selected based on what you read. From Pocket, now part of Mozilla.",
|
||||
"section_disclaimer_topstories_linktext": "Learn how it works.",
|
||||
"section_disclaimer_topstories_buttontext": "Okay, got it",
|
||||
"prefs_home_header": "Firefox Home Content",
|
||||
"prefs_home_description": "Choose what content you want on your Firefox Home screen.",
|
||||
"section_disclaimer_topstories": "आपण जे वाचतो त्यानुसार निवडलेल्या, वेबवरील सर्वात मनोरंजक कथा. Pocket कडून, आता Mozilla चा भाग.",
|
||||
"section_disclaimer_topstories_linktext": "कसे कार्य करते ते जाणून घ्या.",
|
||||
"section_disclaimer_topstories_buttontext": "ठीक आहे, समजले",
|
||||
"prefs_home_header": "फायरफॉक्स होम वरील मजकूर",
|
||||
"prefs_home_description": "आपल्या फायरफॉक्सचा मुख्यपृष्ठवर आपल्याला कोणती माहिती पाहिजे ते निवडा.",
|
||||
"prefs_section_rows_option": "{num} row;{num} rows",
|
||||
"prefs_search_header": "Web Search",
|
||||
"prefs_topsites_description": "The sites you visit most",
|
||||
"prefs_topstories_description2": "Great content from around the web, personalized for you",
|
||||
"prefs_search_header": "वेब शोध",
|
||||
"prefs_topsites_description": "आपण सर्वाधिक भेट देता त्या साइट",
|
||||
"prefs_topstories_description2": "आपल्यासाठी वैयक्तिकीकृत केलेल्या वेबवरील छान सामग्री",
|
||||
"prefs_topstories_options_sponsored_label": "Sponsored Stories",
|
||||
"prefs_topstories_sponsored_learn_more": "Learn more",
|
||||
"prefs_highlights_description": "A selection of sites that you’ve saved or visited",
|
||||
"prefs_topstories_sponsored_learn_more": "अधिक जाणून घ्या",
|
||||
"prefs_highlights_description": "आपण जतन केलेल्या किंवा भेट दिलेल्या साइट्सचा एक निवडक साठा",
|
||||
"prefs_highlights_options_visited_label": "Visited Pages",
|
||||
"prefs_highlights_options_download_label": "Most Recent Download",
|
||||
"prefs_highlights_options_pocket_label": "Pages Saved to Pocket",
|
||||
"prefs_snippets_description": "Updates from Mozilla and Firefox",
|
||||
"prefs_snippets_description": "Mozilla आणि Firefox कडून अद्यतने",
|
||||
"settings_pane_button_label": "आपले नवीन टॅब पृष्ठ सानुकूलित करा",
|
||||
"settings_pane_topsites_header": "Top Sites",
|
||||
"settings_pane_highlights_header": "Highlights",
|
||||
"settings_pane_highlights_options_bookmarks": "Bookmarks",
|
||||
"settings_pane_snippets_header": "Snippets",
|
||||
"edit_topsites_button_text": "Edit",
|
||||
"edit_topsites_edit_button": "Edit this site",
|
||||
"topsites_form_add_header": "New Top Site",
|
||||
"topsites_form_edit_header": "Edit Top Site",
|
||||
"topsites_form_title_label": "Title",
|
||||
"topsites_form_title_placeholder": "Enter a title",
|
||||
"settings_pane_topsites_header": "शीर्ष साइट्स",
|
||||
"settings_pane_highlights_header": "ठळक",
|
||||
"settings_pane_highlights_options_bookmarks": "वाचनखुणा",
|
||||
"settings_pane_snippets_header": "कात्रणे",
|
||||
"edit_topsites_button_text": "संपादित करा",
|
||||
"edit_topsites_edit_button": "ही साइट संपादित करा",
|
||||
"topsites_form_add_header": "नवीन खास साइट",
|
||||
"topsites_form_edit_header": "खास साईट संपादित करा",
|
||||
"topsites_form_title_label": "शिर्षक",
|
||||
"topsites_form_title_placeholder": "शिर्षक प्रविष्ट करा",
|
||||
"topsites_form_url_label": "URL",
|
||||
"topsites_form_image_url_label": "Custom Image URL",
|
||||
"topsites_form_url_placeholder": "Type or paste a URL",
|
||||
"topsites_form_url_placeholder": "URL चिकटवा किंवा टाईप करा",
|
||||
"topsites_form_use_image_link": "Use a custom image…",
|
||||
"topsites_form_preview_button": "Preview",
|
||||
"topsites_form_add_button": "Add",
|
||||
"topsites_form_save_button": "Save",
|
||||
"topsites_form_cancel_button": "Cancel",
|
||||
"topsites_form_url_validation": "Valid URL required",
|
||||
"topsites_form_preview_button": "पूर्वावलोकन",
|
||||
"topsites_form_add_button": "समाविष्ट करा",
|
||||
"topsites_form_save_button": "जतन करा",
|
||||
"topsites_form_cancel_button": "रद्द करा",
|
||||
"topsites_form_url_validation": "वैध URL आवश्यक",
|
||||
"topsites_form_image_validation": "Image failed to load. Try a different URL.",
|
||||
"pocket_read_more": "Popular Topics:",
|
||||
"pocket_read_even_more": "View More Stories",
|
||||
"highlights_empty_state": "Start browsing, and we’ll show some of the great articles, videos, and other pages you’ve recently visited or bookmarked here.",
|
||||
"topstories_empty_state": "You’ve caught up. Check back later for more top stories from {provider}. Can’t wait? Select a popular topic to find more great stories from around the web.",
|
||||
"manual_migration_explanation2": "Try Firefox with the bookmarks, history and passwords from another browser.",
|
||||
"manual_migration_cancel_button": "No Thanks",
|
||||
"manual_migration_import_button": "Import Now",
|
||||
"pocket_read_more": "लोकप्रिय विषय:",
|
||||
"pocket_read_even_more": "अधिक कथा पहा",
|
||||
"highlights_empty_state": "ब्राउझिंग सुरू करा, आणि आम्ही आपल्याला इथे आपण अलीकडील भेट दिलेले किंवा वाचनखूण लावलेले उत्कृष्ठ लेख, व्हिडिओ, आणि इतर पृष्ठांपैकी काही दाखवू.",
|
||||
"topstories_empty_state": "तुम्ही सर्व बघितले. {provider} कडून आणखी महत्वाच्या गोष्टी बघण्यासाठी नंतर परत तपासा. प्रतीक्षा करू शकत नाही? वेबवरील छान गोष्टी शोधण्यासाठी लोकप्रिय विषय निवडा.",
|
||||
"manual_migration_explanation2": "दुसऱ्या ब्राऊझरमधील वाचनखूणा, इतिहास आणि पासवर्ड सोबत Firefox ला वापरून पहा.",
|
||||
"manual_migration_cancel_button": "नाही धन्यवाद",
|
||||
"manual_migration_import_button": "आता आयात करा",
|
||||
"error_fallback_default_info": "Oops, something went wrong loading this content.",
|
||||
"error_fallback_default_refresh_suggestion": "Refresh page to try again.",
|
||||
"section_menu_action_remove_section": "Remove Section",
|
||||
@ -87,9 +87,9 @@ window.gActivityStreamStrings = {
|
||||
"section_menu_action_manage_section": "Manage Section",
|
||||
"section_menu_action_manage_webext": "Manage Extension",
|
||||
"section_menu_action_add_topsite": "Add Top Site",
|
||||
"section_menu_action_move_up": "Move Up",
|
||||
"section_menu_action_move_down": "Move Down",
|
||||
"section_menu_action_privacy_notice": "Privacy Notice",
|
||||
"section_menu_action_move_up": "वर जा",
|
||||
"section_menu_action_move_down": "खाली जा",
|
||||
"section_menu_action_privacy_notice": "गोपनीयता सूचना",
|
||||
"firstrun_title": "Take Firefox with You",
|
||||
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
|
||||
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
|
||||
@ -101,24 +101,5 @@ window.gActivityStreamStrings = {
|
||||
"firstrun_terms_of_service": "Terms of Service",
|
||||
"firstrun_privacy_notice": "Privacy Notice",
|
||||
"firstrun_continue_to_login": "Continue",
|
||||
"firstrun_skip_login": "Skip this step",
|
||||
"default_label_loading": "दाखल करीत आहे…",
|
||||
"header_stories": "महत्वाच्या गोष्टी",
|
||||
"header_stories_from": "कडून",
|
||||
"type_label_synced": "इतर साधनावरुन ताळमेळ केले",
|
||||
"type_label_open": "उघडा",
|
||||
"type_label_topic": "विषय",
|
||||
"menu_action_copy_address": "पत्त्याची प्रत बनवा",
|
||||
"menu_action_email_link": "दुवा इमेल करा…",
|
||||
"search_for_something_with": "शोधा {search_term} सोबत:",
|
||||
"search_settings": "शोध सेटिंग बदला",
|
||||
"welcome_title": "नवीन टॅबवर स्वागत आहे",
|
||||
"time_label_less_than_minute": "<1मि",
|
||||
"time_label_minute": "{number}मि",
|
||||
"time_label_hour": "{number}ता",
|
||||
"time_label_day": "{number}दि",
|
||||
"settings_pane_header": "नवीन टॅब प्राधान्ये",
|
||||
"settings_pane_body": "नवीन टॅब उघडल्यानंतर काय दिसायला हवे ते निवडा.",
|
||||
"settings_pane_search_header": "शोध",
|
||||
"settings_pane_search_body": "आपल्या नवीन टॅब वरून वेबवर शोधा."
|
||||
"firstrun_skip_login": "Skip this step"
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ window.gActivityStreamStrings = {
|
||||
"section_disclaimer_topstories_linktext": "Saiba como funciona.",
|
||||
"section_disclaimer_topstories_buttontext": "Ok, entendi",
|
||||
"prefs_home_header": "Conteúdo inicial do Firefox",
|
||||
"prefs_home_description": "Escolha qual conteúdo você quer na sua tela inicial do Firefox.",
|
||||
"prefs_home_description": "Escolha que conteúdo você quer na sua tela inicial do Firefox.",
|
||||
"prefs_section_rows_option": "{num} linha;{num} linhas",
|
||||
"prefs_search_header": "Pesquisa na web",
|
||||
"prefs_topsites_description": "Os sites que você mais visita",
|
||||
@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
|
||||
"firstrun_form_header": "Insira seu email",
|
||||
"firstrun_form_sub_header": "para continuar com o Firefox Sync.",
|
||||
"firstrun_email_input_placeholder": "E-mail",
|
||||
"firstrun_invalid_input": "Email válido requerido",
|
||||
"firstrun_invalid_input": "Necessário e-mail válido",
|
||||
"firstrun_extra_legal_links": "Ao continuar você concorda com os {terms} e {privacy}.",
|
||||
"firstrun_terms_of_service": "Termos de serviço",
|
||||
"firstrun_privacy_notice": "Política de privacidade",
|
||||
|
@ -93,13 +93,13 @@ window.gActivityStreamStrings = {
|
||||
"firstrun_title": "Take Firefox with You",
|
||||
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
|
||||
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
|
||||
"firstrun_form_header": "Enter your email",
|
||||
"firstrun_form_header": "اپنی ای میل داخل کریں",
|
||||
"firstrun_form_sub_header": "to continue to Firefox Sync",
|
||||
"firstrun_email_input_placeholder": "ای میل",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
|
||||
"firstrun_terms_of_service": "Terms of Service",
|
||||
"firstrun_privacy_notice": "Privacy Notice",
|
||||
"firstrun_terms_of_service": "خدمت کی شرائط",
|
||||
"firstrun_privacy_notice": "رازداری کا نوٹس",
|
||||
"firstrun_continue_to_login": "جاری رکھیں",
|
||||
"firstrun_skip_login": "Skip this step"
|
||||
};
|
||||
|
@ -6,6 +6,10 @@ ChromeUtils.defineModuleGetter(this, "AddonManager",
|
||||
"resource://gre/modules/AddonManager.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "ShellService",
|
||||
"resource:///modules/ShellService.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "NewTabUtils",
|
||||
"resource://gre/modules/NewTabUtils.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "PlacesTestUtils",
|
||||
"resource://testing-common/PlacesTestUtils.jsm");
|
||||
|
||||
const {AddonTestUtils} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", {});
|
||||
|
||||
@ -28,7 +32,7 @@ add_task(async function find_matching_message() {
|
||||
];
|
||||
const context = {FOO: true};
|
||||
|
||||
const match = await ASRouterTargeting.findMatchingMessage(messages, {}, context);
|
||||
const match = await ASRouterTargeting.findMatchingMessage({messages, target: {}, context});
|
||||
|
||||
is(match, messages[0], "should match and return the correct message");
|
||||
});
|
||||
@ -37,7 +41,7 @@ add_task(async function return_nothing_for_no_matching_message() {
|
||||
const messages = [{id: "bar", targeting: "!FOO"}];
|
||||
const context = {FOO: true};
|
||||
|
||||
const match = await ASRouterTargeting.findMatchingMessage(messages, {}, context);
|
||||
const match = await ASRouterTargeting.findMatchingMessage({messages, target: {}, context});
|
||||
|
||||
is(match, undefined, "should return nothing since no matching message exists");
|
||||
});
|
||||
@ -49,7 +53,7 @@ add_task(async function checkProfileAgeCreated() {
|
||||
"should return correct profile age creation date");
|
||||
|
||||
const message = {id: "foo", targeting: `profileAgeCreated > ${await profileAccessor.created - 100}`};
|
||||
is(await ASRouterTargeting.findMatchingMessage([message], {}), message,
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
|
||||
"should select correct item by profile age created");
|
||||
});
|
||||
|
||||
@ -59,7 +63,7 @@ add_task(async function checkProfileAgeReset() {
|
||||
"should return correct profile age reset");
|
||||
|
||||
const message = {id: "foo", targeting: `profileAgeReset == ${await profileAccessor.reset}`};
|
||||
is(await ASRouterTargeting.findMatchingMessage([message], {}), message,
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
|
||||
"should select correct item by profile age reset");
|
||||
});
|
||||
|
||||
@ -69,7 +73,7 @@ add_task(async function checkhasFxAccount() {
|
||||
"should return true if a fx account is set");
|
||||
|
||||
const message = {id: "foo", targeting: "hasFxAccount"};
|
||||
is(await ASRouterTargeting.findMatchingMessage([message], {}), message,
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
|
||||
"should select correct item by hasFxAccount");
|
||||
});
|
||||
|
||||
@ -89,11 +93,11 @@ add_task(async function checksearchEngines() {
|
||||
"searchEngines.current should be the current engine name");
|
||||
|
||||
const message = {id: "foo", targeting: `searchEngines[.current == ${Services.search.currentEngine.identifier}]`};
|
||||
is(await ASRouterTargeting.findMatchingMessage([message], {}), message,
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
|
||||
"should select correct item by searchEngines.current");
|
||||
|
||||
const message2 = {id: "foo", targeting: `searchEngines[${Services.search.getVisibleEngines()[0].identifier} in .installed]`};
|
||||
is(await ASRouterTargeting.findMatchingMessage([message2], {}), message2,
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message2], target: {}}), message2,
|
||||
"should select correct item by searchEngines.installed");
|
||||
});
|
||||
|
||||
@ -105,7 +109,7 @@ add_task(async function checkisDefaultBrowser() {
|
||||
is(result, expected,
|
||||
"isDefaultBrowser should be equal to ShellService.isDefaultBrowser()");
|
||||
const message = {id: "foo", targeting: `isDefaultBrowser == ${expected.toString()}`};
|
||||
is(await ASRouterTargeting.findMatchingMessage([message], {}), message,
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
|
||||
"should select correct item by isDefaultBrowser");
|
||||
});
|
||||
|
||||
@ -114,7 +118,7 @@ add_task(async function checkdevToolsOpenedCount() {
|
||||
is(ASRouterTargeting.Environment.devToolsOpenedCount, 5,
|
||||
"devToolsOpenedCount should be equal to devtools.selfxss.count pref value");
|
||||
const message = {id: "foo", targeting: "devToolsOpenedCount >= 5"};
|
||||
is(await ASRouterTargeting.findMatchingMessage([message], {}), message,
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
|
||||
"should select correct item by devToolsOpenedCount");
|
||||
});
|
||||
|
||||
@ -173,6 +177,53 @@ add_task(async function checkAddonsInfo() {
|
||||
"should correctly provide `userDisabled` property from full data");
|
||||
|
||||
ok(Object.prototype.hasOwnProperty.call(testAddon, "installDate") &&
|
||||
(Math.abs(new Date() - new Date(testAddon.installDate)) < 60 * 1000),
|
||||
(Math.abs(Date.now() - new Date(testAddon.installDate)) < 60 * 1000),
|
||||
"should correctly provide `installDate` property from full data");
|
||||
});
|
||||
|
||||
add_task(async function checkFrecentSites() {
|
||||
const now = Date.now();
|
||||
const timeDaysAgo = numDays => now - numDays * 24 * 60 * 60 * 1000;
|
||||
|
||||
const visits = [];
|
||||
for (const [uri, count, visitDate] of [
|
||||
["https://mozilla1.com/", 10, timeDaysAgo(0)], // frecency 1000
|
||||
["https://mozilla2.com/", 5, timeDaysAgo(1)], // frecency 500
|
||||
["https://mozilla3.com/", 1, timeDaysAgo(2)] // frecency 100
|
||||
]) {
|
||||
[...Array(count).keys()].forEach(() => visits.push({
|
||||
uri,
|
||||
visitDate: visitDate * 1000 // Places expects microseconds
|
||||
}));
|
||||
}
|
||||
|
||||
await PlacesTestUtils.addVisits(visits);
|
||||
|
||||
let message = {id: "foo", targeting: "'mozilla3.com' in topFrecentSites|mapToProperty('host')"};
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
|
||||
"should select correct item by host in topFrecentSites");
|
||||
|
||||
message = {id: "foo", targeting: "'non-existent.com' in topFrecentSites|mapToProperty('host')"};
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), undefined,
|
||||
"should not select incorrect item by host in topFrecentSites");
|
||||
|
||||
message = {id: "foo", targeting: "'mozilla2.com' in topFrecentSites[.frecency >= 400]|mapToProperty('host')"};
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
|
||||
"should select correct item when filtering by frecency");
|
||||
|
||||
message = {id: "foo", targeting: "'mozilla2.com' in topFrecentSites[.frecency >= 600]|mapToProperty('host')"};
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), undefined,
|
||||
"should not select incorrect item when filtering by frecency");
|
||||
|
||||
message = {id: "foo", targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${timeDaysAgo(1) - 1}]|mapToProperty('host')`};
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
|
||||
"should select correct item when filtering by lastVisitDate");
|
||||
|
||||
message = {id: "foo", targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${timeDaysAgo(0) - 1}]|mapToProperty('host')`};
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), undefined,
|
||||
"should not select incorrect item when filtering by lastVisitDate");
|
||||
|
||||
message = {id: "foo", targeting: `(topFrecentSites[.frecency >= 900 && .lastVisitDate >= ${timeDaysAgo(1) - 1}]|mapToProperty('host') intersect ['mozilla3.com', 'mozilla2.com', 'mozilla1.com'])|length > 0`};
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
|
||||
"should select correct item when filtering by frecency and lastVisitDate with multiple candidate domains");
|
||||
});
|
||||
|
@ -12,6 +12,8 @@ import {_ASRouter} from "lib/ASRouter.jsm";
|
||||
const FAKE_PROVIDERS = [FAKE_LOCAL_PROVIDER, FAKE_REMOTE_PROVIDER];
|
||||
const ALL_MESSAGE_IDS = [...FAKE_LOCAL_MESSAGES, ...FAKE_REMOTE_MESSAGES].map(message => message.id);
|
||||
const FAKE_BUNDLE = [FAKE_LOCAL_MESSAGES[1], FAKE_LOCAL_MESSAGES[2]];
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
// Creates a message object that looks like messages returned by
|
||||
// RemotePageManager listeners
|
||||
function fakeAsyncMessage(action) {
|
||||
@ -23,14 +25,18 @@ describe("ASRouter", () => {
|
||||
let channel;
|
||||
let sandbox;
|
||||
let blockList;
|
||||
let impressions;
|
||||
let fetchStub;
|
||||
let clock;
|
||||
let getStringPrefStub;
|
||||
let addObserverStub;
|
||||
|
||||
function createFakeStorage() {
|
||||
const getStub = sandbox.stub();
|
||||
getStub.withArgs("blockList").returns(Promise.resolve(blockList));
|
||||
getStub.withArgs("impressions").returns(Promise.resolve(impressions));
|
||||
return {
|
||||
get: sandbox.stub().returns(Promise.resolve(blockList)),
|
||||
get: getStub,
|
||||
set: sandbox.stub().returns(Promise.resolve())
|
||||
};
|
||||
}
|
||||
@ -43,6 +49,7 @@ describe("ASRouter", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
blockList = [];
|
||||
impressions = {};
|
||||
sandbox = sinon.sandbox.create();
|
||||
clock = sandbox.useFakeTimers();
|
||||
fetchStub = sandbox.stub(global, "fetch")
|
||||
@ -77,12 +84,22 @@ describe("ASRouter", () => {
|
||||
assert.calledWith(addObserverStub, "remotePref");
|
||||
});
|
||||
it("should set state.blockList to the block list in persistent storage", async () => {
|
||||
blockList = ["MESSAGE_ID"];
|
||||
|
||||
blockList = ["foo"];
|
||||
Router = new _ASRouter({providers: FAKE_PROVIDERS});
|
||||
await Router.init(channel, createFakeStorage());
|
||||
|
||||
assert.deepEqual(Router.state.blockList, ["MESSAGE_ID"]);
|
||||
assert.deepEqual(Router.state.blockList, ["foo"]);
|
||||
});
|
||||
it("should set state.impressions to the impressions object in persistent storage", async () => {
|
||||
// Note that impressions are only kept if a message exists in router and has a .frequency property,
|
||||
// otherwise they will be cleaned up by .cleanupImpressions()
|
||||
const testMessage = {id: "foo", frequency: {lifetimeCap: 10}};
|
||||
impressions = {foo: [0, 1, 2]};
|
||||
|
||||
Router = new _ASRouter({providers: [{id: "onboarding", type: "local", messages: [testMessage]}]});
|
||||
await Router.init(channel, createFakeStorage());
|
||||
|
||||
assert.deepEqual(Router.state.impressions, impressions);
|
||||
});
|
||||
it("should await .loadMessagesFromAllProviders() and add messages from providers to state.messages", async () => {
|
||||
Router = new _ASRouter({providers: FAKE_PROVIDERS});
|
||||
@ -310,7 +327,6 @@ describe("ASRouter", () => {
|
||||
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[0].id));
|
||||
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[1].id));
|
||||
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
|
||||
assert.calledOnce(Router._storage.set);
|
||||
assert.calledWithExactly(Router._storage.set, "blockList", bundleIds);
|
||||
});
|
||||
});
|
||||
@ -326,7 +342,6 @@ describe("ASRouter", () => {
|
||||
it("should save the blockList", async () => {
|
||||
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
|
||||
|
||||
assert.calledOnce(Router._storage.set);
|
||||
assert.calledWithExactly(Router._storage.set, "blockList", []);
|
||||
});
|
||||
});
|
||||
@ -344,7 +359,6 @@ describe("ASRouter", () => {
|
||||
it("should save the blockList", async () => {
|
||||
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
|
||||
|
||||
assert.calledOnce(Router._storage.set);
|
||||
assert.calledWithExactly(Router._storage.set, "blockList", []);
|
||||
});
|
||||
});
|
||||
@ -521,4 +535,98 @@ describe("ASRouter", () => {
|
||||
assert.calledTwice(Cu.reportError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("impressions", () => {
|
||||
it("should add an impression and update _storage with the current time if the message frequency caps", async () => {
|
||||
clock.tick(42);
|
||||
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo", frequency: {lifetime: 5}}});
|
||||
await Router.onMessage(msg);
|
||||
|
||||
assert.isArray(Router.state.impressions.foo);
|
||||
assert.deepEqual(Router.state.impressions.foo, [42]);
|
||||
assert.calledWith(Router._storage.set, "impressions", {foo: [42]});
|
||||
});
|
||||
it("should not add an impression if the message doesn't have frequency caps", async () => {
|
||||
// Note that storage.set is called during initialization, so it needs to be reset
|
||||
Router._storage.set.reset();
|
||||
clock.tick(42);
|
||||
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo"}});
|
||||
await Router.onMessage(msg);
|
||||
|
||||
assert.notProperty(Router.state.impressions, "foo");
|
||||
assert.notCalled(Router._storage.set);
|
||||
});
|
||||
describe("getLongestPeriod", () => {
|
||||
it("should return the period if there is only one definition", () => {
|
||||
const message = {id: "foo", frequency: {custom: [{period: 200, cap: 2}]}};
|
||||
assert.equal(Router.getLongestPeriod(message), 200);
|
||||
});
|
||||
it("should return the longest period if there are more than one definitions", () => {
|
||||
const message = {id: "foo", frequency: {custom: [{period: 1000, cap: 3}, {period: ONE_DAY, cap: 5}, {period: 100, cap: 2}]}};
|
||||
assert.equal(Router.getLongestPeriod(message), ONE_DAY);
|
||||
});
|
||||
it("should return null if there are is no .frequency", () => {
|
||||
const message = {id: "foo"};
|
||||
assert.isNull(Router.getLongestPeriod(message));
|
||||
});
|
||||
it("should return null if there are is no .frequency.custom", () => {
|
||||
const message = {id: "foo", frequency: {lifetime: 10}};
|
||||
assert.isNull(Router.getLongestPeriod(message));
|
||||
});
|
||||
});
|
||||
describe("cleanup on init", () => {
|
||||
it("should clear impressions for messages which do not exist in state.messages", async () => {
|
||||
const messages = [{id: "foo", frequency: {lifetime: 10}}];
|
||||
impressions = {foo: [0], bar: [0, 1]};
|
||||
// Impressions for "bar" should be removed since that id does not exist in messages
|
||||
const result = {foo: [0]};
|
||||
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
|
||||
assert.calledWith(Router._storage.set, "impressions", result);
|
||||
assert.deepEqual(Router.state.impressions, result);
|
||||
});
|
||||
it("should clear impressions older than the period if no lifetime impression cap is included", async () => {
|
||||
const CURRENT_TIME = ONE_DAY * 2;
|
||||
clock.tick(CURRENT_TIME);
|
||||
const messages = [{id: "foo", frequency: {custom: [{period: ONE_DAY, cap: 5}]}}];
|
||||
impressions = {foo: [0, 1, CURRENT_TIME - 10]};
|
||||
// Only 0 and 1 are more than 24 hours before CURRENT_TIME
|
||||
const result = {foo: [CURRENT_TIME - 10]};
|
||||
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
|
||||
assert.calledWith(Router._storage.set, "impressions", result);
|
||||
assert.deepEqual(Router.state.impressions, result);
|
||||
});
|
||||
it("should clear impressions older than the longest period if no lifetime impression cap is included", async () => {
|
||||
const CURRENT_TIME = ONE_DAY * 2;
|
||||
clock.tick(CURRENT_TIME);
|
||||
const messages = [{id: "foo", frequency: {custom: [{period: ONE_DAY, cap: 5}, {period: 100, cap: 2}]}}];
|
||||
impressions = {foo: [0, 1, CURRENT_TIME - 10]};
|
||||
// Only 0 and 1 are more than 24 hours before CURRENT_TIME
|
||||
const result = {foo: [CURRENT_TIME - 10]};
|
||||
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
|
||||
assert.calledWith(Router._storage.set, "impressions", result);
|
||||
assert.deepEqual(Router.state.impressions, result);
|
||||
});
|
||||
it("should clear impressions if they are not properly formatted", async () => {
|
||||
const messages = [{id: "foo", frequency: {lifetime: 10}}];
|
||||
// this is impromperly formatted since impressions are supposed to be an array
|
||||
impressions = {foo: 0};
|
||||
const result = {};
|
||||
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
|
||||
assert.calledWith(Router._storage.set, "impressions", result);
|
||||
assert.deepEqual(Router.state.impressions, result);
|
||||
});
|
||||
it("should not clear impressions for messages which do exist in state.messages", async () => {
|
||||
const messages = [{id: "foo", frequency: {lifetime: 10}}, {id: "bar", frequency: {lifetime: 10}}];
|
||||
impressions = {foo: [0], bar: []};
|
||||
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
|
||||
assert.notCalled(Router._storage.set);
|
||||
assert.deepEqual(Router.state.impressions, impressions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,100 @@
|
||||
import {ASRouterTargeting} from "lib/ASRouterTargeting.jsm";
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
// Note that tests for the ASRouterTargeting environment can be found in
|
||||
// test/functional/mochitest/browser_asrouter_targeting.js
|
||||
|
||||
describe("ASRouterTargeting#isBelowFrequencyCap", () => {
|
||||
describe("lifetime frequency caps", () => {
|
||||
it("should return true if .frequency is not defined on the message", () => {
|
||||
const message = {id: "msg1"};
|
||||
const impressions = [0, 1];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return true if there are no impressions", () => {
|
||||
const message = {id: "msg1", frequency: {lifetime: 10, custom: [{period: ONE_DAY, cap: 2}]}};
|
||||
const impressions = [];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return true if the # of impressions is less than .frequency.lifetime", () => {
|
||||
const message = {id: "msg1", frequency: {lifetime: 3}};
|
||||
const impressions = [0, 1];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return false if the # of impressions is equal to .frequency.lifetime", () => {
|
||||
const message = {id: "msg1", frequency: {lifetime: 2}};
|
||||
const impressions = [0, 1];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
it("should return false if the # of impressions is greater than .frequency.lifetime", () => {
|
||||
const message = {id: "msg1", frequency: {lifetime: 2}};
|
||||
const impressions = [0, 1, 2];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
});
|
||||
describe("custom frequency caps", () => {
|
||||
let sandbox;
|
||||
let clock;
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
clock = sandbox.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
it("should return true if impressions in the time period < the cap and total impressions < the lifetime cap", () => {
|
||||
clock.tick(ONE_DAY + 10);
|
||||
const message = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}], lifetime: 3}};
|
||||
const impressions = [0, ONE_DAY + 1];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return false if impressions in the time period > the cap and total impressions < the lifetime cap", () => {
|
||||
clock.tick(200);
|
||||
const message = {id: "msg1", frequency: {custom: [{period: 100, cap: 2}], lifetime: 3}};
|
||||
const impressions = [0, 160, 161];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
it("should return false if impressions in one of the time periods > the cap and total impressions < the lifetime cap", () => {
|
||||
clock.tick(ONE_DAY + 200);
|
||||
const messageTrue = {id: "msg2", frequency: {custom: [{period: 100, cap: 2}]}};
|
||||
const messageFalse = {id: "msg1", frequency: {custom: [{period: 100, cap: 2}, {period: ONE_DAY, cap: 3}]}};
|
||||
const impressions = [0, ONE_DAY + 160, ONE_DAY - 100, ONE_DAY - 200];
|
||||
assert.isTrue(ASRouterTargeting.isBelowFrequencyCap(messageTrue, impressions));
|
||||
assert.isFalse(ASRouterTargeting.isBelowFrequencyCap(messageFalse, impressions));
|
||||
});
|
||||
it("should return false if impressions in the time period < the cap and total impressions > the lifetime cap", () => {
|
||||
clock.tick(ONE_DAY + 10);
|
||||
const message = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}], lifetime: 3}};
|
||||
const impressions = [0, 1, 2, 3, ONE_DAY + 1];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
it("should return true if daily impressions < the daily cap and there is no lifetime cap", () => {
|
||||
clock.tick(ONE_DAY + 10);
|
||||
const message = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}]}};
|
||||
const impressions = [0, 1, 2, 3, ONE_DAY + 1];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return false if daily impressions > the daily cap and there is no lifetime cap", () => {
|
||||
clock.tick(ONE_DAY + 10);
|
||||
const message = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}]}};
|
||||
const impressions = [0, 1, 2, 3, ONE_DAY + 1, ONE_DAY + 2, ONE_DAY + 3];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
it("should allow the 'daily' alias for period", () => {
|
||||
clock.tick(ONE_DAY + 10);
|
||||
const message = {id: "msg1", frequency: {custom: [{period: "daily", cap: 2}]}};
|
||||
assert.isFalse(ASRouterTargeting.isBelowFrequencyCap(message, [0, 1, 2, 3, ONE_DAY + 1, ONE_DAY + 2, ONE_DAY + 3]));
|
||||
assert.isTrue(ASRouterTargeting.isBelowFrequencyCap(message, [0, 1, 2, 3, ONE_DAY + 1]));
|
||||
});
|
||||
});
|
||||
});
|
@ -33,19 +33,6 @@ describe("FaviconFeed", () => {
|
||||
siteIconsPref = true;
|
||||
sandbox.stub(global.Services.prefs, "getBoolPref")
|
||||
.withArgs("browser.chrome.site_icons").callsFake(() => siteIconsPref);
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
globals.set("fetch", fetchStub);
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve([{
|
||||
"domains": ["facebook.com"],
|
||||
"image_url": "https://www.facebook.com/icon.png"
|
||||
}, {
|
||||
"domains": ["gmail.com", "mail.google.com"],
|
||||
"image_url": "https://iconserver.com/gmail.png"
|
||||
}])
|
||||
});
|
||||
|
||||
feed = new FaviconFeed();
|
||||
feed.store = {
|
||||
@ -63,151 +50,13 @@ describe("FaviconFeed", () => {
|
||||
assert.instanceOf(feed, FaviconFeed);
|
||||
});
|
||||
|
||||
describe("#getSitesByDomain", () => {
|
||||
it("should loadCachedData and maybeRefresh if _sitesByDomain isn't set", async () => {
|
||||
feed.loadCachedData = sinon.spy(() => ([]));
|
||||
feed.maybeRefresh = sinon.spy(() => {
|
||||
feed._sitesByDomain = {"mozilla.org": {"image_url": "https://mozilla.org/icon.png"}};
|
||||
return [];
|
||||
});
|
||||
await feed.getSitesByDomain();
|
||||
assert.calledOnce(feed.loadCachedData);
|
||||
assert.calledOnce(feed.maybeRefresh);
|
||||
});
|
||||
it("should NOT loadCachedData and maybeRefresh if _sitesByDomain is already set", async () => {
|
||||
feed._sitesByDomain = {"mozilla.org": {"image_url": "https://mozilla.org/icon.png"}};
|
||||
feed.loadCachedData = sinon.spy(() => ([]));
|
||||
feed.maybeRefresh = sinon.spy(() => ([]));
|
||||
await feed.getSitesByDomain();
|
||||
assert.notCalled(feed.loadCachedData);
|
||||
assert.notCalled(feed.maybeRefresh);
|
||||
});
|
||||
it("should resolve to empty object if there is no cache and fetch fails", async () => {
|
||||
feed.loadCachedData = sinon.spy(() => ([]));
|
||||
feed.maybeRefresh = sinon.spy(() => ([]));
|
||||
await feed.getSitesByDomain();
|
||||
assert.deepEqual(feed._sitesByDomain, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#loadCachedData", () => {
|
||||
it("should set _sitesByDomain if there is cached data", async () => {
|
||||
const cachedData = {
|
||||
"mozilla.org": {"image_url": "https://mozilla.org/icon.png"},
|
||||
"_timestamp": Date.now(),
|
||||
"_etag": "foobaretag1234567890"
|
||||
};
|
||||
feed.cache.get = () => cachedData;
|
||||
await feed.loadCachedData();
|
||||
assert.deepEqual(feed._sitesByDomain, cachedData);
|
||||
assert.equal(feed.tippyTopNextUpdate, cachedData._timestamp + 24 * 60 * 60 * 1000);
|
||||
assert.equal(feed._sitesByDomain._etag, cachedData._etag);
|
||||
});
|
||||
it("should NOT set _sitesByDomain if there is no cached data", async () => {
|
||||
feed.cache.get = () => ({});
|
||||
await feed.loadCachedData();
|
||||
assert.isNull(feed._sitesByDomain);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#maybeRefresh", () => {
|
||||
it("should refresh if next update is due", () => {
|
||||
feed.refresh = sinon.spy();
|
||||
feed.tippyTopNextUpdate = Date.now();
|
||||
feed.maybeRefresh();
|
||||
assert.calledOnce(feed.refresh);
|
||||
});
|
||||
it("should NOT refresh if next update is in future", () => {
|
||||
feed.refresh = sinon.spy();
|
||||
feed.tippyTopNextUpdate = Date.now() + 6000;
|
||||
feed.maybeRefresh();
|
||||
assert.notCalled(feed.refresh);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#refresh", () => {
|
||||
it("should loadFromURL with the right URL from prefs", async () => {
|
||||
feed.loadFromURL = sinon.spy(() => ({data: []}));
|
||||
await feed.refresh();
|
||||
assert.calledOnce(feed.loadFromURL);
|
||||
assert.calledWith(feed.loadFromURL, FAKE_ENDPOINT);
|
||||
});
|
||||
it("should set _sitesByDomain if new sites are returned from loadFromURL", async () => {
|
||||
const data = {
|
||||
data: [
|
||||
{"domains": ["mozilla.org"], "image_url": "https://mozilla.org/icon.png"},
|
||||
{"domains": ["facebook.com"], "image_url": "https://facebook.com/icon.png"}
|
||||
],
|
||||
etag: "etag1234567890",
|
||||
status: 200
|
||||
};
|
||||
const expectedData = {
|
||||
"facebook.com": {"image_url": "https://facebook.com/icon.png"},
|
||||
"mozilla.org": {"image_url": "https://mozilla.org/icon.png"},
|
||||
"_etag": "etag1234567890",
|
||||
"_timestamp": Date.now()
|
||||
};
|
||||
feed.loadFromURL = sinon.spy(url => data);
|
||||
feed.cache.set = sinon.spy();
|
||||
await feed.refresh();
|
||||
assert.equal(feed._sitesByDomain._etag, data.etag);
|
||||
assert.deepEqual(feed._sitesByDomain, expectedData);
|
||||
assert.calledOnce(feed.cache.set);
|
||||
assert.calledWith(feed.cache.set, "sites", expectedData);
|
||||
});
|
||||
it("should pass If-None-Match if we have a last known etag", async () => {
|
||||
feed.loadFromURL = sinon.spy(url => ({data: [], status: 304}));
|
||||
feed._sitesByDomain = {};
|
||||
feed._sitesByDomain._etag = "etag1234567890";
|
||||
await feed.refresh();
|
||||
const [, headers] = feed.loadFromURL.getCall(0).args;
|
||||
assert.equal(headers.get("If-None-Match"), feed._sitesByDomain._etag);
|
||||
});
|
||||
it("should not set _sitesByDomain if the remote manifest is not modified since last fetch", async () => {
|
||||
const data = {"mozilla.org": {"image_url": "https://mozilla.org/icon.png"}};
|
||||
feed._sitesByDomain = data;
|
||||
feed._sitesByDomain._timestamp = Date.now() - 1000;
|
||||
feed.loadFromURL = sinon.spy(url => ({data: [], status: 304}));
|
||||
feed.cache.set = sinon.spy();
|
||||
await feed.refresh();
|
||||
assert.deepEqual(feed._sitesByDomain, data);
|
||||
assert.calledOnce(feed.cache.set);
|
||||
assert.calledWith(feed.cache.set, "sites", Object.assign({_timestamp: Date.now()}, data));
|
||||
});
|
||||
it("should handle server errors by retrying with exponential backoff", async () => {
|
||||
const expectedDelay = 5 * 60 * 1000;
|
||||
feed.loadFromURL = sinon.spy(url => ({data: [], status: 500}));
|
||||
await feed.refresh();
|
||||
assert.equal(1, feed.numRetries);
|
||||
assert.equal(expectedDelay, feed.tippyTopNextUpdate);
|
||||
await feed.refresh();
|
||||
assert.equal(2, feed.numRetries);
|
||||
assert.equal(2 * expectedDelay, feed.tippyTopNextUpdate);
|
||||
await feed.refresh();
|
||||
assert.equal(3, feed.numRetries);
|
||||
assert.equal(2 * 2 * expectedDelay, feed.tippyTopNextUpdate);
|
||||
await feed.refresh();
|
||||
assert.equal(4, feed.numRetries);
|
||||
assert.equal(2 * 2 * 2 * expectedDelay, feed.tippyTopNextUpdate);
|
||||
// Verify the delay maxes out at 24 hours.
|
||||
feed.numRetries = 100;
|
||||
await feed.refresh();
|
||||
assert.equal(24 * 60 * 60 * 1000, feed.tippyTopNextUpdate);
|
||||
// Verify the numRetries gets reset on a successful fetch.
|
||||
feed.loadFromURL = sinon.spy(url => ({data: [], status: 200}));
|
||||
await feed.refresh();
|
||||
assert.equal(0, feed.numRetries);
|
||||
assert.equal(24 * 60 * 60 * 1000, feed.tippyTopNextUpdate);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#fetchIcon", () => {
|
||||
let domain;
|
||||
let url;
|
||||
beforeEach(() => {
|
||||
domain = "mozilla.org";
|
||||
url = `https://${domain}/`;
|
||||
feed._sitesByDomain = {[domain]: {url, image_url: `${url}/icon.png`}};
|
||||
feed.getSite = sandbox.stub().returns(Promise.resolve({domain, image_url: `${url}/icon.png`}));
|
||||
feed._queryForRedirects.clear();
|
||||
});
|
||||
|
||||
@ -230,19 +79,14 @@ describe("FaviconFeed", () => {
|
||||
|
||||
assert.notCalled(global.PlacesUtils.favicons.setAndFetchFaviconForPage);
|
||||
});
|
||||
it("should NOT setAndFetchFaviconForPage if the endpoint is empty", async () => {
|
||||
feed.store.state.Prefs.values["tippyTop.service.endpoint"] = "";
|
||||
|
||||
await feed.fetchIcon(url);
|
||||
|
||||
assert.notCalled(global.PlacesUtils.favicons.setAndFetchFaviconForPage);
|
||||
});
|
||||
it("should NOT setAndFetchFaviconForPage if the url is NOT in the TippyTop data", async () => {
|
||||
feed.getSite = sandbox.stub().returns(Promise.resolve(null));
|
||||
await feed.fetchIcon("https://example.com");
|
||||
|
||||
assert.notCalled(global.PlacesUtils.favicons.setAndFetchFaviconForPage);
|
||||
});
|
||||
it("should issue a fetchIconFromRedirects if the url is NOT in the TippyTop data", async () => {
|
||||
feed.getSite = sandbox.stub().returns(Promise.resolve(null));
|
||||
sandbox.spy(global.Services.tm, "idleDispatchToMainThread");
|
||||
|
||||
await feed.fetchIcon("https://example.com");
|
||||
@ -250,6 +94,7 @@ describe("FaviconFeed", () => {
|
||||
assert.calledOnce(global.Services.tm.idleDispatchToMainThread);
|
||||
});
|
||||
it("should only issue fetchIconFromRedirects once on the same url", async () => {
|
||||
feed.getSite = sandbox.stub().returns(Promise.resolve(null));
|
||||
sandbox.spy(global.Services.tm, "idleDispatchToMainThread");
|
||||
|
||||
await feed.fetchIcon("https://example.com");
|
||||
@ -258,6 +103,7 @@ describe("FaviconFeed", () => {
|
||||
assert.calledOnce(global.Services.tm.idleDispatchToMainThread);
|
||||
});
|
||||
it("should issue fetchIconFromRedirects twice on two different urls", async () => {
|
||||
feed.getSite = sandbox.stub().returns(Promise.resolve(null));
|
||||
sandbox.spy(global.Services.tm, "idleDispatchToMainThread");
|
||||
|
||||
await feed.fetchIcon("https://example.com");
|
||||
@ -265,28 +111,29 @@ describe("FaviconFeed", () => {
|
||||
|
||||
assert.calledTwice(global.Services.tm.idleDispatchToMainThread);
|
||||
});
|
||||
it("should cause sites to initialize with fetched sites if no sites", async () => {
|
||||
delete feed._sitesByDomain;
|
||||
});
|
||||
|
||||
await feed.fetchIcon(url);
|
||||
|
||||
assert.containsAllKeys(feed._sitesByDomain, ["facebook.com", "gmail.com", "mail.google.com"]);
|
||||
describe("#getSite", () => {
|
||||
it("should return site data if RemoteSettings has an entry for the domain", async () => {
|
||||
const get = () => Promise.resolve([{domain: "example.com", image_url: "foo.img"}]);
|
||||
feed._tippyTop = {get};
|
||||
const site = await feed.getSite("example.com");
|
||||
assert.equal(site.domain, "example.com");
|
||||
});
|
||||
it("should return null if RemoteSettings doesn't have an entry for the domain", async () => {
|
||||
const get = () => Promise.resolve([]);
|
||||
feed._tippyTop = {get};
|
||||
const site = await feed.getSite("example.com");
|
||||
assert.isNull(site);
|
||||
});
|
||||
it("should lazy init _tippyTop", async () => {
|
||||
assert.isUndefined(feed._tippyTop);
|
||||
await feed.getSite("example.com");
|
||||
assert.ok(feed._tippyTop);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#onAction", () => {
|
||||
it("should maybeRefresh on SYSTEM_TICK if initialized", async () => {
|
||||
feed._sitesByDomain = {"mozilla.org": {}};
|
||||
feed.maybeRefresh = sinon.spy();
|
||||
feed.onAction({type: at.SYSTEM_TICK});
|
||||
assert.calledOnce(feed.maybeRefresh);
|
||||
});
|
||||
it("should NOT maybeRefresh on SYSTEM_TICK if NOT initialized", async () => {
|
||||
feed._sitesByDomain = null;
|
||||
feed.maybeRefresh = sinon.spy();
|
||||
feed.onAction({type: at.SYSTEM_TICK});
|
||||
assert.notCalled(feed.maybeRefresh);
|
||||
});
|
||||
it("should fetchIcon on RICH_ICON_MISSING", async () => {
|
||||
feed.fetchIcon = sinon.spy();
|
||||
const url = "https://mozilla.org";
|
||||
|
@ -45,7 +45,12 @@ const TEST_GLOBAL = {
|
||||
ChromeUtils: {
|
||||
defineModuleGetter() {},
|
||||
generateQI() { return {}; },
|
||||
import() {}
|
||||
import(str) {
|
||||
if (str === "resource://services-settings/remote-settings.js") {
|
||||
return {RemoteSettings: TEST_GLOBAL.RemoteSettings};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
},
|
||||
Components: {isSuccessCode: () => true},
|
||||
// eslint-disable-next-line object-shorthand
|
||||
@ -86,6 +91,7 @@ const TEST_GLOBAL = {
|
||||
fetch() {},
|
||||
// eslint-disable-next-line object-shorthand
|
||||
Image: function() {}, // NB: This is a function/constructor
|
||||
NewTabUtils: {activityStreamProvider: {getTopFrecentSites: () => []}},
|
||||
PlacesUtils: {
|
||||
get bookmarks() {
|
||||
return TEST_GLOBAL.Cc["@mozilla.org/browser/nav-bookmarks-service;1"];
|
||||
@ -184,7 +190,8 @@ const TEST_GLOBAL = {
|
||||
},
|
||||
EventEmitter,
|
||||
ShellService: {isDefaultBrowser: () => true},
|
||||
FilterExpressions: {eval() { return Promise.resolve(true); }}
|
||||
FilterExpressions: {eval() { return Promise.resolve(true); }},
|
||||
RemoteSettings() { return {get() { return Promise.resolve([]); }}; }
|
||||
};
|
||||
overrider.set(TEST_GLOBAL);
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
scripts:
|
||||
# Run the activity-stream mochitests
|
||||
mochitest: (cd $npm_package_config_mc_dir && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest )
|
||||
mochitest: (cd $npm_package_config_mc_dir && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest --headless)
|
||||
|
||||
# Run the activity-stream mochitests with the browser toolbox debugger.
|
||||
# Often handy in combination with adding a "debugger" statement in your
|
||||
|
Loading…
Reference in New Issue
Block a user