Version 5.0.0 beta 6

This commit is contained in:
Neo committed 2025-12-19 17:12:27 -08:00
1 parent 57d9db6ce2
commit fa2a720105
115 files changed
+1823 -1147

No files matched your search

+10 -54
View File
@@ -927,60 +927,6 @@ abstract class Comment extends Content
return Theme::i()->getTemplate( static::$commentTemplate[0][0], static::$commentTemplate[0][1], ( isset( static::$commentTemplate[0][2] ) ) ? static::$commentTemplate[0][2] : NULL )->$template( $this->item(), $this );
}
/**
* Users to receive immediate notifications
*
* @param int|array|null $limit LIMIT clause
* @param boolean $countOnly Just return the count
* @return Select|int
*/
public function notificationRecipients( int|array|null $limit=array( 0, 25 ), bool $countOnly=FALSE ): Select|int
{
/* Do we only want the count? */
if( $countOnly )
{
$count = 0;
$count += $this->author()->followersCount( 3, array( 'immediate' ), $this->mapped('date') );
if( IPS::classUsesTrait( $this->item(), 'IPS\Content\Followable' ) )
{
$count += $this->item()->followersCount( 3, array( 'immediate' ), $this->mapped('date') );
}
return $count;
}
$memberFollowers = $this->author()->followers( 3, array( 'immediate' ), $this->mapped('date'), NULL );
if( $memberFollowers !== NULL AND $memberFollowers !== 0 and IPS::classUsesTrait( $this->item(), 'IPS\Content\Followable' ) )
{
$unions = array(
$this->item()->followers( 3, array( 'immediate' ), $this->mapped('date'), NULL ),
$memberFollowers
);
return Db::i()->union( $unions, 'follow_added', $limit );
}
else if( IPS::classUsesTrait( $this->item(), 'IPS\Content\Followable' ) )
{
return $this->item()->followers( static::FOLLOW_PUBLIC + static::FOLLOW_ANONYMOUS, array( 'immediate' ), $this->mapped('date'), $limit, 'follow_added' );
}
else
{
return $memberFollowers;
}
}
/**
* Create Notification
*
* @param string|NULL $extra Additional data
* @return Notification
*/
protected function createNotification( string|null $extra=NULL ): Notification
{
return new Notification( Application::load( 'core' ), 'new_comment', $this->item(), array( $this ) );
}
/**
* Move Comment to another item
@@ -1764,6 +1710,16 @@ abstract class Comment extends Content
{
return NULL;
}
/**
* Custom where conditions used specifically for finding comments
*
* @return array|null
*/
public static function findCommentWhere() : array|null
{
return static::commentWhere();
}
/**
* Is a featured comment?
+15 -5
View File
@@ -1257,11 +1257,18 @@ abstract class Content extends ActiveRecord
{
return;
}
/* How many followers? */
try
{
$count = $this->notificationRecipients( NULL, TRUE );
if( $this instanceof Comment )
{
$count = $this->item()->notificationRecipientsForComments( null, true, $this );
}
else
{
$count = $this->notificationRecipients( NULL, TRUE );
}
}
catch ( BadMethodCallException $e )
{
@@ -1290,7 +1297,8 @@ abstract class Content extends ActiveRecord
*/
public function sendNotificationsBatch( int $offset=0, array &$sentTo=array(), string|null $extra=NULL ): int|NULL
{
if( !IPS::classUsesTrait( $this, Followable::class ) )
/* Stop here if the content does not use the "followable" trait */
if( ( $this instanceof Item and !IPS::classUsesTrait( $this, Followable::class ) ) or ( $this instanceof Comment and !IPS::classUsesTrait( $this->item(), Followable::class ) ) )
{
return null;
}
@@ -1303,7 +1311,9 @@ abstract class Content extends ActiveRecord
}
$followIds = array();
$followers = $this->notificationRecipients( array( $offset, static::NOTIFICATIONS_PER_BATCH ) );
$followers = ( $this instanceof Comment ) ?
$this->item()->notificationRecipientsForComments( array( $offset, static::NOTIFICATIONS_PER_BATCH ), false, $this ) :
$this->notificationRecipients( array( $offset, static::NOTIFICATIONS_PER_BATCH ) );
/* If $followers is NULL (which can be the case if the follows are just people following the author), just return as there is nothing to do */
if ( $followers === NULL )
@@ -1320,7 +1330,7 @@ abstract class Content extends ActiveRecord
}
/* Send notification */
$notification = $this->createNotification( $extra );
$notification = ( $this instanceof Comment ) ? $this->item()->createNotification( $extra, $this ) : $this->createNotification( $extra );
$notification->unsubscribeType = 'follow';
foreach ( $followers as $follower )
{
+21 -11
View File
@@ -887,7 +887,7 @@ class Controller extends CoverPhotoController
/* @var $databaseColumnMap array */
$titleField = $item::$databaseColumnMap['title'];
if( strlen( Request::i()->newTitle ) > $maxLength )
if( mb_strlen( Request::i()->newTitle ) > $maxLength )
{
throw new LengthException( Member::loggedIn()->language()->addToStack( 'form_maxlength', FALSE, array( 'pluralize' => array( $maxLength ) ) ) );
}
@@ -903,6 +903,12 @@ class Controller extends CoverPhotoController
throw new InvalidArgumentException( Member::loggedIn()->language()->addToStack( 'form_tags_not_allowed', FALSE, array( 'sprintf' => array( Request::i()->newTitle ) ) ) );
}
/* Stop doing anything if the title wasn't changed */
if( $item->$titleField == $newTitle->value )
{
\IPS\Output::i()->json( $item->$titleField );
}
$item->$titleField = $newTitle->value;
$idField = $item::$databaseColumnId;
$item->save();
@@ -916,14 +922,18 @@ class Controller extends CoverPhotoController
if( SearchContent::isSearchable( $item ) )
{
Index::i()->index( $item );
if( $item::$firstCommentRequired )
{
$class = $item->firstComment();
}
else
{
$class = $item;
}
Index::i()->index( $class );
}
/* Only add the mod log entry if we actually changed the title */
if( $item->$titleField !== $oldTitle )
{
Session::i()->modLog( 'modlog__comment_edit_title', array( (string) $item->url() => FALSE, $item->$titleField => FALSE, $oldTitle => FALSE ), $item );
}
Session::i()->modLog( 'modlog__comment_edit_title', array( (string) $item->url() => FALSE, $item->$titleField => FALSE, $oldTitle => FALSE ), $item );
Output::i()->json( $item->$titleField );
}
@@ -1906,7 +1916,7 @@ class Controller extends CoverPhotoController
$class = static::$contentModel;
$item = $class::loadAndCheckPerms( Request::i()->id );
$timeLastRead = $item->timeLastRead();
$timeLastRead = ( IPS::classUsesTrait( $item, ReadMarkers::class ) ) ? $item->timeLastRead() : null;
if ( $timeLastRead instanceof DateTime )
{
@@ -1935,7 +1945,7 @@ class Controller extends CoverPhotoController
}
else
{
if ( $item->unread() )
if ( IPS::classUsesTrait( $item, ReadMarkers::class ) and $item->unread() )
{
/* If we do not have a time last read set for this content, fallback to the reset time */
$resetTimes = Member::loggedIn()->markersResetTimes( $class::$application );
@@ -2694,9 +2704,9 @@ class Controller extends CoverPhotoController
$where[] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['hidden'] . '<>?', -3 );
}
if ( $commentClass::commentWhere() !== NULL )
if ( $commentClass::findCommentWhere() !== NULL )
{
$where[] = $commentClass::commentWhere();
$where[] = $commentClass::findCommentWhere();
}
if ( $container = $item->containerWrapper() )
{
+59 -1
View File
@@ -227,6 +227,11 @@ trait Followable
*/
public function notificationRecipients( int|array|null $limit=array( 0, 25 ), bool $countOnly=FALSE ): Select|int
{
if( $this instanceof Comment )
{
return $this->item()->notificationRecipientsForComments( $limit, $countOnly, $this );
}
/* Do we only want the count? */
if( $countOnly )
{
@@ -259,15 +264,68 @@ trait Followable
return static::containerFollowers( $this->container(), Content::FOLLOW_PUBLIC + Content::FOLLOW_ANONYMOUS, array( 'immediate' ), $this->mapped('date'), $limit, 'follow_added' );
}
}
/**
* Users to receive immediate notifications
*
* @param int|array|null $limit LIMIT clause
* @param boolean $countOnly Just return the count
* @param Comment|null $comment The comment that is triggering this notification
* @return Select|int
*/
public function notificationRecipientsForComments( int|array|null $limit=array( 0, 25 ), bool $countOnly=FALSE, ?Comment $comment=null ): Select|int
{
if( !( $comment instanceof Comment ) or !IPS::classUsesTrait( $this, Followable::class ) or ( !$comment->item() instanceof static ) )
{
return 0;
}
/* Do we only want the count? */
if( $countOnly )
{
$count = 0;
$count += $comment->author()->followersCount( 3, array( 'immediate' ), $comment->mapped('date') );
if( IPS::classUsesTrait( $this, 'IPS\Content\Followable' ) )
{
$count += $this->followersCount( 3, array( 'immediate' ), $comment->mapped('date') );
}
return $count;
}
$memberFollowers = $comment->author()->followers( 3, array( 'immediate' ), $comment->mapped('date'), NULL );
if( $memberFollowers !== 0 )
{
$unions = array(
$this->followers( 3, array( 'immediate' ), $comment->mapped('date'), NULL ),
$memberFollowers
);
return Db::i()->union( $unions, 'follow_added', $limit );
}
else
{
return $this->followers( static::FOLLOW_PUBLIC + static::FOLLOW_ANONYMOUS, array( 'immediate' ), $comment->mapped('date'), $limit, 'follow_added' );
}
}
/**
* Create Notification
*
* @param mixed $extra Additional data
* @param Comment|null $comment
* @return Notification
*/
public function createNotification( mixed $extra=NULL ): Notification
public function createNotification( mixed $extra=NULL, ?Comment $comment=null ): Notification
{
if( $comment instanceof Comment )
{
$key = ( $comment instanceof Review ) ? 'new_review' : 'new_comment';
return new Notification( Application::load( 'core' ), $key, $this, array( $comment ) );
}
// New content is sent with itself as the item as we deliberately do not group notifications about new content items. Unlike comments where you're going to read them all - you might scan the notifications list for topic titles you're interested in
return new Notification( Application::load( 'core' ), 'new_content', $this, array( $this ), $extra );
}
+8 -10
View File
@@ -6527,7 +6527,7 @@ abstract class Item extends Content
$member = $member ?: Member::loggedIn();
/* Check it isn't hidden */
if ( IPS::classUsesTrait( $this, 'IPS\Content\Hideble' ) and $this->hidden() )
if ( $this->hidden() !== 0 )
{
/* If we're a moderator who can see hidden items it's fine, unless it's a guest post before register */
if ( $this->hidden() !== -3 and static::canViewHiddenItems( $member, $this->containerWrapper() ) )
@@ -7147,10 +7147,11 @@ abstract class Item extends Content
if ( IPS::classUsesTrait( $this, 'IPS\Content\FuturePublishing' ) AND $this->isFutureDate() AND static::canFuturePublish($member, $this->container()))
{
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'action' => 'publish' ) ), languageString: 'publish', dataAttributes: ['data-confirm','title=\'{lang="publish_desc"}\''], identifier: 'publish' ) );
$publish = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'publish' ) ), languageString: 'publish', dataAttributes: [ 'title=\'{lang="publish_desc"}\'' ], identifier: 'publish' );
$publish->requiresConfirm();
$menu->add( $publish );
}
if( IPS::classUsesTrait( $this, 'IPS\Content\Pinnable' ) AND $this->canPin( $member ) )
{
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'pin' ) ), 'pin', identifier: 'pin' ) );
@@ -7217,8 +7218,7 @@ abstract class Item extends Content
if( $this->canUnarchive( $member ) )
{
$unarchive = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'unarchive' ) ), languageString: 'unarchive');
$unarchive->addAttribute( 'data-confirm')
->addAttribute( 'data-confirmSubMessage', $this->unarchiveBlurb() );
$unarchive->requiresConfirm( $this->unarchiveBlurb() );
$menu->add( $unarchive );
}
@@ -7262,7 +7262,7 @@ abstract class Item extends Content
if( $this->canDelete( $member ) )
{
$delete = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' =>'delete' ) ), languageString: 'delete');
$delete->addAttribute( 'data-confirm' );
$delete->requiresConfirm();
$menu->add( $delete );
}
@@ -7284,9 +7284,7 @@ abstract class Item extends Content
if( $this->canToggleItemModeration( $member ) )
{
$toggle = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'toggleItemModeration' ) ), languageString: $this->itemModerationEnabled() ?'disable_topic_moderation' : 'enable_topic_moderation' );
$toggle
->addAttribute( 'data-confirm')
->addAttribute( 'data-confirmSubMessage', Member::loggedIn()->language()->addToStack( $this->itemModerationEnabled() ? 'disable_topic_moderation_confirm' : 'enable_topic_moderation_confirm' ) );
$toggle->requiresConfirm( $this->itemModerationEnabled() ? 'disable_topic_moderation_confirm' : 'enable_topic_moderation_confirm' );
$menu->add( $toggle );
}
@@ -7296,7 +7294,7 @@ abstract class Item extends Content
foreach ( $actions as $action )
{
$actionLink = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'savedAction', 'action' => $action->_id ) ), languageString: $action->_title );
$actionLink->addAttribute( 'data-confirm' );
$actionLink->requiresConfirm();
$menu->add( $actionLink );
}
}
-11
View File
@@ -364,17 +364,6 @@ abstract class Review extends Comment
return array( $this->item()->$idColumn, $this->$commentIdColumn, 'review' );
}
/**
* Create Notification
*
* @param string|NULL $extra Additional data
* @return Notification
*/
protected function createNotification( string|null $extra=NULL ): Notification
{
return new Notification( Application::load( 'core' ), 'new_review', $this->item(), array( $this ) );
}
/**
* Delete Review
*
+12 -1
View File
@@ -16,6 +16,7 @@ use Exception;
use IPS\Application;
use IPS\Content;
use IPS\Content\Comment;
use IPS\Content\FuturePublishing;
use IPS\Content\Item;
use IPS\Content\Search\Elastic\MassIndexer;
use IPS\DateTime;
@@ -200,8 +201,18 @@ abstract class Index extends Singleton
return NULL;
}
/* If this is a comment on an item that uses future publishing AND the item is in the future, don't index */
if( $object instanceof Comment )
{
$item = $object->item();
if( $item::$firstCommentRequired and $object->isFirst() and IPS::classUsesTrait( $item, FuturePublishing::class ) and $item->isFutureDate() )
{
return null;
}
}
/* Don't index if this is an item to be published in the future */
if ( IPS::classUsesTrait( $object, 'IPS\Content\FuturePublishing' ) AND $object->isFutureDate() )
if ( IPS::classUsesTrait( $object, FuturePublishing::class ) AND $object->isFutureDate() )
{
return NULL;
}
+7 -3
View File
@@ -12,7 +12,7 @@ namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Content;
use IPS\Content\Item;
use IPS\DateTime;
use IPS\Db;
use IPS\Db\Select;
@@ -102,9 +102,13 @@ abstract class Query
/* Exclude disabled applications */
$filters = array();
foreach ( Content::routedClasses( $member, FALSE, TRUE ) as $class )
foreach( SearchContent::searchableClasses() as $class )
{
$filters[] = ContentFilter::init( $class );
/* We only want items here */
if( is_subclass_of( $class, Item::class ) )
{
$filters[] = ContentFilter::init( $class );
}
}
if ( !empty( $filters ) )
{
+6 -3
View File
@@ -105,7 +105,9 @@ trait Taggable
*/
public static function definedTags(): array
{
return array_values( Tag::getStore() );
$tags = array_values( Tag::getStore() );
natcasesort( $tags );
return $tags;
}
/**
@@ -147,7 +149,7 @@ trait Taggable
$tagList[ $key ] = strtolower( $value );
} );
foreach ( Db::i()->select( '*', 'core_tags', array( 'tag_meta_app=? AND tag_meta_area=? AND tag_meta_id=?', static::$application, static::$module, $this->$idColumn ) ) as $tag )
foreach ( Db::i()->select( '*', 'core_tags', array( 'tag_meta_app=? AND tag_meta_area=? AND tag_meta_id=?', static::$application, static::$module, $this->$idColumn ), 'tag_text' ) as $tag )
{
if ( $tag['tag_prefix'] )
{
@@ -322,7 +324,8 @@ trait Taggable
'resultItemTemplate' => 'core.autocomplete.tagsResultItem',
'freeChoice' => false,
'minimized' => $minimized,
'addTokenText' => Member::loggedIn()->language()->get( 'tags_optional' )
'addTokenText' => Member::loggedIn()->language()->get( 'tags_optional' ),
'alphabetical' => true
) );
if ( Settings::i()->tags_force_lower )