1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-28 16:30:59 +01:00

Improve UI for selecting profile pictures

Summary:
Ref T1703. Move profile pictures to a separate, dedicated interface. Instead of the 35 controls we currently provide, just show all the possible images we can find and then let the user upload an additional one if they want.

Possible improvements to this interface:

  - Write an edge so we can show old profile pictures too.
  - The cropping/scaling got a bit buggy at some point, fix that.
  - Refresh OAuth sources which we're capable of refreshing before showing images (more work than I really want to deal with).
  - We could show little inset icons for the image source ("f" for Facebook, etc.) instead of just the tooltips.

Test Plan:
Chose images, uploaded new images, hit various error cases.

{F49344}

Reviewers: chad, btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2919, T1703

Differential Revision: https://secure.phabricator.com/D6398
This commit is contained in:
epriestley 2013-07-09 16:23:54 -07:00
parent 62ab1dcc62
commit 37b13ef2c9
8 changed files with 373 additions and 137 deletions

View file

@ -3009,6 +3009,15 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/herald/PathTypeahead.js',
),
'people-profile-css' =>
array(
'uri' => '/res/1f0e94c5/rsrc/css/application/people/people-profile.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/people/people-profile.css',
),
'phabricator-action-header-view-css' =>
array(
'uri' => '/res/3b701648/rsrc/css/layout/phabricator-action-header-view.css',
@ -3197,7 +3206,7 @@ celerity_register_resource_map(array(
),
'phabricator-header-view-css' =>
array(
'uri' => '/res/76173bb6/rsrc/css/layout/phabricator-header-view.css',
'uri' => '/res/da35cfa0/rsrc/css/layout/phabricator-header-view.css',
'type' => 'css',
'requires' =>
array(
@ -4140,7 +4149,7 @@ celerity_register_resource_map(array(
), array(
'packages' =>
array(
'd7254b92' =>
'680ace9b' =>
array(
'name' => 'core.pkg.css',
'symbols' =>
@ -4188,7 +4197,7 @@ celerity_register_resource_map(array(
40 => 'phabricator-property-list-view-css',
41 => 'phabricator-tag-view-css',
),
'uri' => '/res/pkg/d7254b92/core.pkg.css',
'uri' => '/res/pkg/680ace9b/core.pkg.css',
'type' => 'css',
),
'75ccea43' =>
@ -4382,16 +4391,16 @@ celerity_register_resource_map(array(
'reverse' =>
array(
'aphront-attached-file-view-css' => 'adc3c36d',
'aphront-dialog-view-css' => 'd7254b92',
'aphront-error-view-css' => 'd7254b92',
'aphront-form-view-css' => 'd7254b92',
'aphront-list-filter-view-css' => 'd7254b92',
'aphront-pager-view-css' => 'd7254b92',
'aphront-panel-view-css' => 'd7254b92',
'aphront-table-view-css' => 'd7254b92',
'aphront-tokenizer-control-css' => 'd7254b92',
'aphront-tooltip-css' => 'd7254b92',
'aphront-typeahead-control-css' => 'd7254b92',
'aphront-dialog-view-css' => '680ace9b',
'aphront-error-view-css' => '680ace9b',
'aphront-form-view-css' => '680ace9b',
'aphront-list-filter-view-css' => '680ace9b',
'aphront-pager-view-css' => '680ace9b',
'aphront-panel-view-css' => '680ace9b',
'aphront-table-view-css' => '680ace9b',
'aphront-tokenizer-control-css' => '680ace9b',
'aphront-tooltip-css' => '680ace9b',
'aphront-typeahead-control-css' => '680ace9b',
'differential-changeset-view-css' => 'dd27a69b',
'differential-core-view-css' => 'dd27a69b',
'differential-inline-comment-editor' => '4ad86dee',
@ -4405,7 +4414,7 @@ celerity_register_resource_map(array(
'differential-table-of-contents-css' => 'dd27a69b',
'diffusion-commit-view-css' => 'c8ce2d88',
'diffusion-icons-css' => 'c8ce2d88',
'global-drag-and-drop-css' => 'd7254b92',
'global-drag-and-drop-css' => '680ace9b',
'inline-comment-summary-css' => 'dd27a69b',
'javelin-aphlict' => '75ccea43',
'javelin-behavior' => 'a9f14d76',
@ -4479,55 +4488,55 @@ celerity_register_resource_map(array(
'javelin-util' => 'a9f14d76',
'javelin-vector' => 'a9f14d76',
'javelin-workflow' => 'a9f14d76',
'lightbox-attachment-css' => 'd7254b92',
'lightbox-attachment-css' => '680ace9b',
'maniphest-task-summary-css' => 'adc3c36d',
'maniphest-transaction-detail-css' => 'adc3c36d',
'phabricator-action-list-view-css' => 'd7254b92',
'phabricator-application-launch-view-css' => 'd7254b92',
'phabricator-action-list-view-css' => '680ace9b',
'phabricator-application-launch-view-css' => '680ace9b',
'phabricator-busy' => '75ccea43',
'phabricator-content-source-view-css' => 'dd27a69b',
'phabricator-core-css' => 'd7254b92',
'phabricator-crumbs-view-css' => 'd7254b92',
'phabricator-core-css' => '680ace9b',
'phabricator-crumbs-view-css' => '680ace9b',
'phabricator-drag-and-drop-file-upload' => '4ad86dee',
'phabricator-dropdown-menu' => '75ccea43',
'phabricator-file-upload' => '75ccea43',
'phabricator-filetree-view-css' => 'd7254b92',
'phabricator-flag-css' => 'd7254b92',
'phabricator-form-view-css' => 'd7254b92',
'phabricator-header-view-css' => 'd7254b92',
'phabricator-filetree-view-css' => '680ace9b',
'phabricator-flag-css' => '680ace9b',
'phabricator-form-view-css' => '680ace9b',
'phabricator-header-view-css' => '680ace9b',
'phabricator-hovercard' => '75ccea43',
'phabricator-jump-nav' => 'd7254b92',
'phabricator-jump-nav' => '680ace9b',
'phabricator-keyboard-shortcut' => '75ccea43',
'phabricator-keyboard-shortcut-manager' => '75ccea43',
'phabricator-main-menu-view' => 'd7254b92',
'phabricator-main-menu-view' => '680ace9b',
'phabricator-menu-item' => '75ccea43',
'phabricator-nav-view-css' => 'd7254b92',
'phabricator-nav-view-css' => '680ace9b',
'phabricator-notification' => '75ccea43',
'phabricator-notification-css' => 'd7254b92',
'phabricator-notification-menu-css' => 'd7254b92',
'phabricator-object-item-list-view-css' => 'd7254b92',
'phabricator-notification-css' => '680ace9b',
'phabricator-notification-menu-css' => '680ace9b',
'phabricator-object-item-list-view-css' => '680ace9b',
'phabricator-object-selector-css' => 'dd27a69b',
'phabricator-phtize' => '75ccea43',
'phabricator-prefab' => '75ccea43',
'phabricator-project-tag-css' => 'adc3c36d',
'phabricator-property-list-view-css' => 'd7254b92',
'phabricator-remarkup-css' => 'd7254b92',
'phabricator-property-list-view-css' => '680ace9b',
'phabricator-remarkup-css' => '680ace9b',
'phabricator-shaped-request' => '4ad86dee',
'phabricator-side-menu-view-css' => 'd7254b92',
'phabricator-standard-page-view' => 'd7254b92',
'phabricator-tag-view-css' => 'd7254b92',
'phabricator-side-menu-view-css' => '680ace9b',
'phabricator-standard-page-view' => '680ace9b',
'phabricator-tag-view-css' => '680ace9b',
'phabricator-textareautils' => '75ccea43',
'phabricator-tooltip' => '75ccea43',
'phabricator-transaction-view-css' => 'd7254b92',
'phabricator-zindex-css' => 'd7254b92',
'phui-button-css' => 'd7254b92',
'phui-form-css' => 'd7254b92',
'phui-icon-view-css' => 'd7254b92',
'phui-spacing-css' => 'd7254b92',
'sprite-apps-large-css' => 'd7254b92',
'sprite-gradient-css' => 'd7254b92',
'sprite-icons-css' => 'd7254b92',
'sprite-menu-css' => 'd7254b92',
'syntax-highlighting-css' => 'd7254b92',
'phabricator-transaction-view-css' => '680ace9b',
'phabricator-zindex-css' => '680ace9b',
'phui-button-css' => '680ace9b',
'phui-form-css' => '680ace9b',
'phui-icon-view-css' => '680ace9b',
'phui-spacing-css' => '680ace9b',
'sprite-apps-large-css' => '680ace9b',
'sprite-gradient-css' => '680ace9b',
'sprite-icons-css' => '680ace9b',
'sprite-menu-css' => '680ace9b',
'syntax-highlighting-css' => '680ace9b',
),
));

View file

@ -1344,6 +1344,7 @@ phutil_register_library_map(array(
'PhabricatorPeopleLogsController' => 'applications/people/controller/PhabricatorPeopleLogsController.php',
'PhabricatorPeopleProfileController' => 'applications/people/controller/PhabricatorPeopleProfileController.php',
'PhabricatorPeopleProfileEditController' => 'applications/people/controller/PhabricatorPeopleProfileEditController.php',
'PhabricatorPeopleProfilePictureController' => 'applications/people/controller/PhabricatorPeopleProfilePictureController.php',
'PhabricatorPeopleQuery' => 'applications/people/query/PhabricatorPeopleQuery.php',
'PhabricatorPeopleSearchEngine' => 'applications/people/query/PhabricatorPeopleSearchEngine.php',
'PhabricatorPeopleTestDataGenerator' => 'applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php',
@ -3303,6 +3304,7 @@ phutil_register_library_map(array(
'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController',
'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController',
'PhabricatorPeopleProfileEditController' => 'PhabricatorPeopleController',
'PhabricatorPeopleProfilePictureController' => 'PhabricatorPeopleController',
'PhabricatorPeopleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPeopleSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPeopleTestDataGenerator' => 'PhabricatorTestDataGenerator',

View file

@ -94,7 +94,6 @@ final class PhabricatorImageTransformer {
}
$cropped = $this->applyScaleWithImagemagick($file, $x, $scaled_y);
if ($cropped != null) {
return $cropped;
}

View file

@ -46,6 +46,8 @@ final class PhabricatorApplicationPeople extends PhabricatorApplication {
'ldap/' => 'PhabricatorPeopleLdapController',
'editprofile/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileEditController',
'picture/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfilePictureController',
),
'/p/(?P<username>[\w._-]+)/(?:(?P<page>\w+)/)?'
=> 'PhabricatorPeopleProfileController',

View file

@ -117,6 +117,14 @@ final class PhabricatorPeopleProfileController
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('image')
->setName(pht('Edit Profile Picture'))
->setHref($this->getApplicationURI('picture/'.$user->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
if ($viewer->getIsAdmin()) {
$actions->addAction(
id(new PhabricatorActionView())

View file

@ -0,0 +1,292 @@
<?php
final class PhabricatorPeopleProfilePictureController
extends PhabricatorPeopleController {
private $id;
public function shouldRequireAdmin() {
return false;
}
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$profile_uri = '/p/'.$user->getUsername().'/';
$supported_formats = PhabricatorFile::getTransformableImageFormats();
$e_file = true;
$errors = array();
if ($request->isFormPost()) {
$phid = $request->getStr('phid');
$is_default = false;
if ($phid == PhabricatorPHIDConstants::PHID_VOID) {
$phid = null;
$is_default = true;
} else if ($phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
} else {
if ($request->getFileExists('picture')) {
$file = PhabricatorFile::newFromPHPUpload(
$_FILES['picture'],
array(
'authorPHID' => $viewer->getPHID(),
));
} else {
$e_file = pht('Required');
$errors[] = pht(
'You must choose a file when uploading a new profile picture.');
}
}
if (!$errors && !$is_default) {
if (!$file->isTransformableImage()) {
$e_file = pht('Not Supported');
$errors[] = pht(
'This server only supports these image formats: %s.',
implode(', ', $supported_formats));
} else {
$xformer = new PhabricatorImageTransformer();
$xformed = $xformer->executeProfileTransform(
$file,
$width = 50,
$min_height = 50,
$max_height = 50);
}
}
if (!$errors) {
$user->setProfileImagePHID($xformed->getPHID());
$user->save();
return id(new AphrontRedirectResponse())->setURI($profile_uri);
}
}
$title = pht('Edit Profile Picture');
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($user->getUsername())
->setHref($profile_uri));
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($title));
$form = id(new AphrontFormLayoutView())
->setUser($viewer);
$default_image = PhabricatorFile::loadBuiltin($viewer, 'profile.png');
$images = array();
$current = $user->getProfileImagePHID();
if ($current) {
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($current))
->execute();
if ($files) {
$file = head($files);
if ($file->isTransformableImage()) {
$images[$current] = array(
'uri' => $file->getBestURI(),
'tip' => pht('Current Picture'),
);
}
}
}
// Try to add external account images for any associated external accounts.
$accounts = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs(array($user->getPHID()))
->needImages(true)
->execute();
foreach ($accounts as $account) {
$file = $account->getProfileImageFile();
if ($account->getProfileImagePHID() != $file->getPHID()) {
// This is a default image, just skip it.
continue;
}
$provider = PhabricatorAuthProvider::getEnabledProviderByKey(
$account->getProviderKey());
if ($provider) {
$tip = pht('Picture From %s', $provider->getProviderName());
} else {
$tip = pht('Picture From External Account');
}
if ($file->isTransformableImage()) {
$images[$file->getPHID()] = array(
'uri' => $file->getBestURI(),
'tip' => $tip,
);
}
}
// Try to add Gravatar images for any email addresses associated with the
// account.
if (PhabricatorEnv::getEnvConfig('security.allow-outbound-http')) {
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s ORDER BY address',
$viewer->getPHID());
$futures = array();
foreach ($emails as $email_object) {
$email = $email_object->getAddress();
$hash = md5(strtolower(trim($email)));
$uri = id(new PhutilURI("https://secure.gravatar.com/avatar/{$hash}"))
->setQueryParams(
array(
'size' => 200,
'default' => '404',
'rating' => 'x',
));
$futures[$email] = new HTTPSFuture($uri);
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
foreach (Futures($futures) as $email => $future) {
try {
list($body) = $future->resolvex();
$file = PhabricatorFile::newFromFileData(
$body,
array(
'name' => 'profile-gravatar',
'ttl' => (60 * 60 * 4),
));
if ($file->isTransformableImage()) {
$images[$file->getPHID()] = array(
'uri' => $file->getBestURI(),
'tip' => pht('Gravatar for %s', $email),
);
}
} catch (Exception $ex) {
// Just continue.
}
}
unset($unguarded);
}
$images[PhabricatorPHIDConstants::PHID_VOID] = array(
'uri' => $default_image->getBestURI(),
'tip' => pht('Default Picture'),
);
require_celerity_resource('people-profile-css');
Javelin::initBehavior('phabricator-tooltips', array());
$buttons = array();
foreach ($images as $phid => $spec) {
$button = javelin_tag(
'button',
array(
'class' => 'grey profile-image-button',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $spec['tip'],
'size' => 300,
),
),
phutil_tag(
'img',
array(
'height' => 50,
'width' => 50,
'src' => $spec['uri'],
)));
$button = array(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'phid',
'value' => $phid,
)),
$button);
$button = phabricator_form(
$viewer,
array(
'class' => 'profile-image-form',
'method' => 'POST',
),
$button);
$buttons[] = $button;
}
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Current Picture'))
->setValue(array_slice($buttons, 0, 1)));
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Use Picture'))
->setValue(array_slice($buttons, 1)));
$upload_head = id(new PhabricatorHeaderView())
->setHeader(pht('Upload New Picture'));
$upload_form = id(new AphrontFormView())
->setUser($user)
->setFlexible(true)
->setEncType('multipart/form-data')
->appendChild(
id(new AphrontFormFileControl())
->setName('picture')
->setLabel(pht('Upload Picture'))
->setError($e_file)
->setCaption(
pht('Supported formats: %s', implode(', ', $supported_formats))))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($profile_uri)
->setValue(pht('Upload Picture')));
if ($errors) {
$errors = id(new AphrontErrorView())->setErrors($errors);
}
return $this->buildApplicationPage(
array(
$crumbs,
$errors,
$form,
$upload_head,
$upload_form,
),
array(
'title' => $title,
'device' => true,
'dust' => true,
));
}
}

View file

@ -18,11 +18,6 @@ final class PhabricatorSettingsPanelProfile
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$profile = $user->loadUserProfile();
$supported_formats = PhabricatorFile::getTransformableImageFormats();
$e_image = null;
$errors = array();
if ($request->isFormPost()) {
$sex = $request->getStr('sex');
@ -36,64 +31,8 @@ final class PhabricatorSettingsPanelProfile
// Checked in runtime.
$user->setTranslation($request->getStr('translation'));
$default_image = $request->getExists('default_image');
$gravatar_email = $request->getStr('gravatar');
if ($default_image) {
$profile->setProfileImagePHID(null);
$user->setProfileImagePHID(null);
} else if (!empty($gravatar_email) || $request->getFileExists('image')) {
$file = null;
if (!empty($gravatar_email)) {
// These steps recommended by:
// https://en.gravatar.com/site/implement/hash/
$trimmed = trim($gravatar_email);
$lower_cased = strtolower($trimmed);
$hash = md5($lower_cased);
$url = 'http://www.gravatar.com/avatar/'.($hash).'?s=200';
$file = PhabricatorFile::newFromFileDownload(
$url,
array(
'name' => 'gravatar',
'authorPHID' => $user->getPHID(),
));
} else if ($request->getFileExists('image')) {
$file = PhabricatorFile::newFromPHPUpload(
$_FILES['image'],
array(
'authorPHID' => $user->getPHID(),
));
}
$okay = $file->isTransformableImage();
if ($okay) {
$xformer = new PhabricatorImageTransformer();
// Generate the large picture for the profile page.
$large_xformed = $xformer->executeProfileTransform(
$file,
$width = 280,
$min_height = 140,
$max_height = 420);
$profile->setProfileImagePHID($large_xformed->getPHID());
// Generate the small picture for comments, etc.
$small_xformed = $xformer->executeProfileTransform(
$file,
$width = 50,
$min_height = 50,
$max_height = 50);
$user->setProfileImagePHID($small_xformed->getPHID());
} else {
$e_image = pht('Not Supported');
$errors[] =
pht('This server only supports these image formats:').
' ' .implode(', ', $supported_formats);
}
}
if (!$errors) {
$user->save();
$profile->save();
$response = id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
return $response;
@ -116,7 +55,6 @@ final class PhabricatorSettingsPanelProfile
}
}
$img_src = $user->loadProfileImageURI();
$profile_uri = PhabricatorEnv::getURI('/p/'.$user->getUsername().'/');
$sexes = array(
@ -144,7 +82,6 @@ final class PhabricatorSettingsPanelProfile
$form = new AphrontFormView();
$form
->setUser($request->getUser())
->setEncType('multipart/form-data')
->appendChild(
id(new AphrontFormSelectControl())
->setOptions($sexes)
@ -156,37 +93,11 @@ final class PhabricatorSettingsPanelProfile
->setOptions($translations)
->setLabel(pht('Translation'))
->setName('translation')
->setValue($user->getTranslation()))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Profile Image'))
->setValue(
phutil_tag(
'img',
array(
'src' => $img_src,
))))
->appendChild(
id(new AphrontFormImageControl())
->setLabel(pht('Change Image'))
->setName('image')
->setError($e_image)
->setCaption(
pht('Supported formats: %s', implode(', ', $supported_formats))));
if (PhabricatorEnv::getEnvConfig('security.allow-outbound-http')) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Import Gravatar'))
->setName('gravatar')
->setError($e_image)
->setCaption(pht('Enter gravatar email address')));
}
->setValue($user->getTranslation()));
$form->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save'))
->addCancelButton('/p/'.$user->getUsername().'/'));
->setValue(pht('Save')));
$header = new PhabricatorHeaderView();
$header->setHeader(pht('Edit Profile Details'));

View file

@ -0,0 +1,13 @@
/**
* @provides people-profile-css
*/
form.profile-image-form {
display: inline-block;
margin: 0 8px 8px 0;
}
button.profile-image-button {
padding: 4px;
margin: 0;
}