Added Content State (#6076)

* Added new Content State

* Added Content SoftDelete

* Added Draft option on WallEntryCreate

* Draft Stream Handling

* Fix show drafts on dashboard

* Reset draft state in Content Form

* Remove Notifications/Activities on soft delete

* Hide new content notifications on draft content

* Added ActivityHelper

* Added possibility to publish draft content

* Added missing message text [skip ci]

* Handle search for non published content

* Mark default delete implementation as deprecated

* Make sure files of deleted content are not longer accessible [skip ci]

* Show badge for deleted content

* Added State Filter for Content Queries

* Added doc

* Added ContentContainerStreamTest

* Added Acceptance Tests

* Fixed UserReleated exception for Guest users

* Fixed popover less

* Minor improvements
This commit is contained in:
Lucas Bartholemy 2023-02-09 12:16:01 +01:00 committed by GitHub
parent dbe3eb919c
commit 032aea3dd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 680 additions and 578 deletions

View File

@ -8,7 +8,9 @@
namespace humhub\modules\activity;
use humhub\components\ActiveRecord;
use humhub\modules\activity\components\MailSummary;
use humhub\modules\activity\helpers\ActivityHelper;
use humhub\modules\activity\jobs\SendMailSummary;
use humhub\modules\activity\models\Activity;
use humhub\modules\admin\permissions\ManageSettings;
@ -19,7 +21,7 @@ use Yii;
use yii\base\ActionEvent;
use yii\base\BaseObject;
use yii\base\Event;
use yii\db\ActiveRecord;
use yii\base\InvalidArgumentException;
use yii\db\IntegrityException;
/**
@ -66,23 +68,10 @@ class Events extends BaseObject
public static function onActiveRecordDelete(Event $event)
{
if (!($event->sender instanceof ActiveRecord)) {
throw new \LogicException('The handler can be applied only to the \yii\db\ActiveRecord.');
throw new InvalidArgumentException('The handler can be applied only to the \humhub\components\ActiveRecord.');
}
/** @var \yii\db\ActiveRecord $activeRecordModel */
$activeRecordModel = $event->sender;
$pk = $activeRecordModel->getPrimaryKey();
// Check if primary key exists and is not array (multiple pk)
if ($pk !== null && !is_array($pk)) {
$modelsActivity = Activity::find()->where([
'object_id' => $pk,
'object_model' => get_class($activeRecordModel)
])->each();
foreach ($modelsActivity as $activity) {
$activity->delete();
}
}
ActivityHelper::deleteActivitiesForRecord($event->sender);
}
public static function onAccountMenuInit($event)

View File

@ -0,0 +1,32 @@
<?php
namespace humhub\modules\activity\helpers;
use humhub\components\ActiveRecord;
use humhub\modules\activity\models\Activity;
use Yii;
class ActivityHelper
{
public static function deleteActivitiesForRecord(ActiveRecord $record)
{
$pk = $record->getPrimaryKey();
// Check if primary key exists and is not array (multiple pk)
if ($pk !== null && !is_array($pk)) {
$modelsActivity = Activity::find()->where([
'object_id' => $pk,
'object_model' => get_class($record)
])->each();
foreach ($modelsActivity as $activity) {
$activity->delete();
}
Yii::debug('Deleted activities for ' . get_class($record) . " with PK " . $pk, 'activity');
}
}
}

View File

@ -15,7 +15,6 @@ use yii\base\Exception;
use yii\base\InvalidConfigException;
use yii\db\ActiveRecord;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\activity\components\ActivityWebRenderer;
use humhub\components\behaviors\PolymorphicRelation;
use yii\db\IntegrityException;
use humhub\modules\activity\widgets\Activity as ActivityStreamEntryWidget;

View File

@ -9,6 +9,7 @@
namespace humhub\modules\content;
use humhub\commands\IntegrityController;
use humhub\components\Event;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\models\Content;
use humhub\modules\search\interfaces\Searchable;
@ -92,7 +93,7 @@ class Events extends BaseObject
/**
* On init of the WallEntryAddonWidget, attach the wall entry links widget.
*
* @param CEvent $event
* @param Event $event
*/
public static function onWallEntryAddonInit($event)
{
@ -105,13 +106,13 @@ class Events extends BaseObject
/**
* On rebuild of the search index, rebuild all user records
*
* @param type $event
* @param Event $event
*/
public static function onSearchRebuild($event)
{
foreach (Content::find()->each() as $content) {
$contentObject = $content->getPolymorphicRelation();
if ($contentObject instanceof Searchable) {
if ($contentObject instanceof Searchable && $content->state === Content::STATE_PUBLISHED) {
Yii::$app->search->add($contentObject);
}
}
@ -126,7 +127,10 @@ class Events extends BaseObject
{
/** @var ContentActiveRecord $record */
$record = $event->sender;
SearchHelper::queueUpdate($record);
if ($record->content->state === Content::STATE_PUBLISHED) {
SearchHelper::queueUpdate($record);
}
}
/**
@ -141,4 +145,14 @@ class Events extends BaseObject
SearchHelper::queueDelete($record);
}
/**
* Callback on daily cron job run
*
* @param \yii\base\Event $event
*/
public static function onCronDailyRun($event): void
{
Yii::$app->queue->push(new jobs\PurgeDeletedContents());
}
}

View File

@ -8,12 +8,14 @@
namespace humhub\modules\content\components;
use humhub\modules\content\models\Content;
use humhub\modules\content\models\ContentTag;
use humhub\modules\content\models\ContentTagRelation;
use humhub\modules\space\models\Space;
use humhub\modules\user\helpers\AuthHelper;
use humhub\modules\user\models\User;
use Yii;
use yii\db\ActiveQuery;
use yii\db\Expression;
/**
@ -23,9 +25,8 @@ use yii\db\Expression;
*
* @author luke
*/
class ActiveQueryContent extends \yii\db\ActiveQuery
class ActiveQueryContent extends ActiveQuery
{
/**
* Own content scope for userRelated
* @see ActiveQueryContent::userRelated
@ -36,6 +37,21 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
const USER_RELATED_SCOPE_FOLLOWED_USERS = 4;
const USER_RELATED_SCOPE_OWN_PROFILE = 5;
/**
* State filter that is used for queries. By default, only Published content is returned.
*
* Example to include drafts:
* ```
* $query = Post::find();
* $query->stateFilterCondition[] = ['content.state' => Content::STATE_DRAFT];
* $posts = $query->readable()->all();
* ```
*
* @since 1.14
* @var array
*/
public $stateFilterCondition = ['OR', ['content.state' => Content::STATE_PUBLISHED]];
/**
* Only returns user readable records
*
@ -49,8 +65,9 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
$user = Yii::$app->user->getIdentity();
}
$this->joinWith(['content', 'content.contentContainer', 'content.createdBy']);
$this->andWhere($this->stateFilterCondition);
$this->joinWith(['content', 'content.contentContainer', 'content.createdBy']);
$this->leftJoin('space', 'contentcontainer.pk=space.id AND contentcontainer.class=:spaceClass', [':spaceClass' => Space::class]);
$this->leftJoin('user cuser', 'contentcontainer.pk=cuser.id AND contentcontainer.class=:userClass', [':userClass' => User::class]);
$conditionSpace = '';
@ -72,7 +89,7 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
}
// Build Access Check based on Space Content Container
$conditionSpace = 'space.id IS NOT NULL' . $conditionSpaceMembershipRestriction; // space content
$conditionSpace = 'space.id IS NOT NULL' . $conditionSpaceMembershipRestriction;
// Build Access Check based on User Content Container
$conditionUser = 'cuser.id IS NOT NULL AND ('; // user content
@ -139,7 +156,7 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
$contentTagQuery = ContentTagRelation::find()->select('content_id');
$contentTagQuery->andWhere(['content_tag_relation.tag_id' => $contentTag->id]);
$contentTagQuery->andWhere('content_tag_relation.content_id=content.id');
$this->andWhere(['content.id' =>$contentTagQuery]);
$this->andWhere(['content.id' => $contentTagQuery]);
}
} else if ($mode == 'OR') {
$names = array_map(function ($v) {
@ -182,6 +199,10 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
public function userRelated($scopes = [], $user = null)
{
if ($user === null) {
if ( Yii::$app->user->isGuest) {
return $this->andWhere('false');
}
$user = Yii::$app->user->getIdentity();
}

View File

@ -474,6 +474,13 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
return static::class;
}
/**
* @deprected Please use `$this->content->softDelete()` instead of the default delete implementation!
*/
public function delete() {
return parent::delete();
}
/**
* @inheritdoc
*/

View File

@ -1,8 +1,8 @@
<?php
use humhub\commands\CronController;
use humhub\modules\content\Events;
use humhub\commands\IntegrityController;
use humhub\modules\content\widgets\WallEntryControls;
use humhub\modules\content\widgets\WallEntryAddons;
use humhub\modules\user\models\User;
use humhub\modules\space\models\Space;
@ -23,6 +23,7 @@ return [
['class' => ContentActiveRecord::class, 'event' => ContentActiveRecord::EVENT_AFTER_INSERT, 'callback' => [Events::class, 'onContentActiveRecordSave']],
['class' => ContentActiveRecord::class, 'event' => ContentActiveRecord::EVENT_AFTER_UPDATE, 'callback' => [Events::class, 'onContentActiveRecordSave']],
['class' => ContentActiveRecord::class, 'event' => ContentActiveRecord::EVENT_AFTER_DELETE, 'callback' => [Events::class, 'onContentActiveRecordDelete']],
['class' => CronController::class, 'event' => CronController::EVENT_ON_DAILY_RUN, 'callback' => [Events::class, 'onCronDailyRun']]
],
];
?>
?>

View File

@ -98,7 +98,7 @@ class ContentController extends Controller
}
$json = [
'success' => $contentObj->delete(),
'success' => $contentObj->softDelete(),
'uniqueId' => $contentObj->getUniqueId(),
'model' => $model,
'pk' => $id
@ -211,7 +211,7 @@ class ContentController extends Controller
throw new BadRequestHttpException();
}
if($form->notify) {
if ($form->notify) {
$contentDeleted = ContentDeleted::instance()
->from(Yii::$app->user->getIdentity())
->payload(['contentTitle' => (new ContentDeleted)->getContentPlainTextInfo($content), 'reason' => $form->message]);
@ -223,7 +223,7 @@ class ContentController extends Controller
}
}
return $this->asJson(['success' => $content->delete()]);
return $this->asJson(['success' => $content->softDelete()]);
}
public function actionReload($id)
@ -402,6 +402,25 @@ class ContentController extends Controller
return $this->asJson($json);
}
public function actionPublishDraft()
{
$this->forcePostRequest();
$json = [];
$json['success'] = false;
$content = Content::findOne(['id' => Yii::$app->request->get('id', '')]);
if ($content !== null && $content->canEdit() && $content->state === Content::STATE_DRAFT) {
$content->state = Content::STATE_PUBLISHED;
$content->save();
$json['message'] = Yii::t('ContentModule.base', 'The content has been successfully published.');
$json['success'] = true;
}
return $this->asJson($json);
}
public function actionNotificationSwitch()
{
$this->forcePostRequest();

View File

@ -0,0 +1,27 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\content\jobs;
use humhub\modules\content\models\Content;
use humhub\modules\queue\ActiveJob;
class PurgeDeletedContents extends ActiveJob
{
/**
* @inheritdoc
*/
public function run()
{
foreach (Content::findAll(['content.state' => Content::STATE_DELETED]) as $content) {
$content->delete();
}
}
}

View File

@ -0,0 +1,46 @@
<?php
use yii\db\Migration;
/**
* Class m230127_195245_content_state
*/
class m230127_195245_content_state extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->addColumn(
'content',
'state',
$this->tinyInteger()->defaultValue(1)->notNull()->after('visibility')
);
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
echo "m230127_195245_content_state cannot be reverted.\n";
return false;
}
/*
// Use up()/down() to run migration code without a transaction.
public function up()
{
}
public function down()
{
echo "m230127_195245_content_state cannot be reverted.\n";
return false;
}
*/
}

View File

@ -12,6 +12,7 @@ use humhub\components\ActiveRecord;
use humhub\components\behaviors\GUID;
use humhub\components\behaviors\PolymorphicRelation;
use humhub\components\Module;
use humhub\modules\activity\helpers\ActivityHelper;
use humhub\modules\admin\permissions\ManageUsers;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\components\ContentContainerActiveRecord;
@ -21,6 +22,7 @@ use humhub\modules\content\live\NewContent;
use humhub\modules\content\permissions\CreatePrivateContent;
use humhub\modules\content\permissions\CreatePublicContent;
use humhub\modules\content\permissions\ManageContent;
use humhub\modules\notification\models\Notification;
use humhub\modules\search\libs\SearchHelper;
use humhub\modules\space\models\Space;
use humhub\modules\user\components\PermissionManager;
@ -29,7 +31,6 @@ use humhub\modules\user\models\User;
use Yii;
use yii\base\Exception;
use yii\base\InvalidArgumentException;
use yii\db\Expression;
use yii\db\IntegrityException;
use yii\helpers\Url;
@ -59,6 +60,7 @@ use yii\helpers\Url;
* @property integer $visibility
* @property integer $pinned
* @property integer $archived
* @property integer $state
* @property integer $locked_comments
* @property string $created_at
* @property integer $created_by
@ -103,6 +105,13 @@ class Content extends ActiveRecord implements Movable, ContentOwner
*/
const VISIBILITY_OWNER = 2;
/**
* Content States - By default, only content with the "Published" state is returned.
*/
const STATE_PUBLISHED = 1;
const STATE_DRAFT = 10;
const STATE_DELETED = 100;
/**
* @var ContentContainerActiveRecord the Container (e.g. Space or User) where this content belongs to.
*/
@ -191,21 +200,13 @@ class Content extends ActiveRecord implements Movable, ContentOwner
throw new Exception("Could not save content with object_model or object_id!");
}
// Set some default values
if (!$this->archived) {
$this->archived = 0;
}
if (!$this->visibility) {
$this->visibility = self::VISIBILITY_PRIVATE;
}
if (!$this->pinned) {
$this->pinned = 0;
}
$this->archived ??= 0;
$this->visibility ??= self::VISIBILITY_PRIVATE;
$this->pinned ??= 0;
$this->state ??= Content::STATE_PUBLISHED;
if ($insert) {
if ($this->created_by == "") {
$this->created_by = Yii::$app->user->id;
}
$this->created_by ??= Yii::$app->user->id;
}
$this->stream_sort_date = date('Y-m-d G:i:s');
@ -222,15 +223,38 @@ class Content extends ActiveRecord implements Movable, ContentOwner
*/
public function afterSave($insert, $changedAttributes)
{
/* @var $contentSource ContentActiveRecord */
$contentSource = $this->getModel();
if (// New Content with State Published:
($insert && $this->state == Content::STATE_PUBLISHED) ||
// Content Updated from Draft to Published
(array_key_exists('state', $changedAttributes) &&
$this->state == Content::STATE_PUBLISHED &&
$changedAttributes['state'] == Content::STATE_DRAFT
)) {
$this->processNewContent();
}
if ($this->state === static::STATE_PUBLISHED) {
SearchHelper::queueUpdate($this->getModel());
} else {
SearchHelper::queueDelete($this->getModel());
}
parent::afterSave($insert, $changedAttributes);
}
private function processNewContent()
{
$record = $this->getModel();
Yii::debug('Process new content: ' . get_class($record) . ' ID: ' . $record->getPrimaryKey(), 'content');
foreach ($this->notifyUsersOfNewContent as $user) {
$contentSource->follow($user->id);
$record->follow($user->id);
}
// TODO: handle ContentCreated notifications and live events for global content
if ($insert && !$this->isMuted()) {
if (!$this->isMuted()) {
$this->notifyContentCreated();
}
@ -241,20 +265,17 @@ class Content extends ActiveRecord implements Movable, ContentOwner
'originator' => $this->createdBy->guid,
'contentContainerId' => $this->container->contentContainerRecord->id,
'visibility' => $this->visibility,
'sourceClass' => get_class($contentSource),
'sourceId' => $contentSource->getPrimaryKey(),
'sourceClass' => get_class($record),
'sourceId' => $record->getPrimaryKey(),
'silent' => $this->isMuted(),
'streamChannel' => $this->stream_channel,
'contentId' => $this->id,
'insert' => $insert
'insert' => true
]));
}
SearchHelper::queueUpdate($contentSource);
parent::afterSave($insert, $changedAttributes);
}
/**
* @return bool checks if the given content allows content creation notifications and activities
* @throws IntegrityException
@ -859,6 +880,11 @@ class Content extends ActiveRecord implements Movable, ContentOwner
return $this->checkGuestAccess();
}
// If content is draft, in trash, unapproved - restrict view access to editors
if ($this->state !== static::STATE_PUBLISHED) {
return $this->canEdit();
}
// Public visible content
if ($this->isPublic()) {
return true;
@ -948,4 +974,29 @@ class Content extends ActiveRecord implements Movable, ContentOwner
{
return $this->created_at !== $this->updated_at && !empty($this->updated_at) && is_string($this->updated_at);
}
/**
* Marks the content as deleted.
*
* Content which are marked as deleted will not longer returned in queries/stream/search.
* A cron job will remove these content permanently.
* If installed, such content can also be restored using the `recycle-bin` module.
*
* @return bool
* @since 1.14
*/
public function softDelete(): bool
{
ActivityHelper::deleteActivitiesForRecord($this->getModel());
Notification::deleteAll([
'source_class' => get_class($this),
'source_pk' => $this->getPrimaryKey(),
]);
$this->state = self::STATE_DELETED;
$this->save();
return true;
}
}

View File

@ -95,6 +95,7 @@ humhub.module('content.form', function(module, require, $) {
this.setDefaultVisibility();
this.resetFilePreview();
this.resetFileUpload();
this.resetDraftState();
$('#public').attr('checked', false);
$('#contentFormBody').find('.humhub-ui-richtext').trigger('clear');
@ -185,6 +186,21 @@ humhub.module('content.form', function(module, require, $) {
}
};
CreateForm.prototype.resetDraftState = function() {
$('#contentForm_draft').prop("checked", false);
$('.label-draft').addClass('hidden');
};
CreateForm.prototype.changeDraftState = function() {
if ($('#contentForm_draft').prop("checked")) {
$('.label-draft').addClass('hidden');
$('#contentForm_draft').prop("checked", false);
} else {
$('.label-draft').removeClass('hidden');
$('#contentForm_draft').prop("checked", true);
}
};
const CreateFormMenu = Widget.extend();
CreateFormMenu.prototype.init = function() {

View File

@ -0,0 +1,43 @@
<?php
use content\AcceptanceTester;
class DraftCest
{
public function testCreateDraftPost(AcceptanceTester $I)
{
$I->amSpaceAdmin(false, 3);
$I->wantTo('create a draft post.');
$I->waitForText('What\'s on your mind?');
$I->click('#contentFormBody .humhub-ui-richtext[contenteditable]');
$I->fillField('#contentFormBody .humhub-ui-richtext[contenteditable]', 'Some Schabernack');
$I->click('#contentFormBody ul.preferences');
$I->waitForText('Create as draft');
$I->click('Create as draft');
$I->waitForText('DRAFT', '10', '.label-container');
$I->click('#post_submit_button', '#contentFormBody');
$I->wantTo('ensure draft has a draft badge.');
$I->waitForText('DRAFT', '5', '//div[@class="wall-entry"][1]');
$I->wantTo('ensure draft is not visible for other users.');
$I->amUser2(true);
$I->amOnSpace3();
$I->dontSee('Schabernack');
$I->wantTo('publish draft');
$I->amSpaceAdmin(true, 3);
$I->waitForText('Schabernack');
$I->click('//div[@class="wall-entry"][1]//ul[contains(@class, "preferences")]');
$I->waitForText('Publish draft', '5');
$I->click('Publish draft');
$I->waitForText('The content has been successfully published.');
$I->dontSee('DRAFT');
$I->wantTo('ensure published draft is now visible for other users.');
$I->amUser2(true);
$I->amOnSpace3();
$I->waitForText('Schabernack');
}
}

View File

@ -95,6 +95,40 @@ class ContentContainerStreamTest extends HumHubDbTestCase
$this->assertTrue(in_array($w2, $ids));
}
public function testDraftContent()
{
$this->becomeUser('User2');
$draft1Id = $this->createPost('Some Draft', Content::VISIBILITY_PRIVATE, Content::STATE_DRAFT);
$regular1Id = $this->createPost('Regular 1 by U2', Content::VISIBILITY_PRIVATE,);
$this->becomeUser('Admin');
$regular2Id = $this->createPost('Regular 2 by Admin', Content::VISIBILITY_PRIVATE);
$this->becomeUser('User2');
$ids = $this->getStreamActionIds($this->space, 2);
// Check draft is first for Author
$this->assertTrue($ids[0] === $draft1Id);
// Check draft is not visible for other users
$this->becomeUser('Admin');
$ids = $this->getStreamActionIds($this->space, 5);
$this->assertTrue(!in_array($draft1Id, $ids));
}
public function testDeletedContent()
{
$this->becomeUser('User2');
$deleteId = $this->createPost('Something to delete', Content::VISIBILITY_PRIVATE);
$post = Post::findOne(['id' => $deleteId]);
$post->content->softDelete();
$ids = $this->getStreamActionIds($this->space, 3);
// Deleted Content should not appear in stream
$this->assertTrue(!in_array($deleteId, $ids));
}
private function getStreamActionIds($container, $limit = 4)
{
$action = new ContentContainerStream('stream', Yii::$app->controller, [
@ -107,7 +141,9 @@ class ContentContainerStreamTest extends HumHubDbTestCase
$wallEntries = $action->getStreamQuery()->all();
$wallEntryIds = array_map(static function($entry) {return $entry->id; }, $wallEntries);
$wallEntryIds = array_map(static function ($entry) {
return $entry->id;
}, $wallEntries);
return $wallEntryIds;
}
@ -122,12 +158,13 @@ class ContentContainerStreamTest extends HumHubDbTestCase
return $this->createPost('Public Post', Content::VISIBILITY_PUBLIC);
}
private function createPost($message, $visibility)
private function createPost($message, $visibility, $state = Content::STATE_PUBLISHED)
{
$post = new Post;
$post->message = $message;
$post->content->setContainer($this->space);
$post->content->visibility = $visibility;
$post->content->state = $state;
$post->save();
return $post->content->id;

View File

@ -0,0 +1,39 @@
<?php
namespace humhub\modules\content\widgets;
use humhub\components\Widget;
use humhub\libs\Html;
use humhub\modules\content\models\Content;
use Yii;
use yii\helpers\Url;
class PublishDraftLink extends Widget
{
/**
* @var \humhub\modules\content\components\ContentActiveRecord
*/
public $content;
/**
* @inheritdoc
*/
public function run()
{
if ($this->content->content->state !== Content::STATE_DRAFT ||
!$this->content->content->canEdit()) {
return '';
}
$publishUrl = Url::to(['/content/content/publish-draft', 'id' => $this->content->content->id]);
return Html::tag('li',
Html::a(
'<i class="fa fa-mail-reply-all"></i> '
. Yii::t('ContentModule.base', 'Publish draft'),
'#', ['data-action-click' => 'publishDraft', 'data-action-url' => $publishUrl])
);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace humhub\modules\content\widgets;
use humhub\components\Widget;
use humhub\libs\Html;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\models\Content;
use Yii;
/**
* Can be used to render an archive icon for archived content.
* @package humhub\modules\content\widgets
* @since 1.14
*/
class StateBadge extends Widget
{
public ?ContentActiveRecord $model;
public function run()
{
if ($this->model === null) {
return '';
}
if ($this->model->content->state === Content::STATE_DRAFT) {
return Html::tag(
'span', Yii::t('ContentModule.base', 'Draft'),
['class' => 'label label-danger label-state-draft']
);
} elseif ($this->model->content->state === Content::STATE_DELETED) {
return Html::tag(
'span', Yii::t('ContentModule.base', 'Deleted'),
['class' => 'label label-danger label-state-deleted']
);
}
}
}

View File

@ -115,6 +115,10 @@ abstract class WallCreateContentForm extends Widget
$record->content->visibility = $visibility;
$record->content->container = $contentContainer;
if ((bool)Yii::$app->request->post('isDraft', false)) {
$record->content->state = Content::STATE_DRAFT;
}
// Handle Notify User Features of ContentFormWidget
// ToDo: Check permissions of user guids
$userGuids = Yii::$app->request->post('notifyUserInput');
@ -129,7 +133,7 @@ abstract class WallCreateContentForm extends Widget
if ($record->save()) {
$topics = Yii::$app->request->post('postTopicInput');
if(!empty($topics)) {
if (!empty($topics)) {
Topic::attach($record->content, $topics);
}

View File

@ -6,6 +6,7 @@ namespace humhub\modules\content\widgets\stream;
use Exception;
use humhub\libs\Html;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\models\Content;
use humhub\modules\content\widgets\ArchiveLink;
use humhub\modules\content\widgets\DeleteLink;
use humhub\modules\content\widgets\LockCommentsLink;
@ -14,6 +15,7 @@ use humhub\modules\content\widgets\MoveContentLink;
use humhub\modules\content\widgets\NotificationSwitchLink;
use humhub\modules\content\widgets\PermaLink;
use humhub\modules\content\widgets\PinLink;
use humhub\modules\content\widgets\PublishDraftLink;
use humhub\modules\content\widgets\VisibilityLink;
use humhub\modules\dashboard\controllers\DashboardController;
use humhub\modules\space\models\Space;
@ -324,6 +326,10 @@ abstract class WallStreamEntryWidget extends StreamEntryWidget
*/
public function getControlsMenuEntries()
{
if ($this->model->content->state === Content::STATE_DELETED) {
return [];
}
if($this->renderOptions->isViewContext([WallStreamEntryOptions::VIEW_CONTEXT_SEARCH])) {
return [
[PermaLink::class, ['content' => $this->model], ['sortOrder' => 200]]
@ -331,6 +337,7 @@ abstract class WallStreamEntryWidget extends StreamEntryWidget
}
$result = [
[PublishDraftLink::class, ['content' => $this->model], ['sortOrder' => 100]],
[PermaLink::class, ['content' => $this->model], ['sortOrder' => 200]],
[DeleteLink::class, ['content' => $this->model], ['sortOrder' => 300]],
new DropdownDivider(['sortOrder' => 350]),

View File

@ -2,8 +2,10 @@
use humhub\libs\Html;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\models\Content;
use humhub\modules\content\widgets\ArchivedIcon;
use humhub\modules\content\widgets\LockCommentsIcon;
use humhub\modules\content\widgets\StateBadge;
use humhub\modules\content\widgets\stream\WallStreamEntryOptions;
use humhub\modules\content\widgets\UpdatedIcon;
use humhub\modules\content\widgets\VisibilityIcon;
@ -29,6 +31,7 @@ $container = $model->content->container;
<?php elseif ($renderOptions->isPinned($model)) : ?>
<?= Icon::get('map-pin', ['htmlOptions' => ['class' => 'icon-pin tt', 'title' => Yii::t('ContentModule.base', 'Pinned')]]) ?>
<?php endif; ?>
<?= StateBadge::widget(['model' => $model]); ?>
</div>
<!-- since v1.2 -->

View File

@ -64,25 +64,31 @@ use yii\helpers\Html;
<?= FileHandlerButtonDropdown::widget(['primaryButton' => $uploadButton, 'handlers' => $fileHandlers, 'cssButtonClass' => 'btn-default']); ?>
<!-- public checkbox -->
<?= Html::checkbox('visibility', '', ['id' => 'contentForm_visibility', 'class' => 'contentForm hidden', 'aria-hidden' => 'true', 'title' => Yii::t('ContentModule.base', 'Content visibility') ]); ?>
<?= Html::checkbox('visibility', '', ['id' => 'contentForm_visibility', 'class' => 'contentForm hidden', 'aria-hidden' => 'true']); ?>
<!-- draft checkbox -->
<?= Html::checkbox('isDraft', '', ['id' => 'contentForm_draft', 'class' => 'contentForm hidden', 'aria-hidden' => 'true']); ?>
<!-- content sharing -->
<div class="pull-right">
<span class="label label-info label-public hidden"><?= Yii::t('ContentModule.base', 'Public'); ?></span>
<span class="label-container">
<span class="label label-info label-public hidden"><?= Yii::t('ContentModule.base', 'Public'); ?></span>
<span class="label label-warning label-draft hidden"><?= Yii::t('ContentModule.base', 'Draft'); ?></span>
</span>
<ul class="nav nav-pills preferences" style="right:0;top:5px">
<li class="dropdown">
<a class="dropdown-toggle" style="padding:5px 10px" data-toggle="dropdown" href="#" aria-label="<?= Yii::t('base', 'Toggle post menu'); ?>" aria-haspopup="true">
<?= Icon::get('cogs')?>
<a class="dropdown-toggle" style="padding:5px 10px" data-toggle="dropdown" href="#"
aria-label="<?= Yii::t('base', 'Toggle post menu'); ?>" aria-haspopup="true">
<?= Icon::get('cogs') ?>
</a>
<ul class="dropdown-menu pull-right">
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Notify members'), 'notifyUser')->icon('bell')?>
<?= Link::withAction(Yii::t('ContentModule.base', 'Notify members'), 'notifyUser')->icon('bell') ?>
</li>
<?php if (TopicPicker::showTopicPicker($contentContainer)) : ?>
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Topics'), 'setTopics')->icon(Yii::$app->getModule('topic')->icon) ?>
<?= Link::withAction(Yii::t('ContentModule.base', 'Topics'), 'setTopics')->icon(Yii::$app->getModule('topic')->icon) ?>
</li>
<?php endif; ?>
<?php if ($canSwitchVisibility): ?>
@ -91,6 +97,10 @@ use yii\helpers\Html;
->id('contentForm_visibility_entry')->icon('unlock') ?>
</li>
<?php endif; ?>
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Create as draft'), 'changeDraftState')
->id('contentForm_visibility_entry')->icon('edit') ?>
</li>
</ul>
</li>
</ul>
@ -99,4 +109,4 @@ use yii\helpers\Html;
<?= UploadProgress::widget(['id' => 'contentFormFiles_progress']) ?>
<?= FilePreview::widget(['id' => 'contentFormFiles_preview', 'edit' => true, 'options' => ['style' => 'margin-top:10px;']]); ?>
</div><!-- /contentForm_Options -->
</div><!-- /contentForm_Options -->

View File

@ -23,10 +23,10 @@ return [
['space_id' => '1', 'user_id' => '1', 'originator_user_id' => null, 'status' => '3', 'request_message' => null, 'last_visit' => '2014-08-08 06:49:57', 'group_id' => 'admin', 'created_at' => '2014-08-08 05:36:05', 'created_by' => '1', 'updated_at' => '2014-08-08 05:36:05', 'updated_by' => '1'],
['space_id' => '1', 'user_id' => '3', 'originator_user_id' => null, 'status' => '3', 'request_message' => null, 'last_visit' => null, 'group_id' => 'member', 'created_at' => '2014-08-10 16:55:41', 'created_by' => null, 'updated_at' => null, 'updated_by' => null],
// User 2 is Member/Admin of Space 2
// User 1 is Member/Admin of Space 2
['space_id' => '2', 'user_id' => '2', 'originator_user_id' => null, 'status' => '3', 'request_message' => null, 'last_visit' => '2014-08-08 06:49:57', 'group_id' => 'admin', 'created_at' => '2014-08-08 05:36:05', 'created_by' => '1', 'updated_at' => '2014-08-08 05:36:05', 'updated_by' => '1'],
// User 1 is admin of space 3 and user 2 & 3 are members
// Admin is admin of space 3 and user 1 & 2 are members
['space_id' => '3', 'user_id' => '1', 'originator_user_id' => null, 'status' => '3', 'request_message' => null, 'last_visit' => '2014-08-08 06:49:57', 'group_id' => 'admin', 'created_at' => '2014-08-08 05:36:05', 'created_by' => '1', 'updated_at' => '2014-08-08 05:36:05', 'updated_by' => '1'],
['space_id' => '3', 'user_id' => '2', 'originator_user_id' => null, 'status' => '3', 'send_notifications' => '1', 'request_message' => null, 'last_visit' => null, 'group_id' => 'member', 'created_at' => '2014-08-10 16:55:41', 'created_by' => null, 'updated_at' => null, 'updated_by' => null],
['space_id' => '3', 'user_id' => '3', 'originator_user_id' => null, 'status' => '3', 'request_message' => null, 'last_visit' => '2014-08-08 06:49:57', 'group_id' => 'moderator', 'created_at' => '2014-08-08 05:36:05', 'created_by' => '1', 'updated_at' => '2014-08-08 05:36:05', 'updated_by' => '1'],

View File

@ -30,7 +30,10 @@ class Module extends \humhub\components\Module
/**
* @var array default content classes which are not suppressed when in a row
*/
public $defaultStreamSuppressQueryIgnore = [\humhub\modules\post\models\Post::class, \humhub\modules\activity\models\Activity::class];
public $defaultStreamSuppressQueryIgnore = [
\humhub\modules\post\models\Post::class,
\humhub\modules\activity\models\Activity::class
];
/**
* @var int number of contents from which "Show more" appears in the stream

View File

@ -22,7 +22,6 @@ use yii\base\InvalidConfigException;
*/
class ContentContainerStream extends Stream
{
/**
* @inheritdoc
*/
@ -40,6 +39,7 @@ class ContentContainerStream extends Stream
protected function initQuery($options = [])
{
$options['container'] = $this->contentContainer;
return parent::initQuery($options);
}
}

View File

@ -124,9 +124,8 @@ abstract class Stream extends Action
public $limit = 4;
/**
* Filters
*
* @var array
* Filters - A list of active filter id's (e.g. `visibility_private`)
* @var string[]
*/
public $filters = [];

View File

@ -7,6 +7,7 @@ namespace humhub\modules\stream\models;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\stream\models\filters\ContentContainerStreamFilter;
use humhub\modules\stream\models\filters\PinnedContentStreamFilter;
use humhub\modules\stream\models\filters\StreamQueryFilter;
use yii\base\InvalidConfigException;
/**
@ -26,11 +27,6 @@ class ContentContainerStreamQuery extends WallStreamQuery
*/
public $pinnedContentSupport = true;
/**
* @var PinnedContentStreamFilter
*/
private $pinnedContentStreamFilter;
/**
* @inheritdoc
* @throws InvalidConfigException
@ -39,25 +35,15 @@ class ContentContainerStreamQuery extends WallStreamQuery
{
parent::beforeApplyFilters();
$this->addFilterHandler(new ContentContainerStreamFilter(['container' => $this->container]));
$this->addFilterHandler(
new ContentContainerStreamFilter(['container' => $this->container]),
true,
true
);
$this->pinnedContentStreamFilter = new PinnedContentStreamFilter(['container' => $this->container]);
if($this->pinnedContentSupport) {
$this->addFilterHandler($this->pinnedContentStreamFilter);
if ($this->pinnedContentSupport) {
$this->addFilterHandler(new PinnedContentStreamFilter(['container' => $this->container]));
}
}
public function all()
{
$result = parent::all();
if(!empty($this->pinnedContentStreamFilter->pinnedContent)) {
$result = array_merge($this->pinnedContentStreamFilter->pinnedContent, $result);
}
return $result;
}
}

View File

@ -4,6 +4,7 @@ namespace humhub\modules\stream\models;
use humhub\modules\stream\models\filters\BlockedUsersStreamFilter;
use humhub\modules\stream\models\filters\DateStreamFilter;
use humhub\modules\stream\models\filters\DraftContentStreamFilter;
use humhub\modules\stream\models\filters\StreamQueryFilter;
use Yii;
use yii\base\InvalidConfigException;
@ -136,9 +137,18 @@ class StreamQuery extends Model
ContentTypeStreamFilter::class,
OriginatorStreamFilter::class,
BlockedUsersStreamFilter::class,
DateStreamFilter::class
DateStreamFilter::class,
DraftContentStreamFilter::class
];
/**
* Per default only content with published state are returned.
*
* @note Used, for example, by the Recycle Bin module to display deleted content in the stream.
* @var array
*/
public $stateFilterCondition = ['OR', ['content.state' => Content::STATE_PUBLISHED]];
/**
* The content query.
*
@ -386,7 +396,30 @@ class StreamQuery extends Model
*/
public function all()
{
return $this->query(!$this->_built)->all();
return $this->postProcessAll(
$this->query(!$this->_built)->all()
);
}
/**
* @param Content[] $result
* @return Content[]
* @since 1.14
*/
protected function postProcessAll(array $result)
{
/**
* Allow FilterHandler to directly modify the stream content result
* e.g. (e.g. Pinned/Drafts) can inject additional content on the first stream batch
*/
foreach ($this->filterHandlers as $filterHandler) {
if ($filterHandler instanceof StreamQueryFilter) {
$filterHandler->postProcessStreamResult($result);
} else {
Yii::warning('StreamQuery::postProcessAll - invalid FilterHandler: ' . var_export($filterHandler, true), 'content');
}
}
return $result;
}
/**
@ -402,6 +435,8 @@ class StreamQuery extends Model
$this->setupCriteria();
$this->setupFilters();
$this->_query->andWhere($this->stateFilterCondition);
if (!empty($this->channel)) {
$this->channel($this->channel);
}
@ -529,8 +564,8 @@ class StreamQuery extends Model
{
$this->beforeApplyFilters();
foreach ($this->filterHandlers as $handler) {
$this->prepareHandler($handler)->apply();
foreach (array_keys($this->filterHandlers) as $i) {
$this->prepareHandler($this->filterHandlers[$i])->apply();
}
$this->afterApplyFilters();
@ -592,24 +627,35 @@ class StreamQuery extends Model
* @throws InvalidConfigException
* @since 1.6
*/
public function addFilterHandler($handler, $overwrite = true)
public function addFilterHandler($handler, $overwrite = true, $prepend = false)
{
if($overwrite) {
if ($overwrite) {
$this->removeFilterHandler($handler);
}
$handler = $this->prepareHandler($handler);
return $this->filterHandlers[] = $handler;
/**
* Some Filters must be prepended, when other filters rely on them.
* E.g. `ContentContainerStreamFilter` is required for `DraftContentStreamFilter` which clones
* the current query to get all drafts (for ContentContainer or Dashboard)
*/
if ($prepend) {
array_unshift($this->filterHandlers, $handler);
} else {
$this->filterHandlers[] = $handler;
}
return $handler;
}
/**
* Can be used to add multiple filter handlers at once.
*
* @see self::addFilterHandler
* @param $handlers
* @param bool $overwrite
* @return string[]|StreamQueryFilter[]
* @throws InvalidConfigException
* @see self::addFilterHandler
*/
public function addFilterHandlers($handlers, $overwrite = true)
{
@ -634,7 +680,7 @@ class StreamQuery extends Model
$result = [];
$handlerToRemoveClass = is_string($handlerToRemove) ? $handlerToRemove : get_class($handlerToRemove);
foreach ($this->filterHandlers as $handler) {
if(!is_a($handler, $handlerToRemoveClass, true)) {
if (!is_a($handler, $handlerToRemoveClass, true)) {
$result[] = $handler;
}
}
@ -650,11 +696,11 @@ class StreamQuery extends Model
* @throws InvalidConfigException
* @since 1.6
*/
public function getFilterHandler($handlerToRemove)
public function getFilterHandler($class): ?StreamQueryFilter
{
$handlerToRemoveClass = is_string($handlerToRemove) ? $handlerToRemove : get_class($handlerToRemove);
$handlerToRemoveClass = is_string($class) ? $class : get_class($class);
foreach ($this->filterHandlers as $handler) {
if(is_a($handler, $handlerToRemoveClass, true)) {
if (is_a($handler, $handlerToRemoveClass, true)) {
return $this->prepareHandler($handler);
}
}
@ -668,7 +714,7 @@ class StreamQuery extends Model
* @throws InvalidConfigException
* @since 1.6
*/
private function prepareHandler($handler)
private function prepareHandler(&$handler)
{
if (is_string($handler)) {
$handler = Yii::createObject([

View File

@ -139,7 +139,7 @@ class StreamSuppressQuery extends StreamQuery
$this->_query->limit = $originalLimit;
$this->isQueryExecuted = true;
return $results;
return $this->postProcessAll($results);
}
/**

View File

@ -0,0 +1,56 @@
<?php
namespace humhub\modules\stream\models\filters;
use humhub\modules\activity\stream\ActivityStreamQuery;
use humhub\modules\content\models\Content;
use humhub\modules\stream\models\StreamQuery;
use Yii;
/**
* @since 1.14
*/
class DraftContentStreamFilter extends StreamQueryFilter
{
/**
* @var Content[]
*/
private array $draftContent = [];
/**
* @inheritDoc
*/
public function apply()
{
if ($this->streamQuery instanceof ActivityStreamQuery && $this->streamQuery->activity) {
return;
}
if ($this->streamQuery->isInitialQuery()) {
$this->fetchDraftContent();
}
}
/**
* @return void
*/
private function fetchDraftContent(): void
{
$draftQuery = clone $this->query;
$draftQuery->andWhere([
'AND', ['content.state' => Content::STATE_DRAFT],
['content.created_by' => Yii::$app->user->id]]
);
$draftQuery->limit(100);
$this->draftContent = $draftQuery->all();
}
/**
* @inheritDoc
*/
public function postProcessStreamResult(array &$results): void
{
$results = array_merge($this->draftContent, $results);
}
}

View File

@ -33,7 +33,7 @@ class PinnedContentStreamFilter extends StreamQueryFilter
/**
* @var Content[]
*/
public $pinnedContent = [];
private $pinnedContent = [];
/**
* @inheritDoc
@ -41,34 +41,44 @@ class PinnedContentStreamFilter extends StreamQueryFilter
public function apply()
{
// Currently we only support pinned entries on container streams
if(!$this->container) {
if (!$this->container) {
return;
}
if ($this->streamQuery->isInitialQuery()) {
$pinnedContentIds = $this->fetchPinnedContent();
if ($this->streamQuery->isInitialQuery()) {
$pinnedContentIds = $this->fetchPinnedContent();
// Exclude pinned content from result, we've already fetched and cached them
if(!empty($pinnedContentIds)) {
// Exclude pinned content from result, we've already fetched and cached them
if (!empty($pinnedContentIds)) {
$this->query->andWhere((['NOT IN', 'content.id', $pinnedContentIds]));
}
} else if(!$this->streamQuery->isSingleContentQuery()) {
} else if (!$this->streamQuery->isSingleContentQuery()) {
// All pinned entries of this container were loaded within the initial request, so don't include them here!
$this->query->andWhere(['OR', ['content.pinned' => 0], ['<>', 'content.contentcontainer_id', $this->container->contentcontainer_id]]);
}
}
/**
* @inheritDoc
*/
public function postProcessStreamResult(array &$results): void
{
$results = array_merge($this->pinnedContent, $results);
}
/**
* Loads pinned content entries into [[pinnedContent]] by means of a cloned stream query.
* @return array array of pinned content ids
*/
private function fetchPinnedContent()
private function fetchPinnedContent(): array
{
$pinnedQuery = clone $this->query;
$pinnedQuery->andWhere(['AND', ['content.pinned' => 1], ['content.contentcontainer_id' => $this->container->contentcontainer_id]]);
$pinnedQuery->andWhere(['AND', [
'content.pinned' => 1],
['content.contentcontainer_id' => $this->container->contentcontainer_id]]);
$pinnedQuery->limit(1000);
$this->pinnedContent = $pinnedQuery->all();
return array_map(function($content) {
return array_map(function ($content) {
return $content->id;
}, $this->pinnedContent);
}

View File

@ -9,6 +9,7 @@
namespace humhub\modules\stream\models\filters;
use humhub\modules\content\models\Content;
use humhub\modules\stream\models\ContentContainerStreamQuery;
use humhub\modules\stream\models\StreamQuery;
use humhub\modules\ui\filter\models\QueryFilter;
@ -16,7 +17,7 @@ use humhub\modules\ui\filter\models\QueryFilter;
abstract class StreamQueryFilter extends QueryFilter
{
/**
* @var StreamQuery | ContentContainerStreamQuery
* @var StreamQuery|ContentContainerStreamQuery
*/
public $streamQuery;
@ -45,4 +46,14 @@ abstract class StreamQueryFilter extends QueryFilter
return $this->formName ?: 'StreamQuery';
}
/**
* This method allows the stream filter direct access to returned Content[] array.
* e.g. additional entries can be injected
*
* @param Content[] $results
*/
public function postProcessStreamResult(array &$results): void
{
}
}

View File

@ -322,6 +322,22 @@ humhub.module('stream.StreamEntry', function (module, require, $) {
});
};
/**
* Publish draft of this entry from the top of the stream.
* @param evt
*/
StreamEntry.prototype.publishDraft = function (evt) {
var that = this;
this.loader();
client.post(evt.url).then(function (data) {
that.stream().init();
module.log.info(data.message, true);
}).catch(function (e) {
module.log.error(e, true);
that.loader(false);
});
};
/**
* Replaces this entries dom element.

View File

@ -10,6 +10,7 @@
namespace humhub\modules\topic\widgets;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\models\Content;
use humhub\widgets\ModalButton;
use humhub\modules\topic\models\Topic;
use humhub\modules\content\widgets\WallEntryControlLink;
@ -26,6 +27,10 @@ class ContentTopicButton extends WallEntryControlLink
public function renderLink()
{
if ($this->record->content->state === Content::STATE_DELETED) {
return '';
}
return ModalButton::asLink(Yii::t('TopicModule.base', 'Topics'))->icon(Topic::getIcon())
->load(Url::to(['/topic/content-topic', 'contentId' => $this->record->content->id]));
}

View File

@ -62,6 +62,7 @@ class LayoutHeader extends Widget
navigator.serviceWorker.register('$serviceWorkUrl', { scope: '$rootPath' })
.then(function (registration) {
if (typeof afterServiceWorkerRegistration === "function") {
// Allow Modules like `fcm-push` to register after registration
afterServiceWorkerRegistration(registration);
}
})

View File

@ -1,195 +1,25 @@
// Content create form
.contentForm_options {
margin-top: 10px;
min-height: 29px;
.btn_container {
position: relative;
.label-public {
position: absolute;
right: 40px;
top: 11px;
}
}
}
#content-topic-bar {
margin-top: 5px;
text-align: right;
.label {
margin-left: 4px;
}
}
#contentFormBody {
.form-group, .help-block-error {
margin: 0;
}
}
// Empty stream info
.placeholder-empty-stream {
background-image: url("../img/placeholder-postform-arrow.png");
background-repeat: no-repeat;
padding: 37px 0 0 70px;
margin-left: 90px;
}
#streamUpdateBadge {
text-align: center;
z-index: 9999;
margin-bottom: 15px;
margin-top: 15px;
.label {
border-radius: 10px;
font-size: 0.8em !important;
padding: 5px 10px;
}
}
#wallStream {
.back_button_holder {
padding-bottom: 15px;
}
}
@wallEntryInnerPadding: 10px;
@wallEntryContentLeftPadding: 50px;
@wallEntryContentLeftPaddingMobile: 0;
// Wall-Entries
.wall-entry {
position: relative;
.panel .panel-body {
padding: @wallEntryInnerPadding;
}
.wall-entry-header {
//
// popover
// --------------------------------------------------
.popover {
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
.popover-title {
background: none;
border-bottom: none;
color: @text-color-highlight;
position: relative;
padding-bottom: @wallEntryInnerPadding;
margin-bottom: @wallEntryInnerPadding;
border-bottom: 1px solid #eeeeee;
.img-space {
top: 25px;
left: 25px;
}
.wall-entry-container-link {
color: @link;
}
.stream-entry-icon-list {
position: absolute;
top: 0;
right: 25px;
display: inline-block;
padding-top: 2px;
i {
padding-right: 5px;
}
.icon-pin {
color: @danger;
}
.fa-archive {
color: @warning;
}
}
.wall-entry-header-image {
display: table-cell;
width: 40px;
padding-right: @wallEntryInnerPadding;
.fa {
font-size: 2.39em;
color: @info;
margin-top: 5px;
}
}
.wall-entry-header-info {
display: table-cell;
padding-right: 30px;
width: 100%;
.media-heading {
font-size: 15px;
padding-top: 1px;
margin-bottom: 3px;
}
i.archived {
color: @warning;
}
}
.preferences {
position: absolute;
right: 0;
top: 0;
}
.media-subheading {
i.fa-caret-right {
margin: 0 2px;
}
.wall-entry-icons {
display: inline-block;
i {
margin-right: 2px;
}
}
.time, i, span {
font-size: 11px;
white-space: nowrap;
}
}
font-weight: 300;
font-size: 16px;
padding: 15px;
}
.wall-entry-body {
padding-left: @wallEntryContentLeftPadding;
padding-right: @wallEntryContentLeftPadding;
.popover-content {
font-size: 13px;
padding: 5px 15px;
color: @text-color-highlight;
.wall-entry-content {
margin-bottom: 5px;
.post-short-text {
font-size: 1.6em;
.emoji {
width: 20px;
}
}
}
audio, video {
width: 100%;
}
}
.wall-stream-footer {
.wall-stream-addons {
.files {
margin-bottom: 5px;
}
}
}
.content {
a {
color: @link;
}
@ -199,273 +29,7 @@
}
}
.media {
overflow: visible;
}
.well {
margin-bottom: 0;
.comment {
.show-all-link {
font-size: 12px;
cursor: pointer;
}
}
}
.media-heading {
font-size: 14px;
padding-top: 1px;
margin-bottom: 3px;
.labels {
padding-right: 32px;
}
.viaLink {
font-size: 13px;
i {
color: @text-color-soft;
padding-left: 4px;
padding-right: 4px;
}
}
}
.media-subheading {
color: @text-color-soft;
font-size: 12px;
.time {
font-size: 12px;
white-space: nowrap;
}
}
[data-ui-richtext] {
h1 {
font-size: 1.45em;
font-weight: normal;
}
h2 {
font-size: 1.3em;
font-weight: normal;
}
h3 {
font-size: 1.2em;
font-weight: normal;
}
h4 {
font-size: 1.1em;
font-weight: normal;
}
h5 {
font-size: 1.0em;
font-weight: normal;
}
h6 {
font-size: .85em;
font-weight: normal;
}
}
}
@media (max-width: 767px) {
.wall-entry .wall-entry-body {
padding-left: @wallEntryContentLeftPaddingMobile;
padding-right: @wallEntryContentLeftPaddingMobile;
}
#wallStream {
.back_button_holder {
padding-bottom: 5px;
text-align:center;
}
}
}
.wall-entry-controls a {
font-size: 11px;
color: @link !important;
margin-top: 10px;
margin-bottom: 0;
}
#wall-stream-filter-nav {
font-size: 12px;
margin-bottom: 10px;
padding-top: 2px;
border-radius: 0 0 4px 4px;
.wall-stream-filter-root {
margin: 0;
border: 0 !important;
}
.filter-panel {
padding: 0 10px;
}
.wall-stream-filter-head {
padding: 5px 5px 10px 5px;
border-bottom: 1px solid #ddd;
}
.wall-stream-filter-body {
overflow: hidden;
background-color: @background-color-secondary;
border: 1px solid #ddd;
border-top: 0;
border-radius: 0 0 4px 4px;
}
hr {
margin: 5px 0 0 0;
}
.topic-remove-label {
float: left;
}
.topic-remove-label, .content-type-remove-label {
margin-right: 6px;
}
.select2 {
width: 260px !important;
margin-bottom: 5px;
margin-top: 2px;
.select2-search__field {
height: 25px !important;
}
.select2-selection__choice {
height: 23px !important;
span, i {
line-height: 19px !important;
}
.img-rounded {
width: 18px !important;
height: 18px !important;
}
}
}
.wall-stream-filter-bar {
display: inline;
float: right;
white-space: normal;
.label {
height: 18px;
padding-top: 4px;
background-color: @background-color-main;
}
.btn, .label {
box-shadow: 0 0 2px @text-color-secondary;
}
}
}
@media (max-width: 767px) {
#wall-stream-filter-nav {
.wall-stream-filter-root {
white-space: nowrap;
}
.wall-stream-filter-body {
overflow: auto;
}
margin-bottom: 5px;
}
}
.filter-root {
margin: 15px;
.row {
display: table !important;
}
.filter-panel {
padding: 0 5px;
display: table-cell !important;
float: none;
.filter-block {
strong {
margin-bottom: 5px;
}
ul.filter-list {
list-style: none;
padding: 0;
margin: 0 0 5px;
li {
font-size: 12px;
padding: 2px;
a {
color: @text-color-highlight;
}
}
}
}
div.filter-block:last-of-type {
ul.filter-list {
margin: 0;
}
}
}
.filter-panel + .filter-panel {
border-left: 2px solid @background-color-page;
}
}
.stream-entry-loader {
float: right;
margin-top: 5px;
}
.load-suppressed {
margin-top: -17px;
margin-bottom: 15px;
text-align: center;
a {
display: inline-block;
background-color: white;
padding: 5px;
border-radius: 0 0 4px 4px;
border: 1px solid #ddd;
font-size: 11px
}
}
@media print {
.wall-entry {
page-break-inside: avoid;
}
#wall-stream-filter-nav,
#contentFormBody {
display: none;
.popover-navigation {
padding: 15px;
}
}

View File

@ -6,7 +6,7 @@
.btn_container {
position: relative;
.label-public {
.label-container {
position: absolute;
right: 40px;
top: 11px;

File diff suppressed because one or more lines are too long