Cordcutter gift guide: Best TV streaming boxes for Canada

[]

-1&&window.location.href.indexOf(“cem1809”)>-1){var variant=localStorage.getItem(“Dw__header-cem1809|global”);var regwallbtn=document.getElementsByClassName(“js-newsletter-wall-register”);var registerbtn=document.getElementsByClassName(“js-onclick–register”);if(registerbtn)for(i=0;i

-1;var isSection=window.tgam.meta.isSectionPage===true;var isHermes=tgam.datalayer.hermesData;var meterCount=localStorage.getItem(“tgam.keytar.wall.subscriber.free”);var isArticle=window.location.href.indexOf(“/article”)>-1;var isPaywallArticle=isArticle?isHermes?getSegment(“hermesData.wall”,”redwall”):getSegment(“article.paywallStatus”, “red”)||getSegment(“article.paywallStatus”,”yellow”)&&meterCount!==null&&JSON.parse(meterCount).split(/[(,*?)]/).length===6:false;var isRegwallArticle=isArticle?isHermes?getSegment(“hermesData.wall”,”regwall”):getSegment(“article.paywallStatus”,”red”)||getSegment(“article.paywallStatus”,”yellow”)&&meterCount!==null&&JSON.parse(meterCount).split(/[(,*?)]/).length===3:false;var isNotAdv=isArticle?getSegment(“article.advContent”,false):true;var count=count||1;if(darwinState.hasOwnProperty(“cem1806”)&& count==1&&darwinState.cem1806.variant){count+=1;setDarwinVariant(“pencil-cem1806”,darwinState.cem1806.variant)}console.log(“cem-1806 segment start: “,isNotSubscriber&&notIE&&isNotInteractive&&isNotPb&&isNotProduct&&(isQA||(isSection||!isPaywallArticle&&!isRegwallArticle))&&isNotAdv);return isNotSubscriber&&notIE&&isNotInteractive&&isNotPb&&isNotProduct&&(isQA||(isSection||!isPaywallArticle&&!isRegwallArticle))&&isNotAdv})}])}window.tgam.darwin.tests.segments.push(dwSegment_pencilcem1806);function dwExecute_pencilcem1806(data){window._dw.push([“global”, “ready”,function onReady(api){var variant=api.get(“pencil-cem1806”);var variantData=data;if(variant.is(“control”)){console.info(“pencils:cem-1806:control”);setUnitState(“cem1806″,”control”);updateHolidayPencil(“control”)}if(variant.is(“experience”)){console.info(“pencils:cem-1806:experience”);setUnitState(“cem1806″,”experience”);if(/CEMQA1806control/.test(window.location.search))updateHolidayPencil(“control”);else updateHolidayPencil(“experience”)}}])}window.tgam.darwin.tests.before.push(dwExecute_pencilcem1806)})();]]> 0||document.documentElement.scrollTop>0){if(document.getElementById(“fixedpencil”).classList.contains(“collapse”)&&expand_subs){expand_subs=false;expandsubs()}}else if(document.getElementById(“fixedpencil”).classList.contains(“expand”)){expand_subs=true;collapsesubs()}}); function expandsubs(){document.getElementById(“fixedpencil”).classList.remove(“collapse”);document.getElementById(“fixedpencil”).className+=” expand”}function collapsesubs(){document.getElementById(“fixedpencil”).classList.remove(“expand”);document.getElementById(“fixedpencil”).className+=” collapse”};]]>

-1; const isNotInteractive = window.location.href.indexOf(‘/article’) > -1 ? (!getSegment(‘article.pagesubtype’, ‘interactive’) && !getSegment(‘article.pagesubtype’, ‘longread’)) : true; const isDesktop = document.documentElement.clientWidth>=768; var count = count || 1; if (darwinState.hasOwnProperty(‘cem1666’) && count === 1 && darwinState.cem1666.variant) { count += 1; setDarwinVariant(‘products-cem1666’, darwinState.cem1666.variant); } return (isQA || isNotSubscriber) && isNotPb && isNotInteractive && isDesktop; }); }]); } window.tgam.darwin.tests.segments.push(dwSegment_productscem1666); function dwExecute_productscem1666(data) { window._dw.push([‘global’, ‘ready’, function onReady(api) { var variant = api.get(“products-cem1666”); var variantData = data; if (variant.is(‘control’)) { console.info(‘products:cem-1666:control’); setUnitState(“cem1666”, “control”); } if (variant.is(‘experience’)) { console.info(‘products:cem-1666:experience’); setUnitState(“cem1666”, “experience”); updateSubscribeBtn(); } }]); } window.tgam.darwin.tests.before.push(dwExecute_productscem1666); })(); ]]>

7&&/arc6735group=/.test(window.location.search))tenure=1;var firstVisits=tenure?tenure

tenMinutes); var showPublishedDateAsMoment = publishedLessThanThreeDaysAgo; var showUpdatedDateAsMoment = !isSectionPageContext && storyHasBeenUpdatedMoreThanTenMinutesSincePublication && updatedWasLessThanThreeDaysAgo; var field = “published”; // Which fields to show (published, updated, or both) var moment = null; // Which fields to show in relative date (i.e., moment) format if (storyHasBeenUpdatedMoreThanTenMinutesSincePublication) { if (publishedAndUpdatedAreTheSameDay && timeSinceUpdate < oneDay) { field = "both"; // Don't show updated date since it's the same as published date. } } if (showUpdatedDateAsMoment) { moment = "updated"; if (showPublishedDateAsMoment) { moment = "both"; } } else if (showPublishedDateAsMoment) { moment = "published"; } return { published: formattedPubDate, updated: formattedUpDate, field: field, moment: moment }; } // Show "time ago" timestamp function humanTime(left, right) { var amount = parseInt(right - left).toFixed(0); var ONE_MINUTE = 60; var ONE_HOUR = 3600; var ONE_DAY = 86400; var THREE_DAYS = 259200; var plural; if (amount < ONE_MINUTE) { plural = amount === 1 ? "" : "s"; return amount + " second" + plural + " ago"; } if (amount = ONE_HOUR && amount = ONE_DAY && amount < THREE_DAYS) { amount = Math.round(amount / ONE_DAY); plural = amount === 1 ? "" : "s"; return amount + " day" + plural + " ago"; } return ""; } // Calculate time since published function since(storyDate) { var left = new Date(storyDate).getTime() / 1000; var right = Date.now() / 1000; return humanTime(left, right); } // ************************************************ // Following functionality // ************************************************ /** * Attach event listeners to following icons/buttons */ function addFollowingFunctionality() { var btns = qsa(".c-your-globe__overlay .c-topic-button"); if (!btns.length) { return; } btns.forEach(function fn(btn) { btn.addEventListener("mouseover", function fn() { btn.classList.remove("js-topic-clicked"); buttonToggle(btn, "over"); }); btn.addEventListener("mouseout", function fn() { buttonToggle(btn, "out"); }); btn.addEventListener("click", function fn() { updateFollowUnfollowStatus(btn); btn.classList.add("js-topic-clicked"); }); }); } /** * Follow or unfollow a topic * @param {HTMLElement} el - button element */ function updateFollowUnfollowStatus(el) { var status = el.getAttribute("data-status"); var topicSlug = el.getAttribute("data-slug"); var topicName = el.getAttribute("data-topic"); var isFollowingAuthor = el.dataset.topicType === "author"; var topicType = isFollowingAuthor ? "author" : "topic"; var action = status === "following" ? "unfollow" : "follow"; followUnfollowTopic(action, topicSlug, isFollowingAuthor) .then(function fn(res) { console.info("[ARC-6637] fetch success: ", res); var setStatus = "following"; var text = "following"; var ariaLabelText = "Unfollow this " + topicType + ": " + topicName; if (status === "following") { setStatus = "unfollowing"; text = "Follow"; ariaLabelText = "Follow this " + topicType + ": " + topicName; } analyticsLayer.push({ "event": "clickEvent", "clickEvent": { "type": "button", "feature": isFollowingAuthor ? "author" : "topic", "label": topicName.toLowerCase(), "action": action } }); el.dataset.status = setStatus; el.setAttribute("aria-label", ariaLabelText); qs(".c-button__text", el).innerText = text; }); } /** * Update icon state on mouseover/mouseout * @param {HTMLElement} el - button element * @param {String} action - either "out" or "over" */ function buttonToggle(el, action) { var status = el.getAttribute("data-status"); var text = "Follow"; var hoverText = "Follow"; if (status === "following") { text = "Following"; hoverText = "Unfollow"; } if (action === "out" && !el.classList.contains("js-topic-clicked")) { qs(".c-button__text", el).innerText = text; } else if (action === "out" && el.classList.contains("js-topic-clicked") && status === "unfollowing") { qs(".c-button__text", el).innerText = "Follow"; } else if (action === "over") { qs(".c-button__text", el).innerText = hoverText; } } // ************************************************ // Helper functions to build the markup // ************************************************ /** * Generate latest stories after some filtering * @param {Array} latest - array of days * @returns {Array} of filtered articles */ function generateLatestStories(latest) { // Latest is an array of days, so we need to create a new array of stories from last three days var articles = latest.reduce(function fn(result, item) { return result.concat(item.items); }, []); // We need to display only five latest articles to the user return articles.slice(0, 5); } /** * For every topic - generate one unique story * @param {Array} topics - array of topics (with five stories for every topic) * @returns {Array} - array of topics with one unique story */ function generateUniqueStory(topics) { var uniqueArticles = topics.map(function fn(t) { t.items = t.items.slice(0, 1); // Only one story is needed return t; }); return uniqueArticles; } /** * Generates image template * @param {Object} image - to render * @returns {String} */ function displayImage(image) { var template = ""; if (image.path) { template = [ "", " ", " ", image.alt, "“, ” ]]>”, “” ].join(“”); } return template; } /** * Generate a template for the label * @param {String} label – the label text * @returns {String} */ function displayLabel(label) { var template = “”; if (label === “opinion”) { template = [ “

“, ” “, label, “”, “

“, ].join(“”); } return template; } /** * Display “Follow”, “Following” buttons * @param {Object} tData * @returns {String} */ function displayFollowingButton(tData) { var topicName = tData.topicName; var topicSlug = tData.topicSlug; var topicType = tData.topicType; var topicImg = tData.topicImg; var isAuthor = topicType === “author”; var authorImg = isAuthor && topicImg ? “” : “”; var template = isAuthor ? “/authors/” : “/topics/”; var href = window.tgam.env.baseRootAbsoluteUrl + template + topicSlug; var linkClasses = isAuthor ? “c-topic-link c-topic-link–author” : “c-topic-link”; return [ ” “, ].join(“”); } /** * Generates story card markup * @param {Object} article – to display * @param {Object} topicData – (topicName, topicSlug, topicType, topicVariation, topicImg) * @param {Boolean} addFollowButton * @returns {String} story card markup */ function storyCard(article, topicData, addFollowButton) { console.info(“[ARC-6637] storyCard”, { article: article, topic: topicData }); if (!article || !topicData) { return “”; } var tName = topicData.topicName; var tType = topicData.topicType; var tVariation = topicData.topicVariation; var followingTopic; var timeTemplate; var analyticsModifier = tType + “: “; if (addFollowButton) { // Display a follow button beside the topic name followingTopic = displayFollowingButton(topicData); timeTemplate = displayDateTag(article); } var label = getLabel(article); var image = getImage(article, label); var labelTemplate = displayLabel(label); var imageTemplate = displayImage(image); var headline = article.headlines.basic; var href = window.tgam.env.baseUrl + article.canonical_url; var sophiId = article._id; var dataAnalyticsClick = JSON.stringify({ type: “link”, feature: “following feed”, contentId: sophiId, label: analyticsModifier + tName.toLowerCase() + “: ” + headline.toLowerCase(), page: “sec:homepage:personalized feed:” + tVariation, hierarchy: 1 }); var cardMarkup = “”; if (!followingTopic) { cardMarkup = [ “”, ” “, ”

“, ” “, tName, “”, ”

“, ”

“, ” “, headline, “”, ”

“, labelTemplate, ” “, ”

“, imageTemplate, “

“, “” ].join(“”); } else { cardMarkup = [ ”

“, followingTopic, ”

“, ” “, ” “, ”

“, ” “, headline, “”, ”

“, ” “, timeTemplate, “”, ” “, ”

“, imageTemplate, “

“, ” “, ].join(“”); } return [ ”

“, cardMarkup, ”

“, ].join(“”); } /** * Adds the overlay trigger dot class * @param {String} type – “unread” or “no-follow” */ function addOverlayTriggerDot(type) { overlayTriggerDotClasses.forEach(function fn(dotClass) { if (dotClass === “c-your-globe__trigger–dot–” + type) { overlayTrigger.classList.add(dotClass); } else { overlayTrigger.classList.remove(dotClass); } }); overlayTrigger.classList.add(“c-your-globe__trigger–dot”); } /** * Removes the overlay trigger dot class */ function removeOverlayTriggerDot() { overlayTriggerDotClasses.forEach(function fn(dotClass) { overlayTrigger.classList.remove(dotClass); }); overlayTrigger.classList.remove(“c-your-globe__trigger–dot”); } /** * Returns a heading element to be displayed inside the overlay * @param {String} text * @returns {String} */ function overlayLabel(text) { return “

” + text + “

“; } var overlayHeadingHasFollowed = ( overlayLabel(“The latest in topics and authors you follow”) + “

View more in Following

” ); var overlayHeadingNoFollowed = ( overlayLabel(“Get started: build your personal news feed”) + “

    ” + “

  1. Follow topics relevant to your reading interests.
  2. ” + “

  3. Check back here or your Following page to view the latest articles on your topics.
  4. ” + “

” ); var upToDateMessage = “

You’re up to date on your Following feed. Check again later for new stories.

“; // ************************************************ // Parse API response and inject markup into overlay // ************************************************ // Story card markup /** * Stories originally came from the following locations in the API response: * – data.articles[i].items[i].topics * – data.articles[i].items[i].authors * @param {Array} stories * @returns {String} story card markup */ function latestStoryCards(stories) { console.info(“[ARC-6637] Display latest stories based on these stories:”, stories); var storyCards = stories.map(function fn(story) { // The “topics” and “authors” arrays will only contain one item // (i.e. the topic or author that the user is following) var topic; var topicData; if (story.topics && story.topics.length) { // Normal topic topic = story.topics[0]; topicData = { topicName: topic.name, topicSlug: topic.slug, topicType: “topic”, topicVariation: “following” }; } else if (story.authors && story.authors.length) { // Author topic topic = story.authors[0]; // Only authors have images associated with them – normal topics do not var authorImg = topic.metadata && topic.metadata.image ? topic.metadata.image : null; topicData = { topicName: topic.byline, topicSlug: topic.slug, topicType: “author”, topicVariation: “following”, topicImg: authorImg }; } // Don’t display a follow button beside the topic because the user is already following it return storyCard(story, topicData, false); }).join(“”); return storyCards; } /** * @param {Array} topics * @param {String} variation – “recommended” or “trending” (used for the click tracking analytics) * @returns {String} story card markup */ function recommendedTrendingStoryCards(topics, variation) { var uniqueStories = generateUniqueStory(topics); var storyCards = uniqueStories.map(function fn(topic) { // Grab one article var story = topic.items[0]; // Only authors have images associated with them – normal topics do not var authorImg = topic.authorTopic && topic.authorMetadata && topic.authorMetadata.image ? topic.authorMetadata.image : null; // Normally we’d display all of the topics that are assigned to an article, but // this API groups articles by topic, so we only have access to that one topic. // Even if we could display additional topics, we wouldn’t want to becuase having // multiple follow buttons would junk up the UI. var topicData = { topicName: topic.name, topicSlug: topic.slug, topicType: topic.authorTopic ? “author” : “topic”, topicVariation: variation, topicImg: authorImg }; // Display the follow button beside the topic because we’re suggesting new topics to follow return storyCard(story, topicData, true); }).join(“”); return storyCards; } /** * Topics originally came from the following locations in the API response: * – data.recommendedAuthors * – data.recommendedTopics * @param {Array} topics * @returns {String} story card markup */ function recommendedStoryCards(topics) { console.info(“[ARC-6637] Display recommended stories based on these topics:”, topics); return recommendedTrendingStoryCards(topics, “recommended”); } /** * Topics originally came from the following location in the API response: * – data.trendingTopics * @param {Array} topics * @returns {String} story card markup */ function trendingStoryCards(topics) { console.info(“[ARC-6637] Display trending stories based on these topics:”, topics); return recommendedTrendingStoryCards(topics, “trending”); } // Markup inside the overlay /** * @param {Array} stories * @returns {Object} markup for the overlay’s header and body content areas */ function showLatestStories(stories) { console.info(“[ARC-6637] Scenario: latest stories”); var storyCardsMarkup = latestStoryCards(stories); return { header: overlayHeadingHasFollowed, body: storyCardsMarkup }; } /** * @param {Array} topics * @returns {Object} markup for the overlay’s header and body content areas */ function upToDateShowRecommended(topics) { console.info(“[ARC-6637] Scenario: up to date, show recommended”); var storyCardsMarkup = recommendedStoryCards(topics); return { header: overlayHeadingHasFollowed, body: ( upToDateMessage + overlayLabel(“Recommended for you”) + storyCardsMarkup ) }; } /** * @param {Array} topics * @returns {Object} markup for the overlay’s header and body content areas */ function upToDateShowTrending(topics) { console.info(“[ARC-6637] Scenario: up to date, show trending”); var storyCardsMarkup = trendingStoryCards(topics); return { header: overlayHeadingHasFollowed, body: ( upToDateMessage + overlayLabel(“Trending topics to follow”) + storyCardsMarkup ) }; } /** * @param {Array} topics * @returns {Object} markup for the overlay’s header and body content areas */ function notFollowingShowRecommended(topics) { console.info(“[ARC-6637] Scenario: not following, show recommended”); var storyCardsMarkup = recommendedStoryCards(topics); return { header: overlayHeadingNoFollowed, body: ( overlayLabel(“Recommended for you”) + storyCardsMarkup ) }; } /** * @param {Array} topics * @returns {Object} markup for the overlay’s header and body content areas */ function notFollowingShowTrending(topics) { console.info(“[ARC-6637] Scenario: not following, show trending”); var storyCardsMarkup = trendingStoryCards(topics); return { header: overlayHeadingNoFollowed, body: ( overlayLabel(“Trending topics to follow”) + storyCardsMarkup ) }; } /** * Parse data from the personalized API and inject markup into the overlay * @see https://confluence.theglobeandmail.com/display/ARC/Logic+for+Embedded+on+Homepage++and+Your+Globe+Overlay * @param {Object} data – topic and story data provided by the API */ function parsePerzonalizedTopicsData(data) { console.info(“[ARC-6637] parsePerzonalizedTopicsData”, data); var overlayHeader = qs(“.c-your-globe__overlay-header”); var overlayHeaderText = qs(“.c-your-globe__overlay-header-text”); var overlayBody = qs(“.c-your-globe__overlay-body”); if (!overlayHeader || !overlayHeaderText || !overlayBody) { return; } var totalTopicsFollowed = data.totalTopicsFollowed || 0; var totalAuthorsFollowed = data.totalAuthorsFollowed || 0; var recommendedTopics = data.recommendedTopics || []; var recommendedAuthors = data.recommendedAuthors || []; var latestStories = data.articles || []; var topics = []; var stories = []; var markup = “”; var headerBorder = true; if (totalTopicsFollowed || totalAuthorsFollowed) { if (latestStories.length) { // Scenario 1: “Latest stories” stories = generateLatestStories(latestStories); markup = showLatestStories(stories); // Similar to displayLatestAndRecommended() in ARC-6292 addOverlayTriggerDot(“unread”); } else { if (recommendedTopics.length || recommendedAuthors.length) { // Scenario 2: “Up to date, show recommended” topics = recommendedAuthors.concat(recommendedTopics); markup = upToDateShowRecommended(topics); // Similar to displayRecommendedTrendingTopicList(“rec”) in ARC-6292 } else { // Scenario 3: “Up to date, show trending” topics = data.trendingTopics || []; markup = upToDateShowTrending(topics); // Similar to displayRecommendedTrendingTopicList(“trend”) in ARC-6292 } } } else { topics = recommendedAuthors.concat(recommendedTopics); if (topics.length) { // Scenario 4: “Not following, show recommended” markup = notFollowingShowRecommended(topics); // Similar to displayThreeTopicsOneStory(“rec”) in ARC-6292 } else { // Scenario 5: “Not following, show trending” topics = data.trendingTopics || []; markup = notFollowingShowTrending(topics); // Similar to displayThreeTopicsOneStory(“trend”) in ARC-6292 } addOverlayTriggerDot(“no-follow”); headerBorder = false; } console.info(“[ARC-6637] Append markup”); var spinner = qs(“.c-spinner”); spinner && spinner.parentElement.removeChild(spinner); overlayHeaderText.insertAdjacentHTML(“afterbegin”, markup.header); overlayBody.insertAdjacentHTML(“afterbegin”, markup.body); if (!headerBorder) { overlayHeader.classList.add(“c-your-globe__overlay-header–no-border”); } addFollowingFunctionality(); } // ************************************************ // API calls // ************************************************ /** * Get the personalized feed * @param {String} hashId – user’s hash id * @param {String} env – environment * @see https://confluence.theglobeandmail.com/display/ARC/Logic+for+Embedded+on+Homepage++and+Your+Globe+Overlay * @returns {Promise

Source