1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-02-19 18:28:39 +01:00

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
This commit is contained in:
Bob Trahan 2013-01-24 17:23:05 -08:00
parent 72734318e7
commit b873f3f991
44 changed files with 3054 additions and 51 deletions

View file

@ -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;

View file

@ -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',
),
));

View file

@ -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',

View file

@ -0,0 +1,48 @@
<?php
/**
* @group conpherence
*/
final class PhabricatorApplicationConpherence extends PhabricatorApplication {
public function isBeta() {
return true;
}
public function getBaseURI() {
return '/conpherence/';
}
public function getQuickCreateURI() {
return $this->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<id>[1-9]\d*)/' => 'ConpherenceViewController',
'update/(?P<id>[1-9]\d*)/' => 'ConpherenceUpdateController',
'(?P<id>[1-9]\d*)/' => 'ConpherenceListController',
),
);
}
}

View file

@ -0,0 +1,8 @@
<?php
/**
* @group conpherence
*/
abstract class ConpherenceConstants {
}

View file

@ -0,0 +1,8 @@
<?php
final class ConpherenceParticipationStatus extends ConpherenceConstants {
const UP_TO_DATE = 0;
const BEHIND = 1;
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceTransactionType extends ConpherenceConstants {
const TYPE_FILES = 'files';
const TYPE_TITLE = 'title';
const TYPE_PICTURE = 'picture';
const TYPE_PARTICIPANTS = 'participants';
}

View file

@ -0,0 +1,225 @@
<?php
/**
* @group conpherence
*/
abstract class ConpherenceController extends PhabricatorController {
private $conpherences;
private $selectedConpherencePHID;
private $readConpherences;
private $unreadConpherences;
public function setUnreadConpherences(array $conpherences) {
assert_instances_of($conpherences, 'ConpherenceThread');
$this->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'
)
);
}
}

View file

@ -0,0 +1,119 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceListController extends
ConpherenceController {
private $conpherenceID;
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();
$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'
),
''
)
)
);
}
}

View file

@ -0,0 +1,201 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceNewController extends ConpherenceController {
public function processRequest() {
$request = $this->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,
)
);
}
}

View file

@ -0,0 +1,219 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceUpdateController extends
ConpherenceController {
private $conpherenceID;
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();
}
$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()
);
}
}

View file

@ -0,0 +1,291 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceViewController extends
ConpherenceController {
private $conpherenceID;
private $conpherence;
public function setConpherence(ConpherenceThread $conpherence) {
$this->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';
}
}

View file

@ -0,0 +1,175 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = ConpherenceTransactionType::TYPE_TITLE;
$types[] = ConpherenceTransactionType::TYPE_PICTURE;
$types[] = ConpherenceTransactionType::TYPE_PARTICIPANTS;
$types[] = ConpherenceTransactionType::TYPE_FILES;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->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;
}
}

View file

@ -0,0 +1,98 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceParticipantQuery
extends PhabricatorOffsetPagedQuery {
private $conpherencePHIDs;
private $participantPHIDs;
private $dateTouched;
private $dateTouchedSort;
private $participationStatus;
public function withConpherencePHIDs(array $phids) {
$this->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';
}
}

View file

@ -0,0 +1,182 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceThreadQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $phids;
private $ids;
private $needWidgetData;
public function needWidgetData($need_widget_data) {
$this->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;
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceTransactionQuery
extends PhabricatorApplicationTransactionQuery {
protected function getTemplateApplicationTransaction() {
return new ConpherenceTransaction();
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* @group conpherence
*/
abstract class ConpherenceDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'conpherence';
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceParticipant extends ConpherenceDAO {
protected $participantPHID;
protected $conpherencePHID;
protected $participationStatus;
protected $behindTransactionPHID;
protected $dateTouched;
public function markUpToDate() {
if (!$this->isUpToDate()) {
$this->setParticipationStatus(ConpherenceParticipationStatus::UP_TO_DATE);
$this->save();
}
return $this;
}
public function isUpToDate() {
return $this->getParticipationStatus() ==
ConpherenceParticipationStatus::UP_TO_DATE;
}
}

View file

@ -0,0 +1,237 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceThread extends ConpherenceDAO
implements PhabricatorPolicyInterface {
protected $id;
protected $phid;
protected $title;
protected $imagePHID;
protected $mailKey;
private $participants;
private $transactions;
private $handles;
private $filePHIDs;
private $widgetData;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => 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()]);
}
}

View file

@ -0,0 +1,124 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceTransaction extends PhabricatorApplicationTransaction {
public function getApplicationName() {
return 'conpherence';
}
public function getApplicationTransactionType() {
return PhabricatorPHIDConstants::PHID_TYPE_CONP;
}
public function getApplicationTransactionCommentObject() {
return new ConpherenceTransactionComment();
}
public function getApplicationObjectTypeName() {
return pht('conpherence');
}
public function shouldHide() {
$old = $this->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;
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceTransactionComment
extends PhabricatorApplicationTransactionComment {
protected $conpherencePHID;
public function getApplicationTransactionObject() {
return new ConpherenceTransaction();
}
}

View file

@ -0,0 +1,144 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceMenuItemView extends AphrontTagView {
private $title;
private $subtitle;
private $imageURI;
private $href;
private $messageText;
private $epoch;
private $unreadCount;
public function setUnreadCount($unread_count) {
$this->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;
}
}

View file

@ -0,0 +1,98 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceTransactionView extends AphrontView {
private $conpherenceTransaction;
private $handles;
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->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();
}
}

View file

@ -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}",

View file

@ -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';

View file

@ -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;
}

View file

@ -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();

View file

@ -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',
);

View file

@ -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',

View file

@ -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

View file

@ -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());
}

View file

@ -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'),
),
);
}

View file

@ -128,6 +128,7 @@ final class AphrontDialogView extends AphrontView {
'action' => $this->submitURI,
'method' => 'post',
'id' => $this->formID,
'enctype' => $this->encType,
);
$hidden_inputs = array();

View file

@ -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()));
}

View file

@ -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;

View file

@ -196,6 +196,7 @@ table.aphront-form-control-checkbox-layout th {
}
.aphront-form-control-image .default-image {
display: inline;
width: 12px;
}

View file

@ -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%;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -0,0 +1,7 @@
/**
* @provides conpherence-update-css
*/
.phabricator-standard-page-body .aphront-dialog-view {
margin: 20px auto 0px auto;
}

View file

@ -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%;
}

View file

@ -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;
}
});

View file

@ -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();
}
);
});

View file

@ -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';
}
}
}
);
});