1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-13 16:21:07 +01:00
phorge-phorge/src/view/page/PhabricatorStandardPageView.php

555 lines
14 KiB
PHP
Raw Normal View History

<?php
/**
* This is a standard Phabricator page with menus, Javelin, DarkConsole, and
* basic styles.
*
*/
final class PhabricatorStandardPageView extends PhabricatorBarePageView {
private $baseURI;
private $applicationName;
private $glyph;
private $menuContent;
private $showChrome = true;
private $disableConsole;
private $pageObjects = array();
private $applicationMenu;
private $showFooter = true;
private $showDurableColumn = true;
public function setShowFooter($show_footer) {
$this->showFooter = $show_footer;
return $this;
}
public function getShowFooter() {
return $this->showFooter;
}
public function setApplicationMenu(PHUIListView $application_menu) {
$this->applicationMenu = $application_menu;
return $this;
}
public function getApplicationMenu() {
return $this->applicationMenu;
}
public function setApplicationName($application_name) {
$this->applicationName = $application_name;
return $this;
}
public function setDisableConsole($disable) {
$this->disableConsole = $disable;
return $this;
}
public function getApplicationName() {
return $this->applicationName;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setShowChrome($show_chrome) {
$this->showChrome = $show_chrome;
return $this;
}
public function getShowChrome() {
return $this->showChrome;
}
public function appendPageObjects(array $objs) {
foreach ($objs as $obj) {
$this->pageObjects[] = $obj;
}
}
public function setShowDurableColumn($show) {
$this->showDurableColumn = $show;
return $this;
}
public function getShowDurableColumn() {
return $this->showDurableColumn;
}
public function getTitle() {
$use_glyph = true;
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
if ($user && $user->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_TITLES) !== 'glyph') {
$use_glyph = false;
}
}
$title = parent::getTitle();
$prefix = null;
if ($use_glyph) {
$prefix = $this->getGlyph();
} else {
$application_name = $this->getApplicationName();
if (strlen($application_name)) {
$prefix = '['.$application_name.']';
}
}
if (strlen($prefix)) {
$title = $prefix.' '.$title;
}
return $title;
}
2011-01-25 20:31:40 +01:00
protected function willRenderPage() {
parent::willRenderPage();
2011-02-02 22:48:52 +01:00
if (!$this->getRequest()) {
throw new Exception(
pht(
'You must set the Request to render a PhabricatorStandardPageView.'));
2011-02-02 22:48:52 +01:00
}
$console = $this->getConsole();
2011-02-02 22:48:52 +01:00
require_celerity_resource('phabricator-core-css');
require_celerity_resource('phabricator-zindex-css');
require_celerity_resource('phui-button-css');
require_celerity_resource('phui-spacing-css');
require_celerity_resource('phui-form-css');
require_celerity_resource('sprite-gradient-css');
2011-01-25 20:31:40 +01:00
require_celerity_resource('phabricator-standard-page-view');
Javelin::initBehavior('workflow', array());
$request = $this->getRequest();
$user = null;
if ($request) {
$user = $request->getUser();
}
if ($user) {
$default_img_uri =
celerity_get_resource_uri(
'rsrc/image/icon/fatcow/document_black.png');
$download_form = phabricator_form(
$user,
array(
'action' => '#',
'method' => 'POST',
'class' => 'lightbox-download-form',
'sigil' => 'download',
),
phutil_tag(
'button',
array(),
pht('Download')));
Javelin::initBehavior(
'lightbox-attachments',
array(
'defaultImageUri' => $default_img_uri,
'downloadForm' => $download_form,
));
}
Javelin::initBehavior('aphront-form-disable-on-submit');
Javelin::initBehavior('toggle-class', array());
Javelin::initBehavior('konami', array());
Javelin::initBehavior('history-install');
Add support for device swipe events Summary: Ref T2700. Allow JS to listen for swipes on devices. There are a bunch of tricky cases here and I probably didn't get them all totally right, but this interaction broadly looks like this: - We implement gesture recognition for the mouse in device modes (narrow browser), and for touch events from an actual device. - The sigil `touchable` indicates that a node wants to react to touch events. - When the user touches a `touchable` node, we start listening for moves. They might be tapping/clicking (in which case we don't care), but they might also be gesturing. - Once the user moves their finger/pointer far enough away from the tap origin, we recognize it as a gesture. I hardcoded this at 20px; I wasn't able to find any "official" Apple value, but 20px seems like a common default. - At this point, we look at where their finger has moved. - If they moved it mostly up/down, we interpret the gesture as "scroll" and just stop listening. The device does its own thing. - However, if they moved it mostly left/right, we interpret it as a "swipe". We start killing the moves so the device doesn't scroll. - Once we've recognized that a gesture is underway, we send a "gesture.swipe.start" event and then "gesture.swipe.move" events for every move. - When the user ends the gesture, we send "gesture.swipe.end". - If the user cancels the gesture (currently, only by tapping with a second finger), we send "gesture.swipe.cancel". - Gesture events have raw position data and some convenience fields. Test Plan: Wrote UI example and used it from the Desktop, iPhone simulator, and a real iphone. - The code always seems to get "scroll" vs "swipe" correct (i.e., consistent with my intentions). - The threshold feels pretty good to me. - Tapping with a second finger cancels the action. Reviewers: chad, btrahan Reviewed By: chad CC: aran Maniphest Tasks: T2700 Differential Revision: https://secure.phabricator.com/D5308
2013-03-09 22:53:15 +01:00
Javelin::initBehavior('phabricator-gesture');
$current_token = null;
if ($user) {
$current_token = $user->getCSRFToken();
}
Javelin::initBehavior(
'refresh-csrf',
array(
'tokenName' => AphrontRequest::getCSRFTokenName(),
'header' => AphrontRequest::getCSRFHeaderName(),
'current' => $current_token,
));
Javelin::initBehavior('device');
if ($user->hasSession()) {
$hisec = ($user->getSession()->getHighSecurityUntil() - time());
if ($hisec > 0) {
$remaining_time = phutil_format_relative_time($hisec);
Javelin::initBehavior(
'high-security-warning',
array(
'uri' => '/auth/session/downgrade/',
'message' => pht(
'Your session is in high security mode. When you '.
'finish using it, click here to leave.',
$remaining_time),
));
}
}
2011-02-02 22:48:52 +01:00
if ($console) {
require_celerity_resource('aphront-dark-console-css');
$headers = array();
if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) {
$headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page';
}
if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) {
$headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true;
}
2011-02-02 22:48:52 +01:00
Javelin::initBehavior(
'dark-console',
array(
// NOTE: We use a generic label here to prevent input reflection
// and mitigate compression attacks like BREACH. See discussion in
// T3684.
'uri' => pht('Main Request'),
'selected' => $user ? $user->getConsoleTab() : null,
'visible' => $user ? (int)$user->getConsoleVisible() : true,
'headers' => $headers,
2011-02-02 22:48:52 +01:00
));
// Change this to initBehavior when there is some behavior to initialize
require_celerity_resource('javelin-behavior-error-log');
2011-02-02 22:48:52 +01:00
}
if ($user) {
$viewer = $user;
} else {
$viewer = new PhabricatorUser();
}
$menu = id(new PhabricatorMainMenuView())
->setUser($viewer);
if ($this->getController()) {
$menu->setController($this->getController());
}
if ($this->getApplicationMenu()) {
$menu->setApplicationMenu($this->getApplicationMenu());
}
$this->menuContent = $menu->render();
}
protected function getHead() {
$monospaced = PhabricatorEnv::getEnvConfig('style.monospace');
$monospaced_win = PhabricatorEnv::getEnvConfig('style.monospace.windows');
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
if ($user) {
$pref = $user->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_MONOSPACED);
$monospaced = nonempty($pref, $monospaced);
$monospaced_win = nonempty($pref, $monospaced_win);
}
}
$response = CelerityAPI::getStaticResourceResponse();
2013-02-13 23:50:15 +01:00
return hsprintf(
'%s<style type="text/css">'.
'.PhabricatorMonospaced, '.
'.phabricator-remarkup .remarkup-code-block '.
'.remarkup-code { font: %s; } '.
'.platform-windows .PhabricatorMonospaced, '.
'.platform-windows .phabricator-remarkup '.
'.remarkup-code-block .remarkup-code { font: %s; }'.
'</style>%s',
parent::getHead(),
2013-02-13 23:50:15 +01:00
phutil_safe_html($monospaced),
phutil_safe_html($monospaced_win),
$response->renderSingleResource('javelin-magical-init', 'phabricator'));
}
public function setGlyph($glyph) {
$this->glyph = $glyph;
return $this;
}
public function getGlyph() {
return $this->glyph;
}
2011-02-02 22:48:52 +01:00
protected function willSendResponse($response) {
$request = $this->getRequest();
$response = parent::willSendResponse($response);
$console = $request->getApplicationConfiguration()->getConsole();
2011-02-02 22:48:52 +01:00
if ($console) {
2013-02-13 23:50:15 +01:00
$response = PhutilSafeHTML::applyFunction(
'str_replace',
hsprintf('<darkconsole />'),
$console->render($request),
2011-02-02 22:48:52 +01:00
$response);
}
2011-02-02 22:48:52 +01:00
return $response;
}
protected function getBody() {
$user = null;
$request = $this->getRequest();
2011-01-26 02:17:19 +01:00
if ($request) {
$user = $request->getUser();
2011-01-26 22:21:12 +01:00
}
$header_chrome = null;
if ($this->getShowChrome()) {
$header_chrome = $this->menuContent;
}
$developer_warning = null;
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') &&
DarkConsoleErrorLogPluginAPI::getErrors()) {
$developer_warning = phutil_tag_div(
'aphront-developer-error-callout',
pht(
'This page raised PHP errors. Find them in DarkConsole '.
'or the error log.'));
}
// Render the "you have unresolved setup issues..." warning.
$setup_warning = null;
if ($user && $user->getIsAdmin()) {
$open = PhabricatorSetupCheck::getOpenSetupIssueKeys();
if ($open) {
$setup_warning = phutil_tag_div(
'setup-warning-callout',
phutil_tag(
'a',
array(
'href' => '/config/issue/',
'title' => implode(', ', $open),
),
pht('You have %d unresolved setup issue(s)...', count($open))));
}
}
Javelin::initBehavior(
'scrollbar',
array(
'nodeID' => 'phabricator-standard-page',
'isMainContent' => true,
));
$main_page = phutil_tag(
'div',
array(
'id' => 'phabricator-standard-page',
'class' => 'phabricator-standard-page',
),
array(
$developer_warning,
$setup_warning,
$header_chrome,
Quicksand, an ignoble successor to Quickling Summary: Ref T2086. Ref T7014. With the persistent column, there is significant value in retaining chrome state through navigation events, because the user may have a lot of state in the chat window (scroll position, text selection, room juggling, partially entered text, etc). We can do this by capturing navigation events and faking them with Javascript. (This can also improve performance, albeit slightly, and I believe there are better approaches to tackle performance any problems which exist with the chrome in many cases). At Facebook, this system was "Photostream" in photos and then "Quickling" in general, and the technical cost of the system was //staggering//. I am loathe to pursue it again. However: - Browsers are less junky now, and we target a smaller set of browsers. A large part of the technical cost of Quickling was the high complexity of emulating nagivation events in IE, where we needed to navigate a hidden iframe to make history entries. All desktop browsers which we might want to use this system on support the History API (although this prototype does not yet implement it). - Javelin and Phabricator's architecture are much cleaner than Facebook's was. A large part of the technical cost of Quickling was inconsistency, inlined `onclick` handlers, and general lack of coordination and abstraction. We will have //some// of this, but "correctly written" behaviors are mostly immune to it by design, and many of Javelin's architectural decisions were influenced by desire to avoid issues we encountered building this stuff for Facebook. - Some of the primitives which Quickling required (like loading resources over Ajax) have existed in a stable state in our codebase for a year or more, and adoption of these primitives was trivial and uneventful (vs a huge production at Facebook). - My hubris is bolstered by recent success with WebSockets and JX.Scrollbar, both of which I would have assessed as infeasibly complex to develop in this project a few years ago. To these points, the developer cost to prototype Photostream was several weeks; the developer cost to prototype this was a bit less than an hour. It is plausible to me that implementing and maintaining this system really will be hundreds of times less complex than it was at Facebook. Test Plan: My plan for this and D11497 is: - Get them in master. - Some secret key / relatively-hidden preference activates the column. - Quicksand activates //only// when the column is open. - We can use column + quicksand for a long period of time (i.e., over the course of Conpherence v2 development) and hammer out the long tail of issues. - When it derps up, you just hide the column and you're good to go. Reviewers: btrahan, chad Reviewed By: chad Subscribers: epriestley Maniphest Tasks: T2086, T7014 Differential Revision: https://secure.phabricator.com/D11507
2015-01-27 23:52:09 +01:00
phutil_tag(
'div',
array(
'id' => 'phabricator-standard-page-body',
'class' => 'phabricator-standard-page-body',
),
$this->renderPageBodyContent()),
));
$durable_column = null;
if ($this->getShowDurableColumn()) {
$durable_column = new PHUIDurableColumn();
}
return phutil_tag(
'div',
array(
'class' => 'main-page-frame',
),
array(
$main_page,
$durable_column,
));
}
Quicksand, an ignoble successor to Quickling Summary: Ref T2086. Ref T7014. With the persistent column, there is significant value in retaining chrome state through navigation events, because the user may have a lot of state in the chat window (scroll position, text selection, room juggling, partially entered text, etc). We can do this by capturing navigation events and faking them with Javascript. (This can also improve performance, albeit slightly, and I believe there are better approaches to tackle performance any problems which exist with the chrome in many cases). At Facebook, this system was "Photostream" in photos and then "Quickling" in general, and the technical cost of the system was //staggering//. I am loathe to pursue it again. However: - Browsers are less junky now, and we target a smaller set of browsers. A large part of the technical cost of Quickling was the high complexity of emulating nagivation events in IE, where we needed to navigate a hidden iframe to make history entries. All desktop browsers which we might want to use this system on support the History API (although this prototype does not yet implement it). - Javelin and Phabricator's architecture are much cleaner than Facebook's was. A large part of the technical cost of Quickling was inconsistency, inlined `onclick` handlers, and general lack of coordination and abstraction. We will have //some// of this, but "correctly written" behaviors are mostly immune to it by design, and many of Javelin's architectural decisions were influenced by desire to avoid issues we encountered building this stuff for Facebook. - Some of the primitives which Quickling required (like loading resources over Ajax) have existed in a stable state in our codebase for a year or more, and adoption of these primitives was trivial and uneventful (vs a huge production at Facebook). - My hubris is bolstered by recent success with WebSockets and JX.Scrollbar, both of which I would have assessed as infeasibly complex to develop in this project a few years ago. To these points, the developer cost to prototype Photostream was several weeks; the developer cost to prototype this was a bit less than an hour. It is plausible to me that implementing and maintaining this system really will be hundreds of times less complex than it was at Facebook. Test Plan: My plan for this and D11497 is: - Get them in master. - Some secret key / relatively-hidden preference activates the column. - Quicksand activates //only// when the column is open. - We can use column + quicksand for a long period of time (i.e., over the course of Conpherence v2 development) and hammer out the long tail of issues. - When it derps up, you just hide the column and you're good to go. Reviewers: btrahan, chad Reviewed By: chad Subscribers: epriestley Maniphest Tasks: T2086, T7014 Differential Revision: https://secure.phabricator.com/D11507
2015-01-27 23:52:09 +01:00
private function renderPageBodyContent() {
$console = $this->getConsole();
return array(
($console ? hsprintf('<darkconsole />') : null),
parent::getBody(),
$this->renderFooter(),
);
}
protected function getTail() {
$request = $this->getRequest();
$user = $request->getUser();
$tail = array(
parent::getTail(),
);
$response = CelerityAPI::getStaticResourceResponse();
if (PhabricatorEnv::getEnvConfig('notification.enabled')) {
if ($user && $user->isLoggedIn()) {
$client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri');
$client_uri = new PhutilURI($client_uri);
if ($client_uri->getDomain() == 'localhost') {
$this_host = $this->getRequest()->getHost();
$this_host = new PhutilURI('http://'.$this_host.'/');
$client_uri->setDomain($this_host->getDomain());
}
Modify the Aphlict server to transmit messages instead of broadcasting them. Summary: Ref T4324. Ref T5284. This adds server-side support for keeping track of a set of PHIDs that the Aphlict clients have subscribed to. Instead of broadcasting a notification to all clients (after which the clients can poll `/notification/individual` in order to determine whether or not they are interested in the notification), transmit notifications only to clients that have subscribed to a PHID that is relevant to the notification. Test Plan: I opened up two clients on the same host (incognito tabs in Chrome). Here is the output from the server: ``` > sudo ./bin/aphlict debug Starting Aphlict server in foreground... Launching server: $ 'nodejs' '/usr/src/phabricator/src/applications/aphlict/management/../../../../support/aphlict/server/aphlict_server.js' --port='22280' --admin='22281' --host='localhost' --user='aphlict' [Wed Jun 11 2014 19:10:27 GMT+0000 (UTC)] Started Server (PID 4546) [Wed Jun 11 2014 19:10:36 GMT+0000 (UTC)] <FlashPolicy> Policy Request From ::ffff:192.168.1.1 [Wed Jun 11 2014 19:10:37 GMT+0000 (UTC)] <Listener/1> Connected from ::ffff:192.168.1.1 [Wed Jun 11 2014 19:10:37 GMT+0000 (UTC)] <Listener/1> Received data: {"command":"subscribe","data":["PHID-USER-cb5af6p4oepy5tlgqypi"]} [Wed Jun 11 2014 19:10:37 GMT+0000 (UTC)] <Listener/1> Subscribed to: ["PHID-USER-cb5af6p4oepy5tlgqypi"] [Wed Jun 11 2014 19:10:39 GMT+0000 (UTC)] <Listener/1> Received data: {"command":"subscribe","data":["PHID-USER-kfohe3ca5oe6ygykmioq"]} [Wed Jun 11 2014 19:10:39 GMT+0000 (UTC)] <Listener/1> Subscribed to: ["PHID-USER-kfohe3ca5oe6ygykmioq"] [Wed Jun 11 2014 19:10:42 GMT+0000 (UTC)] notification: {"key":"6023751084283587681","type":"notification","subscribers":["PHID-USER-cb5af6p4oepy5tlgqypi"]} [Wed Jun 11 2014 19:10:42 GMT+0000 (UTC)] <Listener/1> Wrote Message ``` I verified (using the "Network" tab in Chrome) that an AJAX request to `/notification/individual/` was only made in the tab belonging to the user that triggered the test notification. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: epriestley, Korvin Maniphest Tasks: T5284, T4324 Differential Revision: https://secure.phabricator.com/D9458
2014-06-11 21:17:18 +02:00
$subscriptions = $this->pageObjects;
if ($user) {
$subscriptions[] = $user->getPHID();
}
if ($request->isHTTPS()) {
$client_uri->setProtocol('wss');
} else {
$client_uri->setProtocol('ws');
}
Javelin::initBehavior(
'aphlict-listen',
array(
'websocketURI' => (string)$client_uri,
Modify the Aphlict server to transmit messages instead of broadcasting them. Summary: Ref T4324. Ref T5284. This adds server-side support for keeping track of a set of PHIDs that the Aphlict clients have subscribed to. Instead of broadcasting a notification to all clients (after which the clients can poll `/notification/individual` in order to determine whether or not they are interested in the notification), transmit notifications only to clients that have subscribed to a PHID that is relevant to the notification. Test Plan: I opened up two clients on the same host (incognito tabs in Chrome). Here is the output from the server: ``` > sudo ./bin/aphlict debug Starting Aphlict server in foreground... Launching server: $ 'nodejs' '/usr/src/phabricator/src/applications/aphlict/management/../../../../support/aphlict/server/aphlict_server.js' --port='22280' --admin='22281' --host='localhost' --user='aphlict' [Wed Jun 11 2014 19:10:27 GMT+0000 (UTC)] Started Server (PID 4546) [Wed Jun 11 2014 19:10:36 GMT+0000 (UTC)] <FlashPolicy> Policy Request From ::ffff:192.168.1.1 [Wed Jun 11 2014 19:10:37 GMT+0000 (UTC)] <Listener/1> Connected from ::ffff:192.168.1.1 [Wed Jun 11 2014 19:10:37 GMT+0000 (UTC)] <Listener/1> Received data: {"command":"subscribe","data":["PHID-USER-cb5af6p4oepy5tlgqypi"]} [Wed Jun 11 2014 19:10:37 GMT+0000 (UTC)] <Listener/1> Subscribed to: ["PHID-USER-cb5af6p4oepy5tlgqypi"] [Wed Jun 11 2014 19:10:39 GMT+0000 (UTC)] <Listener/1> Received data: {"command":"subscribe","data":["PHID-USER-kfohe3ca5oe6ygykmioq"]} [Wed Jun 11 2014 19:10:39 GMT+0000 (UTC)] <Listener/1> Subscribed to: ["PHID-USER-kfohe3ca5oe6ygykmioq"] [Wed Jun 11 2014 19:10:42 GMT+0000 (UTC)] notification: {"key":"6023751084283587681","type":"notification","subscribers":["PHID-USER-cb5af6p4oepy5tlgqypi"]} [Wed Jun 11 2014 19:10:42 GMT+0000 (UTC)] <Listener/1> Wrote Message ``` I verified (using the "Network" tab in Chrome) that an AJAX request to `/notification/individual/` was only made in the tab belonging to the user that triggered the test notification. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: epriestley, Korvin Maniphest Tasks: T5284, T4324 Differential Revision: https://secure.phabricator.com/D9458
2014-06-11 21:17:18 +02:00
'pageObjects' => array_fill_keys($this->pageObjects, true),
'subscriptions' => $subscriptions,
));
}
}
$tail[] = $response->renderHTMLFooter();
return $tail;
}
protected function getBodyClasses() {
$classes = array();
if (!$this->getShowChrome()) {
$classes[] = 'phabricator-chromeless-page';
}
$agent = AphrontRequest::getHTTPHeader('User-Agent');
// Try to guess the device resolution based on UA strings to avoid a flash
// of incorrectly-styled content.
$device_guess = 'device-desktop';
if (preg_match('@iPhone|iPod|(Android.*Chrome/[.0-9]* Mobile)@', $agent)) {
$device_guess = 'device-phone device';
} else if (preg_match('@iPad|(Android.*Chrome/)@', $agent)) {
$device_guess = 'device-tablet device';
}
$classes[] = $device_guess;
if (preg_match('@Windows@', $agent)) {
$classes[] = 'platform-windows';
} else if (preg_match('@Macintosh@', $agent)) {
$classes[] = 'platform-mac';
} else if (preg_match('@X11@', $agent)) {
$classes[] = 'platform-linux';
}
if ($this->getRequest()->getStr('__print__')) {
$classes[] = 'printable';
}
if ($this->getRequest()->getStr('__aural__')) {
$classes[] = 'audible';
}
return implode(' ', $classes);
}
private function getConsole() {
if ($this->disableConsole) {
return null;
}
return $this->getRequest()->getApplicationConfiguration()->getConsole();
}
private function renderFooter() {
if (!$this->getShowChrome()) {
return null;
}
if (!$this->getShowFooter()) {
return null;
}
$items = PhabricatorEnv::getEnvConfig('ui.footer-items');
if (!$items) {
return null;
}
$foot = array();
foreach ($items as $item) {
$name = idx($item, 'name', pht('Unnamed Footer Item'));
$href = idx($item, 'href');
if (!PhabricatorEnv::isValidWebResource($href)) {
$href = null;
}
if ($href !== null) {
$tag = 'a';
} else {
$tag = 'span';
}
$foot[] = phutil_tag(
$tag,
array(
'href' => $href,
),
$name);
}
$foot = phutil_implode_html(" \xC2\xB7 ", $foot);
return phutil_tag(
'div',
array(
'class' => 'phabricator-standard-page-footer grouped',
),
$foot);
}
Quicksand, an ignoble successor to Quickling Summary: Ref T2086. Ref T7014. With the persistent column, there is significant value in retaining chrome state through navigation events, because the user may have a lot of state in the chat window (scroll position, text selection, room juggling, partially entered text, etc). We can do this by capturing navigation events and faking them with Javascript. (This can also improve performance, albeit slightly, and I believe there are better approaches to tackle performance any problems which exist with the chrome in many cases). At Facebook, this system was "Photostream" in photos and then "Quickling" in general, and the technical cost of the system was //staggering//. I am loathe to pursue it again. However: - Browsers are less junky now, and we target a smaller set of browsers. A large part of the technical cost of Quickling was the high complexity of emulating nagivation events in IE, where we needed to navigate a hidden iframe to make history entries. All desktop browsers which we might want to use this system on support the History API (although this prototype does not yet implement it). - Javelin and Phabricator's architecture are much cleaner than Facebook's was. A large part of the technical cost of Quickling was inconsistency, inlined `onclick` handlers, and general lack of coordination and abstraction. We will have //some// of this, but "correctly written" behaviors are mostly immune to it by design, and many of Javelin's architectural decisions were influenced by desire to avoid issues we encountered building this stuff for Facebook. - Some of the primitives which Quickling required (like loading resources over Ajax) have existed in a stable state in our codebase for a year or more, and adoption of these primitives was trivial and uneventful (vs a huge production at Facebook). - My hubris is bolstered by recent success with WebSockets and JX.Scrollbar, both of which I would have assessed as infeasibly complex to develop in this project a few years ago. To these points, the developer cost to prototype Photostream was several weeks; the developer cost to prototype this was a bit less than an hour. It is plausible to me that implementing and maintaining this system really will be hundreds of times less complex than it was at Facebook. Test Plan: My plan for this and D11497 is: - Get them in master. - Some secret key / relatively-hidden preference activates the column. - Quicksand activates //only// when the column is open. - We can use column + quicksand for a long period of time (i.e., over the course of Conpherence v2 development) and hammer out the long tail of issues. - When it derps up, you just hide the column and you're good to go. Reviewers: btrahan, chad Reviewed By: chad Subscribers: epriestley Maniphest Tasks: T2086, T7014 Differential Revision: https://secure.phabricator.com/D11507
2015-01-27 23:52:09 +01:00
public function renderForQuicksand() {
// TODO: We could run a lighter version of this and skip some work. In
// particular, we end up including many redundant resources.
$this->willRenderPage();
$response = $this->renderPageBodyContent();
$response = $this->willSendResponse($response);
return array(
'content' => hsprintf('%s', $response),
);
}
}