diff --git a/resources/sql/autopatches/20161115.phamepost.02.header.sql b/resources/sql/autopatches/20161115.phamepost.02.header.sql new file mode 100644 --- /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 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4085,6 +4085,7 @@ '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 @@ '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 --- a/src/applications/phame/application/PhabricatorPhameApplication.php +++ b/src/applications/phame/application/PhabricatorPhameApplication.php @@ -53,6 +53,7 @@ '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 --- a/src/applications/phame/controller/PhameLiveController.php +++ b/src/applications/phame/controller/PhameLiveController.php @@ -90,6 +90,7 @@ 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 --- /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 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -19,9 +19,11 @@ $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 @@ ->setCrumbs($crumbs) ->appendChild( array( + $hero, $document, $about, $properties, @@ -206,6 +209,13 @@ $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') ->setHref($this->getApplicationURI('post/move/'.$id.'/')) ->setName(pht('Move Post')) @@ -307,4 +317,33 @@ 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 --- a/src/applications/phame/editor/PhamePostEditor.php +++ b/src/applications/phame/editor/PhamePostEditor.php @@ -19,6 +19,7 @@ $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 @@ return $object->getBody(); case PhamePostTransaction::TYPE_VISIBILITY: return $object->getVisibility(); + case PhamePostTransaction::TYPE_HEADERIMAGE: + return $object->getHeaderImagePHID(); } } @@ -51,6 +54,7 @@ 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 @@ 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 @@ 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 --- a/src/applications/phame/query/PhamePostQuery.php +++ b/src/applications/phame/query/PhamePostQuery.php @@ -9,6 +9,8 @@ private $publishedAfter; private $phids; + private $needHeaderImage; + public function withIDs(array $ids) { $this->ids = $ids; return $this; @@ -39,6 +41,11 @@ return $this; } + public function needHeaderImage($need) { + $this->needHeaderImage = $need; + return $this; + } + public function newResultObject() { return new PhamePost(); } @@ -71,6 +78,28 @@ $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 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -26,8 +26,10 @@ 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 @@ 'phameTitle' => 'sort64?', 'visibility' => 'uint32', 'mailKey' => 'bytes20', + 'headerImagePHID' => 'phid?', // T6203/NULLABILITY // These seem like they should always be non-null? @@ -172,6 +175,19 @@ 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 --- a/src/applications/phame/storage/PhamePostTransaction.php +++ b/src/applications/phame/storage/PhamePostTransaction.php @@ -7,6 +7,7 @@ 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 @@ 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 @@ '%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 @@ $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(