From ce0cb115ca2ad15dc7d9deafa0bda313b068cf56 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 15 Nov 2016 13:20:13 -0800 Subject: [PATCH] Add Hero Image to Phame Post Summary: Adds a headerimage and lets you set it on posts for added reverence. Is that a word? Test Plan: Add an image, see an image. {F1923010} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16873 --- .../20161115.phamepost.02.header.sql | 2 + src/__phutil_library_map__.php | 2 + .../PhabricatorPhameApplication.php | 1 + .../phame/controller/PhameLiveController.php | 1 + .../post/PhamePostHeaderPictureController.php | 136 ++++++++++++++++++ .../post/PhamePostViewController.php | 41 +++++- .../phame/editor/PhamePostEditor.php | 7 + .../phame/query/PhamePostQuery.php | 29 ++++ src/applications/phame/storage/PhamePost.php | 16 +++ .../phame/storage/PhamePostTransaction.php | 15 ++ 10 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 resources/sql/autopatches/20161115.phamepost.02.header.sql create mode 100644 src/applications/phame/controller/post/PhamePostHeaderPictureController.php diff --git a/resources/sql/autopatches/20161115.phamepost.02.header.sql b/resources/sql/autopatches/20161115.phamepost.02.header.sql new file mode 100644 index 0000000000..fd62d57058 --- /dev/null +++ b/resources/sql/autopatches/20161115.phamepost.02.header.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phame.phame_post + ADD headerImagePHID VARBINARY(64); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 88315817b0..ab88fad093 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4085,6 +4085,7 @@ phutil_register_library_map(array( 'PhamePostEditEngine' => 'applications/phame/editor/PhamePostEditEngine.php', 'PhamePostEditor' => 'applications/phame/editor/PhamePostEditor.php', 'PhamePostFulltextEngine' => 'applications/phame/search/PhamePostFulltextEngine.php', + 'PhamePostHeaderPictureController' => 'applications/phame/controller/post/PhamePostHeaderPictureController.php', 'PhamePostHistoryController' => 'applications/phame/controller/post/PhamePostHistoryController.php', 'PhamePostListController' => 'applications/phame/controller/post/PhamePostListController.php', 'PhamePostListView' => 'applications/phame/view/PhamePostListView.php', @@ -9324,6 +9325,7 @@ phutil_register_library_map(array( 'PhamePostEditEngine' => 'PhabricatorEditEngine', 'PhamePostEditor' => 'PhabricatorApplicationTransactionEditor', 'PhamePostFulltextEngine' => 'PhabricatorFulltextEngine', + 'PhamePostHeaderPictureController' => 'PhamePostController', 'PhamePostHistoryController' => 'PhamePostController', 'PhamePostListController' => 'PhamePostController', 'PhamePostListView' => 'AphrontTagView', diff --git a/src/applications/phame/application/PhabricatorPhameApplication.php b/src/applications/phame/application/PhabricatorPhameApplication.php index f56702ed62..873219886b 100644 --- a/src/applications/phame/application/PhabricatorPhameApplication.php +++ b/src/applications/phame/application/PhabricatorPhameApplication.php @@ -53,6 +53,7 @@ final class PhabricatorPhameApplication extends PhabricatorApplication { 'preview/' => 'PhabricatorMarkupPreviewController', 'move/(?P\d+)/' => 'PhamePostMoveController', 'archive/(?P\d+)/' => 'PhamePostArchiveController', + 'header/(?P[1-9]\d*)/' => 'PhamePostHeaderPictureController', ), 'blog/' => array( '(?:query/(?P[^/]+)/)?' => 'PhameBlogListController', diff --git a/src/applications/phame/controller/PhameLiveController.php b/src/applications/phame/controller/PhameLiveController.php index 2d8b2ee45f..b5b1984816 100644 --- a/src/applications/phame/controller/PhameLiveController.php +++ b/src/applications/phame/controller/PhameLiveController.php @@ -90,6 +90,7 @@ abstract class PhameLiveController extends PhameController { if (strlen($post_id)) { $post_query = id(new PhamePostQuery()) ->setViewer($viewer) + ->needHeaderImage(true) ->withIDs(array($post_id)); if ($blog) { diff --git a/src/applications/phame/controller/post/PhamePostHeaderPictureController.php b/src/applications/phame/controller/post/PhamePostHeaderPictureController.php new file mode 100644 index 0000000000..2e60c9ba71 --- /dev/null +++ b/src/applications/phame/controller/post/PhamePostHeaderPictureController.php @@ -0,0 +1,136 @@ +getViewer(); + $id = $request->getURIData('id'); + + $post = id(new PhamePostQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needHeaderImage(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$post) { + return new Aphront404Response(); + } + + $post_uri = '/phame/post/view/'.$id; + + $supported_formats = PhabricatorFile::getTransformableImageFormats(); + $e_file = true; + $errors = array(); + $delete_header = ($request->getInt('delete') == 1); + + if ($request->isFormPost()) { + if ($request->getFileExists('header')) { + $file = PhabricatorFile::newFromPHPUpload( + $_FILES['header'], + array( + 'authorPHID' => $viewer->getPHID(), + 'canCDN' => true, + )); + } else if (!$delete_header) { + $e_file = pht('Required'); + $errors[] = pht( + 'You must choose a file when uploading a new post header.'); + } + + if (!$errors && !$delete_header) { + if (!$file->isTransformableImage()) { + $e_file = pht('Not Supported'); + $errors[] = pht( + 'This server only supports these image formats: %s.', + implode(', ', $supported_formats)); + } + } + + if (!$errors) { + if ($delete_header) { + $new_value = null; + } else { + $file->attachToObject($post->getPHID()); + $new_value = $file->getPHID(); + } + + $xactions = array(); + $xactions[] = id(new PhamePostTransaction()) + ->setTransactionType(PhamePostTransaction::TYPE_HEADERIMAGE) + ->setNewValue($new_value); + + $editor = id(new PhamePostEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true); + + $editor->applyTransactions($post, $xactions); + + return id(new AphrontRedirectResponse())->setURI($post_uri); + } + } + + $title = pht('Edit Post Header'); + + $upload_form = id(new AphrontFormView()) + ->setUser($viewer) + ->setEncType('multipart/form-data') + ->appendChild( + id(new AphrontFormFileControl()) + ->setName('header') + ->setLabel(pht('Upload Header')) + ->setError($e_file) + ->setCaption( + pht('Supported formats: %s', implode(', ', $supported_formats)))) + ->appendChild( + id(new AphrontFormCheckboxControl()) + ->setName('delete') + ->setLabel(pht('Delete Header')) + ->addCheckbox( + 'delete', + 1, + null, + null)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton($post_uri) + ->setValue(pht('Upload Header'))); + + $upload_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Upload New Header')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setForm($upload_form); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + $post->getTitle(), + $this->getApplicationURI('post/view/'.$id)); + $crumbs->addTextCrumb(pht('Post Header')); + $crumbs->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Edit Post Header')) + ->setHeaderIcon('fa-camera'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter(array( + $upload_box, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild( + array( + $view, + )); + + } +} diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index a056502349..a73876a197 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -19,9 +19,11 @@ final class PhamePostViewController $is_external = $this->getIsExternal(); $header = id(new PHUIHeaderView()) - ->setHeader($post->getTitle()) + ->addClass('phame-header-bar') ->setUser($viewer); + $hero = $this->buildPhamePostHeader($post); + if (!$is_external) { $actions = $this->renderActions($post); $header->setPolicyObject($post); @@ -167,6 +169,7 @@ final class PhamePostViewController ->setCrumbs($crumbs) ->appendChild( array( + $hero, $document, $about, $properties, @@ -204,6 +207,13 @@ final class PhamePostViewController ->setName(pht('Edit Post')) ->setDisabled(!$can_edit)); + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-camera-retro') + ->setHref($this->getApplicationURI('post/header/'.$id.'/')) + ->setName(pht('Edit Header Image')) + ->setDisabled(!$can_edit)); + $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-arrows') @@ -307,4 +317,33 @@ final class PhamePostViewController return array(head($prev), head($next)); } + private function buildPhamePostHeader( + PhamePost $post) { + + $image = null; + if ($post->getHeaderImagePHID()) { + $image = phutil_tag( + 'div', + array( + 'class' => 'phame-header-hero', + ), + phutil_tag( + 'img', + array( + 'src' => $post->getHeaderImageURI(), + 'class' => 'phame-header-image', + ))); + } + + $title = phutil_tag_div('phame-header-title', $post->getTitle()); + $subtitle = null; + if ($post->getSubtitle()) { + $subtitle = phutil_tag_div('phame-header-subtitle', $post->getSubtitle()); + } + + return phutil_tag_div( + 'phame-mega-header', array($image, $title, $subtitle)); + + } + } diff --git a/src/applications/phame/editor/PhamePostEditor.php b/src/applications/phame/editor/PhamePostEditor.php index 156059cae6..929613fe80 100644 --- a/src/applications/phame/editor/PhamePostEditor.php +++ b/src/applications/phame/editor/PhamePostEditor.php @@ -19,6 +19,7 @@ final class PhamePostEditor $types[] = PhamePostTransaction::TYPE_SUBTITLE; $types[] = PhamePostTransaction::TYPE_BODY; $types[] = PhamePostTransaction::TYPE_VISIBILITY; + $types[] = PhamePostTransaction::TYPE_HEADERIMAGE; $types[] = PhabricatorTransactions::TYPE_COMMENT; return $types; @@ -39,6 +40,8 @@ final class PhamePostEditor return $object->getBody(); case PhamePostTransaction::TYPE_VISIBILITY: return $object->getVisibility(); + case PhamePostTransaction::TYPE_HEADERIMAGE: + return $object->getHeaderImagePHID(); } } @@ -51,6 +54,7 @@ final class PhamePostEditor case PhamePostTransaction::TYPE_SUBTITLE: case PhamePostTransaction::TYPE_BODY: case PhamePostTransaction::TYPE_VISIBILITY: + case PhamePostTransaction::TYPE_HEADERIMAGE: case PhamePostTransaction::TYPE_BLOG: return $xaction->getNewValue(); } @@ -69,6 +73,8 @@ final class PhamePostEditor return $object->setBody($xaction->getNewValue()); case PhamePostTransaction::TYPE_BLOG: return $object->setBlogPHID($xaction->getNewValue()); + case PhamePostTransaction::TYPE_HEADERIMAGE: + return $object->setHeaderImagePHID($xaction->getNewValue()); case PhamePostTransaction::TYPE_VISIBILITY: if ($xaction->getNewValue() == PhameConstants::VISIBILITY_DRAFT) { $object->setDatePublished(0); @@ -93,6 +99,7 @@ final class PhamePostEditor case PhamePostTransaction::TYPE_SUBTITLE: case PhamePostTransaction::TYPE_BODY: case PhamePostTransaction::TYPE_VISIBILITY: + case PhamePostTransaction::TYPE_HEADERIMAGE: case PhamePostTransaction::TYPE_BLOG: return; } diff --git a/src/applications/phame/query/PhamePostQuery.php b/src/applications/phame/query/PhamePostQuery.php index 1a29eda869..357418cfb3 100644 --- a/src/applications/phame/query/PhamePostQuery.php +++ b/src/applications/phame/query/PhamePostQuery.php @@ -9,6 +9,8 @@ final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $publishedAfter; private $phids; + private $needHeaderImage; + public function withIDs(array $ids) { $this->ids = $ids; return $this; @@ -39,6 +41,11 @@ final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery { return $this; } + public function needHeaderImage($need) { + $this->needHeaderImage = $need; + return $this; + } + public function newResultObject() { return new PhamePost(); } @@ -71,6 +78,28 @@ final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery { $post->attachBlog($blog); } + if ($this->needHeaderImage) { + $file_phids = mpull($posts, 'getHeaderImagePHID'); + $file_phids = array_filter($file_phids); + if ($file_phids) { + $files = id(new PhabricatorFileQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + } else { + $files = array(); + } + + foreach ($posts as $post) { + $file = idx($files, $post->getHeaderImagePHID()); + if ($file) { + $post->attachHeaderImageFile($file); + } + } + } + return $posts; } diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index 7eb05e8866..fb959e61f7 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -26,8 +26,10 @@ final class PhamePost extends PhameDAO protected $datePublished; protected $blogPHID; protected $mailKey; + protected $headerImagePHID; private $blog = self::ATTACHABLE; + private $headerImageFile = self::ATTACHABLE; public static function initializePost( PhabricatorUser $blogger, @@ -127,6 +129,7 @@ final class PhamePost extends PhameDAO 'phameTitle' => 'sort64?', 'visibility' => 'uint32', 'mailKey' => 'bytes20', + 'headerImagePHID' => 'phid?', // T6203/NULLABILITY // These seem like they should always be non-null? @@ -172,6 +175,19 @@ final class PhamePost extends PhameDAO return PhabricatorSlug::normalizeProjectSlug($this->getTitle(), true); } + public function getHeaderImageURI() { + return $this->getHeaderImageFile()->getBestURI(); + } + + public function attachHeaderImageFile(PhabricatorFile $file) { + $this->headerImageFile = $file; + return $this; + } + + public function getHeaderImageFile() { + return $this->assertAttached($this->headerImageFile); + } + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ diff --git a/src/applications/phame/storage/PhamePostTransaction.php b/src/applications/phame/storage/PhamePostTransaction.php index 5142e0d594..6e54aeda6d 100644 --- a/src/applications/phame/storage/PhamePostTransaction.php +++ b/src/applications/phame/storage/PhamePostTransaction.php @@ -7,6 +7,7 @@ final class PhamePostTransaction const TYPE_SUBTITLE = 'phame.post.subtitle'; const TYPE_BODY = 'phame.post.body'; const TYPE_VISIBILITY = 'phame.post.visibility'; + const TYPE_HEADERIMAGE = 'phame.post.headerimage'; const TYPE_BLOG = 'phame.post.blog'; const MAILTAG_CONTENT = 'phame-post-content'; @@ -71,6 +72,9 @@ final class PhamePostTransaction case PhabricatorTransactions::TYPE_CREATE: return 'fa-plus'; break; + case self::TYPE_HEADERIMAGE: + return 'fa-camera-retro'; + break; case self::TYPE_VISIBILITY: if ($new == PhameConstants::VISIBILITY_PUBLISHED) { return 'fa-globe'; @@ -156,6 +160,11 @@ final class PhamePostTransaction '%s updated the blog post.', $this->renderHandleLink($author_phid)); break; + case self::TYPE_HEADERIMAGE: + return pht( + '%s updated the header image.', + $this->renderHandleLink($author_phid)); + break; case self::TYPE_VISIBILITY: if ($new == PhameConstants::VISIBILITY_DRAFT) { return pht( @@ -222,6 +231,12 @@ final class PhamePostTransaction $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; + case self::TYPE_HEADERIMAGE: + return pht( + '%s updated the header image for post %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + break; case self::TYPE_VISIBILITY: if ($new == PhameConstants::VISIBILITY_DRAFT) { return pht(