diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /browser/components/pocket/content/panels/js/components | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/pocket/content/panels/js/components')
16 files changed, 1356 insertions, 0 deletions
diff --git a/browser/components/pocket/content/panels/js/components/ArticleList/ArticleList.jsx b/browser/components/pocket/content/panels/js/components/ArticleList/ArticleList.jsx new file mode 100644 index 0000000000..25679bc638 --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/ArticleList/ArticleList.jsx @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useState } from "react"; +import TelemetryLink from "../TelemetryLink/TelemetryLink"; + +function ArticleUrl(props) { + // We turn off the link if we're either a saved article, or if the url doesn't exist. + if (props.savedArticle || !props.url) { + return ( + <div className="stp_article_list_saved_article">{props.children}</div> + ); + } + return ( + <TelemetryLink + className="stp_article_list_link" + href={props.url} + source={props.source} + position={props.position} + model={props.model} + > + {props.children} + </TelemetryLink> + ); +} + +function Article(props) { + function encodeThumbnail(rawSource) { + return rawSource + ? `https://img-getpocket.cdn.mozilla.net/80x80/filters:format(jpeg):quality(60):no_upscale():strip_exif()/${encodeURIComponent( + rawSource + )}` + : null; + } + + const [thumbnailLoaded, setThumbnailLoaded] = useState(false); + const [thumbnailLoadFailed, setThumbnailLoadFailed] = useState(false); + + const { + article, + savedArticle, + position, + source, + model, + utmParams, + openInPocketReader, + } = props; + + if (!article.url && !article.resolved_url && !article.given_url) { + return null; + } + const url = new URL(article.url || article.resolved_url || article.given_url); + const urlSearchParams = new URLSearchParams(utmParams); + + if ( + openInPocketReader && + article.item_id && + !url.href.match(/getpocket\.com\/read/) + ) { + url.href = `https://getpocket.com/read/${article.item_id}`; + } + + for (let [key, val] of urlSearchParams.entries()) { + url.searchParams.set(key, val); + } + + // Using array notation because there is a key titled `1` (`images` is an object) + const thumbnail = + article.thumbnail || + encodeThumbnail(article?.top_image_url || article?.images?.["1"]?.src); + const alt = article.alt || "thumbnail image"; + const title = article.title || article.resolved_title || article.given_title; + // Sometimes domain_metadata is not there, depending on the source. + const publisher = + article.publisher || + article.domain_metadata?.name || + article.resolved_domain; + + return ( + <li className="stp_article_list_item"> + <ArticleUrl + url={url.href} + savedArticle={savedArticle} + position={position} + source={source} + model={model} + utmParams={utmParams} + > + <> + {thumbnail && !thumbnailLoadFailed ? ( + <img + className="stp_article_list_thumb" + src={thumbnail} + alt={alt} + width="40" + height="40" + onLoad={() => { + setThumbnailLoaded(true); + }} + onError={() => { + setThumbnailLoadFailed(true); + }} + style={{ + visibility: thumbnailLoaded ? `visible` : `hidden`, + }} + /> + ) : ( + <div className="stp_article_list_thumb_placeholder" /> + )} + <div className="stp_article_list_meta"> + <header className="stp_article_list_header">{title}</header> + <p className="stp_article_list_publisher">{publisher}</p> + </div> + </> + </ArticleUrl> + </li> + ); +} + +function ArticleList(props) { + return ( + <ul className="stp_article_list"> + {props.articles?.map((article, position) => ( + <Article + article={article} + savedArticle={props.savedArticle} + position={position} + source={props.source} + model={props.model} + utmParams={props.utmParams} + openInPocketReader={props.openInPocketReader} + /> + ))} + </ul> + ); +} + +export default ArticleList; diff --git a/browser/components/pocket/content/panels/js/components/ArticleList/ArticleList.scss b/browser/components/pocket/content/panels/js/components/ArticleList/ArticleList.scss new file mode 100644 index 0000000000..261367433d --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/ArticleList/ArticleList.scss @@ -0,0 +1,65 @@ +.stp_article_list { + padding: 0; + list-style: none; + + .stp_article_list_saved_article, + .stp_article_list_link { + display: flex; + border-radius: 4px; + padding: 8px; + margin: 0 -8px; + } + + .stp_article_list_link { + &:hover, &:focus { + text-decoration: none; + background-color: #ECECEE; + + @include theme_dark { + background-color: #2B2A33; + } + } + } + + .stp_article_list_thumb, + .stp_article_list_thumb_placeholder { + width: 40px; + height: 40px; + border-radius: 4px; + margin-inline-end: 8px; + background-color: #ECECEE; + flex-shrink: 0; + } + + .stp_article_list_header { + font-style: normal; + font-weight: 600; + font-size: 0.95rem; + line-height: 1.18rem; + color: #15141A; + margin: 0 0 4px; + + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + display: -webkit-box; + overflow: hidden; + word-break: break-word; + + @include theme_dark { + color: #FBFBFE; + } + } + + .stp_article_list_publisher { + font-style: normal; + font-weight: normal; + font-size: 0.95rem; + line-height: 1.18rem; + color: #52525E; + margin: 4px 0 0; + + @include theme_dark { + color: #CFCFD8; + } + } +} diff --git a/browser/components/pocket/content/panels/js/components/Button/Button.jsx b/browser/components/pocket/content/panels/js/components/Button/Button.jsx new file mode 100644 index 0000000000..7f3d2ea7ce --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/Button/Button.jsx @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import TelemetryLink from "../TelemetryLink/TelemetryLink"; + +function Button(props) { + return ( + <TelemetryLink + href={props.url} + onClick={props.onClick} + className={`stp_button${props?.style && ` stp_button_${props.style}`}`} + source={props.source} + > + {props.children} + </TelemetryLink> + ); +} + +export default Button; diff --git a/browser/components/pocket/content/panels/js/components/Button/Button.scss b/browser/components/pocket/content/panels/js/components/Button/Button.scss new file mode 100644 index 0000000000..c6001e19da --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/Button/Button.scss @@ -0,0 +1,142 @@ +.stp_button { + cursor: pointer; + display: inline-block; + margin: 12px 0; + + &:hover { + text-decoration: none; + } + + &.stp_button_text { + color: #0060DF; + font-size: 0.95rem; + line-height: 1.2rem; + font-style: normal; + font-weight: 600; + + &:focus { + text-decoration: underline; + } + + &:hover { + color: #0250BB; + text-decoration: none; + } + + &:active { + color: #054096; + } + + @include theme_dark { + color: #00DDFF; + } + } + + &.stp_button_primary { + align-items: center; + background: #0060DF; + border-radius: 4px; + color: #FBFBFE; + font-size: 0.85rem; + line-height: 1rem; + font-style: normal; + font-weight: 600; + justify-content: center; + padding: 6px 12px; + + &:focus { + text-decoration: none; + background: #0060DF; + outline: 2px solid #0060df; + outline-offset: 2px; + } + + &:hover { + background: #0250BB; + } + + &:active { + background: #054096; + } + + @include theme_dark { + background: #00DDFF; + color: #15141A; + + &:hover { + background: #80ebfe; + } + + &:focus { + outline: 2px solid #00DDFF; + } + } + } + + &.stp_button_secondary { + align-items: center; + background: #F0F0F4; + border-radius: 4px; + color: #15141A; + font-size: 0.85rem; + line-height: 1rem; + font-style: normal; + font-weight: 600; + padding: 6px 12px; + + &:focus { + text-decoration: none; + background: #F0F0F4; + outline: 2px solid #0060df; + outline-offset: 2px; + } + + &:hover { + background: #E0E0E6; + } + + &:active { + background: #CFCFD8; + } + + @include theme_dark { + background: #2B2A33; + color: #FBFBFE; + + &:focus { + outline: 2px solid #00DDFF; + } + + &:hover { + background: #53535d; + } + } + } +} + +.stp_button_wide { + .stp_button { + display: block; + margin: 12px 0; + text-align: center; + padding: 8px 12px; + + &.stp_button_primary { + font-size: 1.1rem; + line-height: 1.35rem; + } + + &.stp_button_secondary { + font-size: 0.85rem; + line-height: 1rem; + } + } +} + +.stp_button_wide { + .stp_button { + display: block; + margin: 12px 0; + text-align: center; + } +} diff --git a/browser/components/pocket/content/panels/js/components/Header/Header.jsx b/browser/components/pocket/content/panels/js/components/Header/Header.jsx new file mode 100644 index 0000000000..be60fe764c --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/Header/Header.jsx @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; + +function Header(props) { + return ( + <h1 className="stp_header"> + <div className="stp_header_logo" /> + {props.children} + </h1> + ); +} + +export default Header; diff --git a/browser/components/pocket/content/panels/js/components/Header/Header.scss b/browser/components/pocket/content/panels/js/components/Header/Header.scss new file mode 100644 index 0000000000..f6e4eca9d5 --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/Header/Header.scss @@ -0,0 +1,22 @@ +.stp_header { + display: flex; + justify-content: space-between; + align-items: center; + margin: 16px 0 12px; + font-weight: 600; + + .stp_header_logo { + background: url(../img/pocketlogo.svg) bottom center no-repeat; + background-size: contain; + height: 32px; + width: 121px; + + @include theme_dark { + background-image: url(../img/pocketlogo-dark.svg); + } + } + + .stp_button { + margin: 0; + } +} diff --git a/browser/components/pocket/content/panels/js/components/Home/Home.jsx b/browser/components/pocket/content/panels/js/components/Home/Home.jsx new file mode 100644 index 0000000000..fc936bfa56 --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/Home/Home.jsx @@ -0,0 +1,168 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useState, useEffect } from "react"; +import Header from "../Header/Header"; +import ArticleList from "../ArticleList/ArticleList"; +import PopularTopics from "../PopularTopics/PopularTopics"; +import Button from "../Button/Button"; +import panelMessaging from "../../messages"; + +function Home(props) { + const { + locale, + topics, + pockethost, + hideRecentSaves, + utmSource, + utmCampaign, + utmContent, + } = props; + const [{ articles, status }, setArticlesState] = useState({ + articles: [], + // Can be success, loading, or error. + status: "", + }); + + const utmParams = `utm_source=${utmSource}${ + utmCampaign && utmContent + ? `&utm_campaign=${utmCampaign}&utm_content=${utmContent}` + : `` + }`; + + useEffect(() => { + if (!hideRecentSaves) { + // We don't display the loading message until instructed. This is because cache + // loads should be fast, so using the loading message for cache just adds loading jank. + panelMessaging.addMessageListener( + "PKT_loadingRecentSaves", + function (resp) { + setArticlesState({ + articles, + status: "loading", + }); + } + ); + + panelMessaging.addMessageListener( + "PKT_renderRecentSaves", + function (resp) { + const { data } = resp; + + if (data.status === "error") { + setArticlesState({ + articles: [], + status: "error", + }); + return; + } + + setArticlesState({ + articles: data, + status: "success", + }); + } + ); + } + + // tell back end we're ready + panelMessaging.sendMessage("PKT_show_home"); + }, []); + + let recentSavesSection = null; + + if (status === "error" || hideRecentSaves) { + recentSavesSection = ( + <h3 + className="header_medium" + data-l10n-id="pocket-panel-home-new-user-cta" + /> + ); + } else if (status === "loading") { + recentSavesSection = ( + <span data-l10n-id="pocket-panel-home-most-recent-saves-loading" /> + ); + } else if (status === "success") { + if (articles?.length) { + recentSavesSection = ( + <> + <h3 + className="header_medium" + data-l10n-id="pocket-panel-home-most-recent-saves" + /> + {articles.length > 3 ? ( + <> + <ArticleList + articles={articles.slice(0, 3)} + source="home_recent_save" + utmParams={utmParams} + openInPocketReader={true} + /> + <span className="stp_button_wide"> + <Button + style="secondary" + url={`https://${pockethost}/a?${utmParams}`} + source="home_view_list" + > + <span data-l10n-id="pocket-panel-button-show-all" /> + </Button> + </span> + </> + ) : ( + <ArticleList + articles={articles} + source="home_recent_save" + utmParams={utmParams} + /> + )} + </> + ); + } else { + recentSavesSection = ( + <> + <h3 + className="header_medium" + data-l10n-id="pocket-panel-home-new-user-cta" + /> + <h3 + className="header_medium" + data-l10n-id="pocket-panel-home-new-user-message" + /> + </> + ); + } + } + + return ( + <div className="stp_panel_container"> + <div className="stp_panel stp_panel_home"> + <Header> + <Button + style="primary" + url={`https://${pockethost}/a?${utmParams}`} + source="home_view_list" + > + <span data-l10n-id="pocket-panel-header-my-saves" /> + </Button> + </Header> + <hr /> + {recentSavesSection} + <hr /> + {pockethost && locale?.startsWith("en") && topics?.length && ( + <> + <h3 className="header_medium">Explore popular topics:</h3> + <PopularTopics + topics={topics} + pockethost={pockethost} + utmParams={utmParams} + source="home_popular_topic" + /> + </> + )} + </div> + </div> + ); +} + +export default Home; diff --git a/browser/components/pocket/content/panels/js/components/PopularTopics/PopularTopics.jsx b/browser/components/pocket/content/panels/js/components/PopularTopics/PopularTopics.jsx new file mode 100644 index 0000000000..517bd6d53b --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/PopularTopics/PopularTopics.jsx @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import TelemetryLink from "../TelemetryLink/TelemetryLink"; + +function PopularTopics(props) { + return ( + <ul className="stp_popular_topics"> + {props.topics?.map((topic, position) => ( + <li key={`item-${topic.topic}`} className="stp_popular_topic"> + <TelemetryLink + className="stp_popular_topic_link" + href={`https://${props.pockethost}/explore/${topic.topic}?${props.utmParams}`} + source={props.source} + position={position} + > + {topic.title} + </TelemetryLink> + </li> + ))} + </ul> + ); +} + +export default PopularTopics; diff --git a/browser/components/pocket/content/panels/js/components/PopularTopics/PopularTopics.scss b/browser/components/pocket/content/panels/js/components/PopularTopics/PopularTopics.scss new file mode 100644 index 0000000000..bacd55ee42 --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/PopularTopics/PopularTopics.scss @@ -0,0 +1,54 @@ +.stp_popular_topics { + padding: 0; + + .stp_popular_topic { + display: inline-block; + + .stp_popular_topic_link { + display: inline-block; + background: #F0F0F4; + border-radius: 4px; + font-size: 0.85rem; + line-height: 1rem; + font-style: normal; + font-weight: 600; + margin-inline-end: 8px; + margin-bottom: 8px; + padding: 4px 8px; + color: #000; + + &:focus { + text-decoration: none; + background: #F0F0F4; + outline: 2px solid #0060df; + outline-offset: 2px; + } + + &:hover { + background: #E0E0E6; + text-decoration: none; + } + + &:active { + background: #CFCFD8; + } + + &::after { + content: " >"; + } + + @include theme_dark { + background: #2B2A33; + color: #FBFBFE; + + &:focus { + outline: 2px solid #00DDFF; + } + + &:hover { + background: #53535d; + } + } + } + } +} diff --git a/browser/components/pocket/content/panels/js/components/Saved/Saved.jsx b/browser/components/pocket/content/panels/js/components/Saved/Saved.jsx new file mode 100644 index 0000000000..f81c6df198 --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/Saved/Saved.jsx @@ -0,0 +1,203 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useState, useEffect } from "react"; +import Header from "../Header/Header"; +import Button from "../Button/Button"; +import ArticleList from "../ArticleList/ArticleList"; +import TagPicker from "../TagPicker/TagPicker"; +import panelMessaging from "../../messages"; + +function Saved(props) { + const { locale, pockethost, utmSource, utmCampaign, utmContent } = props; + // savedStatus can be success, loading, or error. + const [{ savedStatus, savedErrorId, itemId, itemUrl }, setSavedStatusState] = + useState({ savedStatus: "loading" }); + // removedStatus can be removed, removing, or error. + const [{ removedStatus, removedErrorMessage }, setRemovedStatusState] = + useState({}); + const [savedStory, setSavedStoryState] = useState(); + const [articleInfoAttempted, setArticleInfoAttempted] = useState(); + const [{ similarRecs, similarRecsModel }, setSimilarRecsState] = useState({}); + const utmParams = `utm_source=${utmSource}${ + utmCampaign && utmContent + ? `&utm_campaign=${utmCampaign}&utm_content=${utmContent}` + : `` + }`; + + function removeItem(event) { + event.preventDefault(); + setRemovedStatusState({ removedStatus: "removing" }); + panelMessaging.sendMessage( + "PKT_deleteItem", + { + itemId, + }, + function (resp) { + const { data } = resp; + if (data.status == "success") { + setRemovedStatusState({ removedStatus: "removed" }); + } else if (data.status == "error") { + let errorMessage = ""; + // The server returns English error messages, so in the case of + // non English, we do our best with a generic translated error. + if (data.error.message && locale?.startsWith("en")) { + errorMessage = data.error.message; + } + setRemovedStatusState({ + removedStatus: "error", + removedErrorMessage: errorMessage, + }); + } + } + ); + } + + useEffect(() => { + // Wait confirmation of save before flipping to final saved state + panelMessaging.addMessageListener("PKT_saveLink", function (resp) { + const { data } = resp; + if (data.status == "error") { + // Use localizedKey or fallback to a generic catch all error. + setSavedStatusState({ + savedStatus: "error", + savedErrorId: + data?.error?.localizedKey || "pocket-panel-saved-error-generic", + }); + return; + } + + // Success, so no localized error id needed. + setSavedStatusState({ + savedStatus: "success", + itemId: data.item?.item_id, + itemUrl: data.item?.given_url, + savedErrorId: "", + }); + }); + + panelMessaging.addMessageListener( + "PKT_articleInfoFetched", + function (resp) { + setSavedStoryState(resp?.data?.item_preview); + } + ); + + panelMessaging.addMessageListener( + "PKT_getArticleInfoAttempted", + function (resp) { + setArticleInfoAttempted(true); + } + ); + + panelMessaging.addMessageListener("PKT_renderItemRecs", function (resp) { + const { data } = resp; + + // This is the ML model used to recommend the story. + // Right now this value is the same for all three items returned together, + // so we can just use the first item's value for all. + const model = data?.recommendations?.[0]?.experiment || ""; + setSimilarRecsState({ + similarRecs: data?.recommendations?.map(rec => rec.item), + similarRecsModel: model, + }); + }); + + // tell back end we're ready + panelMessaging.sendMessage("PKT_show_saved"); + }, []); + + if (savedStatus === "error") { + return ( + <div className="stp_panel_container"> + <div className="stp_panel stp_panel_error"> + <div className="stp_panel_error_icon" /> + <h3 + className="header_large" + data-l10n-id="pocket-panel-saved-error-not-saved" + /> + <p data-l10n-id={savedErrorId} /> + </div> + </div> + ); + } + + return ( + <div className="stp_panel_container"> + <div className="stp_panel stp_panel_saved"> + <Header> + <Button + style="primary" + url={`https://${pockethost}/a?${utmParams}`} + source="view_list" + > + <span data-l10n-id="pocket-panel-header-my-saves"></span> + </Button> + </Header> + <hr /> + {!removedStatus && savedStatus === "success" && ( + <> + <h3 className="header_large header_flex"> + <span data-l10n-id="pocket-panel-saved-page-saved-b" /> + <Button style="text" onClick={removeItem}> + <span data-l10n-id="pocket-panel-button-remove"></span> + </Button> + </h3> + {savedStory && ( + <ArticleList + articles={[savedStory]} + openInPocketReader={true} + utmParams={utmParams} + /> + )} + {articleInfoAttempted && <TagPicker tags={[]} itemUrl={itemUrl} />} + {articleInfoAttempted && + similarRecs?.length && + locale?.startsWith("en") && ( + <> + <hr /> + <h3 className="header_medium">Similar Stories</h3> + <ArticleList + articles={similarRecs} + source="on_save_recs" + model={similarRecsModel} + utmParams={utmParams} + /> + </> + )} + </> + )} + {savedStatus === "loading" && ( + <h3 + className="header_large" + data-l10n-id="pocket-panel-saved-saving-tags" + /> + )} + {removedStatus === "removing" && ( + <h3 + className="header_large header_center" + data-l10n-id="pocket-panel-saved-processing-remove" + /> + )} + {removedStatus === "removed" && ( + <h3 + className="header_large header_center" + data-l10n-id="pocket-panel-saved-removed-updated" + /> + )} + {removedStatus === "error" && ( + <> + <h3 + className="header_large" + data-l10n-id="pocket-panel-saved-error-remove" + /> + {removedErrorMessage && <p>{removedErrorMessage}</p>} + </> + )} + </div> + </div> + ); +} + +export default Saved; diff --git a/browser/components/pocket/content/panels/js/components/Saved/Saved.scss b/browser/components/pocket/content/panels/js/components/Saved/Saved.scss new file mode 100644 index 0000000000..08003f14e1 --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/Saved/Saved.scss @@ -0,0 +1,17 @@ +body { + &.stp_saved_body { + overflow: hidden; + } +} + +.stp_panel_error { + margin: 23px 0 32px; + .stp_panel_error_icon { + float: inline-start; + margin-block: 6px 16px; + margin-inline: 7px 17px; + background-image: url(../img/pocketerror@1x.png); + height: 44px; + width: 44px; + } +} diff --git a/browser/components/pocket/content/panels/js/components/Signup/Signup.jsx b/browser/components/pocket/content/panels/js/components/Signup/Signup.jsx new file mode 100644 index 0000000000..7e628c0ad5 --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/Signup/Signup.jsx @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import Header from "../Header/Header"; +import Button from "../Button/Button"; + +function Signup(props) { + const { locale, pockethost, utmSource, utmCampaign, utmContent } = props; + const utmParams = `utm_source=${utmSource}${ + utmCampaign && utmContent + ? `&utm_campaign=${utmCampaign}&utm_content=${utmContent}` + : `` + }`; + return ( + <div className="stp_panel_container"> + <div className="stp_panel stp_panel_signup"> + <Header> + <Button + style="secondary" + url={`https://${pockethost}/login?${utmParams}`} + source="log_in" + > + <span data-l10n-id="pocket-panel-signup-login" /> + </Button> + </Header> + <hr /> + {locale?.startsWith("en") ? ( + <> + <div className="stp_signup_content_wrapper"> + <h3 + className="header_medium" + data-l10n-id="pocket-panel-signup-cta-a-fix" + /> + <p data-l10n-id="pocket-panel-signup-cta-b-updated" /> + </div> + <div className="stp_signup_content_wrapper"> + <hr /> + </div> + <div className="stp_signup_content_wrapper"> + <div className="stp_signup_img_rainbow_reader" /> + <h3 className="header_medium"> + Get thought-provoking article recommendations + </h3> + <p> + Find stories that go deep into a subject or offer a new + perspective. + </p> + </div> + </> + ) : ( + <div className="stp_signup_content_wrapper"> + <h3 + className="header_large" + data-l10n-id="pocket-panel-signup-cta-a-fix" + /> + <p data-l10n-id="pocket-panel-signup-cta-b-short" /> + <strong> + <p data-l10n-id="pocket-panel-signup-cta-c-updated" /> + </strong> + </div> + )} + <hr /> + <span className="stp_button_wide"> + <Button + style="primary" + url={`https://${pockethost}/ff_signup?${utmParams}`} + source="sign_up_1" + > + <span data-l10n-id="pocket-panel-button-activate" /> + </Button> + </span> + </div> + </div> + ); +} + +export default Signup; diff --git a/browser/components/pocket/content/panels/js/components/Signup/Signup.scss b/browser/components/pocket/content/panels/js/components/Signup/Signup.scss new file mode 100644 index 0000000000..21b34ddcb6 --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/Signup/Signup.scss @@ -0,0 +1,19 @@ +body { + &.stp_signup_body { + overflow: hidden; + } +} + +.stp_panel_signup { + .stp_signup_content_wrapper { + margin: 12px 0 20px; + } + .stp_signup_img_rainbow_reader { + background: url(../img/rainbow-reader.svg) bottom center no-repeat; + background-size: contain; + height: 72px; + width: 82px; + float: inline-end; + margin-inline-start: 16px; + } +} diff --git a/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.jsx b/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.jsx new file mode 100644 index 0000000000..9c1f658a80 --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.jsx @@ -0,0 +1,208 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useState, useEffect } from "react"; +import panelMessaging from "../../messages"; + +function TagPicker(props) { + const [tags, setTags] = useState(props.tags); // New tag group to store + const [allTags, setAllTags] = useState([]); // All tags ever used (in no particular order) + const [recentTags, setRecentTags] = useState([]); // Most recently used tags + const [duplicateTag, setDuplicateTag] = useState(null); + const [inputValue, setInputValue] = useState(""); + + // Status can be success, waiting, or error. + const [{ tagInputStatus, tagInputErrorMessage }, setTagInputStatus] = + useState({ + tagInputStatus: "", + tagInputErrorMessage: "", + }); + + let handleKeyDown = e => { + const enterKey = e.keyCode === 13; + const commaKey = e.keyCode === 188; + const tabKey = inputValue && e.keyCode === 9; + + // Submit tags on enter with no input. + // Enter tag on comma, tab, or enter with input. + // Tab to next element with no input. + if (commaKey || enterKey || tabKey) { + e.preventDefault(); + if (inputValue) { + addTag(inputValue.trim()); + setInputValue(``); // Clear out input + } else if (enterKey) { + submitTags(); + } + } + }; + + let addTag = tagToAdd => { + if (!tagToAdd?.length) { + return; + } + + let newDuplicateTag = tags.find(item => item === tagToAdd); + + if (!newDuplicateTag) { + setTags([...tags, tagToAdd]); + } else { + setDuplicateTag(newDuplicateTag); + + setTimeout(() => { + setDuplicateTag(null); + }, 1000); + } + }; + + let removeTag = index => { + let updatedTags = tags.slice(0); // Shallow copied array + updatedTags.splice(index, 1); + setTags(updatedTags); + }; + + let submitTags = () => { + let tagsToSubmit = []; + + if (tags?.length) { + tagsToSubmit = tags; + } + + // Capture tags that have been typed in but not explicitly added to the tag collection + if (inputValue?.trim().length) { + tagsToSubmit.push(inputValue.trim()); + } + + if (!props.itemUrl || !tagsToSubmit?.length) { + return; + } + + setTagInputStatus({ + tagInputStatus: "waiting", + tagInputErrorMessage: "", + }); + panelMessaging.sendMessage( + "PKT_addTags", + { + url: props.itemUrl, + tags: tagsToSubmit, + }, + function (resp) { + const { data } = resp; + + if (data.status === "success") { + setTagInputStatus({ + tagInputStatus: "success", + tagInputErrorMessage: "", + }); + } else if (data.status === "error") { + setTagInputStatus({ + tagInputStatus: "error", + tagInputErrorMessage: data.error.message, + }); + } + } + ); + }; + + useEffect(() => { + panelMessaging.sendMessage("PKT_getTags", {}, resp => { + setAllTags(resp?.data?.tags); + }); + }, []); + + useEffect(() => { + panelMessaging.sendMessage("PKT_getRecentTags", {}, resp => { + setRecentTags(resp?.data?.recentTags); + }); + }, []); + + return ( + <div className="stp_tag_picker"> + {!tagInputStatus && ( + <> + <h3 + className="header_small" + data-l10n-id="pocket-panel-signup-add-tags" + /> + <div className="stp_tag_picker_tags"> + {tags.map((tag, i) => ( + <div + className={`stp_tag_picker_tag${ + duplicateTag === tag ? ` stp_tag_picker_tag_duplicate` : `` + }`} + > + <button + onClick={() => removeTag(i)} + className={`stp_tag_picker_tag_remove`} + > + X + </button> + {tag} + </div> + ))} + <div className="stp_tag_picker_input_wrapper"> + <input + className="stp_tag_picker_input" + type="text" + list="tag-list" + value={inputValue} + onChange={e => setInputValue(e.target.value)} + onKeyDown={e => handleKeyDown(e)} + maxlength="25" + /> + <datalist id="tag-list"> + {allTags + .sort((a, b) => a.search(inputValue) - b.search(inputValue)) + .map(item => ( + <option key={item} value={item} /> + ))} + </datalist> + <button + className="stp_tag_picker_button" + disabled={!inputValue?.length && !tags.length} + data-l10n-id="pocket-panel-saved-save-tags" + onClick={() => submitTags()} + /> + </div> + </div> + <div className="recent_tags"> + {recentTags + .slice(0, 3) + .filter(recentTag => { + return !tags.find(item => item === recentTag); + }) + .map(tag => ( + <div className="stp_tag_picker_tag"> + <button + className="stp_tag_picker_tag_remove" + onClick={() => addTag(tag)} + > + + {tag} + </button> + </div> + ))} + </div> + </> + )} + {tagInputStatus === "waiting" && ( + <h3 + className="header_large" + data-l10n-id="pocket-panel-saved-processing-tags" + /> + )} + {tagInputStatus === "success" && ( + <h3 + className="header_large" + data-l10n-id="pocket-panel-saved-tags-saved" + /> + )} + {tagInputStatus === "error" && ( + <h3 className="header_small">{tagInputErrorMessage}</h3> + )} + </div> + ); +} + +export default TagPicker; diff --git a/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.scss b/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.scss new file mode 100644 index 0000000000..215307d079 --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.scss @@ -0,0 +1,141 @@ +.stp_tag_picker { + .stp_tag_picker_tags { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + padding: 8px; + border: 1px solid #8F8F9D; + border-radius: 4px; + font-style: normal; + font-weight: normal; + font-size: 1rem; + line-height: 1.2rem; + color: #15141A; + margin-bottom: 10px; + } + + .stp_tag_picker_tag { + background: #F0F0F4; + border-radius: 4px; + color: #15141A; + display: inline-block; + font-size: 0.85rem; + line-height: 1rem; + font-style: normal; + font-weight: 600; + padding: 0 8px; + transition: background-color 200ms ease-in-out; + + @include theme_dark { + background: #2B2A33; + color: #FBFBFB; + } + } + + .recent_tags .stp_tag_picker_tag { + margin-inline-end: 5px; + } + + .stp_tag_picker_tag_remove { + padding-top: 5px; + padding-bottom: 5px; + padding-inline-end: 5px; + color: #5B5B66; + font-weight: 400; + + &:hover { + color: #3E3E44; + } + + &:focus { + color: #3E3E44; + outline: 2px solid #0060df; + outline-offset: -4px; + } + + @include theme_dark { + color: #8F8F9D; + + &:hover { + color: #fff; + } + + &:focus { + outline: 2px solid #00DDFF; + } + } + } + + .stp_tag_picker_tag_duplicate { + background-color: #bbb; + + @include theme_dark { + background-color: #666; + } + } + + .stp_tag_picker_input_wrapper { + display: flex; + flex-grow: 1; + } + + .stp_tag_picker_input { + flex-grow: 1; + border: 1px solid #8F8F9D; + padding: 0 6px; + border-start-start-radius: 4px; + border-end-start-radius: 4px; + + &:focus { + border: 1px solid #0060DF; + outline: 1px solid #0060DF; + } + + @include theme_dark { + background: none; + color: #FBFBFB; + + &:focus { + border: 1px solid #00DDFF; + outline: 1px solid #00DDFF; + } + } + } + + .stp_tag_picker_button { + font-size: 0.95rem; + line-height: 1.1rem; + padding: 4px 6px; + background-color: #F0F0F4; + border: 1px solid #8F8F9D; + border-inline-start: none; + border-start-end-radius: 4px; + border-end-end-radius: 4px; + &:disabled { + color: #8F8F9D; + } + &:hover:enabled { + background-color: #DADADF; + } + &:focus:enabled { + border: 1px solid #0060DF; + outline: 1px solid #0060DF; + } + + @include theme_dark { + background-color: #2B2A33; + color: #FBFBFB; + &:disabled { + color: #666; + } + &:hover:enabled { + background-color: #53535d; + } + &:focus:enabled { + border: 1px solid #00DDFF; + outline: 1px solid #00DDFF; + } + } + } +} diff --git a/browser/components/pocket/content/panels/js/components/TelemetryLink/TelemetryLink.jsx b/browser/components/pocket/content/panels/js/components/TelemetryLink/TelemetryLink.jsx new file mode 100644 index 0000000000..c23a24897f --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/TelemetryLink/TelemetryLink.jsx @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import panelMessaging from "../../messages"; + +function TelemetryLink(props) { + function onClick(event) { + if (props.onClick) { + props.onClick(event); + } else { + event.preventDefault(); + panelMessaging.sendMessage("PKT_openTabWithUrl", { + url: event.currentTarget.getAttribute(`href`), + source: props.source, + model: props.model, + position: props.position, + }); + } + } + + return ( + <a + href={props.href} + onClick={onClick} + target="_blank" + className={props.className} + > + {props.children} + </a> + ); +} + +export default TelemetryLink; |