From f6f229e4cc058a4b76fb077269a9caa0758f8401 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Thu, 5 Nov 2026 15:41:13 +0100 Subject: [PATCH 1/2] Add option to flag articles as updated --- com.woltlab.wcf/package.xml | 1 + .../templates/articleContentHeaderTitle.tpl | 11 ++++ .../templates/articleListItems.tpl | 1 + .../update_com.woltlab.wcf_6.3_step1.php | 7 +++ ....woltlab.wcf_6.3_articleLastUpdateTime.php | 11 ++++ .../lib/acp/form/ArticleAddForm.class.php | 51 +++++++++++++++++++ .../files/lib/data/article/Article.class.php | 8 +-- .../lib/data/article/ArticleAction.class.php | 4 ++ .../lib/data/article/ArticleBuilder.class.php | 36 +++++++++++++ .../data/article/TaggedArticleList.class.php | 2 +- .../data/article/ViewableArticle.class.php | 8 +-- .../category/ArticleCategoryCache.class.php | 4 +- .../files/lib/page/ArticlePage.class.php | 14 ++--- .../lib/page/ArticleRssFeedPage.class.php | 4 +- .../system/importer/ArticleImporter.class.php | 4 ++ .../listView/user/ArticleListView.class.php | 6 +-- .../user/RelatedArticleListView.class.php | 2 +- .../WatchedArticleListPageHandler.class.php | 4 +- .../object/ArticleSitemapObject.class.php | 19 +++++++ wcfsetup/install/lang/de.xml | 7 ++- wcfsetup/install/lang/en.xml | 7 ++- wcfsetup/setup/db/install_com.woltlab.wcf.php | 5 ++ 22 files changed, 189 insertions(+), 27 deletions(-) create mode 100644 wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_articleLastUpdateTime.php diff --git a/com.woltlab.wcf/package.xml b/com.woltlab.wcf/package.xml index 2ec5a44d1b9..3a013029303 100644 --- a/com.woltlab.wcf/package.xml +++ b/com.woltlab.wcf/package.xml @@ -58,6 +58,7 @@ Required order of the following steps for the update to 6.3: acp/update_com.woltlab.wcf_6.3_likeObject.php acp/database/update_com.woltlab.wcf_6.3_step1.php + acp/update_com.woltlab.wcf_6.3_articleLastUpdateTime.php acp/update_com.woltlab.wcf_6.3_conditions.php The `objectType` PIP must run after `update_com.woltlab.wcf_6.3_conditions.php`, because deleting the legacy condition object types cascades to `wcf1_condition`. diff --git a/com.woltlab.wcf/templates/articleContentHeaderTitle.tpl b/com.woltlab.wcf/templates/articleContentHeaderTitle.tpl index 82fa8f7899b..cf7211528e5 100644 --- a/com.woltlab.wcf/templates/articleContentHeaderTitle.tpl +++ b/com.woltlab.wcf/templates/articleContentHeaderTitle.tpl @@ -16,8 +16,16 @@ {icon name='clock'} {time time=$article->time} + + {if $article->flagAsUpdated} +
  • + {icon name='arrows-rotate'} + {time time=$article->lastUpdateTime} +
  • + {/if} + {if $articleContent->getReadingTime()}
  • {icon name='hourglass'} @@ -61,6 +69,9 @@ {if $article->isFeatured} {lang}wcf.article.featured{/lang} {/if} + {if $article->flagAsUpdated} + {lang}wcf.article.updated{/lang} + {/if} {if $article->isDeleted} {lang}wcf.message.status.deleted{/lang} {/if} diff --git a/com.woltlab.wcf/templates/articleListItems.tpl b/com.woltlab.wcf/templates/articleListItems.tpl index 4771e8747f5..51dfc8e4854 100644 --- a/com.woltlab.wcf/templates/articleListItems.tpl +++ b/com.woltlab.wcf/templates/articleListItems.tpl @@ -31,6 +31,7 @@
    {content} {if $article->isFeatured}{lang}wcf.article.featured{/lang}{/if} + {if $article->flagAsUpdated}{lang}wcf.article.updated{/lang}{/if} {if $article->isDeleted}{lang}wcf.message.status.deleted{/lang}{/if} {if !$article->isPublished()}{lang}wcf.message.status.disabled{/lang}{/if} diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php index 382dd6c9bd1..33f9302a997 100644 --- a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php @@ -35,6 +35,13 @@ ->defaultValue(0) ->drop(), DefaultFalseBooleanDatabaseTableColumn::create('isFeatured'), + DefaultFalseBooleanDatabaseTableColumn::create('flagAsUpdated'), + NotNullInt10DatabaseTableColumn::create('lastUpdateTime') + ->defaultValue(0), + ]) + ->indices([ + DatabaseTableIndex::create('lastUpdateTime') + ->columns(['lastUpdateTime']), ]), PartialDatabaseTable::create('wcf1_article_content') ->columns([ diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_articleLastUpdateTime.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_articleLastUpdateTime.php new file mode 100644 index 00000000000..243745eae3b --- /dev/null +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_articleLastUpdateTime.php @@ -0,0 +1,11 @@ +prepare($sql); +$statement->execute([0]); diff --git a/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php b/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php index ac26e3e52eb..70b19185c40 100644 --- a/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php @@ -26,6 +26,7 @@ use wcf\system\form\builder\field\AbstractFormField; use wcf\system\form\builder\field\BooleanFormField; use wcf\system\form\builder\field\DateFormField; +use wcf\system\form\builder\field\dependency\NonEmptyFormFieldDependency; use wcf\system\form\builder\field\dependency\ValueFormFieldDependency; use wcf\system\form\builder\field\HiddenFormField; use wcf\system\form\builder\field\IFormField; @@ -245,6 +246,56 @@ protected function createForm(): void ->loadValueCallback(static function (Article $object, IFormField $field) { $field->value($object->time); }), + BooleanFormField::create('flagAsUpdated') + ->label('wcf.acp.article.flagAsUpdated') + ->description('wcf.acp.article.flagAsUpdated.description') + // The flag drives the time of the last update. + ->saveValueCallback(static function (ArticleBuilder $builder, IFormField $field) { + $dateField = $field->getDocument()->getFormField('lastUpdateTime'); + $builder->setLastUpdate( + (bool)$field->getSaveValue() + && $dateField !== null + && $dateField->getSaveValue() !== null + ? (int)$dateField->getSaveValue() + : null + ); + }) + ->loadValueCallback(static function (Article $object, IFormField $field) { + $field->value((bool)$object->flagAsUpdated); + }), + DateFormField::create('lastUpdateTime') + ->supportTime() + ->required() + ->label('wcf.acp.article.lastUpdateTime') + ->value(\TIME_NOW) + ->addDependency( + NonEmptyFormFieldDependency::create('flagAsUpdated') + ->fieldId('flagAsUpdated') + ) + ->addValidator(new FormFieldValidator('timeCheck', static function (IFormField $field) { + $timeField = $field->getDocument()->getFormField('time'); + $time = $timeField !== null ? (int)$timeField->getSaveValue() : 0; + $lastUpdateTime = (int)$field->getSaveValue(); + + // Delayed articles may be flagged as updated in the future, + // published articles must not. + $statusField = $field->getDocument()->getFormField('publicationStatus'); + $status = $statusField !== null + ? (int)$statusField->getSaveValue() + : Article::UNPUBLISHED; + + if ( + $lastUpdateTime < $time + || ($status === Article::PUBLISHED && $lastUpdateTime > \TIME_NOW) + ) { + $field->addValidationError(new FormFieldValidationError('invalid')); + } + })) + ->loadValueCallback(static function (Article $object, IFormField $field) { + if ($object->flagAsUpdated !== 0) { + $field->value($object->lastUpdateTime); + } + }), RadioButtonFormField::create('publicationStatus') ->label('wcf.acp.article.publicationStatus') ->options([ diff --git a/wcfsetup/install/files/lib/data/article/Article.class.php b/wcfsetup/install/files/lib/data/article/Article.class.php index 7316ba3f7eb..c1d38dd92ab 100644 --- a/wcfsetup/install/files/lib/data/article/Article.class.php +++ b/wcfsetup/install/files/lib/data/article/Article.class.php @@ -43,6 +43,8 @@ * @property-read 0|1 $isDeleted is 1 if the article is in trash bin, otherwise 0 * @property-read 0|1 $hasLabels is `1` if labels are assigned to the article * @property-read 0|1 $isFeatured is `1` if the article is featured which pins it at the top, otherwise `0` + * @property-read 0|1 $flagAsUpdated is `1` if the article is flagged as updated, otherwise `0` + * @property-read int $lastUpdateTime timestamp of the last update or the publication time if the article is not flagged as updated * * @extends CollectionDatabaseObject */ @@ -495,7 +497,7 @@ public function getVisitTime(): int */ public function isNew(): bool { - return $this->time > $this->getEffectiveVisitTime(); + return $this->lastUpdateTime > $this->getEffectiveVisitTime(); } /** @@ -562,12 +564,12 @@ public static function getUnreadArticles(): int $conditionBuilder = new PreparedStatementConditionBuilder(); $conditionBuilder->add('article.categoryID IN (?)', [$categoryIDs]); $conditionBuilder->add( - 'article.time > ?', + 'article.lastUpdateTime > ?', [VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.article')] ); $conditionBuilder->add('article.isDeleted = ?', [0]); $conditionBuilder->add('article.publicationStatus = ?', [Article::PUBLISHED]); - $conditionBuilder->add('(article.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)'); + $conditionBuilder->add('(article.lastUpdateTime > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)'); $sql = "SELECT COUNT(*) FROM wcf1_article article diff --git a/wcfsetup/install/files/lib/data/article/ArticleAction.class.php b/wcfsetup/install/files/lib/data/article/ArticleAction.class.php index 6cf78b81e63..279751364d0 100644 --- a/wcfsetup/install/files/lib/data/article/ArticleAction.class.php +++ b/wcfsetup/install/files/lib/data/article/ArticleAction.class.php @@ -94,6 +94,10 @@ class ArticleAction extends AbstractDatabaseObjectAction #[\Override] public function create() { + if (!isset($this->parameters['data']['lastUpdateTime'])) { + $this->parameters['data']['lastUpdateTime'] = $this->parameters['data']['time'] ?? \TIME_NOW; + } + $article = parent::create(); // save article content diff --git a/wcfsetup/install/files/lib/data/article/ArticleBuilder.class.php b/wcfsetup/install/files/lib/data/article/ArticleBuilder.class.php index de2bd898885..a19584f80a0 100644 --- a/wcfsetup/install/files/lib/data/article/ArticleBuilder.class.php +++ b/wcfsetup/install/files/lib/data/article/ArticleBuilder.class.php @@ -58,9 +58,45 @@ public function setTime(int $time): static { $this->properties['time'] = $time; + // The time of the last update follows the publication time unless the + // article is flagged as updated, it must never precede it though. + if ($this->isFlaggedAsUpdated()) { + $this->properties['lastUpdateTime'] = \max($this->getLastUpdateTime(), $time); + } else { + $this->properties['lastUpdateTime'] = $time; + } + + return $this; + } + + /** + * Flags the article as updated at the given time, or removes the flag when + * passing `null`, causing the time of the last update to fall back to the + * publication time. + */ + public function setLastUpdate(?int $lastUpdateTime): static + { + if ($lastUpdateTime === null) { + $this->properties['flagAsUpdated'] = 0; + $this->properties['lastUpdateTime'] = $this->properties['time'] ?? $this->existingObject->time ?? 0; + } else { + $this->properties['flagAsUpdated'] = 1; + $this->properties['lastUpdateTime'] = $lastUpdateTime; + } + return $this; } + private function isFlaggedAsUpdated(): bool + { + return (bool)($this->properties['flagAsUpdated'] ?? $this->existingObject->flagAsUpdated ?? 0); + } + + private function getLastUpdateTime(): int + { + return (int)($this->properties['lastUpdateTime'] ?? $this->existingObject->lastUpdateTime ?? 0); + } + public function setCategory(ArticleCategory $category): static { $this->properties['categoryID'] = $category->categoryID; diff --git a/wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php b/wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php index 98c38198ed7..819f2d1e5d4 100644 --- a/wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php +++ b/wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php @@ -22,7 +22,7 @@ public function __construct(Tag|array $tags) { parent::__construct(); - $this->sqlOrderBy = 'article.time ' . \ARTICLE_SORT_ORDER; + $this->sqlOrderBy = 'article.lastUpdateTime ' . \ARTICLE_SORT_ORDER; $subselect = TagEngine::getInstance()->getSubselectForObjectsByTags( 'com.woltlab.wcf.article', diff --git a/wcfsetup/install/files/lib/data/article/ViewableArticle.class.php b/wcfsetup/install/files/lib/data/article/ViewableArticle.class.php index 203ae81b3f1..e91e658190c 100644 --- a/wcfsetup/install/files/lib/data/article/ViewableArticle.class.php +++ b/wcfsetup/install/files/lib/data/article/ViewableArticle.class.php @@ -162,12 +162,12 @@ private static function fetchUnreadArticlesForCategory(int $articleCategoryID) $conditionBuilder = new PreparedStatementConditionBuilder(); $conditionBuilder->add('article.categoryID IN (?)', [$categoryIDs]); $conditionBuilder->add( - 'article.time > ?', + 'article.lastUpdateTime > ?', [VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.article')] ); $conditionBuilder->add('article.isDeleted = ?', [0]); $conditionBuilder->add('article.publicationStatus = ?', [Article::PUBLISHED]); - $conditionBuilder->add('(article.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)'); + $conditionBuilder->add('(article.lastUpdateTime > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)'); $sql = "SELECT COUNT(*) FROM wcf1_article article @@ -204,12 +204,12 @@ public static function getWatchedUnreadArticles() $conditionBuilder = new PreparedStatementConditionBuilder(); $conditionBuilder->add('article.categoryID IN (?)', [$categoryIDs]); $conditionBuilder->add( - 'article.time > ?', + 'article.lastUpdateTime > ?', [VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.article')] ); $conditionBuilder->add('article.isDeleted = ?', [0]); $conditionBuilder->add('article.publicationStatus = ?', [Article::PUBLISHED]); - $conditionBuilder->add('(article.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)'); + $conditionBuilder->add('(article.lastUpdateTime > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)'); $sql = "SELECT COUNT(*) FROM wcf1_article article diff --git a/wcfsetup/install/files/lib/data/article/category/ArticleCategoryCache.class.php b/wcfsetup/install/files/lib/data/article/category/ArticleCategoryCache.class.php index a8cf883602b..d248107d7ec 100644 --- a/wcfsetup/install/files/lib/data/article/category/ArticleCategoryCache.class.php +++ b/wcfsetup/install/files/lib/data/article/category/ArticleCategoryCache.class.php @@ -120,12 +120,12 @@ protected function initUnreadArticles(): void $conditionBuilder = new PreparedStatementConditionBuilder(); $conditionBuilder->add( - 'article.time > ?', + 'article.lastUpdateTime > ?', [VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.article')] ); $conditionBuilder->add('article.isDeleted = ?', [0]); $conditionBuilder->add('article.publicationStatus = ?', [Article::PUBLISHED]); - $conditionBuilder->add('(article.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)'); + $conditionBuilder->add('(article.lastUpdateTime > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)'); $sql = "SELECT COUNT(*) AS count, article.categoryID FROM wcf1_article article diff --git a/wcfsetup/install/files/lib/page/ArticlePage.class.php b/wcfsetup/install/files/lib/page/ArticlePage.class.php index acf9c995499..e84242e2708 100644 --- a/wcfsetup/install/files/lib/page/ArticlePage.class.php +++ b/wcfsetup/install/files/lib/page/ArticlePage.class.php @@ -171,10 +171,10 @@ protected function loadNextArticle(): void { $articleList = new CategoryArticleList($this->article->categoryID); $articleList->getConditionBuilder()->add( - 'article.time ' . (\ARTICLE_SORT_ORDER === 'DESC' ? '>' : '<') . ' ?', - [$this->article->time] + 'article.lastUpdateTime ' . (\ARTICLE_SORT_ORDER === 'DESC' ? '>' : '<') . ' ?', + [$this->article->lastUpdateTime] ); - $articleList->sqlOrderBy = 'article.time ' . (\ARTICLE_SORT_ORDER === 'DESC' ? 'ASC' : 'DESC'); + $articleList->sqlOrderBy = 'article.lastUpdateTime ' . (\ARTICLE_SORT_ORDER === 'DESC' ? 'ASC' : 'DESC'); $articleList->sqlLimit = 1; $articleList->readObjects(); foreach ($articleList as $article) { @@ -186,10 +186,10 @@ protected function loadPreviousArticle(): void { $articleList = new CategoryArticleList($this->article->categoryID); $articleList->getConditionBuilder()->add( - 'article.time ' . (\ARTICLE_SORT_ORDER === 'DESC' ? '<' : '>') . ' ?', - [$this->article->time] + 'article.lastUpdateTime ' . (\ARTICLE_SORT_ORDER === 'DESC' ? '<' : '>') . ' ?', + [$this->article->lastUpdateTime] ); - $articleList->sqlOrderBy = 'article.time ' . \ARTICLE_SORT_ORDER; + $articleList->sqlOrderBy = 'article.lastUpdateTime ' . \ARTICLE_SORT_ORDER; $articleList->sqlLimit = 1; $articleList->readObjects(); foreach ($articleList as $article) { @@ -227,7 +227,7 @@ protected function setMetaTags(): void MetaTagHandler::getInstance()->addTag( 'article:modified_time', 'article:modified_time', - \gmdate('c', $this->article->time), + \gmdate('c', $this->article->lastUpdateTime), true ); MetaTagHandler::getInstance()->addTag( diff --git a/wcfsetup/install/files/lib/page/ArticleRssFeedPage.class.php b/wcfsetup/install/files/lib/page/ArticleRssFeedPage.class.php index 5af95de042e..a6fcce0cab3 100644 --- a/wcfsetup/install/files/lib/page/ArticleRssFeedPage.class.php +++ b/wcfsetup/install/files/lib/page/ArticleRssFeedPage.class.php @@ -59,7 +59,7 @@ public function readData() } else { $this->articles = new AccessibleArticleList(); } - $this->articles->sqlOrderBy = 'article.time ' . \ARTICLE_SORT_ORDER; + $this->articles->sqlOrderBy = 'article.lastUpdateTime ' . \ARTICLE_SORT_ORDER; $this->articles->sqlLimit = 20; $this->articles->readObjects(); } @@ -85,7 +85,7 @@ protected function getRssFeed(): RssFeed } if ($this->articles->valid()) { - $channel->lastBuildDateFromTimestamp($this->articles->current()->getTime()); + $channel->lastBuildDateFromTimestamp($this->articles->current()->lastUpdateTime); } $feed->channel($channel); diff --git a/wcfsetup/install/files/lib/system/importer/ArticleImporter.class.php b/wcfsetup/install/files/lib/system/importer/ArticleImporter.class.php index 798b1307e71..b2f4096a2b0 100644 --- a/wcfsetup/install/files/lib/system/importer/ArticleImporter.class.php +++ b/wcfsetup/install/files/lib/system/importer/ArticleImporter.class.php @@ -100,6 +100,10 @@ public function import(mixed $oldID, array $data, array $additionalData = []) $data['enableComments'] = \ARTICLE_ENABLE_COMMENTS_DEFAULT_VALUE !== 0 ? 1 : 0; } + if (!isset($data['lastUpdateTime'])) { + $data['lastUpdateTime'] = $data['time'] ?? \TIME_NOW; + } + // save article $article = ArticleEditor::create($data); diff --git a/wcfsetup/install/files/lib/system/listView/user/ArticleListView.class.php b/wcfsetup/install/files/lib/system/listView/user/ArticleListView.class.php index a4d961e0bc2..ff2abecdf30 100644 --- a/wcfsetup/install/files/lib/system/listView/user/ArticleListView.class.php +++ b/wcfsetup/install/files/lib/system/listView/user/ArticleListView.class.php @@ -37,7 +37,7 @@ class ArticleListView extends AbstractListView public function __construct() { $this->addAvailableSortFields([ - new ListViewSortField('time', 'wcf.global.date', defaultSortOrder: 'DESC'), + new ListViewSortField('time', 'wcf.global.date', 'article.lastUpdateTime', 'DESC'), new ListViewSortField('title', 'wcf.global.title', 'title'), new ListViewSortField('views', 'wcf.article.sortField.views', defaultSortOrder: 'DESC'), ]); @@ -160,7 +160,7 @@ protected function getUnreadFilter(): BooleanFilter public function applyFilter(DatabaseObjectList $list, string $value): void { $list->getConditionBuilder()->add( - 'article.time > ?', + 'article.lastUpdateTime > ?', [VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.article')] ); @@ -169,7 +169,7 @@ public function applyFilter(DatabaseObjectList $list, string $value): void ON tracked_visit.objectTypeID = " . VisitTracker::getInstance()->getObjectTypeID('com.woltlab.wcf.article') . " AND tracked_visit.objectID = article.articleID AND tracked_visit.userID = " . WCF::getUser()->userID; - $list->getConditionBuilder()->add("(article.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)"); + $list->getConditionBuilder()->add("(article.lastUpdateTime > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)"); $list->sqlConditionJoins .= $joins; $list->sqlJoins .= $joins; } diff --git a/wcfsetup/install/files/lib/system/listView/user/RelatedArticleListView.class.php b/wcfsetup/install/files/lib/system/listView/user/RelatedArticleListView.class.php index 5841038b343..675aead1a41 100644 --- a/wcfsetup/install/files/lib/system/listView/user/RelatedArticleListView.class.php +++ b/wcfsetup/install/files/lib/system/listView/user/RelatedArticleListView.class.php @@ -93,7 +93,7 @@ private function getRelatedArticleIDs(int $articleContentID): array " . $conditionBuilder . " GROUP BY tag_to_object.objectID HAVING COUNT(*) >= " . \round(\count($tags) * \ARTICLE_RELATED_ARTICLES_MATCH_THRESHOLD / 100) . " - ORDER BY count DESC, MAX(article.time) DESC"; + ORDER BY count DESC, MAX(article.lastUpdateTime) DESC"; $statement = WCF::getDB()->prepare($sql, \ARTICLE_RELATED_ARTICLES * 4); $statement->execute($conditionBuilder->getParameters()); diff --git a/wcfsetup/install/files/lib/system/page/handler/WatchedArticleListPageHandler.class.php b/wcfsetup/install/files/lib/system/page/handler/WatchedArticleListPageHandler.class.php index 19d1b2979c7..d8f69d9b610 100644 --- a/wcfsetup/install/files/lib/system/page/handler/WatchedArticleListPageHandler.class.php +++ b/wcfsetup/install/files/lib/system/page/handler/WatchedArticleListPageHandler.class.php @@ -50,12 +50,12 @@ private static function getWatchedUnreadArticles(): int $conditionBuilder = new PreparedStatementConditionBuilder(); $conditionBuilder->add('article.categoryID IN (?)', [$categoryIDs]); $conditionBuilder->add( - 'article.time > ?', + 'article.lastUpdateTime > ?', [VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.article')] ); $conditionBuilder->add('article.isDeleted = ?', [0]); $conditionBuilder->add('article.publicationStatus = ?', [Article::PUBLISHED]); - $conditionBuilder->add('(article.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)'); + $conditionBuilder->add('(article.lastUpdateTime > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)'); $sql = "SELECT COUNT(*) FROM wcf1_article article diff --git a/wcfsetup/install/files/lib/system/sitemap/object/ArticleSitemapObject.class.php b/wcfsetup/install/files/lib/system/sitemap/object/ArticleSitemapObject.class.php index 15d7fb1ff69..e671eccbe5d 100644 --- a/wcfsetup/install/files/lib/system/sitemap/object/ArticleSitemapObject.class.php +++ b/wcfsetup/install/files/lib/system/sitemap/object/ArticleSitemapObject.class.php @@ -24,6 +24,25 @@ public function getObjectClass() return ArticleContent::class; } + #[\Override] + public function getObjectList() + { + $articleContentList = new ArticleContentList(); + $articleContentList->sqlSelects = 'article.lastUpdateTime'; + $articleContentList->sqlConditionJoins = ' + INNER JOIN wcf1_article article + ON article.articleID = article_content.articleID'; + $articleContentList->sqlJoins = $articleContentList->sqlConditionJoins; + + return $articleContentList; + } + + #[\Override] + public function getLastModifiedColumn(): string + { + return 'lastUpdateTime'; + } + #[\Override] public function canView(DatabaseObject $object) { diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 15d709d3379..891e2e36010 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -88,6 +88,9 @@ getTitle()}“ wirklich kopieren? Die Kopie wird nicht veröffentlicht.]]> + + + @@ -97,7 +100,6 @@ - @@ -3318,6 +3320,7 @@ freigeschaltet. {if LANGUAGE_USE_INFORMAL_VARIANT}Du kannst{else}Sie können{/if + @@ -3974,6 +3977,7 @@ Dateianhänge: + @@ -5912,5 +5916,6 @@ Erlaubte Dateiendungen: {', '|implode:$allowedFileExtensions}]]> + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index bc1913baf89..bd6ec5aa734 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -88,6 +88,9 @@ getTitle()}”? The copy will not be published.]]> + + + @@ -97,7 +100,6 @@ - @@ -3243,6 +3245,7 @@ Your account on {@PAGE_TITLE|phrase} [URL:{link isEmail=true}{/link}] has been a + @@ -3920,6 +3923,7 @@ Attachments: + @@ -5914,5 +5918,6 @@ Allowed extensions: {', '|implode:$allowedFileExtensions}]]> + diff --git a/wcfsetup/setup/db/install_com.woltlab.wcf.php b/wcfsetup/setup/db/install_com.woltlab.wcf.php index a873083a91b..26d631049a1 100644 --- a/wcfsetup/setup/db/install_com.woltlab.wcf.php +++ b/wcfsetup/setup/db/install_com.woltlab.wcf.php @@ -436,12 +436,17 @@ DefaultFalseBooleanDatabaseTableColumn::create('isDeleted'), DefaultFalseBooleanDatabaseTableColumn::create('hasLabels'), DefaultFalseBooleanDatabaseTableColumn::create('isFeatured'), + DefaultFalseBooleanDatabaseTableColumn::create('flagAsUpdated'), + NotNullInt10DatabaseTableColumn::create('lastUpdateTime') + ->defaultValue(0), ]) ->indices([ DatabaseTablePrimaryIndex::create() ->columns(['articleID']), DatabaseTableIndex::create('time') ->columns(['time']), + DatabaseTableIndex::create('lastUpdateTime') + ->columns(['lastUpdateTime']), ]) ->foreignKeys([ DatabaseTableForeignKey::create() From 5136797373e43022b9ae0543723fcab524f2a9fb Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Thu, 5 Nov 2026 16:26:38 +0100 Subject: [PATCH 2/2] Show updated flag and time of update in the article grid view --- .../gridView/admin/ArticleGridView.class.php | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/wcfsetup/install/files/lib/system/gridView/admin/ArticleGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/ArticleGridView.class.php index 09afb8c2d67..d78910cf6da 100644 --- a/wcfsetup/install/files/lib/system/gridView/admin/ArticleGridView.class.php +++ b/wcfsetup/install/files/lib/system/gridView/admin/ArticleGridView.class.php @@ -153,6 +153,15 @@ public function render(mixed $value, DatabaseObject $row): string WCF::getLanguage()->get('wcf.article.featured') ); } + if ($row->flagAsUpdated !== 0) { + if ($badges !== '') { + $badges .= ' '; + } + $badges .= \sprintf( + '%s', + WCF::getLanguage()->get('wcf.article.updated') + ); + } // @phpstan-ignore property.notFound $articleTitle = StringUtil::encodeHTML($row->title); @@ -185,6 +194,25 @@ public function render(mixed $value, DatabaseObject $row): string ->sortable(defaultSortOrder: 'DESC') ->renderer(new TimeColumnRenderer()) ->filter(TimeFilter::class), + GridViewColumn::for('lastUpdateTime') + ->label('wcf.acp.article.lastUpdateTime') + ->sortable(defaultSortOrder: 'DESC') + ->renderer(new class extends TimeColumnRenderer { + #[\Override] + public function render(mixed $value, DatabaseObject $row): string + { + \assert($row instanceof Article); + + // Articles that are not flagged as updated use the + // publication time, which the `time` column already shows. + if ($row->flagAsUpdated === 0) { + return ''; + } + + return parent::render($value, $row); + } + }) + ->filter(TimeFilter::class), ]); $this->addAvailableFilters([ @@ -213,6 +241,7 @@ public function applyFilter(DatabaseObjectList $list, string $value): void ), new BooleanFilter('isDeleted', 'wcf.acp.article.isDeleted'), new BooleanFilter('isFeatured', 'wcf.article.featured'), + new BooleanFilter('flagAsUpdated', 'wcf.article.updated'), ]); $provider = new ArticleInteractions(); $provider->addInteractions([