Compare commits

...

9 Commits
3.3.0 ... 3.3.2

Author SHA1 Message Date
Athou
6de817f539 release 3.3.2 2023-05-20 14:00:54 +02:00
Athou
08a2746921 restore entry selection indicator 2023-05-19 10:37:59 +02:00
Athou
bc28727e39 remove warnings 2023-05-17 16:14:21 +02:00
Athou
eceaf3a98d remove lodash to reduce bundle size by 100kb 2023-05-17 16:11:32 +02:00
Athou
4a8939e5e5 optimized png sizes 2023-05-17 15:47:22 +02:00
Athou
e90b80c641 send GA pageviews only if initialized 2023-05-17 13:43:25 +02:00
Athou
2979600cc2 add dividers to separate read-only information from forms 2023-05-11 11:45:23 +02:00
Athou
a2deef7f7f release 3.3.1 2023-05-10 20:26:45 +02:00
Athou
b5097d4fc3 fix long feed names not being shortened to respect tree max width (#1055) 2023-05-10 20:25:50 +02:00
21 changed files with 12105 additions and 12073 deletions

View File

@@ -1,12 +1,21 @@
# Changelog # Changelog
## [3.3.2]
- restore entry selection indicator (left orange border) that was lost with the mantine 6.x upgrade (3.3.0)
- add dividers to visually separate read-only information from forms on feed and category details pages
- reduced js bundle size by 10%
## [3.3.1]
- fix long feed names not being shortened to respect tree max width
## [3.3.0] ## [3.3.0]
- there are now database changes, rolling back to 2.x will no longer be possible - there are now database changes, rolling back to 2.x will no longer be possible
- restore support for user custom CSS rules - restore support for user custom CSS rules
- add support for user custom JS code that will be executed on page load - add support for user custom JS code that will be executed on page load
## [3.2.0] ## [3.2.0]
- restore the welcome page - restore the welcome page
@@ -28,10 +37,10 @@
## [3.0.1] ## [3.0.1]
- allow env variable substitution in config.yml - allow env variable substitution in config.yml
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with - e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with
its value its value
- allow env variable prefixed with `CF_` to override config.yml properties - allow env variable prefixed with `CF_` to override config.yml properties
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true` - e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true`
## [3.0.0] ## [3.0.0]

File diff suppressed because it is too large Load Diff

View File

@@ -1,84 +1,84 @@
{ {
"name": "commafeed-client", "name": "commafeed-client",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",
"dev:typescript": "tsc --watch", "dev:typescript": "tsc --watch",
"build": "npm run i18n:compile && tsc && vite build", "build": "npm run i18n:compile && tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest", "test": "vitest",
"test:ci": "vitest run", "test:ci": "vitest run",
"eslint": "eslint --ext=.js,.jsx,.ts,.tsx src", "eslint": "eslint --ext=.js,.jsx,.ts,.tsx src",
"i18n": "npm run i18n:extract && npm run i18n:compile", "i18n": "npm run i18n:extract && npm run i18n:compile",
"i18n:extract": "lingui extract --clean", "i18n:extract": "lingui extract --clean",
"i18n:compile": "lingui compile --typescript", "i18n:compile": "lingui compile --typescript",
"postinstall": "npm run i18n:compile" "postinstall": "npm run i18n:compile"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.0", "@emotion/react": "^11.11.0",
"@fontsource/open-sans": "^4.5.14", "@fontsource/open-sans": "^4.5.14",
"@lingui/core": "^4.0.0", "@lingui/core": "^4.0.0",
"@lingui/macro": "^4.0.0", "@lingui/macro": "^4.0.0",
"@lingui/react": "^4.0.0", "@lingui/react": "^4.0.0",
"@mantine/core": "^6.0.10", "@mantine/core": "^6.0.10",
"@mantine/form": "^6.0.10", "@mantine/form": "^6.0.10",
"@mantine/hooks": "^6.0.10", "@mantine/hooks": "^6.0.10",
"@mantine/modals": "^6.0.10", "@mantine/modals": "^6.0.10",
"@mantine/notifications": "^6.0.10", "@mantine/notifications": "^6.0.10",
"@mantine/spotlight": "^6.0.10", "@mantine/spotlight": "^6.0.10",
"@mantine/styles": "^6.0.10", "@mantine/styles": "^6.0.10",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"axios": "^1.4.0", "axios": "^1.4.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"interweave": "^13.1.0", "interweave": "^13.1.0",
"lodash": "^4.17.21", "mousetrap": "^1.6.5",
"mousetrap": "^1.6.5", "react": "^18.2.0",
"react": "^18.2.0", "react-async-hook": "^4.0.0",
"react-async-hook": "^4.0.0", "react-contexify": "^6.0.0",
"react-contexify": "^6.0.0", "react-dom": "^18.2.0",
"react-dom": "^18.2.0", "react-ga4": "^2.1.0",
"react-ga4": "^2.1.0", "react-icons": "^4.8.0",
"react-icons": "^4.8.0", "react-infinite-scroller": "^1.2.6",
"react-infinite-scroller": "^1.2.6", "react-redux": "^8.0.5",
"react-redux": "^8.0.5", "react-router-dom": "^6.11.1",
"react-router-dom": "^6.11.1", "react-swipeable": "^7.0.0",
"react-swipeable": "^7.0.0", "swagger-ui-react": "^4.18.3",
"swagger-ui-react": "^4.18.3", "throttle-debounce": "^5.0.0",
"tinycon": "^0.6.8", "tinycon": "^0.6.8",
"use-local-storage": "^3.0.0", "use-local-storage": "^3.0.0",
"websocket-heartbeat-js": "^1.1.2" "websocket-heartbeat-js": "^1.1.2"
}, },
"devDependencies": { "devDependencies": {
"@lingui/cli": "^4.0.0", "@lingui/cli": "^4.0.0",
"@lingui/vite-plugin": "^4.0.0", "@lingui/vite-plugin": "^4.0.0",
"@types/eslint": "^8.37.0", "@types/eslint": "^8.37.0",
"@types/lodash": "^4.14.194", "@types/mousetrap": "^1.6.11",
"@types/mousetrap": "^1.6.11", "@types/react": "^18.2.6",
"@types/react": "^18.2.6", "@types/react-dom": "^18.2.4",
"@types/react-dom": "^18.2.4", "@types/react-infinite-scroller": "^1.2.3",
"@types/react-infinite-scroller": "^1.2.3", "@types/swagger-ui-react": "^4.18.0",
"@types/swagger-ui-react": "^4.18.0", "@types/throttle-debounce": "^5.0.0",
"@types/tinycon": "^0.6.3", "@types/tinycon": "^0.6.3",
"@typescript-eslint/eslint-plugin": "^5.59.2", "@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2", "@typescript-eslint/parser": "^5.59.2",
"@vitejs/plugin-react": "^4.0.0", "@vitejs/plugin-react": "^4.0.0",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"eslint": "^8.40.0", "eslint": "^8.40.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-hooks": "^0.4.3", "eslint-plugin-hooks": "^0.4.3",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.9.0", "rollup-plugin-visualizer": "^5.9.0",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vite": "^4.3.5", "vite": "^4.3.5",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.0", "vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.31.0", "vitest": "^0.31.0",
"vitest-mock-extended": "^1.1.3" "vitest-mock-extended": "^1.1.3"
} }
} }

View File

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

View File

@@ -122,7 +122,7 @@ function GoogleAnalyticsHandler() {
}, [googleAnalyticsCode]) }, [googleAnalyticsCode])
useEffect(() => { useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname }) if (ReactGA.isInitialized) ReactGA.send({ hitType: "pageview", page: location.pathname })
}, [location]) }, [location])
return null return null

View File

@@ -64,3 +64,5 @@ export const openLinkInBackgroundTab = (url: string) => {
}) })
) )
} }
export const truncate = (str: string, n: number) => (str.length > n ? `${str.slice(0, n - 1)}\u2026` : str)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -18,9 +18,9 @@ import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { useMousetrap } from "hooks/useMousetrap" import { useMousetrap } from "hooks/useMousetrap"
import { useViewMode } from "hooks/useViewMode" import { useViewMode } from "hooks/useViewMode"
import throttle from "lodash/throttle"
import { useEffect } from "react" import { useEffect } from "react"
import InfiniteScroll from "react-infinite-scroller" import InfiniteScroll from "react-infinite-scroller"
import { throttle } from "throttle-debounce"
import { FeedEntry } from "./FeedEntry" import { FeedEntry } from "./FeedEntry"
export function FeedEntries() { export function FeedEntries() {
@@ -82,7 +82,7 @@ export function FeedEntries() {
) )
} }
} }
const throttledListener = throttle(listener, 100) const throttledListener = throttle(100, listener)
scrollArea?.addEventListener("scroll", throttledListener) scrollArea?.addEventListener("scroll", throttledListener)
return () => scrollArea?.removeEventListener("scroll", throttledListener) return () => scrollArea?.removeEventListener("scroll", throttledListener)
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry]) }, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])

View File

@@ -63,7 +63,8 @@ const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: View
} }
if (props.showSelectionIndicator) { if (props.showSelectionIndicator) {
styles.paper.borderLeftColor = theme.colorScheme === "dark" ? theme.colors.orange[4] : theme.colors.orange[6] const borderLeftColor = theme.colorScheme === "dark" ? theme.colors.orange[4] : theme.colors.orange[6]
styles.paper.borderLeftColor = `${borderLeftColor} !important`
} }
return styles return styles

View File

@@ -5,11 +5,11 @@ import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries"
import { redirectToFeed } from "app/slices/redirect" import { redirectToFeed } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types" import { Entry } from "app/types"
import { openLinkInBackgroundTab } from "app/utils" import { openLinkInBackgroundTab, truncate } from "app/utils"
import { throttle, truncate } from "lodash"
import { useEffect } from "react" import { useEffect } from "react"
import { Item, Menu, Separator, useContextMenu } from "react-contexify" import { Item, Menu, Separator, useContextMenu } from "react-contexify"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb" import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb"
import { throttle } from "throttle-debounce"
interface FeedEntryContextMenuProps { interface FeedEntryContextMenuProps {
entry: Entry entry: Entry
@@ -92,7 +92,7 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
> >
<Group> <Group>
<TbRss size={iconSize} /> <TbRss size={iconSize} />
<Trans>Go to {truncate(props.entry.feedName)}</Trans> <Trans>Go to {truncate(props.entry.feedName, 30)}</Trans>
</Group> </Group>
</Item> </Item>
</> </>
@@ -118,7 +118,7 @@ export function useFeedEntryContextMenu(entry: Entry) {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
const listener = () => contextMenu.hideAll() const listener = () => contextMenu.hideAll()
const throttledListener = throttle(listener, 100) const throttledListener = throttle(100, listener)
scrollArea?.addEventListener("scroll", throttledListener) scrollArea?.addEventListener("scroll", throttledListener)
return () => scrollArea?.removeEventListener("scroll", throttledListener) return () => scrollArea?.removeEventListener("scroll", throttledListener)

View File

@@ -7,9 +7,9 @@ import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types" import { Entry } from "app/types"
import { ActionButton } from "components/ActionButtton" import { ActionButton } from "components/ActionButtton"
import { ButtonToolbar } from "components/ButtonToolbar" import { ButtonToolbar } from "components/ButtonToolbar"
import { throttle } from "lodash"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb" import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
import { throttle } from "throttle-debounce"
import { ShareButtons } from "./ShareButtons" import { ShareButtons } from "./ShareButtons"
interface FeedEntryFooterProps { interface FeedEntryFooterProps {
@@ -38,7 +38,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
const listener = () => setScrollPosition(scrollArea ? scrollArea.scrollTop : 0) const listener = () => setScrollPosition(scrollArea ? scrollArea.scrollTop : 0)
const throttledListener = throttle(listener, 100) const throttledListener = throttle(100, listener)
scrollArea?.addEventListener("scroll", throttledListener) scrollArea?.addEventListener("scroll", throttledListener)
return () => scrollArea?.removeEventListener("scroll", throttledListener) return () => scrollArea?.removeEventListener("scroll", throttledListener)

View File

@@ -108,6 +108,8 @@ export function CategoryDetailsPage() {
{editable && ( {editable && (
<> <>
<Divider />
<TextInput label={<Trans>Name</Trans>} {...form.getInputProps("name")} required /> <TextInput label={<Trans>Name</Trans>} {...form.getInputProps("name")} required />
<CategorySelect <CategorySelect
label={<Trans>Parent Category</Trans>} label={<Trans>Parent Category</Trans>}

View File

@@ -151,6 +151,8 @@ export function FeedDetailsPage() {
</Box> </Box>
</Input.Wrapper> </Input.Wrapper>
<Divider />
<TextInput label={<Trans>Name</Trans>} {...form.getInputProps("name")} required /> <TextInput label={<Trans>Name</Trans>} {...form.getInputProps("name")} required />
<CategorySelect label={<Trans>Category</Trans>} {...form.getInputProps("categoryId")} clearable /> <CategorySelect label={<Trans>Category</Trans>} {...form.getInputProps("categoryId")} clearable />
<NumberInput label={<Trans>Position</Trans>} {...form.getInputProps("position")} required min={0} /> <NumberInput label={<Trans>Position</Trans>} {...form.getInputProps("position")} required min={0} />

View File

@@ -40,7 +40,7 @@ const sidebarRightBorderWidth = "1px"
const useStyles = createStyles(theme => ({ const useStyles = createStyles(theme => ({
sidebarContent: { sidebarContent: {
maxWidth: `calc(${Constants.layout.sidebarWidth} - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`, maxWidth: `calc(${Constants.layout.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`, maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
}, },

View File

@@ -6,15 +6,12 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>3.3.0</version> <version>3.3.2</version>
</parent> </parent>
<artifactId>commafeed-server</artifactId> <artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name> <name>CommaFeed Server</name>
<properties> <properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<guice.version>5.1.0</guice.version> <guice.version>5.1.0</guice.version>
<querydsl.version>4.4.0</querydsl.version> <querydsl.version>4.4.0</querydsl.version>
<rome.version>2.1.0</rome.version> <rome.version>2.1.0</rome.version>
@@ -42,14 +39,6 @@
</resources> </resources>
<plugins> <plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
@@ -237,7 +226,7 @@
<dependency> <dependency>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<version>3.3.0</version> <version>3.3.2</version>
</dependency> </dependency>
<dependency> <dependency>

View File

@@ -3,7 +3,6 @@ package com.commafeed.backend.cache;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.DefaultJedisClientConfig; import redis.clients.jedis.DefaultJedisClientConfig;
import redis.clients.jedis.HostAndPort; import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisClientConfig; import redis.clients.jedis.JedisClientConfig;
@@ -11,7 +10,6 @@ import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol; import redis.clients.jedis.Protocol;
@Slf4j
@Getter @Getter
public class RedisPoolFactory { public class RedisPoolFactory {

View File

@@ -20,6 +20,8 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
abstract class AbstractCustomCodeServlet extends HttpServlet { abstract class AbstractCustomCodeServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final SessionFactory sessionFactory; private final SessionFactory sessionFactory;
private final UserSettingsDAO userSettingsDAO; private final UserSettingsDAO userSettingsDAO;

View File

@@ -9,6 +9,8 @@ import com.commafeed.backend.model.UserSettings;
public class CustomCssServlet extends AbstractCustomCodeServlet { public class CustomCssServlet extends AbstractCustomCodeServlet {
private static final long serialVersionUID = 1L;
@Inject @Inject
public CustomCssServlet(SessionFactory sessionFactory, UserSettingsDAO userSettingsDAO) { public CustomCssServlet(SessionFactory sessionFactory, UserSettingsDAO userSettingsDAO) {
super(sessionFactory, userSettingsDAO); super(sessionFactory, userSettingsDAO);

View File

@@ -11,6 +11,8 @@ import com.commafeed.backend.model.UserSettings;
@Singleton @Singleton
public class CustomJsServlet extends AbstractCustomCodeServlet { public class CustomJsServlet extends AbstractCustomCodeServlet {
private static final long serialVersionUID = 1L;
@Inject @Inject
public CustomJsServlet(SessionFactory sessionFactory, UserSettingsDAO userSettingsDAO) { public CustomJsServlet(SessionFactory sessionFactory, UserSettingsDAO userSettingsDAO) {
super(sessionFactory, userSettingsDAO); super(sessionFactory, userSettingsDAO);

19
pom.xml
View File

@@ -1,18 +1,33 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <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"> 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> <modelVersion>4.0.0</modelVersion>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>3.3.0</version> <version>3.3.2</version>
<name>CommaFeed</name> <name>CommaFeed</name>
<packaging>pom</packaging> <packaging>pom</packaging>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties> </properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
</plugins>
</build>
<modules> <modules>
<module>commafeed-client</module> <module>commafeed-client</module>
<module>commafeed-server</module> <module>commafeed-server</module>