Compare commits

...

86 Commits
3.6.0 ... 3.9.0

Author SHA1 Message Date
Athou
6fe1c2a3c0 release 3.9.0 2023-08-17 10:01:17 +02:00
Athou
c2e453027c fix desktop entry header shortly rendered as mobile, causing a small visual glitch 2023-08-16 07:36:05 +02:00
Athou
f16bac9b59 remove "required" for more nulalble fields 2023-08-11 19:12:48 +02:00
Athou
8cca826e70 specify in documentation that we support basic auth 2023-08-11 15:12:00 +02:00
Athou
b0165bb26a "created" field is not always filled, remove "required" 2023-08-11 13:34:05 +02:00
Athou
366294ab46 show loader only while loading (#1131) 2023-08-11 12:53:27 +02:00
Jérémie Panzer
2988938440 Merge pull request #1135 from dcelasun/update-tr-locale
Update Turkish translation
2023-08-10 11:21:07 +02:00
D. Can Celasun
e865769e30 Update Turkish translation 2023-08-10 09:53:38 +01:00
Jérémie Panzer
f87be2fc03 Merge pull request #1129 from canoine/master
Update fr/messages.po
2023-08-04 13:31:58 +02:00
Athou
466846d268 add option to disable custom context menu (#1128) 2023-08-04 08:53:34 +02:00
canoine
61b6be4090 Update fr/messages.po
New entries translated
2023-08-04 08:47:34 +02:00
Athou
cb779ec494 add setting to disable mark as read confirmation (#1110) 2023-08-04 07:32:13 +02:00
Athou
da6f2050f9 log as debug because default log level is info and we don't want to see this 2023-08-02 14:39:03 +02:00
Athou
4304f84a55 restore the announcement feature 2023-08-01 16:30:42 +02:00
Athou
8a175d8221 add css parser error handler that just prints info message because it is non-blocking 2023-08-01 15:30:50 +02:00
Athou
f1896d34e2 disable context menu on shift + right click (#1052) 2023-07-21 09:58:08 +02:00
Athou
45d0e0ec98 Merge branch 'dcelasun-configurable-batch-size' 2023-07-12 15:27:18 +02:00
Athou
38c5beec2f remove unused ApplicationSettings type in the client 2023-07-12 15:26:56 +02:00
Athou
c4715dc3f7 rename field to better represent what it does 2023-07-12 15:26:56 +02:00
D. Can Celasun
6ce6b5ef0e Make database cleaning batch size configurable 2023-07-12 15:26:56 +02:00
Athou
1af3dd452c Merge branch 'dcelasun-mariadb-driver-fix' 2023-07-12 15:04:04 +02:00
Athou
1f4ec41222 change other yml files 2023-07-12 15:03:30 +02:00
D. Can Celasun
512c4cc507 Support MariaDB JDBC driver
This fixes #1113
2023-07-11 08:54:17 +01:00
Athou
d391c8f1c9 Merge branch 'ScuttleSE-master' 2023-07-09 14:37:53 +02:00
Athou
46d3e67aec update other yml files 2023-07-09 14:31:46 +02:00
Athou
d9505c4d87 Merge branch 'master' of https://github.com/ScuttleSE/commafeed into ScuttleSE-master 2023-07-09 14:31:13 +02:00
Athou
42491f5778 no need to repeat feed url in message stored in database (#1112) 2023-07-09 14:09:30 +02:00
Gustav Almstrom
9c897c9fb2 Updated MySQL driver 2023-07-09 11:10:27 +02:00
Athou
21b500a96e don't autoclose bugs 2023-07-08 15:56:02 +02:00
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
107 changed files with 2901 additions and 1018 deletions

1
.github/stale.yml vendored
View File

@@ -7,6 +7,7 @@ exemptLabels:
- pinned - pinned
- security - security
- enhancement - enhancement
- bug
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
staleLabel: wontfix staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable

View File

@@ -1,5 +1,46 @@
# Changelog # Changelog
## [3.9.0]
- improve performance by disabling the loader when nothing is loading (most noticeable on mobile)
- added a setting to disable the 'mark all as read' confirmation
- added a setting to disable the custom context menu
- if the custom context is enabled, it can still be disabled by pressing the shift key
- the announcement feature is now working again and supports html ('announcement' configuration element in config.yml)
- add support for MariaDB 11+
- fix entry header shortly rendered as mobile on desktop, causing a small visual glitch
- fix an issue that could cause a feed to not refresh correctly if the url was very long
- database cleanup batch size is now configurable
- css parsing errors are no longer logged to the standard output
- fix small errors in the api documentation
## [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 +67,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 +109,9 @@
## [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.9.0</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

@@ -78,6 +78,7 @@ describe("entries", () => {
sourceWebsiteUrl: "", sourceWebsiteUrl: "",
entries: [{ id: "3" } as Entry], entries: [{ id: "3" } as Entry],
hasMore: true, hasMore: true,
loading: false,
scrollingToEntry: false, scrollingToEntry: false,
}, },
}, },
@@ -102,6 +103,7 @@ describe("entries", () => {
sourceWebsiteUrl: "", sourceWebsiteUrl: "",
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry], entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
hasMore: true, hasMore: true,
loading: false,
scrollingToEntry: false, scrollingToEntry: false,
}, },
}, },
@@ -128,6 +130,7 @@ describe("entries", () => {
sourceWebsiteUrl: "", sourceWebsiteUrl: "",
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry], entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
hasMore: true, hasMore: true,
loading: false,
scrollingToEntry: false, scrollingToEntry: false,
}, },
}, },

View File

@@ -27,6 +27,7 @@ interface EntriesState {
timestamp?: number timestamp?: number
selectedEntryId?: string selectedEntryId?: string
hasMore: boolean hasMore: boolean
loading: boolean
search?: string search?: string
scrollingToEntry: boolean scrollingToEntry: boolean
} }
@@ -40,23 +41,33 @@ const initialState: EntriesState = {
sourceWebsiteUrl: "", sourceWebsiteUrl: "",
entries: [], entries: [],
hasMore: true, hasMore: true,
loading: false,
scrollingToEntry: false, scrollingToEntry: false,
} }
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 +85,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 +140,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 +195,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 +262,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())
}) })
@@ -311,6 +331,10 @@ export const entriesSlice = createSlice({
state.sourceWebsiteUrl = "" state.sourceWebsiteUrl = ""
state.hasMore = true state.hasMore = true
state.selectedEntryId = undefined state.selectedEntryId = undefined
state.loading = true
})
builder.addCase(loadMoreEntries.pending, state => {
state.loading = true
}) })
builder.addCase(loadEntries.fulfilled, (state, action) => { builder.addCase(loadEntries.fulfilled, (state, action) => {
state.entries = action.payload.entries state.entries = action.payload.entries
@@ -318,12 +342,14 @@ export const entriesSlice = createSlice({
state.sourceLabel = action.payload.name state.sourceLabel = action.payload.name
state.sourceWebsiteUrl = action.payload.feedLink state.sourceWebsiteUrl = action.payload.feedLink
state.hasMore = action.payload.hasMore state.hasMore = action.payload.hasMore
state.loading = false
}) })
builder.addCase(loadMoreEntries.fulfilled, (state, action) => { builder.addCase(loadMoreEntries.fulfilled, (state, action) => {
// remove already existing entries // remove already existing entries
const entriesToAdd = action.payload.entries.filter(e => !state.entries.some(e2 => e.id === e2.id)) const entriesToAdd = action.payload.entries.filter(e => !state.entries.some(e2 => e.id === e2.id))
state.entries = [...state.entries, ...entriesToAdd] state.entries = [...state.entries, ...entriesToAdd]
state.hasMore = action.payload.hasMore state.hasMore = action.payload.hasMore
state.loading = false
}) })
builder.addCase(tagEntry.pending, (state, action) => { builder.addCase(tagEntry.pending, (state, action) => {
state.entries state.entries

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,39 @@ 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 changeMarkAllAsReadConfirmation = createAsyncThunk<
void,
boolean,
{
state: RootState
}
>("settings/markAllAsReadConfirmation", (markAllAsReadConfirmation, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, markAllAsReadConfirmation })
})
export const changeCustomContextMenu = createAsyncThunk<
void,
boolean,
{
state: RootState
}
>("settings/customContextMenu", (customContextMenu, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, customContextMenu })
})
export const changeSharingSetting = createAsyncThunk< export const changeSharingSetting = createAsyncThunk<
void, void,
{ site: keyof SharingSettings; value: boolean }, { site: keyof SharingSettings; value: boolean },
@@ -136,6 +169,18 @@ 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(changeMarkAllAsReadConfirmation.pending, (state, action) => {
if (!state.settings) return
state.settings.markAllAsReadConfirmation = action.meta.arg
})
builder.addCase(changeCustomContextMenu.pending, (state, action) => {
if (!state.settings) return
state.settings.customContextMenu = 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 +191,9 @@ export const userSlice = createSlice({
changeScrollSpeed.fulfilled, changeScrollSpeed.fulfilled,
changeShowRead.fulfilled, changeShowRead.fulfilled,
changeScrollMarks.fulfilled, changeScrollMarks.fulfilled,
changeAlwaysScrollToEntry.fulfilled,
changeMarkAllAsReadConfirmation.fulfilled,
changeCustomContextMenu.fulfilled,
changeSharingSetting.fulfilled changeSharingSetting.fulfilled
), ),
() => { () => {

View File

@@ -3,38 +3,6 @@ export interface AddCategoryRequest {
parentId?: string parentId?: string
} }
export interface ApplicationSettings {
publicUrl: string
allowRegistrations: boolean
createDemoAccount: boolean
googleAnalyticsTrackingCode?: string
googleAuthKey?: string
backgroundThreads: number
databaseUpdateThreads: number
smtpHost?: string
smtpPort?: number
smtpTls?: boolean
smtpUserName?: string
smtpPassword?: string
smtpFromAddress?: string
graphiteEnabled?: boolean
graphitePrefix?: string
graphiteHost?: string
graphitePort?: number
graphiteInterval?: number
heavyLoad: boolean
pubsubhubbub: boolean
imageProxyEnabled: boolean
queryTimeout: number
keepStatusDays: number
maxFeedCapacity: number
refreshIntervalMinutes: number
cache: ApplicationSettingsCache
announcement?: string
userAgent?: string
unreadThreshold?: Date
}
export interface Category { export interface Category {
id: string id: string
parentId?: string parentId?: string
@@ -233,6 +201,9 @@ export interface Settings {
customCss?: string customCss?: string
customJs?: string customJs?: string
scrollSpeed: number scrollSpeed: number
alwaysScrollToEntry: boolean
markAllAsReadConfirmation: boolean
customContextMenu: boolean
sharingSettings: SharingSettings sharingSettings: SharingSettings
} }
@@ -271,7 +242,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
} }
@@ -299,8 +270,6 @@ export interface UserModel {
admin: boolean admin: boolean
} }
export type ApplicationSettingsCache = "NOOP" | "REDIS"
export type ReadingMode = "all" | "unread" export type ReadingMode = "all" | "unread"
export type ReadingOrder = "asc" | "desc" export type ReadingOrder = "asc" | "desc"

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

@@ -0,0 +1,36 @@
import { Trans } from "@lingui/macro"
import { Box, Dialog, Text } from "@mantine/core"
import { useAppSelector } from "app/store"
import { Content } from "components/content/Content"
import { useAsync } from "react-async-hook"
import useLocalStorage from "use-local-storage"
const sha256Hex = async (input: string | undefined) => {
const data = new TextEncoder().encode(input)
const buffer = await crypto.subtle.digest("SHA-256", data)
const array = Array.from(new Uint8Array(buffer))
return array.map(b => b.toString(16).padStart(2, "0")).join("")
}
export function AnnouncementDialog() {
const announcement = useAppSelector(state => state.server.serverInfos?.announcement)
const announcementHash = useAsync(sha256Hex, [announcement]).result
const [localStorageHash, setLocalStorageHash] = useLocalStorage("announcement-hash", "no-hash")
const opened = !!announcementHash && announcementHash !== localStorageHash
const onClosed = () => setLocalStorageHash(announcementHash)
if (!announcement) return null
return (
<Dialog opened={opened} withCloseButton onClose={onClosed} size="xl" radius="md">
<Box>
<Text weight="bold">
<Trans>Announcement</Trans>
</Text>
</Box>
<Box>
<Content content={announcement} />
</Box>
</Dialog>
)
}

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,214 @@
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 native menu (desktop)</Trans>
<td> </td>
<Kbd> <td>
<Trans>Long press</Trans> <Kbd>
</Kbd> <Trans>Shift</Trans>
</td> </Kbd>
</tr> <span> + </span>
<tr> <Kbd>
<td> <Trans>Right click</Trans>
<Trans>Show keyboard shortcut help</Trans> </Kbd>
</td> </td>
<td> </tr>
<Kbd>?</Kbd> <tr>
</td> <td>
</tr> <Trans>Show entry menu (mobile)</Trans>
</tbody> </td>
</Table> <td>
<Kbd>
<Trans>Long press</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Toggle sidebar</Trans>
</td>
<td>
<Kbd>F</Kbd>
</td>
</tr>
<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

@@ -1,4 +1,5 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Box } from "@mantine/core"
import { openModal } from "@mantine/modals" import { openModal } from "@mantine/modals"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { import {
@@ -12,13 +13,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 +32,14 @@ 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 loading = useAppSelector(state => state.entries.loading)
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 customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
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 +63,44 @@ export function FeedEntries() {
} }
} }
useEffect(() => { const contextMenu = useContextMenu()
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) const headerRightClicked = (entry: ExpendableEntry, event: React.MouseEvent) => {
if (event.shiftKey || !customContextMenu) return
const listener = () => { event.preventDefault()
contextMenu.show({
id: Constants.dom.entryContextMenuId(entry),
event,
})
}
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 +122,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 +177,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 +215,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 +249,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 +271,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>,
@@ -247,11 +285,9 @@ export function FeedEntries() {
<InfiniteScroll <InfiniteScroll
id="entries" id="entries"
initialLoad={false} initialLoad={false}
loadMore={() => dispatch(loadMoreEntries())} loadMore={() => !loading && dispatch(loadMoreEntries())}
hasMore={hasMore} hasMore={hasMore}
loader={<Loader key={0} />} loader={<Box key={0}>{loading && <Loader />}</Box>}
useWindow={false}
getScrollParent={() => document.getElementById(Constants.dom.mainScrollAreaId)}
> >
{entries.map(entry => ( {entries.map(entry => (
<div <div
@@ -265,7 +301,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"
@@ -13,8 +13,27 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
const source = useAppSelector(state => state.entries.source) const source = useAppSelector(state => state.entries.source)
const sourceLabel = useAppSelector(state => state.entries.sourceLabel) const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now() const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const buttonClicked = () => {
if (markAllAsReadConfirmation) {
setThreshold(0)
setOpened(true)
} else {
dispatch(
markAllEntries({
sourceType: source.type,
req: {
id: source.id,
read: true,
olderThan: entriesTimestamp,
},
})
)
}
}
return ( return (
<> <>
<Modal opened={opened} onClose={() => setOpened(false)} title={<Trans>Mark all entries as read</Trans>}> <Modal opened={opened} onClose={() => setOpened(false)} title={<Trans>Mark all entries as read</Trans>}>
@@ -70,14 +89,7 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
</Group> </Group>
</Stack> </Stack>
</Modal> </Modal>
<ActionButton <ActionButton icon={<TbChecks size={props.iconSize} />} label={<Trans>Mark all as read</Trans>} onClick={buttonClicked} />
icon={<TbChecks size={props.iconSize} />}
label={<Trans>Mark all as read</Trans>}
onClick={() => {
setThreshold(0)
setOpened(true)
}}
/>
</> </>
) )
} }

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,16 @@
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,
changeCustomContextMenu,
changeLanguage,
changeMarkAllAsReadConfirmation,
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 +20,9 @@ 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 markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings) const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@@ -32,6 +44,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}
@@ -44,6 +62,18 @@ export function DisplaySettings() {
onChange={e => dispatch(changeScrollMarks(e.currentTarget.checked))} onChange={e => dispatch(changeScrollMarks(e.currentTarget.checked))}
/> />
<Switch
label={<Trans>Show confirmation when marking all entries as read</Trans>}
checked={markAllAsReadConfirmation}
onChange={e => dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))}
/>
<Switch
label={<Trans>Show CommaFeed's own context menu on right click</Trans>}
checked={customContextMenu}
onChange={e => dispatch(changeCustomContextMenu(e.currentTarget.checked))}
/>
<Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" /> <Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" />
<SimpleGrid cols={2}> <SimpleGrid cols={2}>

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,7 @@
import { useMediaQuery } from "@mantine/hooks"
import { Constants } from "app/constants"
export const useMobile = (breakpoint: string = Constants.layout.mobileBreakpoint) =>
!useMediaQuery(`(min-width: ${breakpoint})`, undefined, {
getInitialValueInEffect: false,
})

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 "تم إرسال بريد إلكتروني إذا تم تسجيل هذا العنوان. "
@@ -83,6 +87,10 @@ msgstr "ملف opml هو ملف XML يحتوي على عناوين URL للتغ
msgid "Analyze feed" msgid "Analyze feed"
msgstr "تحليل التغذية" msgstr "تحليل التغذية"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "مفتاح API" msgstr "مفتاح API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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 "الملف الشخصي"
@@ -648,6 +669,7 @@ msgstr "تم إغلاق التسجيلات في مثيل CommaFeed هذا"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "مشاركة"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "مشاركة المواقع" msgstr "مشاركة المواقع"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "الحلقة" msgstr "الحلقة"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "إظهار موجز ويب والفئات التي لا تحتوي عل
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "إظهار تعليمات اختصار لوحة المفاتيح" msgstr "إظهار تعليمات اختصار لوحة المفاتيح"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Un fitxer opml és un fitxer XML que conté URL i categories de canals.
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analitzar el feed" msgstr "Analitzar el feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "clau API" msgstr "clau API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Els registres estan tancats en aquesta instància de CommaFeed"
msgid "REST API" msgid "REST API"
msgstr "API REST" msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Comparteix"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Compartir llocs" msgstr "Compartir llocs"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "canvi" msgstr "canvi"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Mostra feeds i categories sense entrades no llegides"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Mostra l'ajuda de la drecera del teclat" msgstr "Mostra l'ajuda de la drecera del teclat"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Soubor opml je soubor XML obsahující adresy URL a kategorie zdrojů. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analyzujte krmivo" msgstr "Analyzujte krmivo"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "Klíč API" msgstr "Klíč API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "V této instanci CommaFeed jsou registrace uzavřeny"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Sdílejte"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Stránky pro sdílení" msgstr "Stránky pro sdílení"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Směna" msgstr "Směna"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Zobrazit kanály a kategorie bez nepřečtených položek"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Zobrazit nápovědu ke klávesovým zkratkám" msgstr "Zobrazit nápovědu ke klávesovým zkratkám"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Mae ffeil opml yn ffeil XML sy'n cynnwys URLs porthiant a chategorïau.
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Dadansoddi porthiant" msgstr "Dadansoddi porthiant"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "Allwedd API" msgstr "Allwedd API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Mae cofrestriadau ar gau ar yr achos CommaFeed hwn"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Rhannu"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Rhannu gwefannau" msgstr "Rhannu gwefannau"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "shifft" msgstr "shifft"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Dangos ffrydiau a chategorïau heb unrhyw gofnodion heb eu darllen"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Dangos cymorth llwybr byr bysellfwrdd" msgstr "Dangos cymorth llwybr byr bysellfwrdd"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "En opml-fil er en XML-fil, der indeholder feed-URL'er og kategorier. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analyser foder" msgstr "Analyser foder"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API-nøgle" msgstr "API-nøgle"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Registreringer er lukket på denne CommaFeed-instans"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Del"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Delingssider" msgstr "Delingssider"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Skift" msgstr "Skift"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Vis feeds og kategorier uden ulæste poster"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Vis hjælp til tastaturgenveje" msgstr "Vis hjælp til tastaturgenveje"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Eine opml-Datei ist eine XML-Datei, die Feed-URLs und Kategorien enthäl
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Feed analysieren" msgstr "Feed analysieren"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API-Schlüssel" msgstr "API-Schlüssel"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Registrierungen sind für diese CommaFeed-Instanz geschlossen"
msgid "REST API" msgid "REST API"
msgstr "REST-API" msgstr "REST-API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Teilen"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Seiten teilen" msgstr "Seiten teilen"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Verschiebung" msgstr "Verschiebung"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Feeds und Kategorien ohne ungelesene Einträge anzeigen"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Tastenkürzel-Hilfe anzeigen" msgstr "Tastenkürzel-Hilfe anzeigen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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."
@@ -83,6 +87,10 @@ msgstr "An opml file is an XML file containing feed URLs and categories. You can
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analyze feed" msgstr "Analyze feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr "Announcement"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API key" msgstr "API key"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Registrations are closed on this CommaFeed instance"
msgid "REST API" msgid "REST API"
msgstr "REST API" msgstr "REST API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "Right click" msgstr "Right click"
@@ -699,11 +721,20 @@ msgstr "Share"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Sharing sites" msgstr "Sharing sites"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Shift" msgstr "Shift"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr "Show CommaFeed's own context menu on right click"
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr "Show confirmation when marking all entries as read"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "Show entry menu (desktop)" msgstr "Show entry menu (desktop)"
@@ -720,6 +751,10 @@ msgstr "Show feeds and categories with no unread entries"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Show keyboard shortcut help" msgstr "Show keyboard shortcut help"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr "Show native menu (desktop)"
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Un archivo opml es un archivo XML que contiene categorías y direcciones
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analizar alimentación" msgstr "Analizar alimentación"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "clave API" msgstr "clave API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Los registros están cerrados en esta instancia de CommaFeed"
msgid "REST API" msgid "REST API"
msgstr "API REST" msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Compartir"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Compartir sitios" msgstr "Compartir sitios"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Cambio" msgstr "Cambio"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Mostrar feeds y categorías sin entradas no leídas"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Mostrar ayuda de atajo de teclado" msgstr "Mostrar ayuda de atajo de teclado"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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 "اگر این آدرس ثبت شده باشد ایمیل ارسال شده است. "
@@ -83,6 +87,10 @@ msgstr "یک فایل opml یک فایل XML است که حاوی آدرس‌ه
msgid "Analyze feed" msgid "Analyze feed"
msgstr "خوراک را تجزیه و تحلیل کنید" msgstr "خوراک را تجزیه و تحلیل کنید"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "کلید API" msgstr "کلید API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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 "نمایه"
@@ -648,6 +669,7 @@ msgstr "ثبت نام در این نمونه CommaFeed بسته شده است"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "به اشتراک بگذارید"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "اشتراک گذاری سایت ها" msgstr "اشتراک گذاری سایت ها"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "شیفت" msgstr "شیفت"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "فیدها و دسته ها را بدون ورودی خوانده نشد
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "نمایش راهنمایی میانبر صفحه کلید" msgstr "نمایش راهنمایی میانبر صفحه کلید"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Opml-tiedosto on XML-tiedosto, joka sisältää syötteen URL-osoitteet
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analysoi syöte" msgstr "Analysoi syöte"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API-avain" msgstr "API-avain"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Tämän CommaFeed-esiintymän rekisteröinnit on suljettu"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Jaa"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Sivustojen jakaminen" msgstr "Sivustojen jakaminen"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Vaihto" msgstr "Vaihto"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Näytä syötteet ja luokat ilman lukemattomia merkintöjä"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Näytä pikanäppäimen ohje" msgstr "Näytä pikanäppäimen ohje"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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."
@@ -83,6 +87,10 @@ msgstr "Un fichier OPML est un fichier XML contenant des URL de flux et des cat
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analyser le flux" msgstr "Analyser le flux"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr "Annonces"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "Clé API" msgstr "Clé API"
@@ -127,9 +135,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 +175,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"
@@ -252,7 +268,7 @@ msgstr "Affichage"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/app/DonatePage.tsx #: src/pages/app/DonatePage.tsx
msgid "Donate" msgid "Donate"
msgstr "Faites un don" msgstr "Faire un don"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "Download" msgid "Download"
@@ -311,7 +327,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 +534,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 +564,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Les inscriptions sont fermées sur cette instance de CommaFeed"
msgid "REST API" msgid "REST API"
msgstr "API REST" msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "Clic droit" msgstr "Clic droit"
@@ -699,11 +721,20 @@ msgstr "Partager"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Sites de partage" msgstr "Sites de partage"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Maj" msgstr "Maj"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr "Demander une confirmation avant de tout marquer comme lu"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "Afficher les options de l'entrée (ordinateur)" msgstr "Afficher les options de l'entrée (ordinateur)"
@@ -720,6 +751,10 @@ msgstr "Afficher les flux et les catégories pour lesquels tout est déjà lu"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Montrer les raccourcis clavier" msgstr "Montrer les raccourcis clavier"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr "Afficher les options du navigateur (ordinateur)"
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Un ficheiro opml é un ficheiro XML que contén URL e categorías de fon
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analizar feed" msgstr "Analizar feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "chave API" msgstr "chave API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Os rexistros están pechados nesta instancia de CommaFeed"
msgid "REST API" msgid "REST API"
msgstr "API REST" msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Compartir"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Compartir sitios" msgstr "Compartir sitios"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "quendas" msgstr "quendas"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Mostrar fontes e categorías sen entradas sen ler"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Mostrar axuda do atallo do teclado" msgstr "Mostrar axuda do atallo do teclado"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Az opml-fájl olyan XML-fájl, amely feed URL-címeket és kategóriáka
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Hírcsatorna elemzése" msgstr "Hírcsatorna elemzése"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API kulcs" msgstr "API kulcs"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "A regisztrációk le vannak zárva ezen a CommaFeed példányon"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Oszd meg"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Webhelyek megosztása" msgstr "Webhelyek megosztása"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Hírcsatornák és kategóriák megjelenítése olvasatlan bejegyzések
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "A billentyűparancsok súgójának megjelenítése" msgstr "A billentyűparancsok súgójának megjelenítése"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "File opml adalah file XML yang berisi URL dan kategori feed. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analisis umpan" msgstr "Analisis umpan"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "kunci API" msgstr "kunci API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Pendaftaran ditutup pada instans CommaFeed ini"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Bagikan"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Berbagi situs" msgstr "Berbagi situs"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Pergeseran" msgstr "Pergeseran"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Tampilkan umpan dan kategori tanpa entri yang belum dibaca"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Tampilkan bantuan pintasan keyboard" msgstr "Tampilkan bantuan pintasan keyboard"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Un file opml è un file XML contenente URL e categorie di feed. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analizza feed" msgstr "Analizza feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "Chiave API" msgstr "Chiave API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Le registrazioni sono chiuse su questa istanza CommaFeed"
msgid "REST API" msgid "REST API"
msgstr "API REST" msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Condividi"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Condivisione di siti" msgstr "Condivisione di siti"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Cambio" msgstr "Cambio"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Mostra feed e categorie senza voci non lette"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Mostra la guida alle scorciatoie da tastiera" msgstr "Mostra la guida alle scorciatoie da tastiera"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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 "このアドレスが登録されていれば、メールが送信されました。"
@@ -83,6 +87,10 @@ msgstr "opml ファイルは、フィードの URL とカテゴリを含む XML
msgid "Analyze feed" msgid "Analyze feed"
msgstr "フィードを分析する" msgstr "フィードを分析する"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "APIキー" msgstr "APIキー"
@@ -127,9 +135,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 +175,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 +534,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 +644,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 "プロフィール"
@@ -648,6 +669,7 @@ msgstr "この CommaFeed インスタンスの登録は終了しています"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "シェア"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "共有サイト" msgstr "共有サイト"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "シフト" msgstr "シフト"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "未読エントリのないフィードとカテゴリを表示する"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "キーボード ショートカットのヘルプを表示" msgstr "キーボード ショートカットのヘルプを表示"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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 "이 주소가 등록된 경우 이메일이 전송되었습니다. "
@@ -83,6 +87,10 @@ msgstr "opml 파일은 피드 URL과 카테고리를 포함하는 XML 파일입
msgid "Analyze feed" msgid "Analyze feed"
msgstr "피드 분석" msgstr "피드 분석"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API 키" msgstr "API 키"
@@ -127,9 +135,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 +175,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 +534,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 +644,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 "프로필"
@@ -648,6 +669,7 @@ msgstr "이 CommaFeed 인스턴스에 대한 등록이 마감되었습니다."
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "공유"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "사이트 공유" msgstr "사이트 공유"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "시프트" msgstr "시프트"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "읽지 않은 항목이 없는 피드 및 카테고리 표시"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "키보드 단축키 도움말 표시" msgstr "키보드 단축키 도움말 표시"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Fail opml ialah fail XML yang mengandungi URL suapan dan kategori. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Menganalisis suapan" msgstr "Menganalisis suapan"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "Kunci API" msgstr "Kunci API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Pendaftaran ditutup pada contoh CommaFeed ini"
msgid "REST API" msgid "REST API"
msgstr "REHAT API" msgstr "REHAT API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Kongsi"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Berkongsi tapak" msgstr "Berkongsi tapak"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Anjakan" msgstr "Anjakan"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Tunjukkan suapan dan kategori tanpa entri yang belum dibaca"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Tunjukkan bantuan pintasan papan kekunci" msgstr "Tunjukkan bantuan pintasan papan kekunci"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "En opml-fil er en XML-fil som inneholder feed-URLer og kategorier. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analyser feed" msgstr "Analyser feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API-nøkkel" msgstr "API-nøkkel"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Registreringer er stengt på denne CommaFeed-forekomsten"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Del"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Delingssider" msgstr "Delingssider"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Skift" msgstr "Skift"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Vis feeder og kategorier uten uleste oppføringer"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Vis hurtigtasthjelp" msgstr "Vis hurtigtasthjelp"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Een opml-bestand is een XML-bestand met feed-URL's en categorieën. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analyseer feed" msgstr "Analyseer feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API-sleutel" msgstr "API-sleutel"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Registraties zijn gesloten op deze CommaFeed-instantie"
msgid "REST API" msgid "REST API"
msgstr "REST-API" msgstr "REST-API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Delen"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Sites delen" msgstr "Sites delen"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Toon feeds en categorieën zonder ongelezen items"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Toon hulp bij sneltoetsen" msgstr "Toon hulp bij sneltoetsen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "En opml-fil er en XML-fil som inneholder feed-URLer og kategorier. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analyser feed" msgstr "Analyser feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API-nøkkel" msgstr "API-nøkkel"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Registreringer er stengt på denne CommaFeed-forekomsten"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Del"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Delingssider" msgstr "Delingssider"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Skift" msgstr "Skift"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Vis feeder og kategorier uten uleste oppføringer"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Vis hurtigtasthjelp" msgstr "Vis hurtigtasthjelp"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Plik opml to plik XML zawierający adresy URL i kategorie kanałów. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analizuj kanał" msgstr "Analizuj kanał"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "klucz API" msgstr "klucz API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Rejestracje są zamknięte w tej instancji CommaFeed"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Udostępnij"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Udostępnianie witryn" msgstr "Udostępnianie witryn"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "zmiana" msgstr "zmiana"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Pokaż kanały i kategorie bez nieprzeczytanych wpisów"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Pokaż pomoc dotyczącą skrótów klawiaturowych" msgstr "Pokaż pomoc dotyczącą skrótów klawiaturowych"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "Um arquivo opml é um arquivo XML contendo URLs e categorias de feed. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analisar feed" msgstr "Analisar feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "chave de API" msgstr "chave de API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Os registros estão fechados nesta instância do CommaFeed"
msgid "REST API" msgid "REST API"
msgstr "API REST" msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Compartilhar"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Compartilhando sites" msgstr "Compartilhando sites"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Mudar" msgstr "Mudar"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Mostrar feeds e categorias sem entradas não lidas"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Mostrar ajuda de atalho de teclado" msgstr "Mostrar ajuda de atalho de teclado"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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 "Электронное письмо было отправлено, если этот адрес был зарегистрирован. "
@@ -83,6 +87,10 @@ msgstr "OPML-файл — это XML-файл, содержащий URL-адре
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Анализ канала" msgstr "Анализ канала"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "ключ API" msgstr "ключ API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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 "Профиль"
@@ -648,6 +669,7 @@ msgstr "Регистрация закрыта для этого экземпля
msgid "REST API" msgid "REST API"
msgstr "ОТДЫХА API" msgstr "ОТДЫХА API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Поделиться"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Обмен сайтами" msgstr "Обмен сайтами"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Сдвиг" msgstr "Сдвиг"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Показать каналы и категории без непроч
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Показать справку по сочетаниям клавиш." msgstr "Показать справку по сочетаниям клавиш."
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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á. "
@@ -83,6 +87,10 @@ msgstr "Súbor opml je súbor XML obsahujúci adresy URL kanálov a kategórie.
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analyzujte krmivo" msgstr "Analyzujte krmivo"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "Kľúč API" msgstr "Kľúč API"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "V tejto inštancii CommaFeed sú registrácie uzavreté"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Zdieľať"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Zdieľanie stránok" msgstr "Zdieľanie stránok"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Smena" msgstr "Smena"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Zobraziť kanály a kategórie bez neprečítaných záznamov"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Zobraziť pomoc s klávesovými skratkami" msgstr "Zobraziť pomoc s klávesovými skratkami"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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. "
@@ -83,6 +87,10 @@ msgstr "En opml-fil är en XML-fil som innehåller feed-URL:er och kategorier. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analysera foder" msgstr "Analysera foder"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API-nyckel" msgstr "API-nyckel"
@@ -127,9 +135,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 +175,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 +534,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 +644,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"
@@ -648,6 +669,7 @@ msgstr "Registreringar är stängda på denna CommaFeed-instans"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "Dela"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Delningssajter" msgstr "Delningssajter"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Skift" msgstr "Skift"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "Visa flöden och kategorier utan olästa poster"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Visa kortkommandohjälp" msgstr "Visa kortkommandohjälp"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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

@@ -15,15 +15,15 @@ msgstr ""
#: src/components/content/add/CategorySelect.tsx #: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr "{0} ({1} içinde)"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>." msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr "" msgstr "<0>CommaFeed açık kaynak kodlu bir proje. Kaynak kodları </0><1>GitHub</1>'da."
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr "<0>Tüm sözdizimi </0><1>burada</1>."
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
msgid "<0>Have an account?</0><1>Log in!</1>" msgid "<0>Have an account?</0><1>Log in!</1>"
@@ -31,7 +31,7 @@ msgstr "<0>Hesabınız var mı?</0><1>Giriş yapın!</1>"
#: src/pages/app/DonatePage.tsx #: src/pages/app/DonatePage.tsx
msgid "<0>Hey,</0><1>I'm Jérémie from Belgium and I've been working on CommaFeed in my free time for over 10 years now. Thanks for taking an interest in helping me continue supporting CommaFeed.</1>" msgid "<0>Hey,</0><1>I'm Jérémie from Belgium and I've been working on CommaFeed in my free time for over 10 years now. Thanks for taking an interest in helping me continue supporting CommaFeed.</1>"
msgstr "" msgstr "<0>Merhaba,</0><1>Ben Belçika'dan Jérémie ve 10 yıldır boş zamanlarımda CommaFeed üzerinde çalışıyorum. CommaFeed'i desteklememe ilgi gösterdiğiniz için teşekkürler.</1>"
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "<0>Need an account?</0><1>Sign up!</1>" msgid "<0>Need an account?</0><1>Sign up!</1>"
@@ -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 "Seçilen girişi her zaman sayfanın üstüne kaydır, ekrana tamamen sığsa bile"
#: 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. "
@@ -83,6 +87,10 @@ msgstr "Bir opml dosyası, besleme URL'lerini ve kategorilerini içeren bir XML
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Feed'i analiz et" msgstr "Feed'i analiz et"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr "Duyuru"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API anahtarı" msgstr "API anahtarı"
@@ -127,9 +135,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 "Tarayıcı eklentisi"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -163,13 +175,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 "CommaFeed tarayıcı eklentisi sürüm {browserExtensionVersion}."
#: 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 "CommaFeed sürüm {version} ({revision})."
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -197,7 +213,7 @@ msgstr "Etiket oluştur: {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "" msgstr "Ctrl"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "Current password" msgid "Current password"
@@ -205,15 +221,15 @@ msgstr "Geçerli şifre"
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Custom code" msgid "Custom code"
msgstr "" msgstr "Özel kod"
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
msgid "Custom CSS rules that will be applied" msgid "Custom CSS rules that will be applied"
msgstr "" msgstr "Uygulanacak özel CSS kuralları"
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr "Sayfa yüklendiğinde çalıştırılacak özel JS kodu"
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
@@ -311,7 +327,7 @@ msgstr "Aboneliklerinizi ve kategorilerinizi diğer besleme okuma hizmetlerinde
#: 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 "Eklenti ayarları"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
@@ -325,7 +341,7 @@ msgstr "Feed URL'si"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr "Tüm feed'lerimi şimdi çek"
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
@@ -357,7 +373,7 @@ msgstr "Oluşturulan besleme url'si"
#: src/components/content/FeedEntryContextMenu.tsx #: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}" msgid "Go to {0}"
msgstr "" msgstr "{0}'a git"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Go to the All view" msgid "Go to the All view"
@@ -451,7 +467,7 @@ msgstr "Çıkış"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press" msgid "Long press"
msgstr "" msgstr "Uzun bas"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -483,7 +499,7 @@ msgstr "Metrikler"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click" msgid "Middle click"
msgstr "" msgstr "Orta tuş ile tıkla"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down" msgid "Move the page down"
@@ -518,6 +534,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"
@@ -547,7 +564,7 @@ msgstr "Hata!"
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
msgid "Open CommaFeed" msgid "Open CommaFeed"
msgstr "" msgstr "CommaFeed'i aç"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
@@ -563,11 +580,11 @@ msgstr "Bağlantıyı aç"
#: src/components/content/FeedEntryContextMenu.tsx #: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab" msgid "Open link in new background tab"
msgstr "" msgstr "Bağlantıyı arkaplanda yeni sekmede aç"
#: src/components/content/FeedEntryContextMenu.tsx #: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr "Bağlantıyı yeni sekmede aç"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
@@ -583,7 +600,7 @@ msgstr "Geçerli girişi aç/kapat"
#: src/pages/app/AddPage.tsx #: src/pages/app/AddPage.tsx
msgid "OPML" msgid "OPML"
msgstr "" msgstr "OPML"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "OPML export" msgid "OPML export"
@@ -627,6 +644,10 @@ msgstr "Parolalar eşleşmiyor"
msgid "Position" msgid "Position"
msgstr "Konum" msgstr "Konum"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr "Önceki"
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -646,11 +667,12 @@ msgstr "Bu CommaFeed örneğinde kayıtlar kapalı"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "REST API" msgid "REST API"
msgstr "" msgstr "REST API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr "Sağ tık"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
@@ -699,18 +721,27 @@ msgstr "Paylaş"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Siteleri paylaşma" msgstr "Siteleri paylaşma"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Vardiya" msgstr "Vardiya"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr "Sağ tıkta CommaFeed'in kendi menüsünü göster"
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr "Tüm girişleri okundu işaretlerken onay iste"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr "Giriş menüsünü göster (masaüstü)"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)" msgid "Show entry menu (mobile)"
msgstr "" msgstr "Giriş menüsünü göster (mobil)"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries" msgid "Show feeds and categories with no unread entries"
@@ -720,6 +751,10 @@ msgstr "Okunmamış girişi olmayan beslemeleri ve kategorileri göster"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Klavye kısayolu yardımını göster" msgstr "Klavye kısayolu yardımını göster"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr "Orijinal tarayıcı menüsünü göster (masaüstü)"
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -765,7 +800,7 @@ msgstr "Başarı"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr "Başlığı sağa kaydır"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -775,7 +810,7 @@ msgstr "Karanlık temaya geç"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Açık temaya geç" msgstr "Aydınlık temaya geç"
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
@@ -793,17 +828,21 @@ 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 "Kenar çubuğunu göster/gizle"
#: 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"
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Try the demo!" msgid "Try the demo!"
msgstr "" msgstr "Demo'yu deneyin!"
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
msgid "Unread" msgid "Unread"
msgstr "Okunmadı" msgstr "Okunmamış"
#: src/components/content/FeedEntryContextMenu.tsx #: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
@@ -834,8 +873,8 @@ msgstr "Web sitesi"
#: src/pages/app/FeedEntriesPage.tsx #: src/pages/app/FeedEntriesPage.tsx
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?" msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
msgstr "Henüz aboneliğiniz yok. " msgstr "Henüz aboneliğiniz yok. Sayfanın üstündeki + işaretiyle feed ekleyebilirsiniz."
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh." msgid "Your feeds have been queued for refresh."
msgstr "" msgstr "Feed'leriniz yenileme için sıraya alındı."

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 "如果此地址已注册,则已发送电子邮件。"
@@ -83,6 +87,10 @@ msgstr "opml 文件是包含提要 URL 和类别的 XML 文件。"
msgid "Analyze feed" msgid "Analyze feed"
msgstr "分析饲料" msgstr "分析饲料"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API 密钥" msgstr "API 密钥"
@@ -127,9 +135,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 +175,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 +534,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 +644,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 "配置文件"
@@ -648,6 +669,7 @@ msgstr "此 CommaFeed 实例上的注册已关闭"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -699,11 +721,20 @@ msgstr "分享"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "共享站点" msgstr "共享站点"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "换档" msgstr "换档"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -720,6 +751,10 @@ msgstr "显示没有未读条目的提要和类别"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "显示键盘快捷键帮助" msgstr "显示键盘快捷键帮助"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -793,6 +828,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,41 +13,53 @@ 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 { AnnouncementDialog } from "components/AnnouncementDialog"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { Logo } from "components/Logo" 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 +67,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 +88,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 +137,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 +177,31 @@ 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 => { <AnnouncementDialog />
if (ref) ref.id = Constants.dom.mainScrollAreaId <Outlet />
}} </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
@@ -24,7 +27,10 @@ app:
# number of database updating threads # number of database updating threads
databaseUpdateThreads: 1 databaseUpdateThreads: 1
# rows to delete per query while cleaning up old entries
databaseCleanupBatchSize: 100
# settings for sending emails (password recovery) # settings for sending emails (password recovery)
smtpHost: localhost smtpHost: localhost
smtpPort: 25 smtpPort: 25
@@ -78,8 +84,12 @@ app:
# Database connection # Database connection
# ------------------- # -------------------
# for MariaDB
# driverClass is org.mariadb.jdbc.Driver
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
#
# for MySQL # for MySQL
# driverClass is com.mysql.jdbc.Driver # driverClass is com.mysql.cj.jdbc.Driver
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true # url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
# #
# for PostgreSQL # for PostgreSQL
@@ -114,7 +124,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,28 +3,34 @@
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
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char) # whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
strictPasswordPolicy: true strictPasswordPolicy: true
# create a demo account the first time the app starts # create a demo account the first time the app starts
createDemoAccount: false createDemoAccount: false
# put your google analytics tracking code here # put your google analytics tracking code here
googleAnalyticsTrackingCode: googleAnalyticsTrackingCode:
# put your google server key (used for youtube favicon fetching) # put your google server key (used for youtube favicon fetching)
googleAuthKey: googleAuthKey:
# number of http threads # number of http threads
backgroundThreads: 3 backgroundThreads: 3
# number of database updating threads # number of database updating threads
databaseUpdateThreads: 1 databaseUpdateThreads: 1
# rows to delete per query while cleaning up old entries
databaseCleanupBatchSize: 100
# settings for sending emails (password recovery) # settings for sending emails (password recovery)
smtpHost: smtpHost:
smtpPort: smtpPort:
@@ -40,28 +46,28 @@ app:
graphiteHost: "localhost" graphiteHost: "localhost"
graphitePort: 2003 graphitePort: 2003
graphiteInterval: 60 graphiteInterval: 60
# whether this commafeed instance has a lot of feeds to refresh # whether this commafeed instance has a lot of feeds to refresh
# leave this to false in almost all cases # leave this to false in almost all cases
heavyLoad: false heavyLoad: false
# minimum amount of time commafeed will wait before refreshing the same feed # minimum amount of time commafeed will wait before refreshing the same feed
refreshIntervalMinutes: 5 refreshIntervalMinutes: 5
# whether to enable pubsub # whether to enable pubsub
# probably not needed if refreshIntervalMinutes is low # probably not needed if refreshIntervalMinutes is low
pubsubhubbub: false pubsubhubbub: false
# if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser # if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser
# useful if commafeed is usually accessed through a restricting proxy # useful if commafeed is usually accessed through a restricting proxy
imageProxyEnabled: false imageProxyEnabled: false
# database query timeout (in milliseconds), 0 to disable # database query timeout (in milliseconds), 0 to disable
queryTimeout: 0 queryTimeout: 0
# time to keep unread statuses (in days), 0 to disable # time to keep unread statuses (in days), 0 to disable
keepStatusDays: 0 keepStatusDays: 0
# entries to keep per feed, old entries will be deleted, 0 to disable # entries to keep per feed, old entries will be deleted, 0 to disable
maxFeedCapacity: 500 maxFeedCapacity: 500
@@ -70,17 +76,21 @@ app:
# cache service to use, possible values are 'noop' and 'redis' # cache service to use, possible values are 'noop' and 'redis'
cache: noop cache: noop
# announcement string displayed on the main page # announcement string displayed on the main page
announcement: announcement:
# user-agent string that will be used by the http client, leave empty for the default one # user-agent string that will be used by the http client, leave empty for the default one
userAgent: userAgent:
# Database connection # Database connection
# ------------------- # -------------------
# for MariaDB
# driverClass is org.mariadb.jdbc.Driver
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
#
# for MySQL # for MySQL
# driverClass is com.mysql.jdbc.Driver # driverClass is com.mysql.cj.jdbc.Driver
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true # url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
# #
# for PostgreSQL # for PostgreSQL
@@ -102,7 +112,7 @@ database:
minSize: 1 minSize: 1
maxSize: 50 maxSize: 50
maxConnectionAge: 30m maxConnectionAge: 30m
server: server:
applicationConnectors: applicationConnectors:
- type: http - type: http
@@ -112,14 +122,13 @@ server:
port: 8084 port: 8084
requestLog: requestLog:
appenders: [ ] appenders: [ ]
logging: logging:
level: ERROR level: ERROR
loggers: loggers:
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
@@ -129,7 +138,7 @@ logging:
archivedLogFilenamePattern: log/commafeed-%d.log archivedLogFilenamePattern: log/commafeed-%d.log
archivedFileCount: 5 archivedFileCount: 5
timeZone: UTC timeZone: UTC
# Redis pool configuration # Redis pool configuration
# (only used if app.cache is 'redis') # (only used if app.cache is 'redis')
# ----------------------------------- # -----------------------------------
@@ -138,8 +147,7 @@ redis:
port: 6379 port: 6379
# username is only required when using ACLs # username is only required when using ACLs
username: username:
password: password:
timeout: 2000 timeout: 2000
database: 0 database: 0
maxTotal: 500 maxTotal: 500

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.9.0</version>
</parent> </parent>
<artifactId>commafeed-server</artifactId> <artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name> <name>CommaFeed Server</name>
@@ -142,6 +142,12 @@
<title>CommaFeed</title> <title>CommaFeed</title>
<version>${project.version}</version> <version>${project.version}</version>
</info> </info>
<securityDefinitions>
<securityDefinition>
<name>basicAuth</name>
<type>basic</type>
</securityDefinition>
</securityDefinitions>
<typesToSkip> <typesToSkip>
<typeToSkip>com.commafeed.backend.model.User</typeToSkip> <typeToSkip>com.commafeed.backend.model.User</typeToSkip>
</typesToSkip> </typesToSkip>
@@ -226,7 +232,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.9.0</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -444,6 +450,11 @@
<artifactId>mysql-connector-j</artifactId> <artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version> <version>8.0.33</version>
</dependency> </dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.1.4</version>
</dependency>
<dependency> <dependency>
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>
@@ -494,4 +505,4 @@
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

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

@@ -7,6 +7,7 @@ import javax.validation.Valid;
import javax.validation.constraints.Min; import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
@@ -65,6 +66,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;
@@ -91,6 +96,11 @@ public class CommaFeedConfiguration extends Configuration {
@Valid @Valid
private Integer databaseUpdateThreads; private Integer databaseUpdateThreads;
@NotNull
@Positive
@Valid
private Integer databaseCleanupBatchSize = 100;
private String smtpHost; private String smtpHost;
private int smtpPort; private int smtpPort;
private boolean smtpTls; private boolean smtpTls;

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

@@ -94,11 +94,10 @@ public class FeedRefreshWorker {
return new FeedRefreshWorkerResult(feed, Collections.emptyList()); return new FeedRefreshWorkerResult(feed, Collections.emptyList());
} catch (Exception e) { } catch (Exception e) {
String message = "Unable to refresh feed " + feed.getUrl() + " : " + e.getMessage(); log.debug("unable to refresh feed {}", feed.getUrl(), e);
log.debug(e.getClass().getName() + " " + message, e);
feed.setErrorCount(feed.getErrorCount() + 1); feed.setErrorCount(feed.getErrorCount() + 1);
feed.setMessage(message); feed.setMessage("Unable to refresh feed : " + e.getMessage());
feed.setDisabledUntil(refreshIntervalCalculator.onFetchError(feed)); feed.setDisabledUntil(refreshIntervalCalculator.onFetchError(feed));
return new FeedRefreshWorkerResult(feed, Collections.emptyList()); return new FeedRefreshWorkerResult(feed, Collections.emptyList());

View File

@@ -1,11 +1,8 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.io.StringReader;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
@@ -22,16 +19,10 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.math3.stat.descriptive.SummaryStatistics; import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Document.OutputSettings;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.jsoup.nodes.Entities.EscapeMode;
import org.jsoup.safety.Cleaner;
import org.jsoup.safety.Safelist;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;
import org.netpreserve.urlcanon.Canonicalizer; import org.netpreserve.urlcanon.Canonicalizer;
import org.netpreserve.urlcanon.ParsedUrl; import org.netpreserve.urlcanon.ParsedUrl;
import org.w3c.css.sac.InputSource;
import org.w3c.dom.css.CSSStyleDeclaration;
import com.commafeed.backend.feed.FeedEntryKeyword.Mode; import com.commafeed.backend.feed.FeedEntryKeyword.Mode;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
@@ -41,7 +32,6 @@ import com.google.gwt.i18n.client.HasDirection.Direction;
import com.google.gwt.i18n.shared.BidiUtils; import com.google.gwt.i18n.shared.BidiUtils;
import com.ibm.icu.text.CharsetDetector; import com.ibm.icu.text.CharsetDetector;
import com.ibm.icu.text.CharsetMatch; import com.ibm.icu.text.CharsetMatch;
import com.steadystate.css.parser.CSSOMParser;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -54,12 +44,6 @@ public class FeedUtils {
private static final String ESCAPED_QUESTION_MARK = Pattern.quote("?"); private static final String ESCAPED_QUESTION_MARK = Pattern.quote("?");
private static final List<String> ALLOWED_IFRAME_CSS_RULES = Arrays.asList("height", "width", "border");
private static final List<String> ALLOWED_IMG_CSS_RULES = Arrays.asList("display", "width", "height");
private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] { '(', ')' };
private static final Safelist WHITELIST = buildWhiteList();
public static String truncate(String string, int length) { public static String truncate(String string, int length) {
if (string != null) { if (string != null) {
string = string.substring(0, Math.min(length, string.length())); string = string.substring(0, Math.min(length, string.length()));
@@ -67,40 +51,6 @@ public class FeedUtils {
return string; return string;
} }
private static synchronized Safelist buildWhiteList() {
Safelist whitelist = new Safelist();
whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em", "h1",
"h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong", "sub", "sup",
"table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul");
whitelist.addAttributes("div", "dir");
whitelist.addAttributes("pre", "dir");
whitelist.addAttributes("code", "dir");
whitelist.addAttributes("table", "dir");
whitelist.addAttributes("p", "dir");
whitelist.addAttributes("a", "href", "title");
whitelist.addAttributes("blockquote", "cite");
whitelist.addAttributes("col", "span", "width");
whitelist.addAttributes("colgroup", "span", "width");
whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style");
whitelist.addAttributes("ol", "start", "type");
whitelist.addAttributes("q", "cite");
whitelist.addAttributes("table", "border", "bordercolor", "summary", "width");
whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
whitelist.addAttributes("ul", "type");
whitelist.addProtocols("a", "href", "ftp", "http", "https", "magnet", "mailto");
whitelist.addProtocols("blockquote", "cite", "http", "https");
whitelist.addProtocols("img", "src", "http", "https");
whitelist.addProtocols("q", "cite", "http", "https");
whitelist.addEnforcedAttribute("a", "target", "_blank");
whitelist.addEnforcedAttribute("a", "rel", "noreferrer");
return whitelist;
}
/** /**
* Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the characters used in the * Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the characters used in the
* feed * feed
@@ -233,87 +183,6 @@ public class FeedUtils {
return encoding; return encoding;
} }
public static String handleContent(String content, String baseUri, boolean keepTextOnly) {
if (StringUtils.isNotBlank(content)) {
baseUri = StringUtils.trimToEmpty(baseUri);
Document dirty = Jsoup.parseBodyFragment(content, baseUri);
Cleaner cleaner = new Cleaner(WHITELIST);
Document clean = cleaner.clean(dirty);
for (Element e : clean.select("iframe[style]")) {
String style = e.attr("style");
String escaped = escapeIFrameCss(style);
e.attr("style", escaped);
}
for (Element e : clean.select("img[style]")) {
String style = e.attr("style");
String escaped = escapeImgCss(style);
e.attr("style", escaped);
}
clean.outputSettings(new OutputSettings().escapeMode(EscapeMode.base).prettyPrint(false));
Element body = clean.body();
if (keepTextOnly) {
content = body.text();
} else {
content = body.html();
}
}
return content;
}
public static String escapeIFrameCss(String orig) {
String rule = "";
CSSOMParser parser = new CSSOMParser();
try {
List<String> rules = new ArrayList<>();
CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
for (int i = 0; i < decl.getLength(); i++) {
String property = decl.item(i);
String value = decl.getPropertyValue(property);
if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) {
continue;
}
if (ALLOWED_IFRAME_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) {
rules.add(property + ":" + decl.getPropertyValue(property) + ";");
}
}
rule = StringUtils.join(rules, "");
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return rule;
}
public static String escapeImgCss(String orig) {
String rule = "";
CSSOMParser parser = new CSSOMParser();
try {
List<String> rules = new ArrayList<>();
CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
for (int i = 0; i < decl.getLength(); i++) {
String property = decl.item(i);
String value = decl.getPropertyValue(property);
if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) {
continue;
}
if (ALLOWED_IMG_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) {
rules.add(property + ":" + decl.getPropertyValue(property) + ";");
}
}
rule = StringUtils.join(rules, "");
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return rule;
}
public static boolean isRTL(FeedEntry entry) { public static boolean isRTL(FeedEntry entry) {
String text = entry.getContent().getContent(); String text = entry.getContent().getContent();

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,10 @@ public class UserSettings extends AbstractModel {
@Column(name = "scroll_speed") @Column(name = "scroll_speed")
private int scrollSpeed; private int scrollSpeed;
private boolean alwaysScrollToEntry;
private boolean markAllAsReadConfirmation;
private boolean customContextMenu;
private boolean email; private boolean email;
private boolean gmail; private boolean gmail;
private boolean facebook; private boolean facebook;

View File

@@ -6,6 +6,7 @@ import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryContentDAO; import com.commafeed.backend.dao.FeedEntryContentDAO;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
@@ -14,7 +15,6 @@ import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
@@ -22,11 +22,10 @@ import lombok.extern.slf4j.Slf4j;
* *
*/ */
@Slf4j @Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton @Singleton
public class DatabaseCleaningService { public class DatabaseCleaningService {
private static final int BATCH_SIZE = 100; private final int batchSize;
private final UnitOfWork unitOfWork; private final UnitOfWork unitOfWork;
private final FeedDAO feedDAO; private final FeedDAO feedDAO;
@@ -34,17 +33,28 @@ public class DatabaseCleaningService {
private final FeedEntryContentDAO feedEntryContentDAO; private final FeedEntryContentDAO feedEntryContentDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO; private final FeedEntryStatusDAO feedEntryStatusDAO;
public long cleanFeedsWithoutSubscriptions() { @Inject
public DatabaseCleaningService(CommaFeedConfiguration config, UnitOfWork unitOfWork, FeedDAO feedDAO, FeedEntryDAO feedEntryDAO,
FeedEntryContentDAO feedEntryContentDAO, FeedEntryStatusDAO feedEntryStatusDAO) {
this.unitOfWork = unitOfWork;
this.feedDAO = feedDAO;
this.feedEntryDAO = feedEntryDAO;
this.feedEntryContentDAO = feedEntryContentDAO;
this.feedEntryStatusDAO = feedEntryStatusDAO;
this.batchSize = config.getApplicationSettings().getDatabaseCleanupBatchSize();
}
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(), batchSize));
entriesTotal += entriesDeleted; entriesTotal += entriesDeleted;
log.info("removed {} entries for feeds without subscriptions", entriesTotal); log.info("removed {} entries for feeds without subscriptions", entriesTotal);
} while (entriesDeleted > 0); } while (entriesDeleted > 0);
@@ -54,26 +64,24 @@ 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(batchSize));
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, batchSize));
if (feeds.isEmpty()) { if (feeds.isEmpty()) {
break; break;
} }
@@ -82,7 +90,7 @@ public class DatabaseCleaningService {
long remaining = feed.getCapacity() - maxFeedCapacity; long remaining = feed.getCapacity() - maxFeedCapacity;
do { do {
final long rem = remaining; final long rem = remaining;
int deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.getId(), Math.min(BATCH_SIZE, rem))); int deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.getId(), Math.min(batchSize, rem)));
total += deleted; total += deleted;
remaining -= deleted; remaining -= deleted;
log.info("removed {} entries for feeds exceeding capacity", total); log.info("removed {} entries for feeds exceeding capacity", total);
@@ -90,19 +98,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, batchSize));
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,8 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -8,27 +11,47 @@ import javax.inject.Singleton;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Document.OutputSettings;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Entities.EscapeMode;
import org.jsoup.safety.Cleaner;
import org.jsoup.safety.Safelist;
import org.w3c.css.sac.CSSException;
import org.w3c.css.sac.CSSParseException;
import org.w3c.css.sac.ErrorHandler;
import org.w3c.css.sac.InputSource;
import org.w3c.dom.css.CSSStyleDeclaration;
import com.commafeed.backend.dao.FeedEntryContentDAO; import com.commafeed.backend.dao.FeedEntryContentDAO;
import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.FeedEntryContent;
import com.steadystate.css.parser.CSSOMParser;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Slf4j
@Singleton @Singleton
public class FeedEntryContentService { public class FeedEntryContentService {
private static final Safelist HTML_WHITELIST = buildWhiteList();
private static final List<String> ALLOWED_IFRAME_CSS_RULES = Arrays.asList("height", "width", "border");
private static final List<String> ALLOWED_IMG_CSS_RULES = Arrays.asList("display", "width", "height");
private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] { '(', ')' };
private final FeedEntryContentDAO feedEntryContentDAO; private final FeedEntryContentDAO feedEntryContentDAO;
/** /**
* this is NOT thread-safe * this is NOT thread-safe
*/ */
public FeedEntryContent findOrCreate(FeedEntryContent content, String baseUrl) { public FeedEntryContent findOrCreate(FeedEntryContent content, String baseUrl) {
content.setAuthor(FeedUtils.truncate(FeedUtils.handleContent(content.getAuthor(), baseUrl, true), 128)); content.setAuthor(FeedUtils.truncate(handleContent(content.getAuthor(), baseUrl, true), 128));
content.setTitle(FeedUtils.truncate(FeedUtils.handleContent(content.getTitle(), baseUrl, true), 2048)); content.setTitle(FeedUtils.truncate(handleContent(content.getTitle(), baseUrl, true), 2048));
content.setContent(FeedUtils.handleContent(content.getContent(), baseUrl, false)); content.setContent(handleContent(content.getContent(), baseUrl, false));
content.setMediaDescription(FeedUtils.handleContent(content.getMediaDescription(), baseUrl, false)); content.setMediaDescription(handleContent(content.getMediaDescription(), baseUrl, false));
String contentHash = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getContent())); String contentHash = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getContent()));
content.setContentHash(contentHash); content.setContentHash(contentHash);
@@ -37,7 +60,7 @@ public class FeedEntryContentService {
content.setTitleHash(titleHash); content.setTitleHash(titleHash);
List<FeedEntryContent> existing = feedEntryContentDAO.findExisting(contentHash, titleHash); List<FeedEntryContent> existing = feedEntryContentDAO.findExisting(contentHash, titleHash);
Optional<FeedEntryContent> equivalentContent = existing.stream().filter(c -> content.equivalentTo(c)).findFirst(); Optional<FeedEntryContent> equivalentContent = existing.stream().filter(content::equivalentTo).findFirst();
if (equivalentContent.isPresent()) { if (equivalentContent.isPresent()) {
return equivalentContent.get(); return equivalentContent.get();
} }
@@ -45,4 +68,140 @@ public class FeedEntryContentService {
feedEntryContentDAO.saveOrUpdate(content); feedEntryContentDAO.saveOrUpdate(content);
return content; return content;
} }
private static Safelist buildWhiteList() {
Safelist whitelist = new Safelist();
whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em", "h1",
"h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong", "sub", "sup",
"table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul");
whitelist.addAttributes("div", "dir");
whitelist.addAttributes("pre", "dir");
whitelist.addAttributes("code", "dir");
whitelist.addAttributes("table", "dir");
whitelist.addAttributes("p", "dir");
whitelist.addAttributes("a", "href", "title");
whitelist.addAttributes("blockquote", "cite");
whitelist.addAttributes("col", "span", "width");
whitelist.addAttributes("colgroup", "span", "width");
whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style");
whitelist.addAttributes("ol", "start", "type");
whitelist.addAttributes("q", "cite");
whitelist.addAttributes("table", "border", "bordercolor", "summary", "width");
whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
whitelist.addAttributes("ul", "type");
whitelist.addProtocols("a", "href", "ftp", "http", "https", "magnet", "mailto");
whitelist.addProtocols("blockquote", "cite", "http", "https");
whitelist.addProtocols("img", "src", "http", "https");
whitelist.addProtocols("q", "cite", "http", "https");
whitelist.addEnforcedAttribute("a", "target", "_blank");
whitelist.addEnforcedAttribute("a", "rel", "noreferrer");
return whitelist;
}
private String handleContent(String content, String baseUri, boolean keepTextOnly) {
if (StringUtils.isNotBlank(content)) {
baseUri = StringUtils.trimToEmpty(baseUri);
Document dirty = Jsoup.parseBodyFragment(content, baseUri);
Cleaner cleaner = new Cleaner(HTML_WHITELIST);
Document clean = cleaner.clean(dirty);
for (Element e : clean.select("iframe[style]")) {
String style = e.attr("style");
String escaped = escapeIFrameCss(style);
e.attr("style", escaped);
}
for (Element e : clean.select("img[style]")) {
String style = e.attr("style");
String escaped = escapeImgCss(style);
e.attr("style", escaped);
}
clean.outputSettings(new OutputSettings().escapeMode(EscapeMode.base).prettyPrint(false));
Element body = clean.body();
if (keepTextOnly) {
content = body.text();
} else {
content = body.html();
}
}
return content;
}
private String escapeIFrameCss(String orig) {
String rule = "";
try {
List<String> rules = new ArrayList<>();
CSSStyleDeclaration decl = buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig)));
for (int i = 0; i < decl.getLength(); i++) {
String property = decl.item(i);
String value = decl.getPropertyValue(property);
if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) {
continue;
}
if (ALLOWED_IFRAME_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) {
rules.add(property + ":" + decl.getPropertyValue(property) + ";");
}
}
rule = StringUtils.join(rules, "");
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return rule;
}
private String escapeImgCss(String orig) {
String rule = "";
try {
List<String> rules = new ArrayList<>();
CSSStyleDeclaration decl = buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig)));
for (int i = 0; i < decl.getLength(); i++) {
String property = decl.item(i);
String value = decl.getPropertyValue(property);
if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) {
continue;
}
if (ALLOWED_IMG_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) {
rules.add(property + ":" + decl.getPropertyValue(property) + ";");
}
}
rule = StringUtils.join(rules, "");
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return rule;
}
private CSSOMParser buildCssParser() {
CSSOMParser parser = new CSSOMParser();
parser.setErrorHandler(new ErrorHandler() {
@Override
public void warning(CSSParseException exception) throws CSSException {
log.debug("warning while parsing css: {}", exception.getMessage(), exception);
}
@Override
public void error(CSSParseException exception) throws CSSException {
log.debug("error while parsing css: {}", exception.getMessage(), exception);
}
@Override
public void fatalError(CSSParseException exception) throws CSSException {
log.debug("fatal error while parsing css: {}", exception.getMessage(), exception);
}
});
return parser;
}
} }

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,15 @@ 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 = "ask for confirmation when marking all entries as read", required = true)
private boolean markAllAsReadConfirmation;
@ApiModelProperty(value = "show commafeed's own context menu on right click", required = true)
private boolean customContextMenu;
@ApiModelProperty(value = "sharing settings", required = true) @ApiModelProperty(value = "sharing settings", required = true)
private SharingSettings sharingSettings = new SharingSettings(); private SharingSettings sharingSettings = new SharingSettings();

View File

@@ -23,19 +23,16 @@ public class Subscription implements Serializable {
@ApiModelProperty(value = "subscription name", required = true) @ApiModelProperty(value = "subscription name", required = true)
private String name; private String name;
@ApiModelProperty(value = "error message while fetching the feed", required = true) @ApiModelProperty(value = "error message while fetching the feed")
private String message; private String message;
@ApiModelProperty(value = "error count", required = true) @ApiModelProperty(value = "error count", required = true)
private int errorCount; private int errorCount;
@ApiModelProperty(value = "last time the feed was refreshed", dataType = "number", required = true) @ApiModelProperty(value = "last time the feed was refreshed", dataType = "number")
private Date lastRefresh; private Date lastRefresh;
@ApiModelProperty( @ApiModelProperty(value = "next time the feed refresh is planned, null if refresh is already queued", dataType = "number")
value = "next time the feed refresh is planned, null if refresh is already queued",
dataType = "number",
required = true)
private Date nextRefresh; private Date nextRefresh;
@ApiModelProperty(value = "this subscription's feed url", required = true) @ApiModelProperty(value = "this subscription's feed url", required = true)
@@ -54,7 +51,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

@@ -30,7 +30,7 @@ public class UserModel implements Serializable {
@ApiModelProperty(value = "account status", required = true) @ApiModelProperty(value = "account status", required = true)
private boolean enabled; private boolean enabled;
@ApiModelProperty(value = "account creation date", dataType = "number", required = true) @ApiModelProperty(value = "account creation date", dataType = "number")
private Date created; private Date created;
@ApiModelProperty(value = "last login date", dataType = "number") @ApiModelProperty(value = "last login date", dataType = "number")

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,9 @@ 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.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation());
s.setCustomContextMenu(settings.isCustomContextMenu());
} else { } else {
s.setReadingMode(ReadingMode.unread.name()); s.setReadingMode(ReadingMode.unread.name());
s.setReadingOrder(ReadingOrder.desc.name()); s.setReadingOrder(ReadingOrder.desc.name());
@@ -120,6 +123,9 @@ public class UserREST {
s.setScrollMarks(true); s.setScrollMarks(true);
s.setLanguage("en"); s.setLanguage("en");
s.setScrollSpeed(400); s.setScrollSpeed(400);
s.setAlwaysScrollToEntry(false);
s.setMarkAllAsReadConfirmation(true);
s.setCustomContextMenu(true);
} }
return Response.ok(s).build(); return Response.ok(s).build();
} }
@@ -145,6 +151,9 @@ 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.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation());
s.setCustomContextMenu(settings.isCustomContextMenu());
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) {

Some files were not shown because too many files have changed in this diff Show More