Compare commits

...

57 Commits
3.6.0 ... 3.8.1

Author SHA1 Message Date
Athou
04c74b5daa release 3.8.1 2023-07-04 18:40:15 +02:00
Athou
3edb8a3ee2 don't scroll to entry if it's already selected (#1108) 2023-07-04 08:37:56 +02:00
Athou
922346bef6 fetch only ids to improve performance during cleanup 2023-07-01 22:54:28 +02:00
Athou
82cf0e154a release 3.8.0 2023-06-28 20:40:22 +02:00
Athou
efe32e86c9 remove warning about vite not finding custom code at build time 2023-06-27 19:25:44 +02:00
Athou
e208d4ae1e escape input before using it as a regex 2023-06-27 19:11:17 +02:00
Athou
adf20327bd fix broken welcome page mobile layout 2023-06-27 19:05:38 +02:00
Jérémie Panzer
781c41b452 Merge pull request #1107 from canoine/master
Update fr/messages.po
2023-06-27 12:21:17 +02:00
canoine
2b597f9b43 Update fr/messages.po
Translating new entries
2023-06-27 12:19:21 +02:00
Athou
2e26f34135 reduce button spacing on desktop to be able to reduce breakpoint (#1106) 2023-06-27 11:18:56 +02:00
Athou
9e59a472da fix typo 2023-06-27 08:21:30 +02:00
Athou
970043467c make sure there's enough room to show all buttons 2023-06-27 08:21:05 +02:00
Athou
3e903fc6bc use a single call to useContextMenu as recommended in the docs 2023-06-26 20:06:46 +02:00
Athou
95f4cffa7c avoid using sx in feed entry list to improve performance 2023-06-25 21:12:27 +02:00
Athou
6ebe0fa827 memoize feed entry content because Interweave is costly 2023-06-25 20:58:36 +02:00
Athou
488a88fe95 we removed the usage of the deprecated hibernate id generator, we no longer need to ignore warning log messages about it 2023-06-25 07:32:14 +02:00
Athou
d5898a0173 throttle scroll listener 2023-06-24 23:04:52 +02:00
Athou
bdcfbc22bf remove ScrollArea as it causes performance issues on chrome (#1087) 2023-06-24 23:00:25 +02:00
Athou
53b06f41f3 add divider to avoid misclicks 2023-06-24 18:30:26 +02:00
Athou
872247d80f add previous and next buttons (#1096) 2023-06-24 13:30:58 +02:00
Athou
7c226f41db add a setting to always scroll selected entry to the top of the page, even if it fits entirely on screen (#1088) 2023-06-24 09:48:59 +02:00
Athou
bb55a91a14 format absolute dates in popups in user locale instead of GMT 2023-06-22 22:33:53 +02:00
Athou
f140650b4e monaco mobile support is poor, fallback to textarea 2023-06-22 22:19:40 +02:00
Athou
8a64a9db31 host monaco ourselves, don't download it from a CDN 2023-06-22 20:40:28 +02:00
Athou
c1520652f2 move RichCodeEditor to its own component 2023-06-22 18:41:12 +02:00
Athou
90e3044249 wait for tab to be activated to load rich code editor 2023-06-22 16:30:00 +02:00
Athou
f7786d9962 add rich editor for custom code 2023-06-22 14:37:56 +02:00
Athou
aeaaeaee0e fix warning 2023-06-22 10:36:03 +02:00
Athou
4d0a8fd133 fix user count 2023-06-22 07:19:33 +02:00
Athou
b1938c234c use useMobile 2023-06-21 21:58:11 +02:00
Athou
6a5052787d clicking on the body of an entry in expanded mode selects it and marks it as read (#1089) 2023-06-21 20:30:08 +02:00
Athou
877fc33180 move swipe callback next to other callbacks 2023-06-21 20:30:08 +02:00
Jérémie Panzer
8b0b9b1a66 Merge pull request #1092 from canoine/patch-1
Update fr/messages.po
2023-06-21 17:02:10 +02:00
canoine
689c329430 Update fr/messages.po
Traduction des nouveaux messages
2023-06-21 15:45:11 +02:00
Athou
52f911f303 add websocket metrics 2023-06-21 14:20:14 +02:00
Athou
91d0988177 add useMobile 2023-06-21 09:13:20 +02:00
Athou
4f644ba9f5 remove workaround to make popovers follow their target on scroll, it causes lagging issues and was fixed in https://github.com/mantinedev/mantine/issues/3351 (#1087) 2023-06-20 10:46:42 +02:00
Athou
4f699d9675 release 3.7.0 2023-06-20 09:18:18 +02:00
Athou
a5aba6f7ae content is no longer limited to 650px when sidebar is hidden (same as commafeed v2) (#1084) 2023-06-18 12:46:42 +02:00
Athou
78c8711a79 add tooltips to all relative dates with exact time 2023-06-17 22:53:07 +02:00
Athou
8325236d0e hide horizontal scrollbar (#1084) 2023-06-17 22:43:23 +02:00
Athou
437401e73f fix sidebar scrolling (#1084) 2023-06-17 08:37:41 +02:00
Athou
fa06d321d5 restore F shortcut to hide sidebar (#1084) 2023-06-16 21:49:08 +02:00
Athou
d1ddcb6ace resizeable tree (#1084) 2023-06-16 21:24:34 +02:00
Athou
6944d4dc0b fix unreadable api documentation page with dark theme (#1082) 2023-06-16 20:07:36 +02:00
Athou
c835d805b1 restore a version of findNextUpdatable that handles inactive users better than the one we removed a while ago 2023-06-16 15:27:39 +02:00
Athou
4a90e1f69d add some debugging 2023-06-16 13:14:37 +02:00
Athou
fcfeaa462e on user login and in heavy load mode, only force refresh feeds that are up for refresh 2023-06-16 13:14:37 +02:00
Athou
b16978d8fe position is now always set (#1076) 2023-06-15 21:12:10 +02:00
Athou
68c62b4528 no need to push the extension this much 2023-06-14 01:09:31 +02:00
Athou
18f68aab31 fallback to ctrl+click simulation if extension is not installed (#1074 #1075) 2023-06-14 01:03:59 +02:00
Athou
8abb2770ec fix release script, it's the CHANGELOG that needs to be updated 2023-06-13 11:15:29 +02:00
Athou
9156b8b6d0 add a setting to hide commafeed from search engines 2023-06-13 10:51:12 +02:00
Athou
2c32fa1e13 make "b" keyboard shortcut work in extension popup 2023-06-13 10:29:18 +02:00
Athou
7e48afe36c correctly detect the extension if the hook is not used on the initial page 2023-06-12 21:47:54 +02:00
Athou
cd94a3b56f update browser extension badge unread count 2023-06-12 20:54:40 +02:00
Athou
22e0f1f382 use browser extension to open tab in background (#1074) 2023-06-11 17:59:46 +02:00
99 changed files with 1921 additions and 763 deletions

View File

@@ -1,5 +1,32 @@
# Changelog # Changelog
## [3.8.1]
- in expanded mode, don't scroll when clicking on the body of the current entry
- improve content cleanup task performance for instances with a very large number of feeds
## [3.8.0]
- add previous and next buttons in the toolbar
- add a setting to always scroll selected entry to the top of the page, even if it fits entirely on screen
- clicking on the body of an entry in expanded mode selects it and marks it as read
- add rich text editor with autocomplete for custom css and js code in settings (desktop only)
- dramatically improve performance while scrolling
- fix broken welcome page mobile layout
- format dates in user locale instead of GMT in relative date popups
## [3.7.0]
- the sidebar is now resizable
- added the "f" keyboard shortcut to hide the sidebar
- added tooltips to relative dates with the exact date
- add a setting to hide commafeed from search engines (exposes a robots.txt file, enabled by default)
- the browser extension unread count now updates when articles are marked as read/unread in the app
- The "b" keyboard shortcut now works as expected on Chrome but requires the browser extension to be installed
- dark mode has been disabled on the api documentation page as it was unreadable
- improvement to the feed refresh queuing logic when "heavy load" mode is enabled
- fix a bug that could prevent feeds and categories from being edited
## [3.6.0] ## [3.6.0]
- add a button to open CommaFeed in a new tab and a button to open options when using the browser extension - add a button to open CommaFeed in a new tab and a button to open options when using the browser extension
@@ -26,7 +53,8 @@
- add divider to visually separate read-only information from form on the profile settings page - add divider to visually separate read-only information from form on the profile settings page
- reduce javascript bundle size by 30% by loading only the necessary translations - reduce javascript bundle size by 30% by loading only the necessary translations
- add a standalone donate page with all ways to support CommaFeed - add a standalone donate page with all ways to support CommaFeed
- fix an issue introduced in 3.1.0 that could make CommaFeed not refresh feeds as fast as before on instances with lots of feeds - fix an issue introduced in 3.1.0 that could make CommaFeed not refresh feeds as fast as before on instances with lots
of feeds
- fix alignment of icon with text for category tree nodes - fix alignment of icon with text for category tree nodes
- fix alignment of burger button with the rest of the header on mobile - fix alignment of burger button with the rest of the header on mobile
@@ -67,10 +95,10 @@
## [3.0.1] ## [3.0.1]
- allow env variable substitution in config.yml - allow env variable substitution in config.yml
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with - e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with
its value its value
- allow env variable prefixed with `CF_` to override config.yml properties - allow env variable prefixed with `CF_` to override config.yml properties
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true` - e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true`
## [3.0.0] ## [3.0.0]

View File

@@ -4,13 +4,11 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="manifest" href="manifest.json" /> <link rel="manifest" href="manifest.json" />
<link rel="stylesheet" href="custom_css.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>CommaFeed</title> <title>CommaFeed</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
<script src="custom_js.js"></script>
</body> </body>
</html> </html>

View File

@@ -20,11 +20,15 @@
"@mantine/notifications": "^6.0.11", "@mantine/notifications": "^6.0.11",
"@mantine/spotlight": "^6.0.11", "@mantine/spotlight": "^6.0.11",
"@mantine/styles": "^6.0.11", "@mantine/styles": "^6.0.11",
"@monaco-editor/react": "^4.5.1",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"axios": "^1.4.0", "axios": "^1.4.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0", "interweave": "^13.1.0",
"monaco-editor": "^0.38.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"re-resizable": "^6.9.9",
"react": "^18.2.0", "react": "^18.2.0",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0", "react-contexify": "^6.0.0",
@@ -2054,6 +2058,17 @@
"stylis": "4.2.0" "stylis": "4.2.0"
} }
}, },
"node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@emotion/cache": { "node_modules/@emotion/cache": {
"version": "11.11.0", "version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
@@ -3048,6 +3063,30 @@
"moo": "^0.5.1" "moo": "^0.5.1"
} }
}, },
"node_modules/@monaco-editor/loader": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.3.tgz",
"integrity": "sha512-6KKF4CTzcJiS8BJwtxtfyYt9shBiEv32ateQ9T4UVogwn4HM/uPo9iJd2Dmbkpz8CM6Y0PDUpjnZzCwC+eYo2Q==",
"dependencies": {
"state-local": "^1.0.6"
},
"peerDependencies": {
"monaco-editor": ">= 0.21.0 < 1"
}
},
"node_modules/@monaco-editor/react": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.5.1.tgz",
"integrity": "sha512-NNDFdP+2HojtNhCkRfE6/D6ro6pBNihaOzMbGK84lNWzRu+CfBjwzGt4jmnqimLuqp5yE5viHS2vi+QOAnD5FQ==",
"dependencies": {
"@monaco-editor/loader": "^1.3.3"
},
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1", "version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -6666,11 +6705,11 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
}, },
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
"version": "4.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"engines": { "engines": {
"node": ">=10" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -7148,6 +7187,18 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/eslint-scope": { "node_modules/eslint/node_modules/eslint-scope": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
@@ -8998,6 +9049,11 @@
"ufo": "^1.1.2" "ufo": "^1.1.2"
} }
}, },
"node_modules/monaco-editor": {
"version": "0.38.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.38.0.tgz",
"integrity": "sha512-11Fkh6yzEmwx7O0YoLxeae0qEGFwmyPRlVxpg7oF9czOOCB/iCjdJrG5I67da5WiXK3YJCxoz9TJFE8Tfq/v9A=="
},
"node_modules/moo": { "node_modules/moo": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
@@ -10035,6 +10091,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/re-resizable": {
"version": "6.9.9",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.9.tgz",
"integrity": "sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA==",
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react": { "node_modules/react": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -11000,6 +11065,11 @@
"resolved": "https://registry.npmjs.org/stampit/-/stampit-4.3.2.tgz", "resolved": "https://registry.npmjs.org/stampit/-/stampit-4.3.2.tgz",
"integrity": "sha512-pE2org1+ZWQBnIxRPrBM2gVupkuDD0TTNIo1H6GdT/vO82NXli2z8lRE8cu/nBIHrcOCXFBAHpb9ZldrB2/qOA==" "integrity": "sha512-pE2org1+ZWQBnIxRPrBM2gVupkuDD0TTNIo1H6GdT/vO82NXli2z8lRE8cu/nBIHrcOCXFBAHpb9ZldrB2/qOA=="
}, },
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="
},
"node_modules/std-env": { "node_modules/std-env": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz",

View File

@@ -26,11 +26,15 @@
"@mantine/notifications": "^6.0.11", "@mantine/notifications": "^6.0.11",
"@mantine/spotlight": "^6.0.11", "@mantine/spotlight": "^6.0.11",
"@mantine/styles": "^6.0.11", "@mantine/styles": "^6.0.11",
"@monaco-editor/react": "^4.5.1",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"axios": "^1.4.0", "axios": "^1.4.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0", "interweave": "^13.1.0",
"monaco-editor": "^0.38.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"re-resizable": "^6.9.9",
"react": "^18.2.0", "react": "^18.2.0",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0", "react-contexify": "^6.0.0",

View File

@@ -5,7 +5,7 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>3.6.0</version> <version>3.8.1</version>
</parent> </parent>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name> <name>CommaFeed Client</name>

View File

@@ -12,6 +12,7 @@ import { categoryUnreadCount } from "app/utils"
import { ErrorBoundary } from "components/ErrorBoundary" import { ErrorBoundary } from "components/ErrorBoundary"
import { Header } from "components/header/Header" import { Header } from "components/header/Header"
import { Tree } from "components/sidebar/Tree" import { Tree } from "components/sidebar/Tree"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useI18n } from "i18n" import { useI18n } from "i18n"
import { AdminUsersPage } from "pages/admin/AdminUsersPage" import { AdminUsersPage } from "pages/admin/AdminUsersPage"
import { MetricsPage } from "pages/admin/MetricsPage" import { MetricsPage } from "pages/admin/MetricsPage"
@@ -37,7 +38,7 @@ import useLocalStorage from "use-local-storage"
function Providers(props: { children: React.ReactNode }) { function Providers(props: { children: React.ReactNode }) {
const preferredColorScheme = useColorScheme() const preferredColorScheme = useColorScheme()
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>("color-scheme", preferredColorScheme) const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>("color-scheme", preferredColorScheme)
const toggleColorScheme = (value?: ColorScheme) => setColorScheme(value || (colorScheme === "dark" ? "light" : "dark")) const toggleColorScheme = (value?: ColorScheme) => setColorScheme(value ?? (colorScheme === "dark" ? "light" : "dark"))
return ( return (
<I18nProvider i18n={i18n}> <I18nProvider i18n={i18n}>
@@ -65,6 +66,9 @@ function Providers(props: { children: React.ReactNode }) {
const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage")) const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage"))
function AppRoutes() { function AppRoutes() {
const sidebarWidth = useAppSelector(state => state.tree.sidebarWidth)
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
return ( return (
<Routes> <Routes>
<Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} /> <Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} />
@@ -73,7 +77,7 @@ function AppRoutes() {
<Route path="register" element={<RegistrationPage />} /> <Route path="register" element={<RegistrationPage />} />
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} /> <Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
<Route path="api" element={<ApiDocumentationPage />} /> <Route path="api" element={<ApiDocumentationPage />} />
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} />}> <Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} sidebarWidth={sidebarVisible ? sidebarWidth : 0} />}>
<Route path="category"> <Route path="category">
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} /> <Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
<Route path=":id/details" element={<CategoryDetailsPage />} /> <Route path=":id/details" element={<CategoryDetailsPage />} />
@@ -134,13 +138,28 @@ function FaviconHandler() {
const root = useAppSelector(state => state.tree.rootCategory) const root = useAppSelector(state => state.tree.rootCategory)
useEffect(() => { useEffect(() => {
const unreadCount = categoryUnreadCount(root) const unreadCount = categoryUnreadCount(root)
if (unreadCount === 0) Tinycon.reset() if (unreadCount === 0) {
else Tinycon.setBubble(unreadCount) Tinycon.reset()
} else {
Tinycon.setBubble(unreadCount)
}
}, [root]) }, [root])
return null return null
} }
function BrowserExtensionBadgeUnreadCountHandler() {
const root = useAppSelector(state => state.tree.rootCategory)
const { setBadgeUnreadCount } = useBrowserExtension()
useEffect(() => {
if (!root) return
const unreadCount = categoryUnreadCount(root)
setBadgeUnreadCount(unreadCount)
}, [root, setBadgeUnreadCount])
return null
}
export function App() { export function App() {
useI18n() useI18n()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@@ -153,6 +172,7 @@ export function App() {
<Providers> <Providers>
<> <>
<FaviconHandler /> <FaviconHandler />
<BrowserExtensionBadgeUnreadCountHandler />
<HashRouter> <HashRouter>
<GoogleAnalyticsHandler /> <GoogleAnalyticsHandler />
<RedirectHandler /> <RedirectHandler />

View File

@@ -88,14 +88,14 @@ export const Constants = {
layout: { layout: {
mobileBreakpoint: DEFAULT_THEME.breakpoints.md, mobileBreakpoint: DEFAULT_THEME.breakpoints.md,
headerHeight: 60, headerHeight: 60,
sidebarWidth: 350,
entryMaxWidth: 650, entryMaxWidth: 650,
isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight, isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,
isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight, isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight,
}, },
dom: { dom: {
mainScrollAreaId: "main-scroll-area-id",
entryId: (entry: Entry) => `entry-id-${entry.id}`, entryId: (entry: Entry) => `entry-id-${entry.id}`,
entryContextMenuId: (entry: Entry) => entry.id,
}, },
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e", bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
} }

View File

@@ -45,18 +45,27 @@ const initialState: EntriesState = {
const getEndpoint = (sourceType: EntrySourceType) => const getEndpoint = (sourceType: EntrySourceType) =>
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
export const loadEntries = createAsyncThunk<Entries, { source: EntrySource; clearSearch: boolean }, { state: RootState }>( export const loadEntries = createAsyncThunk<
"entries/load", Entries,
async (arg, thunkApi) => { { source: EntrySource; clearSearch: boolean },
if (arg.clearSearch) thunkApi.dispatch(setSearch("")) {
state: RootState
const state = thunkApi.getState()
const endpoint = getEndpoint(arg.source.type)
const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0))
return result.data
} }
) >("entries/load", async (arg, thunkApi) => {
export const loadMoreEntries = createAsyncThunk<Entries, void, { state: RootState }>("entries/loadMore", async (_, thunkApi) => { if (arg.clearSearch) thunkApi.dispatch(setSearch(""))
const state = thunkApi.getState()
const endpoint = getEndpoint(arg.source.type)
const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0))
return result.data
})
export const loadMoreEntries = createAsyncThunk<
Entries,
void,
{
state: RootState
}
>("entries/loadMore", async (_, thunkApi) => {
const state = thunkApi.getState() const state = thunkApi.getState()
const { source } = state.entries const { source } = state.entries
const offset = const offset =
@@ -74,7 +83,13 @@ const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource,
tag: source.type === "tag" ? source.id : undefined, tag: source.type === "tag" ? source.id : undefined,
keywords: state.entries.search, keywords: state.entries.search,
}) })
export const reloadEntries = createAsyncThunk<void, void, { state: RootState }>("entries/reload", async (arg, thunkApi) => { export const reloadEntries = createAsyncThunk<
void,
void,
{
state: RootState
}
>("entries/reload", async (arg, thunkApi) => {
const state = thunkApi.getState() const state = thunkApi.getState()
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false })) thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
}) })
@@ -123,15 +138,18 @@ export const markEntriesUpToEntry = createAsyncThunk<void, Entry, { state: RootS
) )
} }
) )
export const markAllEntries = createAsyncThunk<void, { sourceType: EntrySourceType; req: MarkRequest }, { state: RootState }>( export const markAllEntries = createAsyncThunk<
"entries/entry/markAll", void,
async (arg, thunkApi) => { { sourceType: EntrySourceType; req: MarkRequest },
const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries {
await endpoint(arg.req) state: RootState
thunkApi.dispatch(reloadEntries())
thunkApi.dispatch(reloadTree())
} }
) >("entries/entry/markAll", async (arg, thunkApi) => {
const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries
await endpoint(arg.req)
thunkApi.dispatch(reloadEntries())
thunkApi.dispatch(reloadTree())
})
export const starEntry = createAsyncThunk("entries/entry/star", (arg: { entry: Entry; starred: boolean }) => { export const starEntry = createAsyncThunk("entries/entry/star", (arg: { entry: Entry; starred: boolean }) => {
client.entry.star({ client.entry.star({
id: arg.entry.id, id: arg.entry.id,
@@ -175,31 +193,25 @@ export const selectEntry = createAsyncThunk<
if (arg.scrollToEntry) { if (arg.scrollToEntry) {
const entryElement = document.getElementById(Constants.dom.entryId(entry)) const entryElement = document.getElementById(Constants.dom.entryId(entry))
if (entryElement) { if (entryElement) {
const scrollSpeed = state.user.settings?.scrollSpeed const alwaysScrollToEntry = state.user.settings?.alwaysScrollToEntry
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true)) const entryEntirelyVisible = Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false))) if (alwaysScrollToEntry || !entryEntirelyVisible) {
const scrollSpeed = state.user.settings?.scrollSpeed
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true))
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false)))
}
} }
} }
}) })
const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => { const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
// the entry is entirely visible, no need to scroll scrollToWithCallback({
if (Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)) { options: {
onScrollEnded() // add a small gap between the top of the content and the top of the page
return top: entryElement.offsetTop - Constants.layout.headerHeight - 3,
} behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
},
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) onScrollEnded,
if (scrollArea) { })
scrollToWithCallback({
element: scrollArea,
options: {
// add a small gap between the top of the content and the top of the page
top: entryElement.offsetTop - 3,
behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
},
onScrollEnded,
})
}
} }
export const selectPreviousEntry = createAsyncThunk< export const selectPreviousEntry = createAsyncThunk<
@@ -248,7 +260,13 @@ export const selectNextEntry = createAsyncThunk<
) )
} }
}) })
export const tagEntry = createAsyncThunk<void, TagRequest, { state: RootState }>("entries/entry/tag", async (arg, thunkApi) => { export const tagEntry = createAsyncThunk<
void,
TagRequest,
{
state: RootState
}
>("entries/entry/tag", async (arg, thunkApi) => {
await client.entry.tag(arg) await client.entry.tag(arg)
thunkApi.dispatch(reloadTags()) thunkApi.dispatch(reloadTags())
}) })

View File

@@ -9,10 +9,14 @@ import { redirectTo } from "./redirect"
interface TreeState { interface TreeState {
rootCategory?: Category rootCategory?: Category
mobileMenuOpen: boolean mobileMenuOpen: boolean
sidebarWidth: number
sidebarVisible: boolean
} }
const initialState: TreeState = { const initialState: TreeState = {
mobileMenuOpen: false, mobileMenuOpen: false,
sidebarWidth: 350,
sidebarVisible: true,
} }
export const reloadTree = createAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data)) export const reloadTree = createAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data))
@@ -27,6 +31,12 @@ export const treeSlice = createSlice({
setMobileMenuOpen: (state, action: PayloadAction<boolean>) => { setMobileMenuOpen: (state, action: PayloadAction<boolean>) => {
state.mobileMenuOpen = action.payload state.mobileMenuOpen = action.payload
}, },
setSidebarWidth: (state, action: PayloadAction<number>) => {
state.sidebarWidth = action.payload
},
toggleSidebar: state => {
state.sidebarVisible = !state.sidebarVisible
},
}, },
extraReducers: builder => { extraReducers: builder => {
builder.addCase(reloadTree.fulfilled, (state, action) => { builder.addCase(reloadTree.fulfilled, (state, action) => {
@@ -54,5 +64,5 @@ export const treeSlice = createSlice({
}, },
}) })
export const { setMobileMenuOpen } = treeSlice.actions export const { setMobileMenuOpen, setSidebarWidth, toggleSidebar } = treeSlice.actions
export default treeSlice.reducer export default treeSlice.reducer

View File

@@ -80,6 +80,17 @@ export const changeScrollMarks = createAsyncThunk<
if (!settings) return if (!settings) return
client.user.saveSettings({ ...settings, scrollMarks }) client.user.saveSettings({ ...settings, scrollMarks })
}) })
export const changeAlwaysScrollToEntry = createAsyncThunk<
void,
boolean,
{
state: RootState
}
>("settings/alwaysScrollToEntry", (alwaysScrollToEntry, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, alwaysScrollToEntry })
})
export const changeSharingSetting = createAsyncThunk< export const changeSharingSetting = createAsyncThunk<
void, void,
{ site: keyof SharingSettings; value: boolean }, { site: keyof SharingSettings; value: boolean },
@@ -136,6 +147,10 @@ export const userSlice = createSlice({
if (!state.settings) return if (!state.settings) return
state.settings.scrollMarks = action.meta.arg state.settings.scrollMarks = action.meta.arg
}) })
builder.addCase(changeAlwaysScrollToEntry.pending, (state, action) => {
if (!state.settings) return
state.settings.alwaysScrollToEntry = action.meta.arg
})
builder.addCase(changeSharingSetting.pending, (state, action) => { builder.addCase(changeSharingSetting.pending, (state, action) => {
if (!state.settings) return if (!state.settings) return
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
@@ -146,6 +161,7 @@ export const userSlice = createSlice({
changeScrollSpeed.fulfilled, changeScrollSpeed.fulfilled,
changeShowRead.fulfilled, changeShowRead.fulfilled,
changeScrollMarks.fulfilled, changeScrollMarks.fulfilled,
changeAlwaysScrollToEntry.fulfilled,
changeSharingSetting.fulfilled changeSharingSetting.fulfilled
), ),
() => { () => {

View File

@@ -233,6 +233,7 @@ export interface Settings {
customCss?: string customCss?: string
customJs?: string customJs?: string
scrollSpeed: number scrollSpeed: number
alwaysScrollToEntry: boolean
sharingSettings: SharingSettings sharingSettings: SharingSettings
} }
@@ -271,7 +272,7 @@ export interface Subscription {
iconUrl: string iconUrl: string
unread: number unread: number
categoryId?: string categoryId?: string
position?: number position: number
newestItemTime?: number newestItemTime?: number
filter?: string filter?: string
} }

View File

@@ -1,3 +1,4 @@
import { throttle } from "throttle-debounce"
import { Category } from "./types" import { Category } from "./types"
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void { export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
@@ -26,43 +27,21 @@ export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?:
return { width: placeholderWidth, height: placeholderHeight } return { width: placeholderWidth, height: placeholderHeight }
} }
export const scrollToWithCallback = ({ export const scrollToWithCallback = ({ options, onScrollEnded }: { options: ScrollToOptions; onScrollEnded: () => void }) => {
element,
options,
onScrollEnded,
}: {
element: HTMLElement
options: ScrollToOptions
onScrollEnded: () => void
}) => {
const offset = (options.top ?? 0).toFixed() const offset = (options.top ?? 0).toFixed()
const onScroll = () => { const onScroll = throttle(100, () => {
if (element.offsetTop.toFixed() === offset) { if (window.scrollY.toFixed() === offset) {
element.removeEventListener("scroll", onScroll) window.removeEventListener("scroll", onScroll)
onScrollEnded() onScrollEnded()
} }
} })
window.addEventListener("scroll", onScroll)
element.addEventListener("scroll", onScroll)
// scrollTo does not trigger if there's nothing to do, trigger it manually // scrollTo does not trigger if there's nothing to do, trigger it manually
onScroll() onScroll()
element.scrollTo(options) window.scrollTo(options)
}
export const openLinkInBackgroundTab = (url: string) => {
// simulate ctrl+click to open tab in background
const a = document.createElement("a")
a.href = url
a.rel = "noreferrer"
a.dispatchEvent(
new MouseEvent("click", {
ctrlKey: true,
metaKey: true,
})
)
} }
export const truncate = (str: string, n: number) => (str.length > n ? `${str.slice(0, n - 1)}\u2026` : str) export const truncate = (str: string, n: number) => (str.length > n ? `${str.slice(0, n - 1)}\u2026` : str)

View File

@@ -1,7 +1,7 @@
import { ActionIcon, Button, Tooltip, useMantineTheme } from "@mantine/core" import { ActionIcon, Button, Tooltip, useMantineTheme } from "@mantine/core"
import { ActionIconProps } from "@mantine/core/lib/ActionIcon/ActionIcon" import { ActionIconProps } from "@mantine/core/lib/ActionIcon/ActionIcon"
import { ButtonProps } from "@mantine/core/lib/Button/Button" import { ButtonProps } from "@mantine/core/lib/Button/Button"
import { useMediaQuery } from "@mantine/hooks" import { useActionButton } from "hooks/useActionButton"
import { forwardRef, MouseEventHandler, ReactNode } from "react" import { forwardRef, MouseEventHandler, ReactNode } from "react"
interface ActionButtonProps { interface ActionButtonProps {
@@ -18,9 +18,9 @@ interface ActionButtonProps {
* Switches between Button with label (desktop) and ActionIcon (mobile) * Switches between Button with label (desktop) and ActionIcon (mobile)
*/ */
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => { export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
const { mobile } = useActionButton()
const theme = useMantineTheme() const theme = useMantineTheme()
const variant = props.variant ?? "subtle" const variant = props.variant ?? "subtle"
const mobile = !useMediaQuery(`(min-width: ${theme.breakpoints.lg})`)
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop) const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
return iconOnly ? ( return iconOnly ? (
<Tooltip label={props.label} openDelay={500}> <Tooltip label={props.label} openDelay={500}>

View File

@@ -1,5 +0,0 @@
import { Group } from "@mantine/core"
export function ButtonToolbar(props: { children: React.ReactNode }) {
return <Group spacing={14}>{props.children}</Group>
}

View File

@@ -1,183 +1,200 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Kbd, Table } from "@mantine/core" import { Anchor, Box, Kbd, Stack, Table } from "@mantine/core"
import { Constants } from "app/constants"
export function KeyboardShortcutsHelp() { export function KeyboardShortcutsHelp() {
return ( return (
<Table striped highlightOnHover> <Stack spacing="xs">
<tbody> <Table striped highlightOnHover>
<tr> <tbody>
<td> <tr>
<Trans>Refresh</Trans> <td>
</td> <Trans>Refresh</Trans>
<td> </td>
<Kbd>R</Kbd> <td>
</td> <Kbd>R</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Open next entry</Trans> <td>
</td> <Trans>Open next entry</Trans>
<td> </td>
<Kbd>J</Kbd> <td>
</td> <Kbd>J</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Open previous entry</Trans> <td>
</td> <Trans>Open previous entry</Trans>
<td> </td>
<Kbd>K</Kbd> <td>
</td> <Kbd>K</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Set focus on next entry without opening it</Trans> <td>
</td> <Trans>Set focus on next entry without opening it</Trans>
<td> </td>
<Kbd>N</Kbd> <td>
</td> <Kbd>N</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Set focus on previous entry without opening it</Trans> <td>
</td> <Trans>Set focus on previous entry without opening it</Trans>
<td> </td>
<Kbd>P</Kbd> <td>
</td> <Kbd>P</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Move the page down</Trans> <td>
</td> <Trans>Move the page down</Trans>
<td> </td>
<Kbd> <td>
<Trans>Space</Trans> <Kbd>
</Kbd> <Trans>Space</Trans>
</td> </Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Move the page up</Trans> <td>
</td> <Trans>Move the page up</Trans>
<td> </td>
<Kbd> <td>
<Trans>Shift</Trans> <Kbd>
</Kbd> <Trans>Shift</Trans>
<span> + </span> </Kbd>
<Kbd> <span> + </span>
<Trans>Space</Trans> <Kbd>
</Kbd> <Trans>Space</Trans>
</td> </Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Open/close current entry</Trans> <td>
</td> <Trans>Open/close current entry</Trans>
<td> </td>
<Kbd>O</Kbd> <td>
<span>, </span> <Kbd>O</Kbd>
<Kbd> <span>, </span>
<Trans>Enter</Trans> <Kbd>
</Kbd> <Trans>Enter</Trans>
</td> </Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Open current entry in a new tab</Trans> <td>
</td> <Trans>Open current entry in a new tab</Trans>
<td> </td>
<Kbd>V</Kbd> <td>
</td> <Kbd>V</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Open current entry in a new tab in the background</Trans> <td>
</td> <Trans>Open current entry in a new tab in the background</Trans>
<td> </td>
<Kbd>B</Kbd> <td>
<span>, </span> <Kbd>B</Kbd>
<Kbd> <span>*, </span>
<Trans>Middle click</Trans> <Kbd>
</Kbd> <Trans>Middle click</Trans>
</td> </Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Toggle read status of current entry</Trans> <td>
</td> <Trans>Toggle read status of current entry</Trans>
<td> </td>
<Kbd>M</Kbd> <td>
<span>, </span> <Kbd>M</Kbd>
<Trans>Swipe header to the right</Trans> <span>, </span>
</td> <Trans>Swipe header to the right</Trans>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Mark all entries as read</Trans> <td>
</td> <Trans>Mark all entries as read</Trans>
<td> </td>
<Kbd> <td>
<Trans>Shift</Trans> <Kbd>
</Kbd> <Trans>Shift</Trans>
<span> + </span> </Kbd>
<Kbd>A</Kbd> <span> + </span>
</td> <Kbd>A</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Go to the All view</Trans> <td>
</td> <Trans>Go to the All view</Trans>
<td> </td>
<Kbd>G</Kbd> <td>
<span> </span> <Kbd>G</Kbd>
<Kbd>A</Kbd> <span> </span>
</td> <Kbd>A</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Navigate to a subscription by entering its name</Trans> <td>
</td> <Trans>Navigate to a subscription by entering its name</Trans>
<td> </td>
<Kbd> <td>
<Trans>Ctrl</Trans> <Kbd>
</Kbd> <Trans>Ctrl</Trans>
<span> + </span> </Kbd>
<Kbd>K</Kbd> <span> + </span>
<span>, </span> <Kbd>K</Kbd>
<Kbd>G</Kbd> <span>, </span>
<span> </span> <Kbd>G</Kbd>
<Kbd>U</Kbd> <span> </span>
</td> <Kbd>U</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Show entry menu (desktop)</Trans> <td>
</td> <Trans>Show entry menu (desktop)</Trans>
<td> </td>
<Kbd> <td>
<Trans>Right click</Trans> <Kbd>
</Kbd> <Trans>Right click</Trans>
</td> </Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Show entry menu (mobile)</Trans> <td>
</td> <Trans>Show entry menu (mobile)</Trans>
<td> </td>
<Kbd> <td>
<Trans>Long press</Trans> <Kbd>
</Kbd> <Trans>Long press</Trans>
</td> </Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Show keyboard shortcut help</Trans> <td>
</td> <Trans>Toggle sidebar</Trans>
<td> </td>
<Kbd>?</Kbd> <td>
</td> <Kbd>F</Kbd>
</tr> </td>
</tbody> </tr>
</Table> <tr>
<td>
<Trans>Show keyboard shortcut help</Trans>
</td>
<td>
<Kbd>?</Kbd>
</td>
</tr>
</tbody>
</Table>
<Box>
<span>* </span>
<Anchor href={Constants.browserExtensionUrl} target="_blank" rel="noreferrer">
<Trans>Browser extension required for Chrome</Trans>
</Anchor>
</Box>
</Stack>
) )
} }

View File

@@ -1,4 +1,5 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Tooltip } from "@mantine/core"
import dayjs from "dayjs" import dayjs from "dayjs"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
@@ -10,5 +11,10 @@ export function RelativeDate(props: { date: Date | number | undefined }) {
}, []) }, [])
if (!props.date) return <Trans>N/A</Trans> if (!props.date) return <Trans>N/A</Trans>
return <>{dayjs(props.date).from(dayjs(now))}</> const date = dayjs(props.date)
return (
<Tooltip label={date.toDate().toLocaleString()} openDelay={500}>
<span>{date.from(dayjs(now))}</span>
</Tooltip>
)
} }

View File

@@ -0,0 +1,36 @@
import { Input, Textarea } from "@mantine/core"
import RichCodeEditor from "components/code/RichCodeEditor"
import { useMobile } from "hooks/useMobile"
import { ReactNode } from "react"
interface CodeEditorProps {
description?: ReactNode
language: "css" | "javascript"
value: string
onChange: (value: string | undefined) => void
}
export function CodeEditor(props: CodeEditorProps) {
const mobile = useMobile()
return mobile ? (
// monaco mobile support is poor, fallback to textarea
<Textarea
autosize
minRows={4}
maxRows={15}
description={props.description}
styles={{
input: {
fontFamily: "monospace",
},
}}
value={props.value}
onChange={e => props.onChange(e.currentTarget.value)}
/>
) : (
<Input.Wrapper description={props.description}>
<RichCodeEditor height="30vh" language={props.language} value={props.value} onChange={props.onChange} />
</Input.Wrapper>
)
}

View File

@@ -0,0 +1,52 @@
import { useMantineTheme } from "@mantine/core"
import { Loader } from "components/Loader"
import { useAsync } from "react-async-hook"
const init = async () => {
window.MonacoEnvironment = {
async getWorker(_, label) {
let worker
if (label === "css") {
worker = await import("monaco-editor/esm/vs/language/css/css.worker?worker")
} else if (label === "javascript") {
worker = await import("monaco-editor/esm/vs/language/typescript/ts.worker?worker")
} else {
worker = await import("monaco-editor/esm/vs/editor/editor.worker?worker")
}
// eslint-disable-next-line new-cap
return new worker.default()
},
}
const monacoReact = await import("@monaco-editor/react")
const monaco = await import("monaco-editor")
monacoReact.loader.config({ monaco })
return monacoReact.Editor
}
interface RichCodeEditorProps {
height: number | string
language: "css" | "javascript"
value: string
onChange: (value: string | undefined) => void
}
function RichCodeEditor(props: RichCodeEditorProps) {
const theme = useMantineTheme()
const editorTheme = theme.colorScheme === "dark" ? "vs-dark" : "light"
const { result: Editor } = useAsync(init, [])
if (!Editor) return <Loader />
return (
<Editor
height={props.height}
defaultLanguage={props.language}
theme={editorTheme}
options={{ minimap: { enabled: false } }}
value={props.value}
onChange={props.onChange}
/>
)
}
export default RichCodeEditor

View File

@@ -1,12 +1,14 @@
import { Box, createStyles, Mark, TypographyStylesProvider } from "@mantine/core" import { Box, createStyles, Mark, TypographyStylesProvider } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { useAppSelector } from "app/store"
import { calculatePlaceholderSize } from "app/utils" import { calculatePlaceholderSize } from "app/utils"
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading" import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
import escapeStringRegexp from "escape-string-regexp"
import { ChildrenNode, Interweave, Matcher, MatchResponse, Node, TransformCallback } from "interweave" import { ChildrenNode, Interweave, Matcher, MatchResponse, Node, TransformCallback } from "interweave"
import React from "react"
export interface ContentProps { export interface ContentProps {
content: string content: string
highlight?: string
} }
const useStyles = createStyles(theme => ({ const useStyles = createStyles(theme => ({
@@ -63,7 +65,7 @@ class HighlightMatcher extends Matcher {
constructor(search: string) { constructor(search: string) {
super("highlight") super("highlight")
this.search = search this.search = escapeStringRegexp(search)
} }
match(string: string): MatchResponse<unknown> | null { match(string: string): MatchResponse<unknown> | null {
@@ -82,10 +84,10 @@ class HighlightMatcher extends Matcher {
} }
} }
export function Content(props: ContentProps) { // memoize component because Interweave is costly
const Content = React.memo((props: ContentProps) => {
const { classes } = useStyles() const { classes } = useStyles()
const search = useAppSelector(state => state.entries.search) const matchers = props.highlight ? [new HighlightMatcher(props.highlight)] : []
const matchers = search ? [new HighlightMatcher(search)] : []
return ( return (
<TypographyStylesProvider> <TypographyStylesProvider>
@@ -94,4 +96,6 @@ export function Content(props: ContentProps) {
</Box> </Box>
</TypographyStylesProvider> </TypographyStylesProvider>
) )
} })
export { Content }

View File

@@ -12,13 +12,15 @@ import {
selectPreviousEntry, selectPreviousEntry,
} from "app/slices/entries" } from "app/slices/entries"
import { redirectToRootCategory } from "app/slices/redirect" import { redirectToRootCategory } from "app/slices/redirect"
import { toggleSidebar } from "app/slices/tree"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { openLinkInBackgroundTab } from "app/utils"
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp" import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useMousetrap } from "hooks/useMousetrap" import { useMousetrap } from "hooks/useMousetrap"
import { useViewMode } from "hooks/useViewMode" import { useViewMode } from "hooks/useViewMode"
import { useEffect } from "react" import { useEffect } from "react"
import { useContextMenu } from "react-contexify"
import InfiniteScroll from "react-infinite-scroller" import InfiniteScroll from "react-infinite-scroller"
import { throttle } from "throttle-debounce" import { throttle } from "throttle-debounce"
import { FeedEntry } from "./FeedEntry" import { FeedEntry } from "./FeedEntry"
@@ -29,10 +31,12 @@ export function FeedEntries() {
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) const entriesTimestamp = useAppSelector(state => state.entries.timestamp)
const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId) const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId)
const hasMore = useAppSelector(state => state.entries.hasMore) const hasMore = useAppSelector(state => state.entries.hasMore)
const { viewMode } = useViewMode()
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks) const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry) const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry)
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
const { viewMode } = useViewMode()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { openLinkInBackgroundTab } = useBrowserExtension()
const selectedEntry = entries.find(e => e.id === selectedEntryId) const selectedEntry = entries.find(e => e.id === selectedEntryId)
@@ -56,10 +60,42 @@ export function FeedEntries() {
} }
} }
useEffect(() => { const contextMenu = useContextMenu()
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) const headerRightClicked = (entry: ExpendableEntry, event: React.MouseEvent) => {
event.preventDefault()
contextMenu.show({
id: Constants.dom.entryContextMenuId(entry),
event,
})
}
const listener = () => { const bodyClicked = (entry: ExpendableEntry) => {
if (viewMode !== "expanded") return
// entry is already selected
if (entry.id === selectedEntryId) return
dispatch(
selectEntry({
entry,
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
}
const swipedRight = (entry: ExpendableEntry) => dispatch(markEntry({ entry, read: !entry.read }))
// close context menu on scroll
useEffect(() => {
const listener = throttle(100, () => contextMenu.hideAll())
window.addEventListener("scroll", listener)
return () => window.removeEventListener("scroll", listener)
}, [contextMenu])
useEffect(() => {
const listener = throttle(100, () => {
if (viewMode !== "expanded") return if (viewMode !== "expanded") return
if (scrollingToEntry) return if (scrollingToEntry) return
@@ -81,11 +117,10 @@ export function FeedEntries() {
}) })
) )
} }
} })
const throttledListener = throttle(100, listener) window.addEventListener("scroll", listener)
scrollArea?.addEventListener("scroll", throttledListener) return () => window.removeEventListener("scroll", listener)
return () => scrollArea?.removeEventListener("scroll", throttledListener) }, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry])
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])
useMousetrap("r", () => dispatch(reloadEntries())) useMousetrap("r", () => dispatch(reloadEntries()))
useMousetrap("j", () => useMousetrap("j", () =>
@@ -137,9 +172,8 @@ export function FeedEntries() {
}) })
) )
} else { } else {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) window.scrollTo({
scrollArea?.scrollTo({ top: window.scrollY + document.documentElement.clientHeight * 0.8,
top: scrollArea.scrollTop + scrollArea.clientHeight * 0.8,
behavior: "smooth", behavior: "smooth",
}) })
} }
@@ -176,9 +210,8 @@ export function FeedEntries() {
}) })
) )
} else { } else {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) window.scrollTo({
scrollArea?.scrollTo({ top: window.scrollY - document.documentElement.clientHeight * 0.8,
top: scrollArea.scrollTop - scrollArea.clientHeight * 0.8,
behavior: "smooth", behavior: "smooth",
}) })
} }
@@ -211,7 +244,6 @@ export function FeedEntries() {
window.open(selectedEntry.url, "_blank", "noreferrer") window.open(selectedEntry.url, "_blank", "noreferrer")
}) })
useMousetrap("b", () => { useMousetrap("b", () => {
// simulate ctrl+click to open tab in background
if (!selectedEntry) return if (!selectedEntry) return
openLinkInBackgroundTab(selectedEntry.url) openLinkInBackgroundTab(selectedEntry.url)
}) })
@@ -234,6 +266,7 @@ export function FeedEntries() {
) )
}) })
useMousetrap("g a", () => dispatch(redirectToRootCategory())) useMousetrap("g a", () => dispatch(redirectToRootCategory()))
useMousetrap("f", () => dispatch(toggleSidebar()))
useMousetrap("?", () => useMousetrap("?", () =>
openModal({ openModal({
title: <Trans>Keyboard shortcuts</Trans>, title: <Trans>Keyboard shortcuts</Trans>,
@@ -250,8 +283,6 @@ export function FeedEntries() {
loadMore={() => dispatch(loadMoreEntries())} loadMore={() => dispatch(loadMoreEntries())}
hasMore={hasMore} hasMore={hasMore}
loader={<Loader key={0} />} loader={<Loader key={0} />}
useWindow={false}
getScrollParent={() => document.getElementById(Constants.dom.mainScrollAreaId)}
> >
{entries.map(entry => ( {entries.map(entry => (
<div <div
@@ -265,7 +296,11 @@ export function FeedEntries() {
expanded={!!entry.expanded || viewMode === "expanded"} expanded={!!entry.expanded || viewMode === "expanded"}
selected={entry.id === selectedEntryId} selected={entry.id === selectedEntryId}
showSelectionIndicator={entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")} showSelectionIndicator={entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")}
maxWidth={sidebarVisible ? Constants.layout.entryMaxWidth : undefined}
onHeaderClick={event => headerClicked(entry, event)} onHeaderClick={event => headerClicked(entry, event)}
onHeaderRightClick={event => headerRightClicked(entry, event)}
onBodyClick={() => bodyClicked(entry)}
onSwipedRight={() => swipedRight(entry)}
/> />
</div> </div>
))} ))}

View File

@@ -1,15 +1,13 @@
import { Box, createStyles, Divider, Paper } from "@mantine/core" import { Box, createStyles, Divider, Paper } from "@mantine/core"
import { MantineNumberSize } from "@mantine/styles" import { MantineNumberSize } from "@mantine/styles"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { markEntry } from "app/slices/entries"
import { useAppDispatch } from "app/store"
import { Entry, ViewMode } from "app/types" import { Entry, ViewMode } from "app/types"
import { useViewMode } from "hooks/useViewMode" import { useViewMode } from "hooks/useViewMode"
import React from "react" import React from "react"
import { useSwipeable } from "react-swipeable" import { useSwipeable } from "react-swipeable"
import { FeedEntryBody } from "./FeedEntryBody" import { FeedEntryBody } from "./FeedEntryBody"
import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader" import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader"
import { FeedEntryContextMenu, useFeedEntryContextMenu } from "./FeedEntryContextMenu" import { FeedEntryContextMenu } from "./FeedEntryContextMenu"
import { FeedEntryFooter } from "./FeedEntryFooter" import { FeedEntryFooter } from "./FeedEntryFooter"
import { FeedEntryHeader } from "./FeedEntryHeader" import { FeedEntryHeader } from "./FeedEntryHeader"
@@ -18,30 +16,50 @@ interface FeedEntryProps {
expanded: boolean expanded: boolean
selected: boolean selected: boolean
showSelectionIndicator: boolean showSelectionIndicator: boolean
maxWidth?: number
onHeaderClick: (e: React.MouseEvent) => void onHeaderClick: (e: React.MouseEvent) => void
onHeaderRightClick: (e: React.MouseEvent) => void
onBodyClick: (e: React.MouseEvent) => void
onSwipedRight: () => void
} }
const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: ViewMode }) => { const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: ViewMode }) => {
let backgroundColor let backgroundColor
if (theme.colorScheme === "dark") backgroundColor = props.entry.read ? "inherit" : theme.colors.dark[5] if (theme.colorScheme === "dark") {
else backgroundColor = props.entry.read && !props.expanded ? theme.colors.gray[0] : "inherit" backgroundColor = props.entry.read ? "inherit" : theme.colors.dark[5]
} else {
backgroundColor = props.entry.read && !props.expanded ? theme.colors.gray[0] : "inherit"
}
let marginY = 10 let marginY = 10
if (props.viewMode === "title") marginY = 2 if (props.viewMode === "title") {
else if (props.viewMode === "cozy") marginY = 6 marginY = 2
} else if (props.viewMode === "cozy") {
marginY = 6
}
let mobileMarginY = 6 let mobileMarginY = 6
if (props.viewMode === "title") mobileMarginY = 2 if (props.viewMode === "title") {
else if (props.viewMode === "cozy") mobileMarginY = 4 mobileMarginY = 2
} else if (props.viewMode === "cozy") {
mobileMarginY = 4
}
let backgroundHoverColor = backgroundColor let backgroundHoverColor = backgroundColor
if (!props.expanded && !props.entry.read) { if (!props.expanded && !props.entry.read) {
backgroundHoverColor = theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1] backgroundHoverColor = theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]
} }
const styles = { let paperBorderLeftColor
if (props.showSelectionIndicator) {
const borderLeftColor = theme.colorScheme === "dark" ? theme.colors.orange[4] : theme.colors.orange[6]
paperBorderLeftColor = `${borderLeftColor} !important`
}
return {
paper: { paper: {
backgroundColor, backgroundColor,
borderLeftColor: paperBorderLeftColor,
marginTop: marginY, marginTop: marginY,
marginBottom: marginY, marginBottom: marginY,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
@@ -59,40 +77,36 @@ const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: View
textDecoration: "none", textDecoration: "none",
}, },
body: { body: {
maxWidth: Constants.layout.entryMaxWidth, direction: props.entry.rtl ? "rtl" : "ltr",
maxWidth: props.maxWidth ?? "100%",
}, },
} }
if (props.showSelectionIndicator) {
const borderLeftColor = theme.colorScheme === "dark" ? theme.colors.orange[4] : theme.colors.orange[6]
styles.paper.borderLeftColor = `${borderLeftColor} !important`
}
return styles
}) })
export function FeedEntry(props: FeedEntryProps) { export function FeedEntry(props: FeedEntryProps) {
const { viewMode } = useViewMode() const { viewMode } = useViewMode()
const { classes, cx } = useStyles({ ...props, viewMode }) const { classes, cx } = useStyles({ ...props, viewMode })
const dispatch = useAppDispatch()
const swipeHandlers = useSwipeable({ const swipeHandlers = useSwipeable({
onSwipedRight: () => dispatch(markEntry({ entry: props.entry, read: !props.entry.read })), onSwipedRight: props.onSwipedRight,
}) })
const { onContextMenu } = useFeedEntryContextMenu(props.entry)
let paddingX: MantineNumberSize = "xs" let paddingX: MantineNumberSize = "xs"
if (viewMode === "title" || viewMode === "cozy") paddingX = 6 if (viewMode === "title" || viewMode === "cozy") paddingX = 6
let paddingY: MantineNumberSize = "xs" let paddingY: MantineNumberSize = "xs"
if (viewMode === "title") paddingY = 4 if (viewMode === "title") {
else if (viewMode === "cozy") paddingY = 8 paddingY = 4
} else if (viewMode === "cozy") {
paddingY = 8
}
let borderRadius: MantineNumberSize = "sm" let borderRadius: MantineNumberSize = "sm"
if (viewMode === "title") borderRadius = 0 if (viewMode === "title") {
else if (viewMode === "cozy") borderRadius = "xs" borderRadius = 0
} else if (viewMode === "cozy") {
borderRadius = "xs"
}
const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy") const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy")
return ( return (
@@ -114,7 +128,7 @@ export function FeedEntry(props: FeedEntryProps) {
rel="noreferrer" rel="noreferrer"
onClick={props.onHeaderClick} onClick={props.onHeaderClick}
onAuxClick={props.onHeaderClick} onAuxClick={props.onHeaderClick}
onContextMenu={onContextMenu} onContextMenu={props.onHeaderRightClick}
> >
<Box px={paddingX} py={paddingY} {...swipeHandlers}> <Box px={paddingX} py={paddingY} {...swipeHandlers}>
{compactHeader && <FeedEntryCompactHeader entry={props.entry} />} {compactHeader && <FeedEntryCompactHeader entry={props.entry} />}
@@ -122,8 +136,8 @@ export function FeedEntry(props: FeedEntryProps) {
</Box> </Box>
</a> </a>
{props.expanded && ( {props.expanded && (
<Box px={paddingX} pb={paddingY}> <Box px={paddingX} pb={paddingY} onClick={props.onBodyClick}>
<Box className={classes.body} sx={{ direction: props.entry.rtl ? "rtl" : "ltr" }}> <Box className={classes.body}>
<FeedEntryBody entry={props.entry} /> <FeedEntryBody entry={props.entry} />
</Box> </Box>
<Divider variant="dashed" my={paddingY} /> <Divider variant="dashed" my={paddingY} />

View File

@@ -1,4 +1,5 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { useAppSelector } from "app/store"
import { Entry } from "app/types" import { Entry } from "app/types"
import { Content } from "./Content" import { Content } from "./Content"
import { Enclosure } from "./Enclosure" import { Enclosure } from "./Enclosure"
@@ -9,10 +10,11 @@ export interface FeedEntryBodyProps {
} }
export function FeedEntryBody(props: FeedEntryBodyProps) { export function FeedEntryBody(props: FeedEntryBodyProps) {
const search = useAppSelector(state => state.entries.search)
return ( return (
<Box> <Box>
<Box> <Box>
<Content content={props.entry.content} /> <Content content={props.entry.content} highlight={search} />
</Box> </Box>
{props.entry.enclosureType && props.entry.enclosureUrl && ( {props.entry.enclosureType && props.entry.enclosureUrl && (
<Box pt="md"> <Box pt="md">

View File

@@ -5,11 +5,10 @@ import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries"
import { redirectToFeed } from "app/slices/redirect" import { redirectToFeed } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types" import { Entry } from "app/types"
import { openLinkInBackgroundTab, truncate } from "app/utils" import { truncate } from "app/utils"
import { useEffect } from "react" import { useBrowserExtension } from "hooks/useBrowserExtension"
import { Item, Menu, Separator, useContextMenu } from "react-contexify" import { Item, Menu, Separator } from "react-contexify"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb" import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb"
import { throttle } from "throttle-debounce"
interface FeedEntryContextMenuProps { interface FeedEntryContextMenuProps {
entry: Entry entry: Entry
@@ -28,15 +27,14 @@ const useStyles = createStyles(theme => ({
}, },
})) }))
const menuId = (entry: Entry) => entry.id
export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) { export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
const { classes, theme } = useStyles() const { classes, theme } = useStyles()
const sourceType = useAppSelector(state => state.entries.source.type) const sourceType = useAppSelector(state => state.entries.source.type)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { openLinkInBackgroundTab } = useBrowserExtension()
return ( return (
<Menu id={menuId(props.entry)} theme={theme.colorScheme} animation={false} className={classes.menu}> <Menu id={Constants.dom.entryContextMenuId(props.entry)} theme={theme.colorScheme} animation={false} className={classes.menu}>
<Item <Item
onClick={() => { onClick={() => {
window.open(props.entry.url, "_blank", "noreferrer") window.open(props.entry.url, "_blank", "noreferrer")
@@ -100,29 +98,3 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
</Menu> </Menu>
) )
} }
export function useFeedEntryContextMenu(entry: Entry) {
const contextMenu = useContextMenu({
id: menuId(entry),
})
const onContextMenu = (event: React.MouseEvent) => {
event.preventDefault()
contextMenu.show({
event,
})
}
// close context menu on scroll
useEffect(() => {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
const listener = () => contextMenu.hideAll()
const throttledListener = throttle(100, listener)
scrollArea?.addEventListener("scroll", throttledListener)
return () => scrollArea?.removeEventListener("scroll", throttledListener)
}, [contextMenu])
return { onContextMenu }
}

View File

@@ -1,15 +1,12 @@
import { t, Trans } from "@lingui/macro" import { t, Trans } from "@lingui/macro"
import { Group, Indicator, MultiSelect, Popover } from "@mantine/core" import { Group, Indicator, MultiSelect, Popover } from "@mantine/core"
import { useMediaQuery } from "@mantine/hooks"
import { Constants } from "app/constants"
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/slices/entries" import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/slices/entries"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types" import { Entry } from "app/types"
import { ActionButton } from "components/ActionButtton" import { ActionButton } from "components/ActionButton"
import { ButtonToolbar } from "components/ButtonToolbar" import { useActionButton } from "hooks/useActionButton"
import { useEffect, useState } from "react" import { useMobile } from "hooks/useMobile"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb" import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
import { throttle } from "throttle-debounce"
import { ShareButtons } from "./ShareButtons" import { ShareButtons } from "./ShareButtons"
interface FeedEntryFooterProps { interface FeedEntryFooterProps {
@@ -17,10 +14,10 @@ interface FeedEntryFooterProps {
} }
export function FeedEntryFooter(props: FeedEntryFooterProps) { export function FeedEntryFooter(props: FeedEntryFooterProps) {
const [scrollPosition, setScrollPosition] = useState(0)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings) const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const tags = useAppSelector(state => state.user.tags) const tags = useAppSelector(state => state.user.tags)
const mobile = !useMediaQuery(`(min-width: ${Constants.layout.mobileBreakpoint})`) const mobile = useMobile()
const { spacing } = useActionButton()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const showSharingButtons = sharingSettings && Object.values(sharingSettings).some(v => v) const showSharingButtons = sharingSettings && Object.values(sharingSettings).some(v => v)
@@ -34,19 +31,9 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
}) })
) )
useEffect(() => {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
const listener = () => setScrollPosition(scrollArea ? scrollArea.scrollTop : 0)
const throttledListener = throttle(100, listener)
scrollArea?.addEventListener("scroll", throttledListener)
return () => scrollArea?.removeEventListener("scroll", throttledListener)
}, [])
return ( return (
<Group position="apart"> <Group position="apart">
<ButtonToolbar> <Group spacing={spacing}>
{props.entry.markable && ( {props.entry.markable && (
<ActionButton <ActionButton
icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />} icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />}
@@ -61,7 +48,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
/> />
{showSharingButtons && ( {showSharingButtons && (
<Popover withArrow withinPortal shadow="md" positionDependencies={[scrollPosition]} closeOnClickOutside={!mobile}> <Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
<Popover.Target> <Popover.Target>
<ActionButton icon={<TbShare size={18} />} label={<Trans>Share</Trans>} /> <ActionButton icon={<TbShare size={18} />} label={<Trans>Share</Trans>} />
</Popover.Target> </Popover.Target>
@@ -72,7 +59,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
)} )}
{tags && ( {tags && (
<Popover withArrow withinPortal shadow="md" positionDependencies={[scrollPosition]} closeOnClickOutside={!mobile}> <Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
<Popover.Target> <Popover.Target>
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}> <Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
<ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} /> <ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} />
@@ -96,7 +83,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
<a href={props.entry.url} target="_blank" rel="noreferrer"> <a href={props.entry.url} target="_blank" rel="noreferrer">
<ActionButton icon={<TbExternalLink size={18} />} label={<Trans>Open link</Trans>} /> <ActionButton icon={<TbExternalLink size={18} />} label={<Trans>Open link</Trans>} />
</a> </a>
</ButtonToolbar> </Group>
<ActionButton <ActionButton
icon={<TbArrowBarToDown size={18} />} icon={<TbArrowBarToDown size={18} />}

View File

@@ -1,15 +1,29 @@
import { t, Trans } from "@lingui/macro" import { t, Trans } from "@lingui/macro"
import { ActionIcon, Center, Divider, Indicator, Popover, TextInput } from "@mantine/core" import { ActionIcon, Box, Center, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { reloadEntries, search } from "app/slices/entries" import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/slices/entries"
import { changeReadingMode, changeReadingOrder } from "app/slices/user" import { changeReadingMode, changeReadingOrder } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButtton" import { ActionButton } from "components/ActionButton"
import { ButtonToolbar } from "components/ButtonToolbar"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { useActionButton } from "hooks/useActionButton"
import { useBrowserExtension } from "hooks/useBrowserExtension" import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useMobile } from "hooks/useMobile"
import { useEffect } from "react" import { useEffect } from "react"
import { TbArrowDown, TbArrowUp, TbExternalLink, TbEye, TbEyeOff, TbRefresh, TbSearch, TbSettings, TbUser, TbX } from "react-icons/tb" import {
TbArrowDown,
TbArrowUp,
TbExternalLink,
TbEye,
TbEyeOff,
TbRefresh,
TbSearch,
TbSettings,
TbSortAscending,
TbSortDescending,
TbUser,
TbX,
} from "react-icons/tb"
import { MarkAllAsReadButton } from "./MarkAllAsReadButton" import { MarkAllAsReadButton } from "./MarkAllAsReadButton"
import { ProfileMenu } from "./ProfileMenu" import { ProfileMenu } from "./ProfileMenu"
@@ -17,13 +31,32 @@ function HeaderDivider() {
return <Divider orientation="vertical" /> return <Divider orientation="vertical" />
} }
function HeaderToolbar(props: { children: React.ReactNode }) {
const { spacing } = useActionButton()
const mobile = useMobile("480px")
return mobile ? (
// on mobile use all available width
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
}}
>
{props.children}
</Box>
) : (
<Group spacing={spacing}>{props.children}</Group>
)
}
const iconSize = 18 const iconSize = 18
export function Header() { export function Header() {
const settings = useAppSelector(state => state.user.settings) const settings = useAppSelector(state => state.user.settings)
const profile = useAppSelector(state => state.user.profile) const profile = useAppSelector(state => state.user.profile)
const searchFromStore = useAppSelector(state => state.entries.search) const searchFromStore = useAppSelector(state => state.entries.search)
const { isBrowserExtension, openSettingsPage, openAppInNewTab } = useBrowserExtension() const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const searchForm = useForm<{ search: string }>({ const searchForm = useForm<{ search: string }>({
@@ -42,7 +75,36 @@ export function Header() {
if (!settings) return <Loader /> if (!settings) return <Loader />
return ( return (
<Center> <Center>
<ButtonToolbar> <HeaderToolbar>
<ActionButton
icon={<TbArrowDown size={iconSize} />}
label={<Trans>Next</Trans>}
onClick={() =>
dispatch(
selectNextEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
}
/>
<ActionButton
icon={<TbArrowUp size={iconSize} />}
label={<Trans>Previous</Trans>}
onClick={() =>
dispatch(
selectPreviousEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
}
/>
<HeaderDivider />
<ActionButton <ActionButton
icon={<TbRefresh size={iconSize} />} icon={<TbRefresh size={iconSize} />}
label={<Trans>Refresh</Trans>} label={<Trans>Refresh</Trans>}
@@ -58,7 +120,7 @@ export function Header() {
onClick={() => dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))} onClick={() => dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
/> />
<ActionButton <ActionButton
icon={settings.readingOrder === "asc" ? <TbArrowUp size={iconSize} /> : <TbArrowDown size={iconSize} />} icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>} label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>}
onClick={() => dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))} onClick={() => dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
/> />
@@ -90,7 +152,7 @@ export function Header() {
<ProfileMenu control={<ActionButton icon={<TbUser size={iconSize} />} label={profile?.name} />} /> <ProfileMenu control={<ActionButton icon={<TbUser size={iconSize} />} label={profile?.name} />} />
{isBrowserExtension && ( {isBrowserExtensionPopup && (
<> <>
<HeaderDivider /> <HeaderDivider />
@@ -106,7 +168,7 @@ export function Header() {
/> />
</> </>
)} )}
</ButtonToolbar> </HeaderToolbar>
</Center> </Center>
) )
} }

View File

@@ -3,7 +3,7 @@ import { Trans } from "@lingui/macro"
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core" import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
import { markAllEntries } from "app/slices/entries" import { markAllEntries } from "app/slices/entries"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButtton" import { ActionButton } from "components/ActionButton"
import { useState } from "react" import { useState } from "react"
import { TbChecks } from "react-icons/tb" import { TbChecks } from "react-icons/tb"

View File

@@ -1,11 +1,8 @@
import { Box, MediaQuery } from "@mantine/core" import { Box } from "@mantine/core"
import { Constants } from "app/constants" import { useMobile } from "hooks/useMobile"
import React from "react" import React from "react"
export function OnDesktop(props: { children: React.ReactNode }) { export function OnDesktop(props: { children: React.ReactNode }) {
return ( const mobile = useMobile()
<MediaQuery smallerThan={Constants.layout.mobileBreakpoint} styles={{ display: "none" }}> return <Box>{!mobile && props.children}</Box>
<Box>{props.children}</Box>
</MediaQuery>
)
} }

View File

@@ -1,11 +1,8 @@
import { Box, MediaQuery } from "@mantine/core" import { Box } from "@mantine/core"
import { Constants } from "app/constants" import { useMobile } from "hooks/useMobile"
import React from "react" import React from "react"
export function OnMobile(props: { children: React.ReactNode }) { export function OnMobile(props: { children: React.ReactNode }) {
return ( const mobile = useMobile()
<MediaQuery largerThan={Constants.layout.mobileBreakpoint} styles={{ display: "none" }}> return <Box>{mobile && props.children}</Box>
<Box>{props.children}</Box>
</MediaQuery>
)
} }

View File

@@ -1,96 +1,83 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Box, Button, Group, Stack, Textarea } from "@mantine/core" import { Box, Button, Group, Stack } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToSelectedSource } from "app/slices/redirect" import { redirectToSelectedSource } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useEffect } from "react" import { CodeEditor } from "components/code/CodeEditor"
import { useAsyncCallback } from "react-async-hook" import { useEffect } from "react"
import { TbDeviceFloppy } from "react-icons/tb" import { useAsyncCallback } from "react-async-hook"
import { TbDeviceFloppy } from "react-icons/tb"
interface FormData {
customCss: string interface FormData {
customJs: string customCss: string
} customJs: string
}
export function CustomCodeSettings() {
const settings = useAppSelector(state => state.user.settings) export function CustomCodeSettings() {
const dispatch = useAppDispatch() const settings = useAppSelector(state => state.user.settings)
const dispatch = useAppDispatch()
const form = useForm<FormData>()
const { setValues } = form const form = useForm<FormData>()
const { setValues } = form
const saveCustomCode = useAsyncCallback(
async (d: FormData) => { const saveCustomCode = useAsyncCallback(
if (!settings) return async (d: FormData) => {
await client.user.saveSettings({ if (!settings) return
...settings, await client.user.saveSettings({
customCss: d.customCss, ...settings,
customJs: d.customJs, customCss: d.customCss,
}) customJs: d.customJs,
}, })
{ },
onSuccess: () => { {
window.location.reload() onSuccess: () => {
}, window.location.reload()
} },
) }
)
useEffect(() => {
if (!settings) return useEffect(() => {
setValues({ if (!settings) return
customCss: settings.customCss, setValues({
customJs: settings.customJs, customCss: settings.customCss,
}) customJs: settings.customJs,
}, [setValues, settings]) })
}, [setValues, settings])
return (
<> return (
{saveCustomCode.error && ( <>
<Box mb="md"> {saveCustomCode.error && (
<Alert messages={errorToStrings(saveCustomCode.error)} /> <Box mb="md">
</Box> <Alert messages={errorToStrings(saveCustomCode.error)} />
)} </Box>
)}
<form onSubmit={form.onSubmit(saveCustomCode.execute)}>
<Stack> <form onSubmit={form.onSubmit(saveCustomCode.execute)}>
<Textarea <Stack>
autosize <CodeEditor
minRows={4} description={<Trans>Custom CSS rules that will be applied</Trans>}
maxRows={15} language="css"
{...form.getInputProps("customCss")} {...form.getInputProps("customCss")}
description={<Trans>Custom CSS rules that will be applied</Trans>} />
styles={{
input: { <CodeEditor
fontFamily: "monospace", description={<Trans>Custom JS code that will be executed on page load</Trans>}
}, language="javascript"
}} {...form.getInputProps("customJs")}
/> />
<Textarea <Group>
autosize <Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
minRows={4} <Trans>Cancel</Trans>
maxRows={15} </Button>
{...form.getInputProps("customJs")} <Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}>
description={<Trans>Custom JS code that will be executed on page load</Trans>} <Trans>Save</Trans>
styles={{ </Button>
input: { </Group>
fontFamily: "monospace", </Stack>
}, </form>
}} </>
/> )
}
<Group>
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}>
<Trans>Save</Trans>
</Button>
</Group>
</Stack>
</form>
</>
)
}

View File

@@ -1,7 +1,14 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core" import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { changeLanguage, changeScrollMarks, changeScrollSpeed, changeSharingSetting, changeShowRead } from "app/slices/user" import {
changeAlwaysScrollToEntry,
changeLanguage,
changeScrollMarks,
changeScrollSpeed,
changeSharingSetting,
changeShowRead,
} from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { SharingSettings } from "app/types" import { SharingSettings } from "app/types"
import { locales } from "i18n" import { locales } from "i18n"
@@ -11,6 +18,7 @@ export function DisplaySettings() {
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed) const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
const showRead = useAppSelector(state => state.user.settings?.showRead) const showRead = useAppSelector(state => state.user.settings?.showRead)
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks) const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
const alwaysScrollToEntry = useAppSelector(state => state.user.settings?.alwaysScrollToEntry)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings) const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@@ -32,6 +40,12 @@ export function DisplaySettings() {
onChange={e => dispatch(changeScrollSpeed(e.currentTarget.checked))} onChange={e => dispatch(changeScrollSpeed(e.currentTarget.checked))}
/> />
<Switch
label={<Trans>Always scroll selected entry to the top of the page, even if it fits entirely on screen</Trans>}
checked={alwaysScrollToEntry}
onChange={e => dispatch(changeAlwaysScrollToEntry(e.currentTarget.checked))}
/>
<Switch <Switch
label={<Trans>Show feeds and categories with no unread entries</Trans>} label={<Trans>Show feeds and categories with no unread entries</Trans>}
checked={showRead} checked={showRead}

View File

@@ -0,0 +1,9 @@
import { useMantineTheme } from "@mantine/core"
import { useMobile } from "hooks/useMobile"
export const useActionButton = () => {
const theme = useMantineTheme()
const mobile = useMobile(theme.breakpoints.xl)
const spacing = mobile ? 14 : 0
return { mobile, spacing }
}

View File

@@ -1,9 +1,64 @@
import { useEffect, useState } from "react"
export const useBrowserExtension = () => { export const useBrowserExtension = () => {
// the extension will set the "browser-extension-installed" attribute on the root element
const [browserExtensionVersion, setBrowserExtensionVersion] = useState(
document.documentElement.getAttribute("browser-extension-installed")
)
// monitor the attribute on the root element as it may change after the page was loaded
useEffect(() => {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === "attributes") {
const element = mutation.target as Element
const version = element.getAttribute("browser-extension-installed")
if (version) setBrowserExtensionVersion(version)
}
})
})
observer.observe(document.documentElement, {
attributes: true,
})
return () => observer.disconnect()
}, [])
// when not in an iframe, window.parent is a reference to window // when not in an iframe, window.parent is a reference to window
const isBrowserExtension = window.parent !== window const isBrowserExtensionPopup = window.parent !== window
const isBrowserExtensionInstalled = isBrowserExtensionPopup || !!browserExtensionVersion
const isBrowserExtensionInstallable = !isBrowserExtensionPopup
const openSettingsPage = () => window.parent.postMessage("open-settings-page", "*") const w = isBrowserExtensionPopup ? window.parent : window
const openAppInNewTab = () => window.parent.postMessage("open-app-in-new-tab", "*") const openSettingsPage = () => w.postMessage("open-settings-page", "*")
const openAppInNewTab = () => w.postMessage("open-app-in-new-tab", "*")
const openLinkInBackgroundTab = (url: string) => {
if (isBrowserExtensionInstalled) {
w.postMessage(`open-link-in-background-tab:${url}`, "*")
} else {
// fallback to ctrl+click simulation
const a = document.createElement("a")
a.href = url
a.rel = "noreferrer"
a.dispatchEvent(
new MouseEvent("click", {
ctrlKey: true,
metaKey: true,
})
)
}
}
const setBadgeUnreadCount = (count: number) => w.postMessage(`set-badge-unread-count:${count}`, "*")
return { isBrowserExtension, openSettingsPage, openAppInNewTab } return {
browserExtensionVersion,
isBrowserExtensionInstallable,
isBrowserExtensionInstalled,
isBrowserExtensionPopup,
openSettingsPage,
openAppInNewTab,
openLinkInBackgroundTab,
setBadgeUnreadCount,
}
} }

View File

@@ -0,0 +1,4 @@
import { useMediaQuery } from "@mantine/hooks"
import { Constants } from "app/constants"
export const useMobile = (breakpoint: string = Constants.layout.mobileBreakpoint) => !useMediaQuery(`(min-width: ${breakpoint})`)

View File

@@ -71,6 +71,10 @@ msgstr "إداري"
msgid "All" msgid "All"
msgstr "الكل" msgstr "الكل"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "تم إرسال بريد إلكتروني إذا تم تسجيل هذا العنوان. " msgstr "تم إرسال بريد إلكتروني إذا تم تسجيل هذا العنوان. "
@@ -127,9 +131,13 @@ msgstr "العودة"
msgid "Back to log in" msgid "Back to log in"
msgstr "العودة لتسجيل الدخول" msgstr "العودة لتسجيل الدخول"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "ملحقات المستعرض" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "سيؤدي تغيير كلمة المرور إلى إنشاء مفتاح
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "تأكد من عمل الخلاصة" msgstr "تأكد من عمل الخلاصة"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed التالي العنصر غير المقروء" msgstr "CommaFeed التالي العنصر غير المقروء"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "إصدار CommaFeed {الإصدار} ({مراجعة})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "الأحدث أولاً" msgstr "الأحدث أولاً"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "التالي" msgstr "التالي"
@@ -627,6 +640,10 @@ msgstr "كلمات المرور غير متطابقة"
msgid "Position" msgid "Position"
msgstr "المنـصب" msgstr "المنـصب"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "الملف الشخصي" msgstr "الملف الشخصي"
@@ -793,6 +810,10 @@ msgstr "الموضوع"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "تبديل قراءة حالة الإدخال الحالي" msgstr "تبديل قراءة حالة الإدخال الحالي"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي" msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي"

View File

@@ -71,6 +71,10 @@ msgstr "Administrador"
msgid "All" msgid "All"
msgstr "Tot" msgstr "Tot"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "S'ha enviat un correu electrònic si aquesta adreça estava registrada. " msgstr "S'ha enviat un correu electrònic si aquesta adreça estava registrada. "
@@ -127,9 +131,13 @@ msgstr "Enrere"
msgid "Back to log in" msgid "Back to log in"
msgstr "Tornar a iniciar sessió" msgstr "Tornar a iniciar sessió"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Extensions del navegador" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Canviar la contrasenya generarà una nova clau d'API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Comproveu que el canal funciona" msgstr "Comproveu que el canal funciona"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed següent element no llegit" msgstr "CommaFeed següent element no llegit"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Versió CommaFeed {versió} ({revisió})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "El més nou primer" msgstr "El més nou primer"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Següent" msgstr "Següent"
@@ -627,6 +640,10 @@ msgstr "Les contrasenyes no coincideixen"
msgid "Position" msgid "Position"
msgstr "Posició" msgstr "Posició"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
@@ -793,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Canvia l'estat de lectura de l'entrada actual" msgstr "Canvia l'estat de lectura de l'entrada actual"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo" msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Správce"
msgid "All" msgid "All"
msgstr "Všechny" msgstr "Všechny"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Pokud byla tato adresa zaregistrována, byl odeslán e-mail. " msgstr "Pokud byla tato adresa zaregistrována, byl odeslán e-mail. "
@@ -127,9 +131,13 @@ msgstr "Zpět"
msgid "Back to log in" msgid "Back to log in"
msgstr "Zpět k přihlášení" msgstr "Zpět k přihlášení"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Rozšíření prohlížeče" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Změna hesla vygeneruje nový klíč API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Zkontrolujte, zda zdroj funguje" msgstr "Zkontrolujte, zda zdroj funguje"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed další nepřečtená položka" msgstr "CommaFeed další nepřečtená položka"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed verze {version} ({revision})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Nejnovější jako první" msgstr "Nejnovější jako první"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Další" msgstr "Další"
@@ -627,6 +640,10 @@ msgstr "Hesla se neshodují"
msgid "Position" msgid "Position"
msgstr "Pozice" msgstr "Pozice"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Téma"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Přepne stav čtení aktuálního záznamu" msgstr "Přepne stav čtení aktuálního záznamu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo" msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Gweinyddol"
msgid "All" msgid "All"
msgstr "Pawb" msgstr "Pawb"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Mae e-bost wedi'i anfon os oedd y cyfeiriad hwn wedi'i gofrestru. " msgstr "Mae e-bost wedi'i anfon os oedd y cyfeiriad hwn wedi'i gofrestru. "
@@ -127,9 +131,13 @@ msgstr "Yn ôl"
msgid "Back to log in" msgid "Back to log in"
msgstr "Yn ôl i fewngofnodi" msgstr "Yn ôl i fewngofnodi"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Estyniadau porwr" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Bydd newid cyfrinair yn cynhyrchu allwedd API newydd"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Gwiriwch fod y porthiant yn gweithio" msgstr "Gwiriwch fod y porthiant yn gweithio"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed eitem nesaf heb ei darllen" msgstr "CommaFeed eitem nesaf heb ei darllen"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Fersiwn ComaFeed {fersiwn} ({ adolygu})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Y diweddaraf yn gyntaf" msgstr "Y diweddaraf yn gyntaf"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Nesaf" msgstr "Nesaf"
@@ -627,6 +640,10 @@ msgstr "Nid yw cyfrineiriau yn cyfateb"
msgid "Position" msgid "Position"
msgstr "Swydd" msgstr "Swydd"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Proffil" msgstr "Proffil"
@@ -793,6 +810,10 @@ msgstr "Thema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Toglo statws darllen y cofnod cyfredol" msgstr "Toglo statws darllen y cofnod cyfredol"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo" msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo"

View File

@@ -71,6 +71,10 @@ msgstr ""
msgid "All" msgid "All"
msgstr "Alle" msgstr "Alle"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Der er sendt en e-mail, hvis denne adresse var registreret. " msgstr "Der er sendt en e-mail, hvis denne adresse var registreret. "
@@ -127,9 +131,13 @@ msgstr "Tilbage"
msgid "Back to log in" msgid "Back to log in"
msgstr "Tilbage for at logge ind" msgstr "Tilbage for at logge ind"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Browserudvidelser" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,12 +171,16 @@ msgstr "Ændring af adgangskode vil generere en ny API-nøgle"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Tjek, at foderet virker" msgstr "Tjek, at foderet virker"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed næste ulæste element" msgstr "CommaFeed næste ulæste element"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Nyeste først" msgstr "Nyeste først"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Næste" msgstr "Næste"
@@ -627,6 +640,10 @@ msgstr "Adgangskoder stemmer ikke overens"
msgid "Position" msgid "Position"
msgstr "" msgstr ""
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Skift læsestatus for den aktuelle post" msgstr "Skift læsestatus for den aktuelle post"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo" msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Verwaltung"
msgid "All" msgid "All"
msgstr "Alle" msgstr "Alle"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Eine E-Mail wurde gesendet, wenn diese Adresse registriert wurde. " msgstr "Eine E-Mail wurde gesendet, wenn diese Adresse registriert wurde. "
@@ -127,9 +131,13 @@ msgstr "Zurück"
msgid "Back to log in" msgid "Back to log in"
msgstr "Zurück zum Anmelden" msgstr "Zurück zum Anmelden"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Browsererweiterungen" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Das Ändern des Passworts generiert einen neuen API-Schlüssel"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Überprüfen Sie, ob der Feed funktioniert" msgstr "Überprüfen Sie, ob der Feed funktioniert"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed nächstes ungelesenes Element" msgstr "CommaFeed nächstes ungelesenes Element"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed-Version {Version} ({Revision})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Neueste zuerst" msgstr "Neueste zuerst"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Weiter" msgstr "Weiter"
@@ -627,6 +640,10 @@ msgstr "Passwörter stimmen nicht überein"
msgid "Position" msgid "Position"
msgstr "Stellung" msgstr "Stellung"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Thema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Lesestatus des aktuellen Eintrags umschalten" msgstr "Lesestatus des aktuellen Eintrags umschalten"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo" msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Admin"
msgid "All" msgid "All"
msgstr "All" msgstr "All"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "An email has been sent if this address was registered. Check your inbox." msgstr "An email has been sent if this address was registered. Check your inbox."
@@ -127,9 +131,13 @@ msgstr "Back"
msgid "Back to log in" msgid "Back to log in"
msgstr "Back to log in" msgstr "Back to log in"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr "Browser extension required for Chrome"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Browser extentions" msgstr "Browser extention"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Changing password will generate a new API key"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Check that the feed is working" msgstr "Check that the feed is working"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed browser extension version {browserExtensionVersion}."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed next unread item" msgstr "CommaFeed next unread item"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed version {version} ({revision})" msgstr "CommaFeed version {version} ({revision})."
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Newest first" msgstr "Newest first"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Next" msgstr "Next"
@@ -627,6 +640,10 @@ msgstr "Passwords do not match"
msgid "Position" msgid "Position"
msgstr "Position" msgstr "Position"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr "Previous"
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profile" msgstr "Profile"
@@ -793,6 +810,10 @@ msgstr "Theme"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Toggle read status of current entry" msgstr "Toggle read status of current entry"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr "Toggle sidebar"
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Try out CommaFeed with the demo account: demo/demo" msgstr "Try out CommaFeed with the demo account: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Administrador"
msgid "All" msgid "All"
msgstr "Todo" msgstr "Todo"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Se ha enviado un correo electrónico si se registró esta dirección. " msgstr "Se ha enviado un correo electrónico si se registró esta dirección. "
@@ -127,9 +131,13 @@ msgstr "Atrás"
msgid "Back to log in" msgid "Back to log in"
msgstr "Volver a iniciar sesión" msgstr "Volver a iniciar sesión"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Extensiones del navegador" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Cambiar la contraseña generará una nueva clave API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Compruebe que el feed funciona" msgstr "Compruebe que el feed funciona"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed siguiente elemento no leído" msgstr "CommaFeed siguiente elemento no leído"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "versión de CommaFeed {versión} ({revisión})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "más reciente primero" msgstr "más reciente primero"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Siguiente" msgstr "Siguiente"
@@ -627,6 +640,10 @@ msgstr "Las contraseñas no coinciden"
msgid "Position" msgid "Position"
msgstr "Posición" msgstr "Posición"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
@@ -793,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Alternar estado de lectura de la entrada actual" msgstr "Alternar estado de lectura de la entrada actual"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo" msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "مدیر"
msgid "All" msgid "All"
msgstr "همه" msgstr "همه"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "اگر این آدرس ثبت شده باشد ایمیل ارسال شده است. " msgstr "اگر این آدرس ثبت شده باشد ایمیل ارسال شده است. "
@@ -127,9 +131,13 @@ msgstr "برگشت"
msgid "Back to log in" msgid "Back to log in"
msgstr "بازگشت برای ورود به سیستم" msgstr "بازگشت برای ورود به سیستم"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "گسترش مرورگر" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "تغییر رمز عبور یک کلید API جدید ایجاد می ک
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "بررسی کنید که خوراک کار می کند" msgstr "بررسی کنید که خوراک کار می کند"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "مورد خوانده نشده بعدی CommaFeed" msgstr "مورد خوانده نشده بعدی CommaFeed"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "نسخه {نسخه} CommaFeed ({نسخه})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "ابتدا جدیدترین" msgstr "ابتدا جدیدترین"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "بعد" msgstr "بعد"
@@ -627,6 +640,10 @@ msgstr "گذرواژه ها مطابقت ندارند"
msgid "Position" msgid "Position"
msgstr "موقعیت" msgstr "موقعیت"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "نمایه" msgstr "نمایه"
@@ -793,6 +810,10 @@ msgstr "تم"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید" msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو" msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو"

View File

@@ -71,6 +71,10 @@ msgstr "Järjestelmänvalvoja"
msgid "All" msgid "All"
msgstr "Kaikki" msgstr "Kaikki"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Sähköposti on lähetetty, jos tämä osoite on rekisteröity. " msgstr "Sähköposti on lähetetty, jos tämä osoite on rekisteröity. "
@@ -127,9 +131,13 @@ msgstr "Takaisin"
msgid "Back to log in" msgid "Back to log in"
msgstr "Takaisin sisäänkirjautumiseen" msgstr "Takaisin sisäänkirjautumiseen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Selaimen laajennukset" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Salasanan vaihtaminen luo uuden API-avaimen"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Tarkista, että syöttö toimii" msgstr "Tarkista, että syöttö toimii"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed seuraava lukematon kohde" msgstr "CommaFeed seuraava lukematon kohde"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed-versio {version} ({versio})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Uusin ensin" msgstr "Uusin ensin"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Seuraava" msgstr "Seuraava"
@@ -627,6 +640,10 @@ msgstr "Salasanat eivät täsmää"
msgid "Position" msgid "Position"
msgstr "Sijainti" msgstr "Sijainti"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profiili" msgstr "Profiili"
@@ -793,6 +810,10 @@ msgstr "Teema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Vaihda nykyisen merkinnän lukutila" msgstr "Vaihda nykyisen merkinnän lukutila"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Kokeile CommaFeediä demotilillä: demo/demo" msgstr "Kokeile CommaFeediä demotilillä: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Administrateur"
msgid "All" msgid "All"
msgstr "Tout" msgstr "Tout"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr "Toujours remonter l'entrée sélectionnée en haut de la page, même si elle s'affiche complètement à l'écran"
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Un e-mail a été envoyé si cette adresse est enregistrée. Vérifiez votre boîte de réception." msgstr "Un e-mail a été envoyé si cette adresse est enregistrée. Vérifiez votre boîte de réception."
@@ -127,9 +131,13 @@ msgstr "Retour"
msgid "Back to log in" msgid "Back to log in"
msgstr "Retour à la connexion" msgstr "Retour à la connexion"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr "L'extension navigateur est nécessaire sur Chrome"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Extensions pour navigateurs" msgstr "Extension navigateur"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Changer de mot de passe générera une nouvelle clé API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Vérifie que le flux fonctionne" msgstr "Vérifie que le flux fonctionne"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed prochain article non lu" msgstr "CommaFeed prochain article non lu"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed version {version} ({revision})" msgstr "CommaFeed version {version} ({revision})."
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -311,7 +323,7 @@ msgstr "Exporter vos abonnements et catégories en tant que fichier OPML qui peu
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Extension options" msgid "Extension options"
msgstr "" msgstr "Options de l'extension"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Plus récent en premier" msgstr "Plus récent en premier"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Suivant" msgstr "Suivant"
@@ -547,7 +560,7 @@ msgstr "Oups !"
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
msgid "Open CommaFeed" msgid "Open CommaFeed"
msgstr "" msgstr "Ouvrir CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
@@ -627,6 +640,10 @@ msgstr "Les mots de passe ne correspondent pas"
msgid "Position" msgid "Position"
msgstr "Position" msgstr "Position"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr "Précédent"
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Thème"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Marquer l'entrée actuelle comme lue/non lue" msgstr "Marquer l'entrée actuelle comme lue/non lue"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr "Montrer/cacher la barre latérale"
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo" msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Administración"
msgid "All" msgid "All"
msgstr "Todos" msgstr "Todos"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Enviouse un correo electrónico se este enderezo estaba rexistrado. " msgstr "Enviouse un correo electrónico se este enderezo estaba rexistrado. "
@@ -127,9 +131,13 @@ msgstr "Atrás"
msgid "Back to log in" msgid "Back to log in"
msgstr "Volver para iniciar sesión" msgstr "Volver para iniciar sesión"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Extensións do navegador" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "O cambio de contrasinal xerará unha nova clave de API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Comproba que a fonte funciona" msgstr "Comproba que a fonte funciona"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed seguinte elemento non lido" msgstr "CommaFeed seguinte elemento non lido"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Versión de CommaFeed {versión} ({revisión})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "o máis novo primeiro" msgstr "o máis novo primeiro"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Seguinte" msgstr "Seguinte"
@@ -627,6 +640,10 @@ msgstr "Os contrasinais non coinciden"
msgid "Position" msgid "Position"
msgstr "Posición" msgstr "Posición"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
@@ -793,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "alternar o estado de lectura da entrada actual" msgstr "alternar o estado de lectura da entrada actual"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proba CommaFeed coa conta de demostración: demo/demo" msgstr "Proba CommaFeed coa conta de demostración: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr ""
msgid "All" msgid "All"
msgstr "Mind" msgstr "Mind"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "E-mailt küldtünk, ha ez a cím regisztrálva volt. " msgstr "E-mailt küldtünk, ha ez a cím regisztrálva volt. "
@@ -127,9 +131,13 @@ msgstr "Vissza"
msgid "Back to log in" msgid "Back to log in"
msgstr "Vissza a bejelentkezéshez" msgstr "Vissza a bejelentkezéshez"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Böngészőbővítések" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "A jelszó megváltoztatása új API-kulcsot generál"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Ellenőrizze, hogy a feed működik-e" msgstr "Ellenőrizze, hogy a feed működik-e"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed következő olvasatlan elem" msgstr "CommaFeed következő olvasatlan elem"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed verzió {version} ({revision})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "A legújabbak először" msgstr "A legújabbak először"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Következő" msgstr "Következő"
@@ -627,6 +640,10 @@ msgstr "A jelszavak nem egyeznek"
msgid "Position" msgid "Position"
msgstr "Pozíció" msgstr "Pozíció"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Téma"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Az aktuális bejegyzés olvasási állapotának váltása" msgstr "Az aktuális bejegyzés olvasási állapotának váltása"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo" msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr ""
msgid "All" msgid "All"
msgstr "Semua" msgstr "Semua"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Email telah dikirim jika alamat ini terdaftar. " msgstr "Email telah dikirim jika alamat ini terdaftar. "
@@ -127,9 +131,13 @@ msgstr "Kembali"
msgid "Back to log in" msgid "Back to log in"
msgstr "Kembali untuk masuk" msgstr "Kembali untuk masuk"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Ekstensi peramban" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Mengubah kata sandi akan menghasilkan kunci API baru"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Periksa apakah umpannya berfungsi" msgstr "Periksa apakah umpannya berfungsi"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed item yang belum dibaca berikutnya" msgstr "CommaFeed item yang belum dibaca berikutnya"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed versi {versi} ({revisi})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Terbaru dulu" msgstr "Terbaru dulu"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Selanjutnya" msgstr "Selanjutnya"
@@ -627,6 +640,10 @@ msgstr "Kata sandi tidak cocok"
msgid "Position" msgid "Position"
msgstr "Posisi" msgstr "Posisi"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Beralih status baca entri saat ini" msgstr "Beralih status baca entri saat ini"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cobalah CommaFeed dengan akun demo: demo/demo" msgstr "Cobalah CommaFeed dengan akun demo: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Ammin"
msgid "All" msgid "All"
msgstr "Tutto" msgstr "Tutto"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "È stata inviata un'e-mail se questo indirizzo è stato registrato. " msgstr "È stata inviata un'e-mail se questo indirizzo è stato registrato. "
@@ -127,9 +131,13 @@ msgstr "Indietro"
msgid "Back to log in" msgid "Back to log in"
msgstr "Torna per accedere" msgstr "Torna per accedere"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Estensioni del browser" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "La modifica della password genererà una nuova chiave API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Verifica che il feed funzioni" msgstr "Verifica che il feed funzioni"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed successivo elemento non letto" msgstr "CommaFeed successivo elemento non letto"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Versione CommaFeed {versione} ({revisione})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Il più recente prima" msgstr "Il più recente prima"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Avanti" msgstr "Avanti"
@@ -627,6 +640,10 @@ msgstr "Le password non corrispondono"
msgid "Position" msgid "Position"
msgstr "Posizione" msgstr "Posizione"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profilo" msgstr "Profilo"
@@ -793,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Commuta lo stato di lettura della voce corrente" msgstr "Commuta lo stato di lettura della voce corrente"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed con il conto demo: demo/demo" msgstr "Prova CommaFeed con il conto demo: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "管理人"
msgid "All" msgid "All"
msgstr "全員" msgstr "全員"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "このアドレスが登録されていれば、メールが送信されました。" msgstr "このアドレスが登録されていれば、メールが送信されました。"
@@ -127,9 +131,13 @@ msgstr "裏"
msgid "Back to log in" msgid "Back to log in"
msgstr "ログインに戻る" msgstr "ログインに戻る"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "ブラウザ拡張機能" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "パスワードを変更すると、新しい API キーが生成され
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "フィードが動作していることを確認してください" msgstr "フィードが動作していることを確認してください"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "次の未読アイテムをカンマフィード" msgstr "次の未読アイテムをカンマフィード"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "コンマフィードのバージョン {version} ({revision})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "最新順" msgstr "最新順"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "次へ" msgstr "次へ"
@@ -627,6 +640,10 @@ msgstr "パスワードが一致しません"
msgid "Position" msgid "Position"
msgstr "位置" msgstr "位置"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "プロフィール" msgstr "プロフィール"
@@ -793,6 +810,10 @@ msgstr "テーマ"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "現在のエントリの読み取りステータスを切り替えます" msgstr "現在のエントリの読み取りステータスを切り替えます"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "デモアカウントで CommaFeed を試す: demo/demo" msgstr "デモアカウントで CommaFeed を試す: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "관리자"
msgid "All" msgid "All"
msgstr "전체" msgstr "전체"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "이 주소가 등록된 경우 이메일이 전송되었습니다. " msgstr "이 주소가 등록된 경우 이메일이 전송되었습니다. "
@@ -127,9 +131,13 @@ msgstr "뒤로"
msgid "Back to log in" msgid "Back to log in"
msgstr "로그인으로 돌아가기" msgstr "로그인으로 돌아가기"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "브라우저 확장" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "비밀번호를 변경하면 새 API 키가 생성됩니다."
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "피드가 작동하는지 확인" msgstr "피드가 작동하는지 확인"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "다음 읽지 않은 항목을 쉼표로 피드" msgstr "다음 읽지 않은 항목을 쉼표로 피드"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "쉼표 피드 버전 {버전}({개정})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "최신순" msgstr "최신순"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "다음" msgstr "다음"
@@ -627,6 +640,10 @@ msgstr "비밀번호가 일치하지 않습니다"
msgid "Position" msgid "Position"
msgstr "위치" msgstr "위치"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "프로필" msgstr "프로필"
@@ -793,6 +810,10 @@ msgstr "테마"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "현재 항목의 읽기 상태 전환" msgstr "현재 항목의 읽기 상태 전환"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo" msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Pentadbir"
msgid "All" msgid "All"
msgstr "Semua" msgstr "Semua"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "E-mel telah dihantar jika alamat ini didaftarkan. " msgstr "E-mel telah dihantar jika alamat ini didaftarkan. "
@@ -127,9 +131,13 @@ msgstr "Kembali"
msgid "Back to log in" msgid "Back to log in"
msgstr "Kembali untuk log masuk" msgstr "Kembali untuk log masuk"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Peluasan penyemak imbas" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Menukar kata laluan akan menjana kunci API baharu"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Semak sama ada suapan berfungsi" msgstr "Semak sama ada suapan berfungsi"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed item belum dibaca seterusnya" msgstr "CommaFeed item belum dibaca seterusnya"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Versi CommaFeed {versi} ({semakan})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Terbaharu dahulu" msgstr "Terbaharu dahulu"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Seterusnya" msgstr "Seterusnya"
@@ -627,6 +640,10 @@ msgstr "Kata laluan tidak sepadan"
msgid "Position" msgid "Position"
msgstr "Kedudukan" msgstr "Kedudukan"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Togol status bacaan entri semasa" msgstr "Togol status bacaan entri semasa"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cuba CommaFeed dengan akaun demo: demo/demo" msgstr "Cuba CommaFeed dengan akaun demo: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr ""
msgid "All" msgid "All"
msgstr "Alle" msgstr "Alle"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "En e-post er sendt hvis denne adressen var registrert. " msgstr "En e-post er sendt hvis denne adressen var registrert. "
@@ -127,9 +131,13 @@ msgstr "Tilbake"
msgid "Back to log in" msgid "Back to log in"
msgstr "Tilbake for å logge inn" msgstr "Tilbake for å logge inn"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Nettleserutvidelser" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Endring av passord vil generere en ny API-nøkkel"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Sjekk at feeden fungerer" msgstr "Sjekk at feeden fungerer"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed neste uleste element" msgstr "CommaFeed neste uleste element"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed versjon {versjon} ({revisjon})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Nyeste først" msgstr "Nyeste først"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Neste" msgstr "Neste"
@@ -627,6 +640,10 @@ msgstr "Passordene samsvarer ikke"
msgid "Position" msgid "Position"
msgstr "Posisjon" msgstr "Posisjon"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Veksle lesestatus for gjeldende oppføring" msgstr "Veksle lesestatus for gjeldende oppføring"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo" msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Beheerder"
msgid "All" msgid "All"
msgstr "Alles" msgstr "Alles"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Er is een e-mail verzonden als dit adres is geregistreerd. " msgstr "Er is een e-mail verzonden als dit adres is geregistreerd. "
@@ -127,9 +131,13 @@ msgstr "Terug"
msgid "Back to log in" msgid "Back to log in"
msgstr "Terug naar inloggen" msgstr "Terug naar inloggen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Browserextensies" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Het wijzigen van het wachtwoord genereert een nieuwe API-sleutel"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Controleer of de feed werkt" msgstr "Controleer of de feed werkt"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed volgende ongelezen item" msgstr "CommaFeed volgende ongelezen item"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed-versie {versie} ({revisie})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Nieuwste eerst" msgstr "Nieuwste eerst"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Volgende" msgstr "Volgende"
@@ -627,6 +640,10 @@ msgstr "Wachtwoorden komen niet overeen"
msgid "Position" msgid "Position"
msgstr "Positie" msgstr "Positie"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profiel" msgstr "Profiel"
@@ -793,6 +810,10 @@ msgstr "Thema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Toggle leesstatus van huidige invoer" msgstr "Toggle leesstatus van huidige invoer"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Probeer CommaFeed uit met het demo-account: demo/demo" msgstr "Probeer CommaFeed uit met het demo-account: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr ""
msgid "All" msgid "All"
msgstr "Alle" msgstr "Alle"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "En e-post er sendt hvis denne adressen var registrert. " msgstr "En e-post er sendt hvis denne adressen var registrert. "
@@ -127,9 +131,13 @@ msgstr "Tilbake"
msgid "Back to log in" msgid "Back to log in"
msgstr "Tilbake for å logge inn" msgstr "Tilbake for å logge inn"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Nettleserutvidelser" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Endring av passord vil generere en ny API-nøkkel"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Sjekk at feeden fungerer" msgstr "Sjekk at feeden fungerer"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed neste uleste element" msgstr "CommaFeed neste uleste element"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed versjon {versjon} ({revisjon})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Nyeste først" msgstr "Nyeste først"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Neste" msgstr "Neste"
@@ -627,6 +640,10 @@ msgstr "Passordene samsvarer ikke"
msgid "Position" msgid "Position"
msgstr "Posisjon" msgstr "Posisjon"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Veksle lesestatus for gjeldende oppføring" msgstr "Veksle lesestatus for gjeldende oppføring"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo" msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Administracja"
msgid "All" msgid "All"
msgstr "Wszystkie" msgstr "Wszystkie"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "E-mail został wysłany, jeśli ten adres został zarejestrowany. " msgstr "E-mail został wysłany, jeśli ten adres został zarejestrowany. "
@@ -127,9 +131,13 @@ msgstr "Powrót"
msgid "Back to log in" msgid "Back to log in"
msgstr "Powrót do logowania" msgstr "Powrót do logowania"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Rozszerzenia przeglądarki" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Zmiana hasła spowoduje wygenerowanie nowego klucza API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Sprawdź, czy kanał działa" msgstr "Sprawdź, czy kanał działa"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "Przecinek następny nieprzeczytany element" msgstr "Przecinek następny nieprzeczytany element"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Wersja CommaFeed {wersja} ({wersja})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Najnowsze jako pierwsze" msgstr "Najnowsze jako pierwsze"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Dalej" msgstr "Dalej"
@@ -627,6 +640,10 @@ msgstr "Hasła nie pasują"
msgid "Position" msgid "Position"
msgstr "Pozycja" msgstr "Pozycja"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Motyw"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Przełącz stan odczytu bieżącego wpisu" msgstr "Przełącz stan odczytu bieżącego wpisu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Wypróbuj CommaFeed z kontem demo: demo/demo" msgstr "Wypróbuj CommaFeed z kontem demo: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Administrador"
msgid "All" msgid "All"
msgstr "Todos" msgstr "Todos"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Um email foi enviado se este endereço foi registrado. " msgstr "Um email foi enviado se este endereço foi registrado. "
@@ -127,9 +131,13 @@ msgstr "Voltar"
msgid "Back to log in" msgid "Back to log in"
msgstr "Voltar para logar" msgstr "Voltar para logar"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Extensões do navegador" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "A alteração da senha gerará uma nova chave de API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Verifique se o feed está funcionando" msgstr "Verifique se o feed está funcionando"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed próximo item não lido" msgstr "CommaFeed próximo item não lido"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Versão do CommaFeed {versão} ({revisão})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Mais novo primeiro" msgstr "Mais novo primeiro"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Próximo" msgstr "Próximo"
@@ -627,6 +640,10 @@ msgstr "Senhas não coincidem"
msgid "Position" msgid "Position"
msgstr "Posição" msgstr "Posição"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
@@ -793,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Alternar o status de leitura da entrada atual" msgstr "Alternar o status de leitura da entrada atual"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Experimente o CommaFeed com a conta demo: demo/demo" msgstr "Experimente o CommaFeed com a conta demo: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Админ"
msgid "All" msgid "All"
msgstr "Все" msgstr "Все"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Электронное письмо было отправлено, если этот адрес был зарегистрирован. " msgstr "Электронное письмо было отправлено, если этот адрес был зарегистрирован. "
@@ -127,9 +131,13 @@ msgstr "Назад"
msgid "Back to log in" msgid "Back to log in"
msgstr "Вернуться к входу" msgstr "Вернуться к входу"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Расширения браузера" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "При изменении пароля будет сгенерирова
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Проверьте, работает ли лента." msgstr "Проверьте, работает ли лента."
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed следующий непрочитанный элемент" msgstr "CommaFeed следующий непрочитанный элемент"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed версия {версия} ({редакция})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Сначала новые" msgstr "Сначала новые"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Далее" msgstr "Далее"
@@ -627,6 +640,10 @@ msgstr "Пароли не совпадают"
msgid "Position" msgid "Position"
msgstr "Позиция" msgstr "Позиция"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Профиль" msgstr "Профиль"
@@ -793,6 +810,10 @@ msgstr "Тема"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Переключить статус чтения текущей записи" msgstr "Переключить статус чтения текущей записи"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Попробуйте CommaFeed на демо-счете: demo/demo" msgstr "Попробуйте CommaFeed на демо-счете: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Správca"
msgid "All" msgid "All"
msgstr "Všetky" msgstr "Všetky"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "E-mail bol odoslaný, ak bola táto adresa zaregistrovaná. " msgstr "E-mail bol odoslaný, ak bola táto adresa zaregistrovaná. "
@@ -127,9 +131,13 @@ msgstr "Späť"
msgid "Back to log in" msgid "Back to log in"
msgstr "Späť na prihlásenie" msgstr "Späť na prihlásenie"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Rozšírenia prehliadača" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Zmena hesla vygeneruje nový kľúč API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Skontrolujte, či feed funguje" msgstr "Skontrolujte, či feed funguje"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed ďalšia neprečítaná položka" msgstr "CommaFeed ďalšia neprečítaná položka"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed verzia {version} ({revision})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Najnovšie ako prvé" msgstr "Najnovšie ako prvé"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Ďalej" msgstr "Ďalej"
@@ -627,6 +640,10 @@ msgstr "Heslá sa nezhodujú"
msgid "Position" msgid "Position"
msgstr "Pozícia" msgstr "Pozícia"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Téma"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Prepne stav čítania aktuálneho záznamu" msgstr "Prepne stav čítania aktuálneho záznamu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Vyskúšajte CommaFeed s demo účtom: demo/demo" msgstr "Vyskúšajte CommaFeed s demo účtom: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr ""
msgid "All" msgid "All"
msgstr "Alla" msgstr "Alla"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Ett e-postmeddelande har skickats om denna adress var registrerad. " msgstr "Ett e-postmeddelande har skickats om denna adress var registrerad. "
@@ -127,9 +131,13 @@ msgstr "Tillbaka"
msgid "Back to log in" msgid "Back to log in"
msgstr "Tillbaka för att logga in" msgstr "Tillbaka för att logga in"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Webbläsartillägg" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,12 +171,16 @@ msgstr "Ändra lösenord kommer att generera en ny API-nyckel"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Kontrollera att matningen fungerar" msgstr "Kontrollera att matningen fungerar"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed nästa olästa objekt" msgstr "CommaFeed nästa olästa objekt"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Nyast först" msgstr "Nyast först"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Nästa" msgstr "Nästa"
@@ -627,6 +640,10 @@ msgstr "Lösenorden matchar inte"
msgid "Position" msgid "Position"
msgstr "" msgstr ""
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Växla lässtatus för aktuell post" msgstr "Växla lässtatus för aktuell post"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed med demokontot: demo/demo" msgstr "Prova CommaFeed med demokontot: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Yönetici"
msgid "All" msgid "All"
msgstr "Tümü" msgstr "Tümü"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Bu adres kayıtlıysa bir e-posta gönderildi. " msgstr "Bu adres kayıtlıysa bir e-posta gönderildi. "
@@ -127,9 +131,13 @@ msgstr "Geri"
msgid "Back to log in" msgid "Back to log in"
msgstr "Giriş yapmak için geri dön" msgstr "Giriş yapmak için geri dön"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Tarayıcı uzantıları" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "Şifreyi değiştirmek yeni bir API anahtarı oluşturacak"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Feed'in çalışıp çalışmadığını kontrol edin" msgstr "Feed'in çalışıp çalışmadığını kontrol edin"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed sonraki okunmamış öğe" msgstr "CommaFeed sonraki okunmamış öğe"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed sürümü {sürüm} ({revizyon})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "Önce en yenisi" msgstr "Önce en yenisi"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Sonraki" msgstr "Sonraki"
@@ -627,6 +640,10 @@ msgstr "Parolalar eşleşmiyor"
msgid "Position" msgid "Position"
msgstr "Konum" msgstr "Konum"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -793,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Geçerli girişin okuma durumunu değiştir" msgstr "Geçerli girişin okuma durumunu değiştir"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo" msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "管理员"
msgid "All" msgid "All"
msgstr "全部" msgstr "全部"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "如果此地址已注册,则已发送电子邮件。" msgstr "如果此地址已注册,则已发送电子邮件。"
@@ -127,9 +131,13 @@ msgstr "返回"
msgid "Back to log in" msgid "Back to log in"
msgstr "返回登录" msgstr "返回登录"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "浏览器扩展" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +171,17 @@ msgstr "更改密码将生成新的 API 密钥"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "检查提要是否正常工作" msgstr "检查提要是否正常工作"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed 下一个未读项目" msgstr "CommaFeed 下一个未读项目"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed 版本 {version} ({revision})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -518,6 +530,7 @@ msgid "Newest first"
msgstr "最新优先" msgstr "最新优先"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "下一个" msgstr "下一个"
@@ -627,6 +640,10 @@ msgstr "密码不匹配"
msgid "Position" msgid "Position"
msgstr "位置" msgstr "位置"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "配置文件" msgstr "配置文件"
@@ -793,6 +810,10 @@ msgstr "主题"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "切换当前条目的读取状态" msgstr "切换当前条目的读取状态"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "使用演示帐户试用 CommaFeeddemo/demo" msgstr "使用演示帐户试用 CommaFeeddemo/demo"

View File

@@ -1,23 +1,32 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Anchor, Box, Center, Container, Divider, Group, Image, Title, useMantineColorScheme } from "@mantine/core" import { Anchor, Box, Center, Container, Divider, Group, Image, Title, useMantineColorScheme } from "@mantine/core"
import { useMediaQuery } from "@mantine/hooks"
import { client } from "app/client" import { client } from "app/client"
import { Constants } from "app/constants"
import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/slices/redirect" import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import welcome_page_dark from "assets/welcome_page_dark.png" import welcome_page_dark from "assets/welcome_page_dark.png"
import welcome_page_light from "assets/welcome_page_light.png" import welcome_page_light from "assets/welcome_page_light.png"
import { ActionButton } from "components/ActionButtton" import { ActionButton } from "components/ActionButton"
import { ButtonToolbar } from "components/ButtonToolbar"
import { useBrowserExtension } from "hooks/useBrowserExtension" import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useMobile } from "hooks/useMobile"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { SiGithub, SiTwitter } from "react-icons/si" import { SiGithub, SiTwitter } from "react-icons/si"
import { TbClock, TbKey, TbMoon, TbSettings, TbSun, TbUserPlus } from "react-icons/tb" import { TbClock, TbKey, TbMoon, TbSettings, TbSun, TbUserPlus } from "react-icons/tb"
import { PageTitle } from "./PageTitle" import { PageTitle } from "./PageTitle"
const iconSize = 18
export function WelcomePage() { export function WelcomePage() {
const serverInfos = useAppSelector(state => state.server.serverInfos)
const { colorScheme } = useMantineColorScheme() const { colorScheme } = useMantineColorScheme()
const dispatch = useAppDispatch()
const image = colorScheme === "light" ? welcome_page_light : welcome_page_dark const image = colorScheme === "light" ? welcome_page_light : welcome_page_dark
const login = useAsyncCallback(client.user.login, {
onSuccess: () => {
dispatch(redirectToRootCategory())
},
})
return ( return (
<Container> <Container>
<Header /> <Header />
@@ -26,6 +35,18 @@ export function WelcomePage() {
<Title order={3}>Bloat-free feed reader</Title> <Title order={3}>Bloat-free feed reader</Title>
</Center> </Center>
{serverInfos?.demoAccountEnabled && (
<Center>
<ActionButton
label={<Trans>Try the demo!</Trans>}
icon={<TbClock size={iconSize} />}
variant="outline"
onClick={() => login.execute({ name: "demo", password: "demo" })}
showLabelOnMobile
/>
</Center>
)}
<Divider my="xl" /> <Divider my="xl" />
<Image src={image} /> <Image src={image} />
@@ -38,7 +59,7 @@ export function WelcomePage() {
} }
function Header() { function Header() {
const mobile = !useMediaQuery(`(min-width: ${Constants.layout.mobileBreakpoint})`) const mobile = useMobile()
if (mobile) { if (mobile) {
return ( return (
@@ -60,30 +81,14 @@ function Header() {
} }
function Buttons() { function Buttons() {
const iconSize = 18
const serverInfos = useAppSelector(state => state.server.serverInfos) const serverInfos = useAppSelector(state => state.server.serverInfos)
const { colorScheme, toggleColorScheme } = useMantineColorScheme() const { colorScheme, toggleColorScheme } = useMantineColorScheme()
const { isBrowserExtension, openSettingsPage } = useBrowserExtension() const { isBrowserExtensionPopup, openSettingsPage } = useBrowserExtension()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const dark = colorScheme === "dark" const dark = colorScheme === "dark"
const login = useAsyncCallback(client.user.login, {
onSuccess: () => {
dispatch(redirectToRootCategory())
},
})
return ( return (
<ButtonToolbar> <Group spacing={14}>
{serverInfos?.demoAccountEnabled && (
<ActionButton
label={<Trans>Try the demo!</Trans>}
icon={<TbClock size={iconSize} />}
variant="outline"
onClick={() => login.execute({ name: "demo", password: "demo" })}
showLabelOnMobile
/>
)}
<ActionButton <ActionButton
label={<Trans>Log in</Trans>} label={<Trans>Log in</Trans>}
icon={<TbKey size={iconSize} />} icon={<TbKey size={iconSize} />}
@@ -108,7 +113,7 @@ function Buttons() {
hideLabelOnDesktop hideLabelOnDesktop
/> />
{isBrowserExtension && ( {isBrowserExtensionPopup && (
<ActionButton <ActionButton
label={<Trans>Extension options</Trans>} label={<Trans>Extension options</Trans>}
icon={<TbSettings size={iconSize} />} icon={<TbSettings size={iconSize} />}
@@ -116,7 +121,7 @@ function Buttons() {
hideLabelOnDesktop hideLabelOnDesktop
/> />
)} )}
</ButtonToolbar> </Group>
) )
} }

View File

@@ -20,6 +20,8 @@ const shownGauges: { [key: string]: string } = {
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Queue size", "com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Queue size",
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Worker active", "com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Worker active",
"com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Updater active", "com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Updater active",
"com.commafeed.frontend.ws.WebSocketSessions.users": "WebSocket users",
"com.commafeed.frontend.ws.WebSocketSessions.sessions": "WebSocket sessions",
} }
export function MetricsPage() { export function MetricsPage() {

View File

@@ -5,6 +5,7 @@ import { redirectToApiDocumentation } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { CategorySelect } from "components/content/add/CategorySelect" import { CategorySelect } from "components/content/add/CategorySelect"
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp" import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import React, { useState } from "react" import React, { useState } from "react"
import { TbHelp, TbKeyboard, TbPuzzle, TbRocket } from "react-icons/tb" import { TbHelp, TbKeyboard, TbPuzzle, TbRocket } from "react-icons/tb"
@@ -60,16 +61,23 @@ function NextUnreadBookmarklet() {
export function AboutPage() { export function AboutPage() {
const version = useAppSelector(state => state.server.serverInfos?.version) const version = useAppSelector(state => state.server.serverInfos?.version)
const revision = useAppSelector(state => state.server.serverInfos?.gitCommit) const revision = useAppSelector(state => state.server.serverInfos?.gitCommit)
const { isBrowserExtensionInstalled, browserExtensionVersion, isBrowserExtensionInstallable } = useBrowserExtension()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
return ( return (
<Container size="xl"> <Container size="xl">
<SimpleGrid cols={2} breakpoints={[{ maxWidth: Constants.layout.mobileBreakpoint, cols: 1 }]}> <SimpleGrid cols={2} breakpoints={[{ maxWidth: Constants.layout.mobileBreakpoint, cols: 1 }]}>
<Section title={<Trans>About</Trans>} icon={<TbHelp size={24} />}> <Section title={<Trans>About</Trans>} icon={<TbHelp size={24} />}>
<Box> <Box>
<Trans> <Trans>
CommaFeed version {version} ({revision}) CommaFeed version {version} ({revision}).
</Trans> </Trans>
</Box> </Box>
{isBrowserExtensionInstallable && isBrowserExtensionInstalled && (
<Box>
<Trans>CommaFeed browser extension version {browserExtensionVersion}.</Trans>
</Box>
)}
<Box mt="md"> <Box mt="md">
<Trans> <Trans>
<span>CommaFeed is an open-source project. Sources are hosted on </span> <span>CommaFeed is an open-source project. Sources are hosted on </span>
@@ -86,8 +94,8 @@ export function AboutPage() {
<Section title={<Trans>Goodies</Trans>} icon={<TbPuzzle size={24} />}> <Section title={<Trans>Goodies</Trans>} icon={<TbPuzzle size={24} />}>
<List> <List>
<List.Item> <List.Item>
<Anchor href="https://github.com/Athou/commafeed-browser-extension" target="_blank" rel="noreferrer"> <Anchor href={Constants.browserExtensionUrl} target="_blank" rel="noreferrer">
<Trans>Browser extentions</Trans> <Trans>Browser extention</Trans>
</Anchor> </Anchor>
</List.Item> </List.Item>
<List.Item> <List.Item>

View File

@@ -1,8 +1,14 @@
import { Box } from "@mantine/core"
import SwaggerUI from "swagger-ui-react" import SwaggerUI from "swagger-ui-react"
import "swagger-ui-react/swagger-ui.css" import "swagger-ui-react/swagger-ui.css"
function ApiDocumentationPage() { function ApiDocumentationPage() {
return <SwaggerUI url="swagger/swagger.json" /> return (
// force white background because swagger is unreadable with dark theme
<Box style={{ backgroundColor: "#fff" }}>
<SwaggerUI url="swagger/swagger.json" />
</Box>
)
} }
export default ApiDocumentationPage export default ApiDocumentationPage

View File

@@ -13,10 +13,9 @@ import {
Title, Title,
useMantineTheme, useMantineTheme,
} from "@mantine/core" } from "@mantine/core"
import { useViewportSize } from "@mantine/hooks"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { redirectToAdd, redirectToRootCategory } from "app/slices/redirect" import { redirectToAdd, redirectToRootCategory } from "app/slices/redirect"
import { reloadTree, setMobileMenuOpen } from "app/slices/tree" import { reloadTree, setMobileMenuOpen, setSidebarWidth } from "app/slices/tree"
import { reloadProfile, reloadSettings, reloadTags } from "app/slices/user" import { reloadProfile, reloadSettings, reloadTags } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
@@ -24,30 +23,42 @@ import { Logo } from "components/Logo"
import { OnDesktop } from "components/responsive/OnDesktop" import { OnDesktop } from "components/responsive/OnDesktop"
import { OnMobile } from "components/responsive/OnMobile" import { OnMobile } from "components/responsive/OnMobile"
import { useAppLoading } from "hooks/useAppLoading" import { useAppLoading } from "hooks/useAppLoading"
import { useMobile } from "hooks/useMobile"
import { useWebSocket } from "hooks/useWebSocket" import { useWebSocket } from "hooks/useWebSocket"
import { LoadingPage } from "pages/LoadingPage" import { LoadingPage } from "pages/LoadingPage"
import { Resizable } from "re-resizable"
import { ReactNode, Suspense, useEffect } from "react" import { ReactNode, Suspense, useEffect } from "react"
import { TbPlus } from "react-icons/tb" import { TbPlus } from "react-icons/tb"
import { Outlet } from "react-router-dom" import { Outlet } from "react-router-dom"
interface LayoutProps { interface LayoutProps {
sidebar: ReactNode sidebar: ReactNode
sidebarWidth: number
header: ReactNode header: ReactNode
} }
const sidebarPadding = DEFAULT_THEME.spacing.xs const sidebarPadding = DEFAULT_THEME.spacing.xs
const sidebarRightBorderWidth = "1px" const sidebarRightBorderWidth = "1px"
const useStyles = createStyles(theme => ({ const useStyles = createStyles((theme, props: LayoutProps) => ({
sidebar: {
"& .mantine-ScrollArea-scrollbar[data-orientation='horizontal']": {
display: "none",
},
},
sidebarContentResizeWrapper: {
padding: sidebarPadding,
minHeight: `calc(100vh - ${Constants.layout.headerHeight}px)`,
},
sidebarContent: { sidebarContent: {
maxWidth: `calc(${Constants.layout.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`, maxWidth: `calc(${props.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`, maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
}, },
}, },
mainContentWrapper: { mainContentWrapper: {
paddingTop: Constants.layout.headerHeight, paddingTop: Constants.layout.headerHeight,
paddingLeft: Constants.layout.sidebarWidth, paddingLeft: props.sidebarWidth,
paddingRight: 0, paddingRight: 0,
paddingBottom: 0, paddingBottom: 0,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
@@ -55,7 +66,7 @@ const useStyles = createStyles(theme => ({
}, },
}, },
mainContent: { mainContent: {
maxWidth: `calc(100vw - ${Constants.layout.sidebarWidth}px)`, maxWidth: `calc(100vw - ${props.sidebarWidth}px)`,
padding: theme.spacing.md, padding: theme.spacing.md,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
maxWidth: "100vw", maxWidth: "100vw",
@@ -76,15 +87,18 @@ function LogoAndTitle() {
) )
} }
export default function Layout({ sidebar, header }: LayoutProps) { export default function Layout(props: LayoutProps) {
const { classes } = useStyles() const { classes } = useStyles(props)
const theme = useMantineTheme() const theme = useMantineTheme()
const viewport = useViewportSize()
const { loading } = useAppLoading() const { loading } = useAppLoading()
const mobile = useMobile()
const mobileMenuOpen = useAppSelector(state => state.tree.mobileMenuOpen) const mobileMenuOpen = useAppSelector(state => state.tree.mobileMenuOpen)
const sidebarHidden = props.sidebarWidth === 0
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
useWebSocket() useWebSocket()
const handleResize = (element: HTMLElement) => dispatch(setSidebarWidth(element.offsetWidth))
useEffect(() => { useEffect(() => {
dispatch(reloadSettings()) dispatch(reloadSettings())
dispatch(reloadProfile()) dispatch(reloadProfile())
@@ -122,13 +136,29 @@ export default function Layout({ sidebar, header }: LayoutProps) {
navbar={ navbar={
<Navbar <Navbar
id="sidebar" id="sidebar"
p={sidebarPadding} hiddenBreakpoint={sidebarHidden ? 99999999 : Constants.layout.mobileBreakpoint}
hiddenBreakpoint={Constants.layout.mobileBreakpoint} hidden={sidebarHidden || !mobileMenuOpen}
hidden={!mobileMenuOpen} width={{ md: props.sidebarWidth }}
width={{ md: Constants.layout.sidebarWidth }} className={classes.sidebar}
> >
<Navbar.Section grow component={ScrollArea} mx="-xs" px="xs"> <Navbar.Section grow component={ScrollArea} mx={mobile ? 0 : "-sm"} px={mobile ? 0 : "sm"}>
<Box className={classes.sidebarContent}>{sidebar}</Box> <Resizable
enable={{
top: false,
right: !mobile,
bottom: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}
onResize={(e, dir, el) => handleResize(el)}
minWidth={120}
className={classes.sidebarContentResizeWrapper}
>
<Box className={classes.sidebarContent}>{props.sidebar}</Box>
</Resizable>
</Navbar.Section> </Navbar.Section>
</Navbar> </Navbar>
} }
@@ -146,37 +176,30 @@ export default function Layout({ sidebar, header }: LayoutProps) {
)} )}
{!mobileMenuOpen && ( {!mobileMenuOpen && (
<Group> <Group>
<Box mr="sm">{burger}</Box> <Box>{burger}</Box>
<Box sx={{ flexGrow: 1 }}>{header}</Box> <Box sx={{ flexGrow: 1 }}>{props.header}</Box>
</Group> </Group>
)} )}
</OnMobile> </OnMobile>
<OnDesktop> <OnDesktop>
<Group> <Group>
<Group position="apart" sx={{ width: Constants.layout.sidebarWidth - 16 }}> <Group position="apart" sx={{ width: props.sidebarWidth - 16 }}>
<Box> <Box>
<LogoAndTitle /> <LogoAndTitle />
</Box> </Box>
<Box>{addButton}</Box> <Box>{addButton}</Box>
</Group> </Group>
<Box sx={{ flexGrow: 1 }}>{header}</Box> <Box sx={{ flexGrow: 1 }}>{props.header}</Box>
</Group> </Group>
</OnDesktop> </OnDesktop>
</Header> </Header>
} }
> >
<ScrollArea <Box id="content" className={classes.mainContent}>
sx={{ height: viewport.height - Constants.layout.headerHeight }} <Suspense fallback={<Loader />}>
viewportRef={ref => { <Outlet />
if (ref) ref.id = Constants.dom.mainScrollAreaId </Suspense>
}} </Box>
>
<Box id="content" className={classes.mainContent}>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
</Box>
</ScrollArea>
</AppShell> </AppShell>
) )
} }

View File

@@ -8,7 +8,7 @@ import { TbCode, TbPhoto, TbUser } from "react-icons/tb"
export function SettingsPage() { export function SettingsPage() {
return ( return (
<Container size="sm" px={0}> <Container size="sm" px={0}>
<Tabs defaultValue="display"> <Tabs defaultValue="display" keepMounted={false}>
<Tabs.List> <Tabs.List>
<Tabs.Tab value="display" icon={<TbPhoto size={16} />}> <Tabs.Tab value="display" icon={<TbPhoto size={16} />}>
<Trans>Display</Trans> <Trans>Display</Trans>

View File

@@ -1,13 +1,41 @@
import { lingui } from "@lingui/vite-plugin" import { lingui } from "@lingui/vite-plugin"
import react from "@vitejs/plugin-react" import react from "@vitejs/plugin-react"
import { visualizer } from "rollup-plugin-visualizer" import { visualizer } from "rollup-plugin-visualizer"
import { defineConfig } from "vite" import { defineConfig, PluginOption } from "vite"
import eslint from "vite-plugin-eslint" import eslint from "vite-plugin-eslint"
import tsconfigPaths from "vite-tsconfig-paths" import tsconfigPaths from "vite-tsconfig-paths"
// inject custom js and css links in html
const customCodeInjector: PluginOption = {
name: "customCodeInjector",
transformIndexHtml: html => {
return {
html,
tags: [
{
tag: "script",
attrs: {
src: "custom_js.js",
},
injectTo: "body",
},
{
tag: "link",
attrs: {
rel: "stylesheet",
href: "custom_css.css",
},
injectTo: "head",
},
],
}
},
}
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
customCodeInjector,
react({ react({
babel: { babel: {
// babel-macro is needed for lingui // babel-macro is needed for lingui
@@ -32,7 +60,7 @@ export default defineConfig({
}, },
}, },
build: { build: {
chunkSizeWarningLimit: 1000, chunkSizeWarningLimit: 3000,
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks: id => { manualChunks: id => {

View File

@@ -3,6 +3,9 @@
app: app:
# url used to access commafeed # url used to access commafeed
publicUrl: http://localhost:8082/ publicUrl: http://localhost:8082/
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
hideFromWebCrawlers: true
# whether to allow user registrations # whether to allow user registrations
allowRegistrations: true allowRegistrations: true
@@ -114,7 +117,6 @@ logging:
liquibase: INFO liquibase: INFO
org.hibernate.SQL: INFO # or ALL for sql debugging org.hibernate.SQL: INFO # or ALL for sql debugging
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
org.hibernate.orm.deprecation: "OFF"
appenders: appenders:
- type: console - type: console
- type: file - type: file

View File

@@ -3,6 +3,9 @@
app: app:
# url used to access commafeed # url used to access commafeed
publicUrl: http://localhost:8082/ publicUrl: http://localhost:8082/
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
hideFromWebCrawlers: true
# whether to allow user registrations # whether to allow user registrations
allowRegistrations: false allowRegistrations: false
@@ -119,7 +122,6 @@ logging:
com.commafeed: INFO com.commafeed: INFO
liquibase: INFO liquibase: INFO
io.dropwizard.server.ServerFactory: INFO io.dropwizard.server.ServerFactory: INFO
org.hibernate.orm.deprecation: "OFF"
appenders: appenders:
- type: console - type: console
- type: file - type: file

View File

@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>3.6.0</version> <version>3.8.1</version>
</parent> </parent>
<artifactId>commafeed-server</artifactId> <artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name> <name>CommaFeed Server</name>
@@ -226,7 +226,7 @@
<dependency> <dependency>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<version>3.6.0</version> <version>3.8.1</version>
</dependency> </dependency>
<dependency> <dependency>

View File

@@ -47,6 +47,7 @@ import com.commafeed.frontend.servlet.CustomCssServlet;
import com.commafeed.frontend.servlet.CustomJsServlet; import com.commafeed.frontend.servlet.CustomJsServlet;
import com.commafeed.frontend.servlet.LogoutServlet; import com.commafeed.frontend.servlet.LogoutServlet;
import com.commafeed.frontend.servlet.NextUnreadServlet; import com.commafeed.frontend.servlet.NextUnreadServlet;
import com.commafeed.frontend.servlet.RobotsTxtDisallowAllServlet;
import com.commafeed.frontend.session.SessionHelperFactoryProvider; import com.commafeed.frontend.session.SessionHelperFactoryProvider;
import com.commafeed.frontend.ws.WebSocketConfigurator; import com.commafeed.frontend.ws.WebSocketConfigurator;
import com.commafeed.frontend.ws.WebSocketEndpoint; import com.commafeed.frontend.ws.WebSocketEndpoint;
@@ -169,6 +170,11 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
environment.servlets().addServlet("customCss", injector.getInstance(CustomCssServlet.class)).addMapping("/custom_css.css"); environment.servlets().addServlet("customCss", injector.getInstance(CustomCssServlet.class)).addMapping("/custom_css.css");
environment.servlets().addServlet("customJs", injector.getInstance(CustomJsServlet.class)).addMapping("/custom_js.js"); environment.servlets().addServlet("customJs", injector.getInstance(CustomJsServlet.class)).addMapping("/custom_js.js");
environment.servlets().addServlet("analytics.js", injector.getInstance(AnalyticsServlet.class)).addMapping("/analytics.js"); environment.servlets().addServlet("analytics.js", injector.getInstance(AnalyticsServlet.class)).addMapping("/analytics.js");
if (Boolean.TRUE.equals(config.getApplicationSettings().getHideFromWebCrawlers())) {
environment.servlets()
.addServlet("robots.txt", injector.getInstance(RobotsTxtDisallowAllServlet.class))
.addMapping("/robots.txt");
}
// WebSocket endpoint // WebSocket endpoint
ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws") ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws")

View File

@@ -65,6 +65,10 @@ public class CommaFeedConfiguration extends Configuration {
@Valid @Valid
private String publicUrl; private String publicUrl;
@NotNull
@Valid
private Boolean hideFromWebCrawlers = true;
@NotNull @NotNull
@Valid @Valid
private Boolean allowRegistrations; private Boolean allowRegistrations;

View File

@@ -15,23 +15,30 @@ import com.commafeed.backend.model.QFeed;
import com.commafeed.backend.model.QFeedSubscription; import com.commafeed.backend.model.QFeedSubscription;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQuery;
@Singleton @Singleton
public class FeedDAO extends GenericDAO<Feed> { public class FeedDAO extends GenericDAO<Feed> {
private final QFeed feed = QFeed.feed; private final QFeed feed = QFeed.feed;
private final QFeedSubscription subscription = QFeedSubscription.feedSubscription;
@Inject @Inject
public FeedDAO(SessionFactory sessionFactory) { public FeedDAO(SessionFactory sessionFactory) {
super(sessionFactory); super(sessionFactory);
} }
public List<Feed> findNextUpdatable(int count) { public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) {
return query().selectFrom(feed) JPAQuery<Feed> query = query().selectFrom(feed).where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date())));
.where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date()))) if (lastLoginThreshold != null) {
.orderBy(feed.disabledUntil.asc()) query.where(JPAExpressions.selectOne()
.limit(count) .from(subscription)
.fetch(); .join(subscription.user)
.where(subscription.feed.id.eq(feed.id), subscription.user.lastLogin.gt(lastLoginThreshold))
.exists());
}
return query.orderBy(feed.disabledUntil.asc()).limit(count).fetch();
} }
public void setDisabledUntil(List<Long> feedIds, Date date) { public void setDisabledUntil(List<Long> feedIds, Date date) {

View File

@@ -28,13 +28,10 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
return query().select(content).from(content).where(content.contentHash.eq(contentHash), content.titleHash.eq(titleHash)).fetch(); return query().select(content).from(content).where(content.contentHash.eq(contentHash), content.titleHash.eq(titleHash)).fetch();
} }
public int deleteWithoutEntries(int max) { public long deleteWithoutEntries(int max) {
JPQLQuery<Integer> subQuery = JPAExpressions.selectOne().from(entry).where(entry.content.id.eq(content.id)); JPQLQuery<Integer> subQuery = JPAExpressions.selectOne().from(entry).where(entry.content.id.eq(content.id));
List<FeedEntryContent> list = query().selectFrom(content).where(subQuery.notExists()).limit(max).fetch(); List<Long> ids = query().select(content.id).from(content).where(subQuery.notExists()).limit(max).fetch();
int deleted = list.size(); return deleteQuery(content).where(content.id.in(ids)).execute();
delete(list);
return deleted;
} }
} }

View File

@@ -48,7 +48,6 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
} }
public int delete(Long feedId, long max) { public int delete(Long feedId, long max) {
List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).limit(max).fetch(); List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).limit(max).fetch();
return delete(list); return delete(list);
} }

View File

@@ -270,8 +270,13 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
return results; return results;
} }
public List<FeedEntryStatus> getOldStatuses(Date olderThan, int limit) { public long deleteOldStatuses(Date olderThan, int limit) {
return query().selectFrom(status).where(status.entryInserted.lt(olderThan), status.starred.isFalse()).limit(limit).fetch(); List<Long> ids = query().select(status.id)
.from(status)
.where(status.entryInserted.lt(olderThan), status.starred.isFalse())
.limit(limit)
.fetch();
return deleteQuery(status).where(status.id.in(ids)).execute();
} }
} }

View File

@@ -7,6 +7,7 @@ import org.hibernate.annotations.QueryHints;
import com.commafeed.backend.model.AbstractModel; import com.commafeed.backend.model.AbstractModel;
import com.querydsl.core.types.EntityPath; import com.querydsl.core.types.EntityPath;
import com.querydsl.jpa.impl.JPADeleteClause;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAQueryFactory;
import com.querydsl.jpa.impl.JPAUpdateClause; import com.querydsl.jpa.impl.JPAUpdateClause;
@@ -30,6 +31,10 @@ public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T>
return new JPAUpdateClause(currentSession(), entityPath); return new JPAUpdateClause(currentSession(), entityPath);
} }
protected JPADeleteClause deleteQuery(EntityPath<T> entityPath) {
return new JPADeleteClause(currentSession(), entityPath);
}
public void saveOrUpdate(T model) { public void saveOrUpdate(T model) {
persist(model); persist(model);
} }

View File

@@ -86,10 +86,12 @@ public class FeedRefreshEngine implements Managed {
Feed feed = queue.take(); Feed feed = queue.take();
// send the feed to be processed // send the feed to be processed
log.debug("got feed {} from the queue, send it for processing", feed.getId());
processFeedAsync(feed); processFeedAsync(feed);
// we removed a feed from the queue, try to refill it as it may now be empty // we removed a feed from the queue, try to refill it as it may now be empty
if (queue.isEmpty()) { if (queue.isEmpty()) {
log.debug("took the last feed from the queue, try to refill");
refillQueueAsync(); refillQueueAsync();
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
@@ -108,9 +110,11 @@ public class FeedRefreshEngine implements Managed {
while (!refillLoopExecutor.isShutdown()) { while (!refillLoopExecutor.isShutdown()) {
try { try {
if (queue.isEmpty()) { if (queue.isEmpty()) {
log.debug("refilling queue");
refillQueueAsync(); refillQueueAsync();
} }
log.debug("sleeping for 15s");
TimeUnit.SECONDS.sleep(15); TimeUnit.SECONDS.sleep(15);
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.debug("interrupted while sleeping"); log.debug("interrupted while sleeping");
@@ -123,6 +127,7 @@ public class FeedRefreshEngine implements Managed {
} }
public void refreshImmediately(Feed feed) { public void refreshImmediately(Feed feed) {
log.debug("add feed {} at the start of the queue", feed.getId());
// remove the feed from the queue if it was already queued to avoid refreshing it twice // remove the feed from the queue if it was already queued to avoid refreshing it twice
queue.removeIf(f -> f.getId().equals(feed.getId())); queue.removeIf(f -> f.getId().equals(feed.getId()));
queue.addFirst(feed); queue.addFirst(feed);
@@ -136,7 +141,9 @@ public class FeedRefreshEngine implements Managed {
refill.mark(); refill.mark();
for (Feed feed : getNextUpdatableFeeds(getBatchSize())) { List<Feed> nextUpdatableFeeds = getNextUpdatableFeeds(getBatchSize());
log.debug("found {} feeds that are up for refresh", nextUpdatableFeeds.size());
for (Feed feed : nextUpdatableFeeds) {
// add the feed only if it was not already queued // add the feed only if it was not already queued
if (queue.stream().noneMatch(f -> f.getId().equals(feed.getId()))) { if (queue.stream().noneMatch(f -> f.getId().equals(feed.getId()))) {
queue.addLast(feed); queue.addLast(feed);
@@ -161,7 +168,10 @@ public class FeedRefreshEngine implements Managed {
private List<Feed> getNextUpdatableFeeds(int max) { private List<Feed> getNextUpdatableFeeds(int max) {
return unitOfWork.call(() -> { return unitOfWork.call(() -> {
List<Feed> feeds = feedDAO.findNextUpdatable(max); Date lastLoginThreshold = Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad())
? DateUtils.addDays(new Date(), -30)
: null;
List<Feed> feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold);
// update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable() // update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable()
Date nextUpdateDate = DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes()); Date nextUpdateDate = DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes());
feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).collect(Collectors.toList()), nextUpdateDate); feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).collect(Collectors.toList()), nextUpdateDate);

View File

@@ -38,6 +38,6 @@ public class FeedCategory extends AbstractModel {
private boolean collapsed; private boolean collapsed;
private Integer position; private int position;
} }

View File

@@ -38,7 +38,7 @@ public class FeedSubscription extends AbstractModel {
@OneToMany(mappedBy = "subscription", cascade = CascadeType.REMOVE) @OneToMany(mappedBy = "subscription", cascade = CascadeType.REMOVE)
private Set<FeedEntryStatus> statuses; private Set<FeedEntryStatus> statuses;
private Integer position; private int position;
@Column(name = "filtering_expression", length = 4096) @Column(name = "filtering_expression", length = 4096)
private String filter; private String filter;

View File

@@ -8,8 +8,6 @@ import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
import org.apache.commons.lang3.time.DateUtils;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -49,17 +47,4 @@ public class User extends AbstractModel {
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date recoverPasswordTokenDate; private Date recoverPasswordTokenDate;
@Column(name = "last_full_refresh")
@Temporal(TemporalType.TIMESTAMP)
private Date lastFullRefresh;
public boolean shouldRefreshFeedsAt(Date when) {
return lastFullRefresh == null || lastFullRefreshMoreThan30MinutesBefore(when);
}
private boolean lastFullRefreshMoreThan30MinutesBefore(Date when) {
return lastFullRefresh.before(DateUtils.addMinutes(when, -30));
}
} }

View File

@@ -65,6 +65,8 @@ public class UserSettings extends AbstractModel {
@Column(name = "scroll_speed") @Column(name = "scroll_speed")
private int scrollSpeed; private int scrollSpeed;
private boolean alwaysScrollToEntry;
private boolean email; private boolean email;
private boolean gmail; private boolean gmail;
private boolean facebook; private boolean facebook;

View File

@@ -34,15 +34,15 @@ public class DatabaseCleaningService {
private final FeedEntryContentDAO feedEntryContentDAO; private final FeedEntryContentDAO feedEntryContentDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO; private final FeedEntryStatusDAO feedEntryStatusDAO;
public long cleanFeedsWithoutSubscriptions() { public void cleanFeedsWithoutSubscriptions() {
log.info("cleaning feeds without subscriptions"); log.info("cleaning feeds without subscriptions");
long total = 0; long total = 0;
int deleted = 0; int deleted;
long entriesTotal = 0; long entriesTotal = 0;
do { do {
List<Feed> feeds = unitOfWork.call(() -> feedDAO.findWithoutSubscriptions(1)); List<Feed> feeds = unitOfWork.call(() -> feedDAO.findWithoutSubscriptions(1));
for (Feed feed : feeds) { for (Feed feed : feeds) {
int entriesDeleted = 0; long entriesDeleted;
do { do {
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), BATCH_SIZE)); entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), BATCH_SIZE));
entriesTotal += entriesDeleted; entriesTotal += entriesDeleted;
@@ -54,23 +54,21 @@ public class DatabaseCleaningService {
log.info("removed {} feeds without subscriptions", total); log.info("removed {} feeds without subscriptions", total);
} while (deleted != 0); } while (deleted != 0);
log.info("cleanup done: {} feeds without subscriptions deleted", total); log.info("cleanup done: {} feeds without subscriptions deleted", total);
return total;
} }
public long cleanContentsWithoutEntries() { public void cleanContentsWithoutEntries() {
log.info("cleaning contents without entries"); log.info("cleaning contents without entries");
long total = 0; long total = 0;
int deleted = 0; long deleted;
do { do {
deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE)); deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE));
total += deleted; total += deleted;
log.info("removed {} contents without entries", total); log.info("removed {} contents without entries", total);
} while (deleted != 0); } while (deleted != 0);
log.info("cleanup done: {} contents without entries deleted", total); log.info("cleanup done: {} contents without entries deleted", total);
return total;
} }
public long cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) { public void cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) {
long total = 0; long total = 0;
while (true) { while (true) {
List<FeedCapacity> feeds = unitOfWork.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, BATCH_SIZE)); List<FeedCapacity> feeds = unitOfWork.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, BATCH_SIZE));
@@ -90,19 +88,17 @@ public class DatabaseCleaningService {
} }
} }
log.info("cleanup done: {} entries for feeds exceeding capacity deleted", total); log.info("cleanup done: {} entries for feeds exceeding capacity deleted", total);
return total;
} }
public long cleanStatusesOlderThan(final Date olderThan) { public void cleanStatusesOlderThan(final Date olderThan) {
log.info("cleaning old read statuses"); log.info("cleaning old read statuses");
long total = 0; long total = 0;
int deleted = 0; long deleted;
do { do {
deleted = unitOfWork.call(() -> feedEntryStatusDAO.delete(feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE))); deleted = unitOfWork.call(() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, BATCH_SIZE));
total += deleted; total += deleted;
log.info("removed {} old read statuses", total); log.info("removed {} old read statuses", total);
} while (deleted != 0); } while (deleted != 0);
log.info("cleanup done: {} old read statuses deleted", total); log.info("cleanup done: {} old read statuses deleted", total);
return total;
} }
} }

View File

@@ -1,5 +1,6 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -107,6 +108,17 @@ public class FeedSubscriptionService {
} }
} }
public void refreshAllUpForRefresh(User user) {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
for (FeedSubscription sub : subs) {
Date disabledUntil = sub.getFeed().getDisabledUntil();
if (disabledUntil == null || disabledUntil.before(new Date())) {
Feed feed = sub.getFeed();
feedRefreshEngine.refreshImmediately(feed);
}
}
}
public Map<Long, UnreadCount> getUnreadCount(User user) { public Map<Long, UnreadCount> getUnreadCount(User user) {
return feedSubscriptionDAO.findAll(user).stream().collect(Collectors.toMap(FeedSubscription::getId, s -> getUnreadCount(user, s))); return feedSubscriptionDAO.findAll(user).stream().collect(Collectors.toMap(FeedSubscription::getId, s -> getUnreadCount(user, s)));
} }

View File

@@ -25,25 +25,20 @@ public class PostLoginActivities {
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
public void executeFor(User user) { public void executeFor(User user) {
Date lastLogin = user.getLastLogin(); // only update lastLogin every once in a while in order to avoid invalidating the cache every time someone logs in
Date now = new Date(); Date now = new Date();
Date lastLogin = user.getLastLogin();
boolean saveUser = false; if (lastLogin == null || lastLogin.before(DateUtils.addMinutes(now, -30))) {
// only update lastLogin field every hour in order to not
// invalidate the cache every time someone logs in
if (lastLogin == null || lastLogin.before(DateUtils.addHours(now, -1))) {
user.setLastLogin(now); user.setLastLogin(now);
saveUser = true;
}
if (Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad()) && user.shouldRefreshFeedsAt(now)) { boolean heavyLoad = Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad());
feedSubscriptionService.refreshAll(user); if (heavyLoad) {
user.setLastFullRefresh(now); // the amount of feeds in the database that are up for refresh might be very large since we're in heavy load mode
saveUser = true; // the feed refresh engine might not be able to catch up quickly enough
} // put feeds from online users that are up for refresh at the top of the queue
feedSubscriptionService.refreshAllUpForRefresh(user);
}
if (saveUser) {
// Post login activites are susceptible to run for any webservice call. // Post login activites are susceptible to run for any webservice call.
// We update the user in a new transaction to update the user immediately. // We update the user in a new transaction to update the user immediately.
// If we didn't and the webservice call takes time, subsequent webservice calls would have to wait for the first call to // If we didn't and the webservice call takes time, subsequent webservice calls would have to wait for the first call to
@@ -51,5 +46,4 @@ public class PostLoginActivities {
unitOfWork.run(() -> userDAO.saveOrUpdate(user)); unitOfWork.run(() -> userDAO.saveOrUpdate(user));
} }
} }
} }

View File

@@ -35,5 +35,5 @@ public class Category implements Serializable {
private boolean expanded; private boolean expanded;
@ApiModelProperty(value = "position of the category in the list", required = true) @ApiModelProperty(value = "position of the category in the list", required = true)
private Integer position; private int position;
} }

View File

@@ -35,6 +35,9 @@ public class Settings implements Serializable {
@ApiModelProperty(value = "user's preferred scroll speed when navigating between entries", required = true) @ApiModelProperty(value = "user's preferred scroll speed when navigating between entries", required = true)
private int scrollSpeed; private int scrollSpeed;
@ApiModelProperty(value = "always scroll selected entry to the top of the page, even if it fits entirely on screen", required = true)
private boolean alwaysScrollToEntry;
@ApiModelProperty(value = "sharing settings", required = true) @ApiModelProperty(value = "sharing settings", required = true)
private SharingSettings sharingSettings = new SharingSettings(); private SharingSettings sharingSettings = new SharingSettings();

View File

@@ -54,7 +54,7 @@ public class Subscription implements Serializable {
private String categoryId; private String categoryId;
@ApiModelProperty("position of the subscription's in the list") @ApiModelProperty("position of the subscription's in the list")
private Integer position; private int position;
@ApiModelProperty(value = "date of the newest item", dataType = "number") @ApiModelProperty(value = "date of the newest item", dataType = "number")
private Date newestItemTime; private Date newestItemTime;

View File

@@ -458,7 +458,7 @@ public class CategoryREST {
category.getChildren().add(child); category.getChildren().add(child);
} }
} }
Collections.sort(category.getChildren(), (o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition())); category.getChildren().sort(Comparator.comparing(Category::getPosition).thenComparing(Category::getName));
for (FeedSubscription subscription : subscriptions) { for (FeedSubscription subscription : subscriptions) {
if (id == null && subscription.getCategory() == null if (id == null && subscription.getCategory() == null
@@ -468,7 +468,7 @@ public class CategoryREST {
category.getFeeds().add(sub); category.getFeeds().add(sub);
} }
} }
Collections.sort(category.getFeeds(), (o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition())); category.getFeeds().sort(Comparator.comparing(Subscription::getPosition).thenComparing(Subscription::getName));
return category; return category;
} }

View File

@@ -103,6 +103,7 @@ public class UserREST {
s.setCustomJs(settings.getCustomJs()); s.setCustomJs(settings.getCustomJs());
s.setLanguage(settings.getLanguage()); s.setLanguage(settings.getLanguage());
s.setScrollSpeed(settings.getScrollSpeed()); s.setScrollSpeed(settings.getScrollSpeed());
s.setAlwaysScrollToEntry(settings.isAlwaysScrollToEntry());
} else { } else {
s.setReadingMode(ReadingMode.unread.name()); s.setReadingMode(ReadingMode.unread.name());
s.setReadingOrder(ReadingOrder.desc.name()); s.setReadingOrder(ReadingOrder.desc.name());
@@ -120,6 +121,7 @@ public class UserREST {
s.setScrollMarks(true); s.setScrollMarks(true);
s.setLanguage("en"); s.setLanguage("en");
s.setScrollSpeed(400); s.setScrollSpeed(400);
s.setAlwaysScrollToEntry(false);
} }
return Response.ok(s).build(); return Response.ok(s).build();
} }
@@ -145,6 +147,7 @@ public class UserREST {
s.setCustomJs(settings.getCustomJs()); s.setCustomJs(settings.getCustomJs());
s.setLanguage(settings.getLanguage()); s.setLanguage(settings.getLanguage());
s.setScrollSpeed(settings.getScrollSpeed()); s.setScrollSpeed(settings.getScrollSpeed());
s.setAlwaysScrollToEntry(settings.isAlwaysScrollToEntry());
s.setEmail(settings.getSharingSettings().isEmail()); s.setEmail(settings.getSharingSettings().isEmail());
s.setGmail(settings.getSharingSettings().isGmail()); s.setGmail(settings.getSharingSettings().isGmail());

View File

@@ -0,0 +1,22 @@
package com.commafeed.frontend.servlet;
import java.io.IOException;
import javax.inject.Singleton;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Singleton
public class RobotsTxtDisallowAllServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain");
resp.getWriter().write("User-agent: *");
resp.getWriter().write("\n");
resp.getWriter().write("Disallow: /");
}
}

View File

@@ -5,9 +5,12 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import javax.websocket.Session; import javax.websocket.Session;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -16,15 +19,23 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class WebSocketSessions { public class WebSocketSessions {
// a user may have multiple sessions (two tabs, on mobile, ...) // a user may have multiple sessions (two tabs, two devices, ...)
private final Map<Long, Set<Session>> sessions = new ConcurrentHashMap<>(); private final Map<Long, Set<Session>> sessions = new ConcurrentHashMap<>();
@Inject
public WebSocketSessions(MetricRegistry metrics) {
metrics.register(MetricRegistry.name(getClass(), "users"),
(Gauge<Long>) () -> sessions.values().stream().filter(v -> !v.isEmpty()).count());
metrics.register(MetricRegistry.name(getClass(), "sessions"),
(Gauge<Long>) () -> sessions.values().stream().mapToLong(Set::size).sum());
}
public void add(Long userId, Session session) { public void add(Long userId, Session session) {
sessions.computeIfAbsent(userId, v -> ConcurrentHashMap.newKeySet()).add(session); sessions.computeIfAbsent(userId, v -> ConcurrentHashMap.newKeySet()).add(session);
} }
public void remove(Session session) { public void remove(Session session) {
sessions.values().forEach(v -> v.removeIf(e -> e.equals(session))); sessions.values().forEach(v -> v.remove(session));
} }
public void sendMessage(User user, String text) { public void sendMessage(User user, String text) {

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="position-required" author="athou">
<update tableName="FEEDCATEGORIES">
<column name="position" valueNumeric="0" />
<where>position is null</where>
</update>
<addNotNullConstraint tableName="FEEDCATEGORIES" columnName="position" columnDataType="int" />
<update tableName="FEEDSUBSCRIPTIONS">
<column name="position" valueNumeric="0" />
<where>position is null</where>
</update>
<addNotNullConstraint tableName="FEEDSUBSCRIPTIONS" columnName="position" columnDataType="int" />
</changeSet>
<changeSet id="drop-unused-last-full-refresh" author="athou">
<dropColumn tableName="USERS" columnName="last_full_refresh" />
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="always-scroll-to-entry" author="athou">
<addColumn tableName="USERSETTINGS">
<column name="alwaysScrollToEntry" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false" />
</column>
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@@ -19,5 +19,7 @@
<include file="changelogs/db.changelog-2.6.xml" /> <include file="changelogs/db.changelog-2.6.xml" />
<include file="changelogs/db.changelog-3.2.xml" /> <include file="changelogs/db.changelog-3.2.xml" />
<include file="changelogs/db.changelog-3.5.xml" /> <include file="changelogs/db.changelog-3.5.xml" />
<include file="changelogs/db.changelog-3.6.xml" />
<include file="changelogs/db.changelog-3.8.xml" />
</databaseChangeLog> </databaseChangeLog>

View File

@@ -3,6 +3,9 @@
app: app:
# url used to access commafeed # url used to access commafeed
publicUrl: http://localhost:8082/ publicUrl: http://localhost:8082/
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
hideFromWebCrawlers: true
# whether to allow user registrations # whether to allow user registrations
allowRegistrations: true allowRegistrations: true

View File

@@ -5,7 +5,7 @@
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>3.6.0</version> <version>3.8.1</version>
<name>CommaFeed</name> <name>CommaFeed</name>
<packaging>pom</packaging> <packaging>pom</packaging>

View File

@@ -13,8 +13,8 @@ if [[ "$BRANCH" != "master" ]]; then
exit exit
fi fi
# make sure README.md has been updated # make sure CHANGELOG.md has been updated
read -r -p "Has README.md been updated? (Y/n) " CONFIRM read -r -p "Has CHANGELOG.md been updated? (Y/n) " CONFIRM
case "$CONFIRM" in case "$CONFIRM" in
n | N) exit ;; n | N) exit ;;
esac esac