Compare commits

..

95 Commits

Author SHA1 Message Date
Athou
2b51de8e5b release 3.10.1 2023-12-08 17:19:31 +01:00
Athou
0ba70d29bd readme tweaks 2023-11-24 08:37:41 +01:00
Athou
197b3b258b also build with jdk 21 now that it's been released 2023-11-17 08:52:30 +01:00
Athou
850f66999c use less memory by returning unused memory to the OS (https://openjdk.org/jeps/346) 2023-11-16 08:41:29 +01:00
Athou
d7d3574e36 swap next and previous buttons (#1159) 2023-11-15 07:59:17 +01:00
Jérémie Panzer
435d612cbf Merge pull request #1164 from canoine/master
Update fr/messages.po
2023-11-02 12:53:05 +01:00
canoine
3d3a7c6496 Merge pull request #1 from canoine/canoine-patch-1
Update fr/messages.po
2023-10-18 09:10:41 +02:00
canoine
fba57fe0a7 Update fr/messages.po
Translation of the new fields.
2023-10-18 09:09:15 +02:00
Athou
ce7933f320 add mention of PikaPods 2023-10-02 19:38:06 +02:00
Athou
8ac452afc9 shorten count starting at 10k and add a tooltip with the exact count(#1150) 2023-09-23 16:44:25 +02:00
Jérémie Panzer
a11cb3ac7a Merge pull request #1154 from joerg376/patch-1
Update messages.po
2023-09-23 16:44:11 +02:00
joerg376
39808bbafc Update messages.po 2023-09-23 11:48:10 +02:00
Athou
aee56e3dbe no need to reload everything when websocket connection status changes 2023-09-19 12:31:19 +02:00
Athou
40f451c762 increase websocket ping interval to just under a minute instead of the default 15s 2023-09-12 20:22:34 +02:00
Athou
d633803ab5 only poll tree if websocket connection is unavailable 2023-09-12 20:22:03 +02:00
Athou
d7a3b75687 indicate that the feedLink property is not always filled (#1146) 2023-09-08 07:10:44 +02:00
Athou
df8c4056b6 indicate that the method returns the id of the newly created feed (#1147) 2023-09-08 07:07:29 +02:00
Athou
06319c1eb0 release 3.10.0 2023-09-06 09:04:21 +02:00
Athou
b7ede8eba2 add instructions for the Fever API 2023-09-05 11:04:52 +02:00
Athou
1a4517d6a3 add support for FeedMe 2023-09-05 11:04:52 +02:00
Athou
a402c5d7d8 add support for FocusReader 2023-09-05 11:04:52 +02:00
Athou
408809787e add support for Raven Reader 2023-09-05 11:04:52 +02:00
Athou
d7b0d572c1 add fever-compatible api 2023-09-05 11:04:52 +02:00
Athou
b356be3e6f show the whole title in the detailed view (#1097 #1144) 2023-09-05 09:10:26 +02:00
Athou
998385334b add metric for deleted entries 2023-09-03 12:16:43 +02:00
Athou
c6d613d81a add "s" keyboard shortcut to star/unstar entries (#1142) 2023-08-27 11:43:30 +02:00
Athou
9981d8763d don't set default values for env variables (#1141) 2023-08-24 07:51:10 +02:00
Athou
b37680333c clean database after each test 2023-08-23 20:36:57 +02:00
Athou
66d1eb3f1f store sessions in database 2023-08-23 20:34:29 +02:00
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
112 changed files with 3098 additions and 752 deletions

1
.github/stale.yml vendored
View File

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

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ "8", "11", "17" ]
java: [ "8", "11", "17", "21" ]
steps:
- name: Checkout

View File

@@ -1,5 +1,51 @@
# Changelog
## [3.10.1]
- swap next and previous buttons (#1159)
- unread count for subscriptions will now be shortened starting at 10k instead of 1k
- increased websocket ping interval to just under a minute to reduce data and battery usage on mobile
- only refresh subscription tree on a timer if websocket connection is unavailable
- the Docker image now uses less memory by returning unused memory to the OS
- add support for Java 21
## [3.10.0]
- added a Fever-compatible API that is usable with mobile clients that support the Fever API (see instructions in Settings -> Profile)
- long entry titles are no longer shortened in the detailed view
- added the "s" keyboard shortcut to star/unstar entries
- http sessions are now stored in the database (they were stored on disk before)
- fixed an issue that made it impossible to override the database url in a config.yml mounted in the Docker image
## [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
@@ -80,10 +126,9 @@
## [3.0.1]
- 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
its value
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with its value
- 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]

View File

@@ -4,10 +4,9 @@ EXPOSE 8082
RUN mkdir -p /commafeed/data
VOLUME /commafeed/data
ENV CF_SESSION_PATH=/commafeed/data/sessions
ENV CF_DATABASE_URL=jdbc:h2:/commafeed/data/db
COPY commafeed-server/config.yml.example config.yml
COPY commafeed-server/target/commafeed.jar .
CMD ["java", "-Djava.net.preferIPv4Stack=true", "-jar", "commafeed.jar", "server", "config.yml"]
ENV JAVA_TOOL_OPTIONS -Djava.net.preferIPv4Stack=true -Xms20m -XX:+UseG1GC -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
CMD ["java", "-jar", "commafeed.jar", "server", "config.yml"]

View File

@@ -7,22 +7,32 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ
## Features
- 4 different layouts
- Dark theme
- Light/Dark theme
- Fully responsive
- Keyboard shortcuts for almost everything
- Support for right-to-left feeds
- Translated in 25+ languages
- Supports thousands of users and millions of feeds
- OPML import/export
- REST API
- REST API and a Fever-compatible API for native mobile apps
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
## Deployment on your own server
## Deployment
### Docker
Docker is the easiest way to get started with CommaFeed.
Docker images are built automatically and are available at https://hub.docker.com/r/athou/commafeed
### Cloud hosting
[PikaPods](https://www.pikapods.com) offers 1-click cloud hosting solutions starting at $1/month with a free $5
welcome credit and officially supports CommaFeed.
PikaPods shares 20% of the revenue back to CommaFeed.
[![PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=commafeed)
### Download precompiled package
mkdir commafeed && cd commafeed

View File

@@ -4,13 +4,11 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<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" />
<title>CommaFeed</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="custom_js.js"></script>
</body>
</html>

View File

@@ -20,10 +20,13 @@
"@mantine/notifications": "^6.0.11",
"@mantine/spotlight": "^6.0.11",
"@mantine/styles": "^6.0.11",
"@monaco-editor/react": "^4.5.1",
"@reduxjs/toolkit": "^1.9.5",
"axios": "^1.4.0",
"dayjs": "^1.11.7",
"escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0",
"monaco-editor": "^0.38.0",
"mousetrap": "^1.6.5",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
@@ -2055,6 +2058,17 @@
"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": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
@@ -3049,6 +3063,30 @@
"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": {
"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",
@@ -6667,11 +6705,11 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"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==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"engines": {
"node": ">=10"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -7149,6 +7187,18 @@
"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": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
@@ -8999,6 +9049,11 @@
"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": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
@@ -11010,6 +11065,11 @@
"resolved": "https://registry.npmjs.org/stampit/-/stampit-4.3.2.tgz",
"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": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz",

View File

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

View File

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

View File

@@ -93,8 +93,8 @@ export const Constants = {
isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight,
},
dom: {
mainScrollAreaId: "main-scroll-area-id",
entryId: (entry: Entry) => `entry-id-${entry.id}`,
entryContextMenuId: (entry: Entry) => entry.id,
},
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",

View File

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

View File

@@ -27,6 +27,7 @@ interface EntriesState {
timestamp?: number
selectedEntryId?: string
hasMore: boolean
loading: boolean
search?: string
scrollingToEntry: boolean
}
@@ -40,23 +41,33 @@ const initialState: EntriesState = {
sourceWebsiteUrl: "",
entries: [],
hasMore: true,
loading: false,
scrollingToEntry: false,
}
const getEndpoint = (sourceType: EntrySourceType) =>
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
export const loadEntries = createAsyncThunk<Entries, { source: EntrySource; clearSearch: boolean }, { state: RootState }>(
"entries/load",
async (arg, 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 loadEntries = createAsyncThunk<
Entries,
{ source: EntrySource; clearSearch: boolean },
{
state: RootState
}
)
export const loadMoreEntries = createAsyncThunk<Entries, void, { state: RootState }>("entries/loadMore", async (_, thunkApi) => {
>("entries/load", async (arg, 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 { source } = state.entries
const offset =
@@ -74,7 +85,13 @@ const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource,
tag: source.type === "tag" ? source.id : undefined,
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()
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 }>(
"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 markAllEntries = createAsyncThunk<
void,
{ sourceType: EntrySourceType; req: MarkRequest },
{
state: RootState
}
)
>("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 }) => {
client.entry.star({
id: arg.entry.id,
@@ -175,31 +195,25 @@ export const selectEntry = createAsyncThunk<
if (arg.scrollToEntry) {
const entryElement = document.getElementById(Constants.dom.entryId(entry))
if (entryElement) {
const scrollSpeed = state.user.settings?.scrollSpeed
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true))
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false)))
const alwaysScrollToEntry = state.user.settings?.alwaysScrollToEntry
const entryEntirelyVisible = Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)
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) => {
// the entry is entirely visible, no need to scroll
if (Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)) {
onScrollEnded()
return
}
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
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,
})
}
scrollToWithCallback({
options: {
// add a small gap between the top of the content and the top of the page
top: entryElement.offsetTop - Constants.layout.headerHeight - 3,
behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
},
onScrollEnded,
})
}
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)
thunkApi.dispatch(reloadTags())
})
@@ -311,6 +331,10 @@ export const entriesSlice = createSlice({
state.sourceWebsiteUrl = ""
state.hasMore = true
state.selectedEntryId = undefined
state.loading = true
})
builder.addCase(loadMoreEntries.pending, state => {
state.loading = true
})
builder.addCase(loadEntries.fulfilled, (state, action) => {
state.entries = action.payload.entries
@@ -318,12 +342,14 @@ export const entriesSlice = createSlice({
state.sourceLabel = action.payload.name
state.sourceWebsiteUrl = action.payload.feedLink
state.hasMore = action.payload.hasMore
state.loading = false
})
builder.addCase(loadMoreEntries.fulfilled, (state, action) => {
// remove already existing entries
const entriesToAdd = action.payload.entries.filter(e => !state.entries.some(e2 => e.id === e2.id))
state.entries = [...state.entries, ...entriesToAdd]
state.hasMore = action.payload.hasMore
state.loading = false
})
builder.addCase(tagEntry.pending, (state, action) => {
state.entries

View File

@@ -1,18 +1,25 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
import { PayloadAction, createAsyncThunk, createSlice } from "@reduxjs/toolkit"
import { client } from "app/client"
import { ServerInfo } from "app/types"
interface ServerState {
serverInfos?: ServerInfo
webSocketConnected: boolean
}
const initialState: ServerState = {}
const initialState: ServerState = {
webSocketConnected: false,
}
export const reloadServerInfos = createAsyncThunk("server/infos", () => client.server.getServerInfos().then(r => r.data))
export const serverSlice = createSlice({
name: "server",
initialState,
reducers: {},
reducers: {
setWebSocketConnected: (state, action: PayloadAction<boolean>) => {
state.webSocketConnected = action.payload
},
},
extraReducers: builder => {
builder.addCase(reloadServerInfos.fulfilled, (state, action) => {
state.serverInfos = action.payload
@@ -20,4 +27,5 @@ export const serverSlice = createSlice({
},
})
export const { setWebSocketConnected } = serverSlice.actions
export default serverSlice.reducer

View File

@@ -80,6 +80,39 @@ export const changeScrollMarks = createAsyncThunk<
if (!settings) return
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<
void,
{ site: keyof SharingSettings; value: boolean },
@@ -136,6 +169,18 @@ export const userSlice = createSlice({
if (!state.settings) return
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) => {
if (!state.settings) return
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
@@ -146,6 +191,9 @@ export const userSlice = createSlice({
changeScrollSpeed.fulfilled,
changeShowRead.fulfilled,
changeScrollMarks.fulfilled,
changeAlwaysScrollToEntry.fulfilled,
changeMarkAllAsReadConfirmation.fulfilled,
changeCustomContextMenu.fulfilled,
changeSharingSetting.fulfilled
),
() => {

View File

@@ -3,38 +3,6 @@ export interface AddCategoryRequest {
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 {
id: string
parentId?: string
@@ -233,6 +201,9 @@ export interface Settings {
customCss?: string
customJs?: string
scrollSpeed: number
alwaysScrollToEntry: boolean
markAllAsReadConfirmation: boolean
customContextMenu: boolean
sharingSettings: SharingSettings
}
@@ -299,8 +270,6 @@ export interface UserModel {
admin: boolean
}
export type ApplicationSettingsCache = "NOOP" | "REDIS"
export type ReadingMode = "all" | "unread"
export type ReadingOrder = "asc" | "desc"

View File

@@ -1,3 +1,4 @@
import { throttle } from "throttle-debounce"
import { Category } from "./types"
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
@@ -26,30 +27,21 @@ export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?:
return { width: placeholderWidth, height: placeholderHeight }
}
export const scrollToWithCallback = ({
element,
options,
onScrollEnded,
}: {
element: HTMLElement
options: ScrollToOptions
onScrollEnded: () => void
}) => {
export const scrollToWithCallback = ({ options, onScrollEnded }: { options: ScrollToOptions; onScrollEnded: () => void }) => {
const offset = (options.top ?? 0).toFixed()
const onScroll = () => {
if (element.offsetTop.toFixed() === offset) {
element.removeEventListener("scroll", onScroll)
const onScroll = throttle(100, () => {
if (window.scrollY.toFixed() === offset) {
window.removeEventListener("scroll", onScroll)
onScrollEnded()
}
}
element.addEventListener("scroll", onScroll)
})
window.addEventListener("scroll", onScroll)
// scrollTo does not trigger if there's nothing to do, trigger it manually
onScroll()
element.scrollTo(options)
window.scrollTo(options)
}
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 { ActionIconProps } from "@mantine/core/lib/ActionIcon/ActionIcon"
import { ButtonProps } from "@mantine/core/lib/Button/Button"
import { useMediaQuery } from "@mantine/hooks"
import { useActionButton } from "hooks/useActionButton"
import { forwardRef, MouseEventHandler, ReactNode } from "react"
interface ActionButtonProps {
@@ -18,9 +18,9 @@ interface ActionButtonProps {
* Switches between Button with label (desktop) and ActionIcon (mobile)
*/
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
const { mobile } = useActionButton()
const theme = useMantineTheme()
const variant = props.variant ?? "subtle"
const mobile = !useMediaQuery(`(min-width: ${theme.breakpoints.lg})`)
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
return iconOnly ? (
<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

@@ -113,6 +113,14 @@ export function KeyboardShortcutsHelp() {
<Trans>Swipe header to the right</Trans>
</td>
</tr>
<tr>
<td>
<Trans>Toggle starred status of current entry</Trans>
</td>
<td>
<Kbd>S</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Mark all entries as read</Trans>
@@ -161,6 +169,20 @@ export function KeyboardShortcutsHelp() {
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Show native menu (desktop)</Trans>
</td>
<td>
<Kbd>
<Trans>Shift</Trans>
</Kbd>
<span> + </span>
<Kbd>
<Trans>Right click</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Show entry menu (mobile)</Trans>

View File

@@ -13,7 +13,7 @@ export function RelativeDate(props: { date: Date | number | undefined }) {
if (!props.date) return <Trans>N/A</Trans>
const date = dayjs(props.date)
return (
<Tooltip label={date.toString()} openDelay={500}>
<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 { Constants } from "app/constants"
import { useAppSelector } from "app/store"
import { calculatePlaceholderSize } from "app/utils"
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
import escapeStringRegexp from "escape-string-regexp"
import { ChildrenNode, Interweave, Matcher, MatchResponse, Node, TransformCallback } from "interweave"
import React from "react"
export interface ContentProps {
content: string
highlight?: string
}
const useStyles = createStyles(theme => ({
@@ -63,7 +65,7 @@ class HighlightMatcher extends Matcher {
constructor(search: string) {
super("highlight")
this.search = search
this.search = escapeStringRegexp(search)
}
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 search = useAppSelector(state => state.entries.search)
const matchers = search ? [new HighlightMatcher(search)] : []
const matchers = props.highlight ? [new HighlightMatcher(props.highlight)] : []
return (
<TypographyStylesProvider>
@@ -94,4 +96,6 @@ export function Content(props: ContentProps) {
</Box>
</TypographyStylesProvider>
)
}
})
export { Content }

View File

@@ -1,4 +1,5 @@
import { Trans } from "@lingui/macro"
import { Box } from "@mantine/core"
import { openModal } from "@mantine/modals"
import { Constants } from "app/constants"
import {
@@ -10,6 +11,7 @@ import {
selectEntry,
selectNextEntry,
selectPreviousEntry,
starEntry,
} from "app/slices/entries"
import { redirectToRootCategory } from "app/slices/redirect"
import { toggleSidebar } from "app/slices/tree"
@@ -20,6 +22,7 @@ import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useMousetrap } from "hooks/useMousetrap"
import { useViewMode } from "hooks/useViewMode"
import { useEffect } from "react"
import { useContextMenu } from "react-contexify"
import InfiniteScroll from "react-infinite-scroller"
import { throttle } from "throttle-debounce"
import { FeedEntry } from "./FeedEntry"
@@ -30,9 +33,11 @@ export function FeedEntries() {
const entriesTimestamp = useAppSelector(state => state.entries.timestamp)
const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId)
const hasMore = useAppSelector(state => state.entries.hasMore)
const loading = useAppSelector(state => state.entries.loading)
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
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 { openLinkInBackgroundTab } = useBrowserExtension()
@@ -59,10 +64,44 @@ export function FeedEntries() {
}
}
useEffect(() => {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
const contextMenu = useContextMenu()
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 (scrollingToEntry) return
@@ -84,11 +123,10 @@ export function FeedEntries() {
})
)
}
}
const throttledListener = throttle(100, listener)
scrollArea?.addEventListener("scroll", throttledListener)
return () => scrollArea?.removeEventListener("scroll", throttledListener)
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])
})
window.addEventListener("scroll", listener)
return () => window.removeEventListener("scroll", listener)
}, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry])
useMousetrap("r", () => dispatch(reloadEntries()))
useMousetrap("j", () =>
@@ -140,9 +178,8 @@ export function FeedEntries() {
})
)
} else {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
scrollArea?.scrollTo({
top: scrollArea.scrollTop + scrollArea.clientHeight * 0.8,
window.scrollTo({
top: window.scrollY + document.documentElement.clientHeight * 0.8,
behavior: "smooth",
})
}
@@ -179,9 +216,8 @@ export function FeedEntries() {
})
)
} else {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
scrollArea?.scrollTo({
top: scrollArea.scrollTop - scrollArea.clientHeight * 0.8,
window.scrollTo({
top: window.scrollY - document.documentElement.clientHeight * 0.8,
behavior: "smooth",
})
}
@@ -222,6 +258,11 @@ export function FeedEntries() {
if (!selectedEntry) return
dispatch(markEntry({ entry: selectedEntry, read: !selectedEntry.read }))
})
useMousetrap("s", () => {
// toggle starred status
if (!selectedEntry) return
dispatch(starEntry({ entry: selectedEntry, starred: !selectedEntry.starred }))
})
useMousetrap("shift+a", () => {
// mark all entries as read
dispatch(
@@ -250,11 +291,9 @@ export function FeedEntries() {
<InfiniteScroll
id="entries"
initialLoad={false}
loadMore={() => dispatch(loadMoreEntries())}
loadMore={() => !loading && dispatch(loadMoreEntries())}
hasMore={hasMore}
loader={<Loader key={0} />}
useWindow={false}
getScrollParent={() => document.getElementById(Constants.dom.mainScrollAreaId)}
loader={<Box key={0}>{loading && <Loader />}</Box>}
>
{entries.map(entry => (
<div
@@ -270,6 +309,9 @@ export function FeedEntries() {
showSelectionIndicator={entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")}
maxWidth={sidebarVisible ? Constants.layout.entryMaxWidth : undefined}
onHeaderClick={event => headerClicked(entry, event)}
onHeaderRightClick={event => headerRightClicked(entry, event)}
onBodyClick={() => bodyClicked(entry)}
onSwipedRight={() => swipedRight(entry)}
/>
</div>
))}

View File

@@ -1,15 +1,13 @@
import { Box, createStyles, Divider, Paper } from "@mantine/core"
import { MantineNumberSize } from "@mantine/styles"
import { Constants } from "app/constants"
import { markEntry } from "app/slices/entries"
import { useAppDispatch } from "app/store"
import { Entry, ViewMode } from "app/types"
import { useViewMode } from "hooks/useViewMode"
import React from "react"
import { useSwipeable } from "react-swipeable"
import { FeedEntryBody } from "./FeedEntryBody"
import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader"
import { FeedEntryContextMenu, useFeedEntryContextMenu } from "./FeedEntryContextMenu"
import { FeedEntryContextMenu } from "./FeedEntryContextMenu"
import { FeedEntryFooter } from "./FeedEntryFooter"
import { FeedEntryHeader } from "./FeedEntryHeader"
@@ -20,6 +18,9 @@ interface FeedEntryProps {
showSelectionIndicator: boolean
maxWidth?: number
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 }) => {
@@ -49,9 +50,16 @@ const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: View
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: {
backgroundColor,
borderLeftColor: paperBorderLeftColor,
marginTop: marginY,
marginBottom: marginY,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
@@ -69,30 +77,20 @@ const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: View
textDecoration: "none",
},
body: {
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) {
const { viewMode } = useViewMode()
const { classes, cx } = useStyles({ ...props, viewMode })
const dispatch = useAppDispatch()
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"
if (viewMode === "title" || viewMode === "cozy") paddingX = 6
@@ -130,7 +128,7 @@ export function FeedEntry(props: FeedEntryProps) {
rel="noreferrer"
onClick={props.onHeaderClick}
onAuxClick={props.onHeaderClick}
onContextMenu={onContextMenu}
onContextMenu={props.onHeaderRightClick}
>
<Box px={paddingX} py={paddingY} {...swipeHandlers}>
{compactHeader && <FeedEntryCompactHeader entry={props.entry} />}
@@ -138,8 +136,8 @@ export function FeedEntry(props: FeedEntryProps) {
</Box>
</a>
{props.expanded && (
<Box px={paddingX} pb={paddingY}>
<Box className={classes.body} sx={{ direction: props.entry.rtl ? "rtl" : "ltr" }}>
<Box px={paddingX} pb={paddingY} onClick={props.onBodyClick}>
<Box className={classes.body}>
<FeedEntryBody entry={props.entry} />
</Box>
<Divider variant="dashed" my={paddingY} />

View File

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

View File

@@ -7,10 +7,8 @@ import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types"
import { truncate } from "app/utils"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useEffect } from "react"
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 { throttle } from "throttle-debounce"
interface FeedEntryContextMenuProps {
entry: Entry
@@ -29,8 +27,6 @@ const useStyles = createStyles(theme => ({
},
}))
const menuId = (entry: Entry) => entry.id
export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
const { classes, theme } = useStyles()
const sourceType = useAppSelector(state => state.entries.source.type)
@@ -38,7 +34,7 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
const { openLinkInBackgroundTab } = useBrowserExtension()
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
onClick={() => {
window.open(props.entry.url, "_blank", "noreferrer")
@@ -102,29 +98,3 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
</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 { 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 { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types"
import { ActionButton } from "components/ActionButtton"
import { ButtonToolbar } from "components/ButtonToolbar"
import { useEffect, useState } from "react"
import { ActionButton } from "components/ActionButton"
import { useActionButton } from "hooks/useActionButton"
import { useMobile } from "hooks/useMobile"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
import { throttle } from "throttle-debounce"
import { ShareButtons } from "./ShareButtons"
interface FeedEntryFooterProps {
@@ -17,10 +14,10 @@ interface FeedEntryFooterProps {
}
export function FeedEntryFooter(props: FeedEntryFooterProps) {
const [scrollPosition, setScrollPosition] = useState(0)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
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 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 (
<Group position="apart">
<ButtonToolbar>
<Group spacing={spacing}>
{props.entry.markable && (
<ActionButton
icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />}
@@ -61,7 +48,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
/>
{showSharingButtons && (
<Popover withArrow withinPortal shadow="md" positionDependencies={[scrollPosition]} closeOnClickOutside={!mobile}>
<Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
<Popover.Target>
<ActionButton icon={<TbShare size={18} />} label={<Trans>Share</Trans>} />
</Popover.Target>
@@ -72,7 +59,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
)}
{tags && (
<Popover withArrow withinPortal shadow="md" positionDependencies={[scrollPosition]} closeOnClickOutside={!mobile}>
<Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
<Popover.Target>
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
<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">
<ActionButton icon={<TbExternalLink size={18} />} label={<Trans>Open link</Trans>} />
</a>
</ButtonToolbar>
</Group>
<ActionButton
icon={<TbArrowBarToDown size={18} />}

View File

@@ -1,4 +1,4 @@
import { Box, createStyles, Text } from "@mantine/core"
import { Box, createStyles, Space, Text } from "@mantine/core"
import { Entry } from "app/types"
import { RelativeDate } from "components/RelativeDate"
import { FeedEntryTitle } from "./FeedEntryTitle"
@@ -12,17 +12,11 @@ export interface FeedEntryHeaderProps {
const useStyles = createStyles((theme, props: FeedEntryHeaderProps) => ({
headerText: {
fontWeight: theme.colorScheme === "light" && !props.entry.read ? "bold" : "inherit",
whiteSpace: props.expanded ? "inherit" : "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
},
headerSubtext: {
display: "flex",
alignItems: "center",
fontSize: "90%",
whiteSpace: props.expanded ? "inherit" : "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
},
}))
export function FeedEntryHeader(props: FeedEntryHeaderProps) {
@@ -33,18 +27,13 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
<FeedEntryTitle entry={props.entry} />
</Box>
<Box className={classes.headerSubtext}>
<Box mr={6}>
<FeedFavicon url={props.entry.iconUrl} />
</Box>
<Box>
<Text color="dimmed">{props.entry.feedName}</Text>
</Box>
<Box>
<Text color="dimmed">
<span>&nbsp;·&nbsp;</span>
<RelativeDate date={props.entry.date} />
</Text>
</Box>
<FeedFavicon url={props.entry.iconUrl} />
<Space w={6} />
<Text color="dimmed">
{props.entry.feedName}
<span> · </span>
<RelativeDate date={props.entry.date} />
</Text>
</Box>
{props.expanded && (
<Box className={classes.headerSubtext}>

View File

@@ -1,15 +1,29 @@
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 { reloadEntries, search } from "app/slices/entries"
import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/slices/entries"
import { changeReadingMode, changeReadingOrder } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButtton"
import { ButtonToolbar } from "components/ButtonToolbar"
import { ActionButton } from "components/ActionButton"
import { Loader } from "components/Loader"
import { useActionButton } from "hooks/useActionButton"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useMobile } from "hooks/useMobile"
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 { ProfileMenu } from "./ProfileMenu"
@@ -17,6 +31,25 @@ function HeaderDivider() {
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
export function Header() {
@@ -42,7 +75,36 @@ export function Header() {
if (!settings) return <Loader />
return (
<Center>
<ButtonToolbar>
<HeaderToolbar>
<ActionButton
icon={<TbArrowUp size={iconSize} />}
label={<Trans>Previous</Trans>}
onClick={() =>
dispatch(
selectPreviousEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
}
/>
<ActionButton
icon={<TbArrowDown size={iconSize} />}
label={<Trans>Next</Trans>}
onClick={() =>
dispatch(
selectNextEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
}
/>
<HeaderDivider />
<ActionButton
icon={<TbRefresh size={iconSize} />}
label={<Trans>Refresh</Trans>}
@@ -58,7 +120,7 @@ export function Header() {
onClick={() => dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
/>
<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>}
onClick={() => dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
/>
@@ -106,7 +168,7 @@ export function Header() {
/>
</>
)}
</ButtonToolbar>
</HeaderToolbar>
</Center>
)
}

View File

@@ -3,7 +3,7 @@ import { Trans } from "@lingui/macro"
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
import { markAllEntries } from "app/slices/entries"
import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButtton"
import { ActionButton } from "components/ActionButton"
import { useState } from "react"
import { TbChecks } from "react-icons/tb"
@@ -13,8 +13,27 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
const source = useAppSelector(state => state.entries.source)
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
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 (
<>
<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>
</Stack>
</Modal>
<ActionButton
icon={<TbChecks size={props.iconSize} />}
label={<Trans>Mark all as read</Trans>}
onClick={() => {
setThreshold(0)
setOpened(true)
}}
/>
<ActionButton icon={<TbChecks size={props.iconSize} />} label={<Trans>Mark all as read</Trans>} onClick={buttonClicked} />
</>
)
}

View File

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

View File

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

View File

@@ -1,96 +1,83 @@
import { Trans } from "@lingui/macro"
import { Box, Button, Group, Stack, Textarea } from "@mantine/core"
import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
import { redirectToSelectedSource } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store"
import { Alert } from "components/Alert"
import { useEffect } from "react"
import { useAsyncCallback } from "react-async-hook"
import { TbDeviceFloppy } from "react-icons/tb"
interface FormData {
customCss: string
customJs: string
}
export function CustomCodeSettings() {
const settings = useAppSelector(state => state.user.settings)
const dispatch = useAppDispatch()
const form = useForm<FormData>()
const { setValues } = form
const saveCustomCode = useAsyncCallback(
async (d: FormData) => {
if (!settings) return
await client.user.saveSettings({
...settings,
customCss: d.customCss,
customJs: d.customJs,
})
},
{
onSuccess: () => {
window.location.reload()
},
}
)
useEffect(() => {
if (!settings) return
setValues({
customCss: settings.customCss,
customJs: settings.customJs,
})
}, [setValues, settings])
return (
<>
{saveCustomCode.error && (
<Box mb="md">
<Alert messages={errorToStrings(saveCustomCode.error)} />
</Box>
)}
<form onSubmit={form.onSubmit(saveCustomCode.execute)}>
<Stack>
<Textarea
autosize
minRows={4}
maxRows={15}
{...form.getInputProps("customCss")}
description={<Trans>Custom CSS rules that will be applied</Trans>}
styles={{
input: {
fontFamily: "monospace",
},
}}
/>
<Textarea
autosize
minRows={4}
maxRows={15}
{...form.getInputProps("customJs")}
description={<Trans>Custom JS code that will be executed on page load</Trans>}
styles={{
input: {
fontFamily: "monospace",
},
}}
/>
<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>
</>
)
}
import { Trans } from "@lingui/macro"
import { Box, Button, Group, Stack } from "@mantine/core"
import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
import { redirectToSelectedSource } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store"
import { Alert } from "components/Alert"
import { CodeEditor } from "components/code/CodeEditor"
import { useEffect } from "react"
import { useAsyncCallback } from "react-async-hook"
import { TbDeviceFloppy } from "react-icons/tb"
interface FormData {
customCss: string
customJs: string
}
export function CustomCodeSettings() {
const settings = useAppSelector(state => state.user.settings)
const dispatch = useAppDispatch()
const form = useForm<FormData>()
const { setValues } = form
const saveCustomCode = useAsyncCallback(
async (d: FormData) => {
if (!settings) return
await client.user.saveSettings({
...settings,
customCss: d.customCss,
customJs: d.customJs,
})
},
{
onSuccess: () => {
window.location.reload()
},
}
)
useEffect(() => {
if (!settings) return
setValues({
customCss: settings.customCss,
customJs: settings.customJs,
})
}, [setValues, settings])
return (
<>
{saveCustomCode.error && (
<Box mb="md">
<Alert messages={errorToStrings(saveCustomCode.error)} />
</Box>
)}
<form onSubmit={form.onSubmit(saveCustomCode.execute)}>
<Stack>
<CodeEditor
description={<Trans>Custom CSS rules that will be applied</Trans>}
language="css"
{...form.getInputProps("customCss")}
/>
<CodeEditor
description={<Trans>Custom JS code that will be executed on page load</Trans>}
language="javascript"
{...form.getInputProps("customJs")}
/>
<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 { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
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 { SharingSettings } from "app/types"
import { locales } from "i18n"
@@ -11,6 +20,9 @@ export function DisplaySettings() {
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
const showRead = useAppSelector(state => state.user.settings?.showRead)
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 dispatch = useAppDispatch()
@@ -32,6 +44,12 @@ export function DisplaySettings() {
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
label={<Trans>Show feeds and categories with no unread entries</Trans>}
checked={showRead}
@@ -44,6 +62,18 @@ export function DisplaySettings() {
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" />
<SimpleGrid cols={2}>

View File

@@ -77,10 +77,7 @@ export function ProfileSettings() {
<form onSubmit={form.onSubmit(saveProfile.execute)}>
<Stack>
<Input.Wrapper label={<Trans>User name</Trans>}>
<Box>{profile?.name}</Box>
</Input.Wrapper>
<TextInput label={<Trans>User name</Trans>} readOnly value={profile?.name} />
<TextInput label={<Trans>API key</Trans>} readOnly value={profile?.apiKey} />
<Input.Wrapper
@@ -98,6 +95,22 @@ export function ProfileSettings() {
</Box>
</Input.Wrapper>
<Input.Wrapper
label={<Trans>Fever API</Trans>}
description={
<Trans>
CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client.
The username is your user name and the password is your API key.
</Trans>
}
>
<Box>
<Anchor href={`rest/fever/user/${profile?.id}`} target="_blank">
<Trans>Fever API URL</Trans>
</Anchor>
</Box>
</Input.Wrapper>
<Divider />
<PasswordInput

View File

@@ -1,4 +1,4 @@
import { Badge, createStyles } from "@mantine/core"
import { Badge, createStyles, Tooltip } from "@mantine/core"
const useStyles = createStyles(() => ({
badge: {
@@ -13,6 +13,10 @@ export function UnreadCount(props: { unreadCount: number }) {
if (props.unreadCount <= 0) return null
const count = props.unreadCount >= 1000 ? "999+" : props.unreadCount
return <Badge className={classes.badge}>{count}</Badge>
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
return (
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count}>
<Badge className={classes.badge}>{count}</Badge>
</Tooltip>
)
}

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

@@ -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

@@ -1,3 +1,4 @@
import { setWebSocketConnected } from "app/slices/server"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch } from "app/store"
import { useEffect } from "react"
@@ -11,7 +12,14 @@ export const useWebSocket = () => {
const wsProtocol = currentUrl.protocol === "http:" ? "ws" : "wss"
const wsUrl = `${wsProtocol}://${currentUrl.hostname}:${currentUrl.port}/ws`
const ws = new WebsocketHeartbeatJs({ url: wsUrl, pingMsg: "ping" })
const ws = new WebsocketHeartbeatJs({
url: wsUrl,
pingMsg: "ping",
// ping interval, just under a minute to prevent firewalls from closing idle connections
pingTimeout: 55000,
})
ws.onopen = () => dispatch(setWebSocketConnected(true))
ws.onclose = () => dispatch(setWebSocketConnected(false))
ws.onmessage = event => {
const { data } = event
if (typeof data === "string") {

View File

@@ -71,6 +71,10 @@ msgstr "إداري"
msgid "All"
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
msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "تم إرسال بريد إلكتروني إذا تم تسجيل هذا العنوان. "
@@ -83,6 +87,10 @@ msgstr "ملف opml هو ملف XML يحتوي على عناوين URL للتغ
msgid "Analyze feed"
msgstr "تحليل التغذية"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "مفتاح API"
@@ -171,6 +179,10 @@ msgstr "تأكد من عمل الخلاصة"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed التالي العنصر غير المقروء"
@@ -335,6 +347,14 @@ msgstr "موجز URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "الملف مطلوب"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "الأحدث أولاً"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "التالي"
@@ -635,6 +656,10 @@ msgstr "كلمات المرور غير متطابقة"
msgid "Position"
msgstr "المنـصب"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "الملف الشخصي"
@@ -656,6 +681,7 @@ msgstr "تم إغلاق التسجيلات في مثيل CommaFeed هذا"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "مشاركة"
msgid "Sharing sites"
msgstr "مشاركة المواقع"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "إظهار موجز ويب والفئات التي لا تحتوي عل
msgid "Show keyboard shortcut help"
msgstr "إظهار تعليمات اختصار لوحة المفاتيح"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "تبديل قراءة حالة الإدخال الحالي"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي"

View File

@@ -71,6 +71,10 @@ msgstr "Administrador"
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "Un fitxer opml és un fitxer XML que conté URL i categories de canals.
msgid "Analyze feed"
msgstr "Analitzar el feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "clau API"
@@ -171,6 +179,10 @@ msgstr "Comproveu que el canal funciona"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed següent element no llegit"
@@ -335,6 +347,14 @@ msgstr "URL del canal"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "el fitxer és necessari"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "El més nou primer"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Següent"
@@ -635,6 +656,10 @@ msgstr "Les contrasenyes no coincideixen"
msgid "Position"
msgstr "Posició"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Perfil"
@@ -656,6 +681,7 @@ msgstr "Els registres estan tancats en aquesta instància de CommaFeed"
msgid "REST API"
msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Comparteix"
msgid "Sharing sites"
msgstr "Compartir llocs"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Mostra feeds i categories sense entrades no llegides"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Canvia l'estat de lectura de l'entrada actual"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Správce"
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "Soubor opml je soubor XML obsahující adresy URL a kategorie zdrojů. "
msgid "Analyze feed"
msgstr "Analyzujte krmivo"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "Klíč API"
@@ -171,6 +179,10 @@ msgstr "Zkontrolujte, zda zdroj funguje"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed další nepřečtená položka"
@@ -335,6 +347,14 @@ msgstr "URL zdroje"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr ""
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Nejnovější jako první"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Další"
@@ -635,6 +656,10 @@ msgstr "Hesla se neshodují"
msgid "Position"
msgstr "Pozice"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -656,6 +681,7 @@ msgstr "V této instanci CommaFeed jsou registrace uzavřeny"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Sdílejte"
msgid "Sharing sites"
msgstr "Stránky pro sdílení"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Zobrazit kanály a kategorie bez nepřečtených položek"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Přepne stav čtení aktuálního záznamu"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Gweinyddol"
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "Mae ffeil opml yn ffeil XML sy'n cynnwys URLs porthiant a chategorïau.
msgid "Analyze feed"
msgstr "Dadansoddi porthiant"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "Allwedd API"
@@ -171,6 +179,10 @@ msgstr "Gwiriwch fod y porthiant yn gweithio"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed eitem nesaf heb ei darllen"
@@ -335,6 +347,14 @@ msgstr "URL porthiant"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "mae angen y ffeil"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Y diweddaraf yn gyntaf"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Nesaf"
@@ -635,6 +656,10 @@ msgstr "Nid yw cyfrineiriau yn cyfateb"
msgid "Position"
msgstr "Swydd"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Proffil"
@@ -656,6 +681,7 @@ msgstr "Mae cofrestriadau ar gau ar yr achos CommaFeed hwn"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Rhannu"
msgid "Sharing sites"
msgstr "Rhannu gwefannau"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Dangos ffrydiau a chategorïau heb unrhyw gofnodion heb eu darllen"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Toglo statws darllen y cofnod cyfredol"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo"

View File

@@ -71,6 +71,10 @@ msgstr ""
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "En opml-fil er en XML-fil, der indeholder feed-URL'er og kategorier. "
msgid "Analyze feed"
msgstr "Analyser foder"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "API-nøgle"
@@ -171,6 +179,10 @@ msgstr "Tjek, at foderet virker"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed næste ulæste element"
@@ -335,6 +347,14 @@ msgstr ""
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil er påkrævet"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Nyeste først"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Næste"
@@ -635,6 +656,10 @@ msgstr "Adgangskoder stemmer ikke overens"
msgid "Position"
msgstr ""
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -656,6 +681,7 @@ msgstr "Registreringer er lukket på denne CommaFeed-instans"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Del"
msgid "Sharing sites"
msgstr "Delingssider"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Vis feeds og kategorier uden ulæste poster"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Skift læsestatus for den aktuelle post"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -19,11 +19,11 @@ msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
msgstr "CommaFeed ist ein Open Source Projekt. Der Quellcode wird auf auf </0><1>GitHub</1> gehostet."
#: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr ""
msgstr "Vollständiger Syntax ist </0><1>hier</1> verfügbar."
#: src/pages/auth/RegistrationPage.tsx
msgid "<0>Have an account?</0><1>Log in!</1>"
@@ -35,12 +35,12 @@ msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Benötigen Sie ein Konto?</0><1>Melden Sie sich an!</1>"
msgstr "<0>Benötigen Sie ein Konto?</0><1>Hier geht's zur Registrierung!</1>"
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/AboutPage.tsx
msgid "About"
msgstr "Ungefähr"
msgstr "Über"
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
@@ -71,18 +71,26 @@ msgstr "Verwaltung"
msgid "All"
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
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. Bitte den Posteingang prüfen."
#: src/components/content/add/ImportOpml.tsx
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
msgstr "Eine opml-Datei ist eine XML-Datei, die Feed-URLs und Kategorien enthält. "
msgstr "Eine opml-Datei ist eine XML-Datei, die Feed-URLs und Kategorien enthält."
#: src/components/content/add/Subscribe.tsx
msgid "Analyze feed"
msgstr "Feed analysieren"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr "Ankündigung"
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "API-Schlüssel"
@@ -97,7 +105,7 @@ msgstr "Sind Sie sicher, dass Sie Benutzer <0>{userName}</0> löschen möchten?"
#: src/components/settings/ProfileSettings.tsx
msgid "Are you sure you want to delete your account? There's no turning back!"
msgstr "Sind Sie sicher, dass Sie Ihr Konto löschen möchten? "
msgstr "Sind Sie sicher, dass Sie Ihr Konto löschen möchten?"
#: src/components/header/MarkAllAsReadButton.tsx
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
@@ -113,7 +121,7 @@ msgstr "Sind Sie sicher, dass Sie <0>{feedName}</0> abbestellen möchten?"
#: src/components/header/Header.tsx
msgid "Asc"
msgstr "Asz"
msgstr "Aufsteigend"
#: src/pages/app/FeedDetailsPage.tsx
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
@@ -125,15 +133,15 @@ msgstr "Zurück"
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Back to log in"
msgstr "Zurück zum Anmelden"
msgstr "Zurück zum Login"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
msgstr "Browser-Erweiterung für Chrome benötigt"
#: src/pages/app/AboutPage.tsx
msgid "Browser extention"
msgstr ""
msgstr "Browser-Erweiterung"
#: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx
@@ -171,6 +179,10 @@ msgstr "Überprüfen Sie, ob der Feed funktioniert"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr "CommaFeed ist kompatibel zur Fever API. Benutzen Sie folgende URL in Ihrem Fever-kompatiblen Mobilclient. Der Benutzername ist Ihr User Name, das Passwort ist der API-Schlüssel."
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed nächstes ungelesenes Element"
@@ -260,7 +272,7 @@ msgstr "Anzeige"
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/DonatePage.tsx
msgid "Donate"
msgstr ""
msgstr "Spenden"
#: src/components/settings/ProfileSettings.tsx
msgid "Download"
@@ -319,7 +331,7 @@ msgstr "Exportieren Sie Ihre Abonnements und Kategorien als OPML-Datei, die in a
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
msgstr "Erweiterungsoptionen"
#: src/components/content/add/Subscribe.tsx
msgid "Feed name"
@@ -333,6 +345,14 @@ msgstr "Feed-URL"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr "Alle Feeds jetzt abrufen"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
@@ -365,7 +385,7 @@ msgstr "Generierte Feed-URL"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
msgstr "Gehe zu {0}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Go to the All view"
@@ -377,7 +397,7 @@ msgstr "Gehen Sie zur API-Dokumentation."
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Gutes"
msgstr "Goodies"
#: src/pages/admin/AdminUsersPage.tsx
msgid "Id"
@@ -407,7 +427,7 @@ msgstr "Ungelesen lassen"
#: src/components/content/FeedEntries.tsx
#: src/pages/app/AboutPage.tsx
msgid "Keyboard shortcuts"
msgstr "Tastenkürzel"
msgstr "Tastaturkürzel"
#: src/components/settings/DisplaySettings.tsx
msgid "Language"
@@ -459,7 +479,7 @@ msgstr "Abmelden"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
msgstr "Langer Tastendruck"
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
@@ -491,7 +511,7 @@ msgstr "Metriken"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
msgstr "Mittelklick"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
@@ -504,7 +524,7 @@ msgstr "Bewege die Seite nach oben"
#: src/components/RelativeDate.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "N/A"
msgstr "n. z"
msgstr "n.v."
#: src/components/admin/UserEdit.tsx
#: src/pages/admin/AdminUsersPage.tsx
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Neueste zuerst"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Weiter"
@@ -571,11 +592,11 @@ msgstr "Link öffnen"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
msgstr "Link in neuem Tab im Hintergrund öffnen"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
msgstr "Link in neuem Tab öffnen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
@@ -608,11 +629,11 @@ msgstr "Bestellung"
#: src/components/content/add/AddCategory.tsx
msgid "Parent"
msgstr "Elternteil"
msgstr "Übergeordnet"
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Parent Category"
msgstr "Elternkategorie"
msgstr "Übergeordnete Kategorie"
#: src/components/admin/UserEdit.tsx
#: src/pages/auth/LoginPage.tsx
@@ -633,7 +654,11 @@ msgstr "Passwörter stimmen nicht überein"
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Position"
msgstr "Stellung"
msgstr "Position"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr "Vorheriges"
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
@@ -656,9 +681,10 @@ msgstr "Registrierungen sind für diese CommaFeed-Instanz geschlossen"
msgid "REST API"
msgstr "REST-API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
msgstr "Rechtsklick"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
@@ -670,7 +696,7 @@ msgstr "Speichern"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Geschwindes Scrollen beim Navigieren zwischen Einträgen"
msgstr "Schnelles Scrollen beim Navigieren zwischen Einträgen"
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
@@ -707,11 +733,20 @@ msgstr "Teilen"
msgid "Sharing sites"
msgstr "Seiten teilen"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
msgstr "Verschiebung"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr "CommaFeed-Kontextmenü anzeigen bei Rechtsklick"
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr "Bestätigung beim Markieren von allen Einträgen als gelesen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Feeds und Kategorien ohne ungelesene Einträge anzeigen"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -778,12 +817,12 @@ msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme"
msgstr "Zum dunklen Design wechseln"
msgstr "Zum Darkmode wechseln"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme"
msgstr "Wechseln Sie zum Lichtdesign"
msgstr "Zum Lightmode wechseln"
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
@@ -803,7 +842,11 @@ msgstr "Lesestatus des aktuellen Eintrags umschalten"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
msgstr "Sidebar an- und ausschalten"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr "Markierungsstatus des aktuellen Eintrags ändern"
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
@@ -811,7 +854,7 @@ msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"
#: src/pages/WelcomePage.tsx
msgid "Try the demo!"
msgstr ""
msgstr "Testen Sie die Demo!"
#: src/components/header/Header.tsx
msgid "Unread"
@@ -850,4 +893,4 @@ msgstr "Sie haben noch keine Abonnements. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
msgstr "Ihr Feed wurde für die Aktualisierung eingereiht."

View File

@@ -71,6 +71,10 @@ msgstr "Admin"
msgid "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
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."
@@ -83,6 +87,10 @@ msgstr "An opml file is an XML file containing feed URLs and categories. You can
msgid "Analyze feed"
msgstr "Analyze feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr "Announcement"
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "API key"
@@ -171,6 +179,10 @@ msgstr "Check that the feed is working"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed browser extension version {browserExtensionVersion}."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed next unread item"
@@ -335,6 +347,14 @@ msgstr "Feed URL"
msgid "Fetch all my feeds now"
msgstr "Fetch all my feeds now"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr "Fever API"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr "Fever API URL"
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "file is required"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Newest first"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Next"
@@ -635,6 +656,10 @@ msgstr "Passwords do not match"
msgid "Position"
msgstr "Position"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr "Previous"
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profile"
@@ -656,6 +681,7 @@ msgstr "Registrations are closed on this CommaFeed instance"
msgid "REST API"
msgstr "REST API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr "Right click"
@@ -707,11 +733,20 @@ msgstr "Share"
msgid "Sharing sites"
msgstr "Sharing sites"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "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
msgid "Show entry menu (desktop)"
msgstr "Show entry menu (desktop)"
@@ -728,6 +763,10 @@ msgstr "Show feeds and categories with no unread entries"
msgid "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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Toggle read status of current entry"
msgid "Toggle sidebar"
msgstr "Toggle sidebar"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr "Toggle starred status of current entry"
#: src/pages/auth/LoginPage.tsx
msgid "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"
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
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. "
@@ -83,6 +87,10 @@ msgstr "Un archivo opml es un archivo XML que contiene categorías y direcciones
msgid "Analyze feed"
msgstr "Analizar alimentación"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "clave API"
@@ -171,6 +179,10 @@ msgstr "Compruebe que el feed funciona"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed siguiente elemento no leído"
@@ -335,6 +347,14 @@ msgstr "URL de fuente"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "archivo requerido"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "más reciente primero"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Siguiente"
@@ -635,6 +656,10 @@ msgstr "Las contraseñas no coinciden"
msgid "Position"
msgstr "Posición"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Perfil"
@@ -656,6 +681,7 @@ msgstr "Los registros están cerrados en esta instancia de CommaFeed"
msgid "REST API"
msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Compartir"
msgid "Sharing sites"
msgstr "Compartir sitios"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Mostrar feeds y categorías sin entradas no leídas"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Alternar estado de lectura de la entrada actual"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "مدیر"
msgid "All"
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
msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "اگر این آدرس ثبت شده باشد ایمیل ارسال شده است. "
@@ -83,6 +87,10 @@ msgstr "یک فایل opml یک فایل XML است که حاوی آدرس‌ه
msgid "Analyze feed"
msgstr "خوراک را تجزیه و تحلیل کنید"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "کلید API"
@@ -171,6 +179,10 @@ msgstr "بررسی کنید که خوراک کار می کند"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "مورد خوانده نشده بعدی CommaFeed"
@@ -335,6 +347,14 @@ msgstr "URL فید"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "فایل مورد نیاز است"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "ابتدا جدیدترین"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "بعد"
@@ -635,6 +656,10 @@ msgstr "گذرواژه ها مطابقت ندارند"
msgid "Position"
msgstr "موقعیت"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "نمایه"
@@ -656,6 +681,7 @@ msgstr "ثبت نام در این نمونه CommaFeed بسته شده است"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "به اشتراک بگذارید"
msgid "Sharing sites"
msgstr "اشتراک گذاری سایت ها"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "فیدها و دسته ها را بدون ورودی خوانده نشد
msgid "Show keyboard shortcut help"
msgstr "نمایش راهنمایی میانبر صفحه کلید"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو"

View File

@@ -71,6 +71,10 @@ msgstr "Järjestelmänvalvoja"
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "Opml-tiedosto on XML-tiedosto, joka sisältää syötteen URL-osoitteet
msgid "Analyze feed"
msgstr "Analysoi syöte"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "API-avain"
@@ -171,6 +179,10 @@ msgstr "Tarkista, että syöttö toimii"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed seuraava lukematon kohde"
@@ -335,6 +347,14 @@ msgstr "Syötteen URL-osoite"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "tiedosto vaaditaan"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Uusin ensin"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Seuraava"
@@ -635,6 +656,10 @@ msgstr "Salasanat eivät täsmää"
msgid "Position"
msgstr "Sijainti"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profiili"
@@ -656,6 +681,7 @@ msgstr "Tämän CommaFeed-esiintymän rekisteröinnit on suljettu"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Jaa"
msgid "Sharing sites"
msgstr "Sivustojen jakaminen"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Näytä syötteet ja luokat ilman lukemattomia merkintöjä"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Vaihda nykyisen merkinnän lukutila"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Kokeile CommaFeediä demotilillä: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Administrateur"
msgid "All"
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
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."
@@ -83,6 +87,10 @@ msgstr "Un fichier OPML est un fichier XML contenant des URL de flux et des cat
msgid "Analyze feed"
msgstr "Analyser le flux"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr "Annonces"
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "Clé API"
@@ -129,11 +137,11 @@ msgstr "Retour à la connexion"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
msgstr "L'extension navigateur est nécessaire sur Chrome"
#: src/pages/app/AboutPage.tsx
msgid "Browser extention"
msgstr ""
msgstr "Extension navigateur"
#: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx
@@ -169,7 +177,11 @@ msgstr "Vérifie que le flux fonctionne"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr "Commafeed est compatible avec l'API Fever, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre clef API comme mot de passe."
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
@@ -177,7 +189,7 @@ msgstr "CommaFeed prochain article non lu"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})."
msgstr ""
msgstr "CommaFeed version {version} ({revision})."
#: src/components/header/ProfileMenu.tsx
msgid "Compact"
@@ -260,7 +272,7 @@ msgstr "Affichage"
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/DonatePage.tsx
msgid "Donate"
msgstr "Faites un don"
msgstr "Faire un don"
#: src/components/settings/ProfileSettings.tsx
msgid "Download"
@@ -319,7 +331,7 @@ msgstr "Exporter vos abonnements et catégories en tant que fichier OPML qui peu
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
msgstr "Options de l'extension"
#: src/components/content/add/Subscribe.tsx
msgid "Feed name"
@@ -335,6 +347,14 @@ msgstr "URL du flux"
msgid "Fetch all my feeds now"
msgstr "Rafraîchir tous mes flux"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr "API Fever"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr "URL API Fever"
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fichier requis"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Plus récent en premier"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Suivant"
@@ -555,7 +576,7 @@ msgstr "Oups !"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
msgstr "Ouvrir CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab"
@@ -635,6 +656,10 @@ msgstr "Les mots de passe ne correspondent pas"
msgid "Position"
msgstr "Position"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr "Précédent"
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -656,6 +681,7 @@ msgstr "Les inscriptions sont fermées sur cette instance de CommaFeed"
msgid "REST API"
msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr "Clic droit"
@@ -707,11 +733,20 @@ msgstr "Partager"
msgid "Sharing sites"
msgstr "Sites de partage"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
msgstr "Maj"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr "Afficher le menu contextuel de Commafeed lors d'un clic droit"
#: 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
msgid "Show entry menu (desktop)"
msgstr "Afficher les options de l'entrée (ordinateur)"
@@ -728,6 +763,10 @@ msgstr "Afficher les flux et les catégories pour lesquels tout est déjà lu"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -803,7 +842,11 @@ msgstr "Marquer l'entrée actuelle comme lue/non lue"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
msgstr "Montrer/cacher la barre latérale"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr "Montrer/cacher le statut favori de l'entrée"
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Administración"
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "Un ficheiro opml é un ficheiro XML que contén URL e categorías de fon
msgid "Analyze feed"
msgstr "Analizar feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "chave API"
@@ -171,6 +179,10 @@ msgstr "Comproba que a fonte funciona"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed seguinte elemento non lido"
@@ -335,6 +347,14 @@ msgstr "URL da fonte"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "é necesario o ficheiro"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "o máis novo primeiro"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Seguinte"
@@ -635,6 +656,10 @@ msgstr "Os contrasinais non coinciden"
msgid "Position"
msgstr "Posición"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Perfil"
@@ -656,6 +681,7 @@ msgstr "Os rexistros están pechados nesta instancia de CommaFeed"
msgid "REST API"
msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Compartir"
msgid "Sharing sites"
msgstr "Compartir sitios"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Mostrar fontes e categorías sen entradas sen ler"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "alternar o estado de lectura da entrada actual"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proba CommaFeed coa conta de demostración: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr ""
msgid "All"
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
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. "
@@ -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"
msgstr "Hírcsatorna elemzése"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "API kulcs"
@@ -171,6 +179,10 @@ msgstr "Ellenőrizze, hogy a feed működik-e"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed következő olvasatlan elem"
@@ -335,6 +347,14 @@ msgstr ""
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fájl szükséges"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "A legújabbak először"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Következő"
@@ -635,6 +656,10 @@ msgstr "A jelszavak nem egyeznek"
msgid "Position"
msgstr "Pozíció"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -656,6 +681,7 @@ msgstr "A regisztrációk le vannak zárva ezen a CommaFeed példányon"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Oszd meg"
msgid "Sharing sites"
msgstr "Webhelyek megosztása"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Hírcsatornák és kategóriák megjelenítése olvasatlan bejegyzések
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Az aktuális bejegyzés olvasási állapotának váltása"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: 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"
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
msgid "An email has been sent if this address was registered. Check your inbox."
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"
msgstr "Analisis umpan"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "kunci API"
@@ -171,6 +179,10 @@ msgstr "Periksa apakah umpannya berfungsi"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed item yang belum dibaca berikutnya"
@@ -335,6 +347,14 @@ msgstr "URL Umpan"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "file diperlukan"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Terbaru dulu"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Selanjutnya"
@@ -635,6 +656,10 @@ msgstr "Kata sandi tidak cocok"
msgid "Position"
msgstr "Posisi"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -656,6 +681,7 @@ msgstr "Pendaftaran ditutup pada instans CommaFeed ini"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Bagikan"
msgid "Sharing sites"
msgstr "Berbagi situs"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Tampilkan umpan dan kategori tanpa entri yang belum dibaca"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Beralih status baca entri saat ini"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cobalah CommaFeed dengan akun demo: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Ammin"
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "Un file opml è un file XML contenente URL e categorie di feed. "
msgid "Analyze feed"
msgstr "Analizza feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "Chiave API"
@@ -171,6 +179,10 @@ msgstr "Verifica che il feed funzioni"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed successivo elemento non letto"
@@ -335,6 +347,14 @@ msgstr "URL feed"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "è richiesto il file"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Il più recente prima"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Avanti"
@@ -635,6 +656,10 @@ msgstr "Le password non corrispondono"
msgid "Position"
msgstr "Posizione"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profilo"
@@ -656,6 +681,7 @@ msgstr "Le registrazioni sono chiuse su questa istanza CommaFeed"
msgid "REST API"
msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Condividi"
msgid "Sharing sites"
msgstr "Condivisione di siti"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Mostra feed e categorie senza voci non lette"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Commuta lo stato di lettura della voce corrente"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed con il conto demo: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "管理人"
msgid "All"
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
msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "このアドレスが登録されていれば、メールが送信されました。"
@@ -83,6 +87,10 @@ msgstr "opml ファイルは、フィードの URL とカテゴリを含む XML
msgid "Analyze feed"
msgstr "フィードを分析する"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "APIキー"
@@ -171,6 +179,10 @@ msgstr "フィードが動作していることを確認してください"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "次の未読アイテムをカンマフィード"
@@ -335,6 +347,14 @@ msgstr "フィード URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "ファイルが必要です"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "最新順"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "次へ"
@@ -635,6 +656,10 @@ msgstr "パスワードが一致しません"
msgid "Position"
msgstr "位置"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "プロフィール"
@@ -656,6 +681,7 @@ msgstr "この CommaFeed インスタンスの登録は終了しています"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "シェア"
msgid "Sharing sites"
msgstr "共有サイト"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "未読エントリのないフィードとカテゴリを表示する"
msgid "Show keyboard shortcut help"
msgstr "キーボード ショートカットのヘルプを表示"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "現在のエントリの読み取りステータスを切り替えます
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "デモアカウントで CommaFeed を試す: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "관리자"
msgid "All"
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
msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "이 주소가 등록된 경우 이메일이 전송되었습니다. "
@@ -83,6 +87,10 @@ msgstr "opml 파일은 피드 URL과 카테고리를 포함하는 XML 파일입
msgid "Analyze feed"
msgstr "피드 분석"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "API 키"
@@ -171,6 +179,10 @@ msgstr "피드가 작동하는지 확인"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "다음 읽지 않은 항목을 쉼표로 피드"
@@ -335,6 +347,14 @@ msgstr "피드 URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "파일이 필요합니다"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "최신순"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "다음"
@@ -635,6 +656,10 @@ msgstr "비밀번호가 일치하지 않습니다"
msgid "Position"
msgstr "위치"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "프로필"
@@ -656,6 +681,7 @@ msgstr "이 CommaFeed 인스턴스에 대한 등록이 마감되었습니다."
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "공유"
msgid "Sharing sites"
msgstr "사이트 공유"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "읽지 않은 항목이 없는 피드 및 카테고리 표시"
msgid "Show keyboard shortcut help"
msgstr "키보드 단축키 도움말 표시"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "현재 항목의 읽기 상태 전환"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Pentadbir"
msgid "All"
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
msgid "An email has been sent if this address was registered. Check your inbox."
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"
msgstr "Menganalisis suapan"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "Kunci API"
@@ -171,6 +179,10 @@ msgstr "Semak sama ada suapan berfungsi"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed item belum dibaca seterusnya"
@@ -335,6 +347,14 @@ msgstr "URL Suapan"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fail diperlukan"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Terbaharu dahulu"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Seterusnya"
@@ -635,6 +656,10 @@ msgstr "Kata laluan tidak sepadan"
msgid "Position"
msgstr "Kedudukan"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -656,6 +681,7 @@ msgstr "Pendaftaran ditutup pada contoh CommaFeed ini"
msgid "REST API"
msgstr "REHAT API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Kongsi"
msgid "Sharing sites"
msgstr "Berkongsi tapak"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Tunjukkan suapan dan kategori tanpa entri yang belum dibaca"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Togol status bacaan entri semasa"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cuba CommaFeed dengan akaun demo: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr ""
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "En opml-fil er en XML-fil som inneholder feed-URLer og kategorier. "
msgid "Analyze feed"
msgstr "Analyser feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "API-nøkkel"
@@ -171,6 +179,10 @@ msgstr "Sjekk at feeden fungerer"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed neste uleste element"
@@ -335,6 +347,14 @@ msgstr "Feed-URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil kreves"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Nyeste først"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Neste"
@@ -635,6 +656,10 @@ msgstr "Passordene samsvarer ikke"
msgid "Position"
msgstr "Posisjon"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -656,6 +681,7 @@ msgstr "Registreringer er stengt på denne CommaFeed-forekomsten"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Del"
msgid "Sharing sites"
msgstr "Delingssider"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Vis feeder og kategorier uten uleste oppføringer"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Veksle lesestatus for gjeldende oppføring"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Beheerder"
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "Een opml-bestand is een XML-bestand met feed-URL's en categorieën. "
msgid "Analyze feed"
msgstr "Analyseer feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "API-sleutel"
@@ -171,6 +179,10 @@ msgstr "Controleer of de feed werkt"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed volgende ongelezen item"
@@ -335,6 +347,14 @@ msgstr "Feed-URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "bestand is vereist"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Nieuwste eerst"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Volgende"
@@ -635,6 +656,10 @@ msgstr "Wachtwoorden komen niet overeen"
msgid "Position"
msgstr "Positie"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profiel"
@@ -656,6 +681,7 @@ msgstr "Registraties zijn gesloten op deze CommaFeed-instantie"
msgid "REST API"
msgstr "REST-API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Delen"
msgid "Sharing sites"
msgstr "Sites delen"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Toon feeds en categorieën zonder ongelezen items"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Toggle leesstatus van huidige invoer"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Probeer CommaFeed uit met het demo-account: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr ""
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "En opml-fil er en XML-fil som inneholder feed-URLer og kategorier. "
msgid "Analyze feed"
msgstr "Analyser feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "API-nøkkel"
@@ -171,6 +179,10 @@ msgstr "Sjekk at feeden fungerer"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed neste uleste element"
@@ -335,6 +347,14 @@ msgstr "Feed-URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil kreves"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Nyeste først"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Neste"
@@ -635,6 +656,10 @@ msgstr "Passordene samsvarer ikke"
msgid "Position"
msgstr "Posisjon"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -656,6 +681,7 @@ msgstr "Registreringer er stengt på denne CommaFeed-forekomsten"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Del"
msgid "Sharing sites"
msgstr "Delingssider"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Vis feeder og kategorier uten uleste oppføringer"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Veksle lesestatus for gjeldende oppføring"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Administracja"
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "Plik opml to plik XML zawierający adresy URL i kategorie kanałów. "
msgid "Analyze feed"
msgstr "Analizuj kanał"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "klucz API"
@@ -171,6 +179,10 @@ msgstr "Sprawdź, czy kanał działa"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "Przecinek następny nieprzeczytany element"
@@ -335,6 +347,14 @@ msgstr "URL kanału"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "plik jest wymagany"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Najnowsze jako pierwsze"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Dalej"
@@ -635,6 +656,10 @@ msgstr "Hasła nie pasują"
msgid "Position"
msgstr "Pozycja"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -656,6 +681,7 @@ msgstr "Rejestracje są zamknięte w tej instancji CommaFeed"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Udostępnij"
msgid "Sharing sites"
msgstr "Udostępnianie witryn"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Pokaż kanały i kategorie bez nieprzeczytanych wpisów"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Przełącz stan odczytu bieżącego wpisu"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Wypróbuj CommaFeed z kontem demo: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Administrador"
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "Um arquivo opml é um arquivo XML contendo URLs e categorias de feed. "
msgid "Analyze feed"
msgstr "Analisar feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "chave de API"
@@ -171,6 +179,10 @@ msgstr "Verifique se o feed está funcionando"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed próximo item não lido"
@@ -335,6 +347,14 @@ msgstr "URL do feed"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "o arquivo é obrigatório"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Mais novo primeiro"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Próximo"
@@ -635,6 +656,10 @@ msgstr "Senhas não coincidem"
msgid "Position"
msgstr "Posição"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Perfil"
@@ -656,6 +681,7 @@ msgstr "Os registros estão fechados nesta instância do CommaFeed"
msgid "REST API"
msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Compartilhar"
msgid "Sharing sites"
msgstr "Compartilhando sites"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Mostrar feeds e categorias sem entradas não lidas"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Alternar o status de leitura da entrada atual"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Experimente o CommaFeed com a conta demo: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Админ"
msgid "All"
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
msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Электронное письмо было отправлено, если этот адрес был зарегистрирован. "
@@ -83,6 +87,10 @@ msgstr "OPML-файл — это XML-файл, содержащий URL-адре
msgid "Analyze feed"
msgstr "Анализ канала"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "ключ API"
@@ -171,6 +179,10 @@ msgstr "Проверьте, работает ли лента."
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed следующий непрочитанный элемент"
@@ -335,6 +347,14 @@ msgstr "URL-адрес фида"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "требуется файл"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Сначала новые"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Далее"
@@ -635,6 +656,10 @@ msgstr "Пароли не совпадают"
msgid "Position"
msgstr "Позиция"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Профиль"
@@ -656,6 +681,7 @@ msgstr "Регистрация закрыта для этого экземпля
msgid "REST API"
msgstr "ОТДЫХА API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Поделиться"
msgid "Sharing sites"
msgstr "Обмен сайтами"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Показать каналы и категории без непроч
msgid "Show keyboard shortcut help"
msgstr "Показать справку по сочетаниям клавиш."
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Переключить статус чтения текущей запи
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Попробуйте CommaFeed на демо-счете: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr "Správca"
msgid "All"
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
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á. "
@@ -83,6 +87,10 @@ msgstr "Súbor opml je súbor XML obsahujúci adresy URL kanálov a kategórie.
msgid "Analyze feed"
msgstr "Analyzujte krmivo"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "Kľúč API"
@@ -171,6 +179,10 @@ msgstr "Skontrolujte, či feed funguje"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed ďalšia neprečítaná položka"
@@ -335,6 +347,14 @@ msgstr "URL informačného kanála"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr ""
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Najnovšie ako prvé"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Ďalej"
@@ -635,6 +656,10 @@ msgstr "Heslá sa nezhodujú"
msgid "Position"
msgstr "Pozícia"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -656,6 +681,7 @@ msgstr "V tejto inštancii CommaFeed sú registrácie uzavreté"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Zdieľať"
msgid "Sharing sites"
msgstr "Zdieľanie stránok"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Zobraziť kanály a kategórie bez neprečítaných záznamov"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Prepne stav čítania aktuálneho záznamu"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Vyskúšajte CommaFeed s demo účtom: demo/demo"

View File

@@ -71,6 +71,10 @@ msgstr ""
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "En opml-fil är en XML-fil som innehåller feed-URL:er och kategorier. "
msgid "Analyze feed"
msgstr "Analysera foder"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "API-nyckel"
@@ -171,6 +179,10 @@ msgstr "Kontrollera att matningen fungerar"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed nästa olästa objekt"
@@ -335,6 +347,14 @@ msgstr "Flödes-URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil krävs"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Nyast först"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Nästa"
@@ -635,6 +656,10 @@ msgstr "Lösenorden matchar inte"
msgid "Position"
msgstr ""
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -656,6 +681,7 @@ msgstr "Registreringar är stängda på denna CommaFeed-instans"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "Dela"
msgid "Sharing sites"
msgstr "Delningssajter"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "Visa flöden och kategorier utan olästa poster"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "Växla lässtatus för aktuell post"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed med demokontot: demo/demo"

View File

@@ -15,15 +15,15 @@ msgstr ""
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
msgstr "{0} ({1} içinde)"
#: src/pages/app/AboutPage.tsx
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
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
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
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
msgid "<0>Need an account?</0><1>Sign up!</1>"
@@ -71,6 +71,10 @@ msgstr "Yönetici"
msgid "All"
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
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. "
@@ -83,6 +87,10 @@ msgstr "Bir opml dosyası, besleme URL'lerini ve kategorilerini içeren bir XML
msgid "Analyze feed"
msgstr "Feed'i analiz et"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr "Duyuru"
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "API anahtarı"
@@ -133,7 +141,7 @@ msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Browser extention"
msgstr ""
msgstr "Tarayıcı eklentisi"
#: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx
@@ -169,6 +177,10 @@ 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/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
@@ -177,7 +189,7 @@ msgstr "CommaFeed sonraki okunmamış öğe"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})."
msgstr ""
msgstr "CommaFeed sürüm {version} ({revision})."
#: src/components/header/ProfileMenu.tsx
msgid "Compact"
@@ -205,7 +217,7 @@ msgstr "Etiket oluştur: {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr ""
msgstr "Ctrl"
#: src/components/settings/ProfileSettings.tsx
msgid "Current password"
@@ -213,15 +225,15 @@ msgstr "Geçerli şifre"
#: src/pages/app/SettingsPage.tsx
msgid "Custom code"
msgstr ""
msgstr "Özel kod"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Custom CSS rules that will be applied"
msgstr ""
msgstr "Uygulanacak özel CSS kuralları"
#: src/components/settings/CustomCodeSettings.tsx
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
msgid "Date created"
@@ -319,7 +331,7 @@ msgstr "Aboneliklerinizi ve kategorilerinizi diğer besleme okuma hizmetlerinde
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
msgstr "Eklenti ayarları"
#: src/components/content/add/Subscribe.tsx
msgid "Feed name"
@@ -333,6 +345,14 @@ msgstr "Feed URL'si"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr "Tüm feed'lerimi şimdi çek"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
@@ -365,7 +385,7 @@ msgstr "Oluşturulan besleme url'si"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
msgstr "{0}'a git"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Go to the All view"
@@ -459,7 +479,7 @@ msgstr "Çıkış"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
msgstr "Uzun bas"
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
@@ -491,7 +511,7 @@ msgstr "Metrikler"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
msgstr "Orta tuş ile tıkla"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "Önce en yenisi"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Sonraki"
@@ -555,7 +576,7 @@ msgstr "Hata!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
msgstr "CommaFeed'i aç"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab"
@@ -571,11 +592,11 @@ msgstr "Bağlantıyı aç"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
msgstr "Bağlantıyı arkaplanda yeni sekmede aç"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
msgstr "Bağlantıyı yeni sekmede aç"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
@@ -591,7 +612,7 @@ msgstr "Geçerli girişi aç/kapat"
#: src/pages/app/AddPage.tsx
msgid "OPML"
msgstr ""
msgstr "OPML"
#: src/components/settings/ProfileSettings.tsx
msgid "OPML export"
@@ -635,6 +656,10 @@ msgstr "Parolalar eşleşmiyor"
msgid "Position"
msgstr "Konum"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr "Önceki"
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -654,11 +679,12 @@ msgstr "Bu CommaFeed örneğinde kayıtlar kapalı"
#: src/pages/app/AboutPage.tsx
msgid "REST API"
msgstr ""
msgstr "REST API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
msgstr "Sağ tık"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
@@ -707,18 +733,27 @@ msgstr "Paylaş"
msgid "Sharing sites"
msgstr "Siteleri paylaşma"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
msgstr "Giriş menüsünü göster (masaüstü)"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
msgstr "Giriş menüsünü göster (mobil)"
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
@@ -728,6 +763,10 @@ msgstr "Okunmamış girişi olmayan beslemeleri ve kategorileri göster"
msgid "Show keyboard shortcut help"
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/WelcomePage.tsx
@@ -773,7 +812,7 @@ msgstr "Başarı"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
msgstr "Başlığı sağa kaydır"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
@@ -783,7 +822,7 @@ msgstr "Karanlık temaya geç"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme"
msgstr "Açık temaya geç"
msgstr "Aydınlık temaya geç"
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
@@ -803,6 +842,10 @@ 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/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
@@ -811,11 +854,11 @@ msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo"
#: src/pages/WelcomePage.tsx
msgid "Try the demo!"
msgstr ""
msgstr "Demo'yu deneyin!"
#: src/components/header/Header.tsx
msgid "Unread"
msgstr "Okunmadı"
msgstr "Okunmamış"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
@@ -846,8 +889,8 @@ msgstr "Web sitesi"
#: 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?"
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
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"
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
msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "如果此地址已注册,则已发送电子邮件。"
@@ -83,6 +87,10 @@ msgstr "opml 文件是包含提要 URL 和类别的 XML 文件。"
msgid "Analyze feed"
msgstr "分析饲料"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
msgstr "API 密钥"
@@ -171,6 +179,10 @@ msgstr "检查提要是否正常工作"
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed 下一个未读项目"
@@ -335,6 +347,14 @@ msgstr "供稿网址"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "文件是必需的"
@@ -526,6 +546,7 @@ msgid "Newest first"
msgstr "最新优先"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "下一个"
@@ -635,6 +656,10 @@ msgstr "密码不匹配"
msgid "Position"
msgstr "位置"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "配置文件"
@@ -656,6 +681,7 @@ msgstr "此 CommaFeed 实例上的注册已关闭"
msgid "REST API"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
@@ -707,11 +733,20 @@ msgstr "分享"
msgid "Sharing sites"
msgstr "共享站点"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift"
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
msgid "Show entry menu (desktop)"
msgstr ""
@@ -728,6 +763,10 @@ msgstr "显示没有未读条目的提要和类别"
msgid "Show keyboard shortcut help"
msgstr "显示键盘快捷键帮助"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx
@@ -805,6 +844,10 @@ msgstr "切换当前条目的读取状态"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "使用演示帐户试用 CommaFeeddemo/demo"

View File

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

View File

@@ -14,12 +14,15 @@ const shownMeters: { [key: string]: string } = {
"com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed update rate",
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit": "Entry cache hit rate",
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate",
"com.commafeed.backend.service.DatabaseCleaningService.entriesDeleted": "Entries deleted",
}
const shownGauges: { [key: string]: string } = {
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Queue size",
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Worker 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() {

View File

@@ -13,17 +13,18 @@ import {
Title,
useMantineTheme,
} from "@mantine/core"
import { useMediaQuery, useViewportSize } from "@mantine/hooks"
import { Constants } from "app/constants"
import { redirectToAdd, redirectToRootCategory } from "app/slices/redirect"
import { reloadTree, setMobileMenuOpen, setSidebarWidth } from "app/slices/tree"
import { reloadProfile, reloadSettings, reloadTags } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store"
import { AnnouncementDialog } from "components/AnnouncementDialog"
import { Loader } from "components/Loader"
import { Logo } from "components/Logo"
import { OnDesktop } from "components/responsive/OnDesktop"
import { OnMobile } from "components/responsive/OnMobile"
import { useAppLoading } from "hooks/useAppLoading"
import { useMobile } from "hooks/useMobile"
import { useWebSocket } from "hooks/useWebSocket"
import { LoadingPage } from "pages/LoadingPage"
import { Resizable } from "re-resizable"
@@ -90,10 +91,10 @@ function LogoAndTitle() {
export default function Layout(props: LayoutProps) {
const { classes } = useStyles(props)
const theme = useMantineTheme()
const viewport = useViewportSize()
const { loading } = useAppLoading()
const mobile = !useMediaQuery(`(min-width: ${Constants.layout.mobileBreakpoint})`)
const mobile = useMobile()
const mobileMenuOpen = useAppSelector(state => state.tree.mobileMenuOpen)
const webSocketConnected = useAppSelector(state => state.server.webSocketConnected)
const sidebarHidden = props.sidebarWidth === 0
const dispatch = useAppDispatch()
useWebSocket()
@@ -101,16 +102,21 @@ export default function Layout(props: LayoutProps) {
const handleResize = (element: HTMLElement) => dispatch(setSidebarWidth(element.offsetWidth))
useEffect(() => {
// load initial data
dispatch(reloadSettings())
dispatch(reloadProfile())
dispatch(reloadTree())
dispatch(reloadTags())
// reload tree periodically
const id = setInterval(() => dispatch(reloadTree()), 30000)
return () => clearInterval(id)
}, [dispatch])
useEffect(() => {
// reload tree periodically if not receiving websocket events
const timer = setInterval(() => {
if (!webSocketConnected) dispatch(reloadTree())
}, 30000)
return () => clearInterval(timer)
}, [dispatch, webSocketConnected])
const burger = (
<Center>
<Burger
@@ -177,7 +183,7 @@ export default function Layout(props: LayoutProps) {
)}
{!mobileMenuOpen && (
<Group>
<Box mr="sm">{burger}</Box>
<Box>{burger}</Box>
<Box sx={{ flexGrow: 1 }}>{props.header}</Box>
</Group>
)}
@@ -196,18 +202,12 @@ export default function Layout(props: LayoutProps) {
</Header>
}
>
<ScrollArea
sx={{ height: viewport.height - Constants.layout.headerHeight }}
viewportRef={ref => {
if (ref) ref.id = Constants.dom.mainScrollAreaId
}}
>
<Box id="content" className={classes.mainContent}>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
</Box>
</ScrollArea>
<Box id="content" className={classes.mainContent}>
<Suspense fallback={<Loader />}>
<AnnouncementDialog />
<Outlet />
</Suspense>
</Box>
</AppShell>
)
}

View File

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

View File

@@ -1,13 +1,41 @@
import { lingui } from "@lingui/vite-plugin"
import react from "@vitejs/plugin-react"
import { visualizer } from "rollup-plugin-visualizer"
import { defineConfig } from "vite"
import { defineConfig, PluginOption } from "vite"
import eslint from "vite-plugin-eslint"
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/
export default defineConfig({
plugins: [
customCodeInjector,
react({
babel: {
// babel-macro is needed for lingui
@@ -32,7 +60,7 @@ export default defineConfig({
},
},
build: {
chunkSizeWarningLimit: 1000,
chunkSizeWarningLimit: 3000,
rollupOptions: {
output: {
manualChunks: id => {

View File

@@ -27,7 +27,10 @@ app:
# number of database updating threads
databaseUpdateThreads: 1
# rows to delete per query while cleaning up old entries
databaseCleanupBatchSize: 100
# settings for sending emails (password recovery)
smtpHost: localhost
smtpPort: 25
@@ -81,8 +84,12 @@ app:
# 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
# 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
#
# for PostgreSQL
@@ -117,7 +124,6 @@ logging:
liquibase: INFO
org.hibernate.SQL: INFO # or ALL for sql debugging
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
org.hibernate.orm.deprecation: "OFF"
appenders:
- type: console
- type: file

View File

@@ -6,28 +6,31 @@ app:
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
hideFromWebCrawlers: true
# whether to allow user registrations
allowRegistrations: false
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
strictPasswordPolicy: true
# create a demo account the first time the app starts
createDemoAccount: false
# put your google analytics tracking code here
googleAnalyticsTrackingCode:
# put your google server key (used for youtube favicon fetching)
googleAuthKey:
# number of http threads
backgroundThreads: 3
# number of database updating threads
databaseUpdateThreads: 1
# rows to delete per query while cleaning up old entries
databaseCleanupBatchSize: 100
# settings for sending emails (password recovery)
smtpHost:
smtpPort:
@@ -43,28 +46,28 @@ app:
graphiteHost: "localhost"
graphitePort: 2003
graphiteInterval: 60
# whether this commafeed instance has a lot of feeds to refresh
# leave this to false in almost all cases
heavyLoad: false
# minimum amount of time commafeed will wait before refreshing the same feed
refreshIntervalMinutes: 5
# whether to enable pubsub
# probably not needed if refreshIntervalMinutes is low
pubsubhubbub: false
# 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
# database query timeout (in milliseconds), 0 to disable
queryTimeout: 0
# time to keep unread statuses (in days), 0 to disable
keepStatusDays: 0
# entries to keep per feed, old entries will be deleted, 0 to disable
maxFeedCapacity: 500
@@ -73,17 +76,21 @@ app:
# cache service to use, possible values are 'noop' and 'redis'
cache: noop
# announcement string displayed on the main page
announcement:
# user-agent string that will be used by the http client, leave empty for the default one
userAgent:
# 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
# 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
#
# for PostgreSQL
@@ -96,7 +103,7 @@ app:
database:
driverClass: org.h2.Driver
url: jdbc:h2:./db/commafeed
url: jdbc:h2:/commafeed/data/db
user: sa
password: sa
properties:
@@ -105,7 +112,7 @@ database:
minSize: 1
maxSize: 50
maxConnectionAge: 30m
server:
applicationConnectors:
- type: http
@@ -115,14 +122,13 @@ server:
port: 8084
requestLog:
appenders: [ ]
logging:
level: ERROR
loggers:
com.commafeed: INFO
liquibase: INFO
io.dropwizard.server.ServerFactory: INFO
org.hibernate.orm.deprecation: "OFF"
appenders:
- type: console
- type: file
@@ -132,7 +138,7 @@ logging:
archivedLogFilenamePattern: log/commafeed-%d.log
archivedFileCount: 5
timeZone: UTC
# Redis pool configuration
# (only used if app.cache is 'redis')
# -----------------------------------
@@ -141,8 +147,7 @@ redis:
port: 6379
# username is only required when using ACLs
username:
password:
password:
timeout: 2000
database: 0
maxTotal: 500

View File

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

View File

@@ -42,6 +42,7 @@ import com.commafeed.frontend.resource.FeedREST;
import com.commafeed.frontend.resource.PubSubHubbubCallbackREST;
import com.commafeed.frontend.resource.ServerREST;
import com.commafeed.frontend.resource.UserREST;
import com.commafeed.frontend.resource.fever.FeverREST;
import com.commafeed.frontend.servlet.AnalyticsServlet;
import com.commafeed.frontend.servlet.CustomCssServlet;
import com.commafeed.frontend.servlet.CustomJsServlet;
@@ -147,7 +148,7 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics()));
// session management
environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build());
environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build(config.getDataSourceFactory()));
// support for "@SecurityCheck User user" injection
environment.jersey().register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserService.class)));
@@ -163,6 +164,7 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
environment.jersey().register(injector.getInstance(PubSubHubbubCallbackREST.class));
environment.jersey().register(injector.getInstance(ServerREST.class));
environment.jersey().register(injector.getInstance(UserREST.class));
environment.jersey().register(injector.getInstance(FeverREST.class));
// Servlets
environment.servlets().addServlet("next", injector.getInstance(NextUnreadServlet.class)).addMapping("/next");

View File

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

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();
}
public int deleteWithoutEntries(int max) {
public long deleteWithoutEntries(int max) {
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();
delete(list);
return deleted;
return deleteQuery(content).where(content.id.in(ids)).execute();
}
}

View File

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

View File

@@ -117,7 +117,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
}
private JPAQuery<FeedEntry> buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List<FeedEntryKeyword> keywords,
Date newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag) {
Date newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag, Long minEntryId, Long maxEntryId) {
JPAQuery<FeedEntry> query = query().selectFrom(entry).where(entry.feed.eq(sub.getFeed()));
@@ -159,6 +159,14 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
query.where(entry.inserted.goe(newerThan));
}
if (minEntryId != null) {
query.where(entry.id.gt(minEntryId));
}
if (maxEntryId != null) {
query.where(entry.id.lt(maxEntryId));
}
if (last != null) {
if (order == ReadingOrder.desc) {
query.where(entry.updated.gt(last.getEntryUpdated()));
@@ -189,7 +197,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly,
List<FeedEntryKeyword> keywords, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
boolean onlyIds, String tag) {
boolean onlyIds, String tag, Long minEntryId, Long maxEntryId) {
int capacity = offset + limit;
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
@@ -197,7 +205,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<>(capacity, comparator);
for (FeedSubscription sub : subs) {
FeedEntryStatus last = (order != null && set.isFull()) ? set.last() : null;
JPAQuery<FeedEntry> query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag);
JPAQuery<FeedEntry> query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag, minEntryId,
maxEntryId);
List<Tuple> tuples = query.select(entry.id, entry.updated, status.id, entry.content.title).fetch();
for (Tuple tuple : tuples) {
@@ -250,7 +259,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
public UnreadCount getUnreadCount(User user, FeedSubscription subscription) {
UnreadCount uc = null;
JPAQuery<FeedEntry> query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null);
JPAQuery<FeedEntry> query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null, null, null);
List<Tuple> tuples = query.select(entry.count(), entry.updated.max()).fetch();
for (Tuple tuple : tuples) {
Long count = tuple.get(entry.count());
@@ -270,8 +279,13 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
return results;
}
public List<FeedEntryStatus> getOldStatuses(Date olderThan, int limit) {
return query().selectFrom(status).where(status.entryInserted.lt(olderThan), status.starred.isFalse()).limit(limit).fetch();
public long deleteOldStatuses(Date olderThan, int limit) {
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.querydsl.core.types.EntityPath;
import com.querydsl.jpa.impl.JPADeleteClause;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
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);
}
protected JPADeleteClause deleteQuery(EntityPath<T> entityPath) {
return new JPADeleteClause(currentSession(), entityPath);
}
public void saveOrUpdate(T model) {
persist(model);
}

View File

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

View File

@@ -1,11 +1,8 @@
package com.commafeed.backend.feed;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
@@ -22,16 +19,10 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
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.jsoup.select.Elements;
import org.netpreserve.urlcanon.Canonicalizer;
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.model.FeedEntry;
@@ -41,7 +32,6 @@ import com.google.gwt.i18n.client.HasDirection.Direction;
import com.google.gwt.i18n.shared.BidiUtils;
import com.ibm.icu.text.CharsetDetector;
import com.ibm.icu.text.CharsetMatch;
import com.steadystate.css.parser.CSSOMParser;
import lombok.extern.slf4j.Slf4j;
@@ -54,12 +44,6 @@ public class FeedUtils {
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) {
if (string != null) {
string = string.substring(0, Math.min(length, string.length()));
@@ -67,40 +51,6 @@ public class FeedUtils {
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
* feed
@@ -233,87 +183,6 @@ public class FeedUtils {
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) {
String text = entry.getContent().getContent();

View File

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

View File

@@ -6,6 +6,9 @@ import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryContentDAO;
import com.commafeed.backend.dao.FeedEntryDAO;
@@ -14,7 +17,6 @@ import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.Feed;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
@@ -22,29 +24,42 @@ import lombok.extern.slf4j.Slf4j;
*
*/
@Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class DatabaseCleaningService {
private static final int BATCH_SIZE = 100;
private final int batchSize;
private final UnitOfWork unitOfWork;
private final FeedDAO feedDAO;
private final FeedEntryDAO feedEntryDAO;
private final FeedEntryContentDAO feedEntryContentDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final Meter entriesDeletedMeter;
public long cleanFeedsWithoutSubscriptions() {
@Inject
public DatabaseCleaningService(CommaFeedConfiguration config, UnitOfWork unitOfWork, FeedDAO feedDAO, FeedEntryDAO feedEntryDAO,
FeedEntryContentDAO feedEntryContentDAO, FeedEntryStatusDAO feedEntryStatusDAO, MetricRegistry metrics) {
this.unitOfWork = unitOfWork;
this.feedDAO = feedDAO;
this.feedEntryDAO = feedEntryDAO;
this.feedEntryContentDAO = feedEntryContentDAO;
this.feedEntryStatusDAO = feedEntryStatusDAO;
this.batchSize = config.getApplicationSettings().getDatabaseCleanupBatchSize();
this.entriesDeletedMeter = metrics.meter(MetricRegistry.name(getClass(), "entriesDeleted"));
}
public void cleanFeedsWithoutSubscriptions() {
log.info("cleaning feeds without subscriptions");
long total = 0;
int deleted = 0;
int deleted;
long entriesTotal = 0;
do {
List<Feed> feeds = unitOfWork.call(() -> feedDAO.findWithoutSubscriptions(1));
for (Feed feed : feeds) {
int entriesDeleted = 0;
long entriesDeleted;
do {
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), BATCH_SIZE));
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize));
entriesDeletedMeter.mark(entriesDeleted);
entriesTotal += entriesDeleted;
log.info("removed {} entries for feeds without subscriptions", entriesTotal);
} while (entriesDeleted > 0);
@@ -54,26 +69,24 @@ public class DatabaseCleaningService {
log.info("removed {} feeds without subscriptions", total);
} while (deleted != 0);
log.info("cleanup done: {} feeds without subscriptions deleted", total);
return total;
}
public long cleanContentsWithoutEntries() {
public void cleanContentsWithoutEntries() {
log.info("cleaning contents without entries");
long total = 0;
int deleted = 0;
long deleted;
do {
deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE));
deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(batchSize));
total += deleted;
log.info("removed {} contents without entries", total);
} while (deleted != 0);
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;
while (true) {
List<FeedCapacity> feeds = unitOfWork.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, BATCH_SIZE));
List<FeedCapacity> feeds = unitOfWork.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize));
if (feeds.isEmpty()) {
break;
}
@@ -82,7 +95,8 @@ public class DatabaseCleaningService {
long remaining = feed.getCapacity() - maxFeedCapacity;
do {
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)));
entriesDeletedMeter.mark(deleted);
total += deleted;
remaining -= deleted;
log.info("removed {} entries for feeds exceeding capacity", total);
@@ -90,19 +104,17 @@ public class DatabaseCleaningService {
}
}
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");
long total = 0;
int deleted = 0;
long deleted;
do {
deleted = unitOfWork.call(() -> feedEntryStatusDAO.delete(feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE)));
deleted = unitOfWork.call(() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize));
total += deleted;
log.info("removed {} old read statuses", total);
} while (deleted != 0);
log.info("cleanup done: {} old read statuses deleted", total);
return total;
}
}

View File

@@ -1,5 +1,8 @@
package com.commafeed.backend.service;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@@ -8,27 +11,47 @@ import javax.inject.Singleton;
import org.apache.commons.codec.digest.DigestUtils;
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.feed.FeedUtils;
import com.commafeed.backend.model.FeedEntryContent;
import com.steadystate.css.parser.CSSOMParser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Slf4j
@Singleton
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;
/**
* this is NOT thread-safe
*/
public FeedEntryContent findOrCreate(FeedEntryContent content, String baseUrl) {
content.setAuthor(FeedUtils.truncate(FeedUtils.handleContent(content.getAuthor(), baseUrl, true), 128));
content.setTitle(FeedUtils.truncate(FeedUtils.handleContent(content.getTitle(), baseUrl, true), 2048));
content.setContent(FeedUtils.handleContent(content.getContent(), baseUrl, false));
content.setMediaDescription(FeedUtils.handleContent(content.getMediaDescription(), baseUrl, false));
content.setAuthor(FeedUtils.truncate(handleContent(content.getAuthor(), baseUrl, true), 128));
content.setTitle(FeedUtils.truncate(handleContent(content.getTitle(), baseUrl, true), 2048));
content.setContent(handleContent(content.getContent(), baseUrl, false));
content.setMediaDescription(handleContent(content.getMediaDescription(), baseUrl, false));
String contentHash = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getContent()));
content.setContentHash(contentHash);
@@ -37,7 +60,7 @@ public class FeedEntryContentService {
content.setTitleHash(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()) {
return equivalentContent.get();
}
@@ -45,4 +68,140 @@ public class FeedEntryContentService {
feedEntryContentDAO.saveOrUpdate(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

@@ -111,7 +111,7 @@ public class FeedEntryService {
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan, List<FeedEntryKeyword> keywords) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null,
false, false, null);
false, false, null, null, null);
markList(statuses, olderThan);
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(user);

View File

@@ -82,6 +82,28 @@ public class UserService {
return Optional.empty();
}
/**
* try to log in with given fever api key
*/
public Optional<User> login(long userId, String feverApiKey) {
if (feverApiKey == null) {
return Optional.empty();
}
User user = userDAO.findById(userId);
if (user == null || user.isDisabled() || user.getApiKey() == null) {
return Optional.empty();
}
String computedFeverApiKey = DigestUtils.md5Hex(user.getName() + ":" + user.getApiKey());
if (!computedFeverApiKey.equals(feverApiKey)) {
return Optional.empty();
}
performPostLoginActivities(user);
return Optional.of(user);
}
/**
* should triggers after successful login
*/

View File

@@ -22,7 +22,7 @@ public class Entries implements Serializable {
@ApiModelProperty(value = "times the server tried to refresh the feed and failed", required = true)
private int errorCount;
@ApiModelProperty(value = "URL of the website, extracted from the feed", required = true)
@ApiModelProperty(value = "URL of the website, extracted from the feed, only filled if querying for feed entries, not category entries")
private String feedLink;
@ApiModelProperty(value = "list generation timestamp", required = true)

View File

@@ -35,6 +35,15 @@ public class Settings implements Serializable {
@ApiModelProperty(value = "user's preferred scroll speed when navigating between entries", required = true)
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)
private SharingSettings sharingSettings = new SharingSettings();

View File

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

View File

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

View File

@@ -141,7 +141,7 @@ public class CategoryREST {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
removeExcludedSubscriptions(subs, excludedIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
offset, limit + 1, order, true, onlyIds, tag);
offset, limit + 1, order, true, onlyIds, tag, null, null);
for (FeedEntryStatus status : list) {
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
@@ -160,7 +160,7 @@ public class CategoryREST {
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(user, categories);
removeExcludedSubscriptions(subs, excludedIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
offset, limit + 1, order, true, onlyIds, tag);
offset, limit + 1, order, true, onlyIds, tag, null, null);
for (FeedEntryStatus status : list) {
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));

View File

@@ -172,7 +172,7 @@ public class FeedREST {
entries.setFeedLink(subscription.getFeed().getLink());
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, Collections.singletonList(subscription), unreadOnly,
entryKeywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null);
entryKeywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null, null, null);
for (FeedEntryStatus status : list) {
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
@@ -379,7 +379,7 @@ public class FeedREST {
@POST
@Path("/subscribe")
@UnitOfWork
@ApiOperation(value = "Subscribe to a feed", notes = "Subscribe to a feed")
@ApiOperation(value = "Subscribe to a feed", notes = "Subscribe to a feed", response = Long.class)
@Timed
public Response subscribe(@ApiParam(hidden = true) @SecurityCheck User user,
@Valid @ApiParam(value = "subscription request", required = true) SubscribeRequest req) {

View File

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

View File

@@ -0,0 +1,320 @@
package com.commafeed.frontend.resource.fever;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriInfo;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import com.codahale.metrics.annotation.Timed;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.favicon.AbstractFaviconFetcher.Favicon;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedService;
import com.commafeed.backend.service.UserService;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverFavicon;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverFeed;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverFeedGroup;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverGroup;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverItem;
import io.dropwizard.hibernate.UnitOfWork;
import lombok.RequiredArgsConstructor;
/**
* Fever-compatible API
*
* <ul>
* <li>url: /rest/fever/user/${userId}</li>
* <li>login: username</li>
* <li>password: api key</li>
* </ul>
*
* See https://feedafever.com/api
*/
@Path("/fever")
@Produces(MediaType.APPLICATION_JSON)
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class FeverREST {
private static final String PATH = "/user/{userId}{optionalTrailingFever : (/fever)?}{optionalTrailingSlash : (/)?}";
private static final int UNREAD_ITEM_IDS_BATCH_SIZE = 1000;
private static final int SAVED_ITEM_IDS_BATCH_SIZE = 1000;
private static final int ITEMS_BATCH_SIZE = 200;
private final UserService userService;
private final FeedEntryService feedEntryService;
private final FeedService feedService;
private final FeedEntryDAO feedEntryDAO;
private final FeedSubscriptionDAO feedSubscriptionDAO;
private final FeedCategoryDAO feedCategoryDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
// expected Fever API
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Path(PATH)
@POST
@UnitOfWork
@Timed
public FeverResponse formUrlencoded(@Context UriInfo uri, @PathParam("userId") Long userId, MultivaluedMap<String, String> form) {
Map<String, String> params = new HashMap<>();
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
form.forEach((k, v) -> params.put(k, v.get(0)));
return handle(userId, params);
}
// workaround for some readers that post data without any media type, and all params in the url
// e.g. FeedMe
@Path(PATH)
@POST
@UnitOfWork
@Timed
public FeverResponse noForm(@Context UriInfo uri, @PathParam("userId") Long userId) {
Map<String, String> params = new HashMap<>();
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
return handle(userId, params);
}
// workaround for some readers that post data using MultiPart FormData instead of the classic POST
// e.g. Raven Reader
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Path(PATH)
@POST
@UnitOfWork
@Timed
public FeverResponse formData(@Context UriInfo uri, @PathParam("userId") Long userId, FormDataMultiPart form) {
Map<String, String> params = new HashMap<>();
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
form.getFields().forEach((k, v) -> params.put(k, v.get(0).getValue()));
return handle(userId, params);
}
public FeverResponse handle(long userId, Map<String, String> params) {
User user = auth(userId, params.get("api_key")).orElse(null);
if (user == null) {
FeverResponse resp = new FeverResponse();
resp.setAuth(false);
return resp;
}
FeverResponse resp = new FeverResponse();
resp.setAuth(true);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
resp.setLastRefreshedOnTime(buildLastRefreshedOnTime(subscriptions));
if (params.containsKey("groups") || params.containsKey("feeds")) {
resp.setFeedsGroups(buildFeedsGroups(subscriptions));
if (params.containsKey("groups")) {
List<FeedCategory> categories = feedCategoryDAO.findAll(user);
resp.setGroups(buildGroups(categories));
}
if (params.containsKey("feeds")) {
resp.setFeeds(buildFeeds(subscriptions));
}
}
if (params.containsKey("unread_item_ids")) {
resp.setUnreadItemIds(buildUnreadItemIds(user, subscriptions));
}
if (params.containsKey("saved_item_ids")) {
resp.setSavedItemIds(buildSavedItemIds(user));
}
if (params.containsKey("items")) {
if (params.containsKey("with_ids")) {
String withIds = params.get("with_ids");
List<String> entryIds = Stream.of(withIds.split(",")).map(String::trim).collect(Collectors.toList());
resp.setItems(buildItems(user, subscriptions, entryIds));
} else {
Long sinceId = params.containsKey("since_id") ? Long.valueOf(params.get("since_id")) : null;
Long maxId = params.containsKey("max_id") ? Long.valueOf(params.get("max_id")) : null;
resp.setItems(buildItems(user, subscriptions, sinceId, maxId));
}
}
if (params.containsKey("favicons")) {
resp.setFavicons(buildFavicons(subscriptions));
}
if (params.containsKey("links")) {
resp.setLinks(Collections.emptyList());
}
if (params.containsKey("mark") && params.containsKey("id") && params.containsKey("as")) {
long id = Long.parseLong(params.get("id"));
String before = params.get("before");
Date olderThan = before == null ? null : Date.from(Instant.ofEpochSecond(Long.parseLong(before)));
mark(user, params.get("mark"), id, params.get("as"), olderThan);
}
return resp;
}
private Optional<User> auth(Long userId, String feverApiKey) {
return userService.login(userId, feverApiKey);
}
private long buildLastRefreshedOnTime(List<FeedSubscription> subscriptions) {
return subscriptions.stream()
.map(FeedSubscription::getFeed)
.map(Feed::getLastUpdated)
.filter(Objects::nonNull)
.max(Comparator.naturalOrder())
.map(d -> d.toInstant().getEpochSecond())
.orElse(0L);
}
private List<FeverFeedGroup> buildFeedsGroups(List<FeedSubscription> subscriptions) {
return subscriptions.stream()
.collect(Collectors.groupingBy(s -> s.getCategory() == null ? 0 : s.getCategory().getId()))
.entrySet()
.stream()
.map(e -> {
FeverFeedGroup fg = new FeverFeedGroup();
fg.setGroupId(e.getKey());
fg.setFeedIds(e.getValue().stream().map(FeedSubscription::getId).collect(Collectors.toList()));
return fg;
})
.collect(Collectors.toList());
}
private List<FeverGroup> buildGroups(List<FeedCategory> categories) {
return categories.stream().map(c -> {
FeverGroup g = new FeverGroup();
g.setId(c.getId());
g.setTitle(c.getName());
return g;
}).collect(Collectors.toList());
}
private List<FeverFeed> buildFeeds(List<FeedSubscription> subscriptions) {
return subscriptions.stream().map(s -> {
FeverFeed f = new FeverFeed();
f.setId(s.getId());
f.setFaviconId(s.getId());
f.setTitle(s.getTitle());
f.setUrl(s.getFeed().getUrl());
f.setSiteUrl(s.getFeed().getLink());
f.setSpark(false);
f.setLastUpdatedOnTime(s.getFeed().getLastUpdated() == null ? 0 : s.getFeed().getLastUpdated().toInstant().getEpochSecond());
return f;
}).collect(Collectors.toList());
}
private List<Long> buildUnreadItemIds(User user, List<FeedSubscription> subscriptions) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0,
UNREAD_ITEM_IDS_BATCH_SIZE, ReadingOrder.desc, false, true, null, null, null);
return statuses.stream().map(s -> s.getEntry().getId()).collect(Collectors.toList());
}
private List<Long> buildSavedItemIds(User user) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findStarred(user, null, 0, SAVED_ITEM_IDS_BATCH_SIZE, ReadingOrder.desc, false);
return statuses.stream().map(s -> s.getEntry().getId()).collect(Collectors.toList());
}
private List<FeverItem> buildItems(User user, List<FeedSubscription> subscriptions, List<String> entryIds) {
List<FeverItem> items = new ArrayList<>();
Map<Long, FeedSubscription> subscriptionsByFeedId = subscriptions.stream()
.collect(Collectors.toMap(s -> s.getFeed().getId(), s -> s));
for (String entryId : entryIds) {
FeedEntry entry = feedEntryDAO.findById(Long.parseLong(entryId));
FeedSubscription sub = subscriptionsByFeedId.get(entry.getFeed().getId());
if (sub != null) {
FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
items.add(mapStatus(status));
}
}
return items;
}
private List<FeverItem> buildItems(User user, List<FeedSubscription> subscriptions, Long sinceId, Long maxId) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, false, null, null, 0, ITEMS_BATCH_SIZE,
ReadingOrder.desc, false, false, null, sinceId, maxId);
return statuses.stream().map(this::mapStatus).collect(Collectors.toList());
}
private FeverItem mapStatus(FeedEntryStatus s) {
FeverItem i = new FeverItem();
i.setId(s.getEntry().getId());
i.setFeedId(s.getSubscription().getId());
i.setTitle(s.getEntry().getContent().getTitle());
i.setAuthor(s.getEntry().getContent().getAuthor());
i.setHtml(Optional.ofNullable(s.getEntry().getContent().getContent()).orElse(""));
i.setUrl(s.getEntry().getUrl());
i.setSaved(s.isStarred());
i.setRead(s.isRead());
i.setCreatedOnTime(s.getEntryUpdated().toInstant().getEpochSecond());
return i;
}
private List<FeverFavicon> buildFavicons(List<FeedSubscription> subscriptions) {
return subscriptions.stream().map(s -> {
Favicon favicon = feedService.fetchFavicon(s.getFeed());
FeverFavicon f = new FeverFavicon();
f.setId(s.getFeed().getId());
f.setData(String.format("data:%s;base64,%s", favicon.getMediaType(), Base64.getEncoder().encodeToString(favicon.getIcon())));
return f;
}).collect(Collectors.toList());
}
private void mark(User user, String source, long id, String action, Date olderThan) {
if ("item".equals(source)) {
if ("read".equals(action) || "unread".equals(action)) {
feedEntryService.markEntry(user, id, "read".equals(action));
} else if ("saved".equals(action) || "unsaved".equals(action)) {
FeedEntry entry = feedEntryDAO.findById(id);
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
feedEntryService.starEntry(user, id, sub.getId(), "saved".equals(action));
}
} else if ("feed".equals(source)) {
FeedSubscription subscription = feedSubscriptionDAO.findById(user, id);
feedEntryService.markSubscriptionEntries(user, Collections.singletonList(subscription), olderThan, null);
} else if ("group".equals(source)) {
FeedCategory parent = feedCategoryDAO.findById(user, id);
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, parent);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, categories);
feedEntryService.markSubscriptionEntries(user, subscriptions, olderThan, null);
}
}
}

View File

@@ -0,0 +1,164 @@
package com.commafeed.frontend.resource.fever;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
@JsonInclude(Include.NON_NULL)
@Data
public class FeverResponse {
@JsonProperty("api_version")
private int apiVersion = 3;
@JsonProperty("auth")
@JsonFormat(shape = Shape.NUMBER)
private boolean auth;
@JsonProperty("last_refreshed_on_time")
private long lastRefreshedOnTime;
@JsonProperty("groups")
private List<FeverGroup> groups;
@JsonProperty("feeds")
private List<FeverFeed> feeds;
@JsonProperty("feeds_groups")
private List<FeverFeedGroup> feedsGroups;
@JsonProperty("unread_item_ids")
@JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class)
private List<Long> unreadItemIds;
@JsonProperty("saved_item_ids")
@JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class)
private List<Long> savedItemIds;
@JsonProperty("items")
private List<FeverItem> items;
@JsonProperty("favicons")
private List<FeverFavicon> favicons;
@JsonProperty("links")
private List<FeverLink> links;
@Data
public static class FeverGroup {
@JsonProperty("id")
private long id;
@JsonProperty("title")
private String title;
}
@Data
public static class FeverFeed {
@JsonProperty("id")
private long id;
@JsonProperty("favicon_id")
private long faviconId;
@JsonProperty("title")
private String title;
@JsonProperty("url")
private String url;
@JsonProperty("site_url")
private String siteUrl;
@JsonProperty("is_spark")
@JsonFormat(shape = Shape.NUMBER)
private boolean spark;
@JsonProperty("last_updated_on_time")
private long lastUpdatedOnTime;
}
@Data
public static class FeverFeedGroup {
@JsonProperty("group_id")
private long groupId;
@JsonProperty("feed_ids")
@JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class)
private List<Long> feedIds;
}
@Data
public static class FeverItem {
@JsonProperty("id")
private long id;
@JsonProperty("feed_id")
private long feedId;
@JsonProperty("title")
private String title;
@JsonProperty("author")
private String author;
@JsonProperty("html")
private String html;
@JsonProperty("url")
private String url;
@JsonProperty("is_saved")
@JsonFormat(shape = Shape.NUMBER)
private boolean saved;
@JsonProperty("is_read")
@JsonFormat(shape = Shape.NUMBER)
private boolean read;
@JsonProperty("created_on_time")
private long createdOnTime;
}
@Data
public static class FeverFavicon {
@JsonProperty("id")
private long id;
@JsonProperty("data")
private String data;
}
@Data
public static class FeverLink {
}
public static class LongListToCommaSeparatedStringSerializer extends JsonSerializer<List<Long>> {
@Override
public void serialize(List<Long> input, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
String output = input.stream().map(String::valueOf).collect(Collectors.joining(","));
jsonGenerator.writeObject(output);
}
}
}

View File

@@ -69,7 +69,7 @@ public class NextUnreadServlet extends HttpServlet {
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user.get());
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, order,
true, false, null);
true, false, null, null, null);
s = Iterables.getFirst(statuses, null);
} else {
FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId));
@@ -77,7 +77,7 @@ public class NextUnreadServlet extends HttpServlet {
List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user.get(), category);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children);
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, null, 0,
1, order, true, false, null);
1, order, true, false, null, null, null);
s = Iterables.getFirst(statuses, null);
}
}

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