From b873f3f991b02ef5c1054c0dd04b8ca917a894a4 Mon Sep 17 00:00:00 2001 From: Bob Trahan Date: Thu, 24 Jan 2013 17:23:05 -0800 Subject: [PATCH] Conpherence V1 Summary: it's ugly. but it works. basically. See T2399 for a roughly prioritized list of what still needs to happen. Test Plan: - created a conpherence with myself from my profile - created a conpherence with myself from "new conpherence" - created a conphernece with another from "new conpherence" - created a conpherence with several others - created a conpherence with files in the initial post - verified files via comment text ("{F232} is awesome!") and via traditional attach - edited a conpherence image - verified it showed up in the header and in the conpherence menu on the left - edited a conpherence title - verified it showed up in the header and in the conpherence menu on the right - verified each widget showed up when clicked and displayed the proper data - calendar being an exception since it sucks so hard right now. Reviewers: epriestley, chad Reviewed By: epriestley CC: aran, epriestley, chad, codeblock, Korvin Maniphest Tasks: T2301 Differential Revision: https://secure.phabricator.com/D4620 --- .../sql/patches/20130111.conpherence.sql | 83 +++++ src/__celerity_resource_map__.php | 166 +++++++--- src/__phutil_library_map__.php | 43 +++ .../PhabricatorApplicationConpherence.php | 48 +++ .../constants/ConpherenceConstants.php | 8 + .../ConpherenceParticipationStatus.php | 8 + .../constants/ConpherenceTransactionType.php | 13 + .../controller/ConpherenceController.php | 225 ++++++++++++++ .../controller/ConpherenceListController.php | 119 +++++++ .../controller/ConpherenceNewController.php | 201 ++++++++++++ .../ConpherenceUpdateController.php | 219 +++++++++++++ .../controller/ConpherenceViewController.php | 291 ++++++++++++++++++ .../conpherence/editor/ConpherenceEditor.php | 175 +++++++++++ .../query/ConpherenceParticipantQuery.php | 98 ++++++ .../query/ConpherenceThreadQuery.php | 182 +++++++++++ .../query/ConpherenceTransactionQuery.php | 13 + .../conpherence/storage/ConpherenceDAO.php | 12 + .../storage/ConpherenceParticipant.php | 27 ++ .../conpherence/storage/ConpherenceThread.php | 237 ++++++++++++++ .../storage/ConpherenceTransaction.php | 124 ++++++++ .../storage/ConpherenceTransactionComment.php | 15 + .../view/ConpherenceMenuItemView.php | 144 +++++++++ .../view/ConpherenceTransactionView.php | 98 ++++++ .../PhabricatorPeopleProfileController.php | 10 +- .../phid/PhabricatorPHIDConstants.php | 1 + .../handle/PhabricatorObjectHandleData.php | 3 + .../PhabricatorApplicationTransaction.php | 9 + .../edges/constants/PhabricatorEdgeConfig.php | 7 + .../PhabricatorBaseEnglishTranslation.php | 33 ++ .../markup/PhabricatorMarkupEngine.php | 16 + .../rule/PhabricatorRemarkupRuleEmbedFile.php | 4 + .../patch/PhabricatorBuiltinPatchList.php | 8 + src/view/AphrontDialogView.php | 1 + .../control/AphrontFormTextAreaControl.php | 22 +- src/view/layout/AphrontSideNavFilterView.php | 16 +- webroot/rsrc/css/aphront/form-view.css | 1 + .../application/conpherence/header-pane.css | 41 +++ .../rsrc/css/application/conpherence/menu.css | 121 ++++++++ .../application/conpherence/message-pane.css | 95 ++++++ .../css/application/conpherence/update.css | 7 + .../application/conpherence/widget-pane.css | 19 ++ .../application/conpherence/behavior-init.js | 20 ++ .../application/conpherence/behavior-menu.js | 96 ++++++ .../conpherence/behavior-widget-pane.js | 26 ++ 44 files changed, 3054 insertions(+), 51 deletions(-) create mode 100644 resources/sql/patches/20130111.conpherence.sql create mode 100644 src/applications/conpherence/application/PhabricatorApplicationConpherence.php create mode 100644 src/applications/conpherence/constants/ConpherenceConstants.php create mode 100644 src/applications/conpherence/constants/ConpherenceParticipationStatus.php create mode 100644 src/applications/conpherence/constants/ConpherenceTransactionType.php create mode 100644 src/applications/conpherence/controller/ConpherenceController.php create mode 100644 src/applications/conpherence/controller/ConpherenceListController.php create mode 100644 src/applications/conpherence/controller/ConpherenceNewController.php create mode 100644 src/applications/conpherence/controller/ConpherenceUpdateController.php create mode 100644 src/applications/conpherence/controller/ConpherenceViewController.php create mode 100644 src/applications/conpherence/editor/ConpherenceEditor.php create mode 100644 src/applications/conpherence/query/ConpherenceParticipantQuery.php create mode 100644 src/applications/conpherence/query/ConpherenceThreadQuery.php create mode 100644 src/applications/conpherence/query/ConpherenceTransactionQuery.php create mode 100644 src/applications/conpherence/storage/ConpherenceDAO.php create mode 100644 src/applications/conpherence/storage/ConpherenceParticipant.php create mode 100644 src/applications/conpherence/storage/ConpherenceThread.php create mode 100644 src/applications/conpherence/storage/ConpherenceTransaction.php create mode 100644 src/applications/conpherence/storage/ConpherenceTransactionComment.php create mode 100644 src/applications/conpherence/view/ConpherenceMenuItemView.php create mode 100644 src/applications/conpherence/view/ConpherenceTransactionView.php create mode 100644 webroot/rsrc/css/application/conpherence/header-pane.css create mode 100644 webroot/rsrc/css/application/conpherence/menu.css create mode 100644 webroot/rsrc/css/application/conpherence/message-pane.css create mode 100644 webroot/rsrc/css/application/conpherence/update.css create mode 100644 webroot/rsrc/css/application/conpherence/widget-pane.css create mode 100644 webroot/rsrc/js/application/conpherence/behavior-init.js create mode 100644 webroot/rsrc/js/application/conpherence/behavior-menu.js create mode 100644 webroot/rsrc/js/application/conpherence/behavior-widget-pane.js diff --git a/resources/sql/patches/20130111.conpherence.sql b/resources/sql/patches/20130111.conpherence.sql new file mode 100644 index 0000000000..bea6b1820e --- /dev/null +++ b/resources/sql/patches/20130111.conpherence.sql @@ -0,0 +1,83 @@ + +CREATE TABLE {$NAMESPACE}_conpherence.conpherence_thread ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + title VARCHAR(255), + imagePHID VARCHAR(64) COLLATE utf8_bin, + mailKey VARCHAR(20) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY(phid) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_conpherence.conpherence_participant ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + participantPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + conpherencePHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + participationStatus INT UNSIGNED NOT NULL DEFAULT 0, + dateTouched INT UNSIGNED NOT NULL, + behindTransactionPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY(conpherencePHID, participantPHID), + KEY(participantPHID, participationStatus, dateTouched) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_conpherence.edge ( + src varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + type varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + dst varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + dateCreated int(10) unsigned NOT NULL, + seq int(10) unsigned NOT NULL, + dataID int(10) unsigned DEFAULT NULL, + PRIMARY KEY (src, type, dst), + KEY src (src, type, dateCreated, seq) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE {$NAMESPACE}_conpherence.edgedata ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + data longtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE {$NAMESPACE}_conpherence.conpherence_transaction ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + commentPHID VARCHAR(64) COLLATE utf8_bin, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL COLLATE utf8_bin, + oldValue LONGTEXT NOT NULL COLLATE utf8_bin, + newValue LONGTEXT NOT NULL COLLATE utf8_bin, + contentSource LONGTEXT NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + KEY `key_object` (objectPHID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_conpherence.conpherence_transaction_comment ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + transactionPHID VARCHAR(64) COLLATE utf8_bin, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + commentVersion INT UNSIGNED NOT NULL, + content LONGTEXT NOT NULL COLLATE utf8_bin, + contentSource LONGTEXT NOT NULL COLLATE utf8_bin, + isDeleted BOOL NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + + conpherencePHID VARCHAR(64) COLLATE utf8_bin, + + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_version` (transactionPHID, commentVersion), + UNIQUE KEY `key_draft` (authorPHID, conpherencePHID, transactionPHID) + +) ENGINE=InnoDB, COLLATE utf8_general_ci; + diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index fd8db3484c..3e6a074c2c 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -618,7 +618,7 @@ celerity_register_resource_map(array( ), 'aphront-form-view-css' => array( - 'uri' => '/res/45162273/rsrc/css/aphront/form-view.css', + 'uri' => '/res/1e191b83/rsrc/css/aphront/form-view.css', 'type' => 'css', 'requires' => array( @@ -725,6 +725,51 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/config/config-options.css', ), + 'conpherence-header-pane-css' => + array( + 'uri' => '/res/5a02bdbe/rsrc/css/application/conpherence/header-pane.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/conpherence/header-pane.css', + ), + 'conpherence-menu-css' => + array( + 'uri' => '/res/b893e529/rsrc/css/application/conpherence/menu.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/conpherence/menu.css', + ), + 'conpherence-message-pane-css' => + array( + 'uri' => '/res/32073bd5/rsrc/css/application/conpherence/message-pane.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/conpherence/message-pane.css', + ), + 'conpherence-update-css' => + array( + 'uri' => '/res/8e4757b5/rsrc/css/application/conpherence/update.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/conpherence/update.css', + ), + 'conpherence-widget-pane-css' => + array( + 'uri' => '/res/7af40cac/rsrc/css/application/conpherence/widget-pane.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/conpherence/widget-pane.css', + ), 'differential-changeset-view-css' => array( 'uri' => '/res/ea694162/rsrc/css/application/differential/changeset-view.css', @@ -1039,6 +1084,45 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/diffusion/behavior-audit-preview.js', ), + 'javelin-behavior-conpherence-init' => + array( + 'uri' => '/res/bd911b43/rsrc/js/application/conpherence/behavior-init.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + ), + 'disk' => '/rsrc/js/application/conpherence/behavior-init.js', + ), + 'javelin-behavior-conpherence-menu' => + array( + 'uri' => '/res/9d21fb86/rsrc/js/application/conpherence/behavior-menu.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-workflow', + 3 => 'javelin-util', + 4 => 'javelin-stratcom', + 5 => 'javelin-uri', + ), + 'disk' => '/rsrc/js/application/conpherence/behavior-menu.js', + ), + 'javelin-behavior-conpherence-widget-pane' => + array( + 'uri' => '/res/f3e0dbba/rsrc/js/application/conpherence/behavior-widget-pane.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + ), + 'disk' => '/rsrc/js/application/conpherence/behavior-widget-pane.js', + ), 'javelin-behavior-countdown-timer' => array( 'uri' => '/res/7468acb7/rsrc/js/application/countdown/timer.js', @@ -3254,7 +3338,7 @@ celerity_register_resource_map(array( ), array( 'packages' => array( - 'd770a9ec' => + '8fd19266' => array( 'name' => 'core.pkg.css', 'symbols' => @@ -3298,7 +3382,7 @@ celerity_register_resource_map(array( 36 => 'phabricator-object-item-list-view-css', 37 => 'global-drag-and-drop-css', ), - 'uri' => '/res/pkg/d770a9ec/core.pkg.css', + 'uri' => '/res/pkg/8fd19266/core.pkg.css', 'type' => 'css', ), '66dca903' => @@ -3488,19 +3572,19 @@ celerity_register_resource_map(array( 'reverse' => array( 'aphront-attached-file-view-css' => 'e30a3fa8', - 'aphront-crumbs-view-css' => 'd770a9ec', - 'aphront-dialog-view-css' => 'd770a9ec', - 'aphront-error-view-css' => 'd770a9ec', - 'aphront-form-view-css' => 'd770a9ec', + 'aphront-crumbs-view-css' => '8fd19266', + 'aphront-dialog-view-css' => '8fd19266', + 'aphront-error-view-css' => '8fd19266', + 'aphront-form-view-css' => '8fd19266', 'aphront-headsup-action-list-view-css' => 'ec01d039', - 'aphront-headsup-view-css' => 'd770a9ec', - 'aphront-list-filter-view-css' => 'd770a9ec', - 'aphront-pager-view-css' => 'd770a9ec', - 'aphront-panel-view-css' => 'd770a9ec', - 'aphront-table-view-css' => 'd770a9ec', - 'aphront-tokenizer-control-css' => 'd770a9ec', - 'aphront-tooltip-css' => 'd770a9ec', - 'aphront-typeahead-control-css' => 'd770a9ec', + 'aphront-headsup-view-css' => '8fd19266', + 'aphront-list-filter-view-css' => '8fd19266', + 'aphront-pager-view-css' => '8fd19266', + 'aphront-panel-view-css' => '8fd19266', + 'aphront-table-view-css' => '8fd19266', + 'aphront-tokenizer-control-css' => '8fd19266', + 'aphront-tooltip-css' => '8fd19266', + 'aphront-typeahead-control-css' => '8fd19266', 'differential-changeset-view-css' => 'ec01d039', 'differential-core-view-css' => 'ec01d039', 'differential-inline-comment-editor' => 'ac53d36a', @@ -3514,7 +3598,7 @@ celerity_register_resource_map(array( 'differential-table-of-contents-css' => 'ec01d039', 'diffusion-commit-view-css' => 'c8ce2d88', 'diffusion-icons-css' => 'c8ce2d88', - 'global-drag-and-drop-css' => 'd770a9ec', + 'global-drag-and-drop-css' => '8fd19266', 'inline-comment-summary-css' => 'ec01d039', 'javelin-aphlict' => '66dca903', 'javelin-behavior' => 'fbeded59', @@ -3584,48 +3668,48 @@ celerity_register_resource_map(array( 'javelin-util' => 'fbeded59', 'javelin-vector' => 'fbeded59', 'javelin-workflow' => 'fbeded59', - 'lightbox-attachment-css' => 'd770a9ec', + 'lightbox-attachment-css' => '8fd19266', 'maniphest-task-summary-css' => 'e30a3fa8', 'maniphest-transaction-detail-css' => 'e30a3fa8', 'phabricator-busy' => '66dca903', 'phabricator-content-source-view-css' => 'ec01d039', - 'phabricator-core-buttons-css' => 'd770a9ec', - 'phabricator-core-css' => 'd770a9ec', - 'phabricator-crumbs-view-css' => 'd770a9ec', - 'phabricator-directory-css' => 'd770a9ec', + 'phabricator-core-buttons-css' => '8fd19266', + 'phabricator-core-css' => '8fd19266', + 'phabricator-crumbs-view-css' => '8fd19266', + 'phabricator-directory-css' => '8fd19266', 'phabricator-drag-and-drop-file-upload' => 'ac53d36a', 'phabricator-dropdown-menu' => '66dca903', 'phabricator-file-upload' => '66dca903', - 'phabricator-filetree-view-css' => 'd770a9ec', - 'phabricator-flag-css' => 'd770a9ec', - 'phabricator-form-view-css' => 'd770a9ec', - 'phabricator-header-view-css' => 'd770a9ec', - 'phabricator-jump-nav' => 'd770a9ec', + 'phabricator-filetree-view-css' => '8fd19266', + 'phabricator-flag-css' => '8fd19266', + 'phabricator-form-view-css' => '8fd19266', + 'phabricator-header-view-css' => '8fd19266', + 'phabricator-jump-nav' => '8fd19266', 'phabricator-keyboard-shortcut' => '66dca903', 'phabricator-keyboard-shortcut-manager' => '66dca903', - 'phabricator-main-menu-view' => 'd770a9ec', + 'phabricator-main-menu-view' => '8fd19266', 'phabricator-menu-item' => '66dca903', - 'phabricator-nav-view-css' => 'd770a9ec', + 'phabricator-nav-view-css' => '8fd19266', 'phabricator-notification' => '66dca903', - 'phabricator-notification-css' => 'd770a9ec', - 'phabricator-notification-menu-css' => 'd770a9ec', - 'phabricator-object-item-list-view-css' => 'd770a9ec', + 'phabricator-notification-css' => '8fd19266', + 'phabricator-notification-menu-css' => '8fd19266', + 'phabricator-object-item-list-view-css' => '8fd19266', 'phabricator-object-selector-css' => 'ec01d039', 'phabricator-paste-file-upload' => '66dca903', 'phabricator-prefab' => '66dca903', 'phabricator-project-tag-css' => 'e30a3fa8', - 'phabricator-remarkup-css' => 'd770a9ec', + 'phabricator-remarkup-css' => '8fd19266', 'phabricator-shaped-request' => 'ac53d36a', - 'phabricator-side-menu-view-css' => 'd770a9ec', - 'phabricator-standard-page-view' => 'd770a9ec', + 'phabricator-side-menu-view-css' => '8fd19266', + 'phabricator-standard-page-view' => '8fd19266', 'phabricator-textareautils' => '66dca903', 'phabricator-tooltip' => '66dca903', - 'phabricator-transaction-view-css' => 'd770a9ec', - 'phabricator-zindex-css' => 'd770a9ec', - 'sprite-apps-large-css' => 'd770a9ec', - 'sprite-gradient-css' => 'd770a9ec', - 'sprite-icon-css' => 'd770a9ec', - 'sprite-menu-css' => 'd770a9ec', - 'syntax-highlighting-css' => 'd770a9ec', + 'phabricator-transaction-view-css' => '8fd19266', + 'phabricator-zindex-css' => '8fd19266', + 'sprite-apps-large-css' => '8fd19266', + 'sprite-gradient-css' => '8fd19266', + 'sprite-icon-css' => '8fd19266', + 'sprite-menu-css' => '8fd19266', + 'syntax-highlighting-css' => '8fd19266', ), )); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5f6fb8a008..8cf098bb7c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -195,6 +195,25 @@ phutil_register_library_map(array( 'ConduitCallTestCase' => 'applications/conduit/call/__tests__/ConduitCallTestCase.php', 'ConduitException' => 'applications/conduit/protocol/ConduitException.php', 'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php', + 'ConpherenceConstants' => 'applications/conpherence/constants/ConpherenceConstants.php', + 'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php', + 'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php', + 'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php', + 'ConpherenceListController' => 'applications/conpherence/controller/ConpherenceListController.php', + 'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php', + 'ConpherenceNewController' => 'applications/conpherence/controller/ConpherenceNewController.php', + 'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php', + 'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php', + 'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php', + 'ConpherenceThread' => 'applications/conpherence/storage/ConpherenceThread.php', + 'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php', + 'ConpherenceTransaction' => 'applications/conpherence/storage/ConpherenceTransaction.php', + 'ConpherenceTransactionComment' => 'applications/conpherence/storage/ConpherenceTransactionComment.php', + 'ConpherenceTransactionQuery' => 'applications/conpherence/query/ConpherenceTransactionQuery.php', + 'ConpherenceTransactionType' => 'applications/conpherence/constants/ConpherenceTransactionType.php', + 'ConpherenceTransactionView' => 'applications/conpherence/view/ConpherenceTransactionView.php', + 'ConpherenceUpdateController' => 'applications/conpherence/controller/ConpherenceUpdateController.php', + 'ConpherenceViewController' => 'applications/conpherence/controller/ConpherenceViewController.php', 'DarkConsoleController' => 'aphront/console/DarkConsoleController.php', 'DarkConsoleCore' => 'aphront/console/DarkConsoleCore.php', 'DarkConsoleErrorLogPlugin' => 'aphront/console/plugin/DarkConsoleErrorLogPlugin.php', @@ -602,6 +621,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationConduit' => 'applications/conduit/application/PhabricatorApplicationConduit.php', 'PhabricatorApplicationConfig' => 'applications/config/application/PhabricatorApplicationConfig.php', 'PhabricatorApplicationConfigOptions' => 'applications/config/option/PhabricatorApplicationConfigOptions.php', + 'PhabricatorApplicationConpherence' => 'applications/conpherence/application/PhabricatorApplicationConpherence.php', 'PhabricatorApplicationCountdown' => 'applications/countdown/application/PhabricatorApplicationCountdown.php', 'PhabricatorApplicationDaemons' => 'applications/daemon/application/PhabricatorApplicationDaemons.php', 'PhabricatorApplicationDetailViewController' => 'applications/meta/controller/PhabricatorApplicationDetailViewController.php', @@ -1646,6 +1666,28 @@ phutil_register_library_map(array( 'ConduitCallTestCase' => 'PhabricatorTestCase', 'ConduitException' => 'Exception', 'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow', + 'ConpherenceController' => 'PhabricatorController', + 'ConpherenceDAO' => 'PhabricatorLiskDAO', + 'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor', + 'ConpherenceListController' => 'ConpherenceController', + 'ConpherenceMenuItemView' => 'AphrontTagView', + 'ConpherenceNewController' => 'ConpherenceController', + 'ConpherenceParticipant' => 'ConpherenceDAO', + 'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery', + 'ConpherenceParticipationStatus' => 'ConpherenceConstants', + 'ConpherenceThread' => + array( + 0 => 'ConpherenceDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'ConpherenceThreadQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'ConpherenceTransaction' => 'PhabricatorApplicationTransaction', + 'ConpherenceTransactionComment' => 'PhabricatorApplicationTransactionComment', + 'ConpherenceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'ConpherenceTransactionType' => 'ConpherenceConstants', + 'ConpherenceTransactionView' => 'AphrontView', + 'ConpherenceUpdateController' => 'ConpherenceController', + 'ConpherenceViewController' => 'ConpherenceController', 'DarkConsoleController' => 'PhabricatorController', 'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin', 'DarkConsoleEventPlugin' => 'DarkConsolePlugin', @@ -1998,6 +2040,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationConduit' => 'PhabricatorApplication', 'PhabricatorApplicationConfig' => 'PhabricatorApplication', 'PhabricatorApplicationConfigOptions' => 'Phobject', + 'PhabricatorApplicationConpherence' => 'PhabricatorApplication', 'PhabricatorApplicationCountdown' => 'PhabricatorApplication', 'PhabricatorApplicationDaemons' => 'PhabricatorApplication', 'PhabricatorApplicationDetailViewController' => 'PhabricatorApplicationsController', diff --git a/src/applications/conpherence/application/PhabricatorApplicationConpherence.php b/src/applications/conpherence/application/PhabricatorApplicationConpherence.php new file mode 100644 index 0000000000..71e7fb1b9f --- /dev/null +++ b/src/applications/conpherence/application/PhabricatorApplicationConpherence.php @@ -0,0 +1,48 @@ +getBaseURI().'new/'; + } + + public function getShortDescription() { + return pht('Messaging'); + } + + public function getIconName() { + return 'conpherence'; + } + + public function getTitleGlyph() { + return "\xE2\x98\x8E"; + } + + public function getApplicationGroup() { + return self::GROUP_COMMUNICATION; + } + + public function getRoutes() { + return array( + '/conpherence/' => array( + '' => 'ConpherenceListController', + 'new/' => 'ConpherenceNewController', + 'view/(?P[1-9]\d*)/' => 'ConpherenceViewController', + 'update/(?P[1-9]\d*)/' => 'ConpherenceUpdateController', + '(?P[1-9]\d*)/' => 'ConpherenceListController', + ), + ); + } + +} diff --git a/src/applications/conpherence/constants/ConpherenceConstants.php b/src/applications/conpherence/constants/ConpherenceConstants.php new file mode 100644 index 0000000000..09dcae8edb --- /dev/null +++ b/src/applications/conpherence/constants/ConpherenceConstants.php @@ -0,0 +1,8 @@ +unreadConpherences = $conpherences; + return $this; + } + public function getUnreadConpherences() { + return $this->unreadConpherences; + } + + public function setReadConpherences(array $conpherences) { + assert_instances_of($conpherences, 'ConpherenceThread'); + $this->readConpherences = $conpherences; + return $this; + } + public function getReadConpherences() { + return $this->readConpherences; + } + + public function setSelectedConpherencePHID($phid) { + $this->selectedConpherencePHID = $phid; + return $this; + } + public function getSelectedConpherencePHID() { + return $this->selectedConpherencePHID; + } + + /** + * Try for a full set of unread conpherences, and if we fail + * load read conpherences. Additional conpherences in either category + * are loaded asynchronously. + */ + public function loadStartingConpherences($current_selection_epoch = null) { + $user = $this->getRequest()->getUser(); + + $read_participant_query = id(new ConpherenceParticipantQuery()) + ->withParticipantPHIDs(array($user->getPHID())); + $read_status = ConpherenceParticipationStatus::UP_TO_DATE; + if ($current_selection_epoch) { + $read_one = $read_participant_query + ->withParticipationStatus($read_status) + ->withDateTouched($current_selection_epoch, '>') + ->execute(); + + $read_two = $read_participant_query + ->withDateTouched($current_selection_epoch, '<=') + ->execute(); + + $read = array_merge($read_one, $read_two); + + } else { + $read = $read_participant_query + ->withParticipationStatus($read_status) + ->execute(); + } + + $unread_status = ConpherenceParticipationStatus::BEHIND; + $unread_participant_query = id(new ConpherenceParticipantQuery()) + ->withParticipantPHIDs(array($user->getPHID())); + $unread = $unread_participant_query + ->withParticipationStatus($unread_status) + ->execute(); + + $all_participation = $unread + $read; + $all_conpherence_phids = array_keys($all_participation); + $all_conpherences = id(new ConpherenceThreadQuery()) + ->setViewer($user) + ->withPHIDs($all_conpherence_phids) + ->execute(); + $unread_conpherences = array_select_keys( + $all_conpherences, + array_keys($unread) + ); + $this->setUnreadConpherences($unread_conpherences); + + $read_conpherences = array_select_keys( + $all_conpherences, + array_keys($read) + ); + $this->setReadConpherences($read_conpherences); + + if (!$this->getSelectedConpherencePHID()) { + $this->setSelectedConpherencePHID(reset($all_conpherence_phids)); + } + + return $this; + } + + public function buildSideNavView($filter = null) { + require_celerity_resource('conpherence-menu-css'); + $unread_conpherences = $this->getUnreadConpherences(); + $read_conpherences = $this->getReadConpherences(); + + $user = $this->getRequest()->getUser(); + + $menu = new PhabricatorMenuView(); + $nav = AphrontSideNavFilterView::newFromMenu($menu); + $nav->addClass('conpherence-menu'); + $nav->setMenuID('conpherence-menu'); + + $nav->addFilter( + 'new', + pht('New Conpherence'), + $this->getApplicationURI('new/') + ); + $nav->addLabel(pht('Unread')); + $nav = $this->addConpherencesToNav($unread_conpherences, $nav); + + $nav->addLabel(pht('Read')); + $nav = $this->addConpherencesToNav($read_conpherences, $nav, true); + + $nav->selectFilter($filter); + + return $nav; + } + + private function addConpherencesToNav( + array $conpherences, + AphrontSideNavFilterView $nav, + $read = false) { + + $user = $this->getRequest()->getUser(); + foreach ($conpherences as $conpherence) { + $uri = $this->getApplicationURI('view/'.$conpherence->getID().'/'); + $data = $conpherence->getDisplayData($user); + $title = $data['title']; + $subtitle = $data['subtitle']; + $unread_count = $data['unread_count']; + $epoch = $data['epoch']; + $image = $data['image']; + $snippet = $data['snippet']; + + $item = id(new ConpherenceMenuItemView()) + ->setUser($user) + ->setTitle($title) + ->setSubtitle($subtitle) + ->setHref($uri) + ->setEpoch($epoch) + ->setImageURI($image) + ->setMessageText($snippet) + ->setUnreadCount($unread_count) + ->setID($conpherence->getPHID()) + ->addSigil('conpherence-menu-click') + ->setMetadata(array('id' => $conpherence->getID())); + if ($this->getSelectedConpherencePHID() == $conpherence->getPHID()) { + $item->addClass('conpherence-selected'); + $item->addClass('hide-unread-count'); + } + $nav->addCustomBlock($item->render()); + } + if (empty($conpherences) || $read) { + $nav->addCustomBlock($this->getNoConpherencesBlock()); + } + + return $nav; + } + + private function getNoConpherencesBlock() { + + return phutil_render_tag( + 'div', + array( + 'class' => 'no-conpherences-menu-item' + ), + pht('No more conpherences.') + ); + } + + public function buildApplicationMenu() { + return $this->buildSideNavView()->getMenu(); + } + + public function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $crumbs + ->addAction( + id(new PhabricatorMenuItemView()) + ->setName(pht('New Conpherence')) + ->setHref($this->getApplicationURI('new/')) + ->setIcon('create') + ) + ->addCrumb( + id(new PhabricatorCrumbView()) + ->setName(pht('Conpherence')) + ); + + return $crumbs; + } + + protected function initJavelinBehaviors() { + + Javelin::initBehavior('conpherence-menu', + array( + 'base_uri' => $this->getApplicationURI(''), + 'header' => 'conpherence-header-pane', + 'messages' => 'conpherence-messages', + 'widgets_pane' => 'conpherence-widget-pane', + 'form_pane' => 'conpherence-form', + 'fancy_ajax' => (bool) $this->getSelectedConpherencePHID() + ) + ); + Javelin::initBehavior('conpherence-init', + array( + 'selected_conpherence_id' => $this->getSelectedConpherencePHID(), + 'menu_pane' => 'conpherence-menu', + 'messages_pane' => 'conpherence-message-pane', + 'messages' => 'conpherence-messages', + 'widgets_pane' => 'conpherence-widget-pane', + 'form_pane' => 'conpherence-form' + ) + ); + } + +} diff --git a/src/applications/conpherence/controller/ConpherenceListController.php b/src/applications/conpherence/controller/ConpherenceListController.php new file mode 100644 index 0000000000..80ef89d66a --- /dev/null +++ b/src/applications/conpherence/controller/ConpherenceListController.php @@ -0,0 +1,119 @@ +conpherenceID = $conpherence_id; + return $this; + } + public function getConpherenceID() { + return $this->conpherenceID; + } + + public function willProcessRequest(array $data) { + $this->setConpherenceID(idx($data, 'id')); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + $title = pht('Conpherence'); + + $conpherence_id = $this->getConpherenceID(); + $current_selection_epoch = null; + if ($conpherence_id) { + $conpherence = id(new ConpherenceThreadQuery()) + ->setViewer($user) + ->withIDs(array($conpherence_id)) + ->executeOne(); + if (!$conpherence) { + return new Aphront404Response(); + } + + if ($conpherence->getTitle()) { + $title = $conpherence->getTitle(); + } + $this->setSelectedConpherencePHID($conpherence->getPHID()); + + $read_status = ConpherenceParticipationStatus::UP_TO_DATE; + $participant = $conpherence->getParticipant($user->getPHID()); + $write_guard = AphrontWriteGuard::beginScopedUnguardedWrites(); + $participant->markUpToDate(); + unset($write_guard); + $current_selection_epoch = $participant->getDateTouched(); + } + + $this->loadStartingConpherences($current_selection_epoch); + $nav = $this->buildSideNavView(); + + $main_pane = $this->renderEmptyMainPane(); + $nav->appendChild( + array( + $main_pane, + )); + + return $this->buildApplicationPage( + $nav, + array( + 'title' => $title, + 'device' => true, + ) + ); + } + + private function renderEmptyMainPane() { + $this->initJavelinBehaviors(); + return phutil_render_tag( + 'div', + array( + 'id' => 'conpherence-main-pane' + ), + phutil_render_tag( + 'div', + array( + 'class' => 'conpherence-header-pane', + 'id' => 'conpherence-header-pane', + ), + '' + ). + phutil_render_tag( + 'div', + array( + 'class' => 'conpherence-widget-pane', + 'id' => 'conpherence-widget-pane' + ), + '' + ). + javelin_render_tag( + 'div', + array( + 'class' => 'conpherence-message-pane', + 'id' => 'conpherence-message-pane' + ), + phutil_render_tag( + 'div', + array( + 'class' => 'conpherence-messages', + 'id' => 'conpherence-messages' + ), + '' + ). + phutil_render_tag( + 'div', + array( + 'id' => 'conpherence-form' + ), + '' + ) + ) + ); + } + + +} diff --git a/src/applications/conpherence/controller/ConpherenceNewController.php b/src/applications/conpherence/controller/ConpherenceNewController.php new file mode 100644 index 0000000000..1d0f082099 --- /dev/null +++ b/src/applications/conpherence/controller/ConpherenceNewController.php @@ -0,0 +1,201 @@ +getRequest(); + $user = $request->getUser(); + + $conpherence = id(new ConpherenceThread()) + ->attachParticipants(array()) + ->attachFilePHIDs(array()); + $title = pht('New Conpherence'); + $participants = array(); + $message = ''; + $files = array(); + $errors = array(); + $e_participants = null; + $e_message = null; + + // this comes from ajax requests from all over. should be a single phid. + $participant_prefill = $request->getStr('participant'); + if ($participant_prefill) { + $participants[] = $participant_prefill; + } + + if ($request->isFormPost()) { + $participants = $request->getArr('participants'); + if (empty($participants)) { + $e_participants = true; + $errors[] = pht('You must specify participants.'); + } else { + $participants[] = $user->getPHID(); + $participants = array_unique($participants); + } + + $message = $request->getStr('message'); + if (empty($message)) { + $e_message = true; + $errors[] = pht('You must write a message.'); + } + + $file_phids = + PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles( + array($message) + ); + if ($file_phids) { + $files = id(new PhabricatorFileQuery()) + ->setViewer($user) + ->withPHIDs($file_phids) + ->execute(); + } + + if (!$errors) { + $conpherence->openTransaction(); + $conpherence->save(); + $xactions = array(); + $xactions[] = id(new ConpherenceTransaction()) + ->setTransactionType(ConpherenceTransactionType::TYPE_PARTICIPANTS) + ->setNewValue(array('+' => $participants)); + $xactions[] = id(new ConpherenceTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) + ->attachComment( + id(new ConpherenceTransactionComment()) + ->setContent($message) + ->setConpherencePHID($conpherence->getPHID()) + ); + if ($files) { + $xactions[] = id(new ConpherenceTransaction()) + ->setTransactionType(ConpherenceTransactionType::TYPE_FILES) + ->setNewValue(array('+' => mpull($files, 'getPHID'))); + } + $content_source = PhabricatorContentSource::newForSource( + PhabricatorContentSource::SOURCE_WEB, + array( + 'ip' => $request->getRemoteAddr() + ) + ); + id(new ConpherenceEditor()) + ->setContentSource($content_source) + ->setContinueOnNoEffect(true) + ->setActor($user) + ->applyTransactions($conpherence, $xactions); + + $conpherence->saveTransaction(); + + if ($request->isAjax()) { + $dialog = id(new AphrontDialogView()) + ->setUser($user) + ->setTitle('Success') + ->addCancelButton('#', 'Okay') + ->appendChild( + phutil_render_tag('p', + array(), + pht('Message sent successfully.') + ) + ); + $response = id(new AphrontDialogResponse()) + ->setDialog($dialog); + } else { + $uri = $this->getApplicationURI($conpherence->getID()); + $response = id(new AphrontRedirectResponse()) + ->setURI($uri); + } + return $response; + } + } + + $error_view = null; + if ($errors) { + $error_view = id(new AphrontErrorView()) + ->setTitle(pht('Conpherence Errors')) + ->setErrors($errors); + } + + $participant_handles = array(); + if ($participants) { + $handles = id(new PhabricatorObjectHandleData($participants)) + ->setViewer($user) + ->loadHandles(); + $participant_handles = mpull($handles, 'getFullName', 'getPHID'); + } + + $submit_uri = $this->getApplicationURI('new/'); + $cancel_uri = $this->getApplicationURI(); + if ($request->isAjax()) { + // TODO - we can get a better cancel_uri once we get better at crazy + // ajax jonx T2086 + if ($participant_prefill) { + $handle = $handles[$participant_prefill]; + $cancel_uri = $handle->getURI(); + } + $form = id(new AphrontDialogView()) + ->setSubmitURI($submit_uri) + ->addSubmitButton() + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->addCancelButton($cancel_uri); + } else { + $form = id(new AphrontFormView()) + ->setID('conpherence-message-pane') + ->setAction($submit_uri); + } + + $form + ->setUser($user) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setName('participants') + ->setValue($participant_handles) + ->setUser($user) + ->setDatasource('/typeahead/common/users/') + ->setLabel(pht('To')) + ->setError($e_participants) + ) + ->appendChild( + id(new PhabricatorRemarkupControl()) + ->setName('message') + ->setValue($message) + ->setLabel(pht('Message')) + ->setPlaceHolder(pht('Drag and drop to include files...')) + ->setError($e_message) + ); + + if ($request->isAjax()) { + $form->setTitle($title); + return id(new AphrontDialogResponse()) + ->setDialog($form); + } + + $form + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Submit')) + ->addCancelButton($cancel_uri) + ); + + $this->loadStartingConpherences(); + $this->setSelectedConpherencePHID(null); + $this->initJavelinBehaviors(); + $nav = $this->buildSideNavView('new'); + $header = id(new PhabricatorHeaderView()) + ->setHeader($title); + + $nav->appendChild( + array( + $header, + $error_view, + $form, + )); + + return $this->buildApplicationPage( + $nav, + array( + 'title' => $title, + 'device' => true, + ) + ); + } +} diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php new file mode 100644 index 0000000000..75f49476e4 --- /dev/null +++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php @@ -0,0 +1,219 @@ +conpherenceID = $conpherence_id; + return $this; + } + public function getConpherenceID() { + return $this->conpherenceID; + } + public function willProcessRequest(array $data) { + $this->setConpherenceID(idx($data, 'id')); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + $conpherence_id = $this->getConpherenceID(); + if (!$conpherence_id) { + return new Aphront404Response(); + } + + $conpherence = id(new ConpherenceThreadQuery()) + ->setViewer($user) + ->withIDs(array($conpherence_id)) + ->executeOne(); + $supported_formats = PhabricatorFile::getTransformableImageFormats(); + + $updated = false; + $error_view = null; + $e_image = null; + $errors = array(); + if ($request->isFormPost()) { + $content_source = PhabricatorContentSource::newForSource( + PhabricatorContentSource::SOURCE_WEB, + array( + 'ip' => $request->getRemoteAddr() + )); + + $action = $request->getStr('action'); + switch ($action) { + case 'message': + $message = $request->getStr('text'); + $files = array(); + $file_phids = + PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles( + array($message) + ); + if ($file_phids) { + $files = id(new PhabricatorFileQuery()) + ->setViewer($user) + ->withPHIDs($file_phids) + ->execute(); + } + $xactions = array(); + if ($files) { + $xactions[] = id(new ConpherenceTransaction()) + ->setTransactionType(ConpherenceTransactionType::TYPE_FILES) + ->setNewValue(array('+' => mpull($files, 'getPHID'))); + } + $xactions[] = id(new ConpherenceTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) + ->attachComment( + id(new ConpherenceTransactionComment()) + ->setContent($message) + ->setConpherencePHID($conpherence->getPHID()) + ); + $time = time(); + $conpherence->openTransaction(); + $xactions = id(new ConpherenceEditor()) + ->setContentSource($content_source) + ->setActor($user) + ->applyTransactions($conpherence, $xactions); + $last_xaction = end($xactions); + $xaction_phid = $last_xaction->getPHID(); + $behind = ConpherenceParticipationStatus::BEHIND; + $up_to_date = ConpherenceParticipationStatus::UP_TO_DATE; + $participants = $conpherence->getParticipants(); + foreach ($participants as $phid => $participant) { + if ($phid != $user->getPHID()) { + if ($participant->getParticipationStatus() != $behind) { + $participant->setBehindTransactionPHID($xaction_phid); + } + $participant->setParticipationStatus($behind); + $participant->setDateTouched($time); + } else { + $participant->setParticipationStatus($up_to_date); + $participant->setDateTouched($time); + } + $participant->save(); + } + $updated = $conpherence->saveTransaction(); + break; + case 'metadata': + $xactions = array(); + $default_image = $request->getExists('default_image'); + if ($default_image) { + $image_phid = null; + $xactions[] = id(new ConpherenceTransaction()) + ->setTransactionType(ConpherenceTransactionType::TYPE_PICTURE) + ->setNewValue($image_phid); + } else if (!empty($_FILES['image'])) { + $err = idx($_FILES['image'], 'error'); + if ($err != UPLOAD_ERR_NO_FILE) { + $file = PhabricatorFile::newFromPHPUpload( + $_FILES['image'], + array( + 'authorPHID' => $user->getPHID(), + )); + $okay = $file->isTransformableImage(); + if ($okay) { + $xformer = new PhabricatorImageTransformer(); + $xformed = $xformer->executeThumbTransform( + $file, + $x = 50, + $y = 50); + $image_phid = $xformed->getPHID(); + $xactions[] = id(new ConpherenceTransaction()) + ->setTransactionType(ConpherenceTransactionType::TYPE_PICTURE) + ->setNewValue($image_phid); + } else { + $e_image = pht('Not Supported'); + $errors[] = + pht('This server only supports these image formats: %s.', + implode(', ', $supported_formats)); + } + } + } + $title = $request->getStr('title'); + if ($title != $conpherence->getTitle()) { + $xactions[] = id(new ConpherenceTransaction()) + ->setTransactionType(ConpherenceTransactionType::TYPE_TITLE) + ->setNewValue($title); + } + + if ($xactions) { + $conpherence->openTransaction(); + $xactions = id(new ConpherenceEditor()) + ->setContentSource($content_source) + ->setActor($user) + ->setContinueOnNoEffect(true) + ->applyTransactions($conpherence, $xactions); + $updated = $conpherence->saveTransaction(); + } else if (empty($errors)) { + $errors[] = pht( + 'That was a non-update. Try cancel.' + ); + } + break; + default: + throw new Exception('Unknown action: '.$action); + break; + } + } + + if ($updated) { + return id(new AphrontRedirectResponse())->setURI( + $this->getApplicationURI($conpherence_id.'/') + ); + } + + if ($errors) { + $error_view = id(new AphrontErrorView()) + ->setTitle(pht('Errors editing conpherence.')) + ->setErrors($errors); + } + + $form = id(new AphrontFormLayoutView()) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Title')) + ->setName('title') + ->setValue($conpherence->getTitle()) + ) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Image')) + ->setValue(phutil_render_tag( + 'img', + array( + 'src' => $conpherence->loadImageURI(), + )) + ) + ) + ->appendChild( + id(new AphrontFormImageControl()) + ->setLabel(pht('Change Image')) + ->setName('image') + ->setCaption('Supported formats: '.implode(', ', $supported_formats)) + ->setError($e_image) + ); + + // TODO -- fix javelin so we can upload files from a workflow + require_celerity_resource('conpherence-update-css'); + return $this->buildStandardPageResponse( + array( + $error_view, + id(new AphrontDialogView()) + ->setUser($user) + ->setTitle(pht('Update Conpherence')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setSubmitURI($this->getApplicationURI('update/'.$conpherence_id.'/')) + ->setEncType('multipart/form-data') + ->addHiddenInput('action', 'metadata') + ->appendChild($form) + ->addSubmitButton() + ->addCancelButton($this->getApplicationURI($conpherence->getID().'/')), + ), + array() + ); + } +} diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php new file mode 100644 index 0000000000..925843c45c --- /dev/null +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -0,0 +1,291 @@ +conpherence = $conpherence; + return $this; + } + public function getConpherence() { + return $this->conpherence; + } + + public function setConpherenceID($conpherence_id) { + $this->conpherenceID = $conpherence_id; + return $this; + } + public function getConpherenceID() { + return $this->conpherenceID; + } + + public function willProcessRequest(array $data) { + $this->setConpherenceID(idx($data, 'id')); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $conpherence_id = $this->getConpherenceID(); + if (!$conpherence_id) { + return new Aphront404Response(); + } + if (!$request->isAjax()) { + return id(new AphrontRedirectResponse()) + ->setURI($this->getApplicationURI($conpherence_id.'/')); + } + $conpherence = id(new ConpherenceThreadQuery()) + ->setViewer($user) + ->withIDs(array($conpherence_id)) + ->needWidgetData(true) + ->executeOne(); + $this->setConpherence($conpherence); + + $participant = $conpherence->getParticipant($user->getPHID()); + $write_guard = AphrontWriteGuard::beginScopedUnguardedWrites(); + $participant->markUpToDate(); + unset($write_guard); + + $header = $this->renderHeaderPaneContent(); + $messages = $this->renderMessagePaneContent(); + $widgets = $this->renderWidgetPaneContent(); + $content = $header + $widgets + $messages; + + return id(new AphrontAjaxResponse())->setContent($content); + } + + private function renderHeaderPaneContent() { + require_celerity_resource('conpherence-header-pane-css'); + $user = $this->getRequest()->getUser(); + $conpherence = $this->getConpherence(); + $display_data = $conpherence->getDisplayData($user); + $edit_href = $this->getApplicationURI('update/'.$conpherence->getID().'/'); + + $header = + phutil_render_tag( + 'a', + array( + 'class' => 'edit', + 'href' => $edit_href, + ), + pht('edit...') + ). + phutil_render_tag( + 'div', + array( + 'class' => 'header-image', + 'style' => 'background-image: url('.$display_data['image'].');' + ), + '' + ). + phutil_render_tag( + 'div', + array( + 'class' => 'title', + ), + phutil_escape_html($display_data['title']) + ). + phutil_render_tag( + 'div', + array( + 'class' => 'subtitle', + ), + phutil_escape_html($display_data['subtitle']) + ); + + return array('header' => $header); + } + + private function renderMessagePaneContent() { + require_celerity_resource('conpherence-message-pane-css'); + $user = $this->getRequest()->getUser(); + $conpherence = $this->getConpherence(); + $handles = $conpherence->getHandles(); + $rendered_transactions = array(); + + $transactions = $conpherence->getTransactions(); + foreach ($transactions as $transaction) { + $rendered_transactions[] = id(new ConpherenceTransactionView()) + ->setUser($user) + ->setConpherenceTransaction($transaction) + ->setHandles($handles) + ->render(); + } + $transactions = implode(' ', $rendered_transactions); + + $form = + id(new AphrontFormView()) + ->setAction($this->getApplicationURI('update/'.$conpherence->getID().'/')) + ->setFlexible(true) + ->setUser($user) + ->addHiddenInput('action', 'message') + ->appendChild( + id(new PhabricatorRemarkupControl()) + ->setUser($user) + ->setName('text') + ) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Pontificate')) + )->render(); + + return array( + 'messages' => $transactions, + 'form' => $form + ); + + } + + private function renderWidgetPaneContent() { + require_celerity_resource('conpherence-widget-pane-css'); + Javelin::initBehavior( + 'conpherence-widget-pane', + array( + 'widgetRegistery' => array( + 'widgets-files' => 1, + 'widgets-tasks' => 1, + 'widgets-calendar' => 1, + ) + ) + ); + + $conpherence = $this->getConpherence(); + + $widgets = phutil_render_tag( + 'div', + array( + 'class' => 'widgets-header' + ), + javelin_render_tag( + 'a', + array( + 'sigil' => 'conpherence-change-widget', + 'meta' => array('widget' => 'widgets-files') + ), + pht('Files') + ).' | '. + javelin_render_tag( + 'a', + array( + 'sigil' => 'conpherence-change-widget', + 'meta' => array('widget' => 'widgets-tasks') + ), + pht('Tasks') + ).' | '. + javelin_render_tag( + 'a', + array( + 'sigil' => 'conpherence-change-widget', + 'meta' => array('widget' => 'widgets-calendar') + ), + pht('Calendar') + ) + ). + phutil_render_tag( + 'div', + array( + 'class' => 'widgets-body', + 'id' => 'widgets-files', + 'style' => 'display: none;' + ), + $this->renderFilesWidgetPaneContent() + ). + phutil_render_tag( + 'div', + array( + 'class' => 'widgets-body', + 'id' => 'widgets-tasks', + ), + $this->renderTaskWidgetPaneContent() + ). + phutil_render_tag( + 'div', + array( + 'class' => 'widgets-body', + 'id' => 'widgets-calendar', + 'style' => 'display: none;' + ), + $this->renderCalendarWidgetPaneContent() + ); + + return array('widgets' => $widgets); + } + + private function renderFilesWidgetPaneContent() { + $conpherence = $this->getConpherence(); + $widget_data = $conpherence->getWidgetData(); + $files = $widget_data['files']; + + $table_data = array(); + foreach ($files as $file) { + $thumb = $file->getThumb60x45URI(); + $table_data[] = array( + phutil_render_tag( + 'img', + array( + 'src' => $thumb + ), + '' + ), + $file->getName() + ); + } + $header = id(new PhabricatorHeaderView()) + ->setHeader(pht('Attached Files')); + $table = id(new AphrontTableView($table_data)) + ->setNoDataString(pht('No files attached to conpherence.')) + ->setHeaders(array('', pht('Name'))) + ->setColumnClasses(array('', 'wide')); + return $header->render() . $table->render(); + } + + private function renderTaskWidgetPaneContent() { + $conpherence = $this->getConpherence(); + $widget_data = $conpherence->getWidgetData(); + $tasks = $widget_data['tasks']; + $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); + $handles = $conpherence->getHandles(); + $content = array(); + foreach ($tasks as $owner_phid => $actual_tasks) { + $handle = $handles[$owner_phid]; + $content[] = id(new PhabricatorHeaderView()) + ->setHeader($handle->getName()) + ->render(); + $actual_tasks = msort($actual_tasks, 'getPriority'); + $actual_tasks = array_reverse($actual_tasks); + $data = array(); + foreach ($actual_tasks as $task) { + $data[] = array( + idx($priority_map, $task->getPriority(), pht('???')), + phutil_render_tag( + 'a', + array( + 'href' => '/T'.$task->getID() + ), + phutil_escape_html($task->getTitle()) + ) + ); + } + $table = id(new AphrontTableView($data)) + ->setNoDataString(pht('No open tasks.')) + ->setHeaders(array(pht('Pri'), pht('Title'))) + ->setColumnClasses(array('', 'wide')); + $content[] = $table->render(); + } + return implode('', $content); + } + + private function renderCalendarWidgetPaneContent() { + $header = id(new PhabricatorHeaderView()) + ->setHeader(pht('Calendar')); + return $header->render() . 'TODO'; + } + +} diff --git a/src/applications/conpherence/editor/ConpherenceEditor.php b/src/applications/conpherence/editor/ConpherenceEditor.php new file mode 100644 index 0000000000..93344d2215 --- /dev/null +++ b/src/applications/conpherence/editor/ConpherenceEditor.php @@ -0,0 +1,175 @@ +getTransactionType()) { + case ConpherenceTransactionType::TYPE_TITLE: + return $object->getTitle(); + case ConpherenceTransactionType::TYPE_PICTURE: + return $object->getImagePHID(); + case ConpherenceTransactionType::TYPE_PARTICIPANTS: + return $object->getParticipantPHIDs(); + case ConpherenceTransactionType::TYPE_FILES: + return $object->getFilePHIDs(); + } + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case ConpherenceTransactionType::TYPE_TITLE: + case ConpherenceTransactionType::TYPE_PICTURE: + return $xaction->getNewValue(); + case ConpherenceTransactionType::TYPE_PARTICIPANTS: + case ConpherenceTransactionType::TYPE_FILES: + return $this->getPHIDTransactionNewValue($xaction); + } + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case ConpherenceTransactionType::TYPE_TITLE: + $object->setTitle($xaction->getNewValue()); + break; + case ConpherenceTransactionType::TYPE_PICTURE: + $object->setImagePHID($xaction->getNewValue()); + break; + } + } + + /** + * For now this only supports adding more files and participants. + */ + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case ConpherenceTransactionType::TYPE_FILES: + $editor = id(new PhabricatorEdgeEditor()) + ->setActor($this->getActor()); + $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE; + foreach ($xaction->getNewValue() as $file_phid) { + $editor->addEdge( + $object->getPHID(), + $edge_type, + $file_phid + ); + } + $editor->save(); + break; + case ConpherenceTransactionType::TYPE_PARTICIPANTS: + foreach ($xaction->getNewValue() as $participant) { + if ($participant == $this->getActor()->getPHID()) { + $status = ConpherenceParticipationStatus::UP_TO_DATE; + } else { + $status = ConpherenceParticipationStatus::BEHIND; + } + id(new ConpherenceParticipant()) + ->setConpherencePHID($object->getPHID()) + ->setParticipantPHID($participant) + ->setParticipationStatus($status) + ->setDateTouched(time()) + ->setBehindTransactionPHID($xaction->getPHID()) + ->save(); + } + break; + } + } + + protected function mergeTransactions( + PhabricatorApplicationTransaction $u, + PhabricatorApplicationTransaction $v) { + + $type = $u->getTransactionType(); + switch ($type) { + case ConpherenceTransactionType::TYPE_TITLE: + case ConpherenceTransactionType::TYPE_PICTURE: + return $v; + case ConpherenceTransactionType::TYPE_FILES: + case ConpherenceTransactionType::TYPE_PARTICIPANTS: + return $this->mergePHIDTransactions($u, $v); + } + + return parent::mergeTransactions($u, $v); + } + + protected function supportsMail() { + return false; + } + + /* TODO + + protected function buildReplyHandler(PhabricatorLiskDAO $object) { + return id(new ConpherenceReplyHandler()) + ->setMailReceiver($object); + } + + protected function buildMailTemplate(PhabricatorLiskDAO $object) { + $id = $object->getID(); + $title = $object->getTitle(); + $phid = $object->getPHID(); + $original_name = $object->getOriginalName(); + + return id(new PhabricatorMetaMTAMail()) + ->setSubject("C{$id}: {$title}") + ->addHeader('Thread-Topic', "C{$id}: {$phid}"); + } + + protected function getMailTo(PhabricatorLiskDAO $object) { + $participants = $object->getParticipants(); + $participants[$this->requireActor()->getPHID()] = true; + return array_keys($participants); + } + + protected function buildMailBody( + PhabricatorLiskDAO $object, + array $xactions) { + + $body = parent::buildMailBody($object, $xactions); + $body->addTextSection( + pht('CONPHERENCE DETAIL'), + PhabricatorEnv::getProductionURI('/conpherence/'.$object->getID().'/')); + + return $body; + } + + protected function getMailSubjectPrefix() { + return PhabricatorEnv::getEnvConfig('metamta.conpherence.subject-prefix'); + } + */ + + protected function supportsFeed() { + return false; + } + + protected function supportsSearch() { + return false; + } + +} diff --git a/src/applications/conpherence/query/ConpherenceParticipantQuery.php b/src/applications/conpherence/query/ConpherenceParticipantQuery.php new file mode 100644 index 0000000000..0c61b6f695 --- /dev/null +++ b/src/applications/conpherence/query/ConpherenceParticipantQuery.php @@ -0,0 +1,98 @@ +conpherencePHIDs = $phids; + return $this; + } + + public function withParticipantPHIDs(array $phids) { + $this->participantPHIDs = $phids; + return $this; + } + + public function withDateTouched($date, $sort = null) { + $this->dateTouched = $date; + $this->dateTouchedSort = $sort ? $sort : '<'; + return $this; + } + + public function withParticipationStatus($participation_status) { + $this->participationStatus = $participation_status; + return $this; + } + + public function execute() { + $table = new ConpherenceParticipant(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T participant %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + $participants = $table->loadAllFromArray($data); + + $participants = mpull($participants, null, 'getConpherencePHID'); + + return $participants; + } + + private function buildWhereClause($conn_r) { + $where = array(); + + if ($this->conpherencePHIDs) { + $where[] = qsprintf( + $conn_r, + 'conpherencePHID IN (%Ls)', + $this->conpherencePHIDs); + } + + if ($this->participantPHIDs) { + $where[] = qsprintf( + $conn_r, + 'participantPHID IN (%Ls)', + $this->participantPHIDs); + } + + if ($this->participationStatus !== null) { + $where[] = qsprintf( + $conn_r, + 'participationStatus = %d', + $this->participationStatus + ); + } + + if ($this->dateTouched) { + if ($this->dateTouchedSort) { + $where[] = qsprintf( + $conn_r, + 'dateTouched %Q %d', + $this->dateTouchedSort, + $this->dateTouched + ); + } + } + + return $this->formatWhereClause($where); + } + + private function buildOrderClause(AphrontDatabaseConnection $conn_r) { + return 'ORDER BY dateTouched DESC'; + } + +} diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php new file mode 100644 index 0000000000..18fc8ec1b5 --- /dev/null +++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php @@ -0,0 +1,182 @@ +needWidgetData = $need_widget_data; + return $this; + } + + public function withIDs(array $ids) { + $this->ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function loadPage() { + $table = new ConpherenceThread(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT conpherence_thread.* FROM %T conpherence_thread %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + $conpherences = $table->loadAllFromArray($data); + + if ($conpherences) { + $conpherences = mpull($conpherences, null, 'getPHID'); + $this->loadParticipants($conpherences); + $this->loadTransactionsAndHandles($conpherences); + $this->loadFilePHIDs($conpherences); + if ($this->needWidgetData) { + $this->loadWidgetData($conpherences); + } + } + + return $conpherences; + } + + protected function buildWhereClause($conn_r) { + $where = array(); + + $where[] = $this->buildPagingClause($conn_r); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + return $this->formatWhereClause($where); + } + + private function loadParticipants(array $conpherences) { + $participants = id(new ConpherenceParticipant()) + ->loadAllWhere('conpherencePHID IN (%Ls)', array_keys($conpherences)); + $map = mgroup($participants, 'getConpherencePHID'); + foreach ($map as $conpherence_phid => $conpherence_participants) { + $current_conpherence = $conpherences[$conpherence_phid]; + $conpherence_participants = mpull( + $conpherence_participants, + null, + 'getParticipantPHID' + ); + $current_conpherence->attachParticipants($conpherence_participants); + } + + return $this; + } + + private function loadTransactionsAndHandles(array $conpherences) { + $transactions = id(new ConpherenceTransactionQuery()) + ->setViewer($this->getViewer()) + ->withObjectPHIDs(array_keys($conpherences)) + ->needHandles(true) + ->loadPage(); + $transactions = mgroup($transactions, 'getObjectPHID'); + foreach ($conpherences as $phid => $conpherence) { + $current_transactions = $transactions[$phid]; + $handles = array(); + foreach ($current_transactions as $transaction) { + $handles += $transaction->getHandles(); + } + $conpherence->attachHandles($handles); + $conpherence->attachTransactions($transactions[$phid]); + } + return $this; + } + + private function loadFilePHIDs(array $conpherences) { + $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE; + $file_edges = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array_keys($conpherences)) + ->withEdgeTypes(array($edge_type)) + ->execute(); + foreach ($file_edges as $conpherence_phid => $data) { + $conpherence = $conpherences[$conpherence_phid]; + $conpherence->attachFilePHIDs(array_keys($data[$edge_type])); + } + return $this; + } + + private function loadWidgetData(array $conpherences) { + $participant_phids = array(); + $file_phids = array(); + foreach ($conpherences as $conpherence) { + $participant_phids[] = array_keys($conpherence->getParticipants()); + $file_phids[] = $conpherence->getFilePHIDs(); + } + $participant_phids = array_mergev($participant_phids); + $file_phids = array_mergev($file_phids); + + // open tasks of all participants + $tasks = id(new ManiphestTaskQuery()) + ->withOwners($participant_phids) + ->withStatus(ManiphestTaskQuery::STATUS_OPEN) + ->execute(); + $tasks = mgroup($tasks, 'getOwnerPHID'); + + // statuses of everyone currently in the conpherence + // until the beginning of the next work week. + // NOTE: this is a bit boring on the weekends. + $end_of_week = phabricator_format_local_time( + strtotime('Monday midnight'), + $this->getViewer(), + 'U' + ); + $statuses = id(new PhabricatorUserStatus()) + ->loadAllWhere( + 'userPHID in (%Ls) AND dateTo <= %d', + $participant_phids, + $end_of_week + ); + $statuses = mgroup($statuses, 'getUserPHID'); + + // attached files + $files = array(); + if ($file_phids) { + $files = id(new PhabricatorFileQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + } + + foreach ($conpherences as $phid => $conpherence) { + $participant_phids = array_keys($conpherence->getParticipants()); + $widget_data = array( + 'tasks' => array_select_keys($tasks, $participant_phids), + 'statuses' => array_select_keys($statuses, $participant_phids), + 'files' => array_select_keys($files, $conpherence->getFilePHIDs()), + ); + $conpherence->attachWidgetData($widget_data); + } + + return $this; + } + +} diff --git a/src/applications/conpherence/query/ConpherenceTransactionQuery.php b/src/applications/conpherence/query/ConpherenceTransactionQuery.php new file mode 100644 index 0000000000..dc65c3c70b --- /dev/null +++ b/src/applications/conpherence/query/ConpherenceTransactionQuery.php @@ -0,0 +1,13 @@ +isUpToDate()) { + $this->setParticipationStatus(ConpherenceParticipationStatus::UP_TO_DATE); + $this->save(); + } + return $this; + } + + public function isUpToDate() { + return $this->getParticipationStatus() == + ConpherenceParticipationStatus::UP_TO_DATE; + } + +} diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php new file mode 100644 index 0000000000..c5c2c4723a --- /dev/null +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -0,0 +1,237 @@ + true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorPHIDConstants::PHID_TYPE_CONP); + } + + public function save() { + if (!$this->getMailKey()) { + $this->setMailKey(Filesystem::readRandomCharacters(20)); + } + return parent::save(); + } + + public function attachParticipants(array $participants) { + assert_instances_of($participants, 'ConpherenceParticipant'); + $this->participants = $participants; + return $this; + } + public function getParticipants() { + if ($this->participants === null) { + throw new Exception( + 'You must attachParticipants first!' + ); + } + return $this->participants; + } + public function getParticipant($phid) { + $participants = $this->getParticipants(); + return $participants[$phid]; + } + public function getParticipantPHIDs() { + $participants = $this->getParticipants(); + return array_keys($participants); + } + + public function attachHandles(array $handles) { + assert_instances_of($handles, 'PhabricatorObjectHandle'); + $this->handles = $handles; + return $this; + } + public function getHandles() { + if ($this->handles === null) { + throw new Exception( + 'You must attachHandles first!' + ); + } + return $this->handles; + } + + public function attachTransactions(array $transactions) { + assert_instances_of($transactions, 'ConpherenceTransaction'); + $this->transactions = $transactions; + return $this; + } + public function getTransactions() { + if ($this->transactions === null) { + throw new Exception( + 'You must attachTransactions first!' + ); + } + return $this->transactions; + } + + public function attachFilePHIDs(array $file_phids) { + $this->filePHIDs = $file_phids; + return $this; + } + public function getFilePHIDs() { + if ($this->filePHIDs === null) { + throw new Exception( + 'You must attachFilePHIDs first!' + ); + } + return $this->filePHIDs; + } + + public function attachWidgetData(array $widget_data) { + $this->widgetData = $widget_data; + return $this; + } + public function getWidgetData() { + if ($this->widgetData === null) { + throw new Exception( + 'You must attachWidgetData first!' + ); + } + return $this->widgetData; + } + + public function loadImageURI() { + $src_phid = $this->getImagePHID(); + + if ($src_phid) { + $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid); + if ($file) { + return $file->getBestURI(); + } + } + + return PhabricatorUser::getDefaultProfileImageURI(); + } + + public function getDisplayData(PhabricatorUser $user) { + $transactions = $this->getTransactions(); + $latest_transaction = end($transactions); + $latest_participant = $latest_transaction->getAuthorPHID(); + $handles = $this->getHandles(); + $latest_handle = $handles[$latest_participant]; + if ($this->getImagePHID()) { + $img_src = $this->loadImageURI(); + } else { + $img_src = $latest_handle->getImageURI(); + } + $title = $this->getTitle(); + if (!$title) { + $title = $latest_handle->getName(); + unset($handles[$latest_participant]); + } + unset($handles[$user->getPHID()]); + + $subtitle = ''; + $count = 0; + $final = false; + foreach ($handles as $handle) { + if ($handle->getType() != PhabricatorPHIDConstants::PHID_TYPE_USER) { + continue; + } + if ($subtitle) { + if ($final) { + $subtitle .= '...'; + break; + } else { + $subtitle .= ', '; + } + } + $subtitle .= $handle->getName(); + $count++; + $final = $count == 3; + } + + $participants = $this->getParticipants(); + $user_participation = $participants[$user->getPHID()]; + $unread_count = 0; + $max_count = 10; + $snippet = null; + if (!$user_participation->isUpToDate()) { + $behind_transaction_phid = + $user_participation->getBehindTransactionPHID(); + } else { + $behind_transaction_phid = null; + } + foreach (array_reverse($transactions) as $transaction) { + switch ($transaction->getTransactionType()) { + case ConpherenceTransactionType::TYPE_PARTICIPANTS: + case ConpherenceTransactionType::TYPE_TITLE: + case ConpherenceTransactionType::TYPE_PICTURE: + continue 2; + case PhabricatorTransactions::TYPE_COMMENT: + if ($snippet === null) { + $snippet = phutil_utf8_shorten( + $transaction->getComment()->getContent(), + 48 + ); + } + // fallthrough intentionally here + case ConpherenceTransactionType::TYPE_FILES: + default: + if ($behind_transaction_phid && + $transaction->getPHID() != $behind_transaction_phid) { + $unread_count++; + } + if ($unread_count > $max_count) { + break 2; + } + break; + } + if ($snippet && !$behind_transaction_phid) { + break; + } + } + if ($unread_count > $max_count) { + $unread_count = $max_count.'+'; + } + + return array( + 'title' => $title, + 'subtitle' => $subtitle, + 'unread_count' => $unread_count, + 'epoch' => $latest_transaction->getDateCreated(), + 'image' => $img_src, + 'snippet' => $snippet, + ); + } + +/* -( PhabricatorPolicyInterface Implementation )-------------------------- */ + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::POLICY_NOONE; + } + + public function hasAutomaticCapability($capability, PhabricatorUser $user) { + $participants = $this->getParticipants(); + return isset($participants[$user->getPHID()]); + } + +} diff --git a/src/applications/conpherence/storage/ConpherenceTransaction.php b/src/applications/conpherence/storage/ConpherenceTransaction.php new file mode 100644 index 0000000000..c75ee7cd1f --- /dev/null +++ b/src/applications/conpherence/storage/ConpherenceTransaction.php @@ -0,0 +1,124 @@ +getOldValue(); + + switch ($this->getTransactionType()) { + case ConpherenceTransactionType::TYPE_PARTICIPANTS: + return ($old === null); + case ConpherenceTransactionType::TYPE_TITLE: + case ConpherenceTransactionType::TYPE_PICTURE: + case ConpherenceTransactionType::TYPE_FILES: + return false; + } + + return parent::shouldHide(); + } + + public function getTitle() { + $author_phid = $this->getAuthorPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case ConpherenceTransactionType::TYPE_TITLE: + if ($old) { + $title = pht( + '%s renamed this conpherence from "%s" to "%s".', + $this->renderHandleLink($author_phid), + phutil_escape_html($old), + phutil_escape_html($new)); + } else { + $title = pht( + '%s named this conpherence "%s".', + $this->renderHandleLink($author_phid), + phutil_escape_html($new)); + } + return $title; + case ConpherenceTransactionType::TYPE_FILES: + return pht( + '%s updated the conpherence files.', + $this->renderHandleLink($author_phid)); + case ConpherenceTransactionType::TYPE_PICTURE: + return pht( + '%s updated the conpherence image.', + $this->renderHandleLink($author_phid)); + case ConpherenceTransactionType::TYPE_PARTICIPANTS: + $add = array_diff($new, $old); + $rem = array_diff($old, $new); + + if ($add && $rem) { + $title = pht( + '%s edited participant(s), added %d: %s; removed %d: %s.', + $this->renderHandleLink($author_phid), + count($add), + $this->renderHandleList($add), + count($rem), + $this->renderHandleList($rem)); + } else if ($add) { + $title = pht( + '%s added %d participant(s): %s.', + $this->renderHandleLink($author_phid), + count($add), + $this->renderHandleList($add)); + } else { + $title = pht( + '%s removed %d partipant(s): %s.', + $this->renderHandleLink($author_phid), + count($rem), + $this->renderHandleList($rem)); + } + return $title; + break; + } + + return parent::getTitle(); + } + + public function getRequiredHandlePHIDs() { + $phids = parent::getRequiredHandlePHIDs(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $phids[] = $this->getAuthorPHID(); + switch ($this->getTransactionType()) { + case ConpherenceTransactionType::TYPE_PICTURE: + $phids[] = $new; + break; + case ConpherenceTransactionType::TYPE_TITLE: + break; + case ConpherenceTransactionType::TYPE_PARTICIPANTS: + case ConpherenceTransactionType::TYPE_FILES: + $phids = array_merge($phids, $this->getOldValue()); + $phids = array_merge($phids, $this->getNewValue()); + break; + + } + + return $phids; + } + +} diff --git a/src/applications/conpherence/storage/ConpherenceTransactionComment.php b/src/applications/conpherence/storage/ConpherenceTransactionComment.php new file mode 100644 index 0000000000..bc61f33977 --- /dev/null +++ b/src/applications/conpherence/storage/ConpherenceTransactionComment.php @@ -0,0 +1,15 @@ +unreadCount = $unread_count; + return $this; + } + public function getUnreadCount() { + return $this->unreadCount; + } + + public function setMessageText($message_text) { + $this->messageText = $message_text; + return $this; + } + public function getMessageText() { + return $this->messageText; + } + + public function setEpoch($epoch) { + $this->epoch = $epoch; + return $this; + } + public function getEpoch() { + return $this->epoch; + } + + public function setHref($href) { + $this->href = $href; + return $this; + } + public function getHref() { + return $this->href; + } + + public function setImageURI($image_uri) { + $this->imageURI = $image_uri; + return $this; + } + public function getImageURI() { + return $this->imageURI; + } + + public function setSubtitle($subtitle) { + $this->subtitle = $subtitle; + return $this; + } + public function getSubtitle() { + return $this->subtitle; + } + + public function setTitle($title) { + $this->title = $title; + return $this; + } + + public function getTitle() { + return $this->title; + } + + protected function getTagName() { + return 'a'; + } + + protected function getTagAttributes() { + $classes = array('conpherence-menu-item-view'); + return array( + 'class' => $classes, + 'href' => $this->href, + ); + } + + protected function getTagContent() { + $image = null; + if ($this->imageURI) { + $image = phutil_render_tag( + 'span', + array( + 'class' => 'conpherence-menu-item-image', + 'style' => 'background-image: url('.$this->imageURI.');' + ), + ''); + } + $title = null; + if ($this->title) { + $title = phutil_render_tag( + 'span', + array( + 'class' => 'conpherence-menu-item-title', + ), + phutil_escape_html($this->title)); + } + $subtitle = null; + if ($this->subtitle) { + $subtitle = phutil_render_tag( + 'span', + array( + 'class' => 'conpherence-menu-item-subtitle', + ), + phutil_escape_html($this->subtitle)); + } + $message = null; + if ($this->messageText) { + $message = phutil_render_tag( + 'span', + array( + 'class' => 'conpherence-menu-item-message-text' + ), + phutil_escape_html($this->messageText)); + } + $epoch = null; + if ($this->epoch) { + $epoch = phutil_render_tag( + 'span', + array( + 'class' => 'conpherence-menu-item-date', + ), + phabricator_relative_date($this->epoch, $this->user)); + } + $unread_count = null; + if ($this->unreadCount) { + $unread_count = phutil_render_tag( + 'span', + array( + 'class' => 'conpherence-menu-item-unread-count' + ), + $this->unreadCount); + } + + return $image.$title.$subtitle.$message.$epoch.$unread_count; + } +} diff --git a/src/applications/conpherence/view/ConpherenceTransactionView.php b/src/applications/conpherence/view/ConpherenceTransactionView.php new file mode 100644 index 0000000000..d60f648414 --- /dev/null +++ b/src/applications/conpherence/view/ConpherenceTransactionView.php @@ -0,0 +1,98 @@ +handles = $handles; + return $this; + } + public function getHandles() { + return $this->handles; + } + + public function setConpherenceTransaction(ConpherenceTransaction $tx) { + $this->conpherenceTransaction = $tx; + return $this; + } + private function getConpherenceTransaction() { + return $this->conpherenceTransaction; + } + + public function render() { + $transaction = $this->getConpherenceTransaction(); + $handles = $this->getHandles(); + $transaction->setHandles($handles); + $author = $handles[$transaction->getAuthorPHID()]; + $transaction_view = id(new PhabricatorTransactionView()) + ->setUser($this->getUser()) + ->setEpoch($transaction->getDateCreated()) + ->setContentSource($transaction->getContentSource()); + + $content_class = null; + switch ($transaction->getTransactionType()) { + case ConpherenceTransactionType::TYPE_TITLE: + $content = $transaction->getTitle(); + $transaction_view->addClass('conpherence-edited'); + break; + case ConpherenceTransactionType::TYPE_FILES: + $content = $transaction->getTitle(); + break; + case ConpherenceTransactionType::TYPE_PICTURE: + $img = $transaction->getHandle($transaction->getNewValue()); + $content = $transaction->getTitle() . + phutil_render_tag( + 'img', + array( + 'src' => $img->getImageURI() + ) + ); + $transaction_view->addClass('conpherence-edited'); + break; + case ConpherenceTransactionType::TYPE_PARTICIPANTS: + $content = $transaction->getTitle(); + $transaction_view->addClass('conpherence-edited'); + break; + case PhabricatorTransactions::TYPE_COMMENT: + $comment = $transaction->getComment(); + $file_ids = + PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles( + array($comment->getContent()) + ); + $markup_field = ConpherenceTransactionComment::MARKUP_FIELD_COMMENT; + $engine = id(new PhabricatorMarkupEngine()) + ->setViewer($this->getUser()); + $engine->addObject( + $comment, + $markup_field + ); + $engine->process(); + $content = $engine->getOutput( + $comment, + $markup_field + ); + $content_class = 'conpherence-message phabricator-remarkup'; + $transaction_view + ->setImageURI($author->getImageURI()) + ->setActions(array($author->renderLink())); + break; + } + + $transaction_view + ->appendChild(phutil_render_tag( + 'div', + array( + 'class' => $content_class + ), + $content) + ); + + return $transaction_view->render(); + } +} diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php index 51dcf11bdf..88a62a3dab 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php @@ -39,13 +39,21 @@ final class PhabricatorPeopleProfileController } $username = phutil_escape_uri($user->getUserName()); + $external_arrow = "\xE2\x86\x97"; + + $conpherence_uri = + new PhutilURI('/conpherence/new/?participant='.$user->getPHID()); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI('/p/'.$username.'/')); $nav->addFilter('feed', 'Feed'); + $nav->addMenuItem( + id(new PhabricatorMenuItemView()) + ->setName(pht('Conpherence').' '.$external_arrow) + ->setHref($conpherence_uri) + ); $nav->addFilter('about', 'About'); $nav->addLabel('Activity'); - $external_arrow = "\xE2\x86\x97"; $nav->addFilter( null, "Revisions {$external_arrow}", diff --git a/src/applications/phid/PhabricatorPHIDConstants.php b/src/applications/phid/PhabricatorPHIDConstants.php index 75138a2494..3b549fc41c 100644 --- a/src/applications/phid/PhabricatorPHIDConstants.php +++ b/src/applications/phid/PhabricatorPHIDConstants.php @@ -31,6 +31,7 @@ final class PhabricatorPHIDConstants { const PHID_TYPE_MOCK = 'MOCK'; const PHID_TYPE_MCRO = 'MCRO'; const PHID_TYPE_CONF = 'CONF'; + const PHID_TYPE_CONP = 'CONP'; const PHID_TYPE_XACT = 'XACT'; const PHID_TYPE_XCMT = 'XCMT'; diff --git a/src/applications/phid/handle/PhabricatorObjectHandleData.php b/src/applications/phid/handle/PhabricatorObjectHandleData.php index c0534837b0..aad3a4a326 100644 --- a/src/applications/phid/handle/PhabricatorObjectHandleData.php +++ b/src/applications/phid/handle/PhabricatorObjectHandleData.php @@ -414,6 +414,9 @@ final class PhabricatorObjectHandleData { $handle->setName($file->getName()); $handle->setURI($file->getBestURI()); $handle->setComplete(true); + if ($file->isViewableImage()) { + $handle->setImageURI($file->getBestURI()); + } } $handles[$phid] = $handle; } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 053ace5e7a..72e7ac08ad 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -121,6 +121,15 @@ abstract class PhabricatorApplicationTransaction return $this->handles[$phid]; } + public function getHandles() { + if ($this->handles === null) { + throw new Exception( + 'Transaction requires handles and it did not load them.' + ); + } + return $this->handles; + } + protected function renderHandleLink($phid) { if ($this->renderingTarget == self::TARGET_HTML) { return $this->getHandle($phid)->renderLink(); diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php index a9dc4560fd..10c39c0726 100644 --- a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php +++ b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php @@ -39,6 +39,9 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { const TYPE_OBJECT_HAS_UNSUBSCRIBER = 23; const TYPE_UNSUBSCRIBED_FROM_OBJECT = 24; + const TYPE_OBJECT_HAS_FILE = 25; + const TYPE_FILE_HAS_OBJECT = 26; + const TYPE_TEST_NO_CYCLE = 9000; public static function getInverse($edge_type) { @@ -78,6 +81,9 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { self::TYPE_OBJECT_HAS_UNSUBSCRIBER => self::TYPE_UNSUBSCRIBED_FROM_OBJECT, self::TYPE_UNSUBSCRIBED_FROM_OBJECT => self::TYPE_OBJECT_HAS_UNSUBSCRIBER, + + self::TYPE_OBJECT_HAS_FILE => self::TYPE_FILE_HAS_OBJECT, + self::TYPE_FILE_HAS_OBJECT => self::TYPE_OBJECT_HAS_FILE, ); return idx($map, $edge_type); @@ -109,6 +115,7 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { PhabricatorPHIDConstants::PHID_TYPE_ANSW => 'PonderAnswer', PhabricatorPHIDConstants::PHID_TYPE_MOCK => 'PholioMock', PhabricatorPHIDConstants::PHID_TYPE_MCRO => 'PhabricatorFileImageMacro', + PhabricatorPHIDConstants::PHID_TYPE_CONP => 'ConpherenceThread', ); diff --git a/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php b/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php index 3318127041..8b102e0822 100644 --- a/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php +++ b/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php @@ -193,6 +193,39 @@ abstract class PhabricatorBaseEnglishTranslation ), ), + '%s removed %d subscriber(s): %s.' => array( + array( + '%s removed a subscriber: %3$s.', + '%s removed subscribers: %3$s.', + ), + array( + '%s removed a subscriber: %3$s.', + '%s removed subscribers: %3$s.', + ), + ), + + '%s added %d participant(s): %s.' => array( + array( + '%s added a participant: %3$s.', + '%s added participants: %3$s.', + ), + array( + '%s added a participant: %3$s.', + '%s added participants: %3$s.', + ), + ), + + '%s removed %d participant(s): %s.' => array( + array( + '%s removed a participant: %3$s.', + '%s removed participants: %3$s.', + ), + array( + '%s removed a participant: %3$s.', + '%s removed participants: %3$s.', + ), + ), + '%2$s Line(s)' => array( '%2$s Line', '%2$s Lines', diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index 94c55f5738..9a82942558 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -480,6 +480,22 @@ final class PhabricatorMarkupEngine { return $mentions; } + public static function extractFilePHIDsFromEmbeddedFiles( + array $content_blocks) { + $files = array(); + + $engine = self::newDifferentialMarkupEngine(); + + foreach ($content_blocks as $content_block) { + $engine->markupText($content_block); + $ids = $engine->getTextMetadata( + PhabricatorRemarkupRuleEmbedFile::KEY_EMBED_FILE_PHIDS, + array()); + $files += $ids; + } + + return $files; + } /** * Produce a corpus summary, in a way that shortens the underlying text diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php index 3b0acf5b85..e5113f8240 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php @@ -7,6 +7,7 @@ final class PhabricatorRemarkupRuleEmbedFile extends PhutilRemarkupRule { const KEY_RULE_EMBED_FILE = 'rule.embed.file'; + const KEY_EMBED_FILE_PHIDS = 'phabricator.embedded-file-phids'; public function apply($text) { return preg_replace_callback( @@ -87,6 +88,7 @@ final class PhabricatorRemarkupRuleEmbedFile return; } + $file_phids = array(); foreach ($metadata as $phid => $bundles) { foreach ($bundles as $data) { @@ -159,7 +161,9 @@ final class PhabricatorRemarkupRuleEmbedFile $engine->overwriteStoredText($data['token'], $embed); } + $file_phids[] = $phid; } + $engine->setTextMetadata(self::KEY_EMBED_FILE_PHIDS, $file_phids); $engine->setTextMetadata($metadata_key, array()); } diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index 8e78e1c462..5344eef6a2 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -159,6 +159,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'db', 'name' => 'pholio', ), + 'db.conpherence' => array( + 'type' => 'db', + 'name' => 'conpherence', + ), '0000.legacy.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('0000.legacy.sql'), @@ -1085,6 +1089,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'sql', 'name' => $this->getPatchPath('20130103.filemetadata.sql'), ), + '20130111.conpherence.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('20130111.conpherence.sql'), + ), ); } diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php index 266286cef2..3a942161be 100644 --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -128,6 +128,7 @@ final class AphrontDialogView extends AphrontView { 'action' => $this->submitURI, 'method' => 'post', 'id' => $this->formID, + 'enctype' => $this->encType, ); $hidden_inputs = array(); diff --git a/src/view/form/control/AphrontFormTextAreaControl.php b/src/view/form/control/AphrontFormTextAreaControl.php index 55327e40cd..921d8caef3 100644 --- a/src/view/form/control/AphrontFormTextAreaControl.php +++ b/src/view/form/control/AphrontFormTextAreaControl.php @@ -12,6 +12,15 @@ class AphrontFormTextAreaControl extends AphrontFormControl { private $height; private $readOnly; private $customClass; + private $placeHolder; + + public function setPlaceHolder($place_holder) { + $this->placeHolder = $place_holder; + return $this; + } + private function getPlaceHolder() { + return $this->placeHolder; + } public function setHeight($height) { $this->height = $height; @@ -55,12 +64,13 @@ class AphrontFormTextAreaControl extends AphrontFormControl { return phutil_render_tag( 'textarea', array( - 'name' => $this->getName(), - 'disabled' => $this->getDisabled() ? 'disabled' : null, - 'readonly' => $this->getReadonly() ? 'readonly' : null, - 'class' => $classes, - 'style' => $this->getControlStyle(), - 'id' => $this->getID(), + 'name' => $this->getName(), + 'disabled' => $this->getDisabled() ? 'disabled' : null, + 'readonly' => $this->getReadonly() ? 'readonly' : null, + 'class' => $classes, + 'style' => $this->getControlStyle(), + 'id' => $this->getID(), + 'placeholder' => $this->getPlaceHolder(), ), phutil_escape_html($this->getValue())); } diff --git a/src/view/layout/AphrontSideNavFilterView.php b/src/view/layout/AphrontSideNavFilterView.php index 4258dd9a4a..bceb20e981 100644 --- a/src/view/layout/AphrontSideNavFilterView.php +++ b/src/view/layout/AphrontSideNavFilterView.php @@ -25,16 +25,25 @@ final class AphrontSideNavFilterView extends AphrontView { private $menu; private $crumbs; private $classes = array(); + private $menuID; - public function addClass($class) { - $this->classes[] = $class; + public function setMenuID($menu_id) { + $this->menuID = $menu_id; return $this; } + public function getMenuID() { + return $this->menuID; + } public function __construct() { $this->menu = new PhabricatorMenuView(); } + public function addClass($class) { + $this->classes[] = $class; + return $this; + } + public static function newFromMenu(PhabricatorMenuView $menu) { $object = new AphrontSideNavFilterView(); $object->setBaseURI(new PhutilURI('/')); @@ -205,7 +214,8 @@ final class AphrontSideNavFilterView extends AphrontView { 'class' => 'phabricator-nav-local phabricator-side-menu', 'id' => $local_id, ), - self::renderSingleView($this->menu)); + self::renderSingleView($this->menu->setID($this->getMenuID())) + ); } $crumbs = null; diff --git a/webroot/rsrc/css/aphront/form-view.css b/webroot/rsrc/css/aphront/form-view.css index 6566e36334..234e7579a0 100644 --- a/webroot/rsrc/css/aphront/form-view.css +++ b/webroot/rsrc/css/aphront/form-view.css @@ -196,6 +196,7 @@ table.aphront-form-control-checkbox-layout th { } .aphront-form-control-image .default-image { + display: inline; width: 12px; } diff --git a/webroot/rsrc/css/application/conpherence/header-pane.css b/webroot/rsrc/css/application/conpherence/header-pane.css new file mode 100644 index 0000000000..a690215806 --- /dev/null +++ b/webroot/rsrc/css/application/conpherence/header-pane.css @@ -0,0 +1,41 @@ +/** + * @provides conpherence-header-pane-css + */ + +.conpherence-header-pane { + border-bottom: 1px solid #ccc; + background: #fff; + height: 50px; + width: 100%; +} + +.conpherence-header-pane .edit { + position: relative; + float: right; + margin: 16px 16px 0px 0px; +} + +.conpherence-header-pane .header-image { + position: absolute; + height: 50px; + width: 50px; +} + +.conpherence-header-pane .title { + position: relative; + font-size: 16px; + left: 62px; + top: 6px; + max-width: 80%; + overflow-x: scroll; +} + +.conpherence-header-pane .subtitle { + position: relative; + left: 62px; + top: 6px; + color: #bfbfbf; + max-width: 80%; +} + + diff --git a/webroot/rsrc/css/application/conpherence/menu.css b/webroot/rsrc/css/application/conpherence/menu.css new file mode 100644 index 0000000000..44750360e0 --- /dev/null +++ b/webroot/rsrc/css/application/conpherence/menu.css @@ -0,0 +1,121 @@ +/** + * @provides conpherence-menu-css + */ + +.no-conpherences-menu-item { + color: #ffffff; + border-top: solid 1px #3B3D3E; + padding: 20px 0px 20px 66px; +} + +.conpherence-menu .phabricator-nav-column-background, +.conpherence-menu .phabricator-nav-local { + width: 320px; +} +.conpherence-menu .phabricator-nav-local { + top: 44px; + bottom: 0px; +} + +.conpherence-menu .phabricator-nav-drag { + left: 320px; +} + +.device-desktop .conpherence-menu .phabricator-nav-content { + margin-left: 320px !important; +} + +.conpherence-menu .phabricator-menu-view { + overflow-x: hidden; + overflow-y: scroll; + margin-bottom: 0; +} + +.conpherence-menu .conpherence-menu-item-view { + display: block; + height: 70px; + width: 100%; + overflow: hidden; + position: relative; + text-decoration: none; + border-top: solid 1px #3B3D3E; + border-bottom: solid 1px #1C1F21; + border-right: 0; + border-left: 2px solid transparent; +} + +.conpherence-menu .conpherence-selected { + background: rgba(0, 0, 0, .6); + border-left: 2px solid #66CCFF; +} + +.conpherence-menu .conpherence-menu-item-view:hover { + background-image: url('/rsrc/image/texture/dark-menu-hover.png'); +} + +.conpherence-menu .conpherence-menu-item-view .conpherence-menu-item-image { + top: 6px; + left: 6px; + display: block; + position: absolute; + width: 50px; + height: 50px; + border: 4px solid rgb(29, 32, 34); + border-radius: 2px; +} + +.conpherence-menu .conpherence-menu-item-view .conpherence-menu-item-title { + display: block; + margin-top: 12px; + margin-left: 70px; + text-align: left; + font-weight: bold; + font-size: 12px; + color: #ffffff; + text-shadow: 0px 1px 1px #000000; +} + +.conpherence-menu .conpherence-menu-item-view .conpherence-menu-item-subtitle { + display: block; + color: #bfbfbf; + font-size: 11px; + margin-top: 2px; + margin-left: 70px; + font-style: italic; +} + +.conpherence-menu .conpherence-menu-item-view +.conpherence-menu-item-message-text { + display: block; + color: #66CCFF; + font-size: 12px; + margin-top: 4px; + margin-left: 70px; +} + +.conpherence-menu .conpherence-menu-item-view +.conpherence-menu-item-unread-count { + position: absolute; + left: 48px; + top: 3px; + background: #f00; + border-radius: 10px; + color: white; + font-weight: bold; + padding: 1px 6px 2px; + border: 1px solid #a00; + font-size: 12px; +} + +.conpherence-menu .hide-unread-count .conpherence-menu-item-unread-count, +.conpherence-menu .conpherence-selected .conpherence-menu-item-unread-count { + display: none; +} + +.conpherence-menu .conpherence-menu-item-view .conpherence-menu-item-date { + position: absolute; + top: 10px; + right: 12px; + color: white; + font-size: 12px; +} diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css new file mode 100644 index 0000000000..2c1ac462ee --- /dev/null +++ b/webroot/rsrc/css/application/conpherence/message-pane.css @@ -0,0 +1,95 @@ +/** + * @provides conpherence-message-pane-css + */ +.conpherence-message-pane { + position: fixed; + left: 320px; + top: 94px; + min-width: 320px; + width: auto; + height: 100%; + margin: 0px 320px 0px 0px; +} + +.conpherence-message-pane .conpherence-messages { + position: fixed; + left: 320px; + right: 321px; + top: 94px; + bottom: 225px; + overflow-y: scroll; +} + +.conpherence-message-pane .phabricator-form-view { + border-width: 0; + background: none; + height: 224px; + padding: 0; + position: fixed; + bottom: 0; + left: 320px; + right: 321px; +} + +.device-tablet +.conpherence-message-pane .phabricator-form-view, +.device-tablet +.conpherence-message-pane .conpherence-messages, +.device-tablet +.conpherence-message-pane { + left: 0px; +} + +.conpherence-message-pane .aphront-form-input { + margin: 0; + width: 100%; +} + +.conpherence-message-pane .phabricator-transaction-view { + margin: 1em 1.25em 1em 1.25em; + display: block; +} + +.conpherence-message-pane .phabricator-transaction-detail { + border-width: 0; +} + +.conpherence-message-pane .phabricator-transaction-header { + background: none; +} + +.conpherence-message-pane .phabricator-transaction-info { + background: none; +} + +.conpherence-message-pane .phabricator-transaction-content { + background: none; +} + +.conpherence-message-pane .conpherence-edited .phabricator-transaction-content { + font-size: 12px; + padding: 0em 1em 0em 1em; + margin-top: -4px; +} + +.conpherence-message-pane .aphront-form-control { + padding: 0; +} + +.conpherence-message-pane .aphront-form-control-submit { + margin-right: 8px; +} +.conpherence-message-pane .remarkup-assist-bar { + border-width: 1px 0 0; + border-color: #CCC; +} + +.conpherence-message-pane .remarkup-assist-textarea { + border-width: 1px 0 1px 0; + border-color: #CCC; +} + +.conpherence-message-pane .remarkup-assist-textarea:focus { + outline: none; +} + diff --git a/webroot/rsrc/css/application/conpherence/update.css b/webroot/rsrc/css/application/conpherence/update.css new file mode 100644 index 0000000000..fcf26c9674 --- /dev/null +++ b/webroot/rsrc/css/application/conpherence/update.css @@ -0,0 +1,7 @@ +/** + * @provides conpherence-update-css + */ + +.phabricator-standard-page-body .aphront-dialog-view { + margin: 20px auto 0px auto; +} diff --git a/webroot/rsrc/css/application/conpherence/widget-pane.css b/webroot/rsrc/css/application/conpherence/widget-pane.css new file mode 100644 index 0000000000..9c5a3c0c8f --- /dev/null +++ b/webroot/rsrc/css/application/conpherence/widget-pane.css @@ -0,0 +1,19 @@ +/** + * @provides conpherence-widget-pane-css + */ + +.conpherence-widget-pane { + position: fixed; + right: 0px; + top: 94px; + width: 320px; + height: 100%; + border-width: 0 0 0 1px; + border-color: #CCC; + border-style: solid; +} + +.conpherence-widget-pane .aphront-form-input { + margin: 0; + width: 100%; +} diff --git a/webroot/rsrc/js/application/conpherence/behavior-init.js b/webroot/rsrc/js/application/conpherence/behavior-init.js new file mode 100644 index 0000000000..a843ef5bcd --- /dev/null +++ b/webroot/rsrc/js/application/conpherence/behavior-init.js @@ -0,0 +1,20 @@ +/** + * @provides javelin-behavior-conpherence-init + * @requires javelin-behavior + * javelin-dom + * javelin-stratcom + */ +JX.behavior('conpherence-init', function(config) { + // select the current message + var selectedConpherence = false; + if (config.selected_conpherence_id) { + var selected = JX.$(config.selected_conpherence_id); + JX.Stratcom.invoke( + 'conpherence-initial-selected', + null, + { selected : selected } + ); + selectedConpherence = true; + } + +}); diff --git a/webroot/rsrc/js/application/conpherence/behavior-menu.js b/webroot/rsrc/js/application/conpherence/behavior-menu.js new file mode 100644 index 0000000000..4ac216bdbf --- /dev/null +++ b/webroot/rsrc/js/application/conpherence/behavior-menu.js @@ -0,0 +1,96 @@ +/** + * @provides javelin-behavior-conpherence-menu + * @requires javelin-behavior + * javelin-dom + * javelin-workflow + * javelin-util + * javelin-stratcom + * javelin-uri + */ + +JX.behavior('conpherence-menu', function(config) { + + function onresponse(context, response) { + var header = JX.$H(response.header); + var messages = JX.$H(response.messages); + var form = JX.$H(response.form); + var widgets = JX.$H(response.widgets); + var headerRoot = JX.$(config.header); + var messagesRoot = JX.$(config.messages); + var formRoot = JX.$(config.form_pane); + var widgetsRoot = JX.$(config.widgets_pane); + JX.DOM.setContent(headerRoot, header); + JX.DOM.setContent(messagesRoot, messages); + messagesRoot.scrollTop = messagesRoot.scrollHeight; + JX.DOM.setContent(formRoot, form); + JX.DOM.setContent(widgetsRoot, widgets); + + for (var i = 0; i < context.parentNode.childNodes.length; i++) { + var current = context.parentNode.childNodes[i]; + if (current.id == context.id) { + JX.DOM.alterClass(current, 'conpherence-selected', true); + JX.DOM.alterClass(current, 'hide-unread-count', true); + } else { + JX.DOM.alterClass(current, 'conpherence-selected', false); + } + } + + // TODO - update the browser URI T2086 + + JX.Stratcom.invoke( + 'conpherence-selected-loaded', + null, + {} + ); + } + + JX.Stratcom.listen( + 'click', + 'conpherence-menu-click', + function(e) { + e.kill(); + var selected = e.getNode(['conpherence-menu-click']); + if (config.fancy_ajax) { + JX.Stratcom.invoke( + 'conpherence-selected', + null, + { selected : selected } + ); + } else { + var data = JX.Stratcom.getData(selected); + var uri = new JX.URI(config.base_uri + data.id + '/'); + uri.go(); + } + } + ); + + JX.Stratcom.listen( + 'conpherence-initial-selected', + null, + function(e) { + var selected = e.getData().selected; + e.kill(); + JX.Stratcom.invoke( + 'conpherence-selected', + null, + { selected : selected } + ); + } + ); + + JX.Stratcom.listen( + 'conpherence-selected', + null, + function(e) { + + var selected = e.getData().selected; + var data = JX.Stratcom.getData(selected); + + var uri = config.base_uri + 'view/' + data.id + '/'; + new JX.Workflow(uri, {}) + .setHandler(JX.bind(null, onresponse, selected)) + .start(); + } + ); + +}); diff --git a/webroot/rsrc/js/application/conpherence/behavior-widget-pane.js b/webroot/rsrc/js/application/conpherence/behavior-widget-pane.js new file mode 100644 index 0000000000..badbe128f6 --- /dev/null +++ b/webroot/rsrc/js/application/conpherence/behavior-widget-pane.js @@ -0,0 +1,26 @@ +/** + * @provides javelin-behavior-conpherence-widget-pane + * @requires javelin-behavior + * javelin-dom + * javelin-stratcom + */ + +JX.behavior('conpherence-widget-pane', function(config) { + + JX.Stratcom.listen( + 'click', + 'conpherence-change-widget', + function(e) { + e.kill(); + var data = e.getNodeData('conpherence-change-widget'); + for (var widget in config.widgetRegistery) { + if (widget == data.widget) { + JX.$(widget).style.display = 'block'; + } else { + JX.$(widget).style.display = 'none'; + } + } + } + ); + +});