Add Infrequent tab and corresponding user setting

This commit is contained in:
2026-03-17 21:38:17 -05:00
parent d05c5b9d7f
commit e2a1630adc
16 changed files with 126 additions and 6 deletions

View File

@@ -18,6 +18,13 @@ const categories: Record<string, Omit<Category, "name">> = {
feeds: [], feeds: [],
position: 1, position: 1,
}, },
infrequent: {
id: "infrequent",
expanded: false,
children: [],
feeds: [],
position: 2,
},
} }
const sharing: { const sharing: {
@@ -105,6 +112,7 @@ export const Constants = {
tooltip: { tooltip: {
delay: 500, delay: 500,
}, },
infrequentThresholdDaysDefault: 7,
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension", browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
customCssDocumentationUrl: "https://athou.github.io/commafeed/documentation/custom-css", customCssDocumentationUrl: "https://athou.github.io/commafeed/documentation/custom-css",
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e", bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",

View File

@@ -31,6 +31,7 @@ export interface Subscription {
filterLegacy?: string filterLegacy?: string
pushNotificationsEnabled: boolean pushNotificationsEnabled: boolean
autoMarkAsReadAfterDays?: number autoMarkAsReadAfterDays?: number
averageEntryIntervalMs?: number
} }
export interface Category { export interface Category {
@@ -284,6 +285,7 @@ export interface Settings {
unreadCountTitle: boolean unreadCountTitle: boolean
unreadCountFavicon: boolean unreadCountFavicon: boolean
disablePullToRefresh: boolean disablePullToRefresh: boolean
infrequentThresholdDays: number
primaryColor?: string primaryColor?: string
sharingSettings: SharingSettings sharingSettings: SharingSettings
pushNotificationSettings: PushNotificationSettings pushNotificationSettings: PushNotificationSettings

View File

@@ -7,6 +7,7 @@ import {
changeDisablePullToRefresh, changeDisablePullToRefresh,
changeEntriesToKeepOnTopWhenScrolling, changeEntriesToKeepOnTopWhenScrolling,
changeExternalLinkIconDisplayMode, changeExternalLinkIconDisplayMode,
changeInfrequentThresholdDays,
changeLanguage, changeLanguage,
changeMarkAllAsReadConfirmation, changeMarkAllAsReadConfirmation,
changeMarkAllAsReadNavigateToUnread, changeMarkAllAsReadNavigateToUnread,
@@ -141,6 +142,10 @@ export const userSlice = createSlice({
if (!state.settings) return if (!state.settings) return
state.settings.disablePullToRefresh = action.meta.arg state.settings.disablePullToRefresh = action.meta.arg
}) })
builder.addCase(changeInfrequentThresholdDays.pending, (state, action) => {
if (!state.settings) return
state.settings.infrequentThresholdDays = action.meta.arg
})
builder.addCase(changePrimaryColor.pending, (state, action) => { builder.addCase(changePrimaryColor.pending, (state, action) => {
if (!state.settings) return if (!state.settings) return
state.settings.primaryColor = action.meta.arg state.settings.primaryColor = action.meta.arg
@@ -171,6 +176,7 @@ export const userSlice = createSlice({
changeUnreadCountTitle.fulfilled, changeUnreadCountTitle.fulfilled,
changeUnreadCountFavicon.fulfilled, changeUnreadCountFavicon.fulfilled,
changeDisablePullToRefresh.fulfilled, changeDisablePullToRefresh.fulfilled,
changeInfrequentThresholdDays.fulfilled,
changePrimaryColor.fulfilled, changePrimaryColor.fulfilled,
changeSharingSetting.fulfilled, changeSharingSetting.fulfilled,
changePushNotificationSettings.fulfilled changePushNotificationSettings.fulfilled

View File

@@ -158,6 +158,15 @@ export const changeSharingSetting = createAppAsyncThunk(
} }
) )
export const changeInfrequentThresholdDays = createAppAsyncThunk(
"settings/infrequentThresholdDays",
(infrequentThresholdDays: number, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, infrequentThresholdDays })
}
)
export const changePushNotificationSettings = createAppAsyncThunk( export const changePushNotificationSettings = createAppAsyncThunk(
"settings/pushNotificationSettings", "settings/pushNotificationSettings",
(pushNotificationSettings: PushNotificationSettings, thunkApi) => { (pushNotificationSettings: PushNotificationSettings, thunkApi) => {

View File

@@ -26,20 +26,22 @@ export function flattenCategoryTree(category: TreeCategory): TreeCategory[] {
return categories return categories
} }
export function categoryUnreadCount(category?: TreeCategory): number { export function categoryUnreadCount(category?: TreeCategory, maxFrequencyThresholdMs?: number): number {
if (!category) return 0 if (!category) return 0
return flattenCategoryTree(category) return flattenCategoryTree(category)
.flatMap(c => c.feeds) .flatMap(c => c.feeds)
.filter(f => !maxFrequencyThresholdMs || (f.averageEntryIntervalMs && f.averageEntryIntervalMs >= maxFrequencyThresholdMs))
.map(f => f.unread) .map(f => f.unread)
.reduce((total, current) => total + current, 0) .reduce((total, current) => total + current, 0)
} }
export function categoryHasNewEntries(category?: TreeCategory): boolean { export function categoryHasNewEntries(category?: TreeCategory, maxFrequencyThresholdMs?: number): boolean {
if (!category) return false if (!category) return false
return flattenCategoryTree(category) return flattenCategoryTree(category)
.flatMap(c => c.feeds) .flatMap(c => c.feeds)
.filter(f => !maxFrequencyThresholdMs || (f.averageEntryIntervalMs && f.averageEntryIntervalMs >= maxFrequencyThresholdMs))
.some(f => f.hasNewEntries) .some(f => f.hasNewEntries)
} }

View File

@@ -12,6 +12,7 @@ import {
changeDisablePullToRefresh, changeDisablePullToRefresh,
changeEntriesToKeepOnTopWhenScrolling, changeEntriesToKeepOnTopWhenScrolling,
changeExternalLinkIconDisplayMode, changeExternalLinkIconDisplayMode,
changeInfrequentThresholdDays,
changeLanguage, changeLanguage,
changeMarkAllAsReadConfirmation, changeMarkAllAsReadConfirmation,
changeMarkAllAsReadNavigateToUnread, changeMarkAllAsReadNavigateToUnread,
@@ -44,6 +45,7 @@ export function DisplaySettings() {
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle) const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon) const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
const disablePullToRefresh = useAppSelector(state => state.user.settings?.disablePullToRefresh) const disablePullToRefresh = useAppSelector(state => state.user.settings?.disablePullToRefresh)
const infrequentThresholdDays = useAppSelector(state => state.user.settings?.infrequentThresholdDays)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings) const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const primaryColor = useAppSelector(state => state.user.settings?.primaryColor) || Constants.theme.defaultPrimaryColor const primaryColor = useAppSelector(state => state.user.settings?.primaryColor) || Constants.theme.defaultPrimaryColor
const { _ } = useLingui() const { _ } = useLingui()
@@ -143,6 +145,14 @@ export function DisplaySettings() {
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))} onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
/> />
<NumberInput
label={<Trans>Infrequent posts threshold (days)</Trans>}
description={<Trans>Feeds posting less often than this (on average) will appear in the Infrequent view</Trans>}
min={1}
value={infrequentThresholdDays}
onChange={async value => await dispatch(changeInfrequentThresholdDays(+value))}
/>
<Divider label={<Trans>Scrolling</Trans>} labelPosition="center" /> <Divider label={<Trans>Scrolling</Trans>} labelPosition="center" />
<Switch <Switch

View File

@@ -1,7 +1,7 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { Box, Stack } from "@mantine/core" import { Box, Stack } from "@mantine/core"
import React from "react" import React from "react"
import { TbChevronDown, TbChevronRight, TbInbox, TbStar, TbTag } from "react-icons/tb" import { TbChevronDown, TbChevronRight, TbClock, TbInbox, TbStar, TbTag } from "react-icons/tb"
import { Constants } from "@/app/constants" import { Constants } from "@/app/constants"
import { import {
redirectToCategory, redirectToCategory,
@@ -23,6 +23,7 @@ import { TreeSearch } from "./TreeSearch"
const allIcon = <TbInbox size={16} /> const allIcon = <TbInbox size={16} />
const starredIcon = <TbStar size={16} /> const starredIcon = <TbStar size={16} />
const infrequentIcon = <TbClock size={16} />
const tagIcon = <TbTag size={16} /> const tagIcon = <TbTag size={16} />
const expandedIcon = <TbChevronDown size={16} /> const expandedIcon = <TbChevronDown size={16} />
const collapsedIcon = <TbChevronRight size={16} /> const collapsedIcon = <TbChevronRight size={16} />
@@ -34,6 +35,10 @@ export function Tree() {
const source = useAppSelector(state => state.entries.source) const source = useAppSelector(state => state.entries.source)
const tags = useAppSelector(state => state.user.tags) const tags = useAppSelector(state => state.user.tags)
const showRead = useAppSelector(state => state.user.settings?.showRead) const showRead = useAppSelector(state => state.user.settings?.showRead)
const infrequentThresholdDays = useAppSelector(
state => state.user.settings?.infrequentThresholdDays ?? Constants.infrequentThresholdDaysDefault
)
const infrequentThresholdMs = infrequentThresholdDays * 24 * 3600 * 1000
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const isFeedDisplayed = (feed: Subscription) => { const isFeedDisplayed = (feed: Subscription) => {
@@ -115,6 +120,22 @@ export function Tree() {
onClick={categoryClicked} onClick={categoryClicked}
/> />
) )
const infrequentCategoryNode = () => (
<TreeNode
id={Constants.categories.infrequent.id}
type="category"
name={<Trans>Infrequent</Trans>}
icon={infrequentIcon}
unread={categoryUnreadCount(root, infrequentThresholdMs)}
hasNewEntries={categoryHasNewEntries(root, infrequentThresholdMs)}
selected={source.type === "category" && source.id === Constants.categories.infrequent.id}
expanded={false}
level={0}
hasError={false}
hasWarning={false}
onClick={categoryClicked}
/>
)
const categoryNode = (category: Category, level = 0) => { const categoryNode = (category: Category, level = 0) => {
if (!isCategoryDisplayed(category)) return null if (!isCategoryDisplayed(category)) return null
@@ -197,6 +218,7 @@ export function Tree() {
<Box className="cf-tree"> <Box className="cf-tree">
{allCategoryNode()} {allCategoryNode()}
{starredCategoryNode()} {starredCategoryNode()}
{infrequentCategoryNode()}
{root.children.map(c => recursiveCategoryNode(c))} {root.children.map(c => recursiveCategoryNode(c))}
{root.feeds.map(f => feedNode(f))} {root.feeds.map(f => feedNode(f))}
{tags?.map(tag => tagNode(tag))} {tags?.map(tag => tagNode(tag))}

View File

@@ -405,6 +405,10 @@ msgstr "Feed name"
msgid "Feed URL" msgid "Feed URL"
msgstr "Feed URL" msgstr "Feed URL"
#: src/components/settings/DisplaySettings.tsx
msgid "Feeds posting less often than this (on average) will appear in the Infrequent view"
msgstr "Feeds posting less often than this (on average) will appear in the Infrequent view"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "Fetch all my feeds now" msgstr "Fetch all my feeds now"
@@ -502,6 +506,14 @@ msgstr "In expanded view, scrolling through entries mark them as read"
msgid "Indigo" msgid "Indigo"
msgstr "Indigo" msgstr "Indigo"
#: src/components/sidebar/Tree.tsx
msgid "Infrequent"
msgstr "Infrequent"
#: src/components/settings/DisplaySettings.tsx
msgid "Infrequent posts threshold (days)"
msgstr "Infrequent posts threshold (days)"
#: src/pages/auth/InitialSetupPage.tsx #: src/pages/auth/InitialSetupPage.tsx
msgid "Initial Setup" msgid "Initial Setup"
msgstr "Initial Setup" msgstr "Initial Setup"

View File

@@ -98,7 +98,7 @@ public class Feed extends AbstractModel {
private String etagHeader; private String etagHeader;
/** /**
* average time between entries in the feed * average time between entries in the feed in milliseconds
*/ */
private Long averageEntryInterval; private Long averageEntryInterval;

View File

@@ -146,6 +146,8 @@ public class UserSettings extends AbstractModel {
private boolean unreadCountFavicon; private boolean unreadCountFavicon;
private boolean disablePullToRefresh; private boolean disablePullToRefresh;
private int infrequentThresholdDays;
private boolean email; private boolean email;
private boolean gmail; private boolean gmail;
private boolean facebook; private boolean facebook;

View File

@@ -76,6 +76,9 @@ public class Settings implements Serializable {
@Schema(description = "disable pull to refresh", required = true) @Schema(description = "disable pull to refresh", required = true)
private boolean disablePullToRefresh; private boolean disablePullToRefresh;
@Schema(description = "threshold in days for the infrequent view", required = true)
private int infrequentThresholdDays;
@Schema(description = "primary theme color to use in the UI") @Schema(description = "primary theme color to use in the UI")
private String primaryColor; private String primaryColor;

View File

@@ -71,6 +71,9 @@ public class Subscription implements Serializable {
@Schema(description = "automatically mark entries as read after this many days (null to disable)") @Schema(description = "automatically mark entries as read after this many days (null to disable)")
private Integer autoMarkAsReadAfterDays; private Integer autoMarkAsReadAfterDays;
@Schema(description = "average time in milliseconds between entries in this feed, null if unknown")
private Long averageEntryIntervalMs;
public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) { public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) {
FeedCategory category = subscription.getCategory(); FeedCategory category = subscription.getCategory();
Feed feed = subscription.getFeed(); Feed feed = subscription.getFeed();
@@ -93,6 +96,7 @@ public class Subscription implements Serializable {
sub.setFilterLegacy(subscription.getFilterLegacy()); sub.setFilterLegacy(subscription.getFilterLegacy());
sub.setPushNotificationsEnabled(subscription.isPushNotificationsEnabled()); sub.setPushNotificationsEnabled(subscription.isPushNotificationsEnabled());
sub.setAutoMarkAsReadAfterDays(subscription.getAutoMarkAsReadAfterDays()); sub.setAutoMarkAsReadAfterDays(subscription.getAutoMarkAsReadAfterDays());
sub.setAverageEntryIntervalMs(feed.getAverageEntryInterval());
return sub; return sub;
} }

View File

@@ -40,12 +40,14 @@ import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.dao.UserSettingsDAO;
import com.commafeed.backend.feed.FeedEntryKeyword; import com.commafeed.backend.feed.FeedEntryKeyword;
import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.model.UserSettings.ReadingMode; import com.commafeed.backend.model.UserSettings.ReadingMode;
import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.service.FeedEntryService; import com.commafeed.backend.service.FeedEntryService;
@@ -83,6 +85,7 @@ public class CategoryREST {
public static final String ALL = "all"; public static final String ALL = "all";
public static final String STARRED = "starred"; public static final String STARRED = "starred";
public static final String INFREQUENT = "infrequent";
private final AuthenticationContext authenticationContext; private final AuthenticationContext authenticationContext;
private final FeedCategoryDAO feedCategoryDAO; private final FeedCategoryDAO feedCategoryDAO;
@@ -90,6 +93,7 @@ public class CategoryREST {
private final FeedSubscriptionDAO feedSubscriptionDAO; private final FeedSubscriptionDAO feedSubscriptionDAO;
private final FeedEntryService feedEntryService; private final FeedEntryService feedEntryService;
private final FeedSubscriptionService feedSubscriptionService; private final FeedSubscriptionService feedSubscriptionService;
private final UserSettingsDAO userSettingsDAO;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
private final UriInfo uri; private final UriInfo uri;
@@ -139,11 +143,15 @@ public class CategoryREST {
} }
User user = authenticationContext.getCurrentUser(); User user = authenticationContext.getCurrentUser();
if (ALL.equals(id)) { if (ALL.equals(id) || INFREQUENT.equals(id)) {
entries.setName(Optional.ofNullable(tag).orElse("All")); entries.setName(Optional.ofNullable(tag).orElse("All"));
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user); List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
removeExcludedSubscriptions(subs, excludedIds); removeExcludedSubscriptions(subs, excludedIds);
if (INFREQUENT.equals(id)) {
entries.setName("Infrequent");
removeFrequentSubscriptions(subs, user);
}
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate, List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
offset, limit + 1, order, true, tag, null, null); offset, limit + 1, order, true, tag, null, null);
@@ -244,9 +252,12 @@ public class CategoryREST {
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords); List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
User user = authenticationContext.getCurrentUser(); User user = authenticationContext.getCurrentUser();
if (ALL.equals(req.getId())) { if (ALL.equals(req.getId()) || INFREQUENT.equals(req.getId())) {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user); List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
removeExcludedSubscriptions(subs, req.getExcludedSubscriptions()); removeExcludedSubscriptions(subs, req.getExcludedSubscriptions());
if (INFREQUENT.equals(req.getId())) {
removeFrequentSubscriptions(subs, user);
}
feedEntryService.markSubscriptionEntries(user, subs, olderThan, insertedBefore, entryKeywords); feedEntryService.markSubscriptionEntries(user, subs, olderThan, insertedBefore, entryKeywords);
} else if (STARRED.equals(req.getId())) { } else if (STARRED.equals(req.getId())) {
feedEntryService.markStarredEntries(user, olderThan, insertedBefore); feedEntryService.markStarredEntries(user, olderThan, insertedBefore);
@@ -260,6 +271,17 @@ public class CategoryREST {
return Response.ok().build(); return Response.ok().build();
} }
private void removeFrequentSubscriptions(List<FeedSubscription> subs, User user) {
UserSettings userSettings = userSettingsDAO.findByUser(user);
int infrequentDays = userSettings != null && userSettings.getInfrequentThresholdDays() > 0
? userSettings.getInfrequentThresholdDays()
: 7;
long infrequentThresholdMs = (long) infrequentDays * 24 * 3600 * 1000;
subs.removeIf(
sub -> sub.getFeed().getAverageEntryInterval() == null || sub.getFeed().getAverageEntryInterval() < infrequentThresholdMs);
}
private void removeExcludedSubscriptions(List<FeedSubscription> subs, List<Long> excludedIds) { private void removeExcludedSubscriptions(List<FeedSubscription> subs, List<Long> excludedIds) {
if (CollectionUtils.isNotEmpty(excludedIds)) { if (CollectionUtils.isNotEmpty(excludedIds)) {
subs.removeIf(sub -> excludedIds.contains(sub.getId())); subs.removeIf(sub -> excludedIds.contains(sub.getId()));

View File

@@ -133,6 +133,7 @@ public class UserREST {
s.setUnreadCountFavicon(settings.isUnreadCountFavicon()); s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
s.setDisablePullToRefresh(settings.isDisablePullToRefresh()); s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
s.setPrimaryColor(settings.getPrimaryColor()); s.setPrimaryColor(settings.getPrimaryColor());
s.setInfrequentThresholdDays(settings.getInfrequentThresholdDays());
if (settings.getPushNotifications() != null) { if (settings.getPushNotifications() != null) {
s.getPushNotificationSettings().setType(settings.getPushNotifications().getType()); s.getPushNotificationSettings().setType(settings.getPushNotifications().getType());
@@ -168,6 +169,7 @@ public class UserREST {
s.setUnreadCountTitle(false); s.setUnreadCountTitle(false);
s.setUnreadCountFavicon(true); s.setUnreadCountFavicon(true);
s.setDisablePullToRefresh(false); s.setDisablePullToRefresh(false);
s.setInfrequentThresholdDays(7);
} }
return s; return s;
} }
@@ -205,6 +207,7 @@ public class UserREST {
s.setUnreadCountFavicon(settings.isUnreadCountFavicon()); s.setUnreadCountFavicon(settings.isUnreadCountFavicon());
s.setDisablePullToRefresh(settings.isDisablePullToRefresh()); s.setDisablePullToRefresh(settings.isDisablePullToRefresh());
s.setPrimaryColor(settings.getPrimaryColor()); s.setPrimaryColor(settings.getPrimaryColor());
s.setInfrequentThresholdDays(settings.getInfrequentThresholdDays());
PushNotificationUserSettings ps = new PushNotificationUserSettings(); PushNotificationUserSettings ps = new PushNotificationUserSettings();
ps.setType(settings.getPushNotificationSettings().getType()); ps.setType(settings.getPushNotificationSettings().getType());

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="add-infrequent-days-threshold" author="athou">
<addColumn tableName="USERSETTINGS">
<column name="infrequentThresholdDays" type="INT" valueNumeric="7">
<constraints nullable="false" />
</column>
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@@ -38,5 +38,6 @@
<include file="changelogs/db.changelog-5.11.xml" /> <include file="changelogs/db.changelog-5.11.xml" />
<include file="changelogs/db.changelog-5.12.xml" /> <include file="changelogs/db.changelog-5.12.xml" />
<include file="changelogs/db.changelog-7.0.xml" /> <include file="changelogs/db.changelog-7.0.xml" />
<include file="changelogs/db.changelog-7.1.xml" />
</databaseChangeLog> </databaseChangeLog>