Streams
Streams are used to asynchronously load batches of content entries which may can be filtered or sorted. The most common type of stream is the WallStream used for example on the dashboard or space stream view. Besides WallStreams the HumHub core only implements one additional stream type, the activity sidebar stream. The following section describes how to implement custom streams and extend existing streams.
Stream Channel
The Content::$stream_channel
property defines the default stream type of this content. If not
otherwise defined the 'default' stream channel will be used, which means this content will be included in wall streams as
the space, profile or dashboard stream.
The ContentActiveRecord::$streamChannel
property of your custom content type can be used to overwrite the default stream type.
Its common practice to set $this->streamChannel = null
for certain records in order to exclude specific entries from the
default wall streams. An example of a custom stream channel is the activity stream. Activity records are not included in
the wall streams but are part of an activity sidebar stream.
Exclude all instances from default wall streams:
class MyContent extends ContentActiveRecord
{
public $streamChannel = null;
//...
}
Exclude specific entries from default wall streams:
class MyContent extends ContentActiveRecord
{
public function beforeSave($insert)
{
if($this->isNotImportant()) {
$this->streamChannel = null;
}
return parent::beforeSave($insert)
}
//...
}
For your custom ContentActiveRecord
you can consider the following stream channel options:
- default: this stream channel will include your content to space/profile walls and the dashboard
- null will exclude the content from the default wall streams (space/profile/dashboard)
- Use a custom stream channel if you exclusively want your content to be included in your own custom stream
Note: A custom stream channel should be unique, so choose a meaningful name preferably with module prefix.
StreamEntryWidget
A StreamEntryWidget
is responsible for rendering stream entries. The StreamEntryWidget
class
is the base widget class for all types of stream entries. The default stream widget class of a content type
is defined by the ContentActiveRecord::$wallEntryClass
property. For wall streams there are two different widget
types available, the WallStreamEntryWidget
and WallStreamModuleEntryWidget
.
WallStreamEntryWidget
The WallStreamEntryWidget
renders a post style wall entry with user image and name in the head part of the wall entry.
This widget type should be used for content which emphasizes the author of the content and contains mainly personal
(not collaborative) content. For content with collaborative nature you should rather use the WallStreamModuleEntryWidget
class.
When extending a WallStreamEntryWidget
you'll have to implement the WallStreamEntryWidget::renderContent()
function
which will be embedded in the content section of your stream entry.
Example:
use humhub\modules\content\widgets\stream\WallStreamEntryWidget;
class WallEntry extends WallStreamEntryWidget
{
protected function renderContent()
{
return $this->render('wallEntry', ['model' => $this->model]);
}
}
WallStreamModuleEntryWidget
The WallStreamModuleEntryWidget
does not emphasize the user, but instead the content type and the content title.
This widget should be used for content types in which the author is not that important as for example a wiki or other
collaborative content.
When extending WallStreamModuleEntryWidget
you'll have to implement the WallStreamEntryWidget::renderContent()
as
well as the WallStreamEntryWidget::getTitle()
function. You may also want to overwrite the WallStreamEntryWidget::getIcon()
function to overwrite the default icon provided by ContentActiveRecord::getIcon()
.
Example:
use humhub\modules\content\widgets\stream\WallStreamModuleEntryWidget;
class WallEntry extends WallStreamModuleEntryWidget
{
protected function renderContent()
{
return $this->render('wallEntry', ['model' => $this->model]);
}
protected function getIcon()
{
// By default we do not have to overwrite this function unless we want to overwrite the default ContentActiveRecord::$icon
return 'tasks';
}
protected function getTitle()
{
return $this->model->title;
}
}
WallEntryControls
The WallEntryControls menu is part of all wall stream entries and includes stream links as Delete or Edit. The following links are added to the wall entry by default (depending on the user permission and view context):
- PermaLink: Opens a modal with the content url
- DeleteLink: Used to delete an entry
- EditLink: Used to edit an entry
- VisibilityLink: Used to switch the visibility of a content
- NotificationSwitchLink: Used enable/disable receiving notifications for a content.
- PinLink: Used to pin/unpin content entries to a stream
- MoveContentLink: Used to move a content entry to another space
- ArchiveLink: Used to archive/unarchive an entry
- TopicLink: Used to manage topics of a content entry
By overwriting WallStreamModuleEntryWidget::getControlsMenuEntries()
your content type can add additional menu entries to
the controls menu of your content type follows:
public function getControlsMenuEntries()
{
$result = parent::getControlsMenuEntries();
$result[] = new MySpecialLink()
return $result;
}
You can also disable default entries as follows:
use humhub\modules\content\widgets\stream\WallStreamEntryWidget;
class WallEntry extends WallStreamEntryWidget
{
//...
protected function init()
{
parent::init();
// Exclude the delete link from the controls menu
$this->renderOptions->disableControlsDelete();
}
}
WallStreamEntryOptions
The StreamEntryWidget::$renderOptions
property can be used to configure the appearance of an wall entry, for example by:
- Disabling controls menu entries
- Disabling the whole controls menu
- Disabling certain stream entry addons
- Changing the output depending on the view context
View context
The appearance of a stream entry may differentiate depending on a view context configuration. Wall entries on the dashboard for example include further information about the container the content is assigned to. Posts in the detail view display the whole content without the use of a collapsed read-more section. The default streams support the following view contexts:
StreamEntryOptions::VIEW_CONTEXT_DEFAULT
: Default appearance (e.g. on space/profile stream)StreamEntryOptions::VIEW_CONTEXT_DASHBOARD
: Dashboard streamStreamEntryOptions::VIEW_CONTEXT_SEARCH
: Content search streamStreamEntryOptions::VIEW_CONTEXT_DETAIL
: Detail view (single entry stream)StreamEntryOptions::VIEW_CONTEXT_MODAL
: Rendered within a modal dialog
Example:
use humhub\modules\content\widgets\stream\WallStreamEntryWidget;
class WallEntry extends WallStreamEntryWidget
{
//...
protected function getControlsMenuEntries()
{
$result = parent::getControlsMenuEntries();
// Only add this link in default (e.g. space) context
if($this->renderOptions->isViewContext(StreamEntryOptions::VIEW_CONTEXT_DEFAULT) {
$result[] = new MySpecialLink(['model' => $this->model])
}
return $result;
}
}
editRoute and editMode
If a WallStreamEntryWidget::$editRoute
is defined an edit menu item will be added to the controls menu in case the current
user is allowed to edit the content. The WallStreamEntryWidget::$editMode
defines how the content should be edited e.g.
within a modal, inline, or within a new page.
There are the following edit modes available:
EDIT_MODE_MODAL
the response ofeditRoute
action will be loaded into a modal.EDIT_MODE_INLINE
the response ofeditRoute
action will be embeded into the WallEntry content.EDIT_MODE_NEW_WINDOW
the page response ofeditRoute
action will be fully loaded.
Custom streams
You can add custom streams as for example own wall or sidebar streams to your custom module. A custom stream implementation contains of the following components:
- Stream action responsible for handling stream requests
- StreamQuery responsible for fetching and filtering the stream result
- StreamViewer widget responsible for rendering the stream container in the view
- Stream filter navigation (optional) can be used to render a filter navigation for your stream
The following sections explain the different components of a stream by implementing a custom content info stream. Our custom stream will render stream entries in form of content metadata blocks by extending the dashboard stream. Furthermore, we'll add an additional custom filter to only include content created by the current user (if active).
StreamViewer
The humhub\modules\stream\widgets\StreamViewer
widget is used to render our custom stream. Note, the stream entries of
a stream are loaded asynchronously and therefore will not directly be rendered by the StreamViewer widget itself.
The StreamViewer widget expects a streamAction
property pointing to the controller action handling stream requests.
The optional streamFilterNavigation
can be used to define a stream filter navigation widget class.
Wall streams come with a default stream filter navigation. In case you want to disable the default navigation
for your custom stream, just set streamFilterNavigation
to false or null.
views/index/index.php:
<?= StreamViewer::widget([
'streamAction' => '/mymodule/stream/stream',
'streamFilterNavigation' => ContentInfoStreamFilterNavigation::class
])?>
Stream filter navigation
The stream filter navigation can be used to add additional filter options to your stream. In our content info stream we replace the default filter navigation with a custom filter navigation widget. The filter navigation will include a single checkbox filter which will only include content created by the current user if active.
widgets\ContentInfoStreamFilterNavigation:
class ContentInfoStreamFilterNavigation extends FilterNavigation
{
protected function initFilterPanels(){}
protected function initFilterBlocks(){}
protected function initFilters(){}
public function getAttributes()
{
return [
'style' => 'padding-left:15px'
];
}
}
In our example we render a very simple and static filter navigation, for more complex and extendable stream navigations,
you might want to work with separated filter blocks and panels, see humhub\modules\stream\widgets\WallStreamFilterNavigation.
In such cases we would register the filters within initFilters
instead of directly rendering them in the view.
widgets\views\filterNavigation.php:
The view of our filter navigation widget looks like the following:
<?= Html::beginTag('div', $options)?>
<?= CheckboxFilterInput::widget([
'id' => OwnContentStreamFilter::FILTER_ID,
'title' => Yii::t('ContentInfoModule.base','Only show my own content')
])?>
<?= Html::endTag('div')?>
The filter in the previous example will be added to the default filters[]
request parameter.
In case you want to use another parameter you need to overwrite the category
property of your filter input.
Stream filter implementation
Stream filters extend humhub\modules\stream\models\filters\StreamQueryFilter
and can be used to add query conditions
to your stream query. In the following example we implement our filter_my_content
filter.
class OwnContentStreamFilter extends StreamQueryFilter
{
const FILTER_ID = 'filter_my_content';
public $filters = [];
public function rules()
{
return [
['filters', 'safe']
];
}
public function apply()
{
if($this->filters === static::FILTER_ID || in_array(static::FILTER_ID, $this->filters, true)) {
$this->streamQuery->query()->andWhere(['content.created_by' => Yii::$app->user->id]);
}
}
}
Stream controller
The humhub\modules\stream\actions\Stream
is the base controller action class
for all streams responsible for handling stream results. The HumHub core provides the following default stream actions:
ContentContainerStream
: Includes an optional content container filter and should be used for streams on container level, e.g. include all content of content type x in space y.DashboardStreamAction
: Includes dashboard content visibility filters
Example Controller
In this example we register our action and register our custom filter handler. Furthermore, we overwrite the widget class used to render the stream entries.
class StreamController extends Controller
{
public function actions()
{
return [
'stream' => [
'class' => DashboardStreamAction::class,
'filterHandlers' => [OwnContentStreamFilter::class],
'streamEntryOptions' => (new WallStreamEntryOptions)
->overwriteWidgetClass(ContentInfoWallStreamEntryWidget::class)
],
];
}
}
Stream action
In case you have a more complex custom stream scenario, or want your stream action to be extendable by events you can consider implementing a custom stream action as follows:
class ContentInfoStreamAction extends DashboardStreamAction
{
protected function initStreamEntryOptions()
{
return (new WallStreamEntryOptions)
->overwriteWidgetClass(ContentInfoWallStreamEntryWidget::class);
}
protected function initQuery($options = [])
{
$query = parent::initQuery($options);
$query->addFilterHandler(OwnContentStreamFilter::class);
return $query;
}
}
Stream query
You can also consider implementing a reusable stream query class as follows:
class ContentInfoStreamAction extends ContentContainerStream
{
public $streamQuery = MyStreamQuery::class;
protected function initStreamEntryOptions()
{
return (new WallStreamEntryOptions)
->overwriteWidgetClass(ContentInfoWallStreamEntryWidget::class);
}
}
class ContentInfoStreamQuery extends ContentContainerStreamQuery
{
protected function beforeApplyFilters()
{
$this->addFilterHandler(MyStreamFilter::class);
parent::beforeApplyFilters();
}
}
Stream entry widget
Since we do not want to use the default entry layout in our content info stream, we need to implement a custom StreamEntryWidget
:
widgets\ContentInfoWallStreamEntryWidget:
use humhub\modules\content\widgets\stream\StreamEntryWidget;
class ContentInfoWallStreamEntryWidget extends StreamEntryWidget
{
protected function renderBody()
{
return $this->render('contentInfoWallStreamEntry', [
'model' => $this->model
]);
}
}
widgets\views\contentInfoWallStreamEntry:
<?php
use humhub\libs\Html;
use humhub\modules\content\widgets\VisibilityIcon; ?>
<div style="border:1px solid var(--info);margin:10px;padding:10px;">
<b>Content id:</b> <?= $model->content->id ?><br>
<b>Content name:</b> <?= $model->getContentName() ?> <?= VisibilityIcon::getByModel($model) ?><br>
<b>Created at:</b> <?= Yii::$app->formatter->asDatetime($model->content->created_at) ?><br>
<b>Created by:</b> <?= Html::containerLink($model->content->createdBy) ?><br>
<?php if($model->content->isUpdated()) :?>
<b>Last updated at:</b> <?= Yii::$app->formatter->asDatetime($model->content->updated_at) ?><br>
<b>Updated by:</b> <?= Html::containerLink($model->content->updatedBy) ?><br>
<?php endif; ?>
<b>Container:</b> <?= Html::containerLink($model->content->container) ?><br>
</div>
Extend stream filters
Since HumHub v1.3 you are able to extend the stream filter navigations by implementing following event handlers:
WallStreamQuery::EVENT_BEFORE_FILTER
to add the filter to the queryWallStreamFilterNavigation::EVENT_BEFORE_RUN
The WallStreamFilterNavigation
navigation contains three filterPanels:
WallStreamFilterNavigation::PANEL_POSITION_LEFT
WallStreamFilterNavigation::PANEL_POSITION_CENTER
WallStreamFilterNavigation::PANEL_POSITION_RIGHT
Each panel can contain multiple filterBlocks. Each block may contain multiple filters sorted by a sortOrder
setting. The following example adds an originator
filter to the wall stream:
Event configuration:
return [
[
'class' => \humhub\modules\stream\models\WallStreamQuery::class,
'event' => \humhub\modules\stream\models\WallStreamQuery::EVENT_BEFORE_FILTER,
'callback' => ['\humhub\modules\demo\Events', 'onStreamFilterBeforeFilter'],
],
[
'class' => \humhub\modules\stream\widgets\WallStreamFilterNavigation::class,
'event' => \humhub\modules\stream\widgets\WallStreamFilterNavigation::EVENT_BEFORE_RUN,
'callback' => ['\humhub\modules\demo\Events', 'onStreamFilterBeforeRun'],
]
]
Event handler implementation:
class Events extends \yii\base\Object
{
const FILTER_BLOCK_ORIGINATOR = 'originator';
const FILTER_ORIGINATOR = 'originator';
public static function onStreamFilterBeforeRun($event)
{
/** @var $wallFilterNavigation WallStreamFilterNavigation */
$wallFilterNavigation = $event->sender;
// Add a new filter block to the last filter panel
$wallFilterNavigation->addFilterBlock(static::FILTER_BLOCK_ORIGINATOR, [
'title' => 'Originator',
'sortOrder' => 300
], WallStreamFilterNavigation::PANEL_POSITION_RIGHT);
// Add a filter of type PickerFilterInput to the new filter block
$wallFilterNavigation->addFilter([
'id' => static::FILTER_ORIGINATOR,
'class' => PickerFilterInput::class,
'picker' => UserPickerField::class,
'category' => 'originators',
'pickerOptions' => [
'id' => 'stream-user-picker',
'itemKey' => 'id',
'name' => 'stream-user-picker'
]], static::FILTER_BLOCK_ORIGINATOR);
}
public static function onStreamFilterBeforeFilter($event)
{
/** @var $streamQuery WallStreamQuery */
$streamQuery = $event->sender;
// Add a new filterHandler to WallStreamQuery
$streamQuery->addFilterHandler(OriginatorStreamFilter::class);
}
}
OriginatorStreamFilter.php
class OriginatorStreamFilter extends StreamQueryFilter
{
public $originators = [];
public function rules() {
return [
[['originators'], 'safe']
];
}
public function apply()
{
if(empty($this->originators)) {
return;
}
if($this->originators instanceof User) {
$this->originators = [$this->originators->id];
} else if(!is_array($this->originators)) {
$this->originators = [$this->originators];
}
$this->query->joinWith('contentContainer');
if (count($this->originators) === 1) {
$this->query->andWhere(["user.guid" => $this->originators[0]]);
} else if (!empty($this->originators)) {
$this->query->andWhere(['IN', 'user.guid', $this->originators]);
}
}
}