├── jest.config.js
├── src
├── assets
│ ├── pocket-save-extension.css
│ └── images
│ │ ├── icon-16.png
│ │ ├── icon-32.png
│ │ ├── icon-48.png
│ │ ├── icon-128.png
│ │ ├── action-icon.png
│ │ └── pocket-logo.png
├── components
│ ├── logo
│ │ ├── logo.stories.js
│ │ └── logo.js
│ ├── footer
│ │ ├── footer.stories.js
│ │ └── footer.js
│ ├── icons
│ │ ├── style.js
│ │ ├── svg
│ │ │ ├── PocketLogo.js
│ │ │ ├── Error.js
│ │ │ ├── Facebook-Mono.js
│ │ │ ├── List-View.js
│ │ │ ├── Twitter-Mono.js
│ │ │ ├── Settings.js
│ │ │ └── Instagram.js
│ │ ├── icons.js
│ │ ├── icons.stories.js
│ │ └── icon.js
│ ├── error-message
│ │ └── error-message.js
│ ├── button
│ │ ├── button.stories.js
│ │ └── extensions-button.js
│ ├── chips
│ │ ├── chips.stories.js
│ │ └── chips.js
│ ├── heading
│ │ ├── heading.stories.js
│ │ └── heading.js
│ ├── item-preview
│ │ ├── item-preview.stories.js
│ │ └── item-preview.js
│ ├── doorhanger
│ │ └── doorhanger.js
│ ├── tagging
│ │ ├── tagging.stories.js
│ │ ├── suggestions
│ │ │ └── suggestions.js
│ │ ├── taginput
│ │ │ └── taginput.js
│ │ └── tagging.js
│ └── loading
│ │ └── loading.js
├── common
│ ├── constants.js
│ ├── api
│ │ ├── index.js
│ │ ├── auth
│ │ │ ├── authorize.js
│ │ │ └── guid.js
│ │ ├── saving
│ │ │ ├── remove.js
│ │ │ ├── save.js
│ │ │ └── tags.js
│ │ └── _request
│ │ │ └── request.js
│ ├── utilities.js
│ ├── _mocks
│ │ └── tags.js
│ ├── locales.js
│ ├── helpers.js
│ └── interface.js
├── pages
│ ├── logout.js
│ ├── options
│ │ ├── options.html
│ │ └── options.js
│ ├── injector
│ │ ├── content.js
│ │ ├── app.js
│ │ └── globalStyles.js
│ ├── login.js
│ └── background
│ │ ├── postSave.js
│ │ ├── index.js
│ │ └── userActions.js
├── connectors
│ ├── footer
│ │ └── footer.js
│ ├── item-preview
│ │ └── item-preview.js
│ ├── heading
│ │ └── heading.js
│ └── tagging
│ │ └── tagging.js
├── actions.js
├── manifest.json
└── _locales
│ ├── zh_CN
│ └── messages.json
│ ├── zh_TW
│ └── messages.json
│ ├── ko
│ └── messages.json
│ ├── ja
│ └── messages.json
│ ├── en
│ └── messages.json
│ ├── pt_BR
│ └── messages.json
│ ├── nl
│ └── messages.json
│ ├── pt_PT
│ └── messages.json
│ ├── pl
│ └── messages.json
│ ├── ru
│ └── messages.json
│ ├── it
│ └── messages.json
│ ├── es_419
│ └── messages.json
│ ├── de
│ └── messages.json
│ ├── es
│ └── messages.json
│ └── fr
│ └── messages.json
├── .prettierrc.yaml
├── .storybook
├── manager.js
├── pocketTheme.js
├── main.js
└── preview.js
├── babel.config.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report---.md
│ └── user-story---.md
├── PULL_REQUEST_TEMPLATE.md
└── prlint.json
├── config
├── jest.setup.js
└── jest.setup.test.js
├── .eslintrc.js
├── LICENSE
├── .gitignore
├── package.json
├── rollup.config.js
├── README.md
└── CONTRIBUTING.md
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | setupFilesAfterEnv: ['./config/jest.setup.js'],
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/pocket-save-extension.css:
--------------------------------------------------------------------------------
1 | /* THIS FILE WILL BE GENERATED: DO NOT MAKE EDITS HERE*/
2 |
--------------------------------------------------------------------------------
/src/assets/images/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arpitbbhayani/extension-save-to-pocket/HEAD/src/assets/images/icon-16.png
--------------------------------------------------------------------------------
/src/assets/images/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arpitbbhayani/extension-save-to-pocket/HEAD/src/assets/images/icon-32.png
--------------------------------------------------------------------------------
/src/assets/images/icon-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arpitbbhayani/extension-save-to-pocket/HEAD/src/assets/images/icon-48.png
--------------------------------------------------------------------------------
/src/assets/images/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arpitbbhayani/extension-save-to-pocket/HEAD/src/assets/images/icon-128.png
--------------------------------------------------------------------------------
/src/assets/images/action-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arpitbbhayani/extension-save-to-pocket/HEAD/src/assets/images/action-icon.png
--------------------------------------------------------------------------------
/src/assets/images/pocket-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arpitbbhayani/extension-save-to-pocket/HEAD/src/assets/images/pocket-logo.png
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | semi: false
2 | tabWidth: 2
3 | bracketSameLine: true
4 | trailingComma: 'all'
5 | singleQuote: true
6 | printWidth": 100
7 | arrowParens: 'always'
8 |
--------------------------------------------------------------------------------
/.storybook/manager.js:
--------------------------------------------------------------------------------
1 | import { addons } from '@storybook/addons';
2 | import pocketTheme from './pocketTheme';
3 |
4 | addons.setConfig({
5 | theme: pocketTheme,
6 | });
7 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", { "targets": { "chrome": 80 } }],
4 | "@babel/preset-react"
5 | ],
6 | "plugins": ["@babel/plugin-proposal-class-properties"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/logo/logo.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Logo } from './logo'
3 |
4 | export default {
5 | title: 'Components/Logo',
6 | component: Logo
7 | }
8 |
9 | export const FullLogo = () =>
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report---.md:
--------------------------------------------------------------------------------
1 | ## Expected Behavior
2 |
3 | ## Actual Behavior
4 |
5 | ## Steps to Reproduce the Problem
6 |
7 | 1.
8 | 1.
9 | 1.
10 |
11 | ## Specifications
12 |
13 | * Version:
14 | * Browser:
15 | * Operating System:
16 |
--------------------------------------------------------------------------------
/src/components/footer/footer.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Footer } from './footer'
3 |
4 | export default {
5 | title: 'Components/Footer',
6 | component: Footer,
7 | };
8 |
9 | export const FooterDefault = () =>
10 |
--------------------------------------------------------------------------------
/.storybook/pocketTheme.js:
--------------------------------------------------------------------------------
1 | import { create } from '@storybook/theming';
2 | import pocketLogo from '/src/assets/images/pocket-logo.png'
3 |
4 | export default create({
5 | base: 'light',
6 | brandTitle: 'Pocket',
7 | brandUrl: 'https://getpocket.com',
8 | brandImage: pocketLogo,
9 | });
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/user-story---.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'User Story'
3 | about: Deliverable that provides a discrete aspect user value
4 | ---
5 |
6 | ## User Story
7 |
8 | A discrete aspect of the product that provides user value
9 |
10 | ## Acceptance Criteria
11 |
12 | ## Implementation Details
13 |
--------------------------------------------------------------------------------
/src/components/icons/style.js:
--------------------------------------------------------------------------------
1 | export const svgStyles = (additional_styles = {}) => {
2 | return {
3 | display: 'inline-block',
4 | verticalAlign: 'middle',
5 | width: '16px',
6 | height: '16px',
7 | marginRight: '5px',
8 | fill: 'currentColor',
9 | lineHeight: '1em',
10 | ...additional_styles
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/config/jest.setup.js:
--------------------------------------------------------------------------------
1 | import { chrome } from 'jest-chrome'
2 |
3 | // @ts-expect-error we need to set this to use browser polyfill
4 | chrome.runtime.id = 'test id'
5 | Object.assign(global, { chrome })
6 |
7 | // We need to require this after we setup jest chrome
8 | const browser = require('webextension-polyfill')
9 | Object.assign(global, { browser })
10 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Goal
2 |
3 | Insert purpose of pull request
4 |
5 | ## Todos:
6 | - [ ] Outstanding todo
7 | - [x] Completed todo
8 |
9 | ## Implementation Decisions
10 |
11 |
12 | ## All Submissions:
13 |
14 | - [ ] Have you followed the guidelines in our Contributing document?
15 | - [ ] Have you checked to ensure there aren't other open [Pull Requests](../pulls) for the same update/change?
16 |
--------------------------------------------------------------------------------
/src/components/error-message/error-message.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css } from 'linaria'
3 |
4 | const errorMessageStyles = css`
5 | color: var(--color-headingIcon);
6 | font-size: 16px;
7 | padding: 15px 10px;
8 | `
9 |
10 | export const ErrorMessage = ({ message }) => {
11 | return (
12 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/common/constants.js:
--------------------------------------------------------------------------------
1 | export const AUTH_URL =
2 | 'https://getpocket.com/signup?src=extension&route=/extension_login_success'
3 | export const LOGOUT_URL = 'https://getpocket.com/lo'
4 | export const SET_SHORTCUTS = 'chrome://extensions/configureCommands'
5 | export const API_URL = 'https://getpocket.com/v3/'
6 | export const POCKET_LIST = 'https://getpocket.com/saves'
7 | export const POCKET_HOME = 'https://getpocket.com/home'
8 | export const CONSUMER_KEY = '__consumerKey__'
9 |
--------------------------------------------------------------------------------
/src/pages/logout.js:
--------------------------------------------------------------------------------
1 | import { sendMessage } from 'common/interface'
2 | import { LOGGED_OUT_OF_POCKET } from 'actions'
3 |
4 | // Check page has loaded and if not add listener for it
5 | if (document.readyState === 'loading') {
6 | document.addEventListener('DOMContentLoaded', setLogoutLoaded)
7 | } else {
8 | setLogoutLoaded()
9 | }
10 |
11 | function setLogoutLoaded() {
12 | setTimeout(function () {
13 | sendMessage({ type: LOGGED_OUT_OF_POCKET })
14 | }, 500)
15 | }
16 |
--------------------------------------------------------------------------------
/src/common/api/index.js:
--------------------------------------------------------------------------------
1 | import { request } from './_request/request'
2 | import { saveToPocket } from './saving/save'
3 | import { getOnSaveTags, syncItemTags, fetchStoredTags } from './saving/tags'
4 | import { removeItem } from './saving/remove'
5 | import { authorize } from './auth/authorize'
6 | import { getGuid } from './auth/guid'
7 |
8 | export {
9 | authorize,
10 | saveToPocket,
11 | getOnSaveTags,
12 | request,
13 | removeItem,
14 | getGuid,
15 | syncItemTags,
16 | fetchStoredTags
17 | }
18 |
--------------------------------------------------------------------------------
/src/common/api/auth/authorize.js:
--------------------------------------------------------------------------------
1 | import { request } from '../_request/request'
2 |
3 | /* API CALLS - Should return promises
4 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
5 | export function authorize(guid, userCookies) {
6 | return request(
7 | {
8 | path: 'oauth/authorize/',
9 | data: {
10 | guid,
11 | token: userCookies.token,
12 | user_id: userCookies.userId,
13 | account: '1',
14 | grant_type: 'extension',
15 | },
16 | },
17 | true,
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/connectors/footer/footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Footer } from 'components/footer/footer'
3 |
4 | import { OPEN_POCKET } from 'actions'
5 | import { OPEN_OPTIONS } from 'actions'
6 |
7 | export const FooterConnector = () => {
8 | const myListClick = () => chrome.runtime.sendMessage({ type: OPEN_POCKET })
9 | const settingsClick = () => chrome.runtime.sendMessage({ type: OPEN_OPTIONS })
10 |
11 | return (
12 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/common/utilities.js:
--------------------------------------------------------------------------------
1 | /* Utilities
2 | /* -----------------------------------------------
3 | /* These are single function utilities that rely on
4 | /* no external libraries or files
5 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
6 | export function arrayHasValues(checkArray) {
7 | return checkArray.filter(value => value && typeof value !== 'undefined')
8 | }
9 |
10 | export function getBool(value) {
11 | return (
12 | value === true ||
13 | value === 'true' ||
14 | value === 1 ||
15 | parseInt(value, 10) === 1
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/button/button.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button } from './extensions-button'
3 |
4 | export default {
5 | title: 'Components/Buttons',
6 | component: Button,
7 | };
8 |
9 | export const Primary = () => {
10 | return (
11 |
12 | )
13 | }
14 |
15 | export const Secondary = () => {
16 | return (
17 |
18 | )
19 | }
20 |
21 | export const Inline = () => {
22 | return (
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | overrides: [
3 | {
4 | files: ['*.test.js'],
5 | env: { jest: true },
6 | },
7 | ],
8 | env: {
9 | browser: true,
10 | es2021: true,
11 | node: true,
12 | webextensions: true,
13 | },
14 | extends: ['eslint:recommended', 'plugin:react/recommended'],
15 | parserOptions: {
16 | ecmaFeatures: {
17 | jsx: true,
18 | },
19 | ecmaVersion: 12,
20 | sourceType: 'module',
21 | },
22 | plugins: ['react'],
23 | rules: {
24 | 'react/prop-types': 'skipUndeclared',
25 | },
26 | }
27 |
--------------------------------------------------------------------------------
/src/pages/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Options - Save To Pocket
6 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/chips/chips.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Chips } from './chips'
3 |
4 | export default {
5 | title: 'Components/Chips',
6 | component: Chips,
7 | };
8 |
9 | export const Standard = () => {
10 | return (
11 | {}}
15 | />
16 | )
17 | }
18 |
19 | export const Marked = () => {
20 | return (
21 | {}}
25 | />
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/icons/svg/PocketLogo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default ({ children, ...rest }) => (
4 |
16 | )
17 |
--------------------------------------------------------------------------------
/src/common/api/saving/remove.js:
--------------------------------------------------------------------------------
1 | import { request } from '../_request/request'
2 |
3 | /* API CALLS - Should return promises
4 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
5 | export function removeItem(itemId) {
6 | return request({
7 | path: 'send/',
8 | data: {
9 | actions: [
10 | {
11 | action: 'delete',
12 | item_id: itemId,
13 | type: 'page'
14 | }
15 | ]
16 | }
17 | }).then(response => {
18 | return response
19 | ? { status: 'ok', response: response.action_results[0] }
20 | : undefined
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/.github/prlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": [
3 | {
4 | "pattern": "^(build|ci|docs|feat|fix|project|refactor|ui|style|test|chore).{1,}",
5 | "message": "Your title needs to be prefixed with on of the following types: build|ci|docs|feat|fix|project|refactor|ui|style|test|chore"
6 | }
7 | ],
8 | "body": [
9 | {
10 | "pattern": ".{1,}",
11 | "message": "You need literally anything in your description"
12 | }
13 | ],
14 | "head.ref": [
15 | {
16 | "pattern": "^(build|ci|docs|feat|fix|perf|refactor|style|test|refactor|project|release)/",
17 | "message": "Your branch name is invalid"
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/src/components/icons/svg/Error.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default ({ children, ...rest }) => (
4 |
19 | )
20 |
--------------------------------------------------------------------------------
/src/components/icons/svg/Facebook-Mono.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default ({ children, ...rest }) => (
4 |
18 | )
19 |
--------------------------------------------------------------------------------
/src/components/icons/svg/List-View.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default ({ children, ...rest }) => (
4 |
19 | )
20 |
--------------------------------------------------------------------------------
/src/common/api/saving/save.js:
--------------------------------------------------------------------------------
1 | import { request } from '../_request/request'
2 |
3 | /* API CALLS - Should return promises
4 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
5 | export function saveToPocket(saveObject) {
6 | return request({
7 | path: 'send/',
8 | data: {
9 | actions: [
10 | {
11 | action: 'add',
12 | url: saveObject.url,
13 | title: saveObject.title,
14 | ...saveObject.actionInfo,
15 | ...saveObject.additionalParams
16 | }
17 | ]
18 | }
19 | }).then(response => {
20 | return response
21 | ? { saveObject, status: 'ok', response: response.action_results[0] }
22 | : undefined
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/heading/heading.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Heading } from './heading'
3 |
4 | export default {
5 | title: 'Components/Heading',
6 | component: Heading,
7 | };
8 |
9 | export const Saving = () =>
10 | export const Saved = () =>
11 | export const Removing = () =>
12 | export const Removed = () =>
13 | export const RemoveFailed = () =>
14 | export const TagsSaving = () =>
15 | export const TagsSaved = () =>
16 | export const TagsSaveFailed = () =>
17 | export const TagsError = () =>
18 |
--------------------------------------------------------------------------------
/src/components/item-preview/item-preview.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ItemPreview as Preview } from './item-preview'
3 |
4 | export default {
5 | title: 'Components/ItemPreview',
6 | component: Preview,
7 | };
8 |
9 | const title = 'How to identify sketchy mushrooms'
10 | const publisher = 'New York Times'
11 | const thumbnail = 'http://placekitten.com/g/100/100'
12 | const longTitle = 'Kindling the energy hidden in matter Tunguska event Jean-François Champollion hydrogen atoms a still more glorious dawn awaits billions upon billions.'
13 |
14 | export const ItemPreview = () =>
15 | export const NoThumbnail = () =>
16 | export const LongTitle = () =>
17 |
--------------------------------------------------------------------------------
/src/components/icons/svg/Twitter-Mono.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default ({ children, ...rest }) => (
4 |
18 | )
19 |
--------------------------------------------------------------------------------
/src/components/doorhanger/doorhanger.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css, cx } from 'linaria'
3 |
4 | const doorhangerStyle = css`
5 | position: fixed;
6 | top: 0;
7 | right: 0;
8 | z-index: 2147483647; // highest z-index possible
9 | font-family: var(--fontSansSerif);
10 |
11 | .doorHanger {
12 | background-color: var(--color-canvas);
13 | border-radius: 30px;
14 | box-sizing: border-box;
15 | width: 393px;
16 | position: absolute;
17 | padding: 10px;
18 | top: 10px;
19 | right: 10px;
20 | transform: translateY(-150%);
21 | transition: all ease-in-out 250ms;
22 | box-shadow: 0px 0px 40px rgba(0, 0, 0, 0.25);
23 |
24 | &.open {
25 | transform: translateY(0);
26 | }
27 | }
28 | `
29 |
30 | export const Doorhanger = ({ isOpen, children }) => {
31 | return (
32 |
33 |
34 | { children }
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/connectors/item-preview/item-preview.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { ItemPreview } from 'components/item-preview/item-preview'
3 | import { UPDATE_ITEM_PREVIEW } from 'actions'
4 |
5 | export const ItemPreviewConnector = () => {
6 | const [itemPreview, setItemPreview] = useState({})
7 |
8 | /* Handle incoming messages
9 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
10 | const handleMessages = (event) => {
11 | const { payload, action = 'Unknown Action' } = event || {}
12 |
13 | switch (action) {
14 | case UPDATE_ITEM_PREVIEW: {
15 | const { item } = payload
16 | return setItemPreview(item)
17 | }
18 |
19 | default: {
20 | return
21 | }
22 | }
23 | }
24 |
25 | useEffect(() => {
26 | chrome.runtime.onMessage.addListener(handleMessages)
27 | return () => chrome.runtime.onMessage.removeListener(handleMessages)
28 | }, [])
29 |
30 | return (
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/common/api/saving/tags.js:
--------------------------------------------------------------------------------
1 | import { request } from '../_request/request'
2 |
3 | /* API CALLS - Should return promises
4 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
5 | export function getOnSaveTags(url) {
6 | return request({
7 | path: 'suggested_tags/',
8 | data: {
9 | url,
10 | version: 2
11 | }
12 | }).then(response => response)
13 | }
14 |
15 | export function syncItemTags(id, tags, actionInfo) {
16 | return request({
17 | path: 'send/',
18 | data: {
19 | actions: [{ action: 'tags_replace', item_id: id, tags, ...actionInfo }]
20 | }
21 | }).then(response => {
22 | return response
23 | ? { status: 'ok', response: response.action_results[0] }
24 | : undefined
25 | })
26 | }
27 |
28 | export function fetchStoredTags(since) {
29 | return request({
30 | path: 'get/',
31 | data: {
32 | tags: 1,
33 | taglist: 1,
34 | forcetaglist: 1,
35 | account: 1,
36 | since: since ? since : 0
37 | }
38 | }).then(response => response)
39 | }
40 |
--------------------------------------------------------------------------------
/config/jest.setup.test.js:
--------------------------------------------------------------------------------
1 | import { chrome as jestChrome } from 'jest-chrome'
2 |
3 | test('browser is defined in the global scope', () => {
4 | expect(browser).toBeDefined()
5 | expect(browser.runtime).toBeDefined()
6 | // This will be undefined if no mock implementation is provided
7 | expect(browser.runtime.sendMessage).toBeUndefined()
8 | })
9 |
10 | test('browser api methods are defined after implementation in chrome api', () => {
11 | // You need to add an implementation for each Chrome API method you use
12 | // Methods will be present in the Chrome API without implementations
13 | // but unused methods in the Browser API will be undefined
14 | jestChrome.runtime.sendMessage.mockImplementation((message, cb) => {
15 | cb({ greeting: 'test-response' })
16 | })
17 |
18 | expect(browser.runtime.sendMessage).toBeDefined()
19 | })
20 |
21 | test('chrome is mocked in the global scope', () => {
22 | expect(chrome).toBeDefined()
23 | expect(chrome.runtime).toBeDefined()
24 | expect(chrome.runtime.sendMessage).toBeDefined()
25 | })
26 |
--------------------------------------------------------------------------------
/src/common/api/auth/guid.js:
--------------------------------------------------------------------------------
1 | import { request } from '../_request/request'
2 | import { getSetting } from '../../interface'
3 |
4 | export async function getGuid() {
5 | const extensionGuid = await getExtensionGuid()
6 | if (extensionGuid) return extensionGuid
7 |
8 | const siteGuid = await getSiteGuid()
9 | if (siteGuid) return siteGuid
10 |
11 | const serverGuid = await getServerGuid()
12 | if (serverGuid) return serverGuid
13 |
14 | return false
15 | }
16 |
17 | export async function getServerGuid() {
18 | try {
19 | return await request({ path: 'guid', data: { abt: 1 } }).then(
20 | (data) => data.guid,
21 | )
22 | } catch (err) {
23 | console.info(err)
24 | }
25 | }
26 |
27 | export async function getExtensionGuid() {
28 | const guid = await getSetting('guid')
29 | return guid ? guid : false
30 | }
31 |
32 | export async function getSiteGuid() {
33 | const cookies = await chrome.cookies.get({
34 | url: 'https://getpocket.com/',
35 | name: 'sess_guid',
36 | })
37 |
38 | return cookies?.value
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2012-2021 Read It Later, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
5 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
6 | webpackFinal: async (config) => {
7 | const alias = {
8 | actions: path.join(__dirname, '../src/actions'),
9 | assets: path.join(__dirname, '../src/assets'),
10 | common: path.join(__dirname, '../src/common'),
11 | components: path.join(__dirname, '../src/components'),
12 | connectors: path.join(__dirname, '../src/connectors'),
13 | pages: path.join(__dirname, '../src/pages'),
14 | }
15 | config.resolve.alias = { ...config.resolve.alias, ...alias }
16 |
17 | // add support for Linaria preprocessing
18 | config.module.rules.push({
19 | test: /\.js$/,
20 | exclude: /node_modules/,
21 | use: [
22 | { loader: 'babel-loader' },
23 | {
24 | loader: 'linaria/loader',
25 | options: {
26 | sourceMap: process.env.NODE_ENV !== 'production'
27 | }
28 | }
29 | ]
30 | })
31 |
32 | // Return the altered config
33 | return config
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { globalVariables, globalReset } from 'pages/injector/globalStyles'
3 | import { css, cx } from 'linaria'
4 |
5 | const storyWrapperStyles = css`
6 | height: 100vh;
7 | width: 100vw;
8 | padding: 10px;
9 | box-sizing: border-box;
10 | font-family: var(--fontSansSerif);
11 | background-color: var(--color-canvas);
12 | `
13 |
14 | export const parameters = {
15 | actions: { argTypesRegex: "^on[A-Z].*" },
16 | controls: {
17 | matchers: {
18 | color: /(background|color)$/i,
19 | date: /Date$/
20 | }
21 | },
22 | layout: 'fullscreen'
23 | }
24 |
25 | export const globalTypes = {
26 | theme: {
27 | name: 'Theme',
28 | description: 'Global theme for components',
29 | defaultValue: 'light',
30 | toolbar: {
31 | icon: 'chromatic',
32 | // array of plain string values or MenuItem shape (see below)
33 | items: ['light', 'dark']
34 | }
35 | }
36 | }
37 |
38 | export const decorators = [
39 | (Story, context) => (
40 |
41 |
42 |
43 | )
44 | ]
45 |
--------------------------------------------------------------------------------
/src/components/icons/icons.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Icon from './icon'
4 | import Error from './svg/Error'
5 | import FacebookMono from './svg/Facebook-Mono'
6 | import Instagram from './svg/Instagram'
7 | import ListView from './svg/List-View'
8 | import PocketLogo from './svg/PocketLogo'
9 | import Settings from './svg/Settings'
10 | import TwitterMono from './svg/Twitter-Mono'
11 |
12 | export const ErrorIcon = (props) => (
13 |
14 |
15 |
16 | )
17 |
18 | export const FacebookIcon = (props) => (
19 |
20 |
21 |
22 | )
23 |
24 | export const InstagramIcon = (props) => (
25 |
26 |
27 |
28 | )
29 |
30 | export const ListViewIcon = (props) => (
31 |
32 |
33 |
34 | )
35 |
36 | export const PocketLogoIcon = (props) => (
37 |
38 |
39 |
40 | )
41 |
42 | export const SettingsIcon = (props) => (
43 |
44 |
45 |
46 | )
47 |
48 | export const TwitterIcon = (props) => (
49 |
50 |
51 |
52 | )
53 |
--------------------------------------------------------------------------------
/src/pages/injector/content.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { App } from './app'
4 | import { SAVE_TO_POCKET_REQUEST } from 'actions'
5 |
6 | /* Initial Setup
7 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
8 | const initialize = function () {
9 | chrome.runtime.onMessage.addListener(function (request) {
10 | const { action } = request
11 | if (action === SAVE_TO_POCKET_REQUEST) injectDomElements()
12 | })
13 | }
14 |
15 | /* Inject content into the DOM
16 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
17 | export function injectDomElements() {
18 | const existingRoot = document.getElementById('pocket-extension-root')
19 | if (existingRoot) return
20 |
21 | const rootElement = document.createElement('div')
22 | rootElement.id = 'pocket-extension-root'
23 | const root = document.body.appendChild(rootElement)
24 |
25 | ReactDOM.render(, root)
26 | }
27 |
28 | //eslint-disable-next-line
29 | ;(function () {
30 | if (window.top === window) {
31 | const setLoaded = () => initialize()
32 |
33 | // Check page has loaded and if not add listener for it
34 | if (document.readyState === 'loading') {
35 | document.addEventListener('DOMContentLoaded', setLoaded)
36 | } else {
37 | setLoaded()
38 | }
39 | }
40 | })()
41 |
--------------------------------------------------------------------------------
/src/components/icons/svg/Settings.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default ({ children, ...rest }) => (
4 |
21 | )
22 |
--------------------------------------------------------------------------------
/src/components/item-preview/item-preview.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css, cx } from 'linaria'
3 |
4 | const previewWrapper = css`
5 | &.item-preview {
6 | display: flex;
7 | background: var(--color-itemPreviewBackground);
8 | padding: 10px;
9 | border-radius: 4px;
10 | margin-top: 10px;
11 | }
12 |
13 | img {
14 | width: 40px;
15 | height: 40px;
16 | margin-right: 15px;
17 | border-radius: 4px;
18 | }
19 |
20 | div {
21 | text-align: left;
22 | }
23 |
24 | h3 {
25 | margin: 0 0 10px;
26 | font-size: 16px;
27 | font-weight: 600;
28 | font-family: var(--fontSansSerif);
29 | line-height: 20px;
30 | color: var(--color-textPrimary);
31 | display: -webkit-box;
32 | -webkit-line-clamp: 3;
33 | -webkit-box-orient: vertical;
34 | overflow: hidden;
35 | }
36 |
37 | p {
38 | margin: 0;
39 | padding: 0;
40 | font-size: 14px;
41 | font-family: var(--fontSansSerif);
42 | color: var(--color-textPrimary);
43 | }
44 | `
45 |
46 | export const ItemPreview = ({ title, thumbnail, publisher }) => {
47 | return (title && publisher) ? (
48 |
49 | {thumbnail ?

: null}
50 |
51 |
{title}
52 |
{publisher}
53 |
54 |
55 | ) : null
56 | }
57 |
--------------------------------------------------------------------------------
/src/actions.js:
--------------------------------------------------------------------------------
1 | export const LOGGED_OUT_OF_POCKET = 'LOGGED_OUT_OF_POCKET'
2 | export const OPEN_POCKET = 'OPEN_POCKET'
3 | export const AUTH_CODE_RECEIVED = 'AUTH_CODE_RECEIVED'
4 | export const COLOR_MODE_CHANGE = 'COLOR_MODE_CHANGE'
5 | export const OPEN_OPTIONS = 'OPEN_OPTIONS'
6 |
7 | export const SAVE_TO_POCKET_REQUEST = 'SAVE_TO_POCKET_REQUEST'
8 | export const SAVE_TO_POCKET_SUCCESS = 'SAVE_TO_POCKET_SUCCESS'
9 | export const SAVE_TO_POCKET_FAILURE = 'SAVE_TO_POCKET_FAILURE'
10 |
11 | export const RESAVE_ITEM = 'RESAVE_ITEM'
12 |
13 | export const UPDATE_ITEM_PREVIEW = 'UPDATE_ITEM_PREVIEW'
14 |
15 | export const REMOVE_ITEM = 'REMOVE_ITEM'
16 | export const REMOVE_ITEM_REQUEST = 'REMOVE_ITEM_REQUEST'
17 | export const REMOVE_ITEM_SUCCESS = 'REMOVE_ITEM_SUCCESS'
18 | export const REMOVE_ITEM_FAILURE = 'REMOVE_ITEM_FAILURE'
19 |
20 | export const SUGGESTED_TAGS_SUCCESS = 'SUGGESTED_TAGS_SUCCESS'
21 | export const SUGGESTED_TAGS_FAILURE = 'SUGGESTED_TAGS_FAILURE'
22 |
23 | export const TAGS_SYNC = 'TAGS_SYNC'
24 | export const TAG_SYNC_REQUEST = 'TAG_SYNC_REQUEST'
25 | export const TAG_SYNC_SUCCESS = 'TAG_SYNC_SUCCESS'
26 | export const TAG_SYNC_FAILURE = 'TAG_SYNC_FAILURE'
27 |
28 | export const UPDATE_STORED_TAGS = 'UPDATE_STORED_TAGS'
29 |
30 | export const SEND_TAG_ERROR = 'SEND_TAG_ERROR'
31 | export const UPDATE_TAG_ERROR = 'UPDATE_TAG_ERROR'
32 |
33 | export const USER_LOG_IN = 'USER_LOG_IN'
34 |
--------------------------------------------------------------------------------
/src/connectors/heading/heading.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { Heading } from 'components/heading/heading'
3 |
4 | import { REMOVE_ITEM } from 'actions'
5 | import { RESAVE_ITEM } from 'actions'
6 | import { UPDATE_ITEM_PREVIEW } from 'actions'
7 |
8 | export const HeadingConnector = ({ saveStatus }) => {
9 | const [itemId, setItemId] = useState(null)
10 |
11 | /* Handle incoming messages
12 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
13 | const handleMessages = (event) => {
14 | const { payload, action = 'Unknown Action' } = event || {}
15 |
16 | switch (action) {
17 | case UPDATE_ITEM_PREVIEW: {
18 | const { item } = payload
19 | setItemId(item?.itemId)
20 | return
21 | }
22 |
23 | default: {
24 | return
25 | }
26 | }
27 | }
28 |
29 | useEffect(() => {
30 | chrome.runtime.onMessage.addListener(handleMessages)
31 | return () => chrome.runtime.onMessage.removeListener(handleMessages)
32 | }, [])
33 |
34 | const removeAction = () => {
35 | chrome.runtime.sendMessage({
36 | type: REMOVE_ITEM,
37 | payload: { itemId }
38 | })
39 | }
40 |
41 | const saveAction = () => {
42 | chrome.runtime.sendMessage({
43 | type: RESAVE_ITEM
44 | })
45 | }
46 |
47 | return (
48 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/icons/icons.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css } from 'linaria'
3 |
4 | import IconHOC from './icon'
5 | import * as icons from './icons'
6 |
7 | export default {
8 | title: 'Components/Icons',
9 | component: IconHOC,
10 | }
11 |
12 | const Label = css`
13 | display: block;
14 | width: 100%;
15 | font-family: 'Courier New', Courier, monospace;
16 | font-size: 14px;
17 | line-height: 24px;
18 | padding: 0.5rem 1rem;
19 | margin: 0;
20 | border-radius: var(--borderRadius);
21 | &:hover {
22 | background-color: var(--color-actionPrimarySubdued);
23 | }
24 | `
25 |
26 | const Grid = css`
27 | display: grid;
28 | grid-template-columns: repeat(4, 4fr);
29 | grid-row-gap: 1em;
30 | grid-column-gap: 1em;
31 | justify-content: space-between;
32 | justify-items: center;
33 | margin: 0 0 2em 0;
34 |
35 | & > div {
36 | width: 100%;
37 | display: flex;
38 | align-items: flex-start;
39 | flex-direction: column;
40 | }
41 | `
42 |
43 | const iconStyle = css`
44 | font-size: 2em;
45 | `
46 |
47 | const iconKeysInAlphaOrder = Object.keys(icons).sort()
48 |
49 | export const Icons = () => {
50 | const iconComponents = iconKeysInAlphaOrder.map((iconKey) => {
51 | const Icon = icons[iconKey]
52 |
53 | return (
54 |
55 |
56 | {iconKey}
57 |
58 |
59 | )
60 | })
61 |
62 | return {iconComponents}
63 | }
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | #debug file
4 | *debug
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # testing
10 | coverage/
11 |
12 | # production
13 | _build/
14 | build
15 | releases
16 |
17 | # misc
18 | .DS_Store
19 | .env.local
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 |
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # Account info
29 | keys.js
30 | keys.json
31 | keys.xcconfig
32 |
33 | *-debug
34 |
35 | # ignore npm package lock, using yarn for CI for consistency
36 | package-lock.json
37 |
38 | # Created by https://www.gitignore.io/api/xcode
39 | # Edit at https://www.gitignore.io/?templates=xcode
40 |
41 | ### Xcode ###
42 | # Xcode
43 | #
44 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
45 |
46 | ## Xcode Patch
47 | *.xcodeproj/*
48 | !*.xcodeproj/project.pbxproj
49 | !*.xcodeproj/xcshareddata/
50 | !*.xcworkspace/contents.xcworkspacedata
51 | /*.gcno
52 |
53 | ### Xcode Patch ###
54 | **/xcshareddata/WorkspaceSettings.xcsettings
55 |
56 | # End of https://www.gitignore.io/api/xcode
57 |
58 | # Ignore Xcode user-related files
59 | xcuserdata/
60 | *.xcuserdatad
61 | *.developerprofile
62 |
63 | # Ignore AppCode user-related files
64 | .idea/
65 | .vscode/
66 |
67 | # Ignore CocoaPods installed files
68 | Pods/
69 |
70 | # Ignore Carthage installed files
71 | Carthage/
72 |
73 | # vsCode
74 | .vscode/
75 |
76 | # linaria
77 | .linaria-cache/
78 |
--------------------------------------------------------------------------------
/src/components/footer/footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css, cx } from 'linaria'
3 | import { SettingsIcon } from 'components/icons/icons'
4 | import { ListViewIcon } from 'components/icons/icons'
5 | import { Button } from 'components/button/extensions-button'
6 |
7 | const footerWrapper = css`
8 | .pocket-extension &.footer {
9 | background-color: var(--color-canvas);
10 | display: flex;
11 | justify-content: space-between;
12 | border-top: 1px solid var(--color-dividerTertiary);
13 | margin: 10px 0 0 0;
14 | padding: 18px 0 8px;
15 | }
16 |
17 | .icon {
18 | display: inline-block;
19 | height: 25px;
20 | width: 25px;
21 | margin: 0 8px 0 0;
22 | border: none;
23 | background-color: transparent;
24 | pointer-events: none;
25 | vertical-align: middle;
26 |
27 | svg {
28 | height: 25px;
29 | width: 25px;
30 | }
31 | }
32 |
33 | .buttonLink {
34 | color: var(--color-textPrimary);
35 |
36 | &:hover {
37 | color: var(--color-actionPrimary);
38 | }
39 | }
40 | `
41 |
42 | export const Footer = ({ myListClick, settingsClick }) => {
43 | return (
44 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "Save to Pocket",
4 | "default_locale": "en",
5 | "icons": {
6 | "16": "assets/images/icon-16.png",
7 | "48": "assets/images/icon-48.png",
8 | "128": "assets/images/icon-128.png"
9 | },
10 | "action": {
11 | "default_icon": {
12 | "38": "assets/images/action-icon.png"
13 | },
14 | "default_title": "Save to Pocket"
15 | },
16 | "background": {
17 | "service_worker": "pages/background/index.js"
18 | },
19 | "options_page": "pages/options/options.html",
20 | "content_scripts": [
21 | {
22 | "matches": [
23 | "*://*/*"
24 | ],
25 | "js": [
26 | "pages/injector/content.js"
27 | ],
28 | "css": [
29 | "assets/fonts/fonts.css",
30 | "assets/pocket-save-extension.css"
31 | ]
32 | },
33 | {
34 | "matches": [
35 | "*://getpocket.com/extension_login_success*"
36 | ],
37 | "js": [
38 | "pages/login.js"
39 | ]
40 | },
41 | {
42 | "matches": [
43 | "*://getpocket.com/login?e=4"
44 | ],
45 | "js": [
46 | "pages/logout.js"
47 | ]
48 | }
49 | ],
50 | "host_permissions": [
51 | "*://getpocket.com/*"
52 | ],
53 | "permissions": [
54 | "tabs",
55 | "contextMenus",
56 | "cookies",
57 | "storage"
58 | ],
59 | "commands": {
60 | "save-to-pocket-action": {
61 | "suggested_key": {
62 | "default": "Ctrl+Shift+P",
63 | "windows": "Ctrl+Shift+P",
64 | "mac": "Command+Shift+P",
65 | "chromeos": "Ctrl+Shift+P",
66 | "linux": "Ctrl+Shift+P"
67 | },
68 | "description": "Save page to Pocket"
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/common/api/_request/request.js:
--------------------------------------------------------------------------------
1 | import { CONSUMER_KEY, API_URL } from 'common/constants'
2 | import { Base64 } from 'js-base64'
3 | import { getSetting } from '../../interface'
4 | import { getAccessToken } from '../../helpers'
5 |
6 | /* Helper Functions
7 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
8 |
9 | async function request(options, skipAuth) {
10 | if (!CONSUMER_KEY) throw new Error('Invalid Auth Key')
11 | if (!skipAuth) options.data.access_token = await getAccessToken()
12 |
13 | options.data.consumer_key = CONSUMER_KEY
14 |
15 | const headers = new Headers({
16 | 'X-Accept': 'application/json',
17 | 'Content-Type': 'application/json',
18 | })
19 |
20 | //?? Is there any way to access this anymore since we no longer use cookie/local storage
21 | //?? We never set this parameter anywhere; propose we delete this block
22 | const serverAuth = await getSetting('base_server_auth')
23 | if (serverAuth) {
24 | headers.append('Authorization', 'Basic ' + Base64.encode(serverAuth))
25 | }
26 |
27 | const fetchSettings = {
28 | method: 'POST',
29 | headers: headers,
30 | body: JSON.stringify(options.data),
31 | }
32 |
33 | return fetch(API_URL + options.path, fetchSettings)
34 | .then(handleErrors)
35 | .then(handleSuccess)
36 | }
37 |
38 | function handleErrors(response) {
39 | const xErrorCode = response.headers.get('x-error-code')
40 | const xError = response.headers.get('x-error')
41 |
42 | // We can reject with the error code and message for better handling
43 | if (!response.ok) return Promise.reject({ xErrorCode, xError })
44 |
45 | return response
46 | }
47 |
48 | function handleSuccess(response) {
49 | return response ? response.json() : false
50 | }
51 |
52 | export { request }
53 |
--------------------------------------------------------------------------------
/src/pages/login.js:
--------------------------------------------------------------------------------
1 | import { sendMessage } from 'common/interface'
2 | import { AUTH_CODE_RECEIVED } from 'actions'
3 |
4 | // Check page has loaded and if not add listener for it
5 | if (document.readyState === 'loading') {
6 | document.addEventListener('DOMContentLoaded', setLoginLoaded)
7 | } else {
8 | setLoginLoaded()
9 | }
10 |
11 | async function setLoginLoaded() {
12 | try {
13 | const siteCookies = getCookies(document.cookie)
14 |
15 | if (!siteCookies['sess_user_id'] || !siteCookies['sess_exttok']) {
16 | console.groupCollapsed('Auth Error')
17 | console.log({
18 | userId: siteCookies['sess_user_id'],
19 | token: siteCookies['sess_exttok']
20 | })
21 | console.groupEnd('Auth Error')
22 | }
23 |
24 | const loginMessage = {
25 | userId: siteCookies['sess_user_id'],
26 | token: siteCookies['sess_exttok'],
27 | }
28 |
29 | // This time out is for user experience so they don't get a flash of
30 | // a page with no context, since we close this page after getting this data
31 | setTimeout(function () {
32 | sendMessage({
33 | type: AUTH_CODE_RECEIVED,
34 | payload: loginMessage,
35 | })
36 | }, 1500)
37 | } catch (err) {
38 | console.log('Unexpected login error', err)
39 | }
40 | }
41 |
42 | /* UTILITIES
43 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
44 | function getCookies(cookieString) {
45 | if (!cookieString || cookieString === '') return {}
46 | return cookieString
47 | .split(';')
48 | .map((x) => x.trim().split(/(=)/))
49 | .reduce(
50 | (cookiesObject, currentArray) => ({
51 | ...cookiesObject,
52 | [currentArray[0]]: decodeURIComponent(currentArray[2]),
53 | }),
54 | {},
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/tagging/tagging.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Tagging as TaggingComponent } from './tagging'
3 |
4 | export default {
5 | title: 'Components/Tagging',
6 | };
7 |
8 | const usedTags = ['test', 'space']
9 | const markedTags = ['space']
10 | const suggestedTags = ['pink elephants', 'lava lamps']
11 | const storedTags = ['jamba','juice','donut','time','perturbedly','uxoriousness','chromogenic','creasiest','dartingness','rippingly','glabellar','auckland','wyoming','luanda','prebesetting','member','watershed','grooveless','inept','balletomane','desdemona','isodimorphous','fishybacking','vip']
12 | const addTag = () => {}
13 | const activateTag = () => {}
14 | const deactivateTag = () => {}
15 | const deactivateTags = () => {}
16 | const removeTag = () => {}
17 | const removeTags = () => {}
18 | const closePanel = () => {}
19 |
20 | export const Tagging = () => {
21 | return (
22 |
35 | )
36 | }
37 |
38 | export const TaggingEmpty = () => {
39 | return (
40 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/icons/svg/Instagram.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default ({ children, ...rest }) => (
4 |
26 | )
27 |
--------------------------------------------------------------------------------
/src/components/chips/chips.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { css, cx } from 'linaria'
4 |
5 | const chipList = css`
6 | &.chip-list {
7 | display: inline;
8 | list-style-type: none;
9 | margin: 0;
10 | padding: 0;
11 | }
12 | `
13 | const chipItem = css`
14 | &.chip-item {
15 | background-color: var(--color-calloutBackgroundPrimary);
16 | border: 1px solid var(--color-calloutBackgroundPrimary);
17 | border-radius: 50px;
18 | color: var(--color-chipsText);
19 | cursor: pointer;
20 | display: inline-block;
21 | font-size: 14px;
22 | font-family: var(--fontSansSerif);
23 | line-height: 16px;
24 | margin-right: 9px;
25 | padding: 8px;
26 | text-align: center;
27 | text-transform: lowercase;
28 | transform: translateZ(0.1);
29 | }
30 |
31 | span {
32 | color: var(--color-chipsText);
33 | margin-left: 10px;
34 | }
35 |
36 | &.active, &:hover {
37 | border: 1px solid var(--color-chipsActive);
38 | }
39 | `
40 |
41 | export const Chips = ({ removeTag, tags, marked, toggleActive }) => {
42 | const listItems = () => {
43 | return tags.map((chip, index) => {
44 | const active = marked.includes(chip)
45 | const handleToggle = (e) => toggleActive(chip, active)
46 | const handleRemove = (e) => {
47 | e.stopPropagation()
48 | removeTag(chip)
49 | }
50 |
51 | return (
52 | event.preventDefault()}
57 | onClick={handleToggle}>
58 | {chip}
59 | {active ? (
60 | ×
61 | ) : null}
62 |
63 | )
64 | })
65 | }
66 |
67 | return (
68 |
69 | )
70 | }
71 |
72 | Chips.propTypes = {
73 | tags: PropTypes.array,
74 | marked: PropTypes.array,
75 | toggleActive: PropTypes.func,
76 | removeTag: PropTypes.func
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/tagging/suggestions/suggestions.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { css, cx } from 'linaria'
4 |
5 | const suggestionsWrapper = css`
6 | &.suggestions {
7 | box-sizing: border-box;
8 | display: block;
9 | font-size: 14px;
10 | line-height: 20px;
11 | text-align: left;
12 | width: 100%;
13 | }
14 |
15 | ul {
16 | display: flex;
17 | flex-wrap: wrap;
18 | list-style-type: none;
19 | margin: 0;
20 | padding: 10px 0 0;
21 | text-align: left;
22 | }
23 |
24 | li {
25 | background-color: var(--color-calloutBackgroundPrimary);
26 | border: 1px solid var(--color-calloutBackgroundPrimary);
27 | border-radius: 50px;
28 | color: var(--color-chipsText);
29 | cursor: pointer;
30 | display: inline-block;
31 | font-size: 14px;
32 | font-family: var(--fontSansSerif);
33 | line-height: 16px;
34 | margin-bottom: 4px;
35 | margin-right: 4px;
36 | padding: 8px;
37 | text-align: center;
38 | text-transform: lowercase;
39 | transform: translateZ(0.1);
40 |
41 | &:hover {
42 | border: 1px solid var(--color-chipsActive);
43 | }
44 | }
45 | `
46 |
47 | const SuggestionItem = ({ suggestion, addTag, used }) => {
48 | const prevent = (e) => e.preventDefault()
49 |
50 | const handleClick = (e) => {
51 | e.stopPropagation()
52 | addTag(suggestion)
53 | }
54 |
55 | return !used.includes(suggestion) ? (
56 |
60 | {suggestion}
61 |
62 | ) : null
63 | }
64 |
65 | export const Suggestions = ({ addTag, suggestions, used }) => {
66 | return (
67 |
68 |
69 | {suggestions.map((suggestion, index) => (
70 |
76 | ))}
77 |
78 |
79 | )
80 | }
81 |
82 | Suggestions.propTypes = {
83 | suggestions: PropTypes.array,
84 | addTag: PropTypes.func,
85 | used: PropTypes.array
86 | }
87 |
--------------------------------------------------------------------------------
/src/common/_mocks/tags.js:
--------------------------------------------------------------------------------
1 | export const CHEMICAL_ELEMENTS = [
2 | 'Hydrogen',
3 | 'Helium',
4 | 'Lithium',
5 | 'Beryllium',
6 | 'Boron',
7 | 'Carbon',
8 | 'Nitrogen',
9 | 'Oxygen',
10 | 'Fluorine',
11 | 'Neon',
12 | 'Sodium',
13 | 'Magnesium',
14 | 'Aluminum',
15 | 'Silicon',
16 | 'Phosphorus',
17 | 'Sulfur',
18 | 'Chlorine',
19 | 'Argon',
20 | 'Potassium',
21 | 'Calcium',
22 | 'Scandium',
23 | 'Titanium',
24 | 'Vanadium',
25 | 'Chromium',
26 | 'Manganese',
27 | 'Iron',
28 | 'Cobalt',
29 | 'Nickel',
30 | 'Copper',
31 | 'Zinc',
32 | 'Gallium',
33 | 'Germanium',
34 | 'Arsenic',
35 | 'Selenium',
36 | 'Bromine',
37 | 'Krypton',
38 | 'Rubidium',
39 | 'Strontium',
40 | 'Yttrium',
41 | 'Zirconium',
42 | 'Niobium',
43 | 'Molybdenum',
44 | 'Technetium',
45 | 'Ruthenium',
46 | 'Rhodium',
47 | 'Palladium',
48 | 'Silver',
49 | 'Cadmium',
50 | 'Indium',
51 | 'Tin',
52 | 'Antimony',
53 | 'Tellurium',
54 | 'Iodine',
55 | 'Xenon',
56 | 'Cesium',
57 | 'Barium',
58 | 'Lanthanum',
59 | 'Cerium',
60 | 'Praseodymium',
61 | 'Neodymium',
62 | 'Promethium',
63 | 'Samarium',
64 | 'Europium',
65 | 'Gadolinium',
66 | 'Terbium',
67 | 'Dysprosium',
68 | 'Holmium',
69 | 'Erbium',
70 | 'Thulium',
71 | 'Ytterbium',
72 | 'Lutetium',
73 | 'Hafnium',
74 | 'Tantalum',
75 | 'Tungsten',
76 | 'Rhenium',
77 | 'Osmium',
78 | 'Iridium',
79 | 'Platinum',
80 | 'Gold',
81 | 'Mercury',
82 | 'Thallium',
83 | 'Lead',
84 | 'Bismuth',
85 | 'Polonium',
86 | 'Astatine',
87 | 'Radon',
88 | 'Francium',
89 | 'Radium',
90 | 'Actinium',
91 | 'Thorium',
92 | 'Protactinium',
93 | 'Uranium',
94 | 'Neptunium',
95 | 'Plutonium',
96 | 'Americium',
97 | 'Curium',
98 | 'Berkelium',
99 | 'Californium',
100 | 'Einsteinium',
101 | 'Fermium',
102 | 'Mendelevium',
103 | 'Nobelium',
104 | 'Lawrencium',
105 | 'Rutherfordium',
106 | 'Dubnium',
107 | 'Seaborgium',
108 | 'Bohrium',
109 | 'Hassium',
110 | 'Meitnerium',
111 | 'Darmstadtium',
112 | 'Roentgenium',
113 | 'Ununbium',
114 | 'Ununtrium',
115 | 'Ununquadium',
116 | 'Ununpentium',
117 | 'Ununhexium',
118 | 'Ununseptium',
119 | 'Ununoctium'
120 | ]
121 |
--------------------------------------------------------------------------------
/src/components/loading/loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css, cx } from 'linaria'
3 |
4 | const loadingWrapper = css`
5 | display: inline-flex;
6 | justify-content: center;
7 |
8 | svg {
9 | display: block;
10 | width: 6px;
11 | height: 6px;
12 | margin: 0 1.5px;
13 | fill: currentColor;
14 | }
15 | .shape {
16 | display: inline-block;
17 | animation: float 1.6s infinite cubic-bezier(0.44, 0.15, 0.59, 0.89) both;
18 | }
19 | .round {
20 | svg {
21 | fill: var(--color-actionBrand);
22 | }
23 | animation-delay: -0.84s;
24 | }
25 | .point {
26 | svg {
27 | transform-origin: 4px 5px;
28 | width: 8px;
29 | height: 8px;
30 | margin-top: -1px;
31 | fill: var(--color-amber);
32 | animation: spinPoint 1.6s infinite ease-in-out forwards;
33 | }
34 | animation-delay: -0.42s;
35 | }
36 | .block {
37 | svg {
38 | margin-bottom: 1px;
39 | animation: spinBlock 1.6s infinite ease-in-out forwards;
40 | fill: #116a65;
41 | }
42 | }
43 |
44 | @keyframes float {
45 | 0%,
46 | 70%,
47 | 100% {
48 | transform: translateY(0);
49 | }
50 | 35% {
51 | transform: translateY(-10px);
52 | }
53 | }
54 |
55 | @keyframes spinPoint {
56 | 0% {
57 | transform: rotate(0deg);
58 | }
59 | 40% {
60 | transform: rotate(120deg);
61 | }
62 | 100% {
63 | transform: rotate(120deg);
64 | }
65 | }
66 |
67 | @keyframes spinBlock {
68 | 0% {
69 | transform: rotate(0deg);
70 | }
71 | 60% {
72 | transform: rotate(90deg);
73 | }
74 | 100% {
75 | transform: rotate(90deg);
76 | }
77 | }
78 | `
79 |
80 | export const Loading = ({ className }) => {
81 | return (
82 |
99 | )
100 | }
101 |
--------------------------------------------------------------------------------
/src/common/locales.js:
--------------------------------------------------------------------------------
1 | import de from '_locales/de/messages.json'
2 | import en from '_locales/en/messages.json'
3 | import es from '_locales/es/messages.json'
4 | import es_419 from '_locales/es_419/messages.json'
5 | import fr from '_locales/fr/messages.json'
6 | import it from '_locales/it/messages.json'
7 | import ja from '_locales/ja/messages.json'
8 | import ko from '_locales/ko/messages.json'
9 | import nl from '_locales/nl/messages.json'
10 | import pl from '_locales/pl/messages.json'
11 | import pt_BR from '_locales/pt_BR/messages.json'
12 | import pt_PT from '_locales/pt_PT/messages.json'
13 | import ru from '_locales/ru/messages.json'
14 | import zh_CN from '_locales/zh_CN/messages.json'
15 | import zh_TW from '_locales/zh_TW/messages.json'
16 |
17 | function getCurrentLanguageCode() {
18 | var language = navigator.languages
19 | ? navigator.languages[0]
20 | : navigator.language || navigator.userLanguage
21 |
22 | language = typeof language !== 'undefined' ? language.toLowerCase() : 'en'
23 |
24 | if (language.indexOf('en') === 0) return 'en' // English
25 | if (language.indexOf('de') === 0) return 'de' // German
26 | if (language.indexOf('fr') === 0) return 'fr' // French
27 | if (language.indexOf('it') === 0) return 'it' // Italian
28 | if (language.indexOf('es_419') === 0) return 'es_419' // Spanish (Latin America and Caribbean)
29 | if (language.indexOf('es') === 0) return 'es' // Spanish
30 | if (language.indexOf('ja') === 0) return 'ja' // Japanese
31 | if (language.indexOf('ru') === 0) return 'ru' // Russian
32 | if (language.indexOf('ko') === 0) return 'ko' // Korean
33 | if (language.indexOf('nl') === 0) return 'nl' // Dutch
34 | if (language.indexOf('pl') === 0) return 'pl' // Polish
35 | if (language.indexOf('pt_BR') === 0) return 'pt_BR' // Portuguese Brazil
36 | if (language.indexOf('pt_PT') === 0) return 'pt_PT' // Portuguese Portugal
37 | if (language.indexOf('zh_CN') === 0) return 'zh_CN' // Chinese Simplified
38 | if (language.indexOf('zh_TW') === 0) return 'zh_TW' // Chinese Traditional
39 | return 'en' // Default is English
40 | }
41 |
42 | function localizedStrings() {
43 | const localizedCopy = {
44 | de,
45 | en,
46 | es,
47 | es_419,
48 | fr,
49 | it,
50 | ja,
51 | ko,
52 | nl,
53 | pl,
54 | pt_BR,
55 | pt_PT,
56 | ru,
57 | zh_CN,
58 | zh_TW
59 | }
60 |
61 | const currentLanguage = getCurrentLanguageCode()
62 | return localizedCopy[currentLanguage] || localizedCopy['en']
63 | }
64 |
65 | const currentStrings = localizedStrings()
66 |
67 | export function localize(string) {
68 | return currentStrings[string]?.message
69 | }
70 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "save-to-pocket",
3 | "version": "4.0.6",
4 | "private": true,
5 | "description": "The easiest, fastest way to capture articles, videos, and more.",
6 | "homepage": "https://github.com/Pocket/extension-save-to-pocket#readme",
7 | "bugs": {
8 | "url": "https://github.com/Pocket/extension-save-to-pocketissues"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/Pocket/extension-save-to-pocket.git"
13 | },
14 | "license": "MIT",
15 | "author": "Pocket",
16 | "scripts": {
17 | "build": "NODE_ENV=production rollup -c",
18 | "release": "cross-env NODE_ENV=production IS_RELEASE=true rollup -c",
19 | "start": "rollup -c -w",
20 | "test": "jest",
21 | "storybook": "start-storybook -p 6006",
22 | "build-storybook": "build-storybook"
23 | },
24 | "dependencies": {
25 | "@sentry/react": "^6.16.1",
26 | "@sentry/tracing": "^6.16.1",
27 | "downshift": "^6.1.7",
28 | "js-base64": "^3.7.2",
29 | "linaria": "^3.0.0-beta.13",
30 | "match-sorter": "^6.3.1",
31 | "prop-types": "^15.7.2",
32 | "react": "^17.0.2",
33 | "react-dom": "^17.0.2",
34 | "react-input-autosize": "^3.0.0",
35 | "webextension-polyfill": "^0.8.0"
36 | },
37 | "devDependencies": {
38 | "@babel/cli": "^7.15.7",
39 | "@babel/core": "^7.15.5",
40 | "@babel/preset-env": "^7.15.6",
41 | "@babel/preset-react": "^7.14.5",
42 | "@rollup/plugin-alias": "^3.1.5",
43 | "@rollup/plugin-babel": "^5.3.0",
44 | "@rollup/plugin-commonjs": "^20.0.0",
45 | "@rollup/plugin-html": "^0.2.3",
46 | "@rollup/plugin-json": "^4.1.0",
47 | "@rollup/plugin-node-resolve": "^13.0.5",
48 | "@rollup/plugin-replace": "^3.0.0",
49 | "@storybook/addon-actions": "^6.3.8",
50 | "@storybook/addon-essentials": "^6.3.8",
51 | "@storybook/addon-links": "^6.3.8",
52 | "@storybook/react": "^6.3.8",
53 | "@types/chrome": "^0.0.158",
54 | "@types/firefox-webext-browser": "^82.0.1",
55 | "@types/jest": "^27.0.2",
56 | "@types/react": "^17.0.24",
57 | "@types/react-dom": "^17.0.9",
58 | "babel-loader": "^8.2.2",
59 | "babel-plugin-module-resolver": "^4.1.0",
60 | "cross-env": "^7.0.3",
61 | "eslint": "^7.32.0",
62 | "eslint-plugin-react": "^7.26.0",
63 | "jest": "^27.2.1",
64 | "jest-chrome": "^0.7.2",
65 | "prettier": "^2.4.1",
66 | "rollup": "^2.57.0",
67 | "rollup-plugin-chrome-extension": "^3.6.4",
68 | "rollup-plugin-copy": "^3.4.0",
69 | "rollup-plugin-css-only": "^3.1.0",
70 | "rollup-plugin-empty-dir": "^1.0.5",
71 | "rollup-plugin-zip": "^1.0.3",
72 | "styled-jsx": "^4.0.1"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/button/extensions-button.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css, cx } from 'linaria'
3 |
4 | const buttonStyles = css`
5 | &.pocket-button {
6 | height: unset;
7 | display: inline-block;
8 | position: relative;
9 | font-family: var(--fontSansSerif);
10 | font-size: 16px;
11 | line-height: 110%;
12 | border: none;
13 | border-radius: 0.25rem;
14 | margin: 0;
15 | padding: 8px 12px;
16 | transition: all 0.15s ease-out;
17 | text-decoration: none;
18 | text-transform: none;
19 | cursor: pointer;
20 | }
21 |
22 | &.disabled {
23 | pointer-events: none;
24 | cursor: default;
25 | opacity: 0.5;
26 | }
27 |
28 | &:focus {
29 | outline: none;
30 |
31 | &::before {
32 | content: '';
33 | position: absolute;
34 | border: 2px solid var(--color-actionFocus);
35 | top: -4px;
36 | bottom: -4px;
37 | left: -4px;
38 | right: -4px;
39 | border-radius: 0.5rem;
40 | }
41 | }
42 |
43 | &:hover {
44 | text-decoration: none;
45 |
46 | &::before {
47 | display: none;
48 | }
49 | }
50 |
51 | &:active {
52 | &::before {
53 | display: none;
54 | }
55 | }
56 |
57 | &.primary {
58 | background-color: var(--color-actionPrimary);
59 | border: 2px solid var(--color-actionPrimary);
60 | color: var(--color-actionPrimaryText);
61 |
62 | &:hover {
63 | background-color: var(--color-actionPrimaryHover);
64 | border-color: var(--color-actionPrimaryHover);
65 | }
66 | }
67 |
68 | &.secondary {
69 | background: none;
70 | border: 2px solid var(--color-actionSecondary);
71 | color: var(--color-actionSecondaryText);
72 |
73 | &:focus {
74 | &::before {
75 | /* offsets adjusted for space taken by outline */
76 | top: -6px;
77 | bottom: -6px;
78 | left: -6px;
79 | right: -6px;
80 | }
81 | }
82 | &:hover {
83 | background-color: var(--color-actionSecondaryHover);
84 | color: var(--color-actionSecondaryHoverText);
85 | }
86 | }
87 |
88 | &.inline {
89 | display: inline;
90 | background: none;
91 | padding: 0;
92 | color: var(--color-inlineButton);
93 | font-size: 14px;
94 | font-weight: 500;
95 |
96 | &:focus {
97 | outline: inherit;
98 |
99 | &::before {
100 | display: none;
101 | }
102 | }
103 |
104 | &:hover {
105 | color: var(--color-inlineButtonHover);
106 | }
107 | }
108 | `
109 |
110 | export const Button = ({children, onClick, type = 'primary', className}) => {
111 | return (
112 |
115 | )
116 | }
117 |
--------------------------------------------------------------------------------
/src/pages/background/postSave.js:
--------------------------------------------------------------------------------
1 | import { setToolbarIcon } from 'common/interface'
2 | import { fetchStoredTags, getOnSaveTags } from 'common/api'
3 | import { getSetting, setSettings } from 'common/interface'
4 | import { deriveItemData } from 'common/helpers'
5 |
6 | import { UPDATE_ITEM_PREVIEW } from 'actions'
7 | import { UPDATE_STORED_TAGS } from 'actions'
8 | import { SUGGESTED_TAGS_SUCCESS } from 'actions'
9 |
10 | /* On successful save
11 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
12 | export async function saveSuccess(tabId, payload) {
13 | // Update toolbar icon
14 | const { resolved_url, given_url, isLink } = payload
15 | // fetch image and title from above
16 | const url = resolved_url || given_url //eslint-disable-line
17 |
18 | if (!isLink) setToolbarIcon(tabId, true)
19 |
20 | // Get item preview
21 | getItemPreview(tabId, payload)
22 |
23 | // Get list of users tags for typeahead
24 | getStoredTags(tabId)
25 |
26 | // Premium: Request suggested tags
27 | getTagSuggestions(url, tabId)
28 | }
29 |
30 | /* Derive item preview from save response
31 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
32 | async function getItemPreview(tabId, payload) {
33 | const item = await deriveItemData(payload)
34 |
35 | chrome.tabs.sendMessage(tabId, {
36 | action: UPDATE_ITEM_PREVIEW,
37 | payload: { item },
38 | })
39 | }
40 |
41 | /* Get stored tags
42 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
43 | async function getStoredTags(tabId) {
44 | // Check for server tags
45 | const fetchedSince = (await getSetting('tags_fetched_timestamp')) || 0
46 | const fetchTags = await fetchStoredTags(fetchedSince)
47 | const fetchedTags = fetchTags ? fetchTags.tags || [] : []
48 | const tagsFromSettings = await getSetting('tags_stored')
49 | const parsedTags = tagsFromSettings ? JSON.parse(tagsFromSettings) : []
50 | const tags_stored = [...new Set([].concat(...parsedTags, ...fetchedTags))]
51 | const tags = JSON.stringify(tags_stored)
52 |
53 | setSettings({
54 | tags_stored: tags,
55 | tags_fetched_timestamp: Date.now(),
56 | })
57 |
58 | chrome.tabs.sendMessage(tabId, {
59 | action: UPDATE_STORED_TAGS,
60 | payload: { tags: tags_stored },
61 | })
62 | }
63 |
64 | /* Get suggested tags for premium users
65 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
66 | async function getTagSuggestions(url, tabId) {
67 | const premiumStatus = await getSetting('premium_status')
68 | if (premiumStatus !== '1') return
69 |
70 | try {
71 | const response = await getOnSaveTags(url)
72 | const suggestedTags = response
73 | ? response.suggested_tags.map((tag) => tag.tag)
74 | : []
75 |
76 | if (response) {
77 | chrome.tabs.sendMessage(tabId, {
78 | action: SUGGESTED_TAGS_SUCCESS,
79 | payload: { suggestedTags },
80 | })
81 | }
82 | } catch (err) {
83 | console.info(err?.xError)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/heading/heading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css, cx } from 'linaria'
3 | import { PocketLogoIcon } from 'components/icons/icons'
4 | import { Loading } from 'components/loading/loading'
5 | import { ErrorIcon } from 'components/icons/icons'
6 | import { Button } from 'components/button/extensions-button'
7 |
8 | const headingStyle = css`
9 | .pocket-extension &.header {
10 | display: flex;
11 | justify-content: space-between;
12 | background-color: var(--color-headingBackground);
13 | border-radius: 30px;
14 | padding: 15px 20px 15px 10px;
15 | font-size: 16px;
16 | }
17 |
18 | .save-status {
19 | display: flex;
20 | }
21 |
22 | .icon-wrapper {
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | width: 40px;
27 | min-height: 25px;
28 | margin-right: 15px;
29 | }
30 |
31 | .icon {
32 | height: 25px;
33 | width: 25px;
34 | margin-top: 0;
35 | border: none;
36 | background-color: transparent;
37 | pointer-events: none;
38 |
39 | svg {
40 | height: 25px;
41 | width: 25px;
42 | }
43 | }
44 |
45 | .saveBlock {
46 | display: inline-flex;
47 | align-items: center;
48 | color: var(--color-textPrimary);
49 | font-family: var(--fontSansSerif);
50 | font-size: 16px;
51 | font-weight: 600;
52 | }
53 |
54 | button.inline {
55 | text-decoration: none;
56 | color: var(--color-inlineButton);
57 | &:hover {
58 | text-decoration: underline;
59 | }
60 | }
61 |
62 | &.error {
63 | background-color: var(--color-headingErrorBackground);
64 |
65 | .saveBlock, .icon {
66 | color: var(--color-headingIcon);
67 | }
68 | }
69 | `
70 |
71 | export const Heading = ({ saveStatus, removeAction, saveAction }) => {
72 | const loadingStatus = ['saving', 'removing', 'tags_saving']
73 | const isLoading = loadingStatus.includes(saveStatus)
74 |
75 | const errorStatus = ['save_failed', 'remove_failed', 'tags_failed', 'tags_error', 'error']
76 | const hasError = errorStatus.includes(saveStatus)
77 |
78 | return (
79 |
95 | )
96 | }
97 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import linaria from 'linaria/rollup'
3 | import css from 'rollup-plugin-css-only'
4 | import alias from '@rollup/plugin-alias'
5 | import babel from '@rollup/plugin-babel'
6 | import commonjs from '@rollup/plugin-commonjs'
7 | import json from '@rollup/plugin-json'
8 | import replace from '@rollup/plugin-replace'
9 | import copy from 'rollup-plugin-copy'
10 | import resolve from '@rollup/plugin-node-resolve'
11 | import { chromeExtension } from 'rollup-plugin-chrome-extension'
12 | import { emptyDir } from 'rollup-plugin-empty-dir'
13 | import zip from 'rollup-plugin-zip'
14 | import keys from './keys.json' // See README on how to get a key
15 |
16 | const isRelease = process.env.IS_RELEASE === 'true'
17 |
18 | const projectRootDir = path.resolve(__dirname)
19 |
20 | export default {
21 | input: 'src/manifest.json',
22 | output: {
23 | dir: 'build',
24 | format: 'esm',
25 | chunkFileNames: path.join('chunks', '[name]-[hash].js'),
26 | },
27 | onwarn: function onwarn(warning, warn) {
28 | if (warning.code === 'FILE_NAME_CONFLICT') return // We are require to conflict due to manifest
29 | warn(warning)
30 | },
31 | plugins: [
32 | chromeExtension({ verbose: false }),
33 | alias({
34 | entries: {
35 | actions: path.resolve(projectRootDir, 'src/actions.js'),
36 | assets: path.resolve(projectRootDir, 'src/assets'),
37 | common: path.resolve(projectRootDir, 'src/common'),
38 | components: path.resolve(projectRootDir, 'src/components'),
39 | connectors: path.resolve(projectRootDir, 'src/connectors'),
40 | containers: path.resolve(projectRootDir, 'src/containers'),
41 | pages: path.resolve(projectRootDir, 'src/pages'),
42 | _locales: path.resolve(projectRootDir, 'src/_locales')
43 | },
44 | }),
45 | // Replace environment variables
46 | replace({
47 | preventAssignment: true,
48 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
49 | 'process.env.IS_RELEASE': JSON.stringify(process.env.IS_RELEASE)
50 | }),
51 | replace({
52 | preventAssignment: false,
53 | __consumerKey__: keys.chrome,
54 | }),
55 | resolve(),
56 | commonjs({
57 | exclude: 'src/**',
58 | }),
59 | linaria(),
60 | json(),
61 | babel({
62 | // Do not transpile dependencies
63 | ignore: ['node_modules'],
64 | babelHelpers: 'bundled',
65 | }),
66 | css({ output: 'assets/pocket-save-extension.css' }),
67 | // Empties the output dir before a new build
68 | emptyDir(),
69 | copy({
70 | targets: [
71 | { src: 'src/assets/fonts/*', dest: 'build/assets/fonts' },
72 | { src: 'src/assets/images/*', dest: 'build/assets/images' },
73 | { src: 'src/_locales/*', dest: 'build/_locales' },
74 | ],
75 | hook: 'writeBundle',
76 | }),
77 | // Outputs a zip file in ./releases
78 | isRelease && zip({ dir: 'releases' }),
79 | ],
80 | }
81 |
--------------------------------------------------------------------------------
/src/pages/background/index.js:
--------------------------------------------------------------------------------
1 | import * as handle from './userActions'
2 | import { setDefaultIcon } from 'common/interface'
3 | import * as Sentry from '@sentry/browser'
4 | import { Integrations } from '@sentry/tracing'
5 |
6 | Sentry.init({
7 | dsn: 'https://11eed553982148d5b0b0288798aa3d85@o28549.ingest.sentry.io/6120053',
8 | tracesSampleRate: 0,
9 | sampleRate: 0.5,
10 | })
11 |
12 | import { AUTH_CODE_RECEIVED } from 'actions'
13 | import { USER_LOG_IN } from 'actions'
14 | import { LOGGED_OUT_OF_POCKET } from 'actions'
15 | import { RESAVE_ITEM } from 'actions'
16 | import { REMOVE_ITEM } from 'actions'
17 | import { TAGS_SYNC } from 'actions'
18 | import { OPEN_POCKET } from 'actions'
19 | import { OPEN_OPTIONS } from 'actions'
20 | import { COLOR_MODE_CHANGE } from 'actions'
21 | import { SEND_TAG_ERROR } from 'actions'
22 |
23 | /* Initial Setup
24 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
25 | chrome.runtime.onInstalled.addListener(function () {
26 | // Use SVG icons over the png for more control
27 | setDefaultIcon()
28 |
29 | handle.setContextMenus()
30 | })
31 |
32 | /* Browser Action - Toolbar
33 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
34 | chrome.action.onClicked.addListener(handle.browserAction)
35 |
36 | chrome.commands.onCommand.addListener((command, tab) => {
37 | if (command === 'save-to-pocket-action') handle.browserAction(tab)
38 | })
39 |
40 | /* Context Menus Handling
41 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
42 | chrome.contextMenus.onClicked.addListener(handle.contextClick)
43 |
44 | /* Tab Handling
45 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
46 | // Update the icon to unsaved if we are change pages
47 | chrome.tabs.onUpdated.addListener(handle.tabUpdated)
48 |
49 | chrome.runtime.onMessage.addListener(function (message, sender) {
50 | const { type, payload } = message
51 | const { tab } = sender
52 |
53 | console.groupCollapsed(`RECEIVE: ${type}`)
54 | console.log(payload)
55 | console.groupEnd(`RECEIVE: ${type}`)
56 |
57 | switch (type) {
58 | case AUTH_CODE_RECEIVED:
59 | handle.authCodeRecieved(tab, payload)
60 | return
61 | case USER_LOG_IN:
62 | handle.logIn(tab)
63 | return
64 | case LOGGED_OUT_OF_POCKET:
65 | handle.loggedOutOfPocket(tab)
66 | return
67 | case REMOVE_ITEM:
68 | handle.removeItemAction(tab, payload)
69 | return
70 | case RESAVE_ITEM:
71 | handle.browserAction(tab)
72 | return
73 | case TAGS_SYNC:
74 | handle.tagsSyncAction(tab, payload)
75 | return
76 | case SEND_TAG_ERROR:
77 | handle.tagsErrorAction(tab, payload)
78 | return
79 | case COLOR_MODE_CHANGE:
80 | handle.setColorMode(tab, payload)
81 | return
82 | case OPEN_POCKET:
83 | handle.openPocket()
84 | return
85 | case OPEN_OPTIONS:
86 | handle.openOptionsPage()
87 | return
88 | default:
89 | return Promise.resolve(`Message received: ${type}`)
90 | }
91 | })
92 |
--------------------------------------------------------------------------------
/src/_locales/zh_CN/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "捕获文章、视频等内容的最轻松、快捷方式。"
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "出错了!"
10 | },
11 | "heading_saving" : {
12 | "message" : "保存..."
13 | },
14 | "heading_saved" : {
15 | "message" : "已保存到 Pocket"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "出错了!"
19 | },
20 | "heading_removing" : {
21 | "message" : "正在删除..."
22 | },
23 | "heading_removed" : {
24 | "message" : "已删除"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "出错了!"
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "正在保存标记……"
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "标记已保存"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "出错了!"
37 | },
38 | "heading_tags_error" : {
39 | "message" : "标记限于 25 个字符"
40 | },
41 | "buttons_remove" : {
42 | "message" : "删除"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "保存内容"
46 | },
47 | "buttons_save" : {
48 | "message" : "保存"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "打开您的 Pocket 保存内容"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "在 Pocket 发现更多内容"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "登录"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "注销"
61 | },
62 | "context_menu_save" : {
63 | "message" : "保存到 Pocket"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "添加标记"
67 | },
68 | "options_header" : {
69 | "message" : "保存到 Pocket 扩展件"
70 | },
71 | "options_login_title" : {
72 | "message" : "登录为"
73 | },
74 | "options_log_out" : {
75 | "message" : "注销"
76 | },
77 | "options_log_in" : {
78 | "message" : "登录"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "键盘快捷键"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "记录新的快捷键"
85 | },
86 | "options_theme_title" : {
87 | "message" : "主题"
88 | },
89 | "options_theme_light" : {
90 | "message" : "浅色"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "深色"
94 | },
95 | "options_theme_system" : {
96 | "message" : "使用系统设置"
97 | },
98 | "options_app_title" : {
99 | "message" : "Pocket 的移动应用"
100 | },
101 | "options_app_apple" : {
102 | "message" : "从 Apple App Store 下载"
103 | },
104 | "options_app_google" : {
105 | "message" : "从 Google Play 获取"
106 | },
107 | "options_need_help" : {
108 | "message" : "需要帮助?"
109 | },
110 | "options_email_us" : {
111 | "message" : "电邮我们"
112 | },
113 | "options_follow" : {
114 | "message" : "关注 Pocket"
115 | },
116 | "options_family" : {
117 | "message" : "Pocket 是 Mozilla 系列产品的一部分。"
118 | },
119 | "options_privacy" : {
120 | "message" : "隐私政策"
121 | },
122 | "options_terms" : {
123 | "message" : "服务条款"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/zh_TW/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "擷取文章、短片等内容的最簡捷方法。"
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "出了點問題!"
10 | },
11 | "heading_saving" : {
12 | "message" : "正在儲存..."
13 | },
14 | "heading_saved" : {
15 | "message" : "已儲存到 Pocket"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "出了點問題!"
19 | },
20 | "heading_removing" : {
21 | "message" : "移除中..."
22 | },
23 | "heading_removed" : {
24 | "message" : "已移除"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "出了點問題!"
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "正在儲存標籤……"
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "標籤已儲存"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "出了點問題!"
37 | },
38 | "heading_tags_error" : {
39 | "message" : "標籤長度限於 25 個字元"
40 | },
41 | "buttons_remove" : {
42 | "message" : "移除"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "儲存項目"
46 | },
47 | "buttons_save" : {
48 | "message" : "儲存"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "開啟您的 Pocket 儲存內容"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "在 Pocket 上發掘更多內容"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "登入"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "登出"
61 | },
62 | "context_menu_save" : {
63 | "message" : "儲存到 Pocket"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "新增標籤"
67 | },
68 | "options_header" : {
69 | "message" : "儲存到 Pocket 擴充功能"
70 | },
71 | "options_login_title" : {
72 | "message" : "已登入為"
73 | },
74 | "options_log_out" : {
75 | "message" : "登出"
76 | },
77 | "options_log_in" : {
78 | "message" : "登入"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "鍵盤快速鍵"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "記錄新捷徑"
85 | },
86 | "options_theme_title" : {
87 | "message" : "主題"
88 | },
89 | "options_theme_light" : {
90 | "message" : "淺色"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "暗色調"
94 | },
95 | "options_theme_system" : {
96 | "message" : "使用系統設定"
97 | },
98 | "options_app_title" : {
99 | "message" : "Pocket 的行動應用程式"
100 | },
101 | "options_app_apple" : {
102 | "message" : "在 Apple App Store 下載"
103 | },
104 | "options_app_google" : {
105 | "message" : "在 Google Play 取得"
106 | },
107 | "options_need_help" : {
108 | "message" : "需要幫助?"
109 | },
110 | "options_email_us" : {
111 | "message" : "電郵我們"
112 | },
113 | "options_follow" : {
114 | "message" : "關注 Pocket"
115 | },
116 | "options_family" : {
117 | "message" : "Pocket 屬於 Mozilla 系列產品之一。"
118 | },
119 | "options_privacy" : {
120 | "message" : "隱私政策"
121 | },
122 | "options_terms" : {
123 | "message" : "服務條款"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/ko/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "아티클, 동영상 등을 캡처하는 가장 쉽고 빠른 방법."
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "문제가 발생했습니다!"
10 | },
11 | "heading_saving" : {
12 | "message" : "저장 중..."
13 | },
14 | "heading_saved" : {
15 | "message" : "Pocket에 저장됨"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "문제가 발생했습니다!"
19 | },
20 | "heading_removing" : {
21 | "message" : "제거 중..."
22 | },
23 | "heading_removed" : {
24 | "message" : "제거됨"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "문제가 발생했습니다!"
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "태그 저장 중..."
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "태그 저장됨"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "문제가 발생했습니다!"
37 | },
38 | "heading_tags_error" : {
39 | "message" : "태그는 25자로 제한됩니다."
40 | },
41 | "buttons_remove" : {
42 | "message" : "제거하기"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "저장"
46 | },
47 | "buttons_save" : {
48 | "message" : "저장"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "Pocket Saves 열기"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "Pocket에서 더 찾기"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "로그인"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "로그아웃"
61 | },
62 | "context_menu_save" : {
63 | "message" : "Pocket에 저장"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "태그 추가"
67 | },
68 | "options_header" : {
69 | "message" : "Pocket 확장에 저장"
70 | },
71 | "options_login_title" : {
72 | "message" : "다음 이름으로 로그인됨"
73 | },
74 | "options_log_out" : {
75 | "message" : "로그아웃"
76 | },
77 | "options_log_in" : {
78 | "message" : "로그인"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "키보드 바로 가기"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "새 바로 가기 기록"
85 | },
86 | "options_theme_title" : {
87 | "message" : "테마"
88 | },
89 | "options_theme_light" : {
90 | "message" : "밝게"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "어둡게"
94 | },
95 | "options_theme_system" : {
96 | "message" : "시스템 설정 사용"
97 | },
98 | "options_app_title" : {
99 | "message" : "Pocket 모바일 앱"
100 | },
101 | "options_app_apple" : {
102 | "message" : "Apple App Store에서 다운로드"
103 | },
104 | "options_app_google" : {
105 | "message" : "Google Play에서 다운로드"
106 | },
107 | "options_need_help" : {
108 | "message" : "도움이 필요하십니까?"
109 | },
110 | "options_email_us" : {
111 | "message" : "이메일 보내기"
112 | },
113 | "options_follow" : {
114 | "message" : "Pocket 팔로우"
115 | },
116 | "options_family" : {
117 | "message" : "Pocket은 Mozilla 제품군의 일부입니다."
118 | },
119 | "options_privacy" : {
120 | "message" : "개인정보 보호정책"
121 | },
122 | "options_terms" : {
123 | "message" : "서비스 약관"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/ja/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "記事、ビデオなどを最も簡単かつ迅速に取り込むことができます。"
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "エラーが発生したようです。"
10 | },
11 | "heading_saving" : {
12 | "message" : "保存中..."
13 | },
14 | "heading_saved" : {
15 | "message" : "Pocket に保存済み"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "エラーが発生したようです。"
19 | },
20 | "heading_removing" : {
21 | "message" : "削除しています..."
22 | },
23 | "heading_removed" : {
24 | "message" : "削除されました"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "エラーが発生したようです。"
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "タグを保存しています..."
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "タグが保存されました"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "エラーが発生したようです。"
37 | },
38 | "heading_tags_error" : {
39 | "message" : "タグは最大25文字となっています"
40 | },
41 | "buttons_remove" : {
42 | "message" : "削除"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "保存アイテム"
46 | },
47 | "buttons_save" : {
48 | "message" : "保存"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "Pocket の保存アイテムを開く"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "Pocket で他のコンテンツを発見"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "ログイン"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "ログアウト"
61 | },
62 | "context_menu_save" : {
63 | "message" : "Pocket に保存"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "タグを追加"
67 | },
68 | "options_header" : {
69 | "message" : "「Pocket に保存」拡張機能"
70 | },
71 | "options_login_title" : {
72 | "message" : "次のユーザー名でログイン中:"
73 | },
74 | "options_log_out" : {
75 | "message" : "ログアウト"
76 | },
77 | "options_log_in" : {
78 | "message" : "ログイン"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "キーボードショートカット"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "新しいショートカットを記録"
85 | },
86 | "options_theme_title" : {
87 | "message" : "テーマ"
88 | },
89 | "options_theme_light" : {
90 | "message" : "ライト"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "ダーク"
94 | },
95 | "options_theme_system" : {
96 | "message" : "システム設定を使用する"
97 | },
98 | "options_app_title" : {
99 | "message" : "Pocket モバイルアプリ"
100 | },
101 | "options_app_apple" : {
102 | "message" : "Apple App Store でダウンロード"
103 | },
104 | "options_app_google" : {
105 | "message" : "Google Playからダウンロード"
106 | },
107 | "options_need_help" : {
108 | "message" : "お困りですか?"
109 | },
110 | "options_email_us" : {
111 | "message" : "メールでお問い合わせ"
112 | },
113 | "options_follow" : {
114 | "message" : "Pocket をフォロー"
115 | },
116 | "options_family" : {
117 | "message" : "Pocket は Mozilla 製品ファミリーの一部です。"
118 | },
119 | "options_privacy" : {
120 | "message" : "プライバシーポリシー"
121 | },
122 | "options_terms" : {
123 | "message" : "サービス利用規約"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description": {
3 | "message": "The easiest, fastest way to capture articles, videos, and more."
4 | },
5 | "heading_idle": {
6 | "message": ""
7 | },
8 | "heading_error": {
9 | "message": "Something went wrong!"
10 | },
11 | "heading_saving": {
12 | "message": "Saving..."
13 | },
14 | "heading_saved": {
15 | "message": "Saved to Pocket"
16 | },
17 | "heading_save_failed": {
18 | "message": "Something went wrong!"
19 | },
20 | "heading_removing": {
21 | "message": "Removing..."
22 | },
23 | "heading_removed": {
24 | "message": "Removed"
25 | },
26 | "heading_remove_failed": {
27 | "message": "Something went wrong!"
28 | },
29 | "heading_tags_saving": {
30 | "message": "Saving tags..."
31 | },
32 | "heading_tags_saved": {
33 | "message": "Tags saved"
34 | },
35 | "heading_tags_failed": {
36 | "message": "Something went wrong!"
37 | },
38 | "heading_tags_error": {
39 | "message": "Tags are limited to 25 characters"
40 | },
41 | "buttons_remove": {
42 | "message": "Remove"
43 | },
44 | "buttons_mylist": {
45 | "message": "Saves"
46 | },
47 | "buttons_save": {
48 | "message": "Save"
49 | },
50 | "context_menu_open_list": {
51 | "message": "Open Your Pocket Saves"
52 | },
53 | "context_menu_discover_more": {
54 | "message": "Discover more at Pocket"
55 | },
56 | "context_menu_log_in": {
57 | "message": "Log In"
58 | },
59 | "context_menu_log_out": {
60 | "message": "Log Out"
61 | },
62 | "context_menu_save": {
63 | "message": "Save To Pocket"
64 | },
65 | "tagging_add_tags": {
66 | "message": "Add Tags"
67 | },
68 | "options_header": {
69 | "message": "Save to Pocket extension"
70 | },
71 | "options_login_title": {
72 | "message": "Logged in as"
73 | },
74 | "options_log_out": {
75 | "message": "Log out"
76 | },
77 | "options_log_in": {
78 | "message": "Log in"
79 | },
80 | "options_shortcut_title": {
81 | "message": "Keyboard Shortcut"
82 | },
83 | "options_shortcut_record": {
84 | "message": "Record a new shortcut"
85 | },
86 | "options_theme_title": {
87 | "message": "Theme"
88 | },
89 | "options_theme_light": {
90 | "message": "Light"
91 | },
92 | "options_theme_dark": {
93 | "message": "Dark"
94 | },
95 | "options_theme_system": {
96 | "message": "Use System Setting"
97 | },
98 | "options_app_title": {
99 | "message": "Pocket’s Mobile App"
100 | },
101 | "options_app_apple": {
102 | "message": "Download on the Apple App Store"
103 | },
104 | "options_app_google": {
105 | "message": "Get it on Google Play"
106 | },
107 | "options_need_help": {
108 | "message": "Need Help?"
109 | },
110 | "options_email_us": {
111 | "message": "Email Us"
112 | },
113 | "options_follow": {
114 | "message": "Follow Pocket"
115 | },
116 | "options_family": {
117 | "message": "Pocket is part of the Mozilla family of products."
118 | },
119 | "options_privacy": {
120 | "message": "Privacy policy"
121 | },
122 | "options_terms": {
123 | "message": "Terms of service"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/pt_BR/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "A maneira mais fácil e rápida de salvar artigos, vídeos e muito mais."
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "Ocorreu um erro!"
10 | },
11 | "heading_saving" : {
12 | "message" : "Salvando…"
13 | },
14 | "heading_saved" : {
15 | "message" : "Salvo no Pocket"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "Ocorreu um erro!"
19 | },
20 | "heading_removing" : {
21 | "message" : "Removendo..."
22 | },
23 | "heading_removed" : {
24 | "message" : "Removido"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "Ocorreu um erro!"
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "Salvando tags..."
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "Tags salvas"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "Ocorreu um erro!"
37 | },
38 | "heading_tags_error" : {
39 | "message" : "As tags têm um limite de 25 caracteres"
40 | },
41 | "buttons_remove" : {
42 | "message" : "Remover"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "Salvos"
46 | },
47 | "buttons_save" : {
48 | "message" : "Salvar"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "Abra seus Salvos no Pocket"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "Descubra mais no Pocket"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "Entrar"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "Sair"
61 | },
62 | "context_menu_save" : {
63 | "message" : "Salvar no Pocket"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "Adicionar tags"
67 | },
68 | "options_header" : {
69 | "message" : "Extensão Salvar no Pocket"
70 | },
71 | "options_login_title" : {
72 | "message" : "Conectado como"
73 | },
74 | "options_log_out" : {
75 | "message" : "Sair"
76 | },
77 | "options_log_in" : {
78 | "message" : "Entrar"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "Atalho do teclado"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "Gravar um novo atalho"
85 | },
86 | "options_theme_title" : {
87 | "message" : "Tema"
88 | },
89 | "options_theme_light" : {
90 | "message" : "Claro"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "Escuro"
94 | },
95 | "options_theme_system" : {
96 | "message" : "Usar a configuração do sistema"
97 | },
98 | "options_app_title" : {
99 | "message" : "Aplicativo móvel do Pocket"
100 | },
101 | "options_app_apple" : {
102 | "message" : "Baixe o aplicativo na App Store da Apple"
103 | },
104 | "options_app_google" : {
105 | "message" : "Baixar no Google Play"
106 | },
107 | "options_need_help" : {
108 | "message" : "Precisa de ajuda?"
109 | },
110 | "options_email_us" : {
111 | "message" : "Envie-nos um e-mail"
112 | },
113 | "options_follow" : {
114 | "message" : "Siga o Pocket"
115 | },
116 | "options_family" : {
117 | "message" : "O Pocket faz parte da linha de produtos da Mozilla."
118 | },
119 | "options_privacy" : {
120 | "message" : "Política de privacidade"
121 | },
122 | "options_terms" : {
123 | "message" : "Termos de serviço"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/nl/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "De gemakkelijkste en snelste manier om artikelen, video's en meer op te slaan."
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "Er is iets misgegaan!"
10 | },
11 | "heading_saving" : {
12 | "message" : "Opslaan..."
13 | },
14 | "heading_saved" : {
15 | "message" : "Opgeslagen naar Pocket"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "Er is iets misgegaan!"
19 | },
20 | "heading_removing" : {
21 | "message" : "Verwijderen..."
22 | },
23 | "heading_removed" : {
24 | "message" : "Verwijderd"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "Er is iets misgegaan!"
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "Tags opslaan..."
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "Tags opgeslagen"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "Er is iets misgegaan!"
37 | },
38 | "heading_tags_error" : {
39 | "message" : "Tags zijn beperkt tot 25 tekens"
40 | },
41 | "buttons_remove" : {
42 | "message" : "Verwijderen"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "Opgeslagen"
46 | },
47 | "buttons_save" : {
48 | "message" : "Opslaan"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "Open Opgeslagen in Pocket"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "Ontdek meer op Pocket"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "Inloggen"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "Uitloggen"
61 | },
62 | "context_menu_save" : {
63 | "message" : "Opslaan naar Pocket"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "Tags toevoegen"
67 | },
68 | "options_header" : {
69 | "message" : "Extensie Naar Pocket opslaan"
70 | },
71 | "options_login_title" : {
72 | "message" : "Ingelogd als"
73 | },
74 | "options_log_out" : {
75 | "message" : "Afmelden"
76 | },
77 | "options_log_in" : {
78 | "message" : "Inloggen"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "Sneltoets"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "Een nieuwe snelkoppeling vastleggen"
85 | },
86 | "options_theme_title" : {
87 | "message" : "Thema"
88 | },
89 | "options_theme_light" : {
90 | "message" : "Licht"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "Donker"
94 | },
95 | "options_theme_system" : {
96 | "message" : "Systeeminstelling gebruiken"
97 | },
98 | "options_app_title" : {
99 | "message" : "Mobiele Pocket-app"
100 | },
101 | "options_app_apple" : {
102 | "message" : "Downloaden in de appstore van Apple"
103 | },
104 | "options_app_google" : {
105 | "message" : "Downloaden via Google Play"
106 | },
107 | "options_need_help" : {
108 | "message" : "Hulp nodig?"
109 | },
110 | "options_email_us" : {
111 | "message" : "Mail ons"
112 | },
113 | "options_follow" : {
114 | "message" : "Volg Pocket"
115 | },
116 | "options_family" : {
117 | "message" : "Pocket maakt deel uit van de Mozilla-productfamilie."
118 | },
119 | "options_privacy" : {
120 | "message" : "Privacybeleid"
121 | },
122 | "options_terms" : {
123 | "message" : "Gebruiksvoorwaarden"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/pt_PT/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "A maneira mais fácil e rápida de capturar artigos, vídeos e muito mais."
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "Algo correu mal!"
10 | },
11 | "heading_saving" : {
12 | "message" : "A guardar…"
13 | },
14 | "heading_saved" : {
15 | "message" : "Guardado no Pocket"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "Algo correu mal!"
19 | },
20 | "heading_removing" : {
21 | "message" : "A remover..."
22 | },
23 | "heading_removed" : {
24 | "message" : "Removido"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "Algo correu mal!"
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "A guardar etiquetas..."
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "Etiquetas guardadas"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "Algo correu mal!"
37 | },
38 | "heading_tags_error" : {
39 | "message" : "As etiquetas têm um limite de 25 caracteres"
40 | },
41 | "buttons_remove" : {
42 | "message" : "Remover"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "Guardados"
46 | },
47 | "buttons_save" : {
48 | "message" : "Guardar"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "Abra os seus Guardados no Pocket"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "Descubra mais no Pocket"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "Entrar"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "Sair"
61 | },
62 | "context_menu_save" : {
63 | "message" : "Guardar no Pocket"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "Adicionar Etiquetas"
67 | },
68 | "options_header" : {
69 | "message" : "Guardar na extensão do Pocket"
70 | },
71 | "options_login_title" : {
72 | "message" : "Sessão iniciada como"
73 | },
74 | "options_log_out" : {
75 | "message" : "Terminar sessão"
76 | },
77 | "options_log_in" : {
78 | "message" : "Iniciar sessão"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "Atalhos de teclado"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "Gravar um atalho novo"
85 | },
86 | "options_theme_title" : {
87 | "message" : "Tema"
88 | },
89 | "options_theme_light" : {
90 | "message" : "Claro"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "Escuro"
94 | },
95 | "options_theme_system" : {
96 | "message" : "Usar a definição do sistema"
97 | },
98 | "options_app_title" : {
99 | "message" : "Aplicação móvel do Pocket"
100 | },
101 | "options_app_apple" : {
102 | "message" : "Transferir na Apple App Store"
103 | },
104 | "options_app_google" : {
105 | "message" : "Obtenha a app no Google Play"
106 | },
107 | "options_need_help" : {
108 | "message" : "Precisa de Ajuda?"
109 | },
110 | "options_email_us" : {
111 | "message" : "Envie-nos um e-mail"
112 | },
113 | "options_follow" : {
114 | "message" : "Seguir o Pocket"
115 | },
116 | "options_family" : {
117 | "message" : "O Pocket faz parte da família de produtos Mozilla."
118 | },
119 | "options_privacy" : {
120 | "message" : "Política de privacidade"
121 | },
122 | "options_terms" : {
123 | "message" : "Condições do serviço"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/pl/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "Najłatwiejsza i najszybsza metoda zapisywania artykułów, filmów i innych treści."
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "Wystąpił problem."
10 | },
11 | "heading_saving" : {
12 | "message" : "Zapisywanie..."
13 | },
14 | "heading_saved" : {
15 | "message" : "Zapisano w aplikacji Pocket"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "Wystąpił problem."
19 | },
20 | "heading_removing" : {
21 | "message" : "Usuwanie..."
22 | },
23 | "heading_removed" : {
24 | "message" : "Usunięto"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "Wystąpił problem."
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "Zapisywanie tagów..."
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "Zapisano tagi"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "Wystąpił problem."
37 | },
38 | "heading_tags_error" : {
39 | "message" : "Tagi mogą mieć maksymalnie 25 znaków"
40 | },
41 | "buttons_remove" : {
42 | "message" : "Usuń"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "Zapisane"
46 | },
47 | "buttons_save" : {
48 | "message" : "Zapisz"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "Otwórz elementy zapisane w Pocket"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "Odkryj więcej w Pocket"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "Zaloguj się"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "Wyloguj się"
61 | },
62 | "context_menu_save" : {
63 | "message" : "Zapisz w aplikacji Pocket"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "Dodaj tagi"
67 | },
68 | "options_header" : {
69 | "message" : "Rozszerzenie Zapisz do Pocket"
70 | },
71 | "options_login_title" : {
72 | "message" : "Zalogowano jako"
73 | },
74 | "options_log_out" : {
75 | "message" : "Wyloguj się"
76 | },
77 | "options_log_in" : {
78 | "message" : "Zaloguj się"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "Skrót klawiaturowy"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "Zarejestruj nowy skrót"
85 | },
86 | "options_theme_title" : {
87 | "message" : "Motyw"
88 | },
89 | "options_theme_light" : {
90 | "message" : "Jasny"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "Ciemny"
94 | },
95 | "options_theme_system" : {
96 | "message" : "Użyj ustawienia systemowego"
97 | },
98 | "options_app_title" : {
99 | "message" : "Aplikacja mobilna Pocket"
100 | },
101 | "options_app_apple" : {
102 | "message" : "Pobierz z serwisu Apple App Store"
103 | },
104 | "options_app_google" : {
105 | "message" : "Pobierz z serwisu Google Play"
106 | },
107 | "options_need_help" : {
108 | "message" : "Potrzebna pomoc?"
109 | },
110 | "options_email_us" : {
111 | "message" : "Napisz do nas wiadomość e-mail"
112 | },
113 | "options_follow" : {
114 | "message" : "Obserwuj Pocket"
115 | },
116 | "options_family" : {
117 | "message" : "Pocket należy do rodziny produktów Mozilla."
118 | },
119 | "options_privacy" : {
120 | "message" : "Zasady ochrony prywatności"
121 | },
122 | "options_terms" : {
123 | "message" : "Warunki użytkowania usługi"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/ru/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "Самый простой и быстрый способ записывать статьи, видео и многое другое."
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "К сожалению, что-то пошло не так!"
10 | },
11 | "heading_saving" : {
12 | "message" : "Сохранение..."
13 | },
14 | "heading_saved" : {
15 | "message" : "Сохранено в Pocket"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "К сожалению, что-то пошло не так!"
19 | },
20 | "heading_removing" : {
21 | "message" : "Удаление..."
22 | },
23 | "heading_removed" : {
24 | "message" : "Удалено"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "К сожалению, что-то пошло не так!"
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "Сохранение тегов..."
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "Теги сохранены"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "К сожалению, что-то пошло не так!"
37 | },
38 | "heading_tags_error" : {
39 | "message" : "Длина тега ограничена 25 символами"
40 | },
41 | "buttons_remove" : {
42 | "message" : "Удалить"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "Сохраненное"
46 | },
47 | "buttons_save" : {
48 | "message" : "Сохранить"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "Откройте сохраненные файлы Pocket"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "Откройте больше с Pocket"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "Вход"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "Выйти"
61 | },
62 | "context_menu_save" : {
63 | "message" : "Сохранить в Pocket"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "Добавить теги"
67 | },
68 | "options_header" : {
69 | "message" : "Расширение \"Сохранить в Pocket\""
70 | },
71 | "options_login_title" : {
72 | "message" : "Вы вошли как"
73 | },
74 | "options_log_out" : {
75 | "message" : "Выйти"
76 | },
77 | "options_log_in" : {
78 | "message" : "Войти"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "Сочетание клавиш"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "Задать новое сочетание клавиш"
85 | },
86 | "options_theme_title" : {
87 | "message" : "Тема"
88 | },
89 | "options_theme_light" : {
90 | "message" : "Светлый"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "Темный"
94 | },
95 | "options_theme_system" : {
96 | "message" : "Использовать системные настройки"
97 | },
98 | "options_app_title" : {
99 | "message" : "Мобильное приложение Pocket"
100 | },
101 | "options_app_apple" : {
102 | "message" : "Скачать в Apple App Store"
103 | },
104 | "options_app_google" : {
105 | "message" : "Загрузить из Google Play"
106 | },
107 | "options_need_help" : {
108 | "message" : "Нужна помощь?"
109 | },
110 | "options_email_us" : {
111 | "message" : "Пишите нам"
112 | },
113 | "options_follow" : {
114 | "message" : "Следите за Pocket"
115 | },
116 | "options_family" : {
117 | "message" : "Pocket входит в семейство продуктов Mozilla."
118 | },
119 | "options_privacy" : {
120 | "message" : "Политика конфиденциальности"
121 | },
122 | "options_terms" : {
123 | "message" : "Условия использования"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/it/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "Il modo più semplice e veloce per raccogliere articoli, video e molto altro."
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "Qualcosa non ha funzionato."
10 | },
11 | "heading_saving" : {
12 | "message" : "Salvataggio..."
13 | },
14 | "heading_saved" : {
15 | "message" : "Salvato in Pocket"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "Qualcosa non ha funzionato."
19 | },
20 | "heading_removing" : {
21 | "message" : "Rimozione in corso…"
22 | },
23 | "heading_removed" : {
24 | "message" : "Rimosso"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "Qualcosa non ha funzionato."
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "Salvataggio tag in corso..."
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "Tag salvati"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "Qualcosa non ha funzionato."
37 | },
38 | "heading_tags_error" : {
39 | "message" : "I tag hanno un limite di 25 caratteri"
40 | },
41 | "buttons_remove" : {
42 | "message" : "Elimina"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "Salvati"
46 | },
47 | "buttons_save" : {
48 | "message" : "Salva"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "Apri i tuoi salvataggi Pocket"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "Scopri di più con Pocket"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "Accedi"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "Esci"
61 | },
62 | "context_menu_save" : {
63 | "message" : "Salva in Pocket"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "Aggiungi tag"
67 | },
68 | "options_header" : {
69 | "message" : "Estensione Salva in Pocket"
70 | },
71 | "options_login_title" : {
72 | "message" : "Accesso eseguito come"
73 | },
74 | "options_log_out" : {
75 | "message" : "Esci"
76 | },
77 | "options_log_in" : {
78 | "message" : "Accedi"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "Scelta rapida da tastiera"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "Memorizza un nuovo tasto di scelta rapida"
85 | },
86 | "options_theme_title" : {
87 | "message" : "Tema"
88 | },
89 | "options_theme_light" : {
90 | "message" : "Chiaro"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "Scuro"
94 | },
95 | "options_theme_system" : {
96 | "message" : "Usa impostazione di sistema"
97 | },
98 | "options_app_title" : {
99 | "message" : "App per dispositivi mobili Pocket"
100 | },
101 | "options_app_apple" : {
102 | "message" : "Scarica dall'App Store di Apple"
103 | },
104 | "options_app_google" : {
105 | "message" : "Scaricalo da Google Play"
106 | },
107 | "options_need_help" : {
108 | "message" : "Hai bisogno di aiuto?"
109 | },
110 | "options_email_us" : {
111 | "message" : "Scrivi un'email"
112 | },
113 | "options_follow" : {
114 | "message" : "Segui Pocket"
115 | },
116 | "options_family" : {
117 | "message" : "Pocket fa parte della famiglia di prodotti Mozilla."
118 | },
119 | "options_privacy" : {
120 | "message" : "Tutela della privacy"
121 | },
122 | "options_terms" : {
123 | "message" : "Condizioni d'uso"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/es_419/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "La forma más fácil y rápida de capturar artículos, videos y más."
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "¡Se produjo un error!"
10 | },
11 | "heading_saving" : {
12 | "message" : "Guardando..."
13 | },
14 | "heading_saved" : {
15 | "message" : "Guardado en Pocket"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "¡Se produjo un error!"
19 | },
20 | "heading_removing" : {
21 | "message" : "Eliminando..."
22 | },
23 | "heading_removed" : {
24 | "message" : "Eliminado"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "¡Se produjo un error!"
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "Guardando etiquetas..."
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "Etiquetas guardadas"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "¡Se produjo un error!"
37 | },
38 | "heading_tags_error" : {
39 | "message" : "Las etiquetas se limitan a 25 caracteres"
40 | },
41 | "buttons_remove" : {
42 | "message" : "Eliminar"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "Guardado"
46 | },
47 | "buttons_save" : {
48 | "message" : "Guardar"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "Abrir el contenido guardado en Pocket"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "Descubre más en Pocket"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "Iniciar sesión"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "Finalizar sesión"
61 | },
62 | "context_menu_save" : {
63 | "message" : "Guardar en Pocket"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "Agregar Etiquetas"
67 | },
68 | "options_header" : {
69 | "message" : "Extensión para guardar en Pocket"
70 | },
71 | "options_login_title" : {
72 | "message" : "Iniciaste sesión como"
73 | },
74 | "options_log_out" : {
75 | "message" : "Cerrar sesión"
76 | },
77 | "options_log_in" : {
78 | "message" : "Iniciar sesión"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "Atajo de teclado"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "Registra un nuevo atajo"
85 | },
86 | "options_theme_title" : {
87 | "message" : "Tema"
88 | },
89 | "options_theme_light" : {
90 | "message" : "Claro"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "Oscuro"
94 | },
95 | "options_theme_system" : {
96 | "message" : "Usar configuración del sistema"
97 | },
98 | "options_app_title" : {
99 | "message" : "Aplicación móvil de Pocket"
100 | },
101 | "options_app_apple" : {
102 | "message" : "Descargar en el App Store de Apple"
103 | },
104 | "options_app_google" : {
105 | "message" : "Obtenerlo en Google Play"
106 | },
107 | "options_need_help" : {
108 | "message" : "¿Necesitas ayuda?"
109 | },
110 | "options_email_us" : {
111 | "message" : "Envíanos un email"
112 | },
113 | "options_follow" : {
114 | "message" : "Seguir a Pocket"
115 | },
116 | "options_family" : {
117 | "message" : "Pocket forma parte de la familia de productos Mozilla."
118 | },
119 | "options_privacy" : {
120 | "message" : "Política de privacidad"
121 | },
122 | "options_terms" : {
123 | "message" : "Términos del Servicio"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/de/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "Die einfachste und schnellste Möglichkeit, Artikel, Videos und mehr zu speichern."
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "Da ist etwas schiefgegangen."
10 | },
11 | "heading_saving" : {
12 | "message" : "Speichern..."
13 | },
14 | "heading_saved" : {
15 | "message" : "In Pocket gespeichert"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "Da ist etwas schiefgegangen."
19 | },
20 | "heading_removing" : {
21 | "message" : "Wird entfernt ..."
22 | },
23 | "heading_removed" : {
24 | "message" : "Entfernt"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "Da ist etwas schiefgegangen."
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "Tags werden gespeichert …"
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "Tags gespeichert"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "Da ist etwas schiefgegangen."
37 | },
38 | "heading_tags_error" : {
39 | "message" : "Tags sind auf 25 Zeichen begrenzt."
40 | },
41 | "buttons_remove" : {
42 | "message" : "Entfernen"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "Gespeichert"
46 | },
47 | "buttons_save" : {
48 | "message" : "Speichern"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "Öffne deine in Pocket Gespeichert"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "Entdecke noch mehr interessante Inhalte auf Pocket"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "Einloggen"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "Ausloggen"
61 | },
62 | "context_menu_save" : {
63 | "message" : "In Pocket speichern"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "Tags hinzufügen"
67 | },
68 | "options_header" : {
69 | "message" : "Erweiterung zum Speichern in Pocket"
70 | },
71 | "options_login_title" : {
72 | "message" : "Eingeloggt als"
73 | },
74 | "options_log_out" : {
75 | "message" : "Ausloggen"
76 | },
77 | "options_log_in" : {
78 | "message" : "Einloggen"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "Tastenkombination"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "Neue Tastenkombination festlegen"
85 | },
86 | "options_theme_title" : {
87 | "message" : "Modus"
88 | },
89 | "options_theme_light" : {
90 | "message" : "Hell"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "Dunkel"
94 | },
95 | "options_theme_system" : {
96 | "message" : "Systemeinstellung verwenden"
97 | },
98 | "options_app_title" : {
99 | "message" : "Die mobile Pocket-App"
100 | },
101 | "options_app_apple" : {
102 | "message" : "Im Apple App Store herunterladen"
103 | },
104 | "options_app_google" : {
105 | "message" : "In Google Play herunterladen"
106 | },
107 | "options_need_help" : {
108 | "message" : "Du brauchst Hilfe?"
109 | },
110 | "options_email_us" : {
111 | "message" : "Schreibe uns eine E-Mail"
112 | },
113 | "options_follow" : {
114 | "message" : "Jetzt Pocket folgen"
115 | },
116 | "options_family" : {
117 | "message" : "Pocket gehört zur Mozilla-Produktfamilie."
118 | },
119 | "options_privacy" : {
120 | "message" : "Datenschutzrichtlinie"
121 | },
122 | "options_terms" : {
123 | "message" : "AGB"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/es/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "La forma más fácil y rápida de capturar artículos, vídeos y mucho más."
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "¡Parece que algo ha ido mal!"
10 | },
11 | "heading_saving" : {
12 | "message" : "Guardando…"
13 | },
14 | "heading_saved" : {
15 | "message" : "Guardado en Pocket"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "¡Parece que algo ha ido mal!"
19 | },
20 | "heading_removing" : {
21 | "message" : "Eliminando..."
22 | },
23 | "heading_removed" : {
24 | "message" : "Eliminado"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "¡Parece que algo ha ido mal!"
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "Guardando etiquetas..."
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "Etiquetas guardadas"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "¡Parece que algo ha ido mal!"
37 | },
38 | "heading_tags_error" : {
39 | "message" : "La longitud máxima de una etiqueta es 25 caracteres."
40 | },
41 | "buttons_remove" : {
42 | "message" : "Eliminar"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "Guardados"
46 | },
47 | "buttons_save" : {
48 | "message" : "Guardar"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "Abrir tus artículos guardados en Pocket"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "Descubre más en Pocket"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "Iniciar sesión"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "Cerrar sesión"
61 | },
62 | "context_menu_save" : {
63 | "message" : "Guardar en Pocket"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "Añadir etiquetas"
67 | },
68 | "options_header" : {
69 | "message" : "Extensión Guardar en Pocket"
70 | },
71 | "options_login_title" : {
72 | "message" : "Has iniciado sesión como"
73 | },
74 | "options_log_out" : {
75 | "message" : "Cerrar sesión"
76 | },
77 | "options_log_in" : {
78 | "message" : "Iniciar sesión"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "Atajo de teclado"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "Grabar un nuevo atajo"
85 | },
86 | "options_theme_title" : {
87 | "message" : "Tema"
88 | },
89 | "options_theme_light" : {
90 | "message" : "Claro"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "Oscuro"
94 | },
95 | "options_theme_system" : {
96 | "message" : "Usar configuración del sistema"
97 | },
98 | "options_app_title" : {
99 | "message" : "Aplicación móvil de Pocket"
100 | },
101 | "options_app_apple" : {
102 | "message" : "Descargar en la App Store de Apple"
103 | },
104 | "options_app_google" : {
105 | "message" : "Consíguela en Google Play"
106 | },
107 | "options_need_help" : {
108 | "message" : "¿Necesitas ayuda?"
109 | },
110 | "options_email_us" : {
111 | "message" : "Envíanos un correo"
112 | },
113 | "options_follow" : {
114 | "message" : "Seguir a Pocket"
115 | },
116 | "options_family" : {
117 | "message" : "Pocket es parte de la familia de productos de Mozilla."
118 | },
119 | "options_privacy" : {
120 | "message" : "Política de privacidad"
121 | },
122 | "options_terms" : {
123 | "message" : "Condiciones de servicio"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/_locales/fr/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_description" : {
3 | "message" : "Le moyen le plus simple et le plus rapide de sauvegarder des articles, des vidéos et plus encore."
4 | },
5 | "heading_idle" : {
6 | "message" : ""
7 | },
8 | "heading_error" : {
9 | "message" : "Un problème est survenu !"
10 | },
11 | "heading_saving" : {
12 | "message" : "Sauvegarde..."
13 | },
14 | "heading_saved" : {
15 | "message" : "Sauvegardé dans Pocket"
16 | },
17 | "heading_save_failed" : {
18 | "message" : "Un problème est survenu !"
19 | },
20 | "heading_removing" : {
21 | "message" : "Suppression en cours..."
22 | },
23 | "heading_removed" : {
24 | "message" : "Suppression effectuée"
25 | },
26 | "heading_remove_failed" : {
27 | "message" : "Un problème est survenu !"
28 | },
29 | "heading_tags_saving" : {
30 | "message" : "Sauvegarde des labels..."
31 | },
32 | "heading_tags_saved" : {
33 | "message" : "Labels sauvegardés"
34 | },
35 | "heading_tags_failed" : {
36 | "message" : "Un problème est survenu !"
37 | },
38 | "heading_tags_error" : {
39 | "message" : "Les labels sont limités à 25 caractères"
40 | },
41 | "buttons_remove" : {
42 | "message" : "Supprimer"
43 | },
44 | "buttons_mylist" : {
45 | "message" : "Sauvegardes"
46 | },
47 | "buttons_save" : {
48 | "message" : "Sauvegarder"
49 | },
50 | "context_menu_open_list" : {
51 | "message" : "Ouvrez vos sauvegardes Pocket"
52 | },
53 | "context_menu_discover_more" : {
54 | "message" : "Découvrez-en plus sur Pocket"
55 | },
56 | "context_menu_log_in" : {
57 | "message" : "Se connecter"
58 | },
59 | "context_menu_log_out" : {
60 | "message" : "Déconnexion"
61 | },
62 | "context_menu_save" : {
63 | "message" : "Sauvegarder dans Pocket"
64 | },
65 | "tagging_add_tags" : {
66 | "message" : "Ajouter des labels"
67 | },
68 | "options_header" : {
69 | "message" : "Extension Sauvegarder dans Pocket"
70 | },
71 | "options_login_title" : {
72 | "message" : "Connecté(e) en tant que"
73 | },
74 | "options_log_out" : {
75 | "message" : "Déconnexion"
76 | },
77 | "options_log_in" : {
78 | "message" : "Se connecter"
79 | },
80 | "options_shortcut_title" : {
81 | "message" : "Raccourci clavier"
82 | },
83 | "options_shortcut_record" : {
84 | "message" : "Enregistrer un nouveau raccourci"
85 | },
86 | "options_theme_title" : {
87 | "message" : "Thème"
88 | },
89 | "options_theme_light" : {
90 | "message" : "Clair"
91 | },
92 | "options_theme_dark" : {
93 | "message" : "Foncé"
94 | },
95 | "options_theme_system" : {
96 | "message" : "Utiliser le paramètre système"
97 | },
98 | "options_app_title" : {
99 | "message" : "Application mobile de Pocket"
100 | },
101 | "options_app_apple" : {
102 | "message" : "Télécharger sur l'Apple Store"
103 | },
104 | "options_app_google" : {
105 | "message" : "Télécharger sur Google Play"
106 | },
107 | "options_need_help" : {
108 | "message" : "Besoin d'aide ?"
109 | },
110 | "options_email_us" : {
111 | "message" : "Nous écrire"
112 | },
113 | "options_follow" : {
114 | "message" : "Suivez Pocket"
115 | },
116 | "options_family" : {
117 | "message" : "Pocket fait partie de la famille de produits Mozilla."
118 | },
119 | "options_privacy" : {
120 | "message" : "Politique de confidentialité"
121 | },
122 | "options_terms" : {
123 | "message" : "Conditions générales d'utilisation"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/components/icons/icon.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React from 'react'
3 | import { css } from 'linaria'
4 |
5 | const iconStyle = css`
6 | display: inline-block;
7 | height: 1em;
8 | line-height: 0;
9 | vertical-align: middle;
10 | margin-top: -0.25em;
11 |
12 | svg {
13 | height: 100%;
14 | }
15 | `
16 |
17 | // Higher Order Component that wraps individual icon components. Is not intended
18 | // to be imported directly. Styles applied to this component will be applied to
19 | // all icons components.
20 | const Icon = ({ children, className, id, title, description, ...rest }) => {
21 | const ariaTitle = title ? `${id}-title` : ''
22 | const ariaDescription = description ? `${id}-description` : ''
23 |
24 | return (
25 |
26 | {/* svg child is cloned so that we can insert aria attribute and accessibility
27 | tags based on prop values */}
28 | {React.cloneElement(
29 | children,
30 | {
31 | // note: only pass valid html/svg attributes here
32 | 'aria-labelledby': `${ariaTitle} ${ariaDescription}`,
33 | },
34 | // accessibility tags are passed as children
35 | [
36 | title ? (
37 |
38 | {title}
39 |
40 | ) : null,
41 | description ? (
42 |
43 | {description}
44 |
45 | ) : null,
46 | ]
47 | )}
48 |
49 | )
50 | }
51 |
52 | Icon.propTypes = {
53 | /**
54 | * The svg markup to be rendered, formatted as valid JSX. This is already provided
55 | * automatically when importing a named icon from `src/components/icons`.
56 | */
57 | children: PropTypes.node.isRequired,
58 |
59 | /**
60 | * CSS class name to apply to the wrapping span.
61 | */
62 | className: PropTypes.string,
63 |
64 | /**
65 | * Identifier for accessibility tags, if needed. Associates the icon with
66 | * and tags if provided.
67 | */
68 | id: PropTypes.string,
69 |
70 | /**
71 | * Title tooltip/accessibility helper for the icon. Should be provided along with
72 | * an id if the icon is meaningful and not just presentational. Requires id prop.
73 | */
74 | title: function (props, propName, componentName, ...rest) {
75 | if (props[propName]) {
76 | PropTypes.string(props, propName, componentName, ...rest)
77 |
78 | // id is a prerequisite for title
79 | if (!props.id) {
80 | return new Error(
81 | `"id" prop is also required if ${propName} is passed to ${componentName}`
82 | )
83 | }
84 | }
85 | },
86 |
87 | /**
88 | * Description to associate with title/id to aid non-visual users, to provide
89 | * additional context to the title if necessary. Requires id and title props.
90 | */
91 | description: function (props, propName, componentName, ...rest) {
92 | if (props[propName]) {
93 | PropTypes.string(props, propName, componentName, ...rest)
94 |
95 | // id and title are a prerequisite for description
96 | if (!props.id || !props.title) {
97 | return new Error(
98 | `"id" and "title" props both required if ${propName} is passed to ${componentName}`
99 | )
100 | }
101 | }
102 | },
103 | }
104 |
105 | Icon.defaultProps = {
106 | className: '',
107 | id: '',
108 | title: null,
109 | description: null,
110 | }
111 |
112 | export default Icon
113 |
--------------------------------------------------------------------------------
/src/common/helpers.js:
--------------------------------------------------------------------------------
1 | import { getSetting } from './interface'
2 |
3 | export function isSystemPage(tab) {
4 | return tab.active && isSystemLink(tab.url)
5 | }
6 |
7 | export function isSystemLink(link) {
8 | return (
9 | link.startsWith('chrome://') ||
10 | link.startsWith('chrome-extension://') ||
11 | link.startsWith('chrome-search://')
12 | )
13 | }
14 |
15 | export async function getAccessToken() {
16 | return await getSetting('access_token')
17 | }
18 |
19 | export function checkDuplicate(list, tagValue) {
20 | return list.filter((tag) => tag.name === tagValue).length
21 | }
22 |
23 | export function closeLoginPage() {
24 | chrome.tabs.query(
25 | { url: '*://getpocket.com/extension_login_success*' },
26 | (tabs) => {
27 | chrome.tabs.remove(tabs.map((tab) => tab.id))
28 | },
29 | )
30 | }
31 |
32 | export function deriveItemData(item) {
33 | return {
34 | itemId: item?.item_id,
35 | title: displayTitle(item),
36 | thumbnail: displayThumbnail(item),
37 | publisher: displayPublisher(item)
38 | }
39 | }
40 |
41 | /** TITLE
42 | * @param {object} feedItem An unreliable item returned from a v3 feed endpoint
43 | * @returns {string} The most appropriate title to show
44 | */
45 | export function displayTitle(item) {
46 | return (
47 | item?.title ||
48 | item?.resolved_title ||
49 | item?.given_title ||
50 | item?.display_url ||
51 | displayPublisher(item) ||
52 | null
53 | )
54 | }
55 |
56 | /** PUBLISHER
57 | * @param {object} feedItem An unreliable item returned from a v3 feed endpoint
58 | * @returns {string} The best text to display as the publisher of this item
59 | */
60 | export function displayPublisher(item) {
61 | const urlToUse = item?.given_url || item?.resolved_url
62 | const derivedDomain = domainForUrl(urlToUse)
63 | return (
64 | item?.domain_metadata?.name ||
65 | item?.domain ||
66 | derivedDomain ||
67 | null
68 | )
69 | }
70 |
71 | /**
72 | * DOMAIN FOR URL
73 | * Get the base domain for a given url
74 | * @param {url} url Url to get domain from
75 | * @return {string} parsed domain
76 | */
77 | export function domainForUrl(url) {
78 | if (!url) return false
79 | const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n?=]+)/im)
80 | return match[1]
81 | }
82 |
83 | /** THUMBNAIL
84 | * @param {object} feedItem An unreliable item returned from a v3 feed endpoint
85 | * @returns {string:url} The most appropriate image to show as a thumbnail
86 | */
87 | export function displayThumbnail(item) {
88 | return (
89 | item?.top_image_url ||
90 | item?.image?.src ||
91 | item?.images?.[Object.keys(item.images)[0]]?.src ||
92 | null
93 | )
94 | }
95 |
96 | /**
97 | * Helper function to figure out what the CSS class name should be based on the
98 | * mode name that maps to the current OS color mode.
99 | * @return {String} Formatted CSS class name
100 | */
101 | export function getOSModeClass() {
102 | if (!window.matchMedia) return 'light'
103 |
104 | const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
105 | const isLightMode = window.matchMedia('(prefers-color-scheme: light)').matches
106 | const isNotSpecified = window.matchMedia(
107 | '(prefers-color-scheme: no-preference)'
108 | ).matches
109 | const hasNoSupport = !isDarkMode && !isLightMode && !isNotSpecified
110 | let mode
111 |
112 | if (isLightMode) {
113 | mode = 'light'
114 | }
115 | if (isDarkMode) {
116 | mode = 'dark'
117 | }
118 | // fallback if no system setting
119 | if (isNotSpecified || hasNoSupport) {
120 | mode = 'light'
121 | }
122 |
123 | return mode
124 | }
125 |
--------------------------------------------------------------------------------
/src/common/interface.js:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/browser'
2 |
3 | /* Messaging
4 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
5 | export function sendMessage(message) {
6 | chrome.runtime.sendMessage(message)
7 | }
8 |
9 | /* Browser
10 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
11 | export function openTabWithUrl(url, inBackground) {
12 | let makeTabActive = inBackground === true ? false : true //eslint-disable-line no-unneeded-ternary
13 | return chrome.tabs.create({ url: url, active: makeTabActive })
14 | }
15 |
16 | /* Action Iconography
17 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
18 | export function setDefaultIcon() {
19 | const imageData = inactiveIcon()
20 | chrome.action.setIcon({ imageData })
21 | }
22 |
23 | export function setToolbarIcon(tabId, isSaved) {
24 | const imageData = isSaved ? savedIcon() : inactiveIcon()
25 | chrome.action.setIcon({ tabId, imageData })
26 | }
27 |
28 | export function savedIcon() {
29 | const canvas = new OffscreenCanvas(32, 32)
30 | const context = canvas.getContext('2d')
31 | let saved = new Path2D(
32 | 'M16 31C24.8366 31 32 23.7868 32 14.8889V5.22167C32 3.44208 30.5673 2 28.8 2H3.2C1.43269 2 0 3.43932 0 5.2189V14.8889C0 23.7868 7.16344 31 16 31ZM10.7314 12.1386C10.1065 11.5094 9.09347 11.5094 8.46863 12.1386C7.84379 12.7677 7.84379 13.7878 8.46863 14.417L14.8686 20.8615C15.4935 21.4906 16.5065 21.4906 17.1314 20.8615L23.5314 14.417C24.1562 13.7878 24.1562 12.7677 23.5314 12.1386C22.9065 11.5094 21.8935 11.5094 21.2686 12.1386L16 17.4438L10.7314 12.1386Z',
33 | )
34 | context.clearRect(0, 0, 32, 32)
35 | context.fillStyle = '#EF4056' // Pocket Brand Coral/Red
36 | context.fill(saved, 'evenodd')
37 | return context.getImageData(0, 0, 32, 32)
38 | }
39 |
40 | export function inactiveIcon() {
41 | const canvas = new OffscreenCanvas(32, 32)
42 | const context = canvas.getContext('2d')
43 |
44 | const outer = new Path2D(
45 | 'M0 5.22222C0 3.44264 1.43269 2 3.2 2H28.8C30.5673 2 32 3.44264 32 5.22222H28.8H3.2H0ZM3.2 5.22222H0V14.8889C0 23.7868 7.16344 31 16 31C24.8366 31 32 23.7868 32 14.8889V5.22222H28.8V14.8889C28.8 22.0072 23.0692 27.7778 16 27.7778C8.93075 27.7778 3.2 22.0072 3.2 14.8889V5.22222Z',
46 | )
47 | const inner = new Path2D(
48 | 'M8.46863 12.1386C9.09347 11.5094 10.1065 11.5094 10.7314 12.1386L16 17.4438L21.2686 12.1386C21.8935 11.5094 22.9065 11.5094 23.5314 12.1386C24.1562 12.7677 24.1562 13.7878 23.5314 14.417L17.1314 20.8615C16.5065 21.4906 15.4935 21.4906 14.8686 20.8615L8.46863 14.417C7.84379 13.7878 7.84379 12.7677 8.46863 12.1386Z',
49 | )
50 |
51 | context.clearRect(0, 0, 32, 32)
52 | context.fillStyle = '#EF4056' // Pocket Brand Coral/Red
53 | context.fill(outer, 'evenodd')
54 | context.fill(inner, 'evenodd')
55 | return context.getImageData(0, 0, 32, 32)
56 | }
57 |
58 | /* Local Storage
59 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
60 | export function getSetting(key) {
61 | return new Promise((resolve, reject) => {
62 | chrome.storage.local.get([key], (result) => {
63 | if (chrome.runtime.lastError) {
64 | handleSettingError(chrome.runtime.lastError)
65 | return reject('Error when retrieving local settings. Please contact Pocket Support')
66 | }
67 | resolve(result[key])
68 | })
69 | })
70 | }
71 |
72 | export function setSettings(values) {
73 | return new Promise((resolve, reject) => {
74 | chrome.storage.local.set(values, () => {
75 | if (chrome.runtime.lastError) {
76 | handleSettingError(chrome.runtime.lastError)
77 | return reject('Error when storing local settings. Please contact Pocket Support')
78 | }
79 | resolve()
80 | })
81 | })
82 | }
83 |
84 | function handleSettingError(err) {
85 | console.error(err)
86 |
87 | Sentry.withScope((scope) => {
88 | scope.setFingerprint('Storage Error')
89 | Sentry.captureMessage(err)
90 | })
91 | }
92 |
--------------------------------------------------------------------------------
/src/components/tagging/taginput/taginput.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import PropTypes from 'prop-types'
3 | import AutosizeInput from 'react-input-autosize'
4 | import { css, cx } from 'linaria'
5 |
6 | const inputWrapper = css`
7 | &.input-wrapper {
8 | max-width: 100%;
9 | display: inline-block;
10 | margin: 4px 0;
11 |
12 | input {
13 | all: unset;
14 | color: var(--color-textPrimary);
15 | font-family: var(--fontSansSerif);
16 | display: inline-block;
17 | line-height: 16px;
18 | margin-bottom: 3px;
19 | margin-left: 0;
20 | margin-right: 3px;
21 | margin-top: 3px;
22 | min-width: 0.3em;
23 | padding: 2px 4px;
24 | }
25 | }
26 |
27 | &.error input {
28 | color: var(--color-coral);
29 | }
30 | `
31 |
32 | const BACKSPACE = 8
33 | const COMMA = 44
34 | const TAB = 9
35 | const ENTER = 13
36 | const DELETE = 46
37 | const ESCAPE = 27
38 | const LEFT = 37
39 | const UP = 38
40 | const RIGHT = 39
41 | const DOWN = 40
42 |
43 | export const TagInput = ({
44 | setValue,
45 | handleRemoveAction,
46 | makeTagsInactive,
47 | addTag,
48 | value,
49 | highlightedIndex,
50 | closePanel,
51 | getInputProps,
52 | inputRef,
53 | setFocus,
54 | setBlur,
55 | submitTaggingError
56 | }) => {
57 | const [errorState, setErrorState] = useState(false)
58 |
59 | const setError = () => {
60 | setErrorState(true)
61 | submitTaggingError(true)
62 | }
63 |
64 | const clearError = () => {
65 | setErrorState(false)
66 | submitTaggingError(false)
67 | }
68 |
69 | /* Input Events
70 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
71 | const onChange = (event) => {
72 | setValue(event.target.value)
73 | }
74 |
75 | const onKeyUp = (event) => {
76 | switch (event.keyCode) {
77 | case BACKSPACE:
78 | case DELETE:
79 | clearError()
80 | return handleRemoveAction()
81 | case ESCAPE:
82 | clearError()
83 | setValue('')
84 | return makeTagsInactive(true)
85 | default:
86 | makeTagsInactive()
87 | }
88 | }
89 |
90 | const onInput = (event) => {
91 | if (event.charCode === COMMA || event.keyCode === TAB) {
92 | event.preventDefault()
93 | addTag(value)
94 | }
95 |
96 | if (event.keyCode === ENTER) {
97 | if (highlightedIndex != null) return
98 | event.preventDefault()
99 | if (value) addTag(value)
100 | else closePanel()
101 | }
102 |
103 | if (
104 | value.length > 24 &&
105 | event.keyCode !== BACKSPACE &&
106 | event.keyCode !== DELETE &&
107 | event.keyCode !== LEFT &&
108 | event.keyCode !== UP &&
109 | event.keyCode !== RIGHT &&
110 | event.keyCode !== DOWN &&
111 | event.keyCode !== ENTER
112 | ) {
113 | setError()
114 | event.preventDefault()
115 | return
116 | }
117 | }
118 |
119 | return (
120 |
134 | )
135 | }
136 |
137 | TagInput.propTypes = {
138 | getInputProps: PropTypes.func,
139 | setBlur: PropTypes.func,
140 | setFocus: PropTypes.func,
141 | addTag: PropTypes.func,
142 | handleRemoveAction: PropTypes.func,
143 | makeTagsInactive: PropTypes.func,
144 | setValue: PropTypes.func,
145 | value: PropTypes.string,
146 | hasTags: PropTypes.bool,
147 | submitTaggingError: PropTypes.func
148 | }
149 |
--------------------------------------------------------------------------------
/src/connectors/tagging/tagging.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { getSetting } from 'common/interface'
3 | import { checkDuplicate } from 'common/helpers'
4 | import { Tagging } from 'components/tagging/tagging'
5 |
6 | import { UPDATE_STORED_TAGS } from 'actions'
7 | import { SEND_TAG_ERROR } from 'actions'
8 | import { SUGGESTED_TAGS_SUCCESS } from 'actions'
9 | import { UPDATE_ITEM_PREVIEW } from 'actions'
10 | import { TAGS_SYNC } from 'actions'
11 |
12 | export const TaggingConnector = ({ closePanel }) => {
13 | const [storedTags, setStoredTags] = useState([])
14 | const [suggestedTags, setSuggestedTags] = useState([])
15 | const [usedTags, setUsedTags] = useState([])
16 | const [markedTags, setMarkedTags] = useState([])
17 | const [itemId, setItemId] = useState(null)
18 |
19 | useEffect(async () => {
20 | const storedTagsString = await getSetting('tags_stored')
21 | const storedTagsDraft = (storedTagsString) ? JSON.parse(storedTagsString) : []
22 | setStoredTags(storedTagsDraft)
23 | }, [])
24 |
25 | /* Handle incoming messages
26 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
27 | const handleMessages = (event) => {
28 | const { payload, action = 'Unknown Action' } = event || {}
29 |
30 | switch (action) {
31 | case UPDATE_STORED_TAGS: {
32 | const { tags } = payload
33 | return setStoredTags(tags)
34 | }
35 |
36 | case SUGGESTED_TAGS_SUCCESS: {
37 | const { suggestedTags } = payload
38 | return setSuggestedTags(suggestedTags)
39 | }
40 |
41 | case UPDATE_ITEM_PREVIEW: {
42 | const { item } = payload
43 | setItemId(item?.itemId)
44 | return
45 | }
46 |
47 | default: {
48 | return
49 | }
50 | }
51 | }
52 |
53 | useEffect(() => {
54 | chrome.runtime.onMessage.addListener(handleMessages)
55 | return () => chrome.runtime.onMessage.removeListener(handleMessages)
56 | }, [])
57 |
58 | const submitChanges = (usedDraft) => {
59 | const usedSuggested = usedDraft.filter(usedTag => suggestedTags.includes(usedTag))
60 | const payload = {
61 | item_id: itemId,
62 | tags: usedDraft,
63 | suggestedCount: suggestedTags.length,
64 | usedSuggestedCount: usedSuggested
65 | }
66 |
67 | chrome.runtime.sendMessage({
68 | type: TAGS_SYNC,
69 | payload
70 | })
71 | }
72 |
73 | const addTag = ({ value }) => {
74 | if (checkDuplicate(usedTags, value)) return
75 | const usedDraft = [ ...usedTags, value ]
76 | setUsedTags(usedDraft)
77 | submitChanges(usedDraft)
78 | }
79 |
80 | const activateTag = ({ tag }) => {
81 | // No Tag has been passed in so use the last used tag
82 | const tagValue = tag ? tag : usedTags[usedTags.length - 1]
83 | const isMarked = checkDuplicate(markedTags, tagValue) > 0
84 | const marked = isMarked ? markedTags : [...markedTags, tagValue]
85 |
86 | setMarkedTags(marked)
87 | }
88 |
89 | const deactivateTag = ({ tag }) => {
90 | const marked = markedTags.filter(item => item !== tag)
91 | setMarkedTags(marked)
92 | }
93 |
94 | const deactivateTags = () => {
95 | setMarkedTags([])
96 | }
97 |
98 | const removeTag = ({ tag }) => {
99 | const usedDraft = usedTags.filter(item => item !== tag)
100 | setUsedTags(usedDraft)
101 | submitChanges(usedDraft)
102 | }
103 |
104 | const removeTags = () => {
105 | if (!markedTags.length) return
106 | const usedDraft = usedTags.filter(tag => !markedTags.includes(tag))
107 | setUsedTags(usedDraft)
108 | setMarkedTags([])
109 | submitChanges(usedDraft)
110 | }
111 |
112 | const submitTaggingError = (errorStatus) => {
113 | chrome.runtime.sendMessage({
114 | type: SEND_TAG_ERROR,
115 | payload: { errorStatus }
116 | })
117 | }
118 |
119 | return (
120 |
134 | )
135 | }
136 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Save To Pocket Extension
2 |
3 | 
4 |
5 | ## Introduction
6 |
7 | Save to Pocket is a browser extension that is used to save pages to a connected Pocket account when clicking a toolbar button, selecting a context menu item, or pressing keyboard shortcut. When a page is saved, a “Saved to Pocket” notification appears and offers additional actions, including:
8 |
9 | - Add Tags (with support for Suggested Tags for Pocket Premium subscribers)
10 | - Remove Page
11 | - View List
12 | - Settings
13 |
14 | ## About this Repository
15 |
16 | This is the skeleton structure for the Save to Pocket extension codebase.
17 |
18 | It leverages a `rollup` build script to keep things simple when working with the operational code.
19 |
20 | At this time it is set up to use the following:
21 |
22 | - React
23 | - Jest for testing
24 | - Eslint for JS linting
25 | - Babel for ES6/7
26 | - Linaria
27 | - Rollup
28 | - Storybook
29 |
30 | ## Getting Started
31 |
32 | ### High level steps
33 |
34 | 1. [Prepare your project](#setupanchor)
35 | 1. [Install dependencies](#installanchor)
36 | 1. [Create a development/production build](#buildanchor)
37 | 1. [Load the extension into your browser](#loadinganchor)
38 |
39 | ---
40 |
41 |
42 |
43 | ### Setup
44 |
45 | Before you get started you will need to do the following:
46 |
47 | 1. Register an API key from [https://getpocket.com/developer/](https://getpocket.com/developer/)
48 | 2. Create a keys.json file in the root directory of the project with the
49 | folowing format:
50 |
51 | ```json
52 | {
53 | "browserName": "key"
54 | }
55 | ```
56 |
57 | 3. During the build process it will inject the key into the manifest file
58 |
59 |
60 |
61 | ### Installation
62 |
63 | The app is bundled with rollup via node. You may use NPM to run the build/start/test scripts.
64 |
65 | `npm install`
66 |
67 |
68 |
69 | ### Creating a build
70 |
71 | ##### _Development_
72 |
73 | Run `npm run build`
74 |
75 | This will create an optimized build and place it inside `_build` at the root
76 | directory.
77 |
78 | Running `npm run storybook` will open a development envorinment to allow for building and testing of simple components.
79 |
80 | ##### _Production_
81 |
82 | Run `npm run release`
83 |
84 | This will create an optimized build, zip it up, and place it inside `_releases` at the root
85 | directory.
86 |
87 |
88 |
89 | ### Loading The Extension
90 |
91 | To load the extension:
92 |
93 | 1. Open chrome and navigate to [chrome://extensions](chrome://extensions)
94 | 2. Check the `Developer mode` in the upper right
95 | 3. Select `Load unpacked extension...`
96 | 4. Select the `_build` folder when prompted.
97 |
98 | ---
99 |
100 | ### Package Deployment
101 |
102 | TBD
103 |
104 | ## Third Party Tools Licenses
105 |
106 | - [downshift](https://github.com/downshift-js/downshift) - MIT License (MIT) Copyright (c) 2017 PayPal
107 | - [linaria](https://github.com/callstack/linaria) - MIT License (MIT) Copyright (c) 2017 Callstack
108 | - [match-sorter](https://github.com/kentcdodds/match-sorter) - MIT License (MIT) Copyright (c) 2020 Kent C. Dodds
109 | - [prop-types](https://github.com/facebook/prop-types) - MIT License (MIT) Copyright (c) 2013-present, Facebook, Inc.
110 | - [react](https://github.com/facebook/react) - MIT License (MIT) Copyright (c) 2013-present, Facebook, Inc.
111 | - [react-dom](https://github.com/facebook/react) - MIT License (MIT) Copyright (c) 2013-present, Facebook, Inc.
112 | - [autosize-input](https://github.com/JedWatson/react-input-autosize) - MIT License (MIT) Copyright (c) 2017 Jed Watson.
113 | - [webextension-polyfill](https://github.com/mozilla/webextension-polyfill) - Mozilla Public License Version 2.0
114 |
115 | - [babel](https://github.com/babel/babel) - MIT License (MIT) Copyright (c) 2014-present Sebastian McKenzie and other contributors
116 | - [rollup](https://github.com/rollup/rollup) - MIT License (MIT) Copyright (c) 2017 [contributers](https://github.com/rollup/rollup/graphs/contributors)
117 | - [storybook](https://github.com/storybookjs/storybook/) - MIT License (MIT) Copyright (c) 2017 Kadira Inc.
118 | - [types](https://github.com/DefinitelyTyped/DefinitelyTyped) - MIT License (MIT)
119 | - [cross-env](https://github.com/kentcdodds/cross-env) - MIT License (MIT) Copyright (c) 2017 Kent C. Dodds
120 | - [eslint](https://github.com/eslint/eslint) - Copyright (c) OpenJS Foundation
121 | - [jest](https://github.com/facebook/jest) - MIT License (MIT) Copyright (c) Facebook, Inc.
122 | - [prettier](https://github.com/prettier/prettier) - MIT License (MIT) Copyright (c) James Long
123 | - [styled-jsx](https://github.com/vercel/styled-jsx) - MIT License (MIT) Copyright (c) 2016-present Vercel, Inc.
124 |
--------------------------------------------------------------------------------
/src/pages/injector/app.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { cx } from 'linaria'
3 | import { Doorhanger } from 'components/doorhanger/doorhanger'
4 | import { HeadingConnector } from 'connectors/heading/heading'
5 | import { ItemPreviewConnector } from 'connectors/item-preview/item-preview'
6 | import { TaggingConnector } from 'connectors/tagging/tagging'
7 | import { FooterConnector } from 'connectors/footer/footer'
8 | import { ErrorMessage } from 'components/error-message/error-message'
9 | import { getSetting } from 'common/interface'
10 | import { getOSModeClass } from 'common/helpers'
11 | import { globalVariables, globalReset } from './globalStyles'
12 | import { getBool } from 'common/utilities'
13 |
14 | import { SAVE_TO_POCKET_REQUEST } from 'actions'
15 | import { SAVE_TO_POCKET_SUCCESS } from 'actions'
16 | import { SAVE_TO_POCKET_FAILURE } from 'actions'
17 |
18 | import { REMOVE_ITEM_REQUEST } from 'actions'
19 | import { REMOVE_ITEM_SUCCESS } from 'actions'
20 | import { REMOVE_ITEM_FAILURE } from 'actions'
21 |
22 | import { TAG_SYNC_REQUEST } from 'actions'
23 | import { TAG_SYNC_SUCCESS } from 'actions'
24 | import { TAG_SYNC_FAILURE } from 'actions'
25 | import { UPDATE_TAG_ERROR } from 'actions'
26 |
27 | const IS_RELEASE = getBool(process.env.IS_RELEASE)
28 |
29 | export const App = () => {
30 | const appTarget = useRef(null)
31 | const [saveStatus, setSaveStatus] = useState('saving')
32 | const [isOpen, setIsOpen] = useState(false)
33 | const [theme, setTheme] = useState('pocket-theme-light')
34 | const [errorMessage, setErrorMessage] = useState('')
35 |
36 | /* Handle incoming messages
37 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
38 | const handleMessages = (event) => {
39 | const { payload, action = 'Unknown Action' } = event || {}
40 |
41 | if (!IS_RELEASE) {
42 | console.groupCollapsed(`RECEIVE: ${action}`)
43 | console.log(payload)
44 | console.groupEnd(`RECEIVE: ${action}`)
45 | }
46 |
47 | switch (action) {
48 | case SAVE_TO_POCKET_REQUEST: {
49 | setIsOpen(true)
50 | return setSaveStatus('saving')
51 | }
52 |
53 | case SAVE_TO_POCKET_SUCCESS: {
54 | return setSaveStatus('saved')
55 | }
56 |
57 | case SAVE_TO_POCKET_FAILURE: {
58 | const { message } = payload
59 | setErrorMessage(message)
60 | return setSaveStatus('save_failed')
61 | }
62 |
63 | case REMOVE_ITEM_REQUEST: {
64 | return setSaveStatus('removing')
65 | }
66 |
67 | case REMOVE_ITEM_SUCCESS: {
68 | return setSaveStatus('removed')
69 | }
70 |
71 | case REMOVE_ITEM_FAILURE: {
72 | return setSaveStatus('remove_failed')
73 | }
74 |
75 | case TAG_SYNC_REQUEST: {
76 | return setSaveStatus('tags_saving')
77 | }
78 |
79 | case TAG_SYNC_SUCCESS: {
80 | return setSaveStatus('tags_saved')
81 | }
82 |
83 | case TAG_SYNC_FAILURE: {
84 | return setSaveStatus('tags_failed')
85 | }
86 |
87 | case UPDATE_TAG_ERROR: {
88 | const { errorStatus } = payload
89 | const errorState = errorStatus ? 'tags_error' : 'saved'
90 | return setSaveStatus(errorState)
91 | }
92 |
93 | default: {
94 | return
95 | }
96 | }
97 | }
98 |
99 | useEffect(async () => {
100 | let newTheme = await getSetting('theme') || 'system'
101 | if (newTheme === 'system') newTheme = getOSModeClass()
102 | setTheme(`pocket-theme-${newTheme}`)
103 | }, [])
104 |
105 | const handleDocumentClick = (e) => {
106 | if (appTarget?.current?.contains(e.target)) return
107 | setIsOpen(false)
108 | }
109 |
110 | const keyPress = (e) => {
111 | // keyCode 27 === ESCAPE
112 | if (e.keyCode === 27) setIsOpen(false)
113 | }
114 |
115 | useEffect(() => {
116 | setIsOpen(true)
117 |
118 | chrome.runtime.onMessage.addListener(handleMessages)
119 | document.addEventListener('click', handleDocumentClick)
120 | document.addEventListener('keyup', keyPress)
121 |
122 | return () => {
123 | chrome.runtime.onMessage.removeListener(handleMessages)
124 | document.removeEventListener('click', handleDocumentClick)
125 | document.addEventListener('keyup', keyPress)
126 | }
127 | }, [])
128 |
129 | const closePanel = () => setIsOpen(false)
130 |
131 | const isRemoved = saveStatus === 'removed'
132 | const hasError = saveStatus === 'save_failed'
133 |
134 | return (
135 |
136 |
137 |
138 | {!isRemoved && !hasError ? : null}
139 | {!isRemoved && !hasError ? : null}
140 | {hasError ? : null}
141 | {!hasError ? : null}
142 |
143 |
144 | )
145 | }
146 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing
2 |
3 | Thank you for checking out our Pocket Extensions work. We welcome contributions from everyone! By participating in this project, you agree to abide by the Mozilla [Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
4 |
5 | ### Asking questions / receiving updates
6 |
7 | * Slack channel (Mozilla staff only): #pocket-extensions-dev
8 |
9 | * Mailing List: [pocket-extensions-dev](https://groups.google.com/a/getpocket.com/forum/#!forum/pocket-extensions-dev)
10 |
11 | * File issues/questions on Github: [https://github.com/Pocket/extension-save-to-pocket/issues](https://github.com/Pocket/extension-save-to-pocket/issues). We typically triage new issues every Monday.
12 |
13 | ### Finding Bugs & Filing Tickets
14 |
15 | If you've found a bug, or have a feature idea that you you'd like to see, follow these simple guidelines:
16 |
17 | * Pick a thoughtful and concise title for the issue (ie. *not* Thing Doesn't Work!)
18 |
19 | * Make sure to mention your browser version, OS and basic system parameters (eg. Chrome 62, Windows XP, 512KB RAM)
20 |
21 | * If you can reproduce the bug, give a step-by-step recipe
22 |
23 | * Include stack traces from the console(s) where appropriate
24 |
25 | * Screenshots and screen recordings welcome!
26 |
27 | * When in doubt, take a look at some existing issues and emulate
28 |
29 | ### Contributing Code
30 |
31 | If you are new to the repo, you might want to pay close attention to these tags, as they are typically a great way to get started: Good First Bug, Bug, Chore, and Polish. If you see a bug that is not yet assigned to anyone, start a conversation with an engineer in the ticket itself, expressing your interest in taking the bug. If you take the bug, someone will set the ticket to Assigned to Contributor, so we can be proactive about helping you succeed in fixing the bug.
32 |
33 | When you have some code written, you can open up a Pull Request, get your code reviewed, and see your code merged into the codebase.
34 |
35 | ### Setting up your development environment
36 |
37 | Please review the [README](https://github.com/Pocket/extension-save-to-pocket/blob/master/README.md) for instructions on setting up your development environment, installing dependencies and building the extensions.
38 |
39 | ### Creating Pull Requests
40 |
41 | You have identified the bug, written code and now want to get it into the main repo using a [Pull Request](https://help.github.com/articles/about-pull-requests/).
42 |
43 | All code is added using a pull request against the master branch of our repo. Before submitting a PR, please go through this checklist:
44 |
45 | * All unit tests must pass (and if you haven't written a unit test, please do!)
46 |
47 | * Fill out the pull request template as outlined
48 |
49 | * Please add a PR / Needs Review tag to your PR (if you have permission). This starts the code review process. If you cannot add a tag, don't worry, we will add it during triage.
50 |
51 | * Make sure your PR will merge gracefully with master at the time you create the PR, and that your commit history is 'clean'
52 |
53 | ### Understanding Code Reviews
54 |
55 | You have created a PR and submitted it to the repo, and now are waiting patiently for you code review feedback. One of the projects module owners will be along and will either:
56 |
57 | * Make suggestions for some improvements
58 |
59 | * Give you a 👍 in the comments section, indicating the review is done and the code can be merged
60 |
61 | Typically, you will iterate on the PR, making changes and pushing your changes to new commits on the PR. When the reviewer is satisfied that your code is good-to-go, you will get the coveted R+ comment, and your code can be merged. If you have commit permission, you can go ahead and merge the code to master, otherwise, it will be done for you.
62 |
63 | Our project prides itself on it's respectful, patient and positive attitude when it comes to reviewing contributor's code, and as such, we expect contributors to be respectful, patient and positive in their communications as well.
64 |
65 | ### Writing Good Git Commit Messages
66 |
67 | We like this overview by Chris Beams on "[How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/)".
68 |
69 | The tl;dr is:
70 |
71 | 1. [Separate subject from body with a blank line](https://chris.beams.io/posts/git-commit/#separate)
72 |
73 | 2. [Limit the subject line to 50 characters](https://chris.beams.io/posts/git-commit/#limit-50)
74 |
75 | 3. [Capitalize the subject line](https://chris.beams.io/posts/git-commit/#capitalize)
76 |
77 | 4. [Do not end the subject line with a period](https://chris.beams.io/posts/git-commit/#end)
78 |
79 | 5. [Use a verb to start your subject line (Add, Remove, Fix, Update, Rework, Polish, etc.)](https://chris.beams.io/posts/git-commit/#imperative)
80 |
81 | 6. [Wrap the body at 72 characters](https://chris.beams.io/posts/git-commit/#wrap-72)
82 |
83 | 7. [Use the body to explain *what* and *why* vs. *how*](https://chris.beams.io/posts/git-commit/#why-not-how)
84 |
85 | ### Understanding How Pocket Triages
86 |
87 | The project team meets weekly (in a closed meeting, for the time being), to discuss project priorities, to triage new tickets, and to redistribute the work amongst team members. Any contributors tickets or PRs are carefully considered, prioritized, and if needed, assigned a reviewer.
88 |
89 |
--------------------------------------------------------------------------------
/src/pages/injector/globalStyles.js:
--------------------------------------------------------------------------------
1 | import { css } from 'linaria'
2 |
3 | export const globalReset = css`
4 | // additional class here for when we need more
5 | // specificity to override page styles, but also
6 | // so we don't have to duplicate overrides to both
7 | // light & dark theme classes
8 | &.pocket-extension,
9 | .pocket-extension {
10 | *:after,
11 | *:before,
12 | header,
13 | footer {
14 | all: unset;
15 | }
16 | }
17 | `
18 |
19 | export const globalVariables = css`
20 | :global() {
21 | .pocket-theme-light {
22 | --color-canvas: #FFFFFF;
23 | --color-textPrimary: #1A1A1A;
24 | --color-textSecondary: #666666;
25 | --color-actionPrimary: #008078;
26 | --color-actionPrimaryHover: #004D48;
27 | --color-actionPrimarySubdued: #E8F7F6;
28 | --color-actionPrimaryText: #FFFFFF;
29 | --color-actionSecondary: #1A1A1A;
30 | --color-actionSecondaryHover: #1A1A1A;
31 | --color-actionSecondaryHoverText: #F2F2F2;
32 | --color-actionSecondaryText: #1A1A1A;
33 | --color-actionBrand: #EF4056;
34 | --color-actionFocus: #009990;
35 | --color-formFieldFocusLabel: #008078;
36 | --color-formFieldBorder: #8C8C8C;
37 | --color-formFieldBorderHover: #333333;
38 | --color-error: #B24000;
39 | --color-dividerPrimary: #333333;
40 | --color-dividerTertiary: #D9D9D9;
41 | --color-calloutBackgroundPrimary: #E8F7F6;
42 | --color-inlineButton: #008078;
43 | --color-inlineButtonHover: #008078;
44 | --color-headingBackground: #E8F7F6;
45 | --color-headingErrorBackground: #FDF2F5;
46 | --color-headingIcon: #EF4056;
47 | --color-chipsBackground: #E8F7F6;
48 | --color-chipsText: #004D48;
49 | --color-chipsActive: #1A1A1A;
50 | --color-taggingBorder: #D9D9D9;
51 | --color-taggingShadow: rgba(0, 0, 0, 0.25);
52 | --color-itemPreviewBackground: #F2F2F2;
53 | }
54 |
55 | .pocket-theme-dark {
56 | --color-canvas: #1A1A1A;
57 | --color-textPrimary: #F2F2F2;
58 | --color-textSecondary: #8C8C8C;
59 | --color-actionPrimary: #008078;
60 | --color-actionPrimaryHover: #004D48;
61 | --color-actionPrimarySubdued: #00403C;
62 | --color-actionPrimaryText: #FFFFFF;
63 | --color-actionSecondary: #F2F2F2;
64 | --color-actionSecondaryHover: #F2F2F2;
65 | --color-actionSecondaryHoverText: #1A1A1A;
66 | --color-actionSecondaryText: #F2F2F2;
67 | --color-actionBrand: #EF4056;
68 | --color-actionFocus: #00CCC0;
69 | --color-formFieldFocusLabel: #00A69C;
70 | --color-formFieldBorder: #737373;
71 | --color-formFieldBorderHover: #CCCCCC;
72 | --color-error: #E55300;
73 | --color-dividerPrimary: #CCCCCC;
74 | --color-dividerTertiary: #404040;
75 | --color-calloutBackgroundPrimary: #004D48;
76 | --color-inlineButton: #E8F7F6;
77 | --color-inlineButtonHover: #E8F7F6;
78 | --color-headingBackground: #404040;
79 | --color-headingErrorBackground: #901424;
80 | --color-headingIcon: #FDF2F5;
81 | --color-chipsBackground: #004D48;
82 | --color-chipsText: #ffffff;
83 | --color-chipsActive: #ffffff;
84 | --color-taggingBorder: #8C8C8C;
85 | --color-taggingShadow: rgba(255, 255, 255, 0.25);
86 | --color-itemPreviewBackground: #404040;
87 | }
88 |
89 | .pocket-extension {
90 | --color-white100: #FFFFFF;
91 | --color-grey10: #1A1A1A;
92 | --color-grey20: #333333;
93 | --color-grey25: #404040;
94 | --color-grey30: #4D4D4D;
95 | --color-grey35: #595959;
96 | --color-grey40: #666666;
97 | --color-grey45: #737373;
98 | --color-grey55: #8C8C8C;
99 | --color-grey65: #A6A6A6;
100 | --color-grey80: #CCCCCC;
101 | --color-grey85: #D9D9D9;
102 | --color-grey95: #F2F2F2;
103 | --color-coral: #EF4056;
104 | --color-amber: #FCB643;
105 | --color-brandPocket: #EF4056;
106 | --fontSansSerif: "Graphik Web", "Helvetica Neue", Helvetica, Arial, Sans-Serif;
107 | }
108 | }
109 | `
110 |
111 | export const radioStyles = css`
112 | input[type='radio'] + label,
113 | input[type='checkbox'] + label {
114 | display: inline-block;
115 | vertical-align: middle;
116 | margin: 0 0 0 12px;
117 | }
118 |
119 | input[type='radio'] {
120 | opacity: 0;
121 | margin: 0;
122 |
123 | & + label {
124 | margin: 4px 0;
125 | display: inline-flex;
126 | align-items: center;
127 | min-height: 24px;
128 | position: relative;
129 | padding: 0 24px;
130 | cursor: pointer;
131 | &:before,
132 | &:after {
133 | box-sizing: border-box;
134 | position: absolute;
135 | content: '';
136 | border-radius: 50%;
137 | transition: all 50ms ease;
138 | transition-property: transform, border-color;
139 | }
140 | // radio button border
141 | &:before {
142 | left: -12px;
143 | top: 0;
144 | width: 24px;
145 | height: 24px;
146 | border: 2px solid var(--color-formFieldBorder);
147 | }
148 | // selected radio button inner circle
149 | &:after {
150 | top: 5px;
151 | left: -7px;
152 | width: 14px;
153 | height: 14px;
154 | transform: scale(0);
155 | background: var(--color-actionPrimary);
156 | }
157 | }
158 |
159 | &:hover:enabled {
160 | & + label:before {
161 | border-color: var(--color-actionPrimaryHover);
162 | }
163 | }
164 | &:disabled {
165 | & + label {
166 | opacity: 0.5;
167 | }
168 | &:hover {
169 | & + label:before,
170 | & + label {
171 | cursor: not-allowed;
172 | }
173 | }
174 | }
175 |
176 | &:checked {
177 | & + label:before {
178 | border-color: var(--color-actionPrimary);
179 | }
180 |
181 | & + label:after {
182 | transform: scale(1);
183 | }
184 |
185 | &:hover:enabled,
186 | &:active:enabled {
187 | & + label:before {
188 | border-color: var(--color-actionPrimaryHover);
189 | }
190 | & + label:after {
191 | background: var(--color-actionPrimaryHover);
192 | }
193 | }
194 | }
195 | // same design element regardless of checked or hover
196 | &:focus {
197 | & + label:before {
198 | box-shadow: 0px 0 0 2px var(--color-canvas),
199 | 0px 0 0 4px var(--color-formFieldFocusLabel);
200 | }
201 | }
202 | }
203 | `
204 |
--------------------------------------------------------------------------------
/src/components/tagging/tagging.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { matchSorter } from 'match-sorter'
4 | import { css, cx } from 'linaria'
5 | import Downshift from 'downshift'
6 | import { Suggestions } from './suggestions/suggestions'
7 | import { TagInput } from './taginput/taginput'
8 | import { Chips } from 'components/chips/chips'
9 |
10 | const taggingWrapper = css`
11 | &.tagging-wrapper {
12 | padding: 10px 0 0;
13 | position: relative;
14 | }
15 |
16 | .tagging-placeholder {
17 | color: var(--color-grey45);
18 | position: absolute;
19 | left: 10px;
20 | top: 14px;
21 | }
22 |
23 | .tagging-well {
24 | background-color: var(--color-canvas);
25 | border: 1px solid var(--color-taggingBorder);
26 | border-radius: 4px;
27 | box-sizing: border-box;
28 | font-size: 16px;
29 | font-family: var(--fontSansSerif);
30 | line-height: 16px;
31 | margin: 0;
32 | padding: 4px 5px;
33 | position: relative;
34 | text-align: left;
35 | }
36 |
37 | .typeahead-wrapper {
38 | position: relative;
39 | z-index: 1;
40 | }
41 |
42 | .typeahead-list {
43 | background-color: var(--color-canvas);
44 | border: 1px solid var(--color-taggingBorder);
45 | border-radius: 0 0 4px 4px;
46 | border-top: none;
47 | box-shadow: 0px 2px 4px var(--color-taggingShadow);
48 | box-sizing: border-box;
49 | color: var(--color-textPrimary);
50 | display: block;
51 | left: 0;
52 | list-style-type: none;
53 | margin: 0;
54 | max-height: 8.8em;
55 | overflow-x: hidden;
56 | overflow-y: auto;
57 | padding: 5px;
58 | position: absolute;
59 | top: 0;
60 | width: 100%;
61 | }
62 |
63 | .typeahead-item {
64 | background-color: var(--color-calloutBackgroundPrimary);
65 | border: 1px solid var(--color-calloutBackgroundPrimary);
66 | border-radius: 50px;
67 | color: var(--color-chipsText);
68 | cursor: pointer;
69 | display: inline-block;
70 | font-size: 14px;
71 | font-family: var(--fontSansSerif);
72 | line-height: 16px;
73 | margin-bottom: 4px;
74 | margin-right: 4px;
75 | padding: 8px;
76 | text-align: center;
77 | text-transform: lowercase;
78 | transform: translateZ(0.1);
79 |
80 | &:hover {
81 | border: 1px solid var(--color-chipsActive);
82 | }
83 | }
84 |
85 | .tag-active .typeahead-item {
86 | border: 1px solid var(--color-chipsActive);
87 | }
88 | `
89 |
90 | export const Tagging = ({
91 | usedTags,
92 | markedTags,
93 | suggestedTags,
94 | storedTags,
95 | addTag,
96 | activateTag,
97 | deactivateTag,
98 | deactivateTags,
99 | removeTag,
100 | removeTags,
101 | closePanel,
102 | submitTaggingError
103 | }) => {
104 | const hasTags = usedTags && usedTags.length
105 |
106 | const [placeholder, setPlaceholder] = useState(!hasTags)
107 | const [inputvalue, setInputValue] = useState('')
108 | const inputRef = useRef(null)
109 |
110 | /* Input Management
111 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
112 | const setFocus = () => {
113 | setPlaceholder(false)
114 | }
115 | const setBlur = () => {
116 | const status = inputvalue.length || hasTags
117 | setPlaceholder(!status)
118 | }
119 |
120 | /* Tag Management
121 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
122 | const addTagAction = (value) => {
123 | if (value === '') return
124 | if (usedTags.indexOf(value) >= 0) return
125 | addTag({ value })
126 | setPlaceholder(false)
127 | setInputValue('')
128 | inputRef.current.focus()
129 | }
130 |
131 | /* Active/Inactive Tagging
132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
133 | const makeTagActive = (tag) => {
134 | return activateTag({ tag })
135 | }
136 |
137 | const makeTagInactive = (tag) => {
138 | return deactivateTag({ tag })
139 | }
140 |
141 | const makeTagsInactive = (blur) => {
142 | if (!markedTags.length) return blur ? inputRef.current.blur() : null
143 | deactivateTags()
144 | }
145 |
146 | const handleRemoveAction = () => {
147 | if (inputvalue.length || !hasTags) return
148 | if (!markedTags.length) return makeTagActive()
149 | removeTags()
150 | }
151 |
152 | const removeTagAction = (tag) => {
153 | removeTag({ tag })
154 | }
155 |
156 | const toggleActive = (tag, active) => {
157 | if (active) makeTagInactive(tag)
158 | else makeTagActive(tag)
159 | inputRef.current.focus()
160 | }
161 |
162 | const onMouseUp = e => {
163 | inputRef.current.focus()
164 | e.stopPropagation()
165 | e.preventDefault()
166 | }
167 |
168 | const onSelect = addTagAction
169 |
170 | const storedTagsList = () => {
171 | const filteredStoredTags = storedTags.filter(
172 | item => usedTags.indexOf(item) < 0
173 | )
174 | return inputvalue ? matchSorter(filteredStoredTags, inputvalue) : []
175 | }
176 |
177 | /* Render Component
178 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
179 | return (
180 |
181 |
182 | {({
183 | getInputProps,
184 | getItemProps,
185 | isOpen,
186 | highlightedIndex
187 | }) => (
188 |
189 |
190 | {placeholder && !hasTags && (
191 |
192 | {chrome.i18n.getMessage('tagging_add_tags')}
193 |
194 | )}
195 |
196 | {!!hasTags ? (
197 |
203 | ) : null }
204 |
205 |
221 |
222 |
223 | {!isOpen || !storedTagsList().length ? null : (
224 |
225 |
226 | {storedTagsList().map((item, index) => (
227 |
234 | ))}
235 |
236 |
237 | )}
238 |
239 | )}
240 |
241 |
242 | {suggestedTags ? (
243 |
248 | ) : null}
249 |
250 | )
251 | }
252 |
253 | Tagging.propTypes = {
254 | usedTags: PropTypes.array,
255 | markedTags: PropTypes.array,
256 | suggestedTags: PropTypes.array,
257 | storedTags: PropTypes.array,
258 | activateTag: PropTypes.func,
259 | deactivateTag: PropTypes.func,
260 | deactivateTags: PropTypes.func,
261 | addTag: PropTypes.func,
262 | removeTag: PropTypes.func,
263 | removeTags: PropTypes.func,
264 | closePanel: PropTypes.func,
265 | submitTaggingError: PropTypes.func
266 | }
267 |
--------------------------------------------------------------------------------
/src/pages/background/userActions.js:
--------------------------------------------------------------------------------
1 | import { saveSuccess } from './postSave'
2 | import * as Sentry from '@sentry/browser'
3 |
4 | import { isSystemPage, isSystemLink } from 'common/helpers'
5 | import { getSetting, setSettings } from 'common/interface'
6 | import { closeLoginPage } from 'common/helpers'
7 | import { setToolbarIcon } from 'common/interface'
8 | import { localize } from 'common/locales'
9 |
10 | import { authorize } from 'common/api'
11 | import { getGuid } from 'common/api'
12 | import { saveToPocket } from 'common/api'
13 | import { syncItemTags } from 'common/api'
14 | import { removeItem } from 'common/api'
15 |
16 | import {
17 | AUTH_URL,
18 | LOGOUT_URL,
19 | POCKET_HOME,
20 | POCKET_LIST,
21 | } from 'common/constants'
22 |
23 | import { SAVE_TO_POCKET_REQUEST } from 'actions'
24 | import { SAVE_TO_POCKET_SUCCESS } from 'actions'
25 | import { SAVE_TO_POCKET_FAILURE } from 'actions'
26 |
27 | import { TAG_SYNC_REQUEST } from 'actions'
28 | import { TAG_SYNC_SUCCESS } from 'actions'
29 | import { TAG_SYNC_FAILURE } from 'actions'
30 |
31 | import { REMOVE_ITEM_REQUEST } from 'actions'
32 | import { REMOVE_ITEM_SUCCESS } from 'actions'
33 | import { REMOVE_ITEM_FAILURE } from 'actions'
34 |
35 | import { UPDATE_TAG_ERROR } from 'actions'
36 |
37 | var postAuthSave = null
38 |
39 | /* Browser Action - Toolbar Icon Clicked
40 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
41 | export function browserAction(tab) {
42 | if (isSystemPage(tab)) return openPocketHome() // open list on non-standard pages
43 |
44 | const { id: tabId, title, url: pageUrl } = tab
45 |
46 | save({ pageUrl, title, tabId })
47 | }
48 |
49 | /* Context Clicks - Right/Option Click Menus
50 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
51 | export function contextClick(info, tab) {
52 | const { menuItemId, linkUrl, pageUrl } = info
53 | const { id: tabId, title } = tab
54 |
55 | if (menuItemId === 'toolbarContextClickHome') return openPocketHome()
56 | if (menuItemId === 'toolbarContextClickList') return openPocketList()
57 | if (menuItemId === 'toolbarContextClickLogOut') return logOut()
58 | if (menuItemId === 'toolbarContextClickLogIn') return logIn()
59 |
60 | // Open list on non-standard pages/links
61 | if (isSystemLink(linkUrl || pageUrl)) return openPocketHome()
62 |
63 | return save({ linkUrl, pageUrl, title, tabId })
64 | }
65 |
66 | /* Saving
67 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
68 | async function save({ linkUrl, pageUrl, title, tabId }) {
69 | // send message that we are requesting a save
70 | chrome.tabs.sendMessage(tabId, { action: SAVE_TO_POCKET_REQUEST })
71 |
72 | try {
73 | // Are we authed?
74 | const access_token = await getSetting('access_token')
75 | if (!access_token) return logIn({ linkUrl, pageUrl, title, tabId })
76 |
77 | const url = linkUrl || pageUrl
78 |
79 | const { response: payload } = await saveToPocket({ url, title, tabId })
80 | // send a message with the response
81 | const message = payload
82 | ? { action: SAVE_TO_POCKET_SUCCESS, payload }
83 | : { action: SAVE_TO_POCKET_FAILURE, payload }
84 |
85 | chrome.tabs.sendMessage(tabId, message)
86 |
87 | if (payload) saveSuccess(tabId, { ...payload, isLink: Boolean(linkUrl) })
88 | } catch (error) {
89 | // If it is an auth error let's redirect the user
90 | if (error?.xErrorCode === '107') {
91 | return logIn({ linkUrl, pageUrl, title, tabId })
92 | }
93 |
94 | // Otherwise let's just show the error message
95 | const payload = { message: error }
96 | const errorMessage = { action: SAVE_TO_POCKET_FAILURE, payload }
97 | chrome.tabs.sendMessage(tabId, errorMessage)
98 | }
99 | }
100 |
101 | /* Remove item
102 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
103 | export async function removeItemAction(tab, payload) {
104 | const { id: tabId } = tab
105 | const { itemId } = payload
106 |
107 | // send message that we are attempting to sync tags
108 | chrome.tabs.sendMessage(tabId, { action: REMOVE_ITEM_REQUEST })
109 |
110 | const { response } = await removeItem(itemId)
111 | const message = response
112 | ? { action: REMOVE_ITEM_SUCCESS, payload }
113 | : { action: REMOVE_ITEM_FAILURE, payload }
114 |
115 | chrome.tabs.sendMessage(tabId, message)
116 |
117 | if (response) setToolbarIcon(tabId, false)
118 | }
119 |
120 | /* Add tags to item
121 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
122 | export async function tagsSyncAction(tab, payload) {
123 | const { id: tabId } = tab
124 | const { item_id, tags, ...actionInfo } = payload
125 |
126 | // send message that we are attempting to sync tags
127 | chrome.tabs.sendMessage(tabId, { action: TAG_SYNC_REQUEST })
128 |
129 | const { response } = await syncItemTags(item_id, tags, actionInfo)
130 | const message = response
131 | ? { action: TAG_SYNC_SUCCESS, payload }
132 | : { action: TAG_SYNC_FAILURE, payload }
133 |
134 | chrome.tabs.sendMessage(tabId, message)
135 | }
136 |
137 | /* Submit tags error
138 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
139 | export async function tagsErrorAction(tab, payload) {
140 | const { id: tabId } = tab
141 | chrome.tabs.sendMessage(tabId, { action: UPDATE_TAG_ERROR, payload })
142 | }
143 |
144 | /* Authentication user
145 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
146 | export async function authCodeRecieved(tab, payload) {
147 | // Getting a Guid to use in the request
148 | // Getting an auth token
149 | try {
150 | const guidResponse = await getGuid()
151 | const authResponse = await authorize(guidResponse, payload)
152 | const { access_token, account, username } = authResponse
153 | const { premium_status } = account
154 | setSettings({ access_token, premium_status, username })
155 | } catch (err) {
156 | Sentry.withScope((scope) => {
157 | scope.setFingerprint('Auth Error')
158 | Sentry.captureMessage(err)
159 | })
160 | }
161 |
162 | closeLoginPage()
163 | setContextMenus()
164 |
165 | if (postAuthSave) save(postAuthSave)
166 | postAuthSave = null
167 | }
168 |
169 | export function logOut() {
170 | chrome.tabs.create({ url: LOGOUT_URL })
171 | }
172 |
173 | export function loggedOutOfPocket() {
174 | chrome.storage.local.clear()
175 | setContextMenus()
176 | }
177 |
178 | export function logIn(saveObject) {
179 | postAuthSave = saveObject
180 | chrome.tabs.create({ url: AUTH_URL })
181 | }
182 |
183 | export function openPocket() {
184 | chrome.tabs.create({ url: POCKET_LIST })
185 | }
186 |
187 | export function openPocketList() {
188 | chrome.tabs.create({ url: POCKET_LIST })
189 | }
190 |
191 | export function openPocketHome() {
192 | chrome.tabs.create({ url: POCKET_HOME })
193 | }
194 |
195 | export function openOptionsPage() {
196 | chrome.runtime.openOptionsPage()
197 | }
198 |
199 | /* Tab Changes
200 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
201 | export function tabUpdated(tabId, changeInfo) {
202 | if (changeInfo.status === 'loading' && changeInfo.url) {
203 | // if actively loading a new page, unset save state on icon
204 | setToolbarIcon(tabId, false)
205 | }
206 | }
207 |
208 | /* Theme Changes
209 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
210 | export async function setColorMode(tab, { theme }) {
211 | await setSettings({ theme })
212 | }
213 |
214 | /* Context Menus
215 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
216 | export async function setContextMenus() {
217 | chrome.contextMenus.removeAll()
218 |
219 | // Page Context - Right click menu on page
220 | chrome.contextMenus.create({
221 | title: localize('context_menu_save'),
222 | id: 'pageContextClick',
223 | contexts: ['page', 'frame', 'editable', 'image', 'video', 'audio', 'link', 'selection'], // prettier-ignore
224 | })
225 |
226 | // Browser Icon - Right click menu
227 | chrome.contextMenus.create({
228 | title: localize('context_menu_open_list'),
229 | id: 'toolbarContextClickList',
230 | contexts: ['action'],
231 | })
232 |
233 | chrome.contextMenus.create({
234 | title: localize('context_menu_discover_more'),
235 | id: 'toolbarContextClickHome',
236 | contexts: ['action'],
237 | })
238 |
239 | // Log In or Out menu item depending on existence of access token
240 | const access_token = await getSetting('access_token')
241 | if (access_token) {
242 | chrome.contextMenus.create({
243 | title: localize('context_menu_log_out'),
244 | id: 'toolbarContextClickLogOut',
245 | contexts: ['action'],
246 | })
247 | } else {
248 | chrome.contextMenus.create({
249 | title: localize('context_menu_log_in'),
250 | id: 'toolbarContextClickLogIn',
251 | contexts: ['action'],
252 | })
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/src/components/logo/logo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css } from 'linaria'
3 |
4 | const logoStyle = css`
5 | width: 94px;
6 | height: 24px;
7 |
8 | .pocket-theme-dark & .light {
9 | display: none;
10 | }
11 |
12 | .pocket-theme-light & .dark {
13 | display: none;
14 | }
15 | `
16 |
17 | const LogoLight = () => (
18 |
35 | )
36 |
37 | const LogoDark = () => (
38 |
55 | )
56 |
57 | export const Logo = () => {
58 | return (
59 |
60 |
61 |
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/src/pages/options/options.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom'
2 | import React, { useEffect, useState } from 'react'
3 | import { css, cx } from 'linaria'
4 | import { openTabWithUrl } from 'common/interface'
5 | import { AUTH_URL, LOGOUT_URL, SET_SHORTCUTS } from 'common/constants'
6 | import { getSetting } from 'common/interface'
7 | import { COLOR_MODE_CHANGE } from 'actions'
8 | import { getOSModeClass } from 'common/helpers'
9 | import { Logo } from 'components/logo/logo'
10 | import { Button } from 'components/button/extensions-button'
11 | import { FacebookIcon } from 'components/icons/icons'
12 | import { TwitterIcon } from 'components/icons/icons'
13 | import { InstagramIcon } from 'components/icons/icons'
14 | import { PocketLogoIcon } from 'components/icons/icons'
15 | import { radioStyles } from '../injector/globalStyles'
16 |
17 | const container = css`
18 | color: var(--color-textPrimary);
19 | font-size: 16px;
20 | width: 100vw;
21 | height: 100vh;
22 |
23 | a {
24 | color: var(--color-textPrimary);
25 | text-decoration: underline;
26 | display: inline-block;
27 | }
28 | `
29 | const wrapper = css`
30 | max-width: 550px;
31 | margin: 0 auto;
32 | padding: 100px 20px;
33 | `
34 | const title = css`
35 | font-size: 33px;
36 | line-height: 40px;
37 | font-weight: 600;
38 | margin: 10px 0 15px 0;
39 | `
40 | const header = css`
41 | border-bottom: 1px solid var(--color-dividerPrimary);
42 | margin-bottom: 20px;
43 | `
44 | const user = css`
45 | margin-right: 10px;
46 | margin-bottom: 10px;
47 | display: inline-block;
48 | `
49 | const section = css`
50 | display: flex;
51 | padding: 20px 0;
52 |
53 | @media (max-width: 599px) {
54 | flex-direction: column;
55 | }
56 | `
57 | const sectionLabel = css`
58 | display: flex;
59 | align-items: center;
60 | flex: 1;
61 | font-weight: 500;
62 | `
63 | const sectionAction = css`
64 | flex: 2;
65 |
66 | @media (max-width: 599px) {
67 | margin: 10px 0 0 20px;
68 | }
69 | `
70 | const appIcon = css`
71 | max-height: 40px;
72 | `
73 | const google = css`
74 | margin-left: 10px;
75 | height: 40px;
76 | overflow: hidden;
77 |
78 | img {
79 | margin: -10px 0 0 -10px;
80 | max-height: 60px;
81 | }
82 | `
83 | const footer = css`
84 | font-size: 16px;
85 | margin-top: 40px;
86 | `
87 | const footerLinks = css`
88 | display: flex;
89 | justify-content: space-between;
90 | margin-right: 100px;
91 |
92 | @media (max-width: 479px) {
93 | margin-right: 0;
94 | }
95 | `
96 | const footerFollow = css`
97 | display: flex;
98 | flex-direction: column;
99 | `
100 | const footerFollowIcons = css`
101 | margin-top: 20px;
102 |
103 | .icon {
104 | width: 25px;
105 | height: 25px;
106 | color: var(--color-textPrimary);
107 | }
108 |
109 | a + a {
110 | margin-left: 20px;
111 | }
112 | `
113 | const footerCopyright = css`
114 | display: flex;
115 | align-items: center;
116 | margin-top: 40px;
117 |
118 | @media (max-width: 599px) {
119 | flex-direction: column;
120 | align-items: flex-start;
121 |
122 | .icon {
123 | margin-bottom: 10px;
124 | }
125 | }
126 |
127 | .icon {
128 | height: 25px;
129 | margin-right: 20px;
130 | }
131 |
132 | span,
133 | a {
134 | margin-right: 15px;
135 |
136 | &:last-child {
137 | margin-right: 0;
138 | }
139 | }
140 |
141 | p {
142 | margin-top: 0;
143 | margin-bottom: 10px;
144 | }
145 | `
146 |
147 | const OptionsApp = () => {
148 | const [storedTheme, setStoredTheme] = useState('light')
149 | const [accessToken, setAccessToken] = useState()
150 | const [userName, setUserName] = useState()
151 |
152 | useEffect(async () => {
153 | updateTheme(await getSetting('theme') || 'system')
154 | setAccessToken(await getSetting('access_token'))
155 | setUserName(await getSetting('username'))
156 | }, [])
157 |
158 | const setShortcuts = () => openTabWithUrl(SET_SHORTCUTS)
159 | const logoutAction = () => openTabWithUrl(LOGOUT_URL)
160 | const loginAction = () => openTabWithUrl(AUTH_URL)
161 |
162 | const updateTheme = (mode) => {
163 | chrome.runtime.sendMessage({ type: COLOR_MODE_CHANGE, payload: { theme: mode } })
164 | const newTheme = (mode === 'system') ? getOSModeClass() : mode
165 | setStoredTheme(mode)
166 |
167 | const htmlTag = document && document.documentElement
168 | htmlTag?.classList.toggle(`pocket-theme-light`, newTheme === 'light')
169 | htmlTag?.classList.toggle(`pocket-theme-dark`, newTheme === 'dark')
170 | }
171 |
172 | return (
173 |
174 |
175 |
176 |
177 |
178 | {chrome.i18n.getMessage('options_header')}
179 |
180 |
181 |
182 |
183 |
184 | {chrome.i18n.getMessage('options_login_title')}
185 |
186 |
187 | {(accessToken && userName) ? (
188 |
189 | {userName}
190 |
193 |
194 | ) : (
195 |
198 | )}
199 |
200 |
201 |
202 |
203 |
204 | {chrome.i18n.getMessage('options_shortcut_title')}
205 |
206 |
207 |
210 |
211 |
212 |
213 |
214 |
215 | {chrome.i18n.getMessage('options_app_title')}
216 |
217 |
240 |
241 |
242 |
243 |
244 | {chrome.i18n.getMessage('options_theme_title')}
245 |
246 |
278 |
279 |
280 |
341 |
342 |
343 | )
344 | }
345 |
346 | const root = document.getElementById('pocket-extension-anchor')
347 |
348 | ReactDOM.render(, root)
349 |
--------------------------------------------------------------------------------