Compare commits

..

71 Commits

Author SHA1 Message Date
Athou
0b16b6bb86 release 3.1.0 2023-05-01 18:25:16 +02:00
Athou
6a8f7f0a40 add release script 2023-05-01 18:23:35 +02:00
Athou
42ca0967b6 create release on tag 2023-05-01 17:39:01 +02:00
Athou
deb29f0e88 fix metrics page 2023-05-01 17:07:17 +02:00
Athou
714af986b0 readme update 2023-05-01 17:05:38 +02:00
Athou
4ff26366a5 there's no need to update disabledUntil here anymore because findNextUpdatableFeeds will always be called when the queue is empty 2023-05-01 10:04:43 +02:00
Athou
9c628a8f53 make each step of feed fetching return its own model 2023-05-01 09:58:19 +02:00
Athou
4a40f2b8f7 no need to log if we're not sending any notification 2023-04-30 23:05:48 +02:00
Athou
9a2dda626c changelog update 2023-04-30 23:05:48 +02:00
Athou
a9ff491da0 hide request log in production 2023-04-30 16:21:38 +02:00
Athou
5c5a7d20de in production, no need to see warnings 2023-04-30 16:03:47 +02:00
Athou
05ae4eb529 replace homemade threadpool framework with rxjava 2023-04-30 15:34:32 +02:00
Athou
15f93b198c remove warning 2023-04-29 09:20:34 +02:00
Athou
0a99dacb6b use urlcanon instead of crawler4j because we only used it for url canonization 2023-04-29 09:20:15 +02:00
Athou
00f6c04611 various dependency updates 2023-04-29 09:17:59 +02:00
Athou
d9b899b53f prevent entries from having the hover background color when clicked on mobile 2023-04-28 19:44:26 +02:00
Athou
d96f8da8fd remove deprecation warnings (already done in config.yml.example) 2023-04-28 19:44:26 +02:00
Athou
ababcf7850 remove unnecessary "subscriptions" field on Feed
hopefully removes the error that happens sometimes:
Illegal attempt to associate a collection with two open sessions. Collection : [com.commafeed.backend.model.Feed.subscriptions]
2023-04-28 19:44:26 +02:00
Athou
f23bfaf694 use a different color for hover than read and unread backgrounds 2023-04-28 19:44:26 +02:00
Athou
cac05dee0b store view mode in localStorage (#1051) 2023-04-27 14:42:55 +02:00
Athou
155c93d371 reorder Dockerfile to put changing layers last 2023-04-27 09:38:23 +02:00
Athou
9a61ee7530 create a 'master' docker tag for the latest master version 2023-04-27 09:38:23 +02:00
Athou
4bea1c5e5c restore hover effect from commafeed v2 2023-04-27 07:57:09 +02:00
Athou
9ccc26b0b0 tweak compact layout a little bit more 2023-04-27 07:30:43 +02:00
Athou
5cd3787d6f add i18n placeholders for new label 2023-04-26 23:37:39 +02:00
Athou
807b1f62a1 add an even more compact entry layout 2023-04-26 22:50:43 +02:00
Athou
c15db54d5a bump versions 2023-04-26 09:43:10 +02:00
Athou
aa7b078121 readme update 2023-04-25 15:34:20 +02:00
Athou
99130d0181 combine EnvironmentSubstitutor and SubstitutingSourceProvider (#1050) 2023-04-25 10:40:14 +02:00
Athou
90e2036cbe build for armv7 too 2023-04-25 10:24:22 +02:00
Athou
c2f3e42867 Readme update 2023-04-25 08:57:55 +02:00
Athou
bd33369a41 Merge branch 'develop' 2023-04-25 08:48:49 +02:00
Athou
4f625d8ed5 add docker support 2023-04-25 07:48:52 +02:00
Athou
866fe56dd2 remove warning "HV000254: Missing parameter metadata" 2023-04-25 07:48:11 +02:00
Athou
5f37dbca4c fix env variables support, now works without having to change yml file 2023-04-24 20:28:58 +02:00
Athou
c49e617dfe lombok update 2023-04-24 18:56:47 +02:00
Athou
e763ffd4cf allow access to entries in json format with api key (fixes #1049) 2023-04-22 07:27:59 +02:00
Athou
20ab7dd3e1 readd bitcoin address 2023-03-16 17:09:12 +01:00
Jérémie Panzer
55741c6332 Merge pull request #1046 from dayuer/patch-1
Update zh.js
2023-03-13 16:41:01 +01:00
dayuer
42d85336a8 Update zh.js
Added some entry translations.
2023-03-13 23:33:05 +08:00
Athou
639b82f494 add goal to start typescript in watch mode, helps during big refactorings 2023-03-10 11:37:29 +01:00
Athou
5003c176a2 display parent category name in category selects (fixes #1045) 2023-03-07 08:40:02 +01:00
Athou
10bfbbec17 restore one-click list refresh (#1040) 2023-03-06 14:08:48 +01:00
Athou
3da900db7f add missing parenthesis in feed entry filtering example 2023-03-01 13:50:07 +01:00
Athou
9f421ec3b0 add context menu to navigate to feed if a category is displayed 2023-02-27 19:56:32 +01:00
Athou
69fb11eee0 add context menu item to open link in new tab 2023-02-27 07:41:13 +01:00
Athou
ffbb85df43 add context menu on entry headers 2023-02-24 15:18:04 +01:00
Athou
a4e78c4e0d throttle event 2023-02-24 14:53:30 +01:00
Jérémie Panzer
274c5ae165 Merge pull request #1039 from bartschinski/patch-1
Update german translation in de.js
2023-02-10 10:35:31 +01:00
Phillip Bartschinski
39c4012a1a Update de.js
Add missing translations
2023-02-10 10:28:21 +01:00
Athou
6d4b0cbdef bump all dependencies 2023-02-04 08:34:23 +01:00
Athou
ea4b120a85 prevent full feed fetching next time we fetch it if caching header values changed but content did not (#1037) 2023-01-27 08:49:59 +01:00
Athou
5c2454c331 bring back "refresh all my feeds" (#1036) 2023-01-19 07:29:19 +01:00
Athou
4ff46965c4 add websocket support to immediately refresh tree when new entries are available 2023-01-18 20:58:45 +01:00
Athou
33e3f7ea3c feeds added manually to the queue now refresh immediately instead of waiting up to 15s (#1036) 2023-01-18 18:53:38 +01:00
Athou
347fc4f2c8 allow to force refresh feed anytime (#1036) 2023-01-18 18:53:38 +01:00
Athou
2b4ff4a8a5 on fetch error and not under heavy load, don't increase refresh interval exponentially 2023-01-18 18:53:38 +01:00
Jorengarenar
f7d34983e0 Allow API key for count of unread 2023-01-10 00:34:25 +01:00
Athou
3271d69fcb allow session configuration (#1028) 2023-01-04 11:32:08 +01:00
Jérémie Panzer
7ea24b21f8 Merge pull request #1023 from Jorengarenar/develop
Enable environment variables in config.yml
2023-01-04 07:26:23 +01:00
Jorengarenar
b2b608e8c3 Enable environment variables in config.yml 2023-01-03 19:46:01 +01:00
Athou
e44ea5bc96 re-add dropwizard-migrations to be able to use liquibase from command line 2022-11-15 10:48:55 +01:00
Athou
fa58b1e53f reload entries after marking everything as read, as commafeed 2.x does 2022-11-15 08:24:18 +01:00
Athou
9466bc544c show placeholder when favicon is loading 2022-11-08 11:57:59 +01:00
Athou
9e65f5726c require src and alt for images 2022-11-08 11:52:47 +01:00
Athou
fc2c3740a0 dependencies update 2022-11-08 11:52:47 +01:00
Athou
2095a6512b make compact mode more compact 2022-11-05 14:13:19 +01:00
Athou
a461a72224 enable maven cache to speed up build 2022-11-04 09:08:21 +01:00
Athou
f9e7653901 restore swipe-to-right to toggle read/unread status (#1019) 2022-11-04 08:57:33 +01:00
Jérémie Panzer
754ac166e0 Merge pull request #1017 from tristianc/bugfix/builddir
Determine build directory from variable instead of hardcoding it.
2022-09-29 15:54:14 +02:00
Tristian Celestin
0b18334236 Determine build directory from variable instead of hardcoding it. 2022-09-29 09:49:20 -04:00
113 changed files with 22145 additions and 18636 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
# ignore everything
*
# allow only what we need
!commafeed-server/target/commafeed.jar
!commafeed-server/config.yml.example

View File

@@ -1,27 +1,81 @@
name: Java CI
on: [push]
on: [ push ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: ["8", "11", "17"]
java: [ "8", "11", "17" ]
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
# Setup
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: ${{ matrix.java }}
distribution: "temurin"
cache: "maven"
# Build
- name: Build with Maven
run: mvn --batch-mode --update-snapshots verify
- uses: actions/upload-artifact@v3
- name: Upload JAR
uses: actions/upload-artifact@v3
if: ${{ matrix.java == '8' }}
with:
name: commafeed.jar
path: commafeed-server/target/commafeed.jar
- name: Create release
uses: softprops/action-gh-release@v1
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }}
with:
name: CommaFeed ${{ github.ref_name }}
body: See changelog at https://github.com/Athou/commafeed/blob/master/CHANGELOG.md
draft: false
prerelease: false
files: |
commafeed-server/target/commafeed.jar
commafeed-server/config.yml.example
# Docker
- name: Login to Container Registry
uses: docker/login-action@v2
if: ${{ matrix.java == '8' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker build and push tag
uses: docker/build-push-action@v4
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }}
with:
context: .
push: true
platforms: linux/amd64,linux/arm/v7
tags: |
athou/commafeed:latest
athou/commafeed:${{ github.ref_name }}
- name: Docker build and push master
uses: docker/build-push-action@v4
if: ${{ matrix.java == '8' && github.ref_name == 'master' }}
with:
context: .
push: true
platforms: linux/amd64,linux/arm/v7
tags: athou/commafeed:master

View File

@@ -1,80 +0,0 @@
v 3.0.0
- complete overhaul of the UI
- backend and frontend are now in separate maven modules
- no changes to the api or the database
v 2.6.0
- add support for media content as a backup for missing content (useful for youtube feeds)
- correctly follow http error code 308 redirects
- fixed a bug that prevented users from deleting their account
- fixed a bug that made commafeed store entry contents multiple times
- fixed a bug that prevented the app to be used as an installed app on mobile devices if the context path of commafeed was not "/"
- fixed a bug that prevented entries from being "marked as read older than xxx" for a feed that was just added
- removed support for google+ and readability as those services no longer exist
- removed support for deploying on openshift
- removed alphabetical sorting of entries because of really poor performance (title cannot be indexed)
- improve performance for instances with the heavy load setting enabled by preventing CommaFeed from fetching feeds from users that did not log in for a long time
- various dependencies upgrades (notably dropwizard from 1.3 to 2.1)
- add support for mariadb
- add support for java17+ runtime
- various security improvements
v 2.5.0
- unread count is now displayed in a favicon badge when supported
- the user agent string for the bot fetching feeds is now configurable
- feed parsing performance improvements
- support for java9+ runtime
- can now properly start from an empty postgresql database
v 2.4.0
- users were not able to change password or delete account
- fix api key generation
- feed entries can now be sorted alphabetically
- fix facebook sharing
- fix layout on iOS
- postgresql driver update (fix for postgres 9.6)
- various internationalization fixes
- security fixes
v 2.3.0
- dropwizard upgrade 0.9.1
- feed enclosures are hidden if they already displayed in the content
- fix youtube favicons
- various internationalization fixes
v 2.2.0
- fix youtube and instagram favicon fetching
- mark as read filter was lost when a feed was rearranged with drag&drop
- feed entry categories are now displayed if available
- various performance and dependencies upgrades
- java8 is now required
v 2.1.0
- dropwizard upgrade to 0.8.0
- you have to remove the "app.contextPath" setting from your yml file, you can optionally use server.applicationContextPath instead
- new setting app.maxFeedCapacity for deleting old entries
- ability to set filtering expressions for subscriptions to automatically mark new entries as read based on title, content, author or url.
- ability to use !keyword or -keyword to exclude a keyword from a search query
- facebook feeds now show user favicon instead of facebook favicon
- new dark theme 'nightsky'
v 2.0.3
- internet explorer ajax cache workaround
- categories are now deletable again
- openshift support is back
- youtube feeds now show user favicon instead of youtube favicon
v 2.0.2
- api using the api key is now working again
- context path is now configurable in config.yml (see app.contextPath in config.yml.example)
- fix login on firefox when fields are autofilled by the browser
- fix scrolling of subscriptions list on mobile
- user is now logged in after registration
- fix link to documentation on home page and about page
- fields autocomplete is disabled on the profile page
- users are able to delete their account again
- chinese and malaysian translation files are now correctly loaded
- software version in user-agent when fetching feeds is no longer hardcoded
- admin settings page is now read only, settings are configured in config.yml
- added link to metrics on the admin settings page
- Rome (rss library) upgrade to 1.5.0
v 2.0.1
- the redis pool no longer throws an exception when it is unable to aquire a new connection
v2.0.0
- The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory consumption and better overall performances.
See the README on how to build CommaFeed from now on.
- CommaFeed should no longer fetch the same feed multiple times in a row
- Users can use their username or email to log in

139
CHANGELOG.md Normal file
View File

@@ -0,0 +1,139 @@
Changelog
=========
3.1.0
-----
- add an even more compact layout
- restore hover effect from commafeed 2.x
- view mode (compact, expanded, ...) is now stored on the device so you can have a different view mode on desktop and
mobile
- fix for the "Illegal attempt to associate a collection with two open sessions." error
- feed fetching workflow is now orchestrated with rxjava, removing a lot of code
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
- allow env variable prefixed with `CF_` to override config.yml properties
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true`
3.0.0
-----
- complete overhaul of the UI
- backend and frontend are now in separate maven modules
- no changes to the api or the database
- Docker images are now automatically built and available at https://hub.docker.com/r/athou/commafeed
2.6.0
-----
- add support for media content as a backup for missing content (useful for youtube feeds)
- correctly follow http error code 308 redirects
- fixed a bug that prevented users from deleting their account
- fixed a bug that made commafeed store entry contents multiple times
- fixed a bug that prevented the app to be used as an installed app on mobile devices if the context path of commafeed
was not "/"
- fixed a bug that prevented entries from being "marked as read older than xxx" for a feed that was just added
- removed support for google+ and readability as those services no longer exist
- removed support for deploying on openshift
- removed alphabetical sorting of entries because of really poor performance (title cannot be indexed)
- improve performance for instances with the heavy load setting enabled by preventing CommaFeed from fetching feeds from
users that did not log in for a long time
- various dependencies upgrades (notably dropwizard from 1.3 to 2.1)
- add support for mariadb
- add support for java17+ runtime
- various security improvements
2.5.0
-----
- unread count is now displayed in a favicon badge when supported
- the user agent string for the bot fetching feeds is now configurable
- feed parsing performance improvements
- support for java9+ runtime
- can now properly start from an empty postgresql database
2.4.0
-----
- users were not able to change password or delete account
- fix api key generation
- feed entries can now be sorted alphabetically
- fix facebook sharing
- fix layout on iOS
- postgresql driver update (fix for postgres 9.6)
- various internationalization fixes
- security fixes
2.3.0
-----
- dropwizard upgrade 0.9.1
- feed enclosures are hidden if they already displayed in the content
- fix youtube favicons
- various internationalization fixes
2.2.0
-----
- fix youtube and instagram favicon fetching
- mark as read filter was lost when a feed was rearranged with drag&drop
- feed entry categories are now displayed if available
- various performance and dependencies upgrades
- java8 is now required
2.1.0
-----
- dropwizard upgrade to 0.8.0
- you have to remove the "app.contextPath" setting from your yml file, you can optionally use
server.applicationContextPath instead
- new setting app.maxFeedCapacity for deleting old entries
- ability to set filtering expressions for subscriptions to automatically mark new entries as read based on title,
content, author or url.
- ability to use !keyword or -keyword to exclude a keyword from a search query
- facebook feeds now show user favicon instead of facebook favicon
- new dark theme 'nightsky'
2.0.3
-----
- internet explorer ajax cache workaround
- categories are now deletable again
- openshift support is back
- youtube feeds now show user favicon instead of youtube favicon
2.0.2
-----
- api using the api key is now working again
- context path is now configurable in config.yml (see app.contextPath in config.yml.example)
- fix login on firefox when fields are autofilled by the browser
- fix scrolling of subscriptions list on mobile
- user is now logged in after registration
- fix link to documentation on home page and about page
- fields autocomplete is disabled on the profile page
- users are able to delete their account again
- chinese and malaysian translation files are now correctly loaded
- software version in user-agent when fetching feeds is no longer hardcoded
- admin settings page is now read only, settings are configured in config.yml
- added link to metrics on the admin settings page
- Rome (rss library) upgrade to 1.5.0
2.0.1
-----
- the redis pool no longer throws an exception when it is unable to aquire a new connection
2.0.0
-----
- The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory
consumption and better overall performances.
See the README on how to build CommaFeed from now on.
- CommaFeed should no longer fetch the same feed multiple times in a row
- Users can use their username or email to log in

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM eclipse-temurin:17-jre
EXPOSE 8082
RUN mkdir -p /commafeed/data
VOLUME /commafeed/data
ENV CF_SESSION_PATH=/commafeed/data/sessions
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"]

View File

@@ -1,9 +1,20 @@
# CommaFeed
Sources for [CommaFeed.com](http://www.commafeed.com/).
Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/TypeScript.
Google Reader inspired self-hosted RSS reader, based on Dropwizard and AngularJS.
CommaFeed is now considered feature-complete and is in maintenance mode.
![preview](https://user-images.githubusercontent.com/1256795/184886828-1973f148-58a9-4c6d-9587-ee5e5d3cc2cb.png)
## Features
- 4 different layouts
- 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
## Related open-source projects
@@ -16,84 +27,53 @@ Browser extensions:
## Deployment on your own server
### The very short version (download precompiled package)
### Docker
Docker images are built automatically and are available at https://hub.docker.com/r/athou/commafeed
### Download precompiled package
mkdir commafeed && cd commafeed
wget https://github.com/Athou/commafeed/releases/download/3.0.0/commafeed.jar
wget https://raw.githubusercontent.com/Athou/commafeed/3.0.0/commafeed-server/config.yml.example -O config.yml
vi config.yml
wget https://github.com/Athou/commafeed/releases/latest/download/commafeed.jar
wget https://github.com/Athou/commafeed/releases/latest/download/config.yml.example -O config.yml
java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml
### The short version (build from sources)
The server will listen on http://localhost:8082. The default
user is `admin` and the default password is `admin`.
### Build from sources
git clone https://github.com/Athou/commafeed.git
cd commafeed
./mvnw clean package
cp commafeed-server/config.yml.example config.yml
vi config.yml
java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml
### The long version (same as the short version, but more detailed)
The server will listen on http://localhost:8082. The default
user is `admin` and the default password is `admin`.
CommaFeed 2.0 has been rewritten to use Dropwizard and gulp instead of using tomee and wro4j. The latest version of the 1.x branch is available [here](https://github.com/Athou/commafeed/tree/1.x).
## Translation
For storage, you can either use an embedded file-based H2 database or an external MySQL, PostgreSQL or SQLServer database.
You also need the Java 1.8+ JDK in order to build the application.
To install the required packages to build CommaFeed on Ubuntu, issue the following commands
# if this commands works and returns a version >= 1.8.0 you're good to go and you can skip JDK installation
javac -version
# if openjdk-8-jdk is not available on your ubuntu version (14.04 LTS), add the following repo first
sudo add-apt-repository ppa:openjdk-r/ppa
sudo apt-get update
sudo apt-get install g++ build-essential openjdk-8-jdk
# Make sure java8 is the selected java version
sudo update-alternatives --config java
sudo update-alternatives --config javac
Clone this repository. If you don't have git you can download the sources as a zip file from [here](https://github.com/Athou/commafeed/archive/master.zip)
git clone https://github.com/Athou/commafeed.git
cd commafeed
Now build the application
./mvnw clean package
Copy `commafeed-server/config.yml.example` to `./config.yml` then edit the file to your liking.
Issue the following command to run the app, the server will listen by default on `http://localhost:8082`. The default user is `admin` and the default password is `admin`.
java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml
You can use a proxy http server such as nginx or apache.
## Translate CommaFeed into your language
Files for internationalization are located [here](https://github.com/Athou/commafeed/tree/master/commafeed-client/src/locales).
Files for internationalization are
located [here](https://github.com/Athou/commafeed/tree/master/commafeed-client/src/locales).
To add a new language:
- edit `commafeed-client/src/i18n.ts`
- add the new locale to the `locales` array.
- import the dayjs locale
- add the new locale to the `locales` array.
- import the dayjs locale
- edit `commafeed-client/.linguirc` and add the new locale to the `locales` array.
- run `npm run i18n` and add translations to the newly created `commafeed-client/src/locales/[locale]/messages.po` file
The name of the locale should be the two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
The name of the locale should be the
two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
## Local development
- `git clone https://github.com/Athou/CommaFeed`
### Backend
- Open `commafeed-server` in your preferred Java IDE.
- CommaFeed uses Lombok, you need the Lombok plugin for your IDE.
- If using Eclipse, Go to Window → Preferences → Maven → Annotation Processing and check "Automatically configure JDT APT"
- CommaFeed uses Lombok, you need the Lombok plugin for your IDE.
- Start `CommaFeedApplication.java` in debug mode with `server config.dev.yml` as arguments
### Frontend
@@ -101,11 +81,13 @@ The name of the locale should be the two-letters [ISO-639-1 language code](http:
- Open `commafeed-client` in your preferred JavaScript IDE.
- run `npm install`
- run `npm run dev`
- the frontend server is now running at http://localhost:8082 and is proxying REST requests to the backend running on port 8083
The frontend server is now running at http://localhost:8082 and is proxying REST requests to the backend running on
port 8083
## Copyright and license
Copyright 2013-2022 CommaFeed.
Copyright 2013-2023 CommaFeed.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this work except in compliance with the License.

View File

@@ -70,8 +70,6 @@
"groups": [
"useLocation",
"useParams",
"useStyles",
"useMantineTheme",
"useState",
"useAppSelector",
"useAppDispatch",

File diff suppressed because it is too large Load Diff

View File

@@ -1,76 +1,82 @@
{
"name": "commafeed-client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "npm run i18n:compile && tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"test:ci": "vitest run",
"eslint": "eslint --ext=.js,.jsx,.ts,.tsx src",
"i18n": "npm run i18n:extract && npm run i18n:compile",
"i18n:extract": "lingui extract --clean",
"i18n:compile": "lingui compile --typescript",
"postinstall": "npm run i18n:compile"
},
"dependencies": {
"@emotion/react": "^11.10.5",
"@fontsource/open-sans": "^4.5.13",
"@lingui/core": "^3.14.0",
"@lingui/macro": "^3.14.0",
"@lingui/react": "^3.14.0",
"@mantine/core": "^5.6.3",
"@mantine/form": "^5.6.3",
"@mantine/hooks": "^5.6.3",
"@mantine/modals": "^5.6.3",
"@mantine/notifications": "^5.6.3",
"@mantine/spotlight": "^5.6.3",
"@reduxjs/toolkit": "^1.8.6",
"axios": "^1.1.3",
"dayjs": "^1.11.6",
"interweave": "^13.0.0",
"lodash": "^4.17.21",
"make-plural": "^7.1.0",
"mousetrap": "^1.6.5",
"react": "^18.2.0",
"react-async-hook": "^4.0.0",
"react-dom": "^18.2.0",
"react-icons": "^4.6.0",
"react-infinite-scroller": "^1.2.6",
"react-redux": "^8.0.4",
"react-router-dom": "^6.4.2",
"swagger-ui-react": "^4.15.2",
"tinycon": "^0.6.8"
},
"devDependencies": {
"@lingui/cli": "^3.14.0",
"@types/eslint": "^8.4.8",
"@types/lodash": "^4.14.186",
"@types/mousetrap": "^1.6.10",
"@types/react": "^18.0.24",
"@types/react-dom": "^18.0.8",
"@types/react-infinite-scroller": "^1.2.3",
"@types/swagger-ui-react": "^4.11.0",
"@types/tinycon": "^0.6.3",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"@typescript-eslint/parser": "^5.41.0",
"@vitejs/plugin-react": "^2.2.0",
"eslint": "^8.26.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-hooks": "^0.4.3",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.7.1",
"rollup-plugin-visualizer": "^5.8.3",
"typescript": "^4.8.4",
"vite": "^3.2.0",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^3.5.2",
"vitest": "^0.24.3",
"vitest-mock-extended": "^1.0.3"
}
"name": "commafeed-client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"dev:typescript": "tsc --watch",
"build": "npm run i18n:compile && tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"test:ci": "vitest run",
"eslint": "eslint --ext=.js,.jsx,.ts,.tsx src",
"i18n": "npm run i18n:extract && npm run i18n:compile",
"i18n:extract": "lingui extract --clean",
"i18n:compile": "lingui compile --typescript",
"postinstall": "npm run i18n:compile"
},
"dependencies": {
"@emotion/react": "^11.10.5",
"@fontsource/open-sans": "^4.5.14",
"@lingui/core": "^3.17.0",
"@lingui/macro": "^3.17.0",
"@lingui/react": "^3.17.0",
"@mantine/core": "^5.10.3",
"@mantine/form": "^5.10.3",
"@mantine/hooks": "^5.10.3",
"@mantine/modals": "^5.10.3",
"@mantine/notifications": "^5.10.3",
"@mantine/spotlight": "^5.10.3",
"@mantine/styles": "^5.10.3",
"@reduxjs/toolkit": "^1.9.2",
"axios": "^1.3.2",
"dayjs": "^1.11.7",
"interweave": "^13.0.0",
"lodash": "^4.17.21",
"make-plural": "^7.2.0",
"mousetrap": "^1.6.5",
"react": "^18.2.0",
"react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0",
"react-dom": "^18.2.0",
"react-icons": "^4.7.1",
"react-infinite-scroller": "^1.2.6",
"react-redux": "^8.0.5",
"react-router-dom": "^6.8.0",
"react-swipeable": "^7.0.0",
"swagger-ui-react": "^4.15.5",
"tinycon": "^0.6.8",
"use-local-storage": "^3.0.0",
"websocket-heartbeat-js": "^1.1.1"
},
"devDependencies": {
"@lingui/cli": "^3.17.0",
"@types/eslint": "^8.21.0",
"@types/lodash": "^4.14.191",
"@types/mousetrap": "^1.6.11",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/react-infinite-scroller": "^1.2.3",
"@types/swagger-ui-react": "^4.11.0",
"@types/tinycon": "^0.6.3",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@vitejs/plugin-react": "^3.1.0",
"eslint": "^8.33.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.6.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-hooks": "^0.4.3",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.8.3",
"rollup-plugin-visualizer": "^5.9.0",
"typescript": "^4.9.5",
"vite": "^4.1.1",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.0.5",
"vitest": "^0.28.4",
"vitest-mock-extended": "^1.0.9"
}
}

View File

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

View File

@@ -1,7 +1,8 @@
import { i18n } from "@lingui/core"
import { I18nProvider } from "@lingui/react"
import { ColorScheme, ColorSchemeProvider, MantineProvider } from "@mantine/core"
import { useColorScheme, useLocalStorage } from "@mantine/hooks"
import { useColorScheme } from "@mantine/hooks"
import useLocalStorage from "use-local-storage"
import { ModalsProvider } from "@mantine/modals"
import { NotificationsProvider } from "@mantine/notifications"
import { Constants } from "app/constants"
@@ -32,11 +33,7 @@ import Tinycon from "tinycon"
function Providers(props: { children: React.ReactNode }) {
const preferredColorScheme = useColorScheme()
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>({
key: "color-scheme",
defaultValue: preferredColorScheme,
getInitialValueInEffect: true,
})
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>("color-scheme", preferredColorScheme)
const toggleColorScheme = (value?: ColorScheme) => setColorScheme(value || (colorScheme === "dark" ? "light" : "dark"))
return (

View File

@@ -58,6 +58,7 @@ export const client = {
getEntries: (req: GetEntriesPaginatedRequest) => axiosInstance.get<Entries>("feed/entries", { params: req }),
markEntries: (req: MarkRequest) => axiosInstance.post("feed/mark", req),
fetchFeed: (req: FeedInfoRequest) => axiosInstance.post<FeedInfo>("feed/fetch", req),
refreshAll: () => axiosInstance.get("feed/refreshAll"),
subscribe: (req: SubscribeRequest) => axiosInstance.post<number>("feed/subscribe", req),
unsubscribe: (req: IDRequest) => axiosInstance.post("feed/unsubscribe", req),
importOpml: (req: File) => {

View File

@@ -97,4 +97,5 @@ export const Constants = {
mainScrollAreaId: "main-scroll-area-id",
entryId: (entry: Entry) => `entry-id-${entry.id}`,
},
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
}

View File

@@ -128,6 +128,7 @@ export const markAllEntries = createAsyncThunk<void, { sourceType: EntrySourceTy
async (arg, thunkApi) => {
const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries
await endpoint(arg.req)
thunkApi.dispatch(reloadEntries())
thunkApi.dispatch(reloadTree())
}
)

View File

@@ -3,7 +3,7 @@ import { showNotification } from "@mantine/notifications"
import { createAsyncThunk, createSlice, isAnyOf } from "@reduxjs/toolkit"
import { client } from "app/client"
import { RootState } from "app/store"
import { ReadingMode, ReadingOrder, Settings, SharingSettings, UserModel, ViewMode } from "app/types"
import { ReadingMode, ReadingOrder, Settings, SharingSettings, UserModel } from "app/types"
// eslint-disable-next-line import/no-cycle
import { reloadEntries } from "./entries"
@@ -36,46 +36,67 @@ export const changeReadingOrder = createAsyncThunk<void, ReadingOrder, { state:
thunkApi.dispatch(reloadEntries())
}
)
export const changeViewMode = createAsyncThunk<void, ViewMode, { state: RootState }>("settings/viewMode", (viewMode, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, viewMode })
thunkApi.dispatch(reloadEntries())
})
export const changeLanguage = createAsyncThunk<void, string, { state: RootState }>("settings/language", (language, thunkApi) => {
export const changeLanguage = createAsyncThunk<
void,
string,
{
state: RootState
}
>("settings/language", (language, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, language })
})
export const changeScrollSpeed = createAsyncThunk<void, boolean, { state: RootState }>("settings/scrollSpeed", (speed, thunkApi) => {
export const changeScrollSpeed = createAsyncThunk<
void,
boolean,
{
state: RootState
}
>("settings/scrollSpeed", (speed, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
})
export const changeShowRead = createAsyncThunk<void, boolean, { state: RootState }>("settings/showRead", (showRead, thunkApi) => {
export const changeShowRead = createAsyncThunk<
void,
boolean,
{
state: RootState
}
>("settings/showRead", (showRead, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, showRead })
})
export const changeScrollMarks = createAsyncThunk<void, boolean, { state: RootState }>("settings/scrollMarks", (scrollMarks, thunkApi) => {
export const changeScrollMarks = createAsyncThunk<
void,
boolean,
{
state: RootState
}
>("settings/scrollMarks", (scrollMarks, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollMarks })
})
export const changeSharingSetting = createAsyncThunk<void, { site: keyof SharingSettings; value: boolean }, { state: RootState }>(
"settings/sharingSetting",
(sharingSetting, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({
...settings,
sharingSettings: {
...settings.sharingSettings,
[sharingSetting.site]: sharingSetting.value,
},
})
export const changeSharingSetting = createAsyncThunk<
void,
{ site: keyof SharingSettings; value: boolean },
{
state: RootState
}
)
>("settings/sharingSetting", (sharingSetting, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({
...settings,
sharingSettings: {
...settings.sharingSettings,
[sharingSetting.site]: sharingSetting.value,
},
})
})
export const userSlice = createSlice({
name: "user",
@@ -99,10 +120,6 @@ export const userSlice = createSlice({
if (!state.settings) return
state.settings.readingOrder = action.meta.arg
})
builder.addCase(changeViewMode.pending, (state, action) => {
if (!state.settings) return
state.settings.viewMode = action.meta.arg
})
builder.addCase(changeLanguage.pending, (state, action) => {
if (!state.settings) return
state.settings.language = action.meta.arg

View File

@@ -38,6 +38,7 @@ export interface ApplicationSettings {
export interface Category {
id: string
parentId?: string
parentName?: string
name: string
children: Category[]
feeds: Subscription[]
@@ -227,7 +228,6 @@ export interface Settings {
language: string
readingMode: ReadingMode
readingOrder: ReadingOrder
viewMode: ViewMode
showRead: boolean
scrollMarks: boolean
theme?: string
@@ -305,4 +305,4 @@ export type ReadingMode = "all" | "unread"
export type ReadingOrder = "asc" | "desc"
export type ViewMode = "title" | "cozy" | "expanded"
export type ViewMode = "title" | "cozy" | "detailed" | "expanded"

View File

@@ -51,3 +51,16 @@ export const scrollToWithCallback = ({
element.scrollTo(options)
}
export const openLinkInBackgroundTab = (url: string) => {
// simulate ctrl+click to open tab in background
const a = document.createElement("a")
a.href = url
a.rel = "noreferrer"
a.dispatchEvent(
new MouseEvent("click", {
ctrlKey: true,
metaKey: true,
})
)
}

View File

@@ -3,13 +3,16 @@ import { useState } from "react"
import { TbPhoto } from "react-icons/tb"
interface ImageWithPlaceholderWhileLoadingProps {
src?: string
alt?: string
src: string
alt: string
title?: string
width?: number
height?: number | "auto"
placeholderWidth?: number
placeholderHeight?: number
placeholderBackgroundColor?: string
placeholderIconSize?: number
placeholderIconColor?: string
}
const useStyles = createStyles((theme, props: ImageWithPlaceholderWhileLoadingProps) => ({
@@ -17,8 +20,8 @@ const useStyles = createStyles((theme, props: ImageWithPlaceholderWhileLoadingPr
width: props.placeholderWidth ?? 400,
height: props.placeholderHeight ?? 600,
maxWidth: "100%",
color: theme.fn.variant({ color: theme.primaryColor, variant: "subtle" }).color,
backgroundColor: theme.colorScheme === "dark" ? theme.colors.dark[5] : theme.colors.gray[1],
color: props.placeholderIconColor ?? theme.fn.variant({ color: theme.primaryColor, variant: "subtle" }).color,
backgroundColor: props.placeholderBackgroundColor ?? (theme.colorScheme === "dark" ? theme.colors.dark[5] : theme.colors.gray[1]),
},
}))
@@ -32,7 +35,7 @@ export function ImageWithPlaceholderWhileLoading(props: ImageWithPlaceholderWhil
<Box>
<Center className={classes.placeholder}>
<div>
<TbPhoto size={48} />
<TbPhoto size={props.placeholderIconSize ?? 48} />
</div>
</Center>
</Box>

View File

@@ -95,6 +95,10 @@ export function KeyboardShortcutsHelp() {
</td>
<td>
<Kbd>B</Kbd>
<span>, </span>
<Kbd>
<Trans>Middle click</Trans>
</Kbd>
</td>
</tr>
<tr>
@@ -103,6 +107,8 @@ export function KeyboardShortcutsHelp() {
</td>
<td>
<Kbd>M</Kbd>
<span>, </span>
<Trans>Swipe header to the right</Trans>
</td>
</tr>
<tr>
@@ -143,6 +149,26 @@ export function KeyboardShortcutsHelp() {
<Kbd>U</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Show entry menu (desktop)</Trans>
</td>
<td>
<Kbd>
<Trans>Right click</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Show entry menu (mobile)</Trans>
</td>
<td>
<Kbd>
<Trans>Long press</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Show keyboard shortcut help</Trans>

View File

@@ -29,7 +29,9 @@ const transform: TransformCallback = node => {
if (node.tagName === "IMG") {
// show placeholders for loading img tags, this allows the entry to have its final height immediately
const src = node.getAttribute("src") ?? undefined
const alt = node.getAttribute("alt") ?? undefined
if (!src) return undefined
const alt = node.getAttribute("alt") ?? "image"
const title = node.getAttribute("title") ?? undefined
const nodeWidth = node.getAttribute("width")
const nodeHeight = node.getAttribute("height")
@@ -40,6 +42,7 @@ const transform: TransformCallback = node => {
height,
maxWidth: Constants.layout.entryMaxWidth,
})
return (
<ImageWithPlaceholderWhileLoading
src={src}

View File

@@ -1,7 +1,7 @@
import { TypographyStylesProvider } from "@mantine/core"
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
export function Enclosure(props: { enclosureType?: string; enclosureUrl?: string }) {
export function Enclosure(props: { enclosureType: string; enclosureUrl: string }) {
const hasVideo = props.enclosureType && props.enclosureType.indexOf("video") === 0
const hasAudio = props.enclosureType && props.enclosureType.indexOf("audio") === 0
const hasImage = props.enclosureType && props.enclosureType.indexOf("image") === 0

View File

@@ -13,6 +13,7 @@ import {
} from "app/slices/entries"
import { redirectToRootCategory } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store"
import { openLinkInBackgroundTab } from "app/utils"
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
import { Loader } from "components/Loader"
import { useMousetrap } from "hooks/useMousetrap"
@@ -20,6 +21,7 @@ import throttle from "lodash/throttle"
import { useEffect } from "react"
import InfiniteScroll from "react-infinite-scroller"
import { FeedEntry } from "./FeedEntry"
import { useViewMode } from "../../hooks/useViewMode"
export function FeedEntries() {
const source = useAppSelector(state => state.entries.source)
@@ -27,7 +29,7 @@ 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 viewMode = useAppSelector(state => state.user.settings?.viewMode)
const { viewMode } = useViewMode()
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry)
const dispatch = useAppDispatch()
@@ -211,15 +213,7 @@ export function FeedEntries() {
useMousetrap("b", () => {
// simulate ctrl+click to open tab in background
if (!selectedEntry) return
const a = document.createElement("a")
a.href = selectedEntry.url
a.rel = "noreferrer"
a.dispatchEvent(
new MouseEvent("click", {
ctrlKey: true,
metaKey: true,
})
)
openLinkInBackgroundTab(selectedEntry.url)
})
useMousetrap("m", () => {
// toggle read status

View File

@@ -1,12 +1,17 @@
import { Anchor, Box, createStyles, Divider, Paper } from "@mantine/core"
import { Constants } from "app/constants"
import { useAppSelector } from "app/store"
import { Entry } from "app/types"
import { markEntry } from "app/slices/entries"
import { useAppDispatch } from "app/store"
import { Entry, ViewMode } from "app/types"
import React from "react"
import { useSwipeable } from "react-swipeable"
import { MantineNumberSize } from "@mantine/styles"
import { FeedEntryBody } from "./FeedEntryBody"
import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader"
import { FeedEntryContextMenu, useFeedEntryContextMenu } from "./FeedEntryContextMenu"
import { FeedEntryFooter } from "./FeedEntryFooter"
import { FeedEntryHeader } from "./FeedEntryHeader"
import { useViewMode } from "../../hooks/useViewMode"
interface FeedEntryProps {
entry: Entry
@@ -15,19 +20,37 @@ interface FeedEntryProps {
onHeaderClick: (e: React.MouseEvent) => void
}
const useStyles = createStyles((theme, props: FeedEntryProps) => {
const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: ViewMode }) => {
let backgroundColor
if (theme.colorScheme === "dark") backgroundColor = props.entry.read ? "inherit" : theme.colors.dark[5]
else backgroundColor = props.entry.read && !props.expanded ? theme.colors.gray[0] : "inherit"
let marginY = theme.spacing.xs
if (props.viewMode === "title") marginY = 2
else if (props.viewMode === "cozy") marginY = 6
let mobileMarginY = 6
if (props.viewMode === "title") mobileMarginY = 2
else if (props.viewMode === "cozy") mobileMarginY = 4
let backgroundHoverColor = backgroundColor
if (!props.expanded) {
backgroundHoverColor = theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]
}
const styles = {
paper: {
backgroundColor,
marginTop: theme.spacing.xs,
marginBottom: theme.spacing.xs,
marginTop: marginY,
marginBottom: marginY,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
marginTop: "6px",
marginBottom: "6px",
marginTop: mobileMarginY,
marginBottom: mobileMarginY,
},
"@media (hover: hover)": {
"&:hover": {
backgroundColor: backgroundHoverColor,
},
},
},
body: {
@@ -43,12 +66,31 @@ const useStyles = createStyles((theme, props: FeedEntryProps) => {
})
export function FeedEntry(props: FeedEntryProps) {
const { classes } = useStyles(props)
const viewMode = useAppSelector(state => state.user.settings?.viewMode)
const compactHeader = viewMode === "title" && !props.expanded
const { viewMode } = useViewMode()
const { classes } = useStyles({ ...props, viewMode })
const dispatch = useAppDispatch()
const swipeHandlers = useSwipeable({
onSwipedRight: () => dispatch(markEntry({ entry: props.entry, read: !props.entry.read })),
})
const { onContextMenu } = useFeedEntryContextMenu(props.entry)
let paddingX: MantineNumberSize = "xs"
if (viewMode === "title" || viewMode === "cozy") paddingX = 6
let paddingY: MantineNumberSize = "xs"
if (viewMode === "title") paddingY = 4
else if (viewMode === "cozy") paddingY = 8
let borderRadius: MantineNumberSize = "sm"
if (viewMode === "title") borderRadius = 0
else if (viewMode === "cozy") borderRadius = "xs"
const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy")
return (
<Paper withBorder className={classes.paper}>
<Paper withBorder radius={borderRadius} className={classes.paper}>
<Anchor
variant="text"
href={props.entry.url}
@@ -56,21 +98,24 @@ export function FeedEntry(props: FeedEntryProps) {
rel="noreferrer"
onClick={props.onHeaderClick}
onAuxClick={props.onHeaderClick}
onContextMenu={onContextMenu}
>
<Box p="xs">
<Box px={paddingX} py={paddingY} {...swipeHandlers}>
{compactHeader && <FeedEntryCompactHeader entry={props.entry} />}
{!compactHeader && <FeedEntryHeader entry={props.entry} expanded={props.expanded} />}
</Box>
</Anchor>
{props.expanded && (
<Box px="xs" pb="xs">
<Box px={paddingX} pb={paddingY}>
<Box className={classes.body} sx={{ direction: props.entry.rtl ? "rtl" : "ltr" }}>
<FeedEntryBody entry={props.entry} />
</Box>
<Divider variant="dashed" my="xs" />
<Divider variant="dashed" my={paddingY} />
<FeedEntryFooter entry={props.entry} />
</Box>
)}
<FeedEntryContextMenu entry={props.entry} />
</Paper>
)
}

View File

@@ -14,7 +14,7 @@ export function FeedEntryBody(props: FeedEntryBodyProps) {
<Box>
<Content content={props.entry.content} />
</Box>
{props.entry.enclosureUrl && (
{props.entry.enclosureType && props.entry.enclosureUrl && (
<Box pt="md">
<Enclosure enclosureType={props.entry.enclosureType} enclosureUrl={props.entry.enclosureUrl} />
</Box>

View File

@@ -1,8 +1,9 @@
import { Box, createStyles, Image, Text } from "@mantine/core"
import { Box, createStyles, Text } from "@mantine/core"
import { Entry } from "app/types"
import { RelativeDate } from "components/RelativeDate"
import { OnDesktop } from "components/responsive/OnDesktop"
import { FeedEntryTitle } from "./FeedEntryTitle"
import { FeedFavicon } from "./FeedFavicon"
export interface FeedEntryHeaderProps {
entry: Entry
@@ -37,7 +38,7 @@ export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
return (
<Box className={classes.wrapper}>
<Box>
<Image withPlaceholder src={props.entry.iconUrl} alt="feed icon" width={18} height={18} />
<FeedFavicon url={props.entry.iconUrl} />
</Box>
<OnDesktop>
<Text color="dimmed" className={classes.feedName}>

View File

@@ -0,0 +1,127 @@
import { t, Trans } from "@lingui/macro"
import { createStyles, Group } from "@mantine/core"
import { Constants } from "app/constants"
import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries"
import { redirectToFeed } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types"
import { openLinkInBackgroundTab } from "app/utils"
import { throttle, truncate } from "lodash"
import { useEffect } from "react"
import { Item, Menu, Separator, useContextMenu } from "react-contexify"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb"
interface FeedEntryContextMenuProps {
entry: Entry
}
const iconSize = 16
const useStyles = createStyles(theme => ({
menu: {
// apply mantine theme from MenuItem.styles.ts
fontSize: theme.fontSizes.sm,
"--contexify-item-color": `${theme.colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
"--contexify-activeItem-color": `${theme.colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
"--contexify-activeItem-bgColor": `${
theme.colorScheme === "dark" ? theme.fn.rgba(theme.colors.dark[3], 0.35) : theme.colors.gray[1]
} !important`,
},
}))
const menuId = (entry: Entry) => entry.id
export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
const { classes, theme } = useStyles()
const sourceType = useAppSelector(state => state.entries.source.type)
const dispatch = useAppDispatch()
return (
<Menu id={menuId(props.entry)} theme={theme.colorScheme} animation={false} className={classes.menu}>
<Item
onClick={() => {
window.open(props.entry.url, "_blank", "noreferrer")
dispatch(markEntry({ entry: props.entry, read: true }))
}}
>
<Group>
<TbExternalLink size={iconSize} />
<Trans>Open link in new tab</Trans>
</Group>
</Item>
<Item
onClick={() => {
openLinkInBackgroundTab(props.entry.url)
dispatch(markEntry({ entry: props.entry, read: true }))
}}
>
<Group>
<TbExternalLink size={iconSize} />
<Trans>Open link in new background tab</Trans>
</Group>
</Item>
<Separator />
<Item onClick={() => dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))}>
<Group>
{props.entry.starred ? <TbStarOff size={iconSize} /> : <TbStar size={iconSize} />}
{props.entry.starred ? t`Unstar` : t`Star`}
</Group>
</Item>
<Item onClick={() => dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}>
<Group>
{props.entry.read ? <TbEyeOff size={iconSize} /> : <TbEyeCheck size={iconSize} />}
{props.entry.read ? t`Keep unread` : t`Mark as read`}
</Group>
</Item>
<Item onClick={() => dispatch(markEntriesUpToEntry(props.entry))}>
<Group>
<TbArrowBarToDown size={iconSize} />
<Trans>Mark as read up to here</Trans>
</Group>
</Item>
{sourceType === "category" && (
<>
<Separator />
<Item
onClick={() => {
dispatch(redirectToFeed(props.entry.feedId))
}}
>
<Group>
<TbRss size={iconSize} />
<Trans>Go to {truncate(props.entry.feedName)}</Trans>
</Group>
</Item>
</>
)}
</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(listener, 100)
scrollArea?.addEventListener("scroll", throttledListener)
return () => scrollArea?.removeEventListener("scroll", throttledListener)
}, [contextMenu])
return { onContextMenu }
}

View File

@@ -7,6 +7,7 @@ import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types"
import { ActionButton } from "components/ActionButtton"
import { ButtonToolbar } from "components/ButtonToolbar"
import { throttle } from "lodash"
import { useEffect, useState } from "react"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
import { ShareButtons } from "./ShareButtons"
@@ -22,8 +23,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
const mobile = !useMediaQuery(`(min-width: ${Constants.layout.mobileBreakpoint}px)`)
const dispatch = useAppDispatch()
const showSharingButtons =
sharingSettings && (Object.values(sharingSettings) as Array<typeof sharingSettings[keyof typeof sharingSettings]>).some(v => v)
const showSharingButtons = sharingSettings && Object.values(sharingSettings).some(v => v)
const readStatusButtonClicked = () => dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))
const onTagsChange = (values: string[]) =>
@@ -38,8 +38,10 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
const listener = () => setScrollPosition(scrollArea ? scrollArea.scrollTop : 0)
scrollArea?.addEventListener("scroll", listener)
return () => scrollArea?.removeEventListener("scroll", listener)
const throttledListener = throttle(listener, 100)
scrollArea?.addEventListener("scroll", throttledListener)
return () => scrollArea?.removeEventListener("scroll", throttledListener)
}, [])
return (

View File

@@ -1,7 +1,8 @@
import { Box, createStyles, Image, Text } from "@mantine/core"
import { Box, createStyles, Text } from "@mantine/core"
import { Entry } from "app/types"
import { RelativeDate } from "components/RelativeDate"
import { FeedEntryTitle } from "./FeedEntryTitle"
import { FeedFavicon } from "./FeedFavicon"
export interface FeedEntryHeaderProps {
entry: Entry
@@ -33,7 +34,7 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
</Box>
<Box className={classes.headerSubtext}>
<Box mr={6}>
<Image withPlaceholder src={props.entry.iconUrl} alt="feed icon" width={18} height={18} />
<FeedFavicon url={props.entry.iconUrl} />
</Box>
<Box>
<Text color="dimmed">{props.entry.feedName}</Text>

View File

@@ -0,0 +1,22 @@
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
export interface FeedFaviconProps {
url: string
size?: number
}
export function FeedFavicon({ url, size = 18 }: FeedFaviconProps) {
return (
<ImageWithPlaceholderWhileLoading
src={url}
alt="feed favicon"
width={size}
height={size}
placeholderWidth={size}
placeholderHeight={size}
placeholderBackgroundColor="inherit"
placeholderIconSize={size}
placeholderIconColor="inherit"
/>
)
}

View File

@@ -4,16 +4,20 @@ import { Constants } from "app/constants"
import { useAppSelector } from "app/store"
import { flattenCategoryTree } from "app/utils"
type CategorySelectProps = Partial<SelectProps> & { withAll?: boolean }
type CategorySelectProps = Partial<SelectProps> & {
withAll?: boolean
withoutCategoryIds?: string[]
}
export function CategorySelect(props: CategorySelectProps) {
const rootCategory = useAppSelector(state => state.tree.rootCategory)
const categories = rootCategory && flattenCategoryTree(rootCategory)
const selectData: SelectItem[] | undefined = categories
?.filter(c => c.id !== Constants.categories.all.id)
.filter(c => !props.withoutCategoryIds || !props.withoutCategoryIds.includes(c.id))
.sort((c1, c2) => c1.name.localeCompare(c2.name))
.map(c => ({
label: c.name,
label: c.parentName ? t`${c.name} (in ${c.parentName})` : c.name,
value: c.id,
}))
if (props.withAll) {

View File

@@ -1,11 +1,26 @@
import { Trans } from "@lingui/macro"
import { t, Trans } from "@lingui/macro"
import { Box, Divider, Group, Menu, SegmentedControl, SegmentedControlItem, useMantineColorScheme } from "@mantine/core"
import { showNotification } from "@mantine/notifications"
import { client } from "app/client"
import { redirectToAbout, redirectToAdminUsers, redirectToMetrics, redirectToSettings } from "app/slices/redirect"
import { changeViewMode } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store"
import { ViewMode } from "app/types"
import { useState } from "react"
import { TbChartLine, TbHelp, TbLayoutList, TbList, TbMoon, TbNotes, TbPower, TbSettings, TbSun, TbUsers } from "react-icons/tb"
import {
TbChartLine,
TbHelp,
TbLayoutList,
TbList,
TbListDetails,
TbMoon,
TbNotes,
TbPower,
TbSettings,
TbSun,
TbUsers,
TbWorldDownload,
} from "react-icons/tb"
import { useViewMode } from "../../hooks/useViewMode"
interface ProfileMenuProps {
control: React.ReactElement
@@ -40,6 +55,17 @@ const viewModeData: ViewModeControlItem[] = [
</Group>
),
},
{
value: "detailed",
label: (
<Group>
<TbListDetails size={iconSize} />
<Box ml={6}>
<Trans>Detailed</Trans>
</Box>
</Group>
),
},
{
value: "expanded",
label: (
@@ -55,7 +81,8 @@ const viewModeData: ViewModeControlItem[] = [
export function ProfileMenu(props: ProfileMenuProps) {
const [opened, setOpened] = useState(false)
const viewMode = useAppSelector(state => state.user.settings?.viewMode)
const { viewMode, setViewMode } = useViewMode()
const profile = useAppSelector(state => state.user.profile)
const admin = useAppSelector(state => state.user.profile?.admin)
const dispatch = useAppDispatch()
const { colorScheme, toggleColorScheme } = useMantineColorScheme()
@@ -69,6 +96,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
<Menu position="bottom-end" closeOnItemClick={false} opened={opened} onChange={setOpened}>
<Menu.Target>{props.control}</Menu.Target>
<Menu.Dropdown>
{profile && <Menu.Label>{profile.name}</Menu.Label>}
<Menu.Item
icon={<TbSettings size={iconSize} />}
onClick={() => {
@@ -78,6 +106,21 @@ export function ProfileMenu(props: ProfileMenuProps) {
>
<Trans>Settings</Trans>
</Menu.Item>
<Menu.Item
icon={<TbWorldDownload size={iconSize} />}
onClick={() =>
client.feed.refreshAll().then(() => {
showNotification({
message: t`Your feeds have been queued for refresh.`,
color: "green",
autoClose: 1000,
})
setOpened(false)
})
}
>
<Trans>Fetch all my feeds now</Trans>
</Menu.Item>
<Divider />
<Menu.Label>
@@ -96,7 +139,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
orientation="vertical"
data={viewModeData}
value={viewMode}
onChange={e => dispatch(changeViewMode(e as ViewMode))}
onChange={e => setViewMode(e as ViewMode)}
mb="xs"
/>

View File

@@ -22,7 +22,7 @@ export function ProfileSettings() {
const form = useForm<FormData>({
validate: {
newPasswordConfirmation: (value: string, values: FormData) => (value !== values.newPassword ? t`Passwords do not match` : null),
newPasswordConfirmation: (value, values) => (value !== values.newPassword ? t`Passwords do not match` : null),
},
})
const { setValues } = form

View File

@@ -1,4 +1,5 @@
import { Box, createStyles, Image } from "@mantine/core"
import { Box, createStyles } from "@mantine/core"
import { FeedFavicon } from "components/content/FeedFavicon"
import React, { ReactNode } from "react"
import { UnreadCount } from "./UnreadCount"
@@ -49,11 +50,7 @@ export function TreeNode(props: TreeNodeProps) {
return (
<Box py={1} pl={props.level * 20} className={classes.node} onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}>
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick && props.onIconClick(e, props.id)}>
{typeof props.icon === "string" ? (
<Image withPlaceholder src={props.icon} alt="favicon" width={18} height={18} />
) : (
props.icon
)}
{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}
</Box>
<Box className={classes.nodeText}>{props.name}</Box>
{!props.expanded && (

View File

@@ -1,9 +1,10 @@
import { t } from "@lingui/macro"
import { Box, Center, Image, Kbd, TextInput } from "@mantine/core"
import { Box, Center, Kbd, TextInput } from "@mantine/core"
import { openSpotlight, SpotlightAction, SpotlightProvider } from "@mantine/spotlight"
import { redirectToFeed } from "app/slices/redirect"
import { useAppDispatch } from "app/store"
import { Subscription } from "app/types"
import { FeedFavicon } from "components/content/FeedFavicon"
import { useMousetrap } from "hooks/useMousetrap"
import { TbSearch } from "react-icons/tb"
@@ -17,7 +18,7 @@ export function TreeSearch(props: TreeSearchProps) {
.sort((f1, f2) => f1.name.localeCompare(f2.name))
.map(f => ({
title: f.name,
icon: <Image withPlaceholder src={f.iconUrl} alt="favicon" width={18} height={18} />,
icon: <FeedFavicon url={f.iconUrl} />,
onTrigger: () => dispatch(redirectToFeed(f.id)),
}))

View File

@@ -0,0 +1,7 @@
import useLocalStorage from "use-local-storage"
import { ViewMode } from "../app/types"
export function useViewMode() {
const [viewMode, setViewMode] = useLocalStorage<ViewMode>("view-mode", "detailed")
return { viewMode, setViewMode }
}

View File

@@ -0,0 +1,24 @@
import { reloadTree } from "app/slices/tree"
import { useAppDispatch } from "app/store"
import { useEffect } from "react"
import WebsocketHeartbeatJs from "websocket-heartbeat-js"
export const useWebSocket = () => {
const dispatch = useAppDispatch()
useEffect(() => {
const currentUrl = new URL(window.location.href)
const wsProtocol = currentUrl.protocol === "http:" ? "ws" : "wss"
const wsUrl = `${wsProtocol}://${currentUrl.hostname}:${currentUrl.port}/ws`
const ws = new WebsocketHeartbeatJs({ url: wsUrl, pingMsg: "ping" })
ws.onmessage = event => {
const { data } = event
if (typeof data === "string") {
if (data.startsWith("new-feed-entries:")) dispatch(reloadTree())
}
}
return () => ws.close()
}, [dispatch])
}

View File

@@ -29,7 +29,37 @@ import "dayjs/locale/sk"
import "dayjs/locale/sv"
import "dayjs/locale/tr"
import "dayjs/locale/zh"
import { ar, ca, cs, cy, da, de, en, es, fa, fi, fr, gl, hu, id, it, ja, ko, ms, nb, nl, nn, pl, pt, ru, sk, sv, tr, zh } from "make-plural"
import {
ar,
ca,
cs,
cy,
da,
de,
en,
es,
fa,
fi,
fr,
gl,
hu,
id,
it,
ja,
ko,
ms,
nb,
nl,
nn,
pl,
PluralCategory,
pt,
ru,
sk,
sv,
tr,
zh,
} from "make-plural"
import { useEffect } from "react"
import { messages as arMessages } from "./locales/ar/messages"
import { messages as caMessages } from "./locales/ca/messages"
@@ -64,7 +94,7 @@ interface Locale {
key: string
label: string
messages: Messages
plurals?: (n: number | string, ord?: boolean) => string
plurals?: (n: number | string, ord?: boolean) => PluralCategory
}
// add an object to the array to add a new locale

View File

@@ -219,6 +219,10 @@ msgstr "حذف المستخدم"
msgid "Desc"
msgstr "تنازلي"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "موجز URL"
msgid "Feed name"
msgstr "اسم الخلاصة"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "تصفية التعبير"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "هل نسيت كلمة المرور؟"
@@ -320,6 +332,10 @@ msgstr "انتقل إلى وثائق API."
msgid "Go to the All view"
msgstr "اذهب إلى طريقة العرض \"الكل\""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "الأشياء الجيدة"
@@ -348,6 +364,7 @@ msgstr "استيراد"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "في العرض الموسع ، التمرير عبر الإدخالات وضع علامة عليها كمقروءة"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "إبقاء غير مقروءة"
@@ -404,6 +421,10 @@ msgstr "تسجيل الدخول"
msgid "Logout"
msgstr "تسجيل الخروج"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "تعليم الكل كمقروء"
msgid "Mark all entries as read"
msgstr "تعليم كافة الإدخالات كمقروءة"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "وضع علامة كمقروء"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "وضع علامة كمقروءة حتى هنا"
@@ -430,6 +453,10 @@ msgstr "وضع علامة كمقروءة حتى هنا"
msgid "Metrics"
msgstr "المقاييس"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "تحريك الصفحة لأسفل"
@@ -515,6 +542,14 @@ msgstr "فتح الإدخال الحالي في علامة تبويب جديدة
msgid "Open link"
msgstr "افتح الرابط"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "فتح الإدخال التالي"
@@ -581,6 +616,10 @@ msgstr "تحديث"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "تم إغلاق التسجيلات في مثيل CommaFeed هذا"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "مشاركة المواقع"
msgid "Shift"
msgstr "الحلقة"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "إظهار موجز ويب والفئات التي لا تحتوي على إدخالات غير مقروءة"
@@ -654,6 +701,7 @@ msgstr "شيء سيء حدث للتو ..."
msgid "Space"
msgstr "فضاء"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "النجم"
@@ -681,6 +729,10 @@ msgstr "الاشتراك في موجز ويب"
msgid "Success"
msgstr "النجاح"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "التبديل إلى النسق الداكن"
@@ -713,6 +765,7 @@ msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجر
msgid "Unread"
msgstr "غير مقروءة"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "إلغاء النجم"
@@ -743,6 +796,14 @@ msgstr "موقع الكتروني"
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 "ليس لديك أي اشتراكات حتى الآن. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "الملف مطلوب"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Suprimeix l'usuari"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL del canal"
msgid "Feed name"
msgstr "Nom del canal"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Expressió de filtratge"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Heu oblidat la contrasenya?"
@@ -320,6 +332,10 @@ msgstr "Vés a la documentació de l'API."
msgid "Go to the All view"
msgstr "Vés a la vista Tot"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Bones"
@@ -348,6 +364,7 @@ msgstr "Importació"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "a la vista ampliada, desplaçant-se per les entrades les marqueu com a llegides"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Mantenir sense llegir"
@@ -404,6 +421,10 @@ msgstr "Inicia sessió"
msgid "Logout"
msgstr "Tanca sessió"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Marca-ho tot com a llegit"
msgid "Mark all entries as read"
msgstr "Marqueu totes les entrades com a llegides"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Marca com a llegit"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Marca com a llegit fins aquí"
@@ -430,6 +453,10 @@ msgstr "Marca com a llegit fins aquí"
msgid "Metrics"
msgstr "mètriques"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Mou la pàgina cap avall"
@@ -515,6 +542,14 @@ msgstr "Obre l'entrada actual en una pestanya nova al fons"
msgid "Open link"
msgstr "Enllaç obert"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Obre la següent entrada"
@@ -581,6 +616,10 @@ msgstr "Actualitzar"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Els registres estan tancats en aquesta instància de CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Compartir llocs"
msgid "Shift"
msgstr "canvi"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Mostra feeds i categories sense entrades no llegides"
@@ -654,6 +701,7 @@ msgstr "Acaba de passar una cosa dolenta..."
msgid "Space"
msgstr "Espai"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Estrella"
@@ -681,6 +729,10 @@ msgstr "Subscriu-te al canal"
msgid "Success"
msgstr "Éxit"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Canvia al tema fosc"
@@ -713,6 +765,7 @@ msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"
msgid "Unread"
msgstr "Sense llegir"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Desestrellar"
@@ -743,6 +796,14 @@ msgstr "Lloc web"
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 "Encara no teniu cap subscripció. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "el fitxer és necessari"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Smazat uživatele"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL zdroje"
msgid "Feed name"
msgstr "Název zdroje"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Filtrování výrazu"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Zapomněli jste heslo?"
@@ -320,6 +332,10 @@ msgstr "Přejděte na dokumentaci API."
msgid "Go to the All view"
msgstr "Přejděte do zobrazení Vše"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Dobroty"
@@ -348,6 +364,7 @@ msgstr ""
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "V rozšířeném zobrazení je procházením označíte jako přečtené"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Ponechat nepřečtené"
@@ -404,6 +421,10 @@ msgstr "Přihlaste se"
msgid "Logout"
msgstr "Odhlášení"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Označit vše jako přečtené"
msgid "Mark all entries as read"
msgstr "Označte všechny položky jako přečtené"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Označit jako přečtené"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Označit jako přečtené až sem"
@@ -430,6 +453,10 @@ msgstr "Označit jako přečtené až sem"
msgid "Metrics"
msgstr "Metriky"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Přesuňte stránku dolů"
@@ -515,6 +542,14 @@ msgstr "Otevřít aktuální položku na nové kartě na pozadí"
msgid "Open link"
msgstr "Otevřít odkaz"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Otevřete další položku"
@@ -581,6 +616,10 @@ msgstr "Obnovit"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "V této instanci CommaFeed jsou registrace uzavřeny"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Stránky pro sdílení"
msgid "Shift"
msgstr "Směna"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Zobrazit kanály a kategorie bez nepřečtených položek"
@@ -654,6 +701,7 @@ msgstr "Právě se stalo něco špatného..."
msgid "Space"
msgstr "Vesmír"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Hvězda"
@@ -681,6 +729,10 @@ msgstr "Přihlaste se k odběru kanálu"
msgid "Success"
msgstr "Úspěch"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Přepněte na tmavý motiv"
@@ -713,6 +765,7 @@ msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo"
msgid "Unread"
msgstr "Nepřečteno"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Odstranit hvězdu"
@@ -743,6 +796,14 @@ msgstr "Webové stránky"
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 "Zatím nemáte žádné předplatné. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr ""
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Dileu defnyddiwr"
msgid "Desc"
msgstr "Rhag"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL porthiant"
msgid "Feed name"
msgstr "Enw porthiant"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Hidlo mynegiant"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Wedi anghofio cyfrinair?"
@@ -320,6 +332,10 @@ msgstr "Ewch i'r ddogfennaeth API."
msgid "Go to the All view"
msgstr "Ewch i'r golwg Pawb"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "nwyddau"
@@ -348,6 +364,7 @@ msgstr "Mewnforio"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "Mewn gwedd estynedig, mae sgrolio trwy gofnodion yn nodi eu bod wedi'u darllen"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Cadwch heb ei ddarllen"
@@ -404,6 +421,10 @@ msgstr "Mewngofnodi"
msgid "Logout"
msgstr "Allgofnodi"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Marciwch y cyfan wedi'i ddarllen"
msgid "Mark all entries as read"
msgstr "Marciwch bob cofnod wedi'i ddarllen"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Marciwch ei fod wedi'i ddarllen"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Marciwch fel y darllenwyd hyd yma"
@@ -430,6 +453,10 @@ msgstr "Marciwch fel y darllenwyd hyd yma"
msgid "Metrics"
msgstr "metrigau"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Symudwch y dudalen i lawr"
@@ -515,6 +542,14 @@ msgstr "Agorwch y cofnod cyfredol mewn tab newydd yn y cefndir"
msgid "Open link"
msgstr "Dolen agored"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Agor y cofnod nesaf"
@@ -581,6 +616,10 @@ msgstr "Adnewyddu"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Mae cofrestriadau ar gau ar yr achos CommaFeed hwn"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Rhannu gwefannau"
msgid "Shift"
msgstr "shifft"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Dangos ffrydiau a chategorïau heb unrhyw gofnodion heb eu darllen"
@@ -654,6 +701,7 @@ msgstr "Mae rhywbeth drwg newydd ddigwydd ..."
msgid "Space"
msgstr "Gofod"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "seren"
@@ -681,6 +729,10 @@ msgstr "Tanysgrifio i'r porthiant"
msgid "Success"
msgstr "Llwyddiant"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Newid i thema dywyll"
@@ -713,6 +765,7 @@ msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo"
msgid "Unread"
msgstr "Heb ei ddarllen"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "dad-seren"
@@ -743,6 +796,14 @@ msgstr "Gwefan"
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 "Nid oes gennych unrhyw danysgrifiadau eto. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "mae angen y ffeil"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Slet bruger"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr ""
msgid "Feed name"
msgstr "Feednavn"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Filtrerende udtryk"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Glemt adgangskode?"
@@ -320,6 +332,10 @@ msgstr "Gå til API-dokumentationen."
msgid "Go to the All view"
msgstr "Gå til visningen Alle"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Godbidder"
@@ -348,6 +364,7 @@ msgstr ""
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "I udvidet visning markerer du dem som læst, når du ruller gennem poster"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Forbehold ulæst"
@@ -404,6 +421,10 @@ msgstr "Log ind"
msgid "Logout"
msgstr "Log ud"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Marker alle som læst"
msgid "Mark all entries as read"
msgstr "Marker alle poster som læst"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Markér som læst"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Markér som læst indtil her"
@@ -430,6 +453,10 @@ msgstr "Markér som læst indtil her"
msgid "Metrics"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Flyt siden ned"
@@ -515,6 +542,14 @@ msgstr "Åbn den aktuelle post i en ny fane i baggrunden"
msgid "Open link"
msgstr "Åbent link"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Åbn næste post"
@@ -581,6 +616,10 @@ msgstr "Opdater"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Registreringer er lukket på denne CommaFeed-instans"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Delingssider"
msgid "Shift"
msgstr "Skift"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Vis feeds og kategorier uden ulæste poster"
@@ -654,6 +701,7 @@ msgstr "Der er lige sket noget slemt..."
msgid "Space"
msgstr "Rum"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Stjerne"
@@ -681,6 +729,10 @@ msgstr "Abonner på feedet"
msgid "Success"
msgstr "Succes"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Skift til mørkt tema"
@@ -713,6 +765,7 @@ msgstr "Prøv CommaFeed med demokontoen: demo/demo"
msgid "Unread"
msgstr "Ulæst"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr ""
@@ -743,6 +796,14 @@ msgstr "Hjemmeside"
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 "Du har ingen abonnementer endnu. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil er påkrævet"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Benutzer löschen"
msgid "Desc"
msgstr "Beschr"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "Feed-URL"
msgid "Feed name"
msgstr "Feedname"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Filterausdruck"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Passwort vergessen?"
@@ -320,6 +332,10 @@ msgstr "Gehen Sie zur API-Dokumentation."
msgid "Go to the All view"
msgstr "Zur Ansicht Alle wechseln"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Gutes"
@@ -348,6 +364,7 @@ msgstr "Importieren"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "In der erweiterten Ansicht werden Einträge beim Scrollen als gelesen markiert"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Ungelesen lassen"
@@ -404,6 +421,10 @@ msgstr "Einloggen"
msgid "Logout"
msgstr "Abmelden"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Alle als gelesen markieren"
msgid "Mark all entries as read"
msgstr "Alle Einträge als gelesen markieren"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Als gelesen markieren"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Bis hierhin als gelesen markieren"
@@ -430,6 +453,10 @@ msgstr "Bis hierhin als gelesen markieren"
msgid "Metrics"
msgstr "Metriken"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Seite nach unten verschieben"
@@ -515,6 +542,14 @@ msgstr "Aktuellen Eintrag in neuem Tab im Hintergrund öffnen"
msgid "Open link"
msgstr "Link öffnen"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Nächsten Eintrag öffnen"
@@ -581,6 +616,10 @@ msgstr "Aktualisieren"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Registrierungen sind für diese CommaFeed-Instanz geschlossen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Seiten teilen"
msgid "Shift"
msgstr "Verschiebung"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Feeds und Kategorien ohne ungelesene Einträge anzeigen"
@@ -654,6 +701,7 @@ msgstr "Etwas Schlimmes ist gerade passiert..."
msgid "Space"
msgstr "Raum"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Stern"
@@ -681,6 +729,10 @@ msgstr "Feed abonnieren"
msgid "Success"
msgstr "Erfolg"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Zum dunklen Design wechseln"
@@ -713,6 +765,7 @@ msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"
msgid "Unread"
msgstr "Ungelesen"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Stern entfernen"
@@ -743,6 +796,14 @@ msgstr "Webseite"
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 "Sie haben noch keine Abonnements. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "Datei ist erforderlich"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Delete user"
msgid "Desc"
msgstr "Desc"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr "Detailed"
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "Feed URL"
msgid "Feed name"
msgstr "Feed name"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr "Fetch all my feeds now"
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Filtering expression"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Forgot password?"
@@ -320,6 +332,10 @@ msgstr "Go to the API documentation."
msgid "Go to the All view"
msgstr "Go to the All view"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr "Go to {0}"
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Goodies"
@@ -348,6 +364,7 @@ msgstr "Import"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "In expanded view, scrolling through entries mark them as read"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Keep unread"
@@ -404,6 +421,10 @@ msgstr "Log in"
msgid "Logout"
msgstr "Logout"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr "Long press"
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Mark all as read"
msgid "Mark all entries as read"
msgstr "Mark all entries as read"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Mark as read"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Mark as read up to here"
@@ -430,6 +453,10 @@ msgstr "Mark as read up to here"
msgid "Metrics"
msgstr "Metrics"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr "Middle click"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Move the page down"
@@ -515,6 +542,14 @@ msgstr "Open current entry in a new tab in the background"
msgid "Open link"
msgstr "Open link"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr "Open link in new background tab"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr "Open link in new tab"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Open next entry"
@@ -581,6 +616,10 @@ msgstr "Refresh"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Registrations are closed on this CommaFeed instance"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr "Right click"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Sharing sites"
msgid "Shift"
msgstr "Shift"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr "Show entry menu (desktop)"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr "Show entry menu (mobile)"
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Show feeds and categories with no unread entries"
@@ -654,6 +701,7 @@ msgstr "Something bad just happened..."
msgid "Space"
msgstr "Space"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Star"
@@ -681,6 +729,10 @@ msgstr "Subscribe to the feed"
msgid "Success"
msgstr "Success"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr "Swipe header to the right"
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Switch to dark theme"
@@ -713,6 +765,7 @@ msgstr "Try out CommaFeed with the demo account: demo/demo"
msgid "Unread"
msgstr "Unread"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Unstar"
@@ -743,6 +796,14 @@ msgstr "Website"
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 "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr "Your feeds have been queued for refresh."
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "file is required"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr "{0} (in {1})"

View File

@@ -219,6 +219,10 @@ msgstr "Borrar usuario"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL de fuente"
msgid "Feed name"
msgstr "Nombre de alimentación"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Expresión de filtrado"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "¿Olvidaste la contraseña?"
@@ -320,6 +332,10 @@ msgstr "Ir a la documentación de la API."
msgid "Go to the All view"
msgstr "Ir a la vista Todo"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "golosinas"
@@ -348,6 +364,7 @@ msgstr "Importar"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "En la vista ampliada, al desplazarse por las entradas, márquelas como leídas"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Mantener sin leer"
@@ -404,6 +421,10 @@ msgstr "Iniciar sesión"
msgid "Logout"
msgstr "Salir"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Marcar todo como leído"
msgid "Mark all entries as read"
msgstr "Marcar todas las entradas como leídas"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Marcar como leído"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Marcar como leído hasta aquí"
@@ -430,6 +453,10 @@ msgstr "Marcar como leído hasta aquí"
msgid "Metrics"
msgstr "Métricas"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Mover la página hacia abajo"
@@ -515,6 +542,14 @@ msgstr "Abrir la entrada actual en una nueva pestaña en segundo plano"
msgid "Open link"
msgstr "Abrir enlace"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Abrir siguiente entrada"
@@ -581,6 +616,10 @@ msgstr "Actualizar"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Los registros están cerrados en esta instancia de CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Compartir sitios"
msgid "Shift"
msgstr "Cambio"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Mostrar feeds y categorías sin entradas no leídas"
@@ -654,6 +701,7 @@ msgstr "Algo malo acaba de pasar..."
msgid "Space"
msgstr "Espacio"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "estrella"
@@ -681,6 +729,10 @@ msgstr "Suscríbete a la fuente"
msgid "Success"
msgstr "Éxito"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Cambiar a tema oscuro"
@@ -713,6 +765,7 @@ msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo"
msgid "Unread"
msgstr "No leído"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Desmarcar"
@@ -743,6 +796,14 @@ msgstr "Sitio web"
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 "Todavía no tienes ninguna suscripción. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "archivo requerido"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "حذف کاربر"
msgid "Desc"
msgstr "توصیف"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL فید"
msgid "Feed name"
msgstr "نام فید"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "بیان فیلتر"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "رمز عبور را فراموش کرده اید؟"
@@ -320,6 +332,10 @@ msgstr "به مستندات API بروید."
msgid "Go to the All view"
msgstr "به نمای All بروید"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "خوبی ها"
@@ -348,6 +364,7 @@ msgstr "واردات"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "در نمای بازشده، پیمایش در ورودی‌ها، آنها را به عنوان خوانده شده علامت‌گذاری می‌کند"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "خوانده نشده نگه دارید"
@@ -404,6 +421,10 @@ msgstr "وارد شوید"
msgid "Logout"
msgstr "خروج"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "همه را به عنوان خوانده شده علامت گذاری ک
msgid "Mark all entries as read"
msgstr "همه ورودی ها را به عنوان خوانده شده علامت گذاری کنید"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "علامت گذاری به عنوان خوانده شده"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "تا اینجا به عنوان خوانده شده علامت بزنید"
@@ -430,6 +453,10 @@ msgstr "تا اینجا به عنوان خوانده شده علامت بزنی
msgid "Metrics"
msgstr "متریک"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "صفحه را به پایین ببرید"
@@ -515,6 +542,14 @@ msgstr "ورودی فعلی را در یک برگه جدید در پس زمین
msgid "Open link"
msgstr "پیوند را باز کنید"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "ورودی بعدی را باز کنید"
@@ -581,6 +616,10 @@ msgstr "تازه کردن"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "ثبت نام در این نمونه CommaFeed بسته شده است"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "اشتراک گذاری سایت ها"
msgid "Shift"
msgstr "شیفت"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "فیدها و دسته ها را بدون ورودی خوانده نشده نشان دهید"
@@ -654,6 +701,7 @@ msgstr "اتفاق بدی افتاد..."
msgid "Space"
msgstr "فضا"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "ستاره"
@@ -681,6 +729,10 @@ msgstr "در فید مشترک شوید"
msgid "Success"
msgstr "موفقیت"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "تغییر به تم تیره"
@@ -713,6 +765,7 @@ msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دم
msgid "Unread"
msgstr "خوانده نشده"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr ""
@@ -743,6 +796,14 @@ msgstr "وب سایت"
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 "شما هنوز هیچ اشتراکی ندارید. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "فایل مورد نیاز است"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Poista käyttäjä"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "Syötteen URL-osoite"
msgid "Feed name"
msgstr "Syötteen nimi"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Suodattava lauseke"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Unohditko salasanan?"
@@ -320,6 +332,10 @@ msgstr "Siirry API-dokumentaatioon."
msgid "Go to the All view"
msgstr "Siirry Kaikki-näkymään"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Hyvää"
@@ -348,6 +364,7 @@ msgstr "Tuo"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "Merkitse ne luetuiksi laajennetussa näkymässä vierittämällä merkintöjä"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Pidä lukematta"
@@ -404,6 +421,10 @@ msgstr "Kirjaudu sisään"
msgid "Logout"
msgstr "Uloskirjautuminen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Merkitse kaikki luetuiksi"
msgid "Mark all entries as read"
msgstr "Merkitse kaikki merkinnät luetuiksi"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Merkitse luetuksi"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Merkitse luetuksi tähän asti"
@@ -430,6 +453,10 @@ msgstr "Merkitse luetuksi tähän asti"
msgid "Metrics"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Siirrä sivua alaspäin"
@@ -515,6 +542,14 @@ msgstr "Avaa nykyinen merkintä uudella välilehdellä taustalla"
msgid "Open link"
msgstr "Avaa linkki"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Avaa seuraava merkintä"
@@ -581,6 +616,10 @@ msgstr "Päivitä"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Tämän CommaFeed-esiintymän rekisteröinnit on suljettu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Sivustojen jakaminen"
msgid "Shift"
msgstr "Vaihto"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Näytä syötteet ja luokat ilman lukemattomia merkintöjä"
@@ -654,6 +701,7 @@ msgstr "Jotain pahaa tapahtui juuri..."
msgid "Space"
msgstr "Avaruus"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Tähti"
@@ -681,6 +729,10 @@ msgstr "Tilaa syöte"
msgid "Success"
msgstr "Onnistui"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Vaihda tummaan teemaan"
@@ -713,6 +765,7 @@ msgstr "Kokeile CommaFeediä demotilillä: demo/demo"
msgid "Unread"
msgstr "Lukematon"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Poista tähti"
@@ -743,6 +796,14 @@ msgstr "Verkkosivusto"
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 "Sinulla ei ole vielä tilauksia. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "tiedosto vaaditaan"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Effacer l'utilisateur"
msgid "Desc"
msgstr "Descendant"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr "Vue détaillée"
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL du flux"
msgid "Feed name"
msgstr "Nom du flux"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Expression de filtrage"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Mot de passe oublié ?"
@@ -320,6 +332,10 @@ msgstr "Aller à la documentation de l'API."
msgid "Go to the All view"
msgstr "Aller à la catégorie Tout"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Extensions"
@@ -348,6 +364,7 @@ msgstr "Importer"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "En mode de lecture étendu, marquer les éléments comme lus lorsque la fenêtre descend."
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Garder non lu"
@@ -404,6 +421,10 @@ msgstr "Connexion"
msgid "Logout"
msgstr "Déconnexion"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Tout marquer comme lu"
msgid "Mark all entries as read"
msgstr "Marquer toutes les entrées comme lues"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Marquer comme lu"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Marquer comme lu jusqu'ici"
@@ -430,6 +453,10 @@ msgstr "Marquer comme lu jusqu'ici"
msgid "Metrics"
msgstr "Métriques"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Faites défiler la page vers le bas"
@@ -515,6 +542,14 @@ msgstr "Ouvrir l'entrée actuelle dans un nouvel onglet en arrière-plan"
msgid "Open link"
msgstr "Ouvrir le lien"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Ouvrir l'entrée suivante"
@@ -581,6 +616,10 @@ msgstr "Rafraîchir"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Les inscriptions sont fermées sur cette instance de CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Sites de partage"
msgid "Shift"
msgstr "Maj"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Afficher les flux et les catégories pour lesquels tout est déjà lu"
@@ -654,6 +701,7 @@ msgstr "Quelque chose s'est mal passé..."
msgid "Space"
msgstr "Espace"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Ajouter aux favoris"
@@ -681,6 +729,10 @@ msgstr "S'abonner au flux"
msgid "Success"
msgstr "Succès"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr "Faire glisser le titre vers la droite"
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Activer le mode sombre"
@@ -713,6 +765,7 @@ msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo"
msgid "Unread"
msgstr "Non lu"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Retirer des favoris"
@@ -743,6 +796,14 @@ msgstr "Site web"
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 "Vous n'avez pas encore d'abonnements. Pourquoi ne pas essayer d'en ajouter un en cliquant sur le signe + en haut de la page ?"
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fichier requis"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Eliminar usuario"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL da fonte"
msgid "Feed name"
msgstr "Nome do feed"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Expresión de filtrado"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Esqueceches o contrasinal?"
@@ -320,6 +332,10 @@ msgstr "Ir á documentación da API."
msgid "Go to the All view"
msgstr "Ir á vista Todos"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "agasallos"
@@ -348,6 +364,7 @@ msgstr "Importación"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "Na vista ampliada, ao desprazarse polas entradas márcaas como lidas"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Manter sen ler"
@@ -404,6 +421,10 @@ msgstr "Iniciar sesión"
msgid "Logout"
msgstr "Pechar sesión"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Marcar todo como lido"
msgid "Mark all entries as read"
msgstr "Marcar todas as entradas como lidas"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Marcar como lido"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Marcar como lido ata aquí"
@@ -430,6 +453,10 @@ msgstr "Marcar como lido ata aquí"
msgid "Metrics"
msgstr "Métricas"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Move a páxina cara abaixo"
@@ -515,6 +542,14 @@ msgstr "Abre a entrada actual nunha nova pestana en segundo plano"
msgid "Open link"
msgstr "ligazón aberta"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Abrir a seguinte entrada"
@@ -581,6 +616,10 @@ msgstr "Actualizar"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Os rexistros están pechados nesta instancia de CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Compartir sitios"
msgid "Shift"
msgstr "quendas"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Mostrar fontes e categorías sen entradas sen ler"
@@ -654,6 +701,7 @@ msgstr "Algo malo pasou..."
msgid "Space"
msgstr "Espazo"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "estrela"
@@ -681,6 +729,10 @@ msgstr "Subscríbete ao feed"
msgid "Success"
msgstr "Éxito"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Cambiar ao tema escuro"
@@ -713,6 +765,7 @@ msgstr "Proba CommaFeed coa conta de demostración: demo/demo"
msgid "Unread"
msgstr "Sen ler"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Desestrela"
@@ -743,6 +796,14 @@ msgstr "Páxina web"
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 "Aínda non tes ningunha subscrición. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "é necesario o ficheiro"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Felhasználó törlése"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr ""
msgid "Feed name"
msgstr "Hírcsatorna neve"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Szűrő kifejezés"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Elfelejtette a jelszavát?"
@@ -320,6 +332,10 @@ msgstr "Nyissa meg az API dokumentációját."
msgid "Go to the All view"
msgstr "Lépjen az Összes nézetre"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Jók"
@@ -348,6 +364,7 @@ msgstr "Importálás"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "Kibontott nézetben a bejegyzések görgetése olvasottként jelöli meg őket"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Olvasatlan marad"
@@ -404,6 +421,10 @@ msgstr "Jelentkezzen be"
msgid "Logout"
msgstr "Kijelentkezés"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Minden megjelölése olvasottként"
msgid "Mark all entries as read"
msgstr "Minden bejegyzés megjelölése olvasottként"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Megjelölés olvasottként"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Megjelölés idáig olvasottként"
@@ -430,6 +453,10 @@ msgstr "Megjelölés idáig olvasottként"
msgid "Metrics"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Mozgassa le az oldalt"
@@ -515,6 +542,14 @@ msgstr "Az aktuális bejegyzés megnyitása egy új lapon a háttérben"
msgid "Open link"
msgstr "Link megnyitása"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Következő bejegyzés megnyitása"
@@ -581,6 +616,10 @@ msgstr "Frissítés"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "A regisztrációk le vannak zárva ezen a CommaFeed példányon"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Webhelyek megosztása"
msgid "Shift"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Hírcsatornák és kategóriák megjelenítése olvasatlan bejegyzések nélkül"
@@ -654,6 +701,7 @@ msgstr "Valami rossz történt..."
msgid "Space"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Csillag"
@@ -681,6 +729,10 @@ msgstr "Feliratkozás a hírfolyamra"
msgid "Success"
msgstr "Siker"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Váltás sötét témára"
@@ -713,6 +765,7 @@ msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo"
msgid "Unread"
msgstr "Olvasatlan"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr ""
@@ -743,6 +796,14 @@ msgstr "Webhely"
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 "Még nincs előfizetése. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fájl szükséges"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Hapus pengguna"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL Umpan"
msgid "Feed name"
msgstr "Nama umpan"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Memfilter ekspresi"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Lupa kata sandi?"
@@ -320,6 +332,10 @@ msgstr "Buka dokumentasi API."
msgid "Go to the All view"
msgstr "Pergi ke tampilan Semua"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Pernak-pernik"
@@ -348,6 +364,7 @@ msgstr "Impor"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "Dalam tampilan yang diperluas, menggulir entri menandainya sebagai telah dibaca"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Tetap belum dibaca"
@@ -404,6 +421,10 @@ msgstr "Masuk"
msgid "Logout"
msgstr "Keluar"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Tandai semua sebagai telah dibaca"
msgid "Mark all entries as read"
msgstr "Tandai semua entri sebagai telah dibaca"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Tandai sebagai telah dibaca"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Tandai sebagai telah dibaca sampai di sini"
@@ -430,6 +453,10 @@ msgstr "Tandai sebagai telah dibaca sampai di sini"
msgid "Metrics"
msgstr "Metrik"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Pindahkan halaman ke bawah"
@@ -515,6 +542,14 @@ msgstr "Buka entri saat ini di tab baru di latar belakang"
msgid "Open link"
msgstr "Buka tautan"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Buka entri berikutnya"
@@ -581,6 +616,10 @@ msgstr "Segarkan"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Pendaftaran ditutup pada instans CommaFeed ini"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Berbagi situs"
msgid "Shift"
msgstr "Pergeseran"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Tampilkan umpan dan kategori tanpa entri yang belum dibaca"
@@ -654,6 +701,7 @@ msgstr "Sesuatu yang buruk baru saja terjadi..."
msgid "Space"
msgstr "Luar Angkasa"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Bintang"
@@ -681,6 +729,10 @@ msgstr "Berlangganan umpan"
msgid "Success"
msgstr "Sukses"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Beralih ke tema gelap"
@@ -713,6 +765,7 @@ msgstr "Cobalah CommaFeed dengan akun demo: demo/demo"
msgid "Unread"
msgstr "Belum Dibaca"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Hapus bintang"
@@ -743,6 +796,14 @@ msgstr "Situs Web"
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 "Anda belum memiliki langganan. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "file diperlukan"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Elimina utente"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL feed"
msgid "Feed name"
msgstr "Nome del feed"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Espressione filtrante"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Password dimenticata?"
@@ -320,6 +332,10 @@ msgstr "Vai alla documentazione dell'API."
msgid "Go to the All view"
msgstr "Vai alla vista Tutto"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Chicche"
@@ -348,6 +364,7 @@ msgstr "Importa"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "Nella vista espansa, scorrendo le voci contrassegnale come lette"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Mantieni non letto"
@@ -404,6 +421,10 @@ msgstr "Accedi"
msgid "Logout"
msgstr "Disconnessione"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Contrassegna tutto come letto"
msgid "Mark all entries as read"
msgstr "Contrassegna tutte le voci come lette"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Contrassegna come letto"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Contrassegna come letto fino a qui"
@@ -430,6 +453,10 @@ msgstr "Contrassegna come letto fino a qui"
msgid "Metrics"
msgstr "Metriche"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Sposta la pagina in basso"
@@ -515,6 +542,14 @@ msgstr "Apri la voce corrente in una nuova scheda in background"
msgid "Open link"
msgstr "Apri collegamento"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Apri voce successiva"
@@ -581,6 +616,10 @@ msgstr "Aggiorna"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Le registrazioni sono chiuse su questa istanza CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Condivisione di siti"
msgid "Shift"
msgstr "Cambio"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Mostra feed e categorie senza voci non lette"
@@ -654,6 +701,7 @@ msgstr "È appena successo qualcosa di brutto..."
msgid "Space"
msgstr "Spazio"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Stella"
@@ -681,6 +729,10 @@ msgstr "Iscriviti al feed"
msgid "Success"
msgstr "Successo"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Passa al tema scuro"
@@ -713,6 +765,7 @@ msgstr "Prova CommaFeed con il conto demo: demo/demo"
msgid "Unread"
msgstr "Non letto"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Elimina le stelle"
@@ -743,6 +796,14 @@ msgstr "Sito web"
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 "Non hai ancora abbonamenti. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "è richiesto il file"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "ユーザーの削除"
msgid "Desc"
msgstr "説明"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "フィード URL"
msgid "Feed name"
msgstr "フィード名"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "フィルタリング式"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "パスワードをお忘れですか?"
@@ -320,6 +332,10 @@ msgstr "API ドキュメントに移動します。"
msgid "Go to the All view"
msgstr "すべてのビューに移動"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "グッディーズ"
@@ -348,6 +364,7 @@ msgstr "インポート"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "展開ビューでエントリをスクロールすると、それらが既読としてマークされます"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "未読のままにする"
@@ -404,6 +421,10 @@ msgstr "ログイン"
msgid "Logout"
msgstr "ログアウト"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "すべて既読にする"
msgid "Mark all entries as read"
msgstr "すべてのエントリを既読にする"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "既読にする"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "ここまで既読にする"
@@ -430,6 +453,10 @@ msgstr "ここまで既読にする"
msgid "Metrics"
msgstr "メトリクス"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "ページを下に移動"
@@ -515,6 +542,14 @@ msgstr "現在のエントリをバックグラウンドで新しいタブで開
msgid "Open link"
msgstr "リンクを開く"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "次のエントリを開く"
@@ -581,6 +616,10 @@ msgstr "リフレッシュ"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "この CommaFeed インスタンスの登録は終了しています"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "共有サイト"
msgid "Shift"
msgstr "シフト"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "未読エントリのないフィードとカテゴリを表示する"
@@ -654,6 +701,7 @@ msgstr "何か悪いことが起こった..."
msgid "Space"
msgstr "スペース"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "スター"
@@ -681,6 +729,10 @@ msgstr "フィードを購読する"
msgid "Success"
msgstr "成功"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "ダークテーマに切り替え"
@@ -713,6 +765,7 @@ msgstr "デモアカウントで CommaFeed を試す: demo/demo"
msgid "Unread"
msgstr "未読"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "スターを外す"
@@ -743,6 +796,14 @@ msgstr "ウェブサイト"
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 "まだサブスクリプションがありません。"
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "ファイルが必要です"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "사용자 삭제"
msgid "Desc"
msgstr "설명"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "피드 URL"
msgid "Feed name"
msgstr "피드 이름"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "필터링 표현식"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "비밀번호를 잊으셨나요?"
@@ -320,6 +332,10 @@ msgstr "API 문서로 이동합니다."
msgid "Go to the All view"
msgstr "전체 보기로 이동"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "굿즈"
@@ -348,6 +364,7 @@ msgstr "가져오기"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "확장 보기에서 항목을 스크롤하면 읽은 것으로 표시됩니다."
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "읽지 않은 상태로 유지"
@@ -404,6 +421,10 @@ msgstr "로그인"
msgid "Logout"
msgstr "로그아웃"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "모두 읽은 상태로 표시"
msgid "Mark all entries as read"
msgstr "모든 항목을 읽은 상태로 표시"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "읽은 상태로 표시"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "여기까지 읽은 것으로 표시"
@@ -430,6 +453,10 @@ msgstr "여기까지 읽은 것으로 표시"
msgid "Metrics"
msgstr "메트릭스"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "페이지를 아래로 이동"
@@ -515,6 +542,14 @@ msgstr "배경의 새 탭에서 현재 항목 열기"
msgid "Open link"
msgstr "링크 열기"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "다음 항목 열기"
@@ -581,6 +616,10 @@ msgstr "새로 고침"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "이 CommaFeed 인스턴스에 대한 등록이 마감되었습니다."
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "사이트 공유"
msgid "Shift"
msgstr "시프트"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "읽지 않은 항목이 없는 피드 및 카테고리 표시"
@@ -654,6 +701,7 @@ msgstr "뭔가 안 좋은 일이 일어났어..."
msgid "Space"
msgstr "우주"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "스타"
@@ -681,6 +729,10 @@ msgstr "피드 구독"
msgid "Success"
msgstr "성공"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "어두운 테마로 전환"
@@ -713,6 +765,7 @@ msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo"
msgid "Unread"
msgstr "읽지 않음"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "별표 제거"
@@ -743,6 +796,14 @@ msgstr "웹사이트"
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 "아직 구독이 없습니다. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "파일이 필요합니다"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Padam pengguna"
msgid "Desc"
msgstr "Dec"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL Suapan"
msgid "Feed name"
msgstr "Nama suapan"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Ungkapan penapisan"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Lupa kata laluan?"
@@ -320,6 +332,10 @@ msgstr "Pergi ke dokumentasi API."
msgid "Go to the All view"
msgstr "Pergi ke paparan Semua"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr ""
@@ -348,6 +364,7 @@ msgstr ""
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "Dalam paparan yang diperluas, menatal melalui entri menandakannya sebagai dibaca"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Teruskan tidak dibaca"
@@ -404,6 +421,10 @@ msgstr "Log masuk"
msgid "Logout"
msgstr "Log Keluar"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Tandai semua sebagai dibaca"
msgid "Mark all entries as read"
msgstr "Tandai semua entri sebagai dibaca"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Tandakan sebagai dibaca"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Tandai sebagai dibaca sehingga di sini"
@@ -430,6 +453,10 @@ msgstr "Tandai sebagai dibaca sehingga di sini"
msgid "Metrics"
msgstr "Metrik"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Gerakkan halaman ke bawah"
@@ -515,6 +542,14 @@ msgstr "Buka entri semasa dalam tab baharu di latar belakang"
msgid "Open link"
msgstr "Buka pautan"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Buka entri seterusnya"
@@ -581,6 +616,10 @@ msgstr "Muat semula"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Pendaftaran ditutup pada contoh CommaFeed ini"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Berkongsi tapak"
msgid "Shift"
msgstr "Anjakan"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Tunjukkan suapan dan kategori tanpa entri yang belum dibaca"
@@ -654,6 +701,7 @@ msgstr "Sesuatu yang buruk baru saja berlaku..."
msgid "Space"
msgstr "Angkasa"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Bintang"
@@ -681,6 +729,10 @@ msgstr "Langgan suapan"
msgid "Success"
msgstr "Kejayaan"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Tukar kepada tema gelap"
@@ -713,6 +765,7 @@ msgstr "Cuba CommaFeed dengan akaun demo: demo/demo"
msgid "Unread"
msgstr "Belum dibaca"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Nyahbintang"
@@ -743,6 +796,14 @@ msgstr "Laman web"
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 "Anda belum mempunyai sebarang langganan lagi. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fail diperlukan"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Slett bruker"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "Feed-URL"
msgid "Feed name"
msgstr "Feednavn"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Filtrerende uttrykk"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Glemt passord?"
@@ -320,6 +332,10 @@ msgstr "Gå til API-dokumentasjonen."
msgid "Go to the All view"
msgstr "Gå til visningen Alle"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Godbiter"
@@ -348,6 +364,7 @@ msgstr ""
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "I utvidet visning merker du dem som lest ved å rulle gjennom oppføringer"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Behold ulest"
@@ -404,6 +421,10 @@ msgstr "Logg inn"
msgid "Logout"
msgstr "Logg ut"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Merk alle som lest"
msgid "Mark all entries as read"
msgstr "Merk alle oppføringer som lest"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Merk som lest"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Merk som lest frem til her"
@@ -430,6 +453,10 @@ msgstr "Merk som lest frem til her"
msgid "Metrics"
msgstr "Beregninger"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Flytt siden ned"
@@ -515,6 +542,14 @@ msgstr "Åpne gjeldende oppføring i en ny fane i bakgrunnen"
msgid "Open link"
msgstr "Åpen lenke"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Åpne neste oppføring"
@@ -581,6 +616,10 @@ msgstr "Oppdater"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Registreringer er stengt på denne CommaFeed-forekomsten"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Delingssider"
msgid "Shift"
msgstr "Skift"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Vis feeder og kategorier uten uleste oppføringer"
@@ -654,6 +701,7 @@ msgstr "Noe ille skjedde akkurat..."
msgid "Space"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Stjerne"
@@ -681,6 +729,10 @@ msgstr "Abonner på feeden"
msgid "Success"
msgstr "Suksess"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Bytt til mørkt tema"
@@ -713,6 +765,7 @@ msgstr "Prøv CommaFeed med demokontoen: demo/demo"
msgid "Unread"
msgstr "Ulest"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Fjern stjerne"
@@ -743,6 +796,14 @@ msgstr "Nettsted"
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 "Du har ingen abonnementer ennå. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil kreves"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Gebruiker verwijderen"
msgid "Desc"
msgstr "Beschrijving"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "Feed-URL"
msgid "Feed name"
msgstr "Feednaam"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Uitdrukking filteren"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Wachtwoord vergeten?"
@@ -320,6 +332,10 @@ msgstr "Ga naar de API-documentatie."
msgid "Go to the All view"
msgstr "Ga naar de weergave Alles"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Goederen"
@@ -348,6 +364,7 @@ msgstr ""
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "In de uitgevouwen weergave markeert het scrollen door items ze als gelezen"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Ongelezen houden"
@@ -404,6 +421,10 @@ msgstr "Inloggen"
msgid "Logout"
msgstr "Uitloggen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Alles markeren als gelezen"
msgid "Mark all entries as read"
msgstr "Markeer alle vermeldingen als gelezen"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Markeren als gelezen"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Markeer als gelezen tot hier"
@@ -430,6 +453,10 @@ msgstr "Markeer als gelezen tot hier"
msgid "Metrics"
msgstr "Metrieken"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Verplaats de pagina naar beneden"
@@ -515,6 +542,14 @@ msgstr "Open huidig item in een nieuw tabblad op de achtergrond"
msgid "Open link"
msgstr "Link openen"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Volgende invoer openen"
@@ -581,6 +616,10 @@ msgstr "Vernieuwen"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Registraties zijn gesloten op deze CommaFeed-instantie"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Sites delen"
msgid "Shift"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Toon feeds en categorieën zonder ongelezen items"
@@ -654,6 +701,7 @@ msgstr "Er is net iets ergs gebeurd..."
msgid "Space"
msgstr "Ruimte"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Ster"
@@ -681,6 +729,10 @@ msgstr "Abonneer je op de feed"
msgid "Success"
msgstr "Succes"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Overschakelen naar donker thema"
@@ -713,6 +765,7 @@ msgstr "Probeer CommaFeed uit met het demo-account: demo/demo"
msgid "Unread"
msgstr "Ongelezen"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Sterren uit"
@@ -743,6 +796,14 @@ msgstr ""
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 "Je hebt nog geen abonnementen. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "bestand is vereist"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Slett bruker"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "Feed-URL"
msgid "Feed name"
msgstr "Feednavn"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Filtrerende uttrykk"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Glemt passord?"
@@ -320,6 +332,10 @@ msgstr "Gå til API-dokumentasjonen."
msgid "Go to the All view"
msgstr "Gå til visningen Alle"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Godbiter"
@@ -348,6 +364,7 @@ msgstr ""
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "I utvidet visning merker du dem som lest ved å rulle gjennom oppføringer"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Behold ulest"
@@ -404,6 +421,10 @@ msgstr "Logg inn"
msgid "Logout"
msgstr "Logg ut"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Merk alle som lest"
msgid "Mark all entries as read"
msgstr "Merk alle oppføringer som lest"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Merk som lest"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Merk som lest frem til her"
@@ -430,6 +453,10 @@ msgstr "Merk som lest frem til her"
msgid "Metrics"
msgstr "Beregninger"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Flytt siden ned"
@@ -515,6 +542,14 @@ msgstr "Åpne gjeldende oppføring i en ny fane i bakgrunnen"
msgid "Open link"
msgstr "Åpen lenke"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Åpne neste oppføring"
@@ -581,6 +616,10 @@ msgstr "Oppdater"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Registreringer er stengt på denne CommaFeed-forekomsten"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Delingssider"
msgid "Shift"
msgstr "Skift"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Vis feeder og kategorier uten uleste oppføringer"
@@ -654,6 +701,7 @@ msgstr "Noe ille skjedde akkurat..."
msgid "Space"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Stjerne"
@@ -681,6 +729,10 @@ msgstr "Abonner på feeden"
msgid "Success"
msgstr "Suksess"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Bytt til mørkt tema"
@@ -713,6 +765,7 @@ msgstr "Prøv CommaFeed med demokontoen: demo/demo"
msgid "Unread"
msgstr "Ulest"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Fjern stjerne"
@@ -743,6 +796,14 @@ msgstr "Nettsted"
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 "Du har ingen abonnementer ennå. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil kreves"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Usuń użytkownika"
msgid "Desc"
msgstr "Opis"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL kanału"
msgid "Feed name"
msgstr "nazwa kanału"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Wyrażenie filtrujące"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Zapomniałeś hasła?"
@@ -320,6 +332,10 @@ msgstr "Przejdź do dokumentacji API."
msgid "Go to the All view"
msgstr "Przejdź do widoku Wszystkie"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Gadżety"
@@ -348,6 +364,7 @@ msgstr ""
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "W widoku rozszerzonym przewijanie wpisów oznacza je jako przeczytane"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Nie przeczytaj"
@@ -404,6 +421,10 @@ msgstr "Zaloguj się"
msgid "Logout"
msgstr "Wyloguj"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Oznacz wszystko jako przeczytane"
msgid "Mark all entries as read"
msgstr "Oznacz wszystkie wpisy jako przeczytane"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Oznacz jako przeczytane"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Oznacz jako przeczytane do tej pory"
@@ -430,6 +453,10 @@ msgstr "Oznacz jako przeczytane do tej pory"
msgid "Metrics"
msgstr "Metryki"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Przesuń stronę w dół"
@@ -515,6 +542,14 @@ msgstr "Otwórz bieżący wpis w nowej karcie w tle"
msgid "Open link"
msgstr "Otwórz link"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Otwórz następny wpis"
@@ -581,6 +616,10 @@ msgstr "Odśwież"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Rejestracje są zamknięte w tej instancji CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Udostępnianie witryn"
msgid "Shift"
msgstr "zmiana"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Pokaż kanały i kategorie bez nieprzeczytanych wpisów"
@@ -654,6 +701,7 @@ msgstr "Coś złego właśnie się stało..."
msgid "Space"
msgstr "Przestrzeń"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Gwiazda"
@@ -681,6 +729,10 @@ msgstr "Subskrybuj kanał"
msgid "Success"
msgstr "Sukces"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Przełącz na ciemny motyw"
@@ -713,6 +765,7 @@ msgstr "Wypróbuj CommaFeed z kontem demo: demo/demo"
msgid "Unread"
msgstr "Nieprzeczytane"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr ""
@@ -743,6 +796,14 @@ msgstr "Strona internetowa"
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 "Nie masz jeszcze żadnych subskrypcji. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "plik jest wymagany"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Excluir usuário"
msgid "Desc"
msgstr "Descrição"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL do feed"
msgid "Feed name"
msgstr "Nome do feed"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Filtrando expressão"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Esqueceu a senha?"
@@ -320,6 +332,10 @@ msgstr "Vá para a documentação da API."
msgid "Go to the All view"
msgstr "Ir para a visualização Tudo"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Brindes"
@@ -348,6 +364,7 @@ msgstr "Importar"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "Na visualização expandida, rolar pelas entradas marca-as como lidas"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Manter não lido"
@@ -404,6 +421,10 @@ msgstr "Entrar"
msgid "Logout"
msgstr "Sair"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Marcar todos como lidos"
msgid "Mark all entries as read"
msgstr "Marcar todas as entradas como lidas"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Marcar como lido"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Marcar como lido até aqui"
@@ -430,6 +453,10 @@ msgstr "Marcar como lido até aqui"
msgid "Metrics"
msgstr "Métricas"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Mova a página para baixo"
@@ -515,6 +542,14 @@ msgstr "Abrir a entrada atual em uma nova aba em segundo plano"
msgid "Open link"
msgstr "Abrir link"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Abrir próxima entrada"
@@ -581,6 +616,10 @@ msgstr "Atualizar"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Os registros estão fechados nesta instância do CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Compartilhando sites"
msgid "Shift"
msgstr "Mudar"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Mostrar feeds e categorias sem entradas não lidas"
@@ -654,6 +701,7 @@ msgstr "Algo ruim acabou de acontecer..."
msgid "Space"
msgstr "Espaço"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Estrela"
@@ -681,6 +729,10 @@ msgstr "Inscrever-se no feed"
msgid "Success"
msgstr "Sucesso"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Mudar para tema escuro"
@@ -713,6 +765,7 @@ msgstr "Experimente o CommaFeed com a conta demo: demo/demo"
msgid "Unread"
msgstr "Não lido"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Desestrelar"
@@ -743,6 +796,14 @@ msgstr "Site"
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 "Você ainda não tem nenhuma assinatura. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "o arquivo é obrigatório"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Удалить пользователя"
msgid "Desc"
msgstr "По убыванию"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL-адрес фида"
msgid "Feed name"
msgstr "Имя фида"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Выражение фильтрации"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Забыли пароль?"
@@ -320,6 +332,10 @@ msgstr "Перейдите к документации по API."
msgid "Go to the All view"
msgstr "Перейти к представлению «Все»"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Сладости"
@@ -348,6 +364,7 @@ msgstr "Импорт"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "В развернутом виде прокрутка записей помечает их как прочитанные."
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Не читать"
@@ -404,6 +421,10 @@ msgstr "Войти"
msgid "Logout"
msgstr "Выйти"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Отметить все как прочитанное"
msgid "Mark all entries as read"
msgstr "Отметить все записи как прочитанные"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Отметить как прочитанное"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Отметить как прочитанное до этого места"
@@ -430,6 +453,10 @@ msgstr "Отметить как прочитанное до этого мест
msgid "Metrics"
msgstr "Метрики"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Переместить страницу вниз"
@@ -515,6 +542,14 @@ msgstr "Открыть текущую запись в новой вкладке
msgid "Open link"
msgstr "Открыть ссылку"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Открыть следующую запись"
@@ -581,6 +616,10 @@ msgstr "Обновить"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Регистрация закрыта для этого экземпляра CommaFeed."
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Обмен сайтами"
msgid "Shift"
msgstr "Сдвиг"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Показать каналы и категории без непрочитанных записей"
@@ -654,6 +701,7 @@ msgstr "Только что случилось что-то плохое..."
msgid "Space"
msgstr "Пробел"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Звезда"
@@ -681,6 +729,10 @@ msgstr "Подписаться на ленту"
msgid "Success"
msgstr "Успех"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Переключиться на темную тему"
@@ -713,6 +765,7 @@ msgstr "Попробуйте CommaFeed на демо-счете: demo/demo"
msgid "Unread"
msgstr "непрочитано"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Снять пометку"
@@ -743,6 +796,14 @@ msgstr "Веб-сайт"
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 "У вас пока нет подписок. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "требуется файл"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Vymažte používateľa"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "URL informačného kanála"
msgid "Feed name"
msgstr "Názov informačného kanála"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Filtrovanie výrazu"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Zabudli ste heslo?"
@@ -320,6 +332,10 @@ msgstr "Prejdite na dokumentáciu API."
msgid "Go to the All view"
msgstr "Prejdite na zobrazenie Všetky"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Dobrôtky"
@@ -348,6 +364,7 @@ msgstr ""
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "V rozšírenom zobrazení ich rolovanie cez položky označí ako prečítané"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Ponechať neprečítané"
@@ -404,6 +421,10 @@ msgstr "Prihláste sa"
msgid "Logout"
msgstr "Odhlásenie"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Označiť všetko ako prečítané"
msgid "Mark all entries as read"
msgstr "Označte všetky položky ako prečítané"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Označiť ako prečítané"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Označiť ako prečítané až sem"
@@ -430,6 +453,10 @@ msgstr "Označiť ako prečítané až sem"
msgid "Metrics"
msgstr "Metriky"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Posuňte stránku nadol"
@@ -515,6 +542,14 @@ msgstr "Otvorte aktuálny záznam na novej karte na pozadí"
msgid "Open link"
msgstr "Otvoriť odkaz"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Otvor ďalší záznam"
@@ -581,6 +616,10 @@ msgstr "Obnoviť"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "V tejto inštancii CommaFeed sú registrácie uzavreté"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Zdieľanie stránok"
msgid "Shift"
msgstr "Smena"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Zobraziť kanály a kategórie bez neprečítaných záznamov"
@@ -654,6 +701,7 @@ msgstr "Práve sa stalo niečo zlé..."
msgid "Space"
msgstr "Vesmír"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Hviezda"
@@ -681,6 +729,10 @@ msgstr "Prihláste sa na odber kanála"
msgid "Success"
msgstr "Úspech"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Prepnúť na tmavú tému"
@@ -713,6 +765,7 @@ msgstr "Vyskúšajte CommaFeed s demo účtom: demo/demo"
msgid "Unread"
msgstr "Neprečítané"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Odobrať hviezdičku"
@@ -743,6 +796,14 @@ msgstr "Webová stránka"
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 "Zatiaľ nemáte žiadne odbery. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr ""
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Ta bort användare"
msgid "Desc"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "Flödes-URL"
msgid "Feed name"
msgstr "Flödesnamn"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Filtrerande uttryck"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Glömt lösenord?"
@@ -320,6 +332,10 @@ msgstr "Gå till API-dokumentationen."
msgid "Go to the All view"
msgstr "Gå till vyn Alla"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Godsaker"
@@ -348,6 +364,7 @@ msgstr ""
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "I utökad vy, rullning genom poster markerar dem som lästa"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Behåll oläst"
@@ -404,6 +421,10 @@ msgstr "Logga in"
msgid "Logout"
msgstr "Logga ut"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Markera alla som lästa"
msgid "Mark all entries as read"
msgstr "Markera alla poster som lästa"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Markera som läst"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Markera som läst hit"
@@ -430,6 +453,10 @@ msgstr "Markera som läst hit"
msgid "Metrics"
msgstr "Mätverk"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Flytta sidan nedåt"
@@ -515,6 +542,14 @@ msgstr "Öppna aktuell post i en ny flik i bakgrunden"
msgid "Open link"
msgstr "Öppen länk"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Öppna nästa post"
@@ -581,6 +616,10 @@ msgstr "Uppdatera"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Registreringar är stängda på denna CommaFeed-instans"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Delningssajter"
msgid "Shift"
msgstr "Skift"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Visa flöden och kategorier utan olästa poster"
@@ -654,6 +701,7 @@ msgstr "Något dåligt hände precis..."
msgid "Space"
msgstr "Rymden"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Stjärna"
@@ -681,6 +729,10 @@ msgstr "Prenumerera på flödet"
msgid "Success"
msgstr "Framgång"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Byt till mörkt tema"
@@ -713,6 +765,7 @@ msgstr "Prova CommaFeed med demokontot: demo/demo"
msgid "Unread"
msgstr "Oläst"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr ""
@@ -743,6 +796,14 @@ msgstr "Webbplats"
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 "Du har inga prenumerationer än. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil krävs"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "Kullanıcıyı sil"
msgid "Desc"
msgstr "Açılış"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "Feed URL'si"
msgid "Feed name"
msgstr "Yayın adı"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "Filtreleme ifadesi"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "Parolanızı mı unuttunuz?"
@@ -320,6 +332,10 @@ msgstr "API belgelerine gidin."
msgid "Go to the All view"
msgstr "Tümü görünümüne git"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "İyilikler"
@@ -348,6 +364,7 @@ msgstr "İçe Aktar"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "Genişletilmiş görünümde, girişler arasında gezinmek onları okundu olarak işaretler"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "Okunmadan sakla"
@@ -404,6 +421,10 @@ msgstr "Giriş"
msgid "Logout"
msgstr "Çıkış"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "Tümünü okundu olarak işaretle"
msgid "Mark all entries as read"
msgstr "Tüm girişleri okundu olarak işaretle"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "Okundu olarak işaretle"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "Buraya kadar okundu olarak işaretle"
@@ -430,6 +453,10 @@ msgstr "Buraya kadar okundu olarak işaretle"
msgid "Metrics"
msgstr "Metrikler"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "Sayfayı aşağı taşı"
@@ -515,6 +542,14 @@ msgstr "Geçerli girişi arka planda yeni bir sekmede aç"
msgid "Open link"
msgstr "Bağlantıyı aç"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Sonraki girişi aç"
@@ -581,6 +616,10 @@ msgstr "Yenile"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "Bu CommaFeed örneğinde kayıtlar kapalı"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "Siteleri paylaşma"
msgid "Shift"
msgstr "Vardiya"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "Okunmamış girişi olmayan beslemeleri ve kategorileri göster"
@@ -654,6 +701,7 @@ msgstr "Az önce kötü bir şey oldu..."
msgid "Space"
msgstr "Uzay"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Yıldız"
@@ -681,6 +729,10 @@ msgstr "beslemeye abone olun"
msgid "Success"
msgstr "Başarı"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "Karanlık temaya geç"
@@ -713,6 +765,7 @@ msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo"
msgid "Unread"
msgstr "Okunmadı"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Yıldızı kaldır"
@@ -743,6 +796,14 @@ msgstr "Web sitesi"
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. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "dosya gerekli"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -219,6 +219,10 @@ msgstr "删除用户"
msgid "Desc"
msgstr "描述"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
msgid "Display"
@@ -288,10 +292,18 @@ msgstr "供稿网址"
msgid "Feed name"
msgstr "提要名称"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "Filtering expression"
msgstr "过滤表达式"
#: src/pages/app/AboutPage.tsx
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Forgot password?"
msgstr "忘记密码?"
@@ -320,6 +332,10 @@ msgstr "转到 API 文档。"
msgid "Go to the All view"
msgstr "转到全部视图"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "好东西"
@@ -348,6 +364,7 @@ msgstr "进口"
msgid "In expanded view, scrolling through entries mark them as read"
msgstr "在展开视图中,滚动条目将它们标记为已读"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Keep unread"
msgstr "保持未读状态"
@@ -404,6 +421,10 @@ msgstr "登录"
msgid "Logout"
msgstr "注销"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
msgid "Manage users"
@@ -418,10 +439,12 @@ msgstr "全部标记为已读"
msgid "Mark all entries as read"
msgstr "将所有条目标记为已读"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read"
msgstr "标记为已读"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Mark as read up to here"
msgstr "标记为已读到这里"
@@ -430,6 +453,10 @@ msgstr "标记为已读到这里"
msgid "Metrics"
msgstr "指标"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
msgstr "页面下移"
@@ -515,6 +542,14 @@ msgstr "在后台的新选项卡中打开当前条目"
msgid "Open link"
msgstr "打开链接"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "打开下一个条目"
@@ -581,6 +616,10 @@ msgstr "刷新"
msgid "Registrations are closed on this CommaFeed instance"
msgstr "此 CommaFeed 实例上的注册已关闭"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/settings/ProfileSettings.tsx
#: src/pages/app/CategoryDetailsPage.tsx
@@ -632,6 +671,14 @@ msgstr "共享站点"
msgid "Shift"
msgstr "换档"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
msgstr "显示没有未读条目的提要和类别"
@@ -654,6 +701,7 @@ msgstr "刚刚发生了不好的事情……"
msgid "Space"
msgstr "空间"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "星星"
@@ -681,6 +729,10 @@ msgstr "订阅订阅源"
msgid "Success"
msgstr "成功"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Switch to dark theme"
msgstr "切换到深色主题"
@@ -713,6 +765,7 @@ msgstr "使用演示帐户试用 CommaFeeddemo/demo"
msgid "Unread"
msgstr "未读"
#: src/components/content/FeedEntryContextMenu.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "解星"
@@ -743,6 +796,14 @@ msgstr "网站"
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 "您还没有任何订阅。"
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "文件是必需的"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""

View File

@@ -2,6 +2,7 @@ import "@fontsource/open-sans"
import { store } from "app/store"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import "react-contexify/ReactContexify.css"
import ReactDOM from "react-dom/client"
import { Provider } from "react-redux"
import { App } from "./App"

View File

@@ -1,7 +1,6 @@
import { Accordion, Box, Tabs } from "@mantine/core"
import { Accordion, Tabs } from "@mantine/core"
import { client } from "app/client"
import { Loader } from "components/Loader"
import { Gauge } from "components/metrics/Gauge"
import { Meter } from "components/metrics/Meter"
import { MetricAccordionItem } from "components/metrics/MetricAccordionItem"
import { Timer } from "components/metrics/Timer"
@@ -9,28 +8,18 @@ import { useAsync } from "react-async-hook"
import { TbChartAreaLine, TbClock } from "react-icons/tb"
const shownMeters: { [key: string]: string } = {
"com.commafeed.backend.feed.FeedQueues.refill": "Refresh queue refill rate",
"com.commafeed.backend.feed.FeedRefreshTaskGiver.feedRefreshed": "Feed refreshed",
"com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed updated",
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit": "Entry cache hit",
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss",
}
const shownGauges: { [key: string]: string } = {
"com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.active": "Feed Updater active",
"com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.pending": "Feed Updater queued",
"com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.active": "Feed Worker active",
"com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.pending": "Feed Worker queued",
"com.commafeed.backend.feed.FeedQueues.addQueue": "Task Giver Add Queue",
"com.commafeed.backend.feed.FeedQueues.takeQueue": "Task Giver Take Queue",
"com.commafeed.backend.feed.FeedQueues.giveBackQueue": "Task Giver Give Back Queue",
"com.commafeed.backend.feed.FeedRefreshEngine.refill": "Feed queue refill rate",
"com.commafeed.backend.feed.FeedRefreshWorker.feedFetched": "Feed fetching rate",
"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",
}
export function MetricsPage() {
const query = useAsync(() => client.admin.getMetrics(), [])
if (!query.result) return <Loader />
const { meters, gauges, timers } = query.result.data
const { meters, timers } = query.result.data
return (
<Tabs defaultValue="stats">
<Tabs.List>
@@ -50,15 +39,6 @@ export function MetricsPage() {
</MetricAccordionItem>
))}
</Accordion>
<Box pt="xs">
{Object.keys(shownGauges).map(g => (
<Box key={g}>
<span>{shownGauges[g]}&nbsp;</span>
<Gauge gauge={gauges[g]} />
</Box>
))}
</Box>
</Tabs.Panel>
<Tabs.Panel value="timers" pt="xs">

View File

@@ -1,5 +1,5 @@
import { t, Trans } from "@lingui/macro"
import { Anchor, Box, Center, Container, createStyles, List, NativeSelect, SimpleGrid, Title } from "@mantine/core"
import { Anchor, Box, Center, Code, Container, createStyles, List, NativeSelect, SimpleGrid, Title } from "@mantine/core"
import { Constants } from "app/constants"
import { redirectToApiDocumentation } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store"
@@ -57,6 +57,7 @@ function NextUnreadBookmarklet() {
)
}
const bitcoinAddress = <Code>{Constants.bitcoinWalletAddress}</Code>
export function AboutPage() {
const version = useAppSelector(state => state.server.serverInfos?.version)
const revision = useAppSelector(state => state.server.serverInfos?.gitCommit)
@@ -70,7 +71,7 @@ export function AboutPage() {
CommaFeed version {version} ({revision})
</Trans>
</Box>
<Box>
<Box mt="md">
<Trans>
CommaFeed is an open-source project. Sources are hosted on&nbsp;
<Anchor href="https://github.com/Athou/commafeed" target="_blank" rel="noreferrer">
@@ -114,6 +115,9 @@ export function AboutPage() {
</Center>
</form>
</Box>
<Box mt="xs">
<Trans>For those of you who prefer bitcoin, here is the address: {bitcoinAddress}</Trans>
</Box>
</Section>
<Section title={t`Goodies`} icon={<TbPuzzle size={24} />}>
<List>

View File

@@ -109,7 +109,12 @@ export function CategoryDetailsPage() {
{editable && (
<>
<TextInput label={t`Name`} {...form.getInputProps("name")} required />
<CategorySelect label={t`Parent Category`} {...form.getInputProps("parentId")} clearable />
<CategorySelect
label={t`Parent Category`}
{...form.getInputProps("parentId")}
clearable
withoutCategoryIds={[id]}
/>
<NumberInput label={t`Position`} {...form.getInputProps("position")} required min={0} />
</>
)}

View File

@@ -17,7 +17,7 @@ import { TbDeviceFloppy, TbTrash } from "react-icons/tb"
import { useParams } from "react-router-dom"
function FilteringExpressionDescription() {
const example = <Code>url.contains('youtube') or (author eq 'athou' and title.contains('github')</Code>
const example = <Code>url.contains('youtube') or (author eq 'athou' and title.contains('github'))</Code>
return (
<div>
<div>

View File

@@ -25,6 +25,7 @@ import { Logo } from "components/Logo"
import { OnDesktop } from "components/responsive/OnDesktop"
import { OnMobile } from "components/responsive/OnMobile"
import { useAppLoading } from "hooks/useAppLoading"
import { useWebSocket } from "hooks/useWebSocket"
import { LoadingPage } from "pages/LoadingPage"
import { ReactNode, Suspense, useEffect } from "react"
import { TbPlus } from "react-icons/tb"
@@ -85,6 +86,7 @@ export default function Layout({ sidebar, header }: LayoutProps) {
const { loading } = useAppLoading()
const mobileMenuOpen = useAppSelector(state => state.tree.mobileMenuOpen)
const dispatch = useAppDispatch()
useWebSocket()
useEffect(() => {
dispatch(reloadSettings())

View File

@@ -22,6 +22,7 @@ export default defineConfig({
port: 8082,
proxy: {
"/rest": "http://localhost:8083",
"/ws": "ws://localhost:8083",
"/swagger": "http://localhost:8083",
},
},

View File

@@ -69,7 +69,7 @@ app:
# user-agent string that will be used by the http client, leave empty for the default one
userAgent:
# Database connection
# -------------------
# for MySQL
@@ -92,7 +92,7 @@ database:
properties:
charSet: UTF-8
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
server:
applicationConnectors:
- type: http
@@ -100,7 +100,7 @@ server:
adminConnectors:
- type: http
port: 8084
logging:
level: INFO
loggers:
@@ -108,6 +108,7 @@ 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
@@ -117,14 +118,14 @@ logging:
archivedLogFilenamePattern: log/commafeed-%d.log
archivedFileCount: 5
timeZone: UTC
# Redis pool configuration
# (only used if app.cache is 'redis')
# -----------------------------------
redis:
host: localhost
port: 6379
password:
password:
timeout: 2000
database: 0
maxTotal: 500

View File

@@ -87,7 +87,7 @@ app:
database:
driverClass: org.h2.Driver
url: jdbc:h2:/home/commafeed/db
url: jdbc:h2:/commafeed/data/db
user: sa
password: sa
properties:
@@ -104,9 +104,11 @@ server:
adminConnectors:
- type: http
port: 8084
requestLog:
appenders: [ ]
logging:
level: WARN
level: ERROR
loggers:
com.commafeed: INFO
liquibase: INFO

View File

@@ -1,19 +1,20 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name>
<properties>
<guice.version>5.1.0</guice.version>
<querydsl.version>4.2.1</querydsl.version>
<rome.version>1.18.0</rome.version>
<querydsl.version>4.4.0</querydsl.version>
<rome.version>2.1.0</rome.version>
</properties>
<dependencyManagement>
@@ -21,7 +22,7 @@
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-dependencies</artifactId>
<version>2.1.1</version>
<version>2.1.6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -41,7 +42,9 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -110,8 +113,10 @@
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.commafeed.CommaFeedApplication</mainClass>
</transformer>
<transformer implementation="org.kordamp.shade.resources.PropertiesFileTransformer">
@@ -228,13 +233,13 @@
<dependency>
<groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<version>1.18.26</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -261,8 +266,8 @@
<artifactId>dropwizard-hibernate</artifactId>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-migrations</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
@@ -285,6 +290,22 @@
<artifactId>dropwizard-web</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>be.tomcools</groupId>
<artifactId>dropwizard-websocket-jee7-bundle</artifactId>
<version>2.1.6</version>
</dependency>
<dependency>
<groupId>io.whitfin</groupId>
<artifactId>dropwizard-environment-substitutor</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
<version>3.1.6</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
@@ -398,50 +419,38 @@
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.14.3</version>
<version>1.15.4</version>
</dependency>
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
<version>70.1</version>
<version>73.1</version>
</dependency>
<dependency>
<groupId>net.sourceforge.cssparser</groupId>
<artifactId>cssparser</artifactId>
<version>0.9.29</version>
<version>0.9.30</version>
</dependency>
<dependency>
<groupId>edu.uci.ics</groupId>
<artifactId>crawler4j</artifactId>
<version>3.5</version>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
<groupId>org.netpreserve</groupId>
<artifactId>urlcanon</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>com.google.gwt</groupId>
<artifactId>gwt-servlet</artifactId>
<version>2.9.0</version>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart</artifactId>
<version>7.2.0</version>
<version>7.4.11</version>
</dependency>
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-youtube</artifactId>
<version>v3-rev139-1.20.0</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava-jdk5</artifactId>
</exclusion>
</exclusions>
<version>v3-rev222-1.25.0</version>
</dependency>
<dependency>
@@ -451,12 +460,12 @@
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.4.1</version>
<version>42.6.0</version>
</dependency>
<dependency>
<groupId>net.sourceforge.jtds</groupId>
@@ -474,10 +483,15 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-junit-jupiter</artifactId>
<version>5.13.2</version>
<version>5.15.0</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -493,7 +507,7 @@
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.24.1</version>
<version>1.32.0</version>
<scope>test</scope>
</dependency>

View File

@@ -13,13 +13,12 @@ import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.websocket.server.ServerEndpointConfig;
import org.hibernate.cfg.AvailableSettings;
import com.codahale.metrics.json.MetricsModule;
import com.commafeed.backend.feed.FeedRefreshTaskGiver;
import com.commafeed.backend.feed.FeedRefreshUpdater;
import com.commafeed.backend.feed.FeedRefreshWorker;
import com.commafeed.backend.feed.FeedRefreshEngine;
import com.commafeed.backend.model.AbstractModel;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory;
@@ -31,7 +30,7 @@ import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.service.StartupService;
import com.commafeed.backend.service.DatabaseStartupService;
import com.commafeed.backend.service.UserService;
import com.commafeed.backend.task.ScheduledTask;
import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
@@ -47,22 +46,31 @@ import com.commafeed.frontend.servlet.CustomCssServlet;
import com.commafeed.frontend.servlet.LogoutServlet;
import com.commafeed.frontend.servlet.NextUnreadServlet;
import com.commafeed.frontend.session.SessionHelperFactoryProvider;
import com.commafeed.frontend.ws.WebSocketConfigurator;
import com.commafeed.frontend.ws.WebSocketEndpoint;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import be.tomcools.dropwizard.websocket.WebsocketBundle;
import io.dropwizard.Application;
import io.dropwizard.assets.AssetsBundle;
import io.dropwizard.configuration.DefaultConfigurationFactoryFactory;
import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
import io.dropwizard.configuration.SubstitutingSourceProvider;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.forms.MultiPartBundle;
import io.dropwizard.hibernate.HibernateBundle;
import io.dropwizard.server.DefaultServerFactory;
import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.servlets.CacheBustingFilter;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import io.dropwizard.web.WebBundle;
import io.dropwizard.web.conf.WebConfiguration;
import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor;
public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
@@ -72,6 +80,7 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
public static final Date STARTUP_TIME = new Date();
private HibernateBundle<CommaFeedConfiguration> hibernateBundle;
private WebsocketBundle<CommaFeedConfiguration> websocketBundle;
@Override
public String getName() {
@@ -80,8 +89,27 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
@Override
public void initialize(Bootstrap<CommaFeedConfiguration> bootstrap) {
bootstrap.setConfigurationFactoryFactory(new DefaultConfigurationFactoryFactory<CommaFeedConfiguration>() {
@Override
protected ObjectMapper configureObjectMapper(ObjectMapper objectMapper) {
// disable case sensitivity because EnvironmentSubstitutor maps MYPROPERTY to myproperty and not to myProperty
return objectMapper
.setConfig(objectMapper.getDeserializationConfig().with(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES));
}
});
// enable config.yml string substitution
// e.g. having a custom config.yml file with app.session.path=${SOME_ENV_VAR} will substitute SOME_ENV_VAR
SubstitutingSourceProvider substitutingSourceProvider = new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(),
new EnvironmentVariableSubstitutor(false));
// enable config.yml properties override with env variables prefixed with CF_
// e.g. setting CF_APP_ALLOWREGISTRATIONS=true will set app.allowRegistrations to true
EnvironmentSubstitutor environmentSubstitutor = new EnvironmentSubstitutor("CF", substitutingSourceProvider);
bootstrap.setConfigurationSourceProvider(environmentSubstitutor);
bootstrap.getObjectMapper().registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
bootstrap.addBundle(websocketBundle = new WebsocketBundle<>());
bootstrap.addBundle(hibernateBundle = new HibernateBundle<CommaFeedConfiguration>(AbstractModel.class, Feed.class,
FeedCategory.class, FeedEntry.class, FeedEntryContent.class, FeedEntryStatus.class, FeedEntryTag.class,
FeedSubscription.class, User.class, UserRole.class, UserSettings.class) {
@@ -107,6 +135,13 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
}
});
bootstrap.addBundle(new MigrationsBundle<CommaFeedConfiguration>() {
@Override
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
return configuration.getDataSourceFactory();
}
});
bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html"));
bootstrap.addBundle(new MultiPartBundle());
}
@@ -126,7 +161,6 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
// REST resources
environment.jersey().setUrlPattern("/rest/*");
((DefaultServerFactory) config.getServerFactory()).setJerseyRootPath("/rest/*");
environment.jersey().register(injector.getInstance(AdminREST.class));
environment.jersey().register(injector.getInstance(CategoryREST.class));
environment.jersey().register(injector.getInstance(EntryREST.class));
@@ -141,6 +175,12 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
environment.servlets().addServlet("customCss", injector.getInstance(CustomCssServlet.class)).addMapping("/custom_css.css");
environment.servlets().addServlet("analytics.js", injector.getInstance(AnalyticsServlet.class)).addMapping("/analytics.js");
// WebSocket endpoint
ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws")
.configurator(injector.getInstance(WebSocketConfigurator.class))
.build();
websocketBundle.addEndpoint(serverEndpointConfig);
// Scheduled tasks
Set<ScheduledTask> tasks = injector.getInstance(Key.get(new TypeLiteral<Set<ScheduledTask>>() {
}));
@@ -153,12 +193,10 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
}
// database init/changelogs
environment.lifecycle().manage(injector.getInstance(StartupService.class));
environment.lifecycle().manage(injector.getInstance(DatabaseStartupService.class));
// background feed fetching
environment.lifecycle().manage(injector.getInstance(FeedRefreshTaskGiver.class));
environment.lifecycle().manage(injector.getInstance(FeedRefreshWorker.class));
environment.lifecycle().manage(injector.getInstance(FeedRefreshUpdater.class));
// start feed fetching engine
environment.lifecycle().manage(injector.getInstance(FeedRefreshEngine.class));
// prevent caching index.html, so that the webapp is always up to date
environment.servlets()

View File

@@ -45,18 +45,14 @@ public class CommaFeedConfiguration extends Configuration {
@JsonProperty("app")
private ApplicationSettings applicationSettings;
private final ResourceBundle bundle;
private final String version;
private final String gitCommit;
public CommaFeedConfiguration() {
bundle = ResourceBundle.getBundle("application");
}
ResourceBundle bundle = ResourceBundle.getBundle("application");
public String getVersion() {
return bundle.getString("version");
}
public String getGitCommit() {
return bundle.getString("git.commit");
this.version = bundle.getString("version");
this.gitCommit = bundle.getString("git.commit");
}
@Getter

View File

@@ -14,7 +14,6 @@ import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
@@ -59,7 +58,7 @@ public class HttpGetter {
}
}
public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, IOException, NotModifiedException {
public HttpResult getBinary(String url, int timeout) throws IOException, NotModifiedException {
return getBinary(url, null, null, timeout);
}
@@ -71,14 +70,10 @@ public class HttpGetter {
* header we got last time we queried that url, or null
* @param eTag
* header we got last time we queried that url, or null
* @return
* @throws ClientProtocolException
* @throws IOException
* @throws NotModifiedException
* if the url hasn't changed since we asked for it last time
*/
public HttpResult getBinary(String url, String lastModified, String eTag, int timeout)
throws ClientProtocolException, IOException, NotModifiedException {
public HttpResult getBinary(String url, String lastModified, String eTag, int timeout) throws IOException, NotModifiedException {
HttpResult result = null;
long start = System.currentTimeMillis();
@@ -175,18 +170,28 @@ public class HttpGetter {
return builder.build();
}
public static void main(String[] args) throws Exception {
CommaFeedConfiguration config = new CommaFeedConfiguration();
HttpGetter getter = new HttpGetter(config);
HttpResult result = getter.getBinary("https://sourceforge.net/projects/mpv-player-windows/rss", 30000);
System.out.println(new String(result.content));
}
@Getter
public static class NotModifiedException extends Exception {
private static final long serialVersionUID = 1L;
/**
* if the value of this header changed, this is its new value
*/
private final String newLastModifiedHeader;
/**
* if the value of this header changed, this is its new value
*/
private final String newEtagHeader;
public NotModifiedException(String message) {
this(message, null, null);
}
public NotModifiedException(String message, String newLastModifiedHeader, String newEtagHeader) {
super(message);
this.newLastModifiedHeader = newLastModifiedHeader;
this.newEtagHeader = newEtagHeader;
}
}

View File

@@ -36,7 +36,7 @@ public class FeedDAO extends GenericDAO<Feed> {
QFeedSubscription subs = QFeedSubscription.feedSubscription;
QUser user = QUser.user;
query.join(feed.subscriptions, subs).join(subs.user, user).where(user.lastLogin.gt(lastLoginThreshold));
query.join(subs).on(subs.feed.id.eq(feed.id)).join(subs.user, user).where(user.lastLogin.gt(lastLoginThreshold));
}
return query.orderBy(feed.disabledUntil.asc()).limit(count).fetch();

View File

@@ -2,6 +2,7 @@ package com.commafeed.backend.feed;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
@@ -13,13 +14,19 @@ import org.apache.commons.codec.digest.DigestUtils;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.feed.FeedParser.FeedParserResult;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.urlprovider.FeedURLProvider;
import com.rometools.rome.io.FeedException;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
/**
* Fetches a feed then parses it
*/
@Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
@@ -29,18 +36,18 @@ public class FeedFetcher {
private final HttpGetter getter;
private final Set<FeedURLProvider> urlProviders;
public FetchedFeed fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag, Date lastPublishedDate,
public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag, Date lastPublishedDate,
String lastContentHash) throws FeedException, IOException, NotModifiedException {
log.debug("Fetching feed {}", feedUrl);
FetchedFeed fetchedFeed = null;
int timeout = 20000;
HttpResult result = getter.getBinary(feedUrl, lastModified, eTag, timeout);
byte[] content = result.getContent();
FeedParserResult parserResult;
try {
fetchedFeed = parser.parse(result.getUrlAfterRedirect(), content);
parserResult = parser.parse(result.getUrlAfterRedirect(), content);
} catch (FeedException e) {
if (extractFeedUrlFromHtml) {
String extractedUrl = extractFeedUrl(urlProviders, feedUrl, StringUtils.newStringUtf8(result.getContent()));
@@ -49,7 +56,7 @@ public class FeedFetcher {
result = getter.getBinary(extractedUrl, lastModified, eTag, timeout);
content = result.getContent();
fetchedFeed = parser.parse(result.getUrlAfterRedirect(), content);
parserResult = parser.parse(result.getUrlAfterRedirect(), content);
} else {
throw e;
}
@@ -62,25 +69,31 @@ public class FeedFetcher {
throw new IOException("Feed content is empty.");
}
boolean lastModifiedHeaderValueChanged = !StringUtils.equals(lastModified, result.getLastModifiedSince());
boolean etagHeaderValueChanged = !StringUtils.equals(eTag, result.getETag());
String hash = DigestUtils.sha1Hex(content);
if (lastContentHash != null && hash != null && lastContentHash.equals(hash)) {
log.debug("content hash not modified: {}", feedUrl);
throw new NotModifiedException("content hash not modified");
throw new NotModifiedException("content hash not modified",
lastModifiedHeaderValueChanged ? result.getLastModifiedSince() : null,
etagHeaderValueChanged ? result.getETag() : null);
}
if (lastPublishedDate != null && fetchedFeed.getFeed().getLastPublishedDate() != null
&& lastPublishedDate.getTime() == fetchedFeed.getFeed().getLastPublishedDate().getTime()) {
if (lastPublishedDate != null && parserResult.getFeed().getLastPublishedDate() != null
&& lastPublishedDate.getTime() == parserResult.getFeed().getLastPublishedDate().getTime()) {
log.debug("publishedDate not modified: {}", feedUrl);
throw new NotModifiedException("publishedDate not modified");
throw new NotModifiedException("publishedDate not modified",
lastModifiedHeaderValueChanged ? result.getLastModifiedSince() : null,
etagHeaderValueChanged ? result.getETag() : null);
}
Feed feed = fetchedFeed.getFeed();
Feed feed = parserResult.getFeed();
feed.setLastModifiedHeader(result.getLastModifiedSince());
feed.setEtagHeader(FeedUtils.truncate(result.getETag(), 255));
feed.setLastContentHash(hash);
fetchedFeed.setFetchDuration(result.getDuration());
fetchedFeed.setUrlAfterRedirect(result.getUrlAfterRedirect());
return fetchedFeed;
return new FeedFetcherResult(parserResult.getFeed(), parserResult.getEntries(), parserResult.getTitle(),
result.getUrlAfterRedirect(), result.getDuration());
}
private static String extractFeedUrl(Set<FeedURLProvider> urlProviders, String url, String urlContent) {
@@ -93,4 +106,14 @@ public class FeedFetcher {
return null;
}
@Value
public static class FeedFetcherResult {
Feed feed;
List<FeedEntry> entries;
String title;
String urlAfterRedirect;
long fetchDuration;
}
}

View File

@@ -3,6 +3,7 @@ package com.commafeed.backend.feed;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@@ -37,8 +38,12 @@ import com.rometools.rome.io.SyndFeedInput;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
/**
* Parses raw xml as a Feed object
*/
@Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
@@ -50,10 +55,7 @@ public class FeedParser {
private static final Date START = new Date(86400000);
private static final Date END = new Date(1000L * Integer.MAX_VALUE - 86400000);
public FetchedFeed parse(String feedUrl, byte[] xml) throws FeedException {
FetchedFeed fetchedFeed = new FetchedFeed();
Feed feed = fetchedFeed.getFeed();
List<FeedEntry> entries = fetchedFeed.getEntries();
public FeedParserResult parse(String feedUrl, byte[] xml) throws FeedException {
try {
Charset encoding = FeedUtils.guessEncoding(xml);
@@ -63,17 +65,19 @@ public class FeedParser {
}
xmlString = FeedUtils.replaceHtmlEntitiesWithNumericEntities(xmlString);
InputSource source = new InputSource(new StringReader(xmlString));
SyndFeed rss = new SyndFeedInput().build(source);
handleForeignMarkup(rss);
fetchedFeed.setTitle(rss.getTitle());
String title = rss.getTitle();
Feed feed = new Feed();
feed.setPushHub(findHub(rss));
feed.setPushTopic(findSelf(rss));
feed.setUrl(feedUrl);
feed.setLink(rss.getLink());
List<SyndEntry> items = rss.getEntries();
for (SyndEntry item : items) {
List<FeedEntry> entries = new ArrayList<>();
for (SyndEntry item : rss.getEntries()) {
FeedEntry entry = new FeedEntry();
String guid = item.getUri();
@@ -121,6 +125,7 @@ public class FeedParser {
entries.add(entry);
}
Date lastEntryDate = null;
Date publishedDate = validateDate(rss.getPublishedDate(), false);
if (!entries.isEmpty()) {
@@ -133,10 +138,10 @@ public class FeedParser {
feed.setAverageEntryInterval(FeedUtils.averageTimeBetweenEntries(entries));
feed.setLastEntryDate(lastEntryDate);
return new FeedParserResult(feed, entries, title);
} catch (Exception e) {
throw new FeedException(String.format("Could not parse feed from %s : %s", feedUrl, e.getMessage()), e);
}
return fetchedFeed;
}
/**
@@ -273,4 +278,11 @@ public class FeedParser {
}
}
@Value
public static class FeedParserResult {
Feed feed;
List<FeedEntry> entries;
String title;
}
}

View File

@@ -1,160 +0,0 @@
package com.commafeed.backend.feed;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.SessionFactory;
import com.codahale.metrics.Gauge;
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.UnitOfWork;
import com.commafeed.backend.model.Feed;
@Singleton
public class FeedQueues {
private SessionFactory sessionFactory;
private final FeedDAO feedDAO;
private final CommaFeedConfiguration config;
private Queue<FeedRefreshContext> addQueue = new ConcurrentLinkedQueue<>();
private Queue<FeedRefreshContext> takeQueue = new ConcurrentLinkedQueue<>();
private Queue<Feed> giveBackQueue = new ConcurrentLinkedQueue<>();
private Meter refill;
@Inject
public FeedQueues(SessionFactory sessionFactory, FeedDAO feedDAO, CommaFeedConfiguration config, MetricRegistry metrics) {
this.sessionFactory = sessionFactory;
this.config = config;
this.feedDAO = feedDAO;
refill = metrics.meter(MetricRegistry.name(getClass(), "refill"));
metrics.register(MetricRegistry.name(getClass(), "addQueue"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return addQueue.size();
}
});
metrics.register(MetricRegistry.name(getClass(), "takeQueue"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return takeQueue.size();
}
});
metrics.register(MetricRegistry.name(getClass(), "giveBackQueue"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return giveBackQueue.size();
}
});
}
/**
* take a feed from the refresh queue
*/
public synchronized FeedRefreshContext take() {
FeedRefreshContext context = takeQueue.poll();
if (context == null) {
refill();
context = takeQueue.poll();
}
return context;
}
/**
* add a feed to the refresh queue
*/
public void add(Feed feed, boolean urgent) {
int refreshInterval = config.getApplicationSettings().getRefreshIntervalMinutes();
if (feed.getLastUpdated() == null || feed.getLastUpdated().before(DateUtils.addMinutes(new Date(), -1 * refreshInterval))) {
boolean alreadyQueued = addQueue.stream().anyMatch(c -> c.getFeed().getId().equals(feed.getId()));
if (!alreadyQueued) {
addQueue.add(new FeedRefreshContext(feed, urgent));
}
}
}
/**
* refills the refresh queue and empties the giveBack queue while at it
*/
private void refill() {
refill.mark();
List<FeedRefreshContext> contexts = new ArrayList<>();
int batchSize = Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads());
// add feeds we got from the add() method
int addQueueSize = addQueue.size();
for (int i = 0; i < Math.min(batchSize, addQueueSize); i++) {
contexts.add(addQueue.poll());
}
// add feeds that are up to refresh from the database
int count = batchSize - contexts.size();
if (count > 0) {
List<Feed> feeds = UnitOfWork.call(sessionFactory, () -> feedDAO.findNextUpdatable(count, getLastLoginThreshold()));
for (Feed feed : feeds) {
contexts.add(new FeedRefreshContext(feed, false));
}
}
// set the disabledDate as we use it in feedDAO to decide what to refresh next. We also use a map to remove
// duplicates.
Map<Long, FeedRefreshContext> map = new LinkedHashMap<>();
for (FeedRefreshContext context : contexts) {
Feed feed = context.getFeed();
feed.setDisabledUntil(DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes()));
map.put(feed.getId(), context);
}
// refill the queue
takeQueue.addAll(map.values());
// add feeds from the giveBack queue to the map, overriding duplicates
int giveBackQueueSize = giveBackQueue.size();
for (int i = 0; i < giveBackQueueSize; i++) {
Feed feed = giveBackQueue.poll();
map.put(feed.getId(), new FeedRefreshContext(feed, false));
}
// update all feeds in the database
List<Feed> feeds = map.values().stream().map(c -> c.getFeed()).collect(Collectors.toList());
UnitOfWork.run(sessionFactory, () -> feedDAO.saveOrUpdate(feeds));
}
/**
* give a feed back, updating it to the database during the next refill()
*/
public void giveBack(Feed feed) {
String normalized = FeedUtils.normalizeURL(feed.getUrl());
feed.setNormalizedUrl(normalized);
feed.setNormalizedUrlHash(DigestUtils.sha1Hex(normalized));
feed.setLastUpdated(new Date());
giveBackQueue.add(feed);
}
private Date getLastLoginThreshold() {
if (config.getApplicationSettings().getHeavyLoad()) {
return DateUtils.addDays(new Date(), -30);
} else {
return null;
}
}
}

View File

@@ -1,22 +0,0 @@
package com.commafeed.backend.feed;
import java.util.List;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class FeedRefreshContext {
private Feed feed;
private List<FeedEntry> entries;
private boolean urgent;
public FeedRefreshContext(Feed feed, boolean isUrgent) {
this.feed = feed;
this.urgent = isUrgent;
}
}

View File

@@ -0,0 +1,108 @@
package com.commafeed.backend.feed;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.SessionFactory;
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.UnitOfWork;
import com.commafeed.backend.model.Feed;
import io.dropwizard.lifecycle.Managed;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.processors.PublishProcessor;
import io.reactivex.rxjava3.schedulers.Schedulers;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class FeedRefreshEngine implements Managed {
private final SessionFactory sessionFactory;
private final FeedDAO feedDAO;
private final FeedRefreshWorker worker;
private final FeedRefreshUpdater updater;
private final CommaFeedConfiguration config;
private final Meter refill;
private final PublishProcessor<Feed> priorityQueue;
private Disposable flow;
@Inject
public FeedRefreshEngine(SessionFactory sessionFactory, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
CommaFeedConfiguration config, MetricRegistry metrics) {
this.sessionFactory = sessionFactory;
this.feedDAO = feedDAO;
this.worker = worker;
this.updater = updater;
this.config = config;
this.refill = metrics.meter(MetricRegistry.name(getClass(), "refill"));
this.priorityQueue = PublishProcessor.create();
}
@Override
public void start() {
Flowable<Feed> database = Flowable.fromCallable(() -> findNextUpdatableFeeds(getBatchSize(), getLastLoginThreshold()))
.onErrorResumeNext(e -> {
log.error("error while fetching next updatable feeds", e);
return Flowable.empty();
})
// repeat query 15s after the flowable has been emptied
// https://github.com/ReactiveX/RxJava/issues/448#issuecomment-233244964
.repeatWhen(o -> o.concatMap(v -> Flowable.timer(15, TimeUnit.SECONDS)))
.flatMap(Flowable::fromIterable);
Flowable<Feed> source = Flowable.merge(priorityQueue, database);
this.flow = source.subscribeOn(Schedulers.io())
// feed fetching
.parallel(config.getApplicationSettings().getBackgroundThreads())
.runOn(Schedulers.io())
.flatMap(f -> Flowable.fromCallable(() -> worker.update(f)).onErrorResumeNext(e -> {
log.error("error while fetching feed", e);
return Flowable.empty();
}))
.sequential()
// database updating
.parallel(config.getApplicationSettings().getDatabaseUpdateThreads())
.runOn(Schedulers.io())
.flatMap(fae -> Flowable.fromCallable(() -> updater.update(fae.getFeed(), fae.getEntries())).onErrorResumeNext(e -> {
log.error("error while updating database", e);
return Flowable.empty();
}))
.sequential()
// end flow
.subscribe();
}
public void refreshImmediately(Feed feed) {
priorityQueue.onNext(feed);
}
private List<Feed> findNextUpdatableFeeds(int max, Date lastLoginThreshold) {
refill.mark();
return UnitOfWork.call(sessionFactory, () -> feedDAO.findNextUpdatable(max, lastLoginThreshold));
}
private int getBatchSize() {
return Math.min(Flowable.bufferSize(), 3 * config.getApplicationSettings().getBackgroundThreads());
}
private Date getLastLoginThreshold() {
return Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad()) ? DateUtils.addDays(new Date(), -30) : null;
}
@Override
public void stop() {
flow.dispose();
}
}

View File

@@ -1,99 +0,0 @@
package com.commafeed.backend.feed;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry;
import lombok.extern.slf4j.Slf4j;
/**
* Wraps a {@link ThreadPoolExecutor} instance. Blocks when queue is full instead of rejecting the task. Allow priority queueing by using
* {@link Task} instead of {@link Runnable}
*
*/
@Slf4j
public class FeedRefreshExecutor {
private String poolName;
private ThreadPoolExecutor pool;
private LinkedBlockingDeque<Runnable> queue;
public FeedRefreshExecutor(final String poolName, int threads, int queueCapacity, MetricRegistry metrics) {
log.info("Creating pool {} with {} threads", poolName, threads);
this.poolName = poolName;
pool = new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS, queue = new LinkedBlockingDeque<Runnable>(queueCapacity) {
private static final long serialVersionUID = 1L;
@Override
public boolean offer(Runnable r) {
Task task = (Task) r;
if (task.isUrgent()) {
return offerFirst(r);
} else {
return offerLast(r);
}
}
}) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
if (t != null) {
log.error("thread from pool {} threw a runtime exception", poolName, t);
}
}
};
pool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
log.debug("{} thread queue full, waiting...", poolName);
try {
Task task = (Task) r;
if (task.isUrgent()) {
queue.putFirst(r);
} else {
queue.put(r);
}
} catch (InterruptedException e1) {
log.error(poolName + " interrupted while waiting for queue.", e1);
}
}
});
metrics.register(MetricRegistry.name(getClass(), poolName, "active"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return pool.getActiveCount();
}
});
metrics.register(MetricRegistry.name(getClass(), poolName, "pending"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return queue.size();
}
});
}
public void execute(Task task) {
pool.execute(task);
}
public void shutdown() {
pool.shutdownNow();
while (!pool.isTerminated()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
log.error("{} interrupted while waiting for threads to finish.", poolName);
}
}
}
public interface Task extends Runnable {
boolean isUrgent();
}
}

View File

@@ -0,0 +1,84 @@
package com.commafeed.backend.feed;
import java.util.Date;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.time.DateUtils;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.model.Feed;
@Singleton
public class FeedRefreshIntervalCalculator {
private boolean heavyLoad;
private int refreshIntervalMinutes;
@Inject
public FeedRefreshIntervalCalculator(CommaFeedConfiguration config) {
this.heavyLoad = config.getApplicationSettings().getHeavyLoad();
this.refreshIntervalMinutes = config.getApplicationSettings().getRefreshIntervalMinutes();
}
public Date onFetchSuccess(Feed feed) {
Date defaultRefreshInterval = getDefaultRefreshInterval();
return heavyLoad ? computeRefreshIntervalForHeavyLoad(feed, defaultRefreshInterval) : defaultRefreshInterval;
}
public Date onFeedNotModified(Feed feed) {
Date defaultRefreshInterval = getDefaultRefreshInterval();
return heavyLoad ? computeRefreshIntervalForHeavyLoad(feed, defaultRefreshInterval) : defaultRefreshInterval;
}
public Date onFetchError(Feed feed) {
int errorCount = feed.getErrorCount();
int retriesBeforeDisable = 3;
if (errorCount < retriesBeforeDisable || !heavyLoad) {
return getDefaultRefreshInterval();
}
int disabledHours = Math.min(24 * 7, errorCount - retriesBeforeDisable + 1);
return DateUtils.addHours(new Date(), disabledHours);
}
private Date getDefaultRefreshInterval() {
return DateUtils.addMinutes(new Date(), refreshIntervalMinutes);
}
private Date computeRefreshIntervalForHeavyLoad(Feed feed, Date defaultRefreshInterval) {
Date now = new Date();
Date publishedDate = feed.getLastEntryDate();
Long averageEntryInterval = feed.getAverageEntryInterval();
if (publishedDate == null) {
// feed with no entries, recheck in 24 hours
return DateUtils.addHours(now, 24);
} else if (publishedDate.before(DateUtils.addMonths(now, -1))) {
// older than a month, recheck in 24 hours
return DateUtils.addHours(now, 24);
} else if (publishedDate.before(DateUtils.addDays(now, -14))) {
// older than two weeks, recheck in 12 hours
return DateUtils.addHours(now, 12);
} else if (publishedDate.before(DateUtils.addDays(now, -7))) {
// older than a week, recheck in 6 hours
return DateUtils.addHours(now, 6);
} else if (averageEntryInterval != null) {
// use average time between entries to decide when to refresh next, divided by factor
int factor = 2;
// not more than 6 hours
long date = Math.min(DateUtils.addHours(now, 6).getTime(), now.getTime() + averageEntryInterval / factor);
// not less than default refresh interval
date = Math.max(defaultRefreshInterval.getTime(), date);
return new Date(date);
} else {
// unknown case, recheck in 24 hours
return DateUtils.addHours(now, 24);
}
}
}

View File

@@ -1,85 +0,0 @@
package com.commafeed.backend.feed;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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 io.dropwizard.lifecycle.Managed;
import lombok.extern.slf4j.Slf4j;
/**
* Infinite loop fetching feeds from @FeedQueues and queuing them to the {@link FeedRefreshWorker} pool.
*
*/
@Slf4j
@Singleton
public class FeedRefreshTaskGiver implements Managed {
private final FeedQueues queues;
private final FeedRefreshWorker worker;
private final ExecutorService executor;
private final Meter feedRefreshed;
private final Meter threadWaited;
@Inject
public FeedRefreshTaskGiver(FeedQueues queues, FeedDAO feedDAO, FeedRefreshWorker worker, CommaFeedConfiguration config,
MetricRegistry metrics) {
this.queues = queues;
this.worker = worker;
executor = Executors.newFixedThreadPool(1);
feedRefreshed = metrics.meter(MetricRegistry.name(getClass(), "feedRefreshed"));
threadWaited = metrics.meter(MetricRegistry.name(getClass(), "threadWaited"));
}
@Override
public void stop() {
log.info("shutting down feed refresh task giver");
executor.shutdownNow();
while (!executor.isTerminated()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
log.error("interrupted while waiting for threads to finish.");
}
}
}
@Override
public void start() {
log.info("starting feed refresh task giver");
executor.execute(new Runnable() {
@Override
public void run() {
while (!executor.isShutdown()) {
try {
FeedRefreshContext context = queues.take();
if (context != null) {
feedRefreshed.mark();
worker.updateFeed(context);
} else {
log.debug("nothing to do, sleeping for 15s");
threadWaited.mark();
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
log.debug("interrupted while sleeping");
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
});
}
}

View File

@@ -20,36 +20,41 @@ import org.hibernate.SessionFactory;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.feed.FeedRefreshExecutor.Task;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.backend.service.FeedUpdateService;
import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedService;
import com.commafeed.backend.service.PubSubService;
import com.commafeed.frontend.ws.WebSocketMessageBuilder;
import com.commafeed.frontend.ws.WebSocketSessions;
import com.google.common.util.concurrent.Striped;
import io.dropwizard.lifecycle.Managed;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Updates the feed in the database and inserts new entries
*/
@Slf4j
@Singleton
public class FeedRefreshUpdater implements Managed {
private final SessionFactory sessionFactory;
private final FeedUpdateService feedUpdateService;
private final FeedService feedService;
private final FeedEntryService feedEntryService;
private final PubSubService pubSubService;
private final FeedQueues queues;
private final CommaFeedConfiguration config;
private final FeedSubscriptionDAO feedSubscriptionDAO;
private final CacheService cache;
private final WebSocketSessions webSocketSessions;
private final FeedRefreshExecutor pool;
private final Striped<Lock> locks;
private final Meter entryCacheMiss;
@@ -58,21 +63,19 @@ public class FeedRefreshUpdater implements Managed {
private final Meter entryInserted;
@Inject
public FeedRefreshUpdater(SessionFactory sessionFactory, FeedUpdateService feedUpdateService, PubSubService pubSubService,
FeedQueues queues, CommaFeedConfiguration config, MetricRegistry metrics, FeedSubscriptionDAO feedSubscriptionDAO,
CacheService cache) {
public FeedRefreshUpdater(SessionFactory sessionFactory, FeedService feedService, FeedEntryService feedEntryService,
PubSubService pubSubService, CommaFeedConfiguration config, MetricRegistry metrics, FeedSubscriptionDAO feedSubscriptionDAO,
CacheService cache, WebSocketSessions webSocketSessions) {
this.sessionFactory = sessionFactory;
this.feedUpdateService = feedUpdateService;
this.feedService = feedService;
this.feedEntryService = feedEntryService;
this.pubSubService = pubSubService;
this.queues = queues;
this.config = config;
this.feedSubscriptionDAO = feedSubscriptionDAO;
this.cache = cache;
this.webSocketSessions = webSocketSessions;
ApplicationSettings settings = config.getApplicationSettings();
int threads = Math.max(settings.getDatabaseUpdateThreads(), 1);
pool = new FeedRefreshExecutor("feed-refresh-updater", threads, Math.min(50 * threads, 1000), metrics);
locks = Striped.lazyWeakLock(threads * 100000);
locks = Striped.lazyWeakLock(100000);
entryCacheMiss = metrics.meter(MetricRegistry.name(getClass(), "entryCacheMiss"));
entryCacheHit = metrics.meter(MetricRegistry.name(getClass(), "entryCacheHit"));
@@ -80,22 +83,9 @@ public class FeedRefreshUpdater implements Managed {
entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
}
@Override
public void start() throws Exception {
}
@Override
public void stop() throws Exception {
log.info("shutting down feed refresh updater");
pool.shutdown();
}
public void updateFeed(FeedRefreshContext context) {
pool.execute(new EntryTask(context));
}
private boolean addEntry(final Feed feed, final FeedEntry entry, final List<FeedSubscription> subscriptions) {
boolean success = false;
private AddEntryResult addEntry(final Feed feed, final FeedEntry entry, final List<FeedSubscription> subscriptions) {
boolean processed = false;
boolean inserted = false;
// lock on feed, make sure we are not updating the same feed twice at
// the same time
@@ -112,14 +102,15 @@ public class FeedRefreshUpdater implements Managed {
boolean locked1 = false;
boolean locked2 = false;
try {
// try to lock, give up after 1 minute
locked1 = lock1.tryLock(1, TimeUnit.MINUTES);
locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
if (locked1 && locked2) {
boolean inserted = UnitOfWork.call(sessionFactory, () -> feedUpdateService.addEntry(feed, entry, subscriptions));
processed = true;
inserted = UnitOfWork.call(sessionFactory, () -> feedEntryService.addEntry(feed, entry, subscriptions));
if (inserted) {
entryInserted.mark();
}
success = true;
} else {
log.error("lock timeout for " + feed.getUrl() + " - " + key1);
}
@@ -133,7 +124,7 @@ public class FeedRefreshUpdater implements Managed {
lock2.unlock();
}
}
return success;
return new AddEntryResult(processed, inserted);
}
private void handlePubSub(final Feed feed) {
@@ -159,68 +150,69 @@ public class FeedRefreshUpdater implements Managed {
}
}
private class EntryTask implements Task {
public boolean update(Feed feed, List<FeedEntry> entries) {
boolean processed = true;
boolean insertedAtLeastOneEntry = false;
private final FeedRefreshContext context;
if (!entries.isEmpty()) {
List<String> lastEntries = cache.getLastEntries(feed);
List<String> currentEntries = new ArrayList<>();
public EntryTask(FeedRefreshContext context) {
this.context = context;
}
@Override
public void run() {
boolean ok = true;
final Feed feed = context.getFeed();
List<FeedEntry> entries = context.getEntries();
if (entries.isEmpty()) {
feed.setMessage("Feed has no entries");
} else {
List<String> lastEntries = cache.getLastEntries(feed);
List<String> currentEntries = new ArrayList<>();
List<FeedSubscription> subscriptions = null;
for (FeedEntry entry : entries) {
String cacheKey = cache.buildUniqueEntryKey(feed, entry);
if (!lastEntries.contains(cacheKey)) {
log.debug("cache miss for {}", entry.getUrl());
if (subscriptions == null) {
subscriptions = UnitOfWork.call(sessionFactory, () -> feedSubscriptionDAO.findByFeed(feed));
}
ok &= addEntry(feed, entry, subscriptions);
entryCacheMiss.mark();
} else {
log.debug("cache hit for {}", entry.getUrl());
entryCacheHit.mark();
List<FeedSubscription> subscriptions = null;
for (FeedEntry entry : entries) {
String cacheKey = cache.buildUniqueEntryKey(feed, entry);
if (!lastEntries.contains(cacheKey)) {
log.debug("cache miss for {}", entry.getUrl());
if (subscriptions == null) {
subscriptions = UnitOfWork.call(sessionFactory, () -> feedSubscriptionDAO.findByFeed(feed));
}
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
processed &= addEntryResult.processed;
insertedAtLeastOneEntry |= addEntryResult.inserted;
currentEntries.add(cacheKey);
entryCacheMiss.mark();
} else {
log.debug("cache hit for {}", entry.getUrl());
entryCacheHit.mark();
}
cache.setLastEntries(feed, currentEntries);
if (subscriptions == null) {
feed.setMessage("No new entries found");
} else if (!subscriptions.isEmpty()) {
List<User> users = subscriptions.stream().map(FeedSubscription::getUser).collect(Collectors.toList());
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(users.toArray(new User[0]));
}
currentEntries.add(cacheKey);
}
cache.setLastEntries(feed, currentEntries);
if (config.getApplicationSettings().getPubsubhubbub()) {
handlePubSub(feed);
}
if (!ok) {
// requeue asap
feed.setDisabledUntil(new Date(0));
if (subscriptions == null) {
feed.setMessage("No new entries found");
} else if (insertedAtLeastOneEntry) {
List<User> users = subscriptions.stream().map(FeedSubscription::getUser).collect(Collectors.toList());
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(users.toArray(new User[0]));
// notify over websocket
subscriptions.forEach(sub -> webSocketSessions.sendMessage(sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub)));
}
}
if (Boolean.TRUE.equals(config.getApplicationSettings().getPubsubhubbub())) {
handlePubSub(feed);
}
if (!processed) {
// requeue asap
feed.setDisabledUntil(new Date(0));
}
if (insertedAtLeastOneEntry) {
feedUpdated.mark();
queues.giveBack(feed);
}
@Override
public boolean isUrgent() {
return context.isUrgent();
}
UnitOfWork.run(sessionFactory, () -> feedService.save(feed));
return processed;
}
@AllArgsConstructor
private static class AddEntryResult {
private final boolean processed;
private final boolean inserted;
}
}

View File

@@ -1,6 +1,6 @@
package com.commafeed.backend.feed;
import java.util.Date;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -10,117 +10,100 @@ import javax.inject.Singleton;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.feed.FeedRefreshExecutor.Task;
import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import io.dropwizard.lifecycle.Managed;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
/**
* Calls {@link FeedFetcher} and handles its outcome
*
* Calls {@link FeedFetcher} and updates the Feed object, but does not update the database ({@link FeedRefreshUpdater} does that)
*/
@Slf4j
@Singleton
public class FeedRefreshWorker implements Managed {
public class FeedRefreshWorker {
private final FeedRefreshUpdater feedRefreshUpdater;
private final FeedRefreshIntervalCalculator refreshIntervalCalculator;
private final FeedFetcher fetcher;
private final FeedQueues queues;
private final CommaFeedConfiguration config;
private final FeedRefreshExecutor pool;
private final Meter feedFetched;
@Inject
public FeedRefreshWorker(FeedRefreshUpdater feedRefreshUpdater, FeedFetcher fetcher, FeedQueues queues, CommaFeedConfiguration config,
public FeedRefreshWorker(FeedRefreshIntervalCalculator refreshIntervalCalculator, FeedFetcher fetcher, CommaFeedConfiguration config,
MetricRegistry metrics) {
this.feedRefreshUpdater = feedRefreshUpdater;
this.refreshIntervalCalculator = refreshIntervalCalculator;
this.fetcher = fetcher;
this.config = config;
this.queues = queues;
int threads = config.getApplicationSettings().getBackgroundThreads();
pool = new FeedRefreshExecutor("feed-refresh-worker", threads, Math.min(20 * threads, 1000), metrics);
this.feedFetched = metrics.meter(MetricRegistry.name(getClass(), "feedFetched"));
}
@Override
public void start() throws Exception {
}
@Override
public void stop() throws Exception {
pool.shutdown();
}
public void updateFeed(FeedRefreshContext context) {
pool.execute(new FeedTask(context));
}
private void update(FeedRefreshContext context) {
Feed feed = context.getFeed();
int refreshInterval = config.getApplicationSettings().getRefreshIntervalMinutes();
Date disabledUntil = DateUtils.addMinutes(new Date(), refreshInterval);
public FeedRefreshWorkerResult update(Feed feed) {
try {
String url = Optional.ofNullable(feed.getUrlAfterRedirect()).orElse(feed.getUrl());
FetchedFeed fetchedFeed = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
FeedFetcherResult feedFetcherResult = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
feed.getLastPublishedDate(), feed.getLastContentHash());
// stops here if NotModifiedException or any other exception is thrown
List<FeedEntry> entries = fetchedFeed.getEntries();
List<FeedEntry> entries = feedFetcherResult.getEntries();
Integer maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity();
if (maxFeedCapacity > 0) {
entries = entries.stream().limit(maxFeedCapacity).collect(Collectors.toList());
}
if (config.getApplicationSettings().getHeavyLoad()) {
disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed.getFeed().getLastEntryDate(),
fetchedFeed.getFeed().getAverageEntryInterval(), disabledUntil);
}
String urlAfterRedirect = fetchedFeed.getUrlAfterRedirect();
String urlAfterRedirect = feedFetcherResult.getUrlAfterRedirect();
if (StringUtils.equals(url, urlAfterRedirect)) {
urlAfterRedirect = null;
}
feed.setUrlAfterRedirect(urlAfterRedirect);
feed.setLink(fetchedFeed.getFeed().getLink());
feed.setLastModifiedHeader(fetchedFeed.getFeed().getLastModifiedHeader());
feed.setEtagHeader(fetchedFeed.getFeed().getEtagHeader());
feed.setLastContentHash(fetchedFeed.getFeed().getLastContentHash());
feed.setLastPublishedDate(fetchedFeed.getFeed().getLastPublishedDate());
feed.setAverageEntryInterval(fetchedFeed.getFeed().getAverageEntryInterval());
feed.setLastEntryDate(fetchedFeed.getFeed().getLastEntryDate());
feed.setLink(feedFetcherResult.getFeed().getLink());
feed.setLastModifiedHeader(feedFetcherResult.getFeed().getLastModifiedHeader());
feed.setEtagHeader(feedFetcherResult.getFeed().getEtagHeader());
feed.setLastContentHash(feedFetcherResult.getFeed().getLastContentHash());
feed.setLastPublishedDate(feedFetcherResult.getFeed().getLastPublishedDate());
feed.setAverageEntryInterval(feedFetcherResult.getFeed().getAverageEntryInterval());
feed.setLastEntryDate(feedFetcherResult.getFeed().getLastEntryDate());
feed.setErrorCount(0);
feed.setMessage(null);
feed.setDisabledUntil(disabledUntil);
feed.setDisabledUntil(refreshIntervalCalculator.onFetchSuccess(feedFetcherResult.getFeed()));
handlePubSub(feed, fetchedFeed.getFeed());
context.setEntries(entries);
feedRefreshUpdater.updateFeed(context);
handlePubSub(feed, feedFetcherResult.getFeed());
return new FeedRefreshWorkerResult(feed, entries);
} catch (NotModifiedException e) {
log.debug("Feed not modified : {} - {}", feed.getUrl(), e.getMessage());
if (config.getApplicationSettings().getHeavyLoad()) {
disabledUntil = FeedUtils.buildDisabledUntil(feed.getLastEntryDate(), feed.getAverageEntryInterval(), disabledUntil);
}
feed.setErrorCount(0);
feed.setMessage(e.getMessage());
feed.setDisabledUntil(disabledUntil);
feed.setDisabledUntil(refreshIntervalCalculator.onFeedNotModified(feed));
queues.giveBack(feed);
if (e.getNewLastModifiedHeader() != null) {
feed.setLastModifiedHeader(e.getNewLastModifiedHeader());
}
if (e.getNewEtagHeader() != null) {
feed.setEtagHeader(e.getNewEtagHeader());
}
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);
feed.setErrorCount(feed.getErrorCount() + 1);
feed.setMessage(message);
feed.setDisabledUntil(FeedUtils.buildDisabledUntil(feed.getErrorCount()));
feed.setDisabledUntil(refreshIntervalCalculator.onFetchError(feed));
queues.giveBack(feed);
return new FeedRefreshWorkerResult(feed, Collections.emptyList());
} finally {
feedFetched.mark();
}
}
@@ -146,22 +129,10 @@ public class FeedRefreshWorker implements Managed {
}
}
private class FeedTask implements Task {
private final FeedRefreshContext context;
public FeedTask(FeedRefreshContext context) {
this.context = context;
}
@Override
public void run() {
update(context);
}
@Override
public boolean isUrgent() {
return context.isUrgent();
}
@Value
public static class FeedRefreshWorkerResult {
Feed feed;
List<FeedEntry> entries;
}
}

View File

@@ -8,7 +8,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
@@ -20,7 +19,6 @@ import org.ahocorasick.trie.Trie.TrieBuilder;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
@@ -30,6 +28,8 @@ 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;
@@ -43,7 +43,6 @@ import com.ibm.icu.text.CharsetDetector;
import com.ibm.icu.text.CharsetMatch;
import com.steadystate.css.parser.CSSOMParser;
import edu.uci.ics.crawler4j.url.URLCanonicalizer;
import lombok.extern.slf4j.Slf4j;
/**
@@ -181,7 +180,10 @@ public class FeedUtils {
if (url == null) {
return null;
}
String normalized = URLCanonicalizer.getCanonicalURL(url);
ParsedUrl parsedUrl = ParsedUrl.parseUrl(url);
Canonicalizer.AGGRESSIVE.canonicalize(parsedUrl);
String normalized = parsedUrl.toString();
if (normalized == null) {
normalized = url;
}
@@ -359,57 +361,6 @@ public class FeedUtils {
return sb.toString();
}
/**
* When there was an error fetching the feed
*
*/
public static Date buildDisabledUntil(int errorCount) {
Date now = new Date();
int retriesBeforeDisable = 3;
if (errorCount >= retriesBeforeDisable) {
int disabledHours = errorCount - retriesBeforeDisable + 1;
disabledHours = Math.min(24 * 7, disabledHours);
return DateUtils.addHours(now, disabledHours);
}
return now;
}
/**
* When the feed was refreshed successfully
*/
public static Date buildDisabledUntil(Date publishedDate, Long averageEntryInterval, Date defaultRefreshInterval) {
Date now = new Date();
if (publishedDate == null) {
// feed with no entries, recheck in 24 hours
return DateUtils.addHours(now, 24);
} else if (publishedDate.before(DateUtils.addMonths(now, -1))) {
// older than a month, recheck in 24 hours
return DateUtils.addHours(now, 24);
} else if (publishedDate.before(DateUtils.addDays(now, -14))) {
// older than two weeks, recheck in 12 hours
return DateUtils.addHours(now, 12);
} else if (publishedDate.before(DateUtils.addDays(now, -7))) {
// older than a week, recheck in 6 hours
return DateUtils.addHours(now, 6);
} else if (averageEntryInterval != null) {
// use average time between entries to decide when to refresh next, divided by factor
int factor = 2;
// not more than 6 hours
long date = Math.min(DateUtils.addHours(now, 6).getTime(), now.getTime() + averageEntryInterval / factor);
// not less than default refresh interval
date = Math.max(defaultRefreshInterval.getTime(), date);
return new Date(date);
} else {
// unknown case, recheck in 24 hours
return DateUtils.addHours(now, 24);
}
}
public static Long averageTimeBetweenEntries(List<FeedEntry> entries) {
if (entries.isEmpty() || entries.size() == 1) {
return null;

View File

@@ -1,23 +0,0 @@
package com.commafeed.backend.feed;
import java.util.ArrayList;
import java.util.List;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class FetchedFeed {
private Feed feed = new Feed();
private List<FeedEntry> entries = new ArrayList<>();
private String title;
private String urlAfterRedirect;
private long fetchDuration;
}

View File

@@ -1,11 +1,9 @@
package com.commafeed.backend.model;
import java.util.Date;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@@ -123,7 +121,4 @@ public class Feed extends AbstractModel {
@Temporal(TemporalType.TIMESTAMP)
private Date pushLastPing;
@OneToMany(mappedBy = "feed")
private Set<FeedSubscription> subscriptions;
}

View File

@@ -31,7 +31,7 @@ public class UserSettings extends AbstractModel {
}
public enum ViewMode {
title, cozy, expanded
title, cozy, detailed, expanded
}
@OneToOne(fetch = FetchType.LAZY)

View File

@@ -25,7 +25,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class StartupService implements Managed {
public class DatabaseStartupService implements Managed {
private final SessionFactory sessionFactory;
private final UserDAO userDAO;

View File

@@ -7,18 +7,24 @@ import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.codec.digest.DigestUtils;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.feed.FeedEntryKeyword;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class FeedEntryService {
@@ -26,8 +32,45 @@ public class FeedEntryService {
private final FeedSubscriptionDAO feedSubscriptionDAO;
private final FeedEntryDAO feedEntryDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final FeedEntryContentService feedEntryContentService;
private final FeedEntryFilteringService feedEntryFilteringService;
private final CacheService cache;
/**
* this is NOT thread-safe
*/
public boolean addEntry(Feed feed, FeedEntry entry, List<FeedSubscription> subscriptions) {
Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed);
if (existing != null) {
return false;
}
FeedEntryContent content = feedEntryContentService.findOrCreate(entry.getContent(), feed.getLink());
entry.setGuidHash(DigestUtils.sha1Hex(entry.getGuid()));
entry.setContent(content);
entry.setInserted(new Date());
entry.setFeed(feed);
feedEntryDAO.saveOrUpdate(entry);
// if filter does not match the entry, mark it as read
for (FeedSubscription sub : subscriptions) {
boolean matches = true;
try {
matches = feedEntryFilteringService.filterMatchesEntry(sub.getFilter(), entry);
} catch (FeedEntryFilteringService.FeedEntryFilterException e) {
log.error("could not evaluate filter {}", sub.getFilter(), e);
}
if (!matches) {
FeedEntryStatus status = new FeedEntryStatus(sub.getUser(), sub, entry);
status.setRead(true);
feedEntryStatusDAO.saveOrUpdate(status);
}
}
return true;
}
public void markEntry(User user, Long entryId, boolean read) {
FeedEntry entry = feedEntryDAO.findById(entryId);

View File

@@ -50,6 +50,14 @@ public class FeedService {
return feed;
}
public void save(Feed feed) {
String normalized = FeedUtils.normalizeURL(feed.getUrl());
feed.setNormalizedUrl(normalized);
feed.setNormalizedUrlHash(DigestUtils.sha1Hex(normalized));
feed.setLastUpdated(new Date());
feedDAO.saveOrUpdate(feed);
}
public Favicon fetchFavicon(Feed feed) {
Favicon icon = null;

View File

@@ -14,7 +14,7 @@ import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.feed.FeedQueues;
import com.commafeed.backend.feed.FeedRefreshEngine;
import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory;
@@ -35,7 +35,7 @@ public class FeedSubscriptionService {
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final FeedSubscriptionDAO feedSubscriptionDAO;
private final FeedService feedService;
private final FeedQueues queues;
private final FeedRefreshEngine feedRefreshEngine;
private final CacheService cache;
private final CommaFeedConfiguration config;
@@ -76,7 +76,7 @@ public class FeedSubscriptionService {
sub.setTitle(FeedUtils.truncate(title, 128));
feedSubscriptionDAO.saveOrUpdate(sub);
queues.add(feed, false);
feedRefreshEngine.refreshImmediately(feed);
cache.invalidateUserRootCategory(user);
return sub.getId();
}
@@ -96,7 +96,7 @@ public class FeedSubscriptionService {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
for (FeedSubscription sub : subs) {
Feed feed = sub.getFeed();
queues.add(feed, true);
feedRefreshEngine.refreshImmediately(feed);
}
}

View File

@@ -1,67 +0,0 @@
package com.commafeed.backend.service;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.codec.digest.DigestUtils;
import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class FeedUpdateService {
private final FeedEntryDAO feedEntryDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final FeedEntryContentService feedEntryContentService;
private final FeedEntryFilteringService feedEntryFilteringService;
/**
* this is NOT thread-safe
*/
public boolean addEntry(Feed feed, FeedEntry entry, List<FeedSubscription> subscriptions) {
Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed);
if (existing != null) {
return false;
}
FeedEntryContent content = feedEntryContentService.findOrCreate(entry.getContent(), feed.getLink());
entry.setGuidHash(DigestUtils.sha1Hex(entry.getGuid()));
entry.setContent(content);
entry.setInserted(new Date());
entry.setFeed(feed);
feedEntryDAO.saveOrUpdate(entry);
// if filter does not match the entry, mark it as read
for (FeedSubscription sub : subscriptions) {
boolean matches = true;
try {
matches = feedEntryFilteringService.filterMatchesEntry(sub.getFilter(), entry);
} catch (FeedEntryFilterException e) {
log.error("could not evaluate filter {}", sub.getFilter(), e);
}
if (!matches) {
FeedEntryStatus status = new FeedEntryStatus(sub.getUser(), sub, entry);
status.setRead(true);
feedEntryStatusDAO.saveOrUpdate(status);
}
}
return true;
}
}

View File

@@ -17,10 +17,11 @@ import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.hibernate.SessionFactory;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.feed.FeedQueues;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.Feed;
import com.commafeed.frontend.resource.PubSubHubbubCallbackREST;
@@ -38,7 +39,8 @@ import lombok.extern.slf4j.Slf4j;
public class PubSubService {
private final CommaFeedConfiguration config;
private final FeedQueues queues;
private final FeedService feedService;
private final SessionFactory sessionFactory;
public void subscribe(Feed feed) {
String hub = feed.getPushHub();
@@ -73,7 +75,7 @@ public class PubSubService {
if (code == 400 && StringUtils.contains(message, pushpressError)) {
String[] tokens = message.split(" ");
feed.setPushTopic(tokens[tokens.length - 1]);
queues.giveBack(feed);
UnitOfWork.run(sessionFactory, () -> feedService.save(feed));
log.debug("handled pushpress subfeed {} : {}", topic, feed.getPushTopic());
} else {
throw new Exception(

View File

@@ -19,6 +19,9 @@ public class Category implements Serializable {
@ApiModelProperty(value = "parent category id")
private String parentId;
@ApiModelProperty(value = "parent category name")
private String parentName;
@ApiModelProperty(value = "category id", required = true)
private String name;

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