diff --git a/resources/builtin/image-100x100.png b/resources/builtin/image-100x100.png index c56a9aa083..d77e1d4584 100644 Binary files a/resources/builtin/image-100x100.png and b/resources/builtin/image-100x100.png differ diff --git a/resources/builtin/image-200x200.png b/resources/builtin/image-200x200.png index 53bc1e785c..520526a2d9 100644 Binary files a/resources/builtin/image-200x200.png and b/resources/builtin/image-200x200.png differ diff --git a/resources/builtin/image-220x220.png b/resources/builtin/image-220x220.png index 928b5b05eb..f9fe4ade48 100644 Binary files a/resources/builtin/image-220x220.png and b/resources/builtin/image-220x220.png differ diff --git a/resources/builtin/image-280x210.png b/resources/builtin/image-280x210.png index 48237b045e..ddece3327d 100644 Binary files a/resources/builtin/image-280x210.png and b/resources/builtin/image-280x210.png differ diff --git a/resources/builtin/image-400x400.png b/resources/builtin/image-400x400.png new file mode 100644 index 0000000000..db23eb01c7 Binary files /dev/null and b/resources/builtin/image-400x400.png differ diff --git a/resources/builtin/image-526x526.png b/resources/builtin/image-526x526.png index 853539d21a..3d666163b2 100644 Binary files a/resources/builtin/image-526x526.png and b/resources/builtin/image-526x526.png differ diff --git a/resources/builtin/image-800x800.png b/resources/builtin/image-800x800.png new file mode 100644 index 0000000000..9755de33f8 Binary files /dev/null and b/resources/builtin/image-800x800.png differ diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php index 5f935c8620..d906c738da 100644 --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -46,6 +46,7 @@ return array( 'javelin-behavior-toggle-class', 'javelin-behavior-lightbox-attachments', 'phabricator-busy', + 'javelin-sound', 'javelin-aphlict', 'phabricator-notification', 'javelin-behavior-aphlict-listen', @@ -160,6 +161,7 @@ return array( 'conpherence.pkg.css' => array( 'conpherence-durable-column-view', 'conpherence-menu-css', + 'conpherence-color-css', 'conpherence-message-pane-css', 'conpherence-notification-css', 'conpherence-transaction-css', diff --git a/resources/sql/autopatches/20170417.files.ngrams.sql b/resources/sql/autopatches/20170417.files.ngrams.sql new file mode 100644 index 0000000000..988b183323 --- /dev/null +++ b/resources/sql/autopatches/20170417.files.ngrams.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_file.file_filename_ngrams ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectID INT UNSIGNED NOT NULL, + ngram CHAR(3) NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_object` (objectID), + KEY `key_ngram` (ngram, objectID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20170418.1.application.01.xaction.sql b/resources/sql/autopatches/20170418.1.application.01.xaction.sql new file mode 100644 index 0000000000..70868ef2f4 --- /dev/null +++ b/resources/sql/autopatches/20170418.1.application.01.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_application.application_applicationtransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20170418.1.application.02.edge.sql b/resources/sql/autopatches/20170418.1.application.02.edge.sql new file mode 100644 index 0000000000..a8f3e1e332 --- /dev/null +++ b/resources/sql/autopatches/20170418.1.application.02.edge.sql @@ -0,0 +1,16 @@ +CREATE TABLE {$NAMESPACE}_application.edge ( + src VARBINARY(64) NOT NULL, + type INT UNSIGNED NOT NULL, + dst VARBINARY(64) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + seq INT UNSIGNED NOT NULL, + dataID INT UNSIGNED, + PRIMARY KEY (src, type, dst), + KEY `src` (src, type, dateCreated, seq), + UNIQUE KEY `key_dst` (dst, type, src) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; + +CREATE TABLE {$NAMESPACE}_application.edgedata ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT} +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20170418.files.isDeleted.sql b/resources/sql/autopatches/20170418.files.isDeleted.sql new file mode 100644 index 0000000000..1349e3cbc7 --- /dev/null +++ b/resources/sql/autopatches/20170418.files.isDeleted.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_file.file + ADD isDeleted BOOL NOT NULL DEFAULT 0; diff --git a/resources/sql/autopatches/20170419.app.01.table.sql b/resources/sql/autopatches/20170419.app.01.table.sql new file mode 100644 index 0000000000..257e0b3eb8 --- /dev/null +++ b/resources/sql/autopatches/20170419.app.01.table.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_application.application_application ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20170419.thread.01.behind.sql b/resources/sql/autopatches/20170419.thread.01.behind.sql new file mode 100644 index 0000000000..08d27337fd --- /dev/null +++ b/resources/sql/autopatches/20170419.thread.01.behind.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_conpherence.conpherence_participant + DROP behindTransactionPHID; diff --git a/resources/sql/autopatches/20170419.thread.02.status.sql b/resources/sql/autopatches/20170419.thread.02.status.sql new file mode 100644 index 0000000000..5f854a4b96 --- /dev/null +++ b/resources/sql/autopatches/20170419.thread.02.status.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_conpherence.conpherence_participant + DROP participationStatus; diff --git a/resources/sql/autopatches/20170419.thread.03.touched.sql b/resources/sql/autopatches/20170419.thread.03.touched.sql new file mode 100644 index 0000000000..f6fee00272 --- /dev/null +++ b/resources/sql/autopatches/20170419.thread.03.touched.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_conpherence.conpherence_participant + DROP dateTouched; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 44a3ca7ad3..1efe9749e7 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -305,12 +305,13 @@ phutil_register_library_map(array( 'ConpherenceParticipantCountQuery' => 'applications/conpherence/query/ConpherenceParticipantCountQuery.php', 'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php', 'ConpherenceParticipantView' => 'applications/conpherence/view/ConpherenceParticipantView.php', - 'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php', 'ConpherenceQueryThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php', 'ConpherenceQueryTransactionConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php', 'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php', 'ConpherenceRoomListController' => 'applications/conpherence/controller/ConpherenceRoomListController.php', 'ConpherenceRoomPictureController' => 'applications/conpherence/controller/ConpherenceRoomPictureController.php', + 'ConpherenceRoomPreferencesController' => 'applications/conpherence/controller/ConpherenceRoomPreferencesController.php', + 'ConpherenceRoomSettings' => 'applications/conpherence/constants/ConpherenceRoomSettings.php', 'ConpherenceRoomTestCase' => 'applications/conpherence/__tests__/ConpherenceRoomTestCase.php', 'ConpherenceSchemaSpec' => 'applications/conpherence/storage/ConpherenceSchemaSpec.php', 'ConpherenceTestCase' => 'applications/conpherence/__tests__/ConpherenceTestCase.php', @@ -321,6 +322,7 @@ phutil_register_library_map(array( 'ConpherenceThreadListView' => 'applications/conpherence/view/ConpherenceThreadListView.php', 'ConpherenceThreadMailReceiver' => 'applications/conpherence/mail/ConpherenceThreadMailReceiver.php', 'ConpherenceThreadMembersPolicyRule' => 'applications/conpherence/policyrule/ConpherenceThreadMembersPolicyRule.php', + 'ConpherenceThreadParticipantsTransaction' => 'applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php', 'ConpherenceThreadPictureTransaction' => 'applications/conpherence/xaction/ConpherenceThreadPictureTransaction.php', 'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php', 'ConpherenceThreadRemarkupRule' => 'applications/conpherence/remarkup/ConpherenceThreadRemarkupRule.php', @@ -349,6 +351,7 @@ phutil_register_library_map(array( 'DarkConsoleEventPlugin' => 'applications/console/plugin/DarkConsoleEventPlugin.php', 'DarkConsoleEventPluginAPI' => 'applications/console/plugin/event/DarkConsoleEventPluginAPI.php', 'DarkConsolePlugin' => 'applications/console/plugin/DarkConsolePlugin.php', + 'DarkConsoleRealtimePlugin' => 'applications/console/plugin/DarkConsoleRealtimePlugin.php', 'DarkConsoleRequestPlugin' => 'applications/console/plugin/DarkConsoleRequestPlugin.php', 'DarkConsoleServicesPlugin' => 'applications/console/plugin/DarkConsoleServicesPlugin.php', 'DarkConsoleStartupPlugin' => 'applications/console/plugin/DarkConsoleStartupPlugin.php', @@ -1097,6 +1100,7 @@ phutil_register_library_map(array( 'FileAllocateConduitAPIMethod' => 'applications/files/conduit/FileAllocateConduitAPIMethod.php', 'FileConduitAPIMethod' => 'applications/files/conduit/FileConduitAPIMethod.php', 'FileCreateMailReceiver' => 'applications/files/mail/FileCreateMailReceiver.php', + 'FileDeletionWorker' => 'applications/files/worker/FileDeletionWorker.php', 'FileDownloadConduitAPIMethod' => 'applications/files/conduit/FileDownloadConduitAPIMethod.php', 'FileInfoConduitAPIMethod' => 'applications/files/conduit/FileInfoConduitAPIMethod.php', 'FileMailReceiver' => 'applications/files/mail/FileMailReceiver.php', @@ -1835,6 +1839,8 @@ phutil_register_library_map(array( 'PhabricatorAppSearchEngine' => 'applications/meta/query/PhabricatorAppSearchEngine.php', 'PhabricatorApplication' => 'applications/base/PhabricatorApplication.php', 'PhabricatorApplicationApplicationPHIDType' => 'applications/meta/phid/PhabricatorApplicationApplicationPHIDType.php', + 'PhabricatorApplicationApplicationTransaction' => 'applications/meta/storage/PhabricatorApplicationApplicationTransaction.php', + 'PhabricatorApplicationApplicationTransactionQuery' => 'applications/meta/query/PhabricatorApplicationApplicationTransactionQuery.php', 'PhabricatorApplicationConfigOptions' => 'applications/config/option/PhabricatorApplicationConfigOptions.php', 'PhabricatorApplicationConfigurationPanel' => 'applications/meta/panel/PhabricatorApplicationConfigurationPanel.php', 'PhabricatorApplicationConfigurationPanelTestCase' => 'applications/meta/panel/__tests__/PhabricatorApplicationConfigurationPanelTestCase.php', @@ -1846,6 +1852,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationPanelController' => 'applications/meta/controller/PhabricatorApplicationPanelController.php', 'PhabricatorApplicationProfileMenuItem' => 'applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php', 'PhabricatorApplicationQuery' => 'applications/meta/query/PhabricatorApplicationQuery.php', + 'PhabricatorApplicationSchemaSpec' => 'applications/meta/storage/PhabricatorApplicationSchemaSpec.php', 'PhabricatorApplicationSearchController' => 'applications/search/controller/PhabricatorApplicationSearchController.php', 'PhabricatorApplicationSearchEngine' => 'applications/search/engine/PhabricatorApplicationSearchEngine.php', 'PhabricatorApplicationSearchEngineTestCase' => 'applications/search/engine/__tests__/PhabricatorApplicationSearchEngineTestCase.php', @@ -1878,6 +1885,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionTemplatedCommentQuery.php', 'PhabricatorApplicationTransactionTextDiffDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php', 'PhabricatorApplicationTransactionTransactionPHIDType' => 'applications/transactions/phid/PhabricatorApplicationTransactionTransactionPHIDType.php', + 'PhabricatorApplicationTransactionType' => 'applications/meta/xactions/PhabricatorApplicationTransactionType.php', 'PhabricatorApplicationTransactionValidationError' => 'applications/transactions/error/PhabricatorApplicationTransactionValidationError.php', 'PhabricatorApplicationTransactionValidationException' => 'applications/transactions/exception/PhabricatorApplicationTransactionValidationException.php', 'PhabricatorApplicationTransactionValidationResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionValidationResponse.php', @@ -2081,20 +2089,6 @@ phutil_register_library_map(array( 'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php', 'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php', 'PhabricatorBoolEditField' => 'applications/transactions/editfield/PhabricatorBoolEditField.php', - 'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php', - 'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php', - 'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php', - 'PhabricatorBotFeedNotificationHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php', - 'PhabricatorBotFlowdockProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php', - 'PhabricatorBotHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotHandler.php', - 'PhabricatorBotLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php', - 'PhabricatorBotMacroHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php', - 'PhabricatorBotMessage' => 'infrastructure/daemon/bot/PhabricatorBotMessage.php', - 'PhabricatorBotObjectNameHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php', - 'PhabricatorBotSymbolHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php', - 'PhabricatorBotTarget' => 'infrastructure/daemon/bot/target/PhabricatorBotTarget.php', - 'PhabricatorBotUser' => 'infrastructure/daemon/bot/target/PhabricatorBotUser.php', - 'PhabricatorBotWhatsNewHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php', 'PhabricatorBritishEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php', 'PhabricatorBuiltinDraftEngine' => 'applications/transactions/draft/PhabricatorBuiltinDraftEngine.php', 'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php', @@ -2253,7 +2247,6 @@ phutil_register_library_map(array( 'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php', 'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php', 'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php', - 'PhabricatorCampfireProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php', 'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php', 'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php', 'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php', @@ -2396,6 +2389,7 @@ phutil_register_library_map(array( 'PhabricatorConpherenceNotificationsSetting' => 'applications/settings/setting/PhabricatorConpherenceNotificationsSetting.php', 'PhabricatorConpherencePreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php', 'PhabricatorConpherenceProfileMenuItem' => 'applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php', + 'PhabricatorConpherenceSoundSetting' => 'applications/settings/setting/PhabricatorConpherenceSoundSetting.php', 'PhabricatorConpherenceThreadPHIDType' => 'applications/conpherence/phid/PhabricatorConpherenceThreadPHIDType.php', 'PhabricatorConpherenceWidgetVisibleSetting' => 'applications/settings/setting/PhabricatorConpherenceWidgetVisibleSetting.php', 'PhabricatorConsoleApplication' => 'applications/console/application/PhabricatorConsoleApplication.php', @@ -2762,6 +2756,7 @@ phutil_register_library_map(array( 'PhabricatorFileDAO' => 'applications/files/storage/PhabricatorFileDAO.php', 'PhabricatorFileDataController' => 'applications/files/controller/PhabricatorFileDataController.php', 'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php', + 'PhabricatorFileDeleteTransaction' => 'applications/files/xaction/PhabricatorFileDeleteTransaction.php', 'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php', 'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php', 'PhabricatorFileEditEngine' => 'applications/files/editor/PhabricatorFileEditEngine.php', @@ -2780,6 +2775,7 @@ phutil_register_library_map(array( 'PhabricatorFileLightboxController' => 'applications/files/controller/PhabricatorFileLightboxController.php', 'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php', 'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php', + 'PhabricatorFileNameNgrams' => 'applications/files/storage/PhabricatorFileNameNgrams.php', 'PhabricatorFileNameTransaction' => 'applications/files/xaction/PhabricatorFileNameTransaction.php', 'PhabricatorFileQuery' => 'applications/files/query/PhabricatorFileQuery.php', 'PhabricatorFileROT13StorageFormat' => 'applications/files/format/PhabricatorFileROT13StorageFormat.php', @@ -2910,7 +2906,6 @@ phutil_register_library_map(array( 'PhabricatorHovercardEngineExtensionModule' => 'applications/search/engineextension/PhabricatorHovercardEngineExtensionModule.php', 'PhabricatorIDsSearchEngineExtension' => 'applications/search/engineextension/PhabricatorIDsSearchEngineExtension.php', 'PhabricatorIDsSearchField' => 'applications/search/field/PhabricatorIDsSearchField.php', - 'PhabricatorIRCProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php', 'PhabricatorIconDatasource' => 'applications/files/typeahead/PhabricatorIconDatasource.php', 'PhabricatorIconRemarkupRule' => 'applications/macro/markup/PhabricatorIconRemarkupRule.php', 'PhabricatorIconSet' => 'applications/files/iconset/PhabricatorIconSet.php', @@ -2924,6 +2919,7 @@ phutil_register_library_map(array( 'PhabricatorIndexEngine' => 'applications/search/index/PhabricatorIndexEngine.php', 'PhabricatorIndexEngineExtension' => 'applications/search/index/PhabricatorIndexEngineExtension.php', 'PhabricatorIndexEngineExtensionModule' => 'applications/search/index/PhabricatorIndexEngineExtensionModule.php', + 'PhabricatorIndexableInterface' => 'applications/search/interface/PhabricatorIndexableInterface.php', 'PhabricatorInfrastructureTestCase' => '__tests__/PhabricatorInfrastructureTestCase.php', 'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php', 'PhabricatorInlineCommentInterface' => 'infrastructure/diff/interface/PhabricatorInlineCommentInterface.php', @@ -3445,7 +3441,6 @@ phutil_register_library_map(array( 'PhabricatorPhurlURL' => 'applications/phurl/storage/PhabricatorPhurlURL.php', 'PhabricatorPhurlURLAccessController' => 'applications/phurl/controller/PhabricatorPhurlURLAccessController.php', 'PhabricatorPhurlURLAliasTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLAliasTransaction.php', - 'PhabricatorPhurlURLCommentController' => 'applications/phurl/controller/PhabricatorPhurlURLCommentController.php', 'PhabricatorPhurlURLCreateCapability' => 'applications/phurl/capability/PhabricatorPhurlURLCreateCapability.php', 'PhabricatorPhurlURLDatasource' => 'applications/phurl/typeahead/PhabricatorPhurlURLDatasource.php', 'PhabricatorPhurlURLDescriptionTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLDescriptionTransaction.php', @@ -3643,7 +3638,6 @@ phutil_register_library_map(array( 'PhabricatorProjectsSearchEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php', 'PhabricatorProjectsWatchersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsWatchersSearchEngineAttachment.php', 'PhabricatorPronounSetting' => 'applications/settings/setting/PhabricatorPronounSetting.php', - 'PhabricatorProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php', 'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php', 'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php', 'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php', @@ -3967,7 +3961,6 @@ phutil_register_library_map(array( 'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php', 'PhabricatorStorageSchemaSpec' => 'infrastructure/storage/schema/PhabricatorStorageSchemaSpec.php', 'PhabricatorStorageSetupCheck' => 'applications/config/check/PhabricatorStorageSetupCheck.php', - 'PhabricatorStreamingProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php', 'PhabricatorStringListEditField' => 'applications/transactions/editfield/PhabricatorStringListEditField.php', 'PhabricatorStringSetting' => 'applications/settings/setting/PhabricatorStringSetting.php', 'PhabricatorSubmitEditField' => 'applications/transactions/editfield/PhabricatorSubmitEditField.php', @@ -5093,12 +5086,13 @@ phutil_register_library_map(array( 'ConpherenceParticipantCountQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceParticipantView' => 'AphrontView', - 'ConpherenceParticipationStatus' => 'ConpherenceConstants', 'ConpherenceQueryThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod', 'ConpherenceQueryTransactionConduitAPIMethod' => 'ConpherenceConduitAPIMethod', 'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler', 'ConpherenceRoomListController' => 'ConpherenceController', 'ConpherenceRoomPictureController' => 'ConpherenceController', + 'ConpherenceRoomPreferencesController' => 'ConpherenceController', + 'ConpherenceRoomSettings' => 'ConpherenceConstants', 'ConpherenceRoomTestCase' => 'ConpherenceTestCase', 'ConpherenceSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'ConpherenceTestCase' => 'PhabricatorTestCase', @@ -5116,6 +5110,7 @@ phutil_register_library_map(array( 'ConpherenceThreadListView' => 'AphrontView', 'ConpherenceThreadMailReceiver' => 'PhabricatorObjectMailReceiver', 'ConpherenceThreadMembersPolicyRule' => 'PhabricatorPolicyRule', + 'ConpherenceThreadParticipantsTransaction' => 'ConpherenceThreadTransactionType', 'ConpherenceThreadPictureTransaction' => 'ConpherenceThreadTransactionType', 'ConpherenceThreadQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'ConpherenceThreadRemarkupRule' => 'PhabricatorObjectRemarkupRule', @@ -5144,6 +5139,7 @@ phutil_register_library_map(array( 'DarkConsoleEventPlugin' => 'DarkConsolePlugin', 'DarkConsoleEventPluginAPI' => 'PhabricatorEventListener', 'DarkConsolePlugin' => 'Phobject', + 'DarkConsoleRealtimePlugin' => 'DarkConsolePlugin', 'DarkConsoleRequestPlugin' => 'DarkConsolePlugin', 'DarkConsoleServicesPlugin' => 'DarkConsolePlugin', 'DarkConsoleStartupPlugin' => 'DarkConsolePlugin', @@ -5974,6 +5970,7 @@ phutil_register_library_map(array( 'FileAllocateConduitAPIMethod' => 'FileConduitAPIMethod', 'FileConduitAPIMethod' => 'ConduitAPIMethod', 'FileCreateMailReceiver' => 'PhabricatorMailReceiver', + 'FileDeletionWorker' => 'PhabricatorWorker', 'FileDownloadConduitAPIMethod' => 'FileConduitAPIMethod', 'FileInfoConduitAPIMethod' => 'FileConduitAPIMethod', 'FileMailReceiver' => 'PhabricatorObjectMailReceiver', @@ -6838,10 +6835,13 @@ phutil_register_library_map(array( 'PhabricatorAphrontViewTestCase' => 'PhabricatorTestCase', 'PhabricatorAppSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorApplication' => array( - 'Phobject', + 'PhabricatorLiskDAO', 'PhabricatorPolicyInterface', + 'PhabricatorApplicationTransactionInterface', ), 'PhabricatorApplicationApplicationPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorApplicationApplicationTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorApplicationApplicationTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorApplicationConfigOptions' => 'Phobject', 'PhabricatorApplicationConfigurationPanel' => 'Phobject', 'PhabricatorApplicationConfigurationPanelTestCase' => 'PhabricatorTestCase', @@ -6853,6 +6853,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorApplicationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorApplicationSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorApplicationSearchController' => 'PhabricatorSearchBaseController', 'PhabricatorApplicationSearchEngine' => 'Phobject', 'PhabricatorApplicationSearchEngineTestCase' => 'PhabricatorTestCase', @@ -6893,6 +6894,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery', 'PhabricatorApplicationTransactionTextDiffDetailView' => 'AphrontView', 'PhabricatorApplicationTransactionTransactionPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorApplicationTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorApplicationTransactionValidationError' => 'Phobject', 'PhabricatorApplicationTransactionValidationException' => 'Exception', 'PhabricatorApplicationTransactionValidationResponse' => 'AphrontProxyResponse', @@ -7130,20 +7132,6 @@ phutil_register_library_map(array( 'PhabricatorBoardRenderingEngine' => 'Phobject', 'PhabricatorBoardResponseEngine' => 'Phobject', 'PhabricatorBoolEditField' => 'PhabricatorEditField', - 'PhabricatorBot' => 'PhabricatorDaemon', - 'PhabricatorBotChannel' => 'PhabricatorBotTarget', - 'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler', - 'PhabricatorBotFeedNotificationHandler' => 'PhabricatorBotHandler', - 'PhabricatorBotFlowdockProtocolAdapter' => 'PhabricatorStreamingProtocolAdapter', - 'PhabricatorBotHandler' => 'Phobject', - 'PhabricatorBotLogHandler' => 'PhabricatorBotHandler', - 'PhabricatorBotMacroHandler' => 'PhabricatorBotHandler', - 'PhabricatorBotMessage' => 'Phobject', - 'PhabricatorBotObjectNameHandler' => 'PhabricatorBotHandler', - 'PhabricatorBotSymbolHandler' => 'PhabricatorBotHandler', - 'PhabricatorBotTarget' => 'Phobject', - 'PhabricatorBotUser' => 'PhabricatorBotTarget', - 'PhabricatorBotWhatsNewHandler' => 'PhabricatorBotHandler', 'PhabricatorBritishEnglishTranslation' => 'PhutilTranslation', 'PhabricatorBuiltinDraftEngine' => 'PhabricatorDraftEngine', 'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList', @@ -7338,7 +7326,6 @@ phutil_register_library_map(array( 'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec', - 'PhabricatorCampfireProtocolAdapter' => 'PhabricatorStreamingProtocolAdapter', 'PhabricatorCelerityApplication' => 'PhabricatorApplication', 'PhabricatorCelerityTestCase' => 'PhabricatorTestCase', 'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase', @@ -7499,6 +7486,7 @@ phutil_register_library_map(array( 'PhabricatorConpherenceNotificationsSetting' => 'PhabricatorSelectSetting', 'PhabricatorConpherencePreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorConpherenceProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorConpherenceSoundSetting' => 'PhabricatorSelectSetting', 'PhabricatorConpherenceThreadPHIDType' => 'PhabricatorPHIDType', 'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting', 'PhabricatorConsoleApplication' => 'PhabricatorApplication', @@ -7906,6 +7894,8 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorConduitResultInterface', + 'PhabricatorIndexableInterface', + 'PhabricatorNgramsInterface', ), 'PhabricatorFileAES256StorageFormat' => 'PhabricatorFileStorageFormat', 'PhabricatorFileBundleLoader' => 'Phobject', @@ -7924,6 +7914,7 @@ phutil_register_library_map(array( 'PhabricatorFileDAO' => 'PhabricatorLiskDAO', 'PhabricatorFileDataController' => 'PhabricatorFileController', 'PhabricatorFileDeleteController' => 'PhabricatorFileController', + 'PhabricatorFileDeleteTransaction' => 'PhabricatorFileTransactionType', 'PhabricatorFileDropUploadController' => 'PhabricatorFileController', 'PhabricatorFileEditController' => 'PhabricatorFileController', 'PhabricatorFileEditEngine' => 'PhabricatorEditEngine', @@ -7952,6 +7943,7 @@ phutil_register_library_map(array( 'PhabricatorFileLightboxController' => 'PhabricatorFileController', 'PhabricatorFileLinkView' => 'AphrontTagView', 'PhabricatorFileListController' => 'PhabricatorFileController', + 'PhabricatorFileNameNgrams' => 'PhabricatorSearchNgrams', 'PhabricatorFileNameTransaction' => 'PhabricatorFileTransactionType', 'PhabricatorFileQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorFileROT13StorageFormat' => 'PhabricatorFileStorageFormat', @@ -8026,6 +8018,7 @@ phutil_register_library_map(array( 'PhabricatorFulltextEngineExtension' => 'Phobject', 'PhabricatorFulltextEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorFulltextIndexEngineExtension' => 'PhabricatorIndexEngineExtension', + 'PhabricatorFulltextInterface' => 'PhabricatorIndexableInterface', 'PhabricatorFulltextResultSet' => 'Phobject', 'PhabricatorFulltextStorageEngine' => 'Phobject', 'PhabricatorFulltextToken' => 'Phobject', @@ -8089,7 +8082,6 @@ phutil_register_library_map(array( 'PhabricatorHovercardEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorIDsSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorIDsSearchField' => 'PhabricatorSearchField', - 'PhabricatorIRCProtocolAdapter' => 'PhabricatorProtocolAdapter', 'PhabricatorIconDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorIconRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorIconSet' => 'Phobject', @@ -8300,6 +8292,7 @@ phutil_register_library_map(array( 'PhabricatorNavigationRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorNeverTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorNgramsIndexEngineExtension' => 'PhabricatorIndexEngineExtension', + 'PhabricatorNgramsInterface' => 'PhabricatorIndexableInterface', 'PhabricatorNotificationBuilder' => 'Phobject', 'PhabricatorNotificationClearController' => 'PhabricatorNotificationController', 'PhabricatorNotificationClient' => 'Phobject', @@ -8711,7 +8704,6 @@ phutil_register_library_map(array( ), 'PhabricatorPhurlURLAccessController' => 'PhabricatorPhurlController', 'PhabricatorPhurlURLAliasTransaction' => 'PhabricatorPhurlURLTransactionType', - 'PhabricatorPhurlURLCommentController' => 'PhabricatorPhurlController', 'PhabricatorPhurlURLCreateCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPhurlURLDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorPhurlURLDescriptionTransaction' => 'PhabricatorPhurlURLTransactionType', @@ -8947,7 +8939,6 @@ phutil_register_library_map(array( 'PhabricatorProjectsSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorProjectsWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorPronounSetting' => 'PhabricatorSelectSetting', - 'PhabricatorProtocolAdapter' => 'Phobject', 'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorQuery' => 'Phobject', 'PhabricatorQueryConstraint' => 'Phobject', @@ -9351,7 +9342,6 @@ phutil_register_library_map(array( 'PhabricatorStoragePatch' => 'Phobject', 'PhabricatorStorageSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorStorageSetupCheck' => 'PhabricatorSetupCheck', - 'PhabricatorStreamingProtocolAdapter' => 'PhabricatorProtocolAdapter', 'PhabricatorStringListEditField' => 'PhabricatorEditField', 'PhabricatorStringSetting' => 'PhabricatorSetting', 'PhabricatorSubmitEditField' => 'PhabricatorEditField', diff --git a/src/aphront/response/AphrontFileResponse.php b/src/aphront/response/AphrontFileResponse.php index 9699c49ad4..6bae4c808f 100644 --- a/src/aphront/response/AphrontFileResponse.php +++ b/src/aphront/response/AphrontFileResponse.php @@ -139,4 +139,29 @@ final class AphrontFileResponse extends AphrontResponse { return $this->getCompressResponse(); } + public function parseHTTPRange($range) { + $begin = null; + $end = null; + + $matches = null; + if (preg_match('/^bytes=(\d+)-(\d*)$/', $range, $matches)) { + // Note that the "Range" header specifies bytes differently than + // we do internally: the range 0-1 has 2 bytes (byte 0 and byte 1). + $begin = (int)$matches[1]; + + // The "Range" may be "200-299" or "200-", meaning "until end of file". + if (strlen($matches[2])) { + $range_end = (int)$matches[2]; + $end = $range_end + 1; + } else { + $range_end = null; + } + + $this->setHTTPResponseCode(206); + $this->setRange($begin, $range_end); + } + + return array($begin, $end); + } + } diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index 93b738d9e0..29f5ed35be 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -9,8 +9,10 @@ * @task meta Application Management */ abstract class PhabricatorApplication - extends Phobject - implements PhabricatorPolicyInterface { + extends PhabricatorLiskDAO + implements + PhabricatorPolicyInterface, + PhabricatorApplicationTransactionInterface { const GROUP_CORE = 'core'; const GROUP_UTILITIES = 'util'; @@ -26,6 +28,30 @@ abstract class PhabricatorApplication ); } + final public function getApplicationName() { + return 'application'; + } + + final public function getTableName() { + return 'application_application'; + } + + final protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + ) + parent::getConfiguration(); + } + + final public function generatePHID() { + return $this->getPHID(); + } + + final public function save() { + // When "save()" is called on applications, we just return without + // actually writing anything to the database. + return $this; + } + /* -( Application Information )-------------------------------------------- */ @@ -613,4 +639,25 @@ abstract class PhabricatorApplication ); } +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhutilMethodNotImplementedException(pht('Coming Soon!')); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorApplicationApplicationTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + + return $timeline; + } } diff --git a/src/applications/celerity/controller/CelerityResourceController.php b/src/applications/celerity/controller/CelerityResourceController.php index 0f1478ec5c..730e5ddc90 100644 --- a/src/applications/celerity/controller/CelerityResourceController.php +++ b/src/applications/celerity/controller/CelerityResourceController.php @@ -104,9 +104,30 @@ abstract class CelerityResourceController extends PhabricatorController { } $response = id(new AphrontFileResponse()) - ->setContent($data) - ->setMimeType($type_map[$type]) - ->setCompressResponse(true); + ->setMimeType($type_map[$type]); + + $range = AphrontRequest::getHTTPHeader('Range'); + + if (strlen($range)) { + $response->setContentLength(strlen($data)); + + list($range_begin, $range_end) = $response->parseHTTPRange($range); + + if ($range_begin !== null) { + if ($range_end !== null) { + $data = substr($data, $range_begin, ($range_end - $range_begin)); + } else { + $data = substr($data, $range_begin); + } + } + + $response->setContentIterator(array($data)); + } else { + $response + ->setContent($data) + ->setCompressResponse(true); + } + // NOTE: This is a piece of magic required to make WOFF fonts work in // Firefox and IE. Possibly we should generalize this more. diff --git a/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php b/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php index 47ab022831..cc0cc2bf45 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php @@ -103,10 +103,20 @@ final class PhabricatorConfigClusterNotificationsController new PhutilNumber(idx($details, 'messages.in')), new PhutilNumber(idx($details, 'messages.out'))); + if (idx($details, 'history.size')) { + $history = pht( + '%s Held / %sms', + new PhutilNumber(idx($details, 'history.size')), + new PhutilNumber(idx($details, 'history.age'))); + } else { + $history = pht('No Messages'); + } + } else { $uptime = null; $clients = null; $stats = null; + $history = null; } $status_view = array( @@ -126,6 +136,7 @@ final class PhabricatorConfigClusterNotificationsController $uptime, $clients, $stats, + $history, $messages, ); } @@ -143,6 +154,7 @@ final class PhabricatorConfigClusterNotificationsController pht('Uptime'), pht('Clients'), pht('Messages'), + pht('History'), null, )) ->setColumnClasses( @@ -155,6 +167,7 @@ final class PhabricatorConfigClusterNotificationsController null, null, null, + null, 'wide', )); diff --git a/src/applications/config/schema/PhabricatorConfigDatabaseSchema.php b/src/applications/config/schema/PhabricatorConfigDatabaseSchema.php index 216a778e56..b9b407ee70 100644 --- a/src/applications/config/schema/PhabricatorConfigDatabaseSchema.php +++ b/src/applications/config/schema/PhabricatorConfigDatabaseSchema.php @@ -11,6 +11,13 @@ final class PhabricatorConfigDatabaseSchema public function addTable(PhabricatorConfigTableSchema $table) { $key = $table->getName(); if (isset($this->tables[$key])) { + + if ($key == 'application_application') { + // NOTE: This is a terrible hack to allow Application subclasses to + // extend LiskDAO so we can apply transactions to them. + return $this; + } + throw new Exception( pht('Trying to add duplicate table "%s"!', $key)); } diff --git a/src/applications/conpherence/__tests__/ConpherenceRoomTestCase.php b/src/applications/conpherence/__tests__/ConpherenceRoomTestCase.php index 85c4b9c1f1..7ef908c315 100644 --- a/src/applications/conpherence/__tests__/ConpherenceRoomTestCase.php +++ b/src/applications/conpherence/__tests__/ConpherenceRoomTestCase.php @@ -107,7 +107,8 @@ final class ConpherenceRoomTestCase extends ConpherenceTestCase { $xactions = array(); $xactions[] = id(new ConpherenceTransaction()) - ->setTransactionType(ConpherenceTransaction::TYPE_PARTICIPANTS) + ->setTransactionType( + ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE) ->setNewValue(array('+' => $participant_phids)); $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( diff --git a/src/applications/conpherence/__tests__/ConpherenceTestCase.php b/src/applications/conpherence/__tests__/ConpherenceTestCase.php index 1ad87d8af8..6c8397c7d8 100644 --- a/src/applications/conpherence/__tests__/ConpherenceTestCase.php +++ b/src/applications/conpherence/__tests__/ConpherenceTestCase.php @@ -9,7 +9,8 @@ abstract class ConpherenceTestCase extends PhabricatorTestCase { $xactions = array( id(new ConpherenceTransaction()) - ->setTransactionType(ConpherenceTransaction::TYPE_PARTICIPANTS) + ->setTransactionType( + ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE) ->setNewValue(array('+' => $participant_phids)), ); $editor = id(new ConpherenceEditor()) @@ -26,7 +27,8 @@ abstract class ConpherenceTestCase extends PhabricatorTestCase { $xactions = array( id(new ConpherenceTransaction()) - ->setTransactionType(ConpherenceTransaction::TYPE_PARTICIPANTS) + ->setTransactionType( + ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE) ->setNewValue(array('-' => $participant_phids)), ); $editor = id(new ConpherenceEditor()) diff --git a/src/applications/conpherence/application/PhabricatorConpherenceApplication.php b/src/applications/conpherence/application/PhabricatorConpherenceApplication.php index 5faaf40b9c..e969b48447 100644 --- a/src/applications/conpherence/application/PhabricatorConpherenceApplication.php +++ b/src/applications/conpherence/application/PhabricatorConpherenceApplication.php @@ -55,6 +55,8 @@ final class PhabricatorConpherenceApplication extends PhabricatorApplication { => 'ConpherenceNotificationPanelController', 'participant/(?P[1-9]\d*)/' => 'ConpherenceParticipantController', + 'preferences/(?P[1-9]\d*)/' + => 'ConpherenceRoomPreferencesController', 'update/(?P[1-9]\d*)/' => 'ConpherenceUpdateController', ), diff --git a/src/applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php b/src/applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php index fd602058a3..0d177e502c 100644 --- a/src/applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php +++ b/src/applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php @@ -36,8 +36,7 @@ final class ConpherenceQueryThreadConduitAPIMethod $offset = $request->getValue('offset'); $query = id(new ConpherenceThreadQuery()) - ->setViewer($user) - ->needParticipantCache(true); + ->setViewer($user); if ($ids) { $conpherences = $query @@ -57,7 +56,7 @@ final class ConpherenceQueryThreadConduitAPIMethod ->setLimit($limit) ->setOffset($offset) ->execute(); - $conpherence_phids = array_keys($participation); + $conpherence_phids = mpull($participation, 'getConpherencePHID'); $query->withPHIDs($conpherence_phids); $conpherences = $query->execute(); $conpherences = array_select_keys($conpherences, $conpherence_phids); diff --git a/src/applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php b/src/applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php index c92cc464ed..e7d927905d 100644 --- a/src/applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php +++ b/src/applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php @@ -69,7 +69,7 @@ final class ConpherenceUpdateThreadConduitAPIMethod if ($add_participant_phids) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( - ConpherenceTransaction::TYPE_PARTICIPANTS) + ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE) ->setNewValue(array('+' => $add_participant_phids)); } if ($remove_participant_phid) { @@ -78,7 +78,7 @@ final class ConpherenceUpdateThreadConduitAPIMethod } $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( - ConpherenceTransaction::TYPE_PARTICIPANTS) + ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE) ->setNewValue(array('-' => array($remove_participant_phid))); } if ($title) { diff --git a/src/applications/conpherence/constants/ConpherenceParticipationStatus.php b/src/applications/conpherence/constants/ConpherenceParticipationStatus.php deleted file mode 100644 index e8f22620cb..0000000000 --- a/src/applications/conpherence/constants/ConpherenceParticipationStatus.php +++ /dev/null @@ -1,8 +0,0 @@ - array( + 'name' => pht('No Sound'), + 'rsrc' => '', + ), + 'alert' => array( + 'name' => pht('Alert'), + 'rsrc' => celerity_get_resource_uri('/rsrc/audio/basic/alert.mp3'), + ), + 'bing' => array( + 'name' => pht('Bing'), + 'rsrc' => celerity_get_resource_uri('/rsrc/audio/basic/bing.mp3'), + ), + 'pock' => array( + 'name' => pht('Pock'), + 'rsrc' => celerity_get_resource_uri('/rsrc/audio/basic/pock.mp3'), + ), + 'tap' => array( + 'name' => pht('Tap'), + 'rsrc' => celerity_get_resource_uri('/rsrc/audio/basic/tap.mp3'), + ), + 'ting' => array( + 'name' => pht('Ting'), + 'rsrc' => celerity_get_resource_uri('/rsrc/audio/basic/ting.mp3'), + ), + ); + } + + public static function getDropdownSoundMap() { + $map = self::getSoundMap(); + return ipull($map, 'name'); + } + + public static function getThemeMap() { + return array( + self::COLOR_LIGHT => pht('Light'), + self::COLOR_BLUE => pht('Blue'), + self::COLOR_INDIGO => pht('Indigo'), + self::COLOR_PEACH => pht('Peach'), + self::COLOR_GREEN => pht('Green'), + self::COLOR_PINK => pht('Pink'), + ); + } + + public static function getThemeClass($theme) { + return 'conpherence-theme-'.$theme; + } + + +} diff --git a/src/applications/conpherence/constants/ConpherenceUpdateActions.php b/src/applications/conpherence/constants/ConpherenceUpdateActions.php index e2d788f5c6..a2b97f9c6a 100644 --- a/src/applications/conpherence/constants/ConpherenceUpdateActions.php +++ b/src/applications/conpherence/constants/ConpherenceUpdateActions.php @@ -8,7 +8,6 @@ final class ConpherenceUpdateActions extends ConpherenceConstants { const JOIN_ROOM = 'join_room'; const ADD_PERSON = 'add_person'; const REMOVE_PERSON = 'remove_person'; - const NOTIFICATIONS = 'notifications'; const ADD_STATUS = 'add_status'; const LOAD = 'load'; } diff --git a/src/applications/conpherence/controller/ConpherenceColumnViewController.php b/src/applications/conpherence/controller/ConpherenceColumnViewController.php index aa4f94edfc..3bd0c6b111 100644 --- a/src/applications/conpherence/controller/ConpherenceColumnViewController.php +++ b/src/applications/conpherence/controller/ConpherenceColumnViewController.php @@ -67,7 +67,7 @@ final class ConpherenceColumnViewController extends $transactions = $conpherence->getTransactions(); $latest_transaction = head($transactions); $write_guard = AphrontWriteGuard::beginScopedUnguardedWrites(); - $participant->markUpToDate($conpherence, $latest_transaction); + $participant->markUpToDate($conpherence); unset($write_guard); $draft = PhabricatorDraft::newFromUserAndKey( diff --git a/src/applications/conpherence/controller/ConpherenceController.php b/src/applications/conpherence/controller/ConpherenceController.php index 09708b7148..f7fd7ef1b4 100644 --- a/src/applications/conpherence/controller/ConpherenceController.php +++ b/src/applications/conpherence/controller/ConpherenceController.php @@ -54,19 +54,29 @@ abstract class ConpherenceController extends PhabricatorController { } protected function buildHeaderPaneContent( - ConpherenceThread $conpherence, - array $policy_objects) { - assert_instances_of($policy_objects, 'PhabricatorPolicy'); + ConpherenceThread $conpherence) { $viewer = $this->getViewer(); $header = null; + $id = $conpherence->getID(); - if ($conpherence->getID()) { + if ($id) { $data = $conpherence->getDisplayData($this->getViewer()); + $header = id(new PHUIHeaderView()) + ->setViewer($viewer) ->setHeader($data['title']) - ->setSubheader($data['topic']) + ->setPolicyObject($conpherence) ->setImage($data['image']); + if (strlen($data['topic'])) { + $topic = id(new PHUITagView()) + ->setName($data['topic']) + ->setShade(PHUITagView::COLOR_VIOLET) + ->setType(PHUITagView::TYPE_SHADE) + ->addClass('conpherence-header-topic'); + $header->addTag($topic); + } + $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $conpherence, @@ -74,15 +84,14 @@ abstract class ConpherenceController extends PhabricatorController { if ($can_edit) { $header->setImageURL( - $this->getApplicationURI('picture/'.$conpherence->getID().'/')); + $this->getApplicationURI("picture/{$id}/")); } $participating = $conpherence->getParticipantIfExists($viewer->getPHID()); $header->addActionItem( id(new PHUIIconCircleView()) - ->setHref( - $this->getApplicationURI('update/'.$conpherence->getID()).'/') + ->setHref($this->getApplicationURI("update/{$id}/")) ->setIcon('fa-pencil') ->addClass('hide-on-device') ->setColor('violet') @@ -90,9 +99,7 @@ abstract class ConpherenceController extends PhabricatorController { $header->addActionItem( id(new PHUIIconCircleView()) - ->setHref( - $this->getApplicationURI('update/'.$conpherence->getID()).'/'. - '?action='.ConpherenceUpdateActions::NOTIFICATIONS) + ->setHref($this->getApplicationURI("preferences/{$id}/")) ->setIcon('fa-gear') ->addClass('hide-on-device') ->setColor('pink') @@ -127,7 +134,7 @@ abstract class ConpherenceController extends PhabricatorController { if (!$participating) { $action = ConpherenceUpdateActions::JOIN_ROOM; - $uri = $this->getApplicationURI('update/'.$conpherence->getID().'/'); + $uri = $this->getApplicationURI("update/{$id}/"); $button = phutil_tag( 'button', array( diff --git a/src/applications/conpherence/controller/ConpherenceListController.php b/src/applications/conpherence/controller/ConpherenceListController.php index 00968af576..d993a1bd5e 100644 --- a/src/applications/conpherence/controller/ConpherenceListController.php +++ b/src/applications/conpherence/controller/ConpherenceListController.php @@ -34,7 +34,7 @@ final class ConpherenceListController extends ConpherenceController { $title = pht('Conpherence'); $conpherence = null; - $limit = (ConpherenceThreadListView::SEE_MORE_LIMIT * 2) + 1; + $limit = ConpherenceThreadListView::SEE_ALL_LIMIT + 1; $all_participation = array(); $mode = $this->determineMode(); @@ -64,7 +64,7 @@ final class ConpherenceListController extends ConpherenceController { } // check to see if the loaded conpherence is going to show up - // within the SEE_MORE_LIMIT amount of conpherences. + // within the SEE_ALL_LIMIT amount of conpherences. // If its not there, then we just pre-pend it as the "first" // conpherence so folks have a navigation item in the menu. $count = 0; @@ -75,7 +75,7 @@ final class ConpherenceListController extends ConpherenceController { break; } $count++; - if ($count > ConpherenceThreadListView::SEE_MORE_LIMIT) { + if ($count > ConpherenceThreadListView::SEE_ALL_LIMIT) { break; } } @@ -89,11 +89,19 @@ final class ConpherenceListController extends ConpherenceController { default: $data = $this->loadDefaultParticipation($limit); $all_participation = $data['all_participation']; + if ($all_participation) { + $conpherence_id = head($all_participation)->getConpherencePHID(); + $conpherence = id(new ConpherenceThreadQuery()) + ->setViewer($user) + ->withPHIDs(array($conpherence_id)) + ->needProfileImage(true) + ->executeOne(); + } + // If $conpherence is null, NUX state will render break; } - $threads = $this->loadConpherenceThreadData( - $all_participation); + $threads = $this->loadConpherenceThreadData($all_participation); $thread_view = id(new ConpherenceThreadListView()) ->setUser($user) @@ -144,6 +152,7 @@ final class ConpherenceListController extends ConpherenceController { ->withParticipantPHIDs(array($viewer->getPHID())) ->setLimit($limit) ->execute(); + $all_participation = mpull($all_participation, null, 'getConpherencePHID'); return array( 'all_participation' => $all_participation, diff --git a/src/applications/conpherence/controller/ConpherenceNewRoomController.php b/src/applications/conpherence/controller/ConpherenceNewRoomController.php index d70395dbc9..6cbe86aaa2 100644 --- a/src/applications/conpherence/controller/ConpherenceNewRoomController.php +++ b/src/applications/conpherence/controller/ConpherenceNewRoomController.php @@ -23,7 +23,8 @@ final class ConpherenceNewRoomController extends ConpherenceController { $participants[] = $user->getPHID(); $participants = array_unique($participants); $xactions[] = id(new ConpherenceTransaction()) - ->setTransactionType(ConpherenceTransaction::TYPE_PARTICIPANTS) + ->setTransactionType( + ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE) ->setNewValue(array('+' => $participants)); $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(ConpherenceThreadTopicTransaction::TRANSACTIONTYPE) diff --git a/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php b/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php index ed728c6ab7..8458a9d5c4 100644 --- a/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php +++ b/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php @@ -7,12 +7,12 @@ final class ConpherenceNotificationPanelController $user = $request->getUser(); $conpherences = array(); require_celerity_resource('conpherence-notification-css'); - $unread_status = ConpherenceParticipationStatus::BEHIND; $participant_data = id(new ConpherenceParticipantQuery()) ->withParticipantPHIDs(array($user->getPHID())) ->setLimit(5) ->execute(); + $participant_data = mpull($participant_data, null, 'getConpherencePHID'); if ($participant_data) { $conpherences = id(new ConpherenceThreadQuery()) @@ -37,7 +37,7 @@ final class ConpherenceNotificationPanelController 'conpherence-notification', ); - if ($p_data->getParticipationStatus() == $unread_status) { + if (!$p_data->isUpToDate($conpherence)) { $classes[] = 'phabricator-notification-unread'; } $uri = $this->getApplicationURI($conpherence->getID().'/'); @@ -95,7 +95,7 @@ final class ConpherenceNotificationPanelController $unread = id(new ConpherenceParticipantCountQuery()) ->withParticipantPHIDs(array($user->getPHID())) - ->withParticipationStatus($unread_status) + ->withUnread(true) ->execute(); $unread_count = idx($unread, $user->getPHID(), 0); diff --git a/src/applications/conpherence/controller/ConpherenceRoomPreferencesController.php b/src/applications/conpherence/controller/ConpherenceRoomPreferencesController.php new file mode 100644 index 0000000000..c868684389 --- /dev/null +++ b/src/applications/conpherence/controller/ConpherenceRoomPreferencesController.php @@ -0,0 +1,113 @@ +getViewer(); + $conpherence_id = $request->getURIData('id'); + + $conpherence = id(new ConpherenceThreadQuery()) + ->setViewer($viewer) + ->withIDs(array($conpherence_id)) + ->executeOne(); + if (!$conpherence) { + return new Aphront404Response(); + } + + $view_uri = $conpherence->getURI(); + + $participant = $conpherence->getParticipantIfExists($viewer->getPHID()); + if (!$participant) { + if ($viewer->isLoggedIn()) { + $text = pht( + 'Notification settings are available after joining the room.'); + } else { + $text = pht( + 'Notification settings are available after logging in and joining '. + 'the room.'); + } + return $this->newDialog() + ->setTitle(pht('Room Preferences')) + ->addCancelButton($view_uri) + ->appendParagraph($text); + } + + // Save the data and redirect + if ($request->isFormPost()) { + $notifications = $request->getStr('notifications'); + $sounds = $request->getArr('sounds'); + $theme = $request->getStr('theme'); + + $participant->setSettings(array( + 'notifications' => $notifications, + 'sounds' => $sounds, + 'theme' => $theme, + )); + $participant->save(); + + return id(new AphrontRedirectResponse()) + ->setURI($view_uri); + } + + $notification_key = PhabricatorConpherenceNotificationsSetting::SETTINGKEY; + $notification_default = $viewer->getUserSetting($notification_key); + + $sound_key = PhabricatorConpherenceSoundSetting::SETTINGKEY; + $sound_default = $viewer->getUserSetting($sound_key); + + $settings = $participant->getSettings(); + $notifications = idx($settings, 'notifications', $notification_default); + $theme = idx($settings, 'theme', ConpherenceRoomSettings::COLOR_LIGHT); + + $sounds = idx($settings, 'sounds', array()); + $map = PhabricatorConpherenceSoundSetting::getDefaultSound($sound_default); + $receive = idx($sounds, + ConpherenceRoomSettings::SOUND_RECEIVE, + $map[ConpherenceRoomSettings::SOUND_RECEIVE]); + $mention = idx($sounds, + ConpherenceRoomSettings::SOUND_MENTION, + $map[ConpherenceRoomSettings::SOUND_MENTION]); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendControl( + id(new AphrontFormRadioButtonControl()) + ->setLabel(pht('Notify')) + ->addButton( + PhabricatorConpherenceNotificationsSetting::VALUE_CONPHERENCE_EMAIL, + PhabricatorConpherenceNotificationsSetting::getSettingLabel( + PhabricatorConpherenceNotificationsSetting::VALUE_CONPHERENCE_EMAIL), + '') + ->addButton( + PhabricatorConpherenceNotificationsSetting::VALUE_CONPHERENCE_NOTIFY, + PhabricatorConpherenceNotificationsSetting::getSettingLabel( + PhabricatorConpherenceNotificationsSetting::VALUE_CONPHERENCE_NOTIFY), + '') + ->setName('notifications') + ->setValue($notifications)) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('New Message')) + ->setName('sounds['.ConpherenceRoomSettings::SOUND_RECEIVE.']') + ->setOptions(ConpherenceRoomSettings::getDropdownSoundMap()) + ->setValue($receive)) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Theme')) + ->setName('theme') + ->setOptions(ConpherenceRoomSettings::getThemeMap()) + ->setValue($theme)); + + return $this->newDialog() + ->setTitle(pht('Room Preferences')) + ->appendForm($form) + ->addCancelButton($view_uri) + ->addSubmitButton(pht('Save')); + } + +} diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php index 9cc666109c..728d917589 100644 --- a/src/applications/conpherence/controller/ConpherenceUpdateController.php +++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php @@ -24,9 +24,6 @@ final class ConpherenceUpdateController case ConpherenceUpdateActions::METADATA: $needed_capabilities[] = PhabricatorPolicyCapability::CAN_EDIT; break; - case ConpherenceUpdateActions::NOTIFICATIONS: - $need_participants = true; - break; case ConpherenceUpdateActions::LOAD: break; } @@ -61,7 +58,7 @@ final class ConpherenceUpdateController case ConpherenceUpdateActions::JOIN_ROOM: $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( - ConpherenceTransaction::TYPE_PARTICIPANTS) + ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE) ->setNewValue(array('+' => array($user->getPHID()))); $delete_draft = true; $message = $request->getStr('text'); @@ -95,7 +92,7 @@ final class ConpherenceUpdateController if (!empty($person_phids)) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( - ConpherenceTransaction::TYPE_PARTICIPANTS) + ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE) ->setNewValue(array('+' => $person_phids)); } break; @@ -108,22 +105,10 @@ final class ConpherenceUpdateController if ($person_phid) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( - ConpherenceTransaction::TYPE_PARTICIPANTS) + ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE) ->setNewValue(array('-' => array($person_phid))); $response_mode = 'go-home'; } - break; - case ConpherenceUpdateActions::NOTIFICATIONS: - $notifications = $request->getStr('notifications'); - $participant = $conpherence->getParticipantIfExists($user->getPHID()); - if (!$participant) { - return id(new Aphront404Response()); - } - $participant->setSettings(array('notifications' => $notifications)); - $participant->save(); - return id(new AphrontRedirectResponse()) - ->setURI('/'.$conpherence->getMonogram()); - break; case ConpherenceUpdateActions::METADATA: $title = $request->getStr('title'); @@ -217,9 +202,6 @@ final class ConpherenceUpdateController } switch ($action) { - case ConpherenceUpdateActions::NOTIFICATIONS: - $dialog = $this->renderPreferencesDialog($conpherence); - break; case ConpherenceUpdateActions::ADD_PERSON: $dialog = $this->renderAddPersonDialog($conpherence); break; @@ -242,64 +224,6 @@ final class ConpherenceUpdateController } - private function renderPreferencesDialog( - ConpherenceThread $conpherence) { - - $request = $this->getRequest(); - $user = $request->getUser(); - - $participant = $conpherence->getParticipantIfExists($user->getPHID()); - if (!$participant) { - if ($user->isLoggedIn()) { - $text = pht( - 'Notification settings are available after joining the room.'); - } else { - $text = pht( - 'Notification settings are available after logging in and joining '. - 'the room.'); - } - return id(new AphrontDialogView()) - ->setTitle(pht('Room Preferences')) - ->appendParagraph($text); - } - - $notification_key = PhabricatorConpherenceNotificationsSetting::SETTINGKEY; - $notification_default = $user->getUserSetting($notification_key); - - $settings = $participant->getSettings(); - $notifications = idx( - $settings, - 'notifications', - $notification_default); - - $form = id(new AphrontFormView()) - ->setUser($user) - ->setFullWidth(true) - ->appendControl( - id(new AphrontFormRadioButtonControl()) - ->addButton( - PhabricatorConpherenceNotificationsSetting::VALUE_CONPHERENCE_EMAIL, - PhabricatorConpherenceNotificationsSetting::getSettingLabel( - PhabricatorConpherenceNotificationsSetting::VALUE_CONPHERENCE_EMAIL), - '') - ->addButton( - PhabricatorConpherenceNotificationsSetting::VALUE_CONPHERENCE_NOTIFY, - PhabricatorConpherenceNotificationsSetting::getSettingLabel( - PhabricatorConpherenceNotificationsSetting::VALUE_CONPHERENCE_NOTIFY), - '') - ->setName('notifications') - ->setValue($notifications)); - - return id(new AphrontDialogView()) - ->setTitle(pht('Room Preferences')) - ->addHiddenInput('action', 'notifications') - ->addHiddenInput( - 'latest_transaction_id', - $request->getInt('latest_transaction_id')) - ->appendForm($form); - - } - private function renderAddPersonDialog( ConpherenceThread $conpherence) { @@ -360,9 +284,9 @@ final class ConpherenceUpdateController $body[] = pht( 'Are you sure you want to leave this room?'); } else { - $title = pht('Banish User'); + $title = pht('Remove Participant'); $body[] = pht( - 'Banish %s from the realm?', + 'Remove %s from this room?', phutil_tag('strong', array(), $removed_user->getUsername())); } @@ -372,7 +296,7 @@ final class ConpherenceUpdateController 'You will be able to rejoin the room later.'); } else { $body[] = pht( - 'This user will be able to rejoin the room later.'); + 'They will be able to rejoin the room later.'); } } else { if ($is_self) { @@ -387,7 +311,7 @@ final class ConpherenceUpdateController } } else { $body[] = pht( - 'This user will not be able to rejoin the room unless invited '. + 'They will not be able to rejoin the room unless invited '. 'again.'); } } @@ -480,7 +404,6 @@ final class ConpherenceUpdateController $need_transactions = true; break; case ConpherenceUpdateActions::REMOVE_PERSON: - case ConpherenceUpdateActions::NOTIFICATIONS: default: break; @@ -496,6 +419,8 @@ final class ConpherenceUpdateController ->executeOne(); $non_update = false; + $participant = $conpherence->getParticipant($user->getPHID()); + if ($need_transactions && $conpherence->getTransactions()) { $data = ConpherenceTransactionRenderer::renderTransactions( $user, @@ -503,9 +428,7 @@ final class ConpherenceUpdateController $key = PhabricatorConpherenceColumnMinimizeSetting::SETTINGKEY; $minimized = $user->getUserSetting($key); if (!$minimized) { - $participant_obj = $conpherence->getParticipant($user->getPHID()); - $participant_obj - ->markUpToDate($conpherence, $data['latest_transaction']); + $participant->markUpToDate($conpherence); } } else if ($need_transactions) { $non_update = true; @@ -522,18 +445,12 @@ final class ConpherenceUpdateController $people_widget = null; switch ($action) { case ConpherenceUpdateActions::METADATA: - $policy_objects = id(new PhabricatorPolicyQuery()) - ->setViewer($user) - ->setObject($conpherence) - ->execute(); - $header = $this->buildHeaderPaneContent( - $conpherence, - $policy_objects); + $header = $this->buildHeaderPaneContent($conpherence); $header = hsprintf('%s', $header); $nav_item = id(new ConpherenceThreadListView()) ->setUser($user) ->setBaseURI($this->getApplicationURI()) - ->renderSingleThread($conpherence, $policy_objects); + ->renderThreadItem($conpherence); $nav_item = hsprintf('%s', $nav_item); break; case ConpherenceUpdateActions::ADD_PERSON: @@ -544,7 +461,6 @@ final class ConpherenceUpdateController $people_widget = hsprintf('%s', $people_widget->render()); break; case ConpherenceUpdateActions::REMOVE_PERSON: - case ConpherenceUpdateActions::NOTIFICATIONS: default: break; } @@ -552,6 +468,11 @@ final class ConpherenceUpdateController $dropdown_query = id(new AphlictDropdownDataQuery()) ->setViewer($user); $dropdown_query->execute(); + + $sounds = $this->getSoundForParticipant($user, $participant); + $receive_sound = $sounds[ConpherenceRoomSettings::SOUND_RECEIVE]; + $mention_sound = $sounds[ConpherenceRoomSettings::SOUND_MENTION]; + $content = array( 'non_update' => $non_update, 'transactions' => hsprintf('%s', $rendered_transactions), @@ -565,9 +486,40 @@ final class ConpherenceUpdateController $dropdown_query->getNotificationData(), $dropdown_query->getConpherenceData(), ), + 'sound' => array( + 'receive' => $receive_sound, + 'mention' => $mention_sound, + ), ); return $content; } + protected function getSoundForParticipant( + PhabricatorUser $user, + ConpherenceParticipant $participant) { + + $sound_key = PhabricatorConpherenceSoundSetting::SETTINGKEY; + $sound_default = $user->getUserSetting($sound_key); + + $settings = $participant->getSettings(); + $sounds = idx($settings, 'sounds', array()); + $map = PhabricatorConpherenceSoundSetting::getDefaultSound($sound_default); + + $receive = idx($sounds, + ConpherenceRoomSettings::SOUND_RECEIVE, + $map[ConpherenceRoomSettings::SOUND_RECEIVE]); + $mention = idx($sounds, + ConpherenceRoomSettings::SOUND_MENTION, + $map[ConpherenceRoomSettings::SOUND_MENTION]); + + $sound_map = ConpherenceRoomSettings::getSoundMap(); + + return array( + ConpherenceRoomSettings::SOUND_RECEIVE => $sound_map[$receive]['rsrc'], + ConpherenceRoomSettings::SOUND_MENTION => $sound_map[$mention]['rsrc'], + ); + + } + } diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index 7a152a1e2e..792a7f8182 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -55,15 +55,15 @@ final class ConpherenceViewController extends } $this->setConpherence($conpherence); - $transactions = $this->getNeededTransactions( - $conpherence, - $old_message_id); - $latest_transaction = head($transactions); $participant = $conpherence->getParticipantIfExists($user->getPHID()); + $theme = ConpherenceRoomSettings::COLOR_LIGHT; + if ($participant) { + $settings = $participant->getSettings(); + $theme = idx($settings, 'theme', ConpherenceRoomSettings::COLOR_LIGHT); if (!$participant->isUpToDate($conpherence)) { $write_guard = AphrontWriteGuard::beginScopedUnguardedWrites(); - $participant->markUpToDate($conpherence, $latest_transaction); + $participant->markUpToDate($conpherence); $user->clearCacheData(PhabricatorUserMessageCountCacheType::KEY_COUNT); unset($write_guard); } @@ -82,11 +82,7 @@ final class ConpherenceViewController extends $form = null; $content = array('transactions' => $messages); } else { - $policy_objects = id(new PhabricatorPolicyQuery()) - ->setViewer($user) - ->setObject($conpherence) - ->execute(); - $header = $this->buildHeaderPaneContent($conpherence, $policy_objects); + $header = $this->buildHeaderPaneContent($conpherence); $search = $this->buildSearchForm(); $form = $this->renderFormContent(); $content = array( @@ -126,6 +122,7 @@ final class ConpherenceViewController extends ->setSearch($search) ->setMessages($messages) ->setReplyForm($form) + ->setTheme($theme) ->setLatestTransactionID($data['latest_transaction_id']) ->setRole('thread'); @@ -203,33 +200,6 @@ final class ConpherenceViewController extends } } - private function getNeededTransactions( - ConpherenceThread $conpherence, - $message_id) { - - if ($message_id) { - $newer_transactions = $conpherence->getTransactions(); - $query = id(new ConpherenceTransactionQuery()) - ->setViewer($this->getRequest()->getUser()) - ->withObjectPHIDs(array($conpherence->getPHID())) - ->setAfterID($message_id) - ->needHandles(true) - ->setLimit(self::OLDER_FETCH_LIMIT); - $older_transactions = $query->execute(); - $handles = array(); - foreach ($older_transactions as $transaction) { - $handles += $transaction->getHandles(); - } - $conpherence->attachHandles($conpherence->getHandles() + $handles); - $transactions = array_merge($newer_transactions, $older_transactions); - $conpherence->attachTransactions($transactions); - } else { - $transactions = $conpherence->getTransactions(); - } - - return $transactions; - } - private function getMainQueryLimit() { $request = $this->getRequest(); $base_limit = ConpherenceThreadQuery::TRANSACTION_LIMIT; diff --git a/src/applications/conpherence/editor/ConpherenceEditor.php b/src/applications/conpherence/editor/ConpherenceEditor.php index e8239821c6..29ffc22251 100644 --- a/src/applications/conpherence/editor/ConpherenceEditor.php +++ b/src/applications/conpherence/editor/ConpherenceEditor.php @@ -37,7 +37,8 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { if (!$errors) { $xactions = array(); $xactions[] = id(new ConpherenceTransaction()) - ->setTransactionType(ConpherenceTransaction::TYPE_PARTICIPANTS) + ->setTransactionType( + ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE) ->setNewValue(array('+' => $participant_phids)); if ($title) { $xactions[] = id(new ConpherenceTransaction()) @@ -87,8 +88,6 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { public function getTransactionTypes() { $types = parent::getTransactionTypes(); - $types[] = ConpherenceTransaction::TYPE_PARTICIPANTS; - $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; @@ -100,29 +99,6 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { return pht('%s created this room.', $author); } - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case ConpherenceTransaction::TYPE_PARTICIPANTS: - if ($this->getIsNewObject()) { - return array(); - } - return $object->getParticipantPHIDs(); - } - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case ConpherenceTransaction::TYPE_PARTICIPANTS: - return $this->getPHIDTransactionNewValue($xaction); - } - } - /** * We really only need a read lock if we have a comment. In that case, we * must update the messagesCount field on the conpherence and @@ -142,72 +118,6 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { return $lock; } - /** - * We need to apply initial effects IFF the conpherence is new. We must - * save the conpherence first thing to make sure we have an id and a phid, as - * well as create the initial set of participants so that we pass policy - * checks. - */ - protected function shouldApplyInitialEffects( - PhabricatorLiskDAO $object, - array $xactions) { - - return $this->getIsNewObject(); - } - - protected function applyInitialEffects( - PhabricatorLiskDAO $object, - array $xactions) { - - $object->save(); - - foreach ($xactions as $xaction) { - switch ($xaction->getTransactionType()) { - case ConpherenceTransaction::TYPE_PARTICIPANTS: - // Since this is a new ConpherenceThread, we have to create the - // participation data asap to pass policy checks. For existing - // ConpherenceThreads, the existing participation is correct - // at this stage. Note that later in applyCustomExternalTransaction - // this participation data will be updated, particularly the - // behindTransactionPHID which is just a generated dummy for now. - $participants = array(); - $phids = $this->getPHIDTransactionNewValue($xaction, array()); - foreach ($phids as $phid) { - if ($phid == $this->getActor()->getPHID()) { - $status = ConpherenceParticipationStatus::UP_TO_DATE; - $message_count = 1; - } else { - $status = ConpherenceParticipationStatus::BEHIND; - $message_count = 0; - } - $participants[$phid] = - id(new ConpherenceParticipant()) - ->setConpherencePHID($object->getPHID()) - ->setParticipantPHID($phid) - ->setParticipationStatus($status) - ->setDateTouched(time()) - ->setBehindTransactionPHID($xaction->generatePHID()) - ->setSeenMessageCount($message_count) - ->save(); - $object->attachParticipants($participants); - } - break; - } - } - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case ConpherenceTransaction::TYPE_PARTICIPANTS: - if (!$this->getIsNewObject()) {} - break; - } - - } - protected function applyBuiltinInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { @@ -221,96 +131,23 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { return parent::applyBuiltinInternalTransaction($object, $xaction); } - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case ConpherenceTransaction::TYPE_PARTICIPANTS: - if ($this->getIsNewObject()) { - continue; - } - $participants = $object->getParticipants(); - - $old_map = array_fuse($xaction->getOldValue()); - $new_map = array_fuse($xaction->getNewValue()); - - $remove = array_keys(array_diff_key($old_map, $new_map)); - foreach ($remove as $phid) { - $remove_participant = $participants[$phid]; - $remove_participant->delete(); - unset($participants[$phid]); - } - - $add = array_keys(array_diff_key($new_map, $old_map)); - foreach ($add as $phid) { - if ($phid == $this->getActor()->getPHID()) { - $status = ConpherenceParticipationStatus::UP_TO_DATE; - $message_count = $object->getMessageCount(); - } else { - $status = ConpherenceParticipationStatus::BEHIND; - $message_count = 0; - } - $participants[$phid] = - id(new ConpherenceParticipant()) - ->setConpherencePHID($object->getPHID()) - ->setParticipantPHID($phid) - ->setParticipationStatus($status) - ->setDateTouched(time()) - ->setBehindTransactionPHID($xaction->getPHID()) - ->setSeenMessageCount($message_count) - ->save(); - } - $object->attachParticipants($participants); - break; - } - } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { - if (!$xactions) { - return $xactions; + $acting_phid = $this->getActingAsPHID(); + $participants = $object->getParticipants(); + foreach ($participants as $participant) { + if ($participant->getParticipantPHID() == $acting_phid) { + $participant->markUpToDate($object); + } } - $message_count = 0; - foreach ($xactions as $xaction) { - switch ($xaction->getTransactionType()) { - case PhabricatorTransactions::TYPE_COMMENT: - $message_count++; - - // update everyone's participation status on a message -only- - $xaction_phid = $xaction->getPHID(); - $behind = ConpherenceParticipationStatus::BEHIND; - $up_to_date = ConpherenceParticipationStatus::UP_TO_DATE; - $participants = $object->getParticipants(); - $user = $this->getActor(); - $time = time(); - foreach ($participants as $phid => $participant) { - if ($phid != $user->getPHID()) { - if ($participant->getParticipationStatus() != $behind) { - $participant->setBehindTransactionPHID($xaction_phid); - $participant->setSeenMessageCount( - $object->getMessageCount() - $message_count); - } - $participant->setParticipationStatus($behind); - $participant->setDateTouched($time); - } else { - $participant->setSeenMessageCount($object->getMessageCount()); - $participant->setBehindTransactionPHID($xaction_phid); - $participant->setParticipationStatus($up_to_date); - $participant->setDateTouched($time); - } - $participant->save(); - } - - PhabricatorUserCache::clearCaches( - PhabricatorUserMessageCountCacheType::KEY_COUNT, - array_keys($participants)); - - break; - } + if ($participants) { + PhabricatorUserCache::clearCaches( + PhabricatorUserMessageCountCacheType::KEY_COUNT, + array_keys($participants)); } if ($xactions) { @@ -334,7 +171,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { parent::requireCapabilities($object, $xaction); switch ($xaction->getTransactionType()) { - case ConpherenceTransaction::TYPE_PARTICIPANTS: + case ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE: $old_map = array_fuse($xaction->getOldValue()); $new_map = array_fuse($xaction->getNewValue()); @@ -359,6 +196,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { PhabricatorPolicyCapability::CAN_EDIT); } break; + case ConpherenceThreadTitleTransaction::TRANSACTIONTYPE: case ConpherenceThreadTopicTransaction::TRANSACTIONTYPE: PhabricatorPolicyFilter::requireCapability( @@ -369,19 +207,6 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { } } - protected function mergeTransactions( - PhabricatorApplicationTransaction $u, - PhabricatorApplicationTransaction $v) { - - $type = $u->getTransactionType(); - switch ($type) { - case ConpherenceTransaction::TYPE_PARTICIPANTS: - return $this->mergePHIDOrEdgeTransactions($u, $v); - } - - return parent::mergeTransactions($u, $v); - } - protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { @@ -484,43 +309,4 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { return true; } - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case ConpherenceTransaction::TYPE_PARTICIPANTS: - foreach ($xactions as $xaction) { - $new_phids = $this->getPHIDTransactionNewValue($xaction, array()); - $old_phids = nonempty($object->getParticipantPHIDs(), array()); - $phids = array_diff($new_phids, $old_phids); - - if (!$phids) { - continue; - } - - $users = id(new PhabricatorPeopleQuery()) - ->setViewer($this->requireActor()) - ->withPHIDs($phids) - ->execute(); - $users = mpull($users, null, 'getPHID'); - foreach ($phids as $phid) { - if (isset($users[$phid])) { - continue; - } - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht('New room participant "%s" is not a valid user.', $phid), - $xaction); - } - } - break; - } - - return $errors; - } } diff --git a/src/applications/conpherence/mail/ConpherenceReplyHandler.php b/src/applications/conpherence/mail/ConpherenceReplyHandler.php index 946501c8b8..a462030fdd 100644 --- a/src/applications/conpherence/mail/ConpherenceReplyHandler.php +++ b/src/applications/conpherence/mail/ConpherenceReplyHandler.php @@ -55,7 +55,8 @@ final class ConpherenceReplyHandler extends PhabricatorMailReplyHandler { $xactions = array(); if ($this->getMailAddedParticipantPHIDs()) { $xactions[] = id(new ConpherenceTransaction()) - ->setTransactionType(ConpherenceTransaction::TYPE_PARTICIPANTS) + ->setTransactionType( + ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE) ->setNewValue(array('+' => $this->getMailAddedParticipantPHIDs())); } diff --git a/src/applications/conpherence/query/ConpherenceParticipantCountQuery.php b/src/applications/conpherence/query/ConpherenceParticipantCountQuery.php index 7a4f170c3a..268af0ccf1 100644 --- a/src/applications/conpherence/query/ConpherenceParticipantCountQuery.php +++ b/src/applications/conpherence/query/ConpherenceParticipantCountQuery.php @@ -1,73 +1,69 @@ withParticipantPHIDs(array($my_phid)) - * ->withParticipationStatus(ConpherenceParticipationStatus::BEHIND) - * ->execute(); - */ final class ConpherenceParticipantCountQuery extends PhabricatorOffsetPagedQuery { private $participantPHIDs; - private $participationStatus; + private $unread; public function withParticipantPHIDs(array $phids) { $this->participantPHIDs = $phids; return $this; } - public function withParticipationStatus($participation_status) { - $this->participationStatus = $participation_status; + public function withUnread($unread) { + $this->unread = $unread; return $this; } public function execute() { + $thread = new ConpherenceThread(); $table = new ConpherenceParticipant(); - $conn_r = $table->establishConnection('r'); + $conn = $table->establishConnection('r'); $rows = queryfx_all( - $conn_r, - 'SELECT COUNT(*) as count, participantPHID '. - 'FROM %T participant %Q %Q %Q', + $conn, + 'SELECT COUNT(*) as count, participantPHID + FROM %T participant JOIN %T thread + ON participant.conpherencePHID = thread.phid %Q %Q %Q', $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildGroupByClause($conn_r), - $this->buildLimitClause($conn_r)); + $thread->getTableName(), + $this->buildWhereClause($conn), + $this->buildGroupByClause($conn), + $this->buildLimitClause($conn)); return ipull($rows, 'count', 'participantPHID'); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); - if ($this->participantPHIDs) { + if ($this->participantPHIDs !== null) { $where[] = qsprintf( - $conn_r, - 'participantPHID IN (%Ls)', + $conn, + 'participant.participantPHID IN (%Ls)', $this->participantPHIDs); } - if ($this->participationStatus !== null) { - $where[] = qsprintf( - $conn_r, - 'participationStatus = %d', - $this->participationStatus); + if ($this->unread !== null) { + if ($this->unread) { + $where[] = qsprintf( + $conn, + 'participant.seenMessageCount < thread.messageCount'); + } else { + $where[] = qsprintf( + $conn, + 'participant.seenMessageCount >= thread.messageCount'); + } } return $this->formatWhereClause($where); } - private function buildGroupByClause(AphrontDatabaseConnection $conn_r) { - $group_by = qsprintf( - $conn_r, + private function buildGroupByClause(AphrontDatabaseConnection $conn) { + return qsprintf( + $conn, 'GROUP BY participantPHID'); - - return $group_by; } } diff --git a/src/applications/conpherence/query/ConpherenceParticipantQuery.php b/src/applications/conpherence/query/ConpherenceParticipantQuery.php index bb879be4e4..fb4c3eff0f 100644 --- a/src/applications/conpherence/query/ConpherenceParticipantQuery.php +++ b/src/applications/conpherence/query/ConpherenceParticipantQuery.php @@ -1,128 +1,50 @@ withParticipantPHIDs(array($my_phid)) - * ->execute(); - * - * - Q: What are the next set of conpherences as I scroll up (more recent) or - * down (less recent) this list of conpherences? - * - A: - * - * id(new ConpherenceParticipantQuery()) - * ->withParticipantPHIDs(array($my_phid)) - * ->withParticipantCursor($top_participant) - * ->setOrder(ConpherenceParticipantQuery::ORDER_NEWER) - * ->execute(); - * - * -or- - * - * id(new ConpherenceParticipantQuery()) - * ->withParticipantPHIDs(array($my_phid)) - * ->withParticipantCursor($bottom_participant) - * ->setOrder(ConpherenceParticipantQuery::ORDER_OLDER) - * ->execute(); - * - * For counts of read, un-read, or all conpherences by participant, see - * @{class:ConpherenceParticipantCountQuery}. - */ final class ConpherenceParticipantQuery extends PhabricatorOffsetPagedQuery { - const LIMIT = 100; - const ORDER_NEWER = 'newer'; - const ORDER_OLDER = 'older'; - private $participantPHIDs; - private $participantCursor; - private $order = self::ORDER_OLDER; public function withParticipantPHIDs(array $phids) { $this->participantPHIDs = $phids; return $this; } - public function withParticipantCursor(ConpherenceParticipant $participant) { - $this->participantCursor = $participant; - return $this; - } - - public function setOrder($order) { - $this->order = $order; - return $this; - } - public function execute() { $table = new ConpherenceParticipant(); - $conn_r = $table->establishConnection('r'); + $thread = new ConpherenceThread(); + + $conn = $table->establishConnection('r'); $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T participant %Q %Q %Q', + $conn, + 'SELECT * FROM %T participant JOIN %T thread + ON participant.conpherencePHID = thread.phid %Q %Q %Q', $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); + $thread->getTableName(), + $this->buildWhereClause($conn), + $this->buildOrderClause($conn), + $this->buildLimitClause($conn)); - $participants = $table->loadAllFromArray($data); - - $participants = mpull($participants, null, 'getConpherencePHID'); - - if ($this->order == self::ORDER_NEWER) { - $participants = array_reverse($participants); - } - - return $participants; + return $table->loadAllFromArray($data); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); - if ($this->participantPHIDs) { + if ($this->participantPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'participantPHID IN (%Ls)', $this->participantPHIDs); } - if ($this->participantCursor) { - $date_touched = $this->participantCursor->getDateTouched(); - $id = $this->participantCursor->getID(); - if ($this->order == self::ORDER_OLDER) { - $compare_date = '<'; - $compare_id = '<='; - } else { - $compare_date = '>'; - $compare_id = '>='; - } - $where[] = qsprintf( - $conn_r, - '(dateTouched %Q %d OR (dateTouched = %d AND id %Q %d))', - $compare_date, - $date_touched, - $date_touched, - $compare_id, - $id); - } - return $this->formatWhereClause($where); } - private function buildOrderClause(AphrontDatabaseConnection $conn_r) { - $order_word = ($this->order == self::ORDER_OLDER) ? 'DESC' : 'ASC'; - // if these are different direction we won't get as efficient a query - // see http://dev.mysql.com/doc/refman/5.5/en/order-by-optimization.html - $order = qsprintf( - $conn_r, - 'ORDER BY dateTouched %Q, id %Q', - $order_word, - $order_word); - - return $order; + private function buildOrderClause(AphrontDatabaseConnection $conn) { + return qsprintf( + $conn, + 'ORDER BY thread.dateModified DESC, thread.id DESC, participant.id DESC'); } } diff --git a/src/applications/conpherence/storage/ConpherenceParticipant.php b/src/applications/conpherence/storage/ConpherenceParticipant.php index 32c3fb38e1..1edf7940e9 100644 --- a/src/applications/conpherence/storage/ConpherenceParticipant.php +++ b/src/applications/conpherence/storage/ConpherenceParticipant.php @@ -4,10 +4,7 @@ final class ConpherenceParticipant extends ConpherenceDAO { protected $participantPHID; protected $conpherencePHID; - protected $participationStatus; - protected $behindTransactionPHID; protected $seenMessageCount; - protected $dateTouched; protected $settings = array(); protected function getConfiguration() { @@ -16,8 +13,6 @@ final class ConpherenceParticipant extends ConpherenceDAO { 'settings' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( - 'participationStatus' => 'uint32', - 'dateTouched' => 'epoch', 'seenMessageCount' => 'uint64', ), self::CONFIG_KEY_SCHEMA => array( @@ -25,11 +20,8 @@ final class ConpherenceParticipant extends ConpherenceDAO { 'columns' => array('conpherencePHID', 'participantPHID'), 'unique' => true, ), - 'unreadCount' => array( - 'columns' => array('participantPHID', 'participationStatus'), - ), - 'participationIndex' => array( - 'columns' => array('participantPHID', 'dateTouched', 'id'), + 'key_thread' => array( + 'columns' => array('participantPHID', 'conpherencePHID'), ), ), ) + parent::getConfiguration(); @@ -39,12 +31,9 @@ final class ConpherenceParticipant extends ConpherenceDAO { return nonempty($this->settings, array()); } - public function markUpToDate( - ConpherenceThread $conpherence, - ConpherenceTransaction $xaction) { + public function markUpToDate(ConpherenceThread $conpherence) { + if (!$this->isUpToDate($conpherence)) { - $this->setParticipationStatus(ConpherenceParticipationStatus::UP_TO_DATE); - $this->setBehindTransactionPHID($xaction->getPHID()); $this->setSeenMessageCount($conpherence->getMessageCount()); $this->save(); @@ -57,11 +46,7 @@ final class ConpherenceParticipant extends ConpherenceDAO { } public function isUpToDate(ConpherenceThread $conpherence) { - return - ($this->getSeenMessageCount() == $conpherence->getMessageCount()) - && - ($this->getParticipationStatus() == - ConpherenceParticipationStatus::UP_TO_DATE); + return ($this->getSeenMessageCount() == $conpherence->getMessageCount()); } } diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index f0d0e60bb3..7e4c3dc09f 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -72,6 +72,10 @@ final class ConpherenceThread extends ConpherenceDAO return 'Z'.$this->getID(); } + public function getURI() { + return '/'.$this->getMonogram(); + } + public function attachParticipants(array $participants) { assert_instances_of($participants, 'ConpherenceParticipant'); $this->participants = $participants; @@ -200,8 +204,13 @@ final class ConpherenceThread extends ConpherenceDAO } $user_participation = $this->getParticipantIfExists($viewer->getPHID()); + $theme = ConpherenceRoomSettings::COLOR_LIGHT; if ($user_participation) { $user_seen_count = $user_participation->getSeenMessageCount(); + $participant = $this->getParticipant($viewer->getPHID()); + $settings = $participant->getSettings(); + $theme = idx($settings, 'theme', $theme); + $theme_class = ConpherenceRoomSettings::getThemeClass($theme); } else { $user_seen_count = 0; } @@ -217,6 +226,7 @@ final class ConpherenceThread extends ConpherenceDAO 'unread_count' => $unread_count, 'epoch' => $this->getDateModified(), 'image' => $img_src, + 'theme' => $theme_class, ); } diff --git a/src/applications/conpherence/storage/ConpherenceTransaction.php b/src/applications/conpherence/storage/ConpherenceTransaction.php index 2fb3fca380..f23882d69b 100644 --- a/src/applications/conpherence/storage/ConpherenceTransaction.php +++ b/src/applications/conpherence/storage/ConpherenceTransaction.php @@ -3,8 +3,6 @@ final class ConpherenceTransaction extends PhabricatorModularTransaction { - const TYPE_PARTICIPANTS = 'participants'; - public function getApplicationName() { return 'conpherence'; } @@ -21,81 +19,4 @@ final class ConpherenceTransaction return 'ConpherenceThreadTransactionType'; } - public function getNoEffectDescription() { - switch ($this->getTransactionType()) { - case self::TYPE_PARTICIPANTS: - return pht( - 'You can not add a participant who has already been added.'); - break; - } - - return parent::getNoEffectDescription(); - } - - public function shouldHide() { - $old = $this->getOldValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_PARTICIPANTS: - return ($old === null); - } - - return parent::shouldHide(); - } - - public function getTitle() { - $author_phid = $this->getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::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 participant(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 self::TYPE_PARTICIPANTS: - $phids = array_merge($phids, $this->getOldValue()); - $phids = array_merge($phids, $this->getNewValue()); - break; - } - - return $phids; - } } diff --git a/src/applications/conpherence/view/ConpherenceLayoutView.php b/src/applications/conpherence/view/ConpherenceLayoutView.php index 6699d61994..7dbc00a325 100644 --- a/src/applications/conpherence/view/ConpherenceLayoutView.php +++ b/src/applications/conpherence/view/ConpherenceLayoutView.php @@ -10,6 +10,7 @@ final class ConpherenceLayoutView extends AphrontTagView { private $search; private $messages; private $replyForm; + private $theme = ConpherenceRoomSettings::COLOR_LIGHT; private $latestTransactionID; public function setMessages($messages) { @@ -56,6 +57,11 @@ final class ConpherenceLayoutView extends AphrontTagView { return $this; } + public function setTheme($theme) { + $this->theme = $theme; + return $this; + } + public function setLatestTransactionID($id) { $this->latestTransactionID = $id; return $this; @@ -66,6 +72,7 @@ final class ConpherenceLayoutView extends AphrontTagView { $classes[] = 'conpherence-layout'; $classes[] = 'hide-widgets'; $classes[] = 'conpherence-role-'.$this->role; + $classes[] = ConpherenceRoomSettings::getThemeClass($this->theme); return array( 'id' => 'conpherence-main-layout', @@ -105,6 +112,7 @@ final class ConpherenceLayoutView extends AphrontTagView { 'canEditSelectedThread' => $can_edit_selected, 'latestTransactionID' => $this->latestTransactionID, 'role' => $this->role, + 'theme' => ConpherenceRoomSettings::getThemeClass($this->theme), 'hasThreadList' => (bool)$this->threadView, 'hasThread' => (bool)$this->messages, 'hasWidgets' => false, diff --git a/src/applications/conpherence/view/ConpherenceThreadListView.php b/src/applications/conpherence/view/ConpherenceThreadListView.php index 21a4ed1030..af15f82a6e 100644 --- a/src/applications/conpherence/view/ConpherenceThreadListView.php +++ b/src/applications/conpherence/view/ConpherenceThreadListView.php @@ -2,7 +2,7 @@ final class ConpherenceThreadListView extends AphrontView { - const SEE_MORE_LIMIT = 15; + const SEE_ALL_LIMIT = 16; private $baseURI; private $threads; @@ -25,84 +25,11 @@ final class ConpherenceThreadListView extends AphrontView { ->addClass('conpherence-menu') ->setID('conpherence-menu'); - $policy_objects = ConpherenceThread::loadViewPolicyObjects( - $this->getUser(), - $this->threads); - - $this->addRoomsToMenu($menu, $this->threads, $policy_objects); - - $menu = phutil_tag_div('phabricator-side-menu', $menu); - $menu = phutil_tag_div('phui-basic-nav', $menu); - - return $menu; - } - - public function renderSingleThread( - ConpherenceThread $thread, - array $policy_objects) { - assert_instances_of($policy_objects, 'PhabricatorPolicy'); - return $this->renderThread($thread, $policy_objects); - } - - private function renderThreadItem( - ConpherenceThread $thread, - array $policy_objects) { - return id(new PHUIListItemView()) - ->setType(PHUIListItemView::TYPE_CUSTOM) - ->setName($this->renderThread($thread, $policy_objects)); - } - - private function renderThread( - ConpherenceThread $thread, - array $policy_objects) { - - $user = $this->getUser(); - - $uri = '/'.$thread->getMonogram(); - $data = $thread->getDisplayData($user); - $icon = id(new PHUIIconView()) - ->addClass('msr') - ->setIcon($thread->getPolicyIconName($policy_objects)); - $title = phutil_tag( - 'span', - array(), - array( - $icon, - $data['title'], - )); - $subtitle = $data['subtitle']; - $unread_count = $data['unread_count']; - $epoch = $data['epoch']; - $image = $data['image']; - $dom_id = $thread->getPHID().'-nav-item'; - - return id(new ConpherenceMenuItemView()) - ->setUser($user) - ->setTitle($title) - ->setSubtitle($subtitle) - ->setHref($uri) - ->setEpoch($epoch) - ->setImageURI($image) - ->setUnreadCount($unread_count) - ->setID($thread->getPHID().'-nav-item') - ->addSigil('conpherence-menu-click') - ->setMetadata( - array( - 'title' => $data['title'], - 'id' => $dom_id, - 'threadID' => $thread->getID(), - )); - } - - private function addRoomsToMenu( - PHUIListView $menu, - array $rooms, - array $policy_objects) { - - $header = $this->renderMenuItemHeader(); + $header = $this->buildHeaderItemView(); $menu->addMenuItem($header); - if (empty($rooms)) { + // Blank State NUX + if (empty($this->threads)) { $join_item = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_LINK) ->setHref('/conpherence/search/') @@ -115,79 +42,70 @@ final class ConpherenceThreadListView extends AphrontView { ->setWorkflow(true) ->setName(pht('Create a Room')); $menu->addMenuItem($create_item); - - return $menu; } - $this->addThreadsToMenu($menu, $rooms, $policy_objects); + $rooms = $this->buildRoomItems($this->threads); + foreach ($rooms as $room) { + $menu->addMenuItem($room); + } + + $menu = phutil_tag_div('phabricator-side-menu', $menu); + $menu = phutil_tag_div('phui-basic-nav', $menu); + return $menu; } - private function addThreadsToMenu( - PHUIListView $menu, - array $threads, - array $policy_objects) { + private function renderThreadItem( + ConpherenceThread $thread) { - // If we have self::SEE_MORE_LIMIT or less, we can just render - // all the threads at once. Otherwise, we render a "See more" - // UI element, which toggles a show / hide on the remaining rooms + $user = $this->getUser(); + $data = $thread->getDisplayData($user); + $dom_id = $thread->getPHID().'-nav-item'; + + return id(new PHUIListItemView()) + ->setName($data['title']) + ->setHref('/'.$thread->getMonogram()) + ->setProfileImage($data['image']) + ->setCount($data['unread_count']) + ->setType(PHUIListItemView::TYPE_CUSTOM) + ->setID($thread->getPHID().'-nav-item') + ->addSigil('conpherence-menu-click') + ->setMetadata( + array( + 'title' => $data['title'], + 'id' => $dom_id, + 'threadID' => $thread->getID(), + 'theme' => $data['theme'], + )); + } + + private function buildRoomItems(array $threads) { + + $items = array(); $show_threads = $threads; - $more_threads = array(); - if (count($threads) > self::SEE_MORE_LIMIT) { - $show_threads = array_slice($threads, 0, self::SEE_MORE_LIMIT); - $more_threads = array_slice($threads, self::SEE_MORE_LIMIT); + $all_threads = false; + if (count($threads) > self::SEE_ALL_LIMIT) { + $show_threads = array_slice($threads, 0, self::SEE_ALL_LIMIT); + $all_threads = true; } foreach ($show_threads as $thread) { - $item = $this->renderThreadItem($thread, $policy_objects); - $menu->addMenuItem($item); + $items[] = $this->renderThreadItem($thread); } - if ($more_threads) { - $search_uri = '/conpherence/search/query/participant/'; - $sigil = 'more-room'; - - $more_item = id(new PHUIListItemView()) + // Send them to application search here + if ($all_threads) { + $items[] = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_LINK) - ->setHref($search_uri) - ->addSigil('conpherence-menu-see-more') - ->setMetadata(array('moreSigil' => $sigil)) - ->setName(pht('See More')); - $menu->addMenuItem($more_item); - $show_more_threads = $more_threads; - $even_more_threads = array(); - if (count($more_threads) > self::SEE_MORE_LIMIT) { - $show_more_threads = array_slice( - $more_threads, - 0, - self::SEE_MORE_LIMIT); - $even_more_threads = array_slice( - $more_threads, - self::SEE_MORE_LIMIT); - } - foreach ($show_more_threads as $thread) { - $item = $this->renderThreadItem($thread, $policy_objects) - ->addSigil($sigil) - ->addClass('hidden'); - $menu->addMenuItem($item); - } - - if ($even_more_threads) { - // kick them to application search here - $even_more_item = id(new PHUIListItemView()) - ->setType(PHUIListItemView::TYPE_LINK) - ->setHref($search_uri) - ->addSigil($sigil) - ->addClass('hidden') - ->setName(pht('See More')); - $menu->addMenuItem($even_more_item); - } + ->setHref('/conpherence/search/query/participant/') + ->setIcon('fa-external-link') + ->setName(pht('See All Joined')); } - return $menu; + return $items; } - private function renderMenuItemHeader() { + private function buildHeaderItemView() { $rooms = phutil_tag( 'a', array( diff --git a/src/applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php b/src/applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php new file mode 100644 index 0000000000..681c6d639c --- /dev/null +++ b/src/applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php @@ -0,0 +1,117 @@ +getParticipantPHIDs(); + } + + public function generateNewValue($object, $value) { + $old = $this->generateOldValue($object); + return $this->getPHIDList($old, $value); + } + + public function applyExternalEffects($object, $value) { + $participants = $object->getParticipants(); + + $old = array_keys($participants); + $new = $value; + + $add_map = array_fuse(array_diff($new, $old)); + $rem_map = array_fuse(array_diff($old, $new)); + + foreach ($rem_map as $phid) { + $remove_participant = $participants[$phid]; + $remove_participant->delete(); + unset($participants[$phid]); + } + + foreach ($add_map as $phid) { + if (isset($participants[$phid])) { + continue; + } + + $participants[$phid] = id(new ConpherenceParticipant()) + ->setConpherencePHID($object->getPHID()) + ->setParticipantPHID($phid) + ->setSeenMessageCount(0) + ->save(); + } + + $object->attachParticipants($participants); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $add = array_diff($new, $old); + $rem = array_diff($old, $new); + + $author_phid = $this->getAuthorPHID(); + + if ($add && $rem) { + return pht( + '%s edited participant(s), added %d: %s; removed %d: %s.', + $this->renderAuthor(), + count($add), + $this->renderHandleList($add), + count($rem), + $this->renderHandleList($rem)); + } else if ((in_array($author_phid, $add)) && (count($add) == 1)) { + return pht( + '%s joined the room.', + $this->renderAuthor()); + } else if ((in_array($author_phid, $rem)) && (count($rem) == 1)) { + return pht( + '%s left the room.', + $this->renderAuthor()); + } else if ($add) { + return pht( + '%s added %d participant(s): %s.', + $this->renderAuthor(), + count($add), + $this->renderHandleList($add)); + } else { + return pht( + '%s removed %d participant(s): %s.', + $this->renderAuthor(), + count($rem), + $this->renderHandleList($rem)); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $old = $object->getParticipantPHIDs(); + + $new = $xaction->getNewValue(); + $new = $this->getPHIDList($old, $new); + + $add_map = array_fuse(array_diff($new, $old)); + $rem_map = array_fuse(array_diff($old, $new)); + + foreach ($add_map as $user_phid) { + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($user_phid)) + ->executeOne(); + if (!$user) { + $errors[] = $this->newInvalidError( + pht( + 'Participant PHID "%s" is not a valid user PHID.', + $user_phid)); + continue; + } + } + } + + return $errors; + } + +} diff --git a/src/applications/console/plugin/DarkConsoleRealtimePlugin.php b/src/applications/console/plugin/DarkConsoleRealtimePlugin.php new file mode 100644 index 0000000000..dbfec95007 --- /dev/null +++ b/src/applications/console/plugin/DarkConsoleRealtimePlugin.php @@ -0,0 +1,78 @@ + 'dark-console-realtime-log', + 'class' => 'dark-console-log-frame', + )); + + $reconnect_label = pht('Reconnect'); + $replay_label = pht('Replay'); + $repaint_label = pht('Repaint'); + + $buttons = phutil_tag( + 'div', + array( + 'class' => 'dark-console-realtime-actions', + ), + array( + id(new PHUIButtonView()) + ->setIcon('fa-refresh') + ->setColor(PHUIButtonView::GREY) + ->setText($reconnect_label) + ->addSigil('dark-console-realtime-action') + ->setMetadata( + array( + 'action' => 'reconnect', + 'label' => $reconnect_label, + )), + id(new PHUIButtonView()) + ->setIcon('fa-backward') + ->setColor(PHUIButtonView::GREY) + ->setText($replay_label) + ->addSigil('dark-console-realtime-action') + ->setMetadata( + array( + 'action' => 'replay', + 'label' => $replay_label, + )), + id(new PHUIButtonView()) + ->setIcon('fa-paint-brush') + ->setColor(PHUIButtonView::GREY) + ->setText($repaint_label) + ->addSigil('dark-console-realtime-action') + ->setMetadata( + array( + 'action' => 'repaint', + 'label' => $repaint_label, + )), + )); + + return phutil_tag( + 'div', + array( + 'class' => 'dark-console-realtime', + ), + array( + $buttons, + $frame, + )); + } + +} diff --git a/src/applications/differential/view/DifferentialReviewersView.php b/src/applications/differential/view/DifferentialReviewersView.php index 291f859d08..034bf99edb 100644 --- a/src/applications/differential/view/DifferentialReviewersView.php +++ b/src/applications/differential/view/DifferentialReviewersView.php @@ -55,80 +55,101 @@ final class DifferentialReviewersView extends AphrontView { $item->setHighlighted($reviewer->hasAuthority($viewer)); + // If someone other than the reviewer acted on the reviewer's behalf, + // show who is responsible for the current state. This is usually a + // user accepting for a package or project. + $authority_phid = $reviewer->getLastActorPHID(); + if ($authority_phid && ($authority_phid !== $phid)) { + $authority_name = $viewer->renderHandle($authority_phid) + ->setAsText(true); + } else { + $authority_name = null; + } + switch ($reviewer->getReviewerStatus()) { case DifferentialReviewerStatus::STATUS_ADDED: if ($comment_phid) { if ($is_current_comment) { - $item->setIcon( - 'fa-comment', - 'blue', - pht('Commented')); + $icon = 'fa-comment'; + $color = 'blue'; + $label = pht('Commented'); } else { - $item->setIcon( - 'fa-comment-o', - 'bluegrey', - pht('Commented Previously')); + $icon = 'fa-comment-o'; + $color = 'bluegrey'; + $label = pht('Commented Previously'); } } else { - $item->setIcon( - PHUIStatusItemView::ICON_OPEN, - 'bluegrey', - pht('Review Requested')); + $icon = PHUIStatusItemView::ICON_OPEN; + $color = 'bluegrey'; + $label = pht('Review Requested'); } break; case DifferentialReviewerStatus::STATUS_ACCEPTED: if ($is_current_action) { - $item->setIcon( - PHUIStatusItemView::ICON_ACCEPT, - 'green', - pht('Accepted')); + $icon = PHUIStatusItemView::ICON_ACCEPT; + $color = 'green'; + if ($authority_name !== null) { + $label = pht('Accepted (by %s)', $authority_name); + } else { + $label = pht('Accepted'); + } } else { - $item->setIcon( - 'fa-check-circle-o', - 'bluegrey', - pht('Accepted Prior Diff')); + $icon = 'fa-check-circle-o'; + $color = 'bluegrey'; + if ($authority_name !== null) { + $label = pht('Accepted Prior Diff (by %s)', $authority_name); + } else { + $label = pht('Accepted Prior Diff'); + } } break; case DifferentialReviewerStatus::STATUS_REJECTED: if ($is_current_action) { - $item->setIcon( - PHUIStatusItemView::ICON_REJECT, - 'red', - pht('Requested Changes')); + $icon = PHUIStatusItemView::ICON_REJECT; + $color = 'red'; + if ($authority_name !== null) { + $label = pht('Requested Changes (by %s)', $authority_name); + } else { + $label = pht('Requested Changes'); + } } else { - $item->setIcon( - 'fa-times-circle-o', - 'bluegrey', - pht('Requested Changes to Prior Diff')); + $icon = 'fa-times-circle-o'; + $color = 'bluegrey'; + if ($authority_name !== null) { + $label = pht( + 'Requested Changes to Prior Diff (by %s)', + $authority_name); + } else { + $label = pht('Requested Changes to Prior Diff'); + } } break; case DifferentialReviewerStatus::STATUS_BLOCKING: - $item->setIcon( - PHUIStatusItemView::ICON_MINUS, - 'red', - pht('Blocking Review')); + $icon = PHUIStatusItemView::ICON_MINUS; + $color = 'red'; + $label = pht('Blocking Review'); break; case DifferentialReviewerStatus::STATUS_RESIGNED: - $item->setIcon( - 'fa-times', - 'grey', - pht('Resigned')); + $icon = 'fa-times'; + $color = 'grey'; + $label = pht('Resigned'); break; default: - $item->setIcon( - PHUIStatusItemView::ICON_QUESTION, - 'bluegrey', - pht('%s?', $reviewer->getReviewerStatus())); + $icon = PHUIStatusItemView::ICON_QUESTION; + $color = 'bluegrey'; + $label = pht('Unknown ("%s")', $reviewer->getReviewerStatus()); break; } + $item->setIcon($icon, $color, $label); $item->setTarget($handle->renderHovercardLink()); + $view->addItem($item); } diff --git a/src/applications/diffusion/controller/DiffusionBranchTableController.php b/src/applications/diffusion/controller/DiffusionBranchTableController.php index 5c76469b5e..e5e033f416 100644 --- a/src/applications/diffusion/controller/DiffusionBranchTableController.php +++ b/src/applications/diffusion/controller/DiffusionBranchTableController.php @@ -57,7 +57,8 @@ final class DiffusionBranchTableController extends DiffusionController { $content = id(new PHUIObjectBoxView()) ->setHeaderText($repository->getName()) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($table); + ->setTable($table) + ->setPager($pager); } $crumbs = $this->buildCrumbs( @@ -66,8 +67,6 @@ final class DiffusionBranchTableController extends DiffusionController { )); $crumbs->setBorder(true); - $pager_box = $this->renderTablePagerBox($pager); - $header = id(new PHUIHeaderView()) ->setHeader(pht('Branches')) ->setHeaderIcon('fa-code-fork'); @@ -76,7 +75,6 @@ final class DiffusionBranchTableController extends DiffusionController { ->setHeader($header) ->setFooter(array( $content, - $pager_box, )); return $this->newPage() diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index b29dcf3d1e..4eb6144ede 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -370,7 +370,8 @@ final class DiffusionBrowseController extends DiffusionController { $browse_panel = id(new PHUIObjectBoxView()) ->setHeader($browse_header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($browse_table); + ->setTable($browse_table) + ->setPager($pager); $browse_panel->setShowHide( array(pht('Show Search')), @@ -395,7 +396,6 @@ final class DiffusionBrowseController extends DiffusionController { 'view' => 'browse', )); - $pager_box = $this->renderTablePagerBox($pager); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) @@ -411,7 +411,6 @@ final class DiffusionBrowseController extends DiffusionController { array( $open_revisions, $readme, - $pager_box, )); if ($details) { @@ -489,14 +488,12 @@ final class DiffusionBrowseController extends DiffusionController { nonempty($drequest->getPath(), '/')); } - $box = id(new PHUIObjectBoxView()) + return id(new PHUIObjectBoxView()) ->setHeaderText($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($table); + ->setTable($table) + ->setPager($pager); - $pager_box = $this->renderTablePagerBox($pager); - - return array($box, $pager_box); } private function renderGrepResults(array $results, $pattern) { @@ -1775,9 +1772,8 @@ final class DiffusionBrowseController extends DiffusionController { } $header = id(new PHUIHeaderView()) - ->setHeader(pht('Open Revisions')) - ->setSubheader( - pht('Recently updated open revisions affecting this file.')); + ->setHeader(pht('Recently Open Revisions')) + ->setHeaderIcon('fa-gear'); $view = id(new DifferentialRevisionListView()) ->setHeader($header) diff --git a/src/applications/diffusion/controller/DiffusionCompareController.php b/src/applications/diffusion/controller/DiffusionCompareController.php index 884e740309..a3104e6da4 100644 --- a/src/applications/diffusion/controller/DiffusionCompareController.php +++ b/src/applications/diffusion/controller/DiffusionCompareController.php @@ -92,6 +92,7 @@ final class DiffusionCompareController extends DiffusionController { array( 'view' => 'compare', )); + $crumbs->setBorder(true); $pager = id(new PHUIPagerView()) ->readFromRequest($request); @@ -310,16 +311,11 @@ final class DiffusionCompareController extends DiffusionController { $header = id(new PHUIHeaderView()) ->setHeader(pht('Commits')); - $object_box = id(new PHUIObjectBoxView()) + return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($history_table); + ->setTable($history_table) + ->setPager($pager); - $pager_box = $this->renderTablePagerBox($pager); - - return array( - $object_box, - $pager_box, - ); } } diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php index eb46211d99..a018eb3dbb 100644 --- a/src/applications/diffusion/controller/DiffusionController.php +++ b/src/applications/diffusion/controller/DiffusionController.php @@ -317,12 +317,6 @@ abstract class DiffusionController extends PhabricatorController { ->appendChild($body); } - protected function renderTablePagerBox(PHUIPagerView $pager) { - return id(new PHUIBoxView()) - ->addMargin(PHUI::MARGIN_LARGE) - ->appendChild($pager); - } - protected function renderCommitHashTag(DiffusionRequest $drequest) { $stable_commit = $drequest->getStableCommit(); $commit = phutil_tag( diff --git a/src/applications/diffusion/controller/DiffusionHistoryController.php b/src/applications/diffusion/controller/DiffusionHistoryController.php index c9df551c43..1a29a4263a 100644 --- a/src/applications/diffusion/controller/DiffusionHistoryController.php +++ b/src/applications/diffusion/controller/DiffusionHistoryController.php @@ -57,7 +57,8 @@ final class DiffusionHistoryController extends DiffusionController { $history_panel = id(new PHUIObjectBoxView()) ->setHeader($history_header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($history_table); + ->setTable($history_table) + ->setPager($pager); $header = $this->buildHeader($drequest); @@ -69,13 +70,10 @@ final class DiffusionHistoryController extends DiffusionController { )); $crumbs->setBorder(true); - $pager_box = $this->renderTablePagerBox($pager); - $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $history_panel, - $pager_box, )); return $this->newPage() diff --git a/src/applications/diffusion/controller/DiffusionLintController.php b/src/applications/diffusion/controller/DiffusionLintController.php index fbd059796a..980dc8d27c 100644 --- a/src/applications/diffusion/controller/DiffusionLintController.php +++ b/src/applications/diffusion/controller/DiffusionLintController.php @@ -456,7 +456,8 @@ final class DiffusionLintController extends DiffusionController { $content[] = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Lint Details')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($table); + ->setTable($table) + ->setPager($pager); $crumbs = $this->buildCrumbs( array( @@ -465,7 +466,6 @@ final class DiffusionLintController extends DiffusionController { 'view' => 'lint', )); - $pager_box = $this->renderTablePagerBox($pager); $header = id(new PHUIHeaderView()) ->setHeader(pht('Lint: %s', $drequest->getRepository()->getDisplayName())) ->setHeaderIcon('fa-code'); @@ -474,7 +474,6 @@ final class DiffusionLintController extends DiffusionController { ->setHeader($header) ->setFooter(array( $content, - $pager_box, )); return $this->newPage() diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php index a18f2da1d7..91b33bcaaa 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php @@ -354,9 +354,9 @@ final class DiffusionRepositoryController extends DiffusionController { } if ($repository->isSVN()) { - $label = pht('Checkout'); + $label = phutil_tag_div('diffusion-clone-label', pht('Checkout')); } else { - $label = pht('Clone'); + $label = phutil_tag_div('diffusion-clone-label', pht('Clone')); } $view->addProperty( @@ -686,15 +686,10 @@ final class DiffusionRepositoryController extends DiffusionController { $pager->setURI($browse_uri, 'offset'); if ($pager->willShowPagingControls()) { - $pager_box = $this->renderTablePagerBox($pager); - } else { - $pager_box = null; + $browse_panel->setPager($pager); } - return array( - $browse_panel, - $pager_box, - ); + return $browse_panel; } private function renderCloneURI( diff --git a/src/applications/diffusion/controller/DiffusionTagListController.php b/src/applications/diffusion/controller/DiffusionTagListController.php index 5abd93c37d..df3b356f5d 100644 --- a/src/applications/diffusion/controller/DiffusionTagListController.php +++ b/src/applications/diffusion/controller/DiffusionTagListController.php @@ -87,15 +87,13 @@ final class DiffusionTagListController extends DiffusionController { $box = id(new PHUIObjectBoxView()) ->setHeaderText($repository->getDisplayName()) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($view); - - $pager_box = $this->renderTablePagerBox($pager); + ->setTable($view) + ->setPager($pager); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $box, - $pager_box, )); return $this->newPage() diff --git a/src/applications/diffusion/view/DiffusionReadmeView.php b/src/applications/diffusion/view/DiffusionReadmeView.php index 16924ddd98..b475fa88ac 100644 --- a/src/applications/diffusion/view/DiffusionReadmeView.php +++ b/src/applications/diffusion/view/DiffusionReadmeView.php @@ -73,18 +73,6 @@ final class DiffusionReadmeView extends DiffusionView { ->getOutput($markup_object, $markup_field); $engine = $markup_object->newMarkupEngine($markup_field); - $toc = PhutilRemarkupHeaderBlockRule::renderTableOfContents($engine); - if ($toc) { - $toc = phutil_tag_div( - 'phabricator-remarkup-toc', - array( - phutil_tag_div( - 'phabricator-remarkup-toc-header', - pht('Table of Contents')), - $toc, - )); - $content = array($toc, $content); - } $readme_content = $content; $class = null; @@ -106,15 +94,13 @@ final class DiffusionReadmeView extends DiffusionView { } $readme_content = phutil_tag_div($class, $readme_content); - $header = id(new PHUIHeaderView()) - ->setHeader($readme_name); - $document = id(new PHUIDocumentViewPro()) ->setFluid(true) ->appendChild($readme_content); return id(new PHUIObjectBoxView()) - ->setHeader($header) + ->setHeaderText($readme_name) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($document) ->addClass('diffusion-readme-view'); } diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php index 31761d1244..c8bfcc488a 100644 --- a/src/applications/files/controller/PhabricatorFileDataController.php +++ b/src/applications/files/controller/PhabricatorFileDataController.php @@ -62,24 +62,8 @@ final class PhabricatorFileDataController extends PhabricatorFileController { // an initial request for bytes 0-1 of the audio file, and things go south // if we can't respond with a 206 Partial Content. $range = $request->getHTTPHeader('range'); - if ($range) { - $matches = null; - if (preg_match('/^bytes=(\d+)-(\d*)$/', $range, $matches)) { - // Note that the "Range" header specifies bytes differently than - // we do internally: the range 0-1 has 2 bytes (byte 0 and byte 1). - $begin = (int)$matches[1]; - - // The "Range" may be "200-299" or "200-", meaning "until end of file". - if (strlen($matches[2])) { - $range_end = (int)$matches[2]; - $end = $range_end + 1; - } else { - $range_end = null; - } - - $response->setHTTPResponseCode(206); - $response->setRange($begin, $range_end); - } + if (strlen($range)) { + list($begin, $end) = $response->parseHTTPRange($range); } $is_viewable = $file->isViewableInBrowser(); @@ -143,6 +127,7 @@ final class PhabricatorFileDataController extends PhabricatorFileController { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($this->phid)) + ->withIsDeleted(false) ->executeOne(); if (!$file) { diff --git a/src/applications/files/controller/PhabricatorFileDeleteController.php b/src/applications/files/controller/PhabricatorFileDeleteController.php index acca7c9b1c..be9cecfcf5 100644 --- a/src/applications/files/controller/PhabricatorFileDeleteController.php +++ b/src/applications/files/controller/PhabricatorFileDeleteController.php @@ -9,6 +9,7 @@ final class PhabricatorFileDeleteController extends PhabricatorFileController { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withIDs(array($id)) + ->withIsDeleted(false) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -25,7 +26,19 @@ final class PhabricatorFileDeleteController extends PhabricatorFileController { } if ($request->isFormPost()) { - $file->delete(); + $xactions = array(); + + $xactions[] = id(new PhabricatorFileTransaction()) + ->setTransactionType(PhabricatorFileDeleteTransaction::TRANSACTIONTYPE) + ->setNewValue(true); + + id(new PhabricatorFileEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($file, $xactions); + return id(new AphrontRedirectResponse())->setURI('/file/'); } diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php index f7e72be2aa..061790aa31 100644 --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileInfoController.php @@ -15,6 +15,7 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) + ->withIsDeleted(false) ->executeOne(); if (!$file) { @@ -25,6 +26,7 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withIDs(array($id)) + ->withIsDeleted(false) ->executeOne(); if (!$file) { return new Aphront404Response(); diff --git a/src/applications/files/editor/PhabricatorFileEditEngine.php b/src/applications/files/editor/PhabricatorFileEditEngine.php index 04c4753bd5..9c5eaef74a 100644 --- a/src/applications/files/editor/PhabricatorFileEditEngine.php +++ b/src/applications/files/editor/PhabricatorFileEditEngine.php @@ -36,7 +36,9 @@ final class PhabricatorFileEditEngine } protected function newObjectQuery() { - return new PhabricatorFileQuery(); + $query = new PhabricatorFileQuery(); + $query->withIsDeleted(false); + return $query; } protected function getObjectCreateTitleText($object) { diff --git a/src/applications/files/editor/PhabricatorFileEditor.php b/src/applications/files/editor/PhabricatorFileEditor.php index 28b781fb37..6a2b797b40 100644 --- a/src/applications/files/editor/PhabricatorFileEditor.php +++ b/src/applications/files/editor/PhabricatorFileEditor.php @@ -71,7 +71,7 @@ final class PhabricatorFileEditor } protected function supportsSearch() { - return false; + return true; } } diff --git a/src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php b/src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php index 1bb86cf67e..30de63b904 100644 --- a/src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php +++ b/src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php @@ -129,7 +129,7 @@ final class PhabricatorChunkedFileStorageEngine foreach ($chunks as $chunk) { $chunk->save(); } - $file->save(); + $file->saveAndIndex(); $file->saveTransaction(); return $file; diff --git a/src/applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php b/src/applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php index c79bb9ba99..bbbdfb9850 100644 --- a/src/applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php +++ b/src/applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php @@ -18,8 +18,10 @@ final class PhabricatorFileTemporaryGarbageCollector 'ttl < %d LIMIT 100', PhabricatorTime::getNow()); + $engine = new PhabricatorDestructionEngine(); + foreach ($files as $file) { - $file->delete(); + $engine->destroyObject($file); } return (count($files) == 100); diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php index c2ac083fea..a2de5ca6ab 100644 --- a/src/applications/files/query/PhabricatorFileQuery.php +++ b/src/applications/files/query/PhabricatorFileQuery.php @@ -15,6 +15,7 @@ final class PhabricatorFileQuery private $maxLength; private $names; private $isPartial; + private $isDeleted; private $needTransforms; private $builtinKeys; @@ -119,6 +120,17 @@ final class PhabricatorFileQuery return $this; } + public function withIsDeleted($deleted) { + $this->isDeleted = $deleted; + return $this; + } + + public function withNameNgrams($ngrams) { + return $this->withNgramsConstraint( + id(new PhabricatorFileNameNgrams()), + $ngrams); + } + public function showOnlyExplicitUploads($explicit_uploads) { $this->explicitUploads = $explicit_uploads; return $this; @@ -390,6 +402,13 @@ final class PhabricatorFileQuery (int)$this->isPartial); } + if ($this->isDeleted !== null) { + $where[] = qsprintf( + $conn, + 'isDeleted = %d', + (int)$this->isDeleted); + } + if ($this->builtinKeys !== null) { $where[] = qsprintf( $conn, diff --git a/src/applications/files/query/PhabricatorFileSearchEngine.php b/src/applications/files/query/PhabricatorFileSearchEngine.php index 1f2f2de4b3..59810c830c 100644 --- a/src/applications/files/query/PhabricatorFileSearchEngine.php +++ b/src/applications/files/query/PhabricatorFileSearchEngine.php @@ -16,7 +16,9 @@ final class PhabricatorFileSearchEngine } public function newQuery() { - return new PhabricatorFileQuery(); + $query = new PhabricatorFileQuery(); + $query->withIsDeleted(false); + return $query; } protected function buildCustomSearchFields() { @@ -38,6 +40,10 @@ final class PhabricatorFileSearchEngine id(new PhabricatorSearchDateField()) ->setKey('createdEnd') ->setLabel(pht('Created Before')), + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Name Contains')) + ->setKey('name') + ->setDescription(pht('Search for files by name substring.')), ); } @@ -68,6 +74,10 @@ final class PhabricatorFileSearchEngine $query->withDateCreatedBefore($map['createdEnd']); } + if ($map['name'] !== null) { + $query->withNameNgrams($map['name']); + } + return $query; } diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index db15fb43e0..9fdae4445a 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -28,7 +28,9 @@ final class PhabricatorFile extends PhabricatorFileDAO PhabricatorFlaggableInterface, PhabricatorPolicyInterface, PhabricatorDestructibleInterface, - PhabricatorConduitResultInterface { + PhabricatorConduitResultInterface, + PhabricatorIndexableInterface, + PhabricatorNgramsInterface { const METADATA_IMAGE_WIDTH = 'width'; const METADATA_IMAGE_HEIGHT = 'height'; @@ -39,6 +41,9 @@ final class PhabricatorFile extends PhabricatorFileDAO const METADATA_STORAGE = 'storage'; const METADATA_INTEGRITY = 'integrity'; + const STATUS_ACTIVE = 'active'; + const STATUS_DELETED = 'deleted'; + protected $name; protected $mimeType; protected $byteSize; @@ -57,6 +62,7 @@ final class PhabricatorFile extends PhabricatorFileDAO protected $isExplicitUpload = 1; protected $viewPolicy = PhabricatorPolicies::POLICY_USER; protected $isPartial = 0; + protected $isDeleted = 0; private $objects = self::ATTACHABLE; private $objectPHIDs = self::ATTACHABLE; @@ -87,7 +93,7 @@ final class PhabricatorFile extends PhabricatorFileDAO 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( - 'name' => 'text255?', + 'name' => 'sort255?', 'mimeType' => 'text255?', 'byteSize' => 'uint64', 'storageEngine' => 'text32', @@ -101,6 +107,7 @@ final class PhabricatorFile extends PhabricatorFileDAO 'mailKey' => 'bytes20', 'isPartial' => 'bool', 'builtinKey' => 'text64?', + 'isDeleted' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, @@ -146,6 +153,12 @@ final class PhabricatorFile extends PhabricatorFileDAO return parent::save(); } + public function saveAndIndex() { + $this->save(); + PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID()); + return $this; + } + public function getMonogram() { return 'F'.$this->getID(); } @@ -232,7 +245,7 @@ final class PhabricatorFile extends PhabricatorFileDAO $new_file->readPropertiesFromParameters($params); - $new_file->save(); + $new_file->saveAndIndex(); return $new_file; } @@ -388,7 +401,7 @@ final class PhabricatorFile extends PhabricatorFileDAO // Do nothing } - $file->save(); + $file->saveAndIndex(); return $file; } @@ -1585,4 +1598,14 @@ final class PhabricatorFile extends PhabricatorFileDAO return array(); } +/* -( PhabricatorNgramInterface )------------------------------------------ */ + + + public function newNgrams() { + return array( + id(new PhabricatorFileNameNgrams()) + ->setValue($this->getName()), + ); + } + } diff --git a/src/applications/files/storage/PhabricatorFileNameNgrams.php b/src/applications/files/storage/PhabricatorFileNameNgrams.php new file mode 100644 index 0000000000..6e97e8f0e4 --- /dev/null +++ b/src/applications/files/storage/PhabricatorFileNameNgrams.php @@ -0,0 +1,18 @@ +save(); + $file->saveAndIndex(); $rope = $this->getRope(); diff --git a/src/applications/files/worker/FileDeletionWorker.php b/src/applications/files/worker/FileDeletionWorker.php new file mode 100644 index 0000000000..0cc89ac30e --- /dev/null +++ b/src/applications/files/worker/FileDeletionWorker.php @@ -0,0 +1,31 @@ +getTaskData(), 'objectPHID'); + if (!$phid) { + throw new PhabricatorWorkerPermanentFailureException( + pht('No "%s" in task data.', 'objectPHID')); + } + + $file = id(new PhabricatorFileQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($phid)) + ->executeOne(); + + if (!$file) { + throw new PhabricatorWorkerPermanentFailureException( + pht('File "%s" does not exist.', $phid)); + } + + return $file; + } + + protected function doWork() { + $file = $this->loadFile(); + $engine = new PhabricatorDestructionEngine(); + $engine->destroyObject($file); + } + +} diff --git a/src/applications/files/xaction/PhabricatorFileDeleteTransaction.php b/src/applications/files/xaction/PhabricatorFileDeleteTransaction.php new file mode 100644 index 0000000000..d0b2c942c4 --- /dev/null +++ b/src/applications/files/xaction/PhabricatorFileDeleteTransaction.php @@ -0,0 +1,45 @@ +setIsDeleted(true); + + PhabricatorWorker::scheduleTask( + 'FileDeletionWorker', + array('objectPHID' => $file->getPHID()), + array('priority' => PhabricatorWorker::PRIORITY_BULK)); + } + + public function getIcon() { + return 'fa-ban'; + } + + public function getColor() { + return 'red'; + } + + public function getTitle() { + return pht( + '%s deleted this file.', + $this->renderAuthor()); + } + + public function getTitleForFeed() { + return pht( + '%s deleted %s.', + $this->renderAuthor(), + $this->renderObject()); + } + +} diff --git a/src/applications/meta/query/PhabricatorApplicationApplicationTransactionQuery.php b/src/applications/meta/query/PhabricatorApplicationApplicationTransactionQuery.php new file mode 100644 index 0000000000..77843f713d --- /dev/null +++ b/src/applications/meta/query/PhabricatorApplicationApplicationTransactionQuery.php @@ -0,0 +1,10 @@ +buildEdgeSchemata(new PhabricatorApplicationsApplication()); + } + +} diff --git a/src/applications/meta/xactions/PhabricatorApplicationTransactionType.php b/src/applications/meta/xactions/PhabricatorApplicationTransactionType.php new file mode 100644 index 0000000000..8f66ce95a2 --- /dev/null +++ b/src/applications/meta/xactions/PhabricatorApplicationTransactionType.php @@ -0,0 +1,4 @@ +loadAllWhere( 'id IN (%Ld)', $ids); diff --git a/src/applications/notification/client/PhabricatorNotificationClient.php b/src/applications/notification/client/PhabricatorNotificationClient.php index ae8ac7eb34..ff5538dbcf 100644 --- a/src/applications/notification/client/PhabricatorNotificationClient.php +++ b/src/applications/notification/client/PhabricatorNotificationClient.php @@ -18,6 +18,11 @@ final class PhabricatorNotificationClient extends Phobject { } public static function tryToPostMessage(array $data) { + $unique_id = Filesystem::readRandomCharacters(32); + $data = $data + array( + 'uniqueID' => $unique_id, + ); + $servers = PhabricatorNotificationServerRef::getEnabledAdminServers(); shuffle($servers); diff --git a/src/applications/notification/controller/PhabricatorNotificationIndividualController.php b/src/applications/notification/controller/PhabricatorNotificationIndividualController.php index 41dade2747..af3e8bcff0 100644 --- a/src/applications/notification/controller/PhabricatorNotificationIndividualController.php +++ b/src/applications/notification/controller/PhabricatorNotificationIndividualController.php @@ -47,6 +47,7 @@ final class PhabricatorNotificationIndividualController 'title' => $data['title'], 'body' => $data['body'], 'content' => hsprintf('%s', $content), + 'uniqueID' => 'story/'.$story->getChronologicalKey(), ); return id(new AphrontAjaxResponse())->setContent($response); diff --git a/src/applications/packages/editor/PhabricatorPackagesEditor.php b/src/applications/packages/editor/PhabricatorPackagesEditor.php index 0065791ad7..492b14643a 100644 --- a/src/applications/packages/editor/PhabricatorPackagesEditor.php +++ b/src/applications/packages/editor/PhabricatorPackagesEditor.php @@ -4,7 +4,7 @@ abstract class PhabricatorPackagesEditor extends PhabricatorApplicationTransactionEditor { public function getEditorApplicationClass() { - return 'PhabricatorPasteApplication'; + return 'PhabricatorPackagesApplication'; } protected function supportsSearch() { diff --git a/src/applications/people/cache/PhabricatorUserMessageCountCacheType.php b/src/applications/people/cache/PhabricatorUserMessageCountCacheType.php index b6ff7b7263..1c253e19ab 100644 --- a/src/applications/people/cache/PhabricatorUserMessageCountCacheType.php +++ b/src/applications/people/cache/PhabricatorUserMessageCountCacheType.php @@ -28,10 +28,9 @@ final class PhabricatorUserMessageCountCacheType $user_phids = mpull($users, 'getPHID'); - $unread_status = ConpherenceParticipationStatus::BEHIND; $unread = id(new ConpherenceParticipantCountQuery()) ->withParticipantPHIDs($user_phids) - ->withParticipationStatus($unread_status) + ->withUnread(true) ->execute(); $empty = array_fill_keys($user_phids, 0); diff --git a/src/applications/phurl/application/PhabricatorPhurlApplication.php b/src/applications/phurl/application/PhabricatorPhurlApplication.php index 38d5d4459a..ebff17186b 100644 --- a/src/applications/phurl/application/PhabricatorPhurlApplication.php +++ b/src/applications/phurl/application/PhabricatorPhurlApplication.php @@ -48,8 +48,6 @@ final class PhabricatorPhurlApplication extends PhabricatorApplication { 'url/' => array( $this->getEditRoutePattern('edit/') => 'PhabricatorPhurlURLEditController', - 'comment/(?P[1-9]\d*)/' - => 'PhabricatorPhurlURLCommentController', ), ), ); diff --git a/src/applications/phurl/controller/PhabricatorPhurlURLCommentController.php b/src/applications/phurl/controller/PhabricatorPhurlURLCommentController.php deleted file mode 100644 index 8726a38706..0000000000 --- a/src/applications/phurl/controller/PhabricatorPhurlURLCommentController.php +++ /dev/null @@ -1,63 +0,0 @@ -isFormPost()) { - return new Aphront400Response(); - } - - $viewer = $request->getViewer(); - $id = $request->getURIData('id'); - - $is_preview = $request->isPreviewRequest(); - $draft = PhabricatorDraft::buildFromRequest($request); - - $phurl = id(new PhabricatorPhurlURLQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->executeOne(); - if (!$phurl) { - return new Aphront404Response(); - } - - $view_uri = '/'.$phurl->getMonogram(); - - $xactions = array(); - $xactions[] = id(new PhabricatorPhurlURLTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) - ->attachComment( - id(new PhabricatorPhurlURLTransactionComment()) - ->setContent($request->getStr('comment'))); - - $editor = id(new PhabricatorPhurlURLEditor()) - ->setActor($viewer) - ->setContinueOnNoEffect($request->isContinueRequest()) - ->setContentSourceFromRequest($request) - ->setIsPreview($is_preview); - - try { - $xactions = $editor->applyTransactions($phurl, $xactions); - } catch (PhabricatorApplicationTransactionNoEffectException $ex) { - return id(new PhabricatorApplicationTransactionNoEffectResponse()) - ->setCancelURI($view_uri) - ->setException($ex); - } - - if ($draft) { - $draft->replaceOrDelete(); - } - - if ($request->isAjax() && $is_preview) { - return id(new PhabricatorApplicationTransactionResponse()) - ->setViewer($viewer) - ->setTransactions($xactions) - ->setIsPreview($is_preview); - } else { - return id(new AphrontRedirectResponse()) - ->setURI($view_uri); - } - } - -} diff --git a/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php b/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php index 4e79670143..578e3ed9f8 100644 --- a/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php +++ b/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php @@ -30,6 +30,7 @@ final class PhabricatorPhurlURLViewController $timeline = $this->buildTransactionTimeline( $url, new PhabricatorPhurlURLTransactionQuery()); + $timeline->setQuoteRef($url->getMonogram()); $header = $this->buildHeaderView($url); $curtain = $this->buildCurtain($url); @@ -39,20 +40,7 @@ final class PhabricatorPhurlURLViewController ->setErrors(array(pht('This URL is invalid due to a bad protocol.'))) ->setIsHidden($url->isValid()); - $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); - $add_comment_header = $is_serious - ? pht('Add Comment') - : pht('More Cowbell'); - $draft = PhabricatorDraft::newFromUserAndKey($viewer, $url->getPHID()); - $comment_uri = $this->getApplicationURI( - '/url/comment/'.$url->getID().'/'); - $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) - ->setUser($viewer) - ->setObjectPHID($url->getPHID()) - ->setDraft($draft) - ->setHeaderText($add_comment_header) - ->setAction($comment_uri) - ->setSubmitButtonName(pht('Add Comment')); + $add_comment_form = $this->buildCommentForm($url, $timeline); $view = id(new PHUITwoColumnView()) ->setHeader($header) @@ -72,7 +60,16 @@ final class PhabricatorPhurlURLViewController array( $view, )); + } + private function buildCommentForm(PhabricatorPhurlURL $url, $timeline) { + $viewer = $this->getViewer(); + $box = id(new PhabricatorPhurlURLEditEngine()) + ->setViewer($viewer) + ->buildEditEngineCommentView($url) + ->setTransactionTimeline($timeline); + + return $box; } private function buildHeaderView(PhabricatorPhurlURL $url) { diff --git a/src/applications/search/interface/PhabricatorFulltextInterface.php b/src/applications/search/interface/PhabricatorFulltextInterface.php index 5cbde615b2..e8f4cca29f 100644 --- a/src/applications/search/interface/PhabricatorFulltextInterface.php +++ b/src/applications/search/interface/PhabricatorFulltextInterface.php @@ -1,6 +1,7 @@ setAncestorClass('PhabricatorFulltextInterface') + ->setAncestorClass('PhabricatorIndexableInterface') ->execute(); $normalized_type = phutil_utf8_strtolower($type); diff --git a/src/applications/settings/setting/PhabricatorConpherenceSoundSetting.php b/src/applications/settings/setting/PhabricatorConpherenceSoundSetting.php new file mode 100644 index 0000000000..77ff6eac46 --- /dev/null +++ b/src/applications/settings/setting/PhabricatorConpherenceSoundSetting.php @@ -0,0 +1,81 @@ + + ConpherenceRoomSettings::DEFAULT_RECEIVE_SOUND, + ConpherenceRoomSettings::SOUND_MENTION => + ConpherenceRoomSettings::DEFAULT_MENTION_SOUND, + ); + break; + case self::VALUE_CONPHERENCE_MENTION: + return array( + ConpherenceRoomSettings::SOUND_RECEIVE => + ConpherenceRoomSettings::DEFAULT_NO_SOUND, + ConpherenceRoomSettings::SOUND_MENTION => + ConpherenceRoomSettings::DEFAULT_MENTION_SOUND, + ); + break; + case self::VALUE_CONPHERENCE_SILENT: + return array( + ConpherenceRoomSettings::SOUND_RECEIVE => + ConpherenceRoomSettings::DEFAULT_NO_SOUND, + ConpherenceRoomSettings::SOUND_MENTION => + ConpherenceRoomSettings::DEFAULT_NO_SOUND, + ); + break; + } + } + + private static function getOptionsMap() { + return array( + self::VALUE_CONPHERENCE_SILENT => pht('No Sounds'), + // self::VALUE_CONPHERENCE_MENTION => pht('Mentions Only'), + self::VALUE_CONPHERENCE_ALL => pht('All Messages'), + ); + } + +} diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index eb6f88d65b..8362d1328d 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1801,7 +1801,10 @@ abstract class PhabricatorApplicationTransactionEditor $old = array_fuse($xaction->getOldValue()); } - $new = $xaction->getNewValue(); + return $this->getPHIDList($old, $xaction->getNewValue()); + } + + public function getPHIDList(array $old, array $new) { $new_add = idx($new, '+', array()); unset($new['+']); $new_rem = idx($new, '-', array()); diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php index a315a5b9cf..9d8510cdc9 100644 --- a/src/applications/transactions/storage/PhabricatorModularTransactionType.php +++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php @@ -309,4 +309,10 @@ abstract class PhabricatorModularTransactionType return $this->getStorage()->getIsCreateTransaction(); } + final protected function getPHIDList(array $old, array $new) { + $editor = $this->getEditor(); + + return $editor->getPHIDList($old, $new); + } + } diff --git a/src/docs/contributor/database.diviner b/src/docs/contributor/database.diviner index 59a7dc2b2c..aaea485dc6 100644 --- a/src/docs/contributor/database.diviner +++ b/src/docs/contributor/database.diviner @@ -10,7 +10,7 @@ Database System Phabricator uses MySQL or another MySQL-compatible database (like MariaDB or Amazon RDS). -Phabricator the InnoDB table engine. The only exception is the +Phabricator uses the InnoDB table engine. The only exception is the `search_documentfield` table which uses MyISAM because MySQL doesn't support fulltext search in InnoDB (recent versions do, but we haven't added support yet). @@ -102,7 +102,7 @@ An example of such usage can be found in column Primary Keys ============ -Most tables have auto-increment column named `id`. Adding an ID column is +Most tables have an auto-increment column named `id`. Adding an ID column is appropriate for most tables (even tables that have another natural unique key), as it improves consistency and makes it easier to perform generic operations on objects. @@ -134,12 +134,12 @@ eventually, but there isn't a strong case for them at the present time. PHIDs ===== -Each globally referencable object in Phabricator has its associated PHID +Each globally referencable object in Phabricator has an associated PHID ("Phabricator ID") which serves as a global identifier, similar to a GUID. We use PHIDs for referencing data in different databases. -We use both autoincrementing IDs and global PHIDs because each is useful in -different contexts. Autoincrementing IDs are meaningfully ordered and allow +We use both auto-incrementing IDs and global PHIDs because each is useful in +different contexts. Auto-incrementing IDs are meaningfully ordered and allow us to construct short, human-readable object names (like `D2258`) and URIs. Global PHIDs allow us to represent relationships between different types of objects in a homogeneous way. @@ -154,7 +154,7 @@ Transactions ============ Transactional code should be written using transactions. Example of such code is -inserting multiple records where one doesn't make sense without the other or +inserting multiple records where one doesn't make sense without the other, or selecting data later used for update. See chapter in @{class:LiskDAO}. Advanced Features @@ -195,12 +195,12 @@ set names: | Variable | Meaning | Notes | |---|---|---| -| {$NAMESPACE} | Storage Namespace | Defaults to `phabricator` | -| {$CHARSET} | Default Charset | Mostly used to specify table charset | -| {$COLLATE_TEXT} | Text Collation | For most text (case-sensitive) | -| {$COLLATE_SORT} | Sort Collation | For sortable text (case-insensitive) | -| {$CHARSET_FULLTEXT} | Fulltext Charset | Specify explicitly for fulltext | -| {$COLLATE_FULLTEXT} | Fulltext Collate | Specify explicitly for fulltext | +| `{$NAMESPACE}` | Storage Namespace | Defaults to `phabricator` | +| `{$CHARSET}` | Default Charset | Mostly used to specify table charset | +| `{$COLLATE_TEXT}` | Text Collation | For most text (case-sensitive) | +| `{$COLLATE_SORT}` | Sort Collation | For sortable text (case-insensitive) | +| `{$CHARSET_FULLTEXT}` | Fulltext Charset | Specify explicitly for fulltext | +| `{$COLLATE_FULLTEXT}` | Fulltext Collate | Specify explicitly for fulltext | **Test your patch**. Run `bin/storage upgrade` to test your patch. diff --git a/src/docs/flavor/project_history.diviner b/src/docs/flavor/project_history.diviner index bfdbe2682e..c3b5363d50 100644 --- a/src/docs/flavor/project_history.diviner +++ b/src/docs/flavor/project_history.diviner @@ -52,9 +52,9 @@ problems. I joined the new Dev Tools team around February 2010 and took over Diffcamp. I renamed it to Differential, moved it to a new Alite-based infrastructure with Javelin, and started making it somewhat less terrible. I eventually wrote -Diffusion and build Herald to replace a very difficult-to-use predecessor. These +Diffusion and built Herald to replace a very difficult-to-use predecessor. These tools were less negatively received than the older versions. By December 2010 I started open sourcing them; Haste became //Celerity// and Alite became //Aphront//. I wrote Maniphest to track open issues with the project in January -or February and we open sourced Phabricator in late April, shortly after I left -Facebook. +or February, left Facebook in April, and shortly after, we open sourced +Phabricator. diff --git a/src/docs/flavor/things_you_should_do_now.diviner b/src/docs/flavor/things_you_should_do_now.diviner index b4681bd0ca..0d3b4135ba 100644 --- a/src/docs/flavor/things_you_should_do_now.diviner +++ b/src/docs/flavor/things_you_should_do_now.diviner @@ -134,5 +134,5 @@ query escaping system the rest of the application does. Hopefully, whatever language you're writing in has good query libraries that can handle escaping for you. If so, use them. If you're using PHP and don't have -a solution in place yet, the Phabricator implementation of qsprintf() is similar -to Facebook's system and was successful there. +a solution in place yet, the Phabricator implementation of `qsprintf()` is +similar to Facebook's system and was successful there. diff --git a/src/docs/tech/chatbot.diviner b/src/docs/tech/chatbot.diviner deleted file mode 100644 index 6f79e2e465..0000000000 --- a/src/docs/tech/chatbot.diviner +++ /dev/null @@ -1,87 +0,0 @@ -@title Chat Bot Technical Documentation -@group bot - -Configuring and extending the chat bot. - -= Overview = - -Phabricator includes a simple chat bot daemon, which is primarily intended as -an example of how you can write an external script that interfaces with -Phabricator over Conduit and does some kind of useful work. If you use IRC or -another supported chat protocol, you can also have the bot hang out in your -channel. - -NOTE: The chat bot is somewhat experimental and not very mature. - -= Configuring the Bot = - -The bot reads a JSON configuration file. You can find an example in: - - resources/chatbot/example_config.json - -These are the configuration values it reads: - - - `server` String, required, the server to connect to. - - `port` Int, optional, the port to connect to (defaults to 6667). - - `ssl` Bool, optional, whether to connect via SSL or not (defaults to - false). - - `nick` String, nickname to use. - - `user` String, optional, username to use (defaults to `nick`). - - `pass` String, optional, password for server. - - `nickpass` String, optional, password for NickServ. - - `join` Array, list of channels to join. - - `handlers` Array, list of handlers to run. These are like plugins for the - bot. - - `conduit.uri`, `conduit.token` Conduit configuration, - see below. - - `notification.channels` Notification configuration, see below. - -= Handlers = - -You specify a list of "handlers", which are basically plugins or modules for -the bot. These are the default handlers available: - - - @{class:PhabricatorBotObjectNameHandler} This handler looks for users - mentioning Phabricator objects like "T123" and "D345" in chat, looks them - up, and says their name with a link to the object. Requires conduit. - - @{class:PhabricatorBotFeedNotificationHandler} This handler posts - notifications about changes to revisions to the channels listed in - `notification.channels`. - - @{class:PhabricatorBotLogHandler} This handler records chatlogs which can - be browsed in the Phabricator web interface. - - @{class:PhabricatorBotSymbolHandler} This handler posts responses to lookups - for symbols in Diffusion - - @{class:PhabricatorBotMacroHandler} This handler looks for users mentioning - macros, if found will convert image to ASCII and output in chat. Configure - with `macro.size` and `macro.aspect` - -You can also write your own handlers, by extending -@{class:PhabricatorBotHandler}. - -= Conduit = - -Some handlers (e.g., @{class:PhabricatorBotObjectNameHandler}) need to read data -from Phabricator over Conduit, Phabricator's HTTP API. You can use this method -to allow other scripts or programs to access Phabricator's data from different -servers and in different languages. - -To allow the bot to access Conduit, you need to create a user that it can login -with. To do this, login to Phabricator as an administrator and go to -`People -> Create New Account`. Create a new account and flag them as a -"Bot/Script". Then in your configuration file, set these parameters: - - - `conduit.uri` The URI for your Phabricator install, like - `http://phabricator.example.com/` - - `conduit.token` The user's conduit API token, from the "Conduit API Tokens" - tab in the user's administrative view. - -Now the bot should be able to connect to Phabricator via Conduit. - -= Starting the Bot = - -The bot is a Phabricator daemon, so start it with `phd`: - - ./bin/phd launch phabricatorbot - -If you have issues you can try `debug` instead of `launch`, see -@{article:Managing Daemons with phd} for more information. diff --git a/src/infrastructure/daemon/bot/PhabricatorBot.php b/src/infrastructure/daemon/bot/PhabricatorBot.php deleted file mode 100644 index b6efdb8b6d..0000000000 --- a/src/infrastructure/daemon/bot/PhabricatorBot.php +++ /dev/null @@ -1,170 +0,0 @@ -getArgv(); - if (count($argv) !== 1) { - throw new Exception( - pht( - 'Usage: %s %s', - __CLASS__, - '')); - } - - $json_raw = Filesystem::readFile($argv[0]); - try { - $config = phutil_json_decode($json_raw); - } catch (PhutilJSONParserException $ex) { - throw new PhutilProxyException( - pht("File '%s' is not valid JSON!", $argv[0]), - $ex); - } - - $nick = idx($config, 'nick', 'phabot'); - $handlers = idx($config, 'handlers', array()); - $protocol_adapter_class = idx( - $config, - 'protocol-adapter', - 'PhabricatorIRCProtocolAdapter'); - $this->pollFrequency = idx($config, 'poll-frequency', 1); - - $this->config = $config; - - foreach ($handlers as $handler) { - $obj = newv($handler, array($this)); - $this->handlers[] = $obj; - } - - $ca_bundle = idx($config, 'https.cabundle'); - if ($ca_bundle) { - HTTPSFuture::setGlobalCABundleFromPath($ca_bundle); - } - - $conduit_uri = idx($config, 'conduit.uri'); - if ($conduit_uri) { - $conduit_token = idx($config, 'conduit.token'); - - // Normalize the path component of the URI so users can enter the - // domain without the "/api/" part. - $conduit_uri = new PhutilURI($conduit_uri); - - $conduit_host = (string)$conduit_uri->setPath('/'); - $conduit_uri = (string)$conduit_uri->setPath('/api/'); - - $conduit = new ConduitClient($conduit_uri); - if ($conduit_token) { - $conduit->setConduitToken($conduit_token); - } else { - $conduit_user = idx($config, 'conduit.user'); - $conduit_cert = idx($config, 'conduit.cert'); - - $response = $conduit->callMethodSynchronous( - 'conduit.connect', - array( - 'client' => __CLASS__, - 'clientVersion' => '1.0', - 'clientDescription' => php_uname('n').':'.$nick, - 'host' => $conduit_host, - 'user' => $conduit_user, - 'certificate' => $conduit_cert, - )); - } - - $this->conduit = $conduit; - } - - // Instantiate Protocol Adapter, for now follow same technique as - // handler instantiation - $this->protocolAdapter = newv($protocol_adapter_class, array()); - $this->protocolAdapter - ->setConfig($this->config) - ->connect(); - - $this->runLoop(); - - $this->protocolAdapter->disconnect(); - } - - public function getConfig($key, $default = null) { - return idx($this->config, $key, $default); - } - - private function runLoop() { - do { - PhabricatorCaches::destroyRequestCache(); - - $this->stillWorking(); - - $messages = $this->protocolAdapter->getNextMessages($this->pollFrequency); - if (count($messages) > 0) { - foreach ($messages as $message) { - $this->routeMessage($message); - } - } - - foreach ($this->handlers as $handler) { - $handler->runBackgroundTasks(); - } - } while (!$this->shouldExit()); - - } - - public function writeMessage(PhabricatorBotMessage $message) { - return $this->protocolAdapter->writeMessage($message); - } - - private function routeMessage(PhabricatorBotMessage $message) { - $ignore = $this->getConfig('ignore'); - if ($ignore) { - $sender = $message->getSender(); - if ($sender && in_array($sender->getName(), $ignore)) { - return; - } - } - - if ($message->getCommand() == 'LOG') { - $this->log('[LOG] '.$message->getBody()); - } - - foreach ($this->handlers as $handler) { - try { - $handler->receiveMessage($message); - } catch (Exception $ex) { - phlog($ex); - } - } - } - - public function getAdapter() { - return $this->protocolAdapter; - } - - public function getConduit() { - if (empty($this->conduit)) { - throw new Exception( - pht( - "This bot is not configured with a Conduit uplink. Set '%s' and ". - "'%s' in the configuration to connect.", - 'conduit.uri', - 'conduit.token')); - } - return $this->conduit; - } - -} diff --git a/src/infrastructure/daemon/bot/PhabricatorBotMessage.php b/src/infrastructure/daemon/bot/PhabricatorBotMessage.php deleted file mode 100644 index 64de22f2cf..0000000000 --- a/src/infrastructure/daemon/bot/PhabricatorBotMessage.php +++ /dev/null @@ -1,52 +0,0 @@ -public = true; - } - - public function setSender(PhabricatorBotTarget $sender = null) { - $this->sender = $sender; - return $this; - } - - public function getSender() { - return $this->sender; - } - - public function setCommand($command) { - $this->command = $command; - return $this; - } - - public function getCommand() { - return $this->command; - } - - public function setBody($body) { - $this->body = $body; - return $this; - } - - public function getBody() { - return $this->body; - } - - public function setTarget(PhabricatorBotTarget $target = null) { - $this->target = $target; - return $this; - } - - public function getTarget() { - return $this->target; - } - -} diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php deleted file mode 100644 index eede9d5142..0000000000 --- a/src/infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php +++ /dev/null @@ -1,93 +0,0 @@ -getConfig('flowdock.organization'); - if (empty($organization)) { - $this->getConfig('organization'); - } - if (empty($organization)) { - throw new Exception( - '"flowdock.organization" configuration variable not set'); - } - - - $ssl = $this->getConfig('ssl'); - - $url = ($ssl) ? 'https://' : 'http://'; - $url .= "{$this->authtoken}@stream.flowdock.com"; - $url .= "/flows/{$organization}/{$channel}"; - return $url; - } - - protected function processMessage(array $m_obj) { - $command = null; - switch ($m_obj['event']) { - case 'message': - $command = 'MESSAGE'; - break; - default: - // For now, ignore anything which we don't otherwise know about. - break; - } - - if ($command === null) { - return false; - } - - // TODO: These should be usernames, not user IDs. - $sender = id(new PhabricatorBotUser()) - ->setName($m_obj['user']); - - $target = id(new PhabricatorBotChannel()) - ->setName($m_obj['flow']); - - return id(new PhabricatorBotMessage()) - ->setCommand($command) - ->setSender($sender) - ->setTarget($target) - ->setBody($m_obj['content']); - } - - public function writeMessage(PhabricatorBotMessage $message) { - switch ($message->getCommand()) { - case 'MESSAGE': - $this->speak( - $message->getBody(), - $message->getTarget()); - break; - } - } - - private function speak( - $body, - PhabricatorBotTarget $flow) { - // The $flow->getName() returns the flow's UUID, - // as such, the Flowdock API does not require the organization - // to be specified in the URI - $this->performPost( - '/messages', - array( - 'flow' => $flow->getName(), - 'event' => 'message', - 'content' => $body, - )); - } - - public function __destruct() { - if ($this->readHandles) { - foreach ($this->readHandles as $read_handle) { - curl_multi_remove_handle($this->multiHandle, $read_handle); - curl_close($read_handle); - } - } - - curl_multi_close($this->multiHandle); - } -} diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php deleted file mode 100644 index eee257880b..0000000000 --- a/src/infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php +++ /dev/null @@ -1,114 +0,0 @@ -getConfig('ssl'); - - $url = ($ssl) ? 'https://' : 'http://'; - $url .= "streaming.campfirenow.com/room/{$channel}/live.json"; - - return $url; - } - - protected function processMessage(array $m_obj) { - $command = null; - switch ($m_obj['type']) { - case 'TextMessage': - $command = 'MESSAGE'; - break; - case 'PasteMessage': - $command = 'PASTE'; - break; - default: - // For now, ignore anything which we don't otherwise know about. - break; - } - - if ($command === null) { - return false; - } - - // TODO: These should be usernames, not user IDs. - $sender = id(new PhabricatorBotUser()) - ->setName($m_obj['user_id']); - - $target = id(new PhabricatorBotChannel()) - ->setName($m_obj['room_id']); - - return id(new PhabricatorBotMessage()) - ->setCommand($command) - ->setSender($sender) - ->setTarget($target) - ->setBody($m_obj['body']); - } - - public function writeMessage(PhabricatorBotMessage $message) { - switch ($message->getCommand()) { - case 'MESSAGE': - $this->speak( - $message->getBody(), - $message->getTarget()); - break; - case 'SOUND': - $this->speak( - $message->getBody(), - $message->getTarget(), - 'SoundMessage'); - break; - case 'PASTE': - $this->speak( - $message->getBody(), - $message->getTarget(), - 'PasteMessage'); - break; - } - } - - protected function joinRoom($room_id) { - $this->performPost("/room/{$room_id}/join.json"); - $this->inRooms[$room_id] = true; - } - - private function leaveRoom($room_id) { - $this->performPost("/room/{$room_id}/leave.json"); - unset($this->inRooms[$room_id]); - } - - private function speak( - $message, - PhabricatorBotTarget $channel, - $type = 'TextMessage') { - - $room_id = $channel->getName(); - - $this->performPost( - "/room/{$room_id}/speak.json", - array( - 'message' => array( - 'type' => $type, - 'body' => $message, - ), - )); - } - - public function __destruct() { - foreach ($this->inRooms as $room_id => $ignored) { - $this->leaveRoom($room_id); - } - - if ($this->readHandles) { - foreach ($this->readHandles as $read_handle) { - curl_multi_remove_handle($this->multiHandle, $read_handle); - curl_close($read_handle); - } - } - - curl_multi_close($this->multiHandle); - } -} diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php deleted file mode 100644 index eee4974877..0000000000 --- a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php +++ /dev/null @@ -1,282 +0,0 @@ -getConfig('network', $this->getConfig('server')); - } - - // Hash map of command translations - public static $commandTranslations = array( - 'PRIVMSG' => 'MESSAGE', - ); - - public function connect() { - $nick = $this->getConfig('nick', 'phabot'); - $server = $this->getConfig('server'); - $port = $this->getConfig('port', 6667); - $pass = $this->getConfig('pass'); - $ssl = $this->getConfig('ssl', false); - $user = $this->getConfig('user', $nick); - - if (!preg_match('/^[A-Za-z0-9_`[{}^|\]\\-]+$/', $nick)) { - throw new Exception( - pht( - "Nickname '%s' is invalid!", - $nick)); - } - - $errno = null; - $error = null; - if (!$ssl) { - $socket = fsockopen($server, $port, $errno, $error); - } else { - $socket = fsockopen('ssl://'.$server, $port, $errno, $error); - } - if (!$socket) { - throw new Exception(pht('Failed to connect, #%d: %s', $errno, $error)); - } - $ok = stream_set_blocking($socket, false); - if (!$ok) { - throw new Exception(pht('Failed to set stream nonblocking.')); - } - - $this->socket = $socket; - if ($pass) { - $this->write("PASS {$pass}"); - } - $this->write("NICK {$nick}"); - $this->write("USER {$user} 0 * :{$user}"); - } - - public function getNextMessages($poll_frequency) { - $messages = array(); - - $read = array($this->socket); - if (strlen($this->writeBuffer)) { - $write = array($this->socket); - } else { - $write = array(); - } - $except = array(); - - $ok = @stream_select($read, $write, $except, $timeout_sec = 1); - if ($ok === false) { - // We may have been interrupted by a signal, like a SIGINT. Try - // selecting again. If the second select works, conclude that the failure - // was most likely because we were signaled. - $ok = @stream_select($read, $write, $except, $timeout_sec = 0); - if ($ok === false) { - throw new Exception(pht('%s failed!', 'stream_select()')); - } - } - - if ($read) { - // Test for connection termination; in PHP, fread() off a nonblocking, - // closed socket is empty string. - if (feof($this->socket)) { - // This indicates the connection was terminated on the other side, - // just exit via exception and let the overseer restart us after a - // delay so we can reconnect. - throw new Exception(pht('Remote host closed connection.')); - } - do { - $data = fread($this->socket, 4096); - if ($data === false) { - throw new Exception(pht('%s failed!', 'fread()')); - } else { - $messages[] = id(new PhabricatorBotMessage()) - ->setCommand('LOG') - ->setBody('>>> '.$data); - $this->readBuffer .= $data; - } - } while (strlen($data)); - } - - if ($write) { - do { - $len = fwrite($this->socket, $this->writeBuffer); - if ($len === false) { - throw new Exception(pht('%s failed!', 'fwrite()')); - } else if ($len === 0) { - break; - } else { - $messages[] = id(new PhabricatorBotMessage()) - ->setCommand('LOG') - ->setBody('>>> '.substr($this->writeBuffer, 0, $len)); - $this->writeBuffer = substr($this->writeBuffer, $len); - } - } while (strlen($this->writeBuffer)); - } - - while (($m = $this->processReadBuffer()) !== false) { - if ($m !== null) { - $messages[] = $m; - } - } - - return $messages; - } - - private function write($message) { - $this->writeBuffer .= $message."\r\n"; - return $this; - } - - public function writeMessage(PhabricatorBotMessage $message) { - switch ($message->getCommand()) { - case 'MESSAGE': - case 'PASTE': - $name = $message->getTarget()->getName(); - $body = $message->getBody(); - $this->write("PRIVMSG {$name} :{$body}"); - return true; - default: - return false; - } - } - - private function processReadBuffer() { - $until = strpos($this->readBuffer, "\r\n"); - if ($until === false) { - return false; - } - - $message = substr($this->readBuffer, 0, $until); - $this->readBuffer = substr($this->readBuffer, $until + 2); - - $pattern = - '/^'. - '(?::(?P(\S+?))(?:!\S*)? )?'. // This may not be present. - '(?P[A-Z0-9]+) '. - '(?P.*)'. - '$/'; - - $matches = null; - if (!preg_match($pattern, $message, $matches)) { - throw new Exception("Unexpected message from server: {$message}"); - } - - if ($this->handleIRCProtocol($matches)) { - return null; - } - - $command = $this->getBotCommand($matches['command']); - list($target, $body) = $this->parseMessageData($command, $matches['data']); - - if (!strlen($matches['sender'])) { - $sender = null; - } else { - $sender = id(new PhabricatorBotUser()) - ->setName($matches['sender']); - } - - $bot_message = id(new PhabricatorBotMessage()) - ->setSender($sender) - ->setCommand($command) - ->setTarget($target) - ->setBody($body); - - return $bot_message; - } - - private function handleIRCProtocol(array $matches) { - $data = $matches['data']; - switch ($matches['command']) { - case '433': // Nickname already in use - // If we receive this error, try appending "-1", "-2", etc. to the nick - $this->nickIncrement++; - $nick = $this->getConfig('nick', 'phabot').'-'.$this->nickIncrement; - $this->write("NICK {$nick}"); - return true; - case '422': // Error - no MOTD - case '376': // End of MOTD - $nickpass = $this->getConfig('nickpass'); - if ($nickpass) { - $this->write("PRIVMSG nickserv :IDENTIFY {$nickpass}"); - } - $join = $this->getConfig('join'); - if (!$join) { - throw new Exception(pht('Not configured to join any channels!')); - } - foreach ($join as $channel) { - $this->write("JOIN {$channel}"); - } - return true; - case 'PING': - $this->write("PONG {$data}"); - return true; - } - - return false; - } - - private function getBotCommand($irc_command) { - if (isset(self::$commandTranslations[$irc_command])) { - return self::$commandTranslations[$irc_command]; - } - - // We have no translation for this command, use as-is - return $irc_command; - } - - private function parseMessageData($command, $data) { - switch ($command) { - case 'MESSAGE': - $matches = null; - if (preg_match('/^(\S+)\s+:?(.*)$/', $data, $matches)) { - - $target_name = $matches[1]; - if (strncmp($target_name, '#', 1) === 0) { - $target = id(new PhabricatorBotChannel()) - ->setName($target_name); - } else { - $target = id(new PhabricatorBotUser()) - ->setName($target_name); - } - - return array( - $target, - rtrim($matches[2], "\r\n"), - ); - } - break; - } - - // By default we assume there is no target, only a body - return array( - null, - $data, - ); - } - - public function disconnect() { - // NOTE: FreeNode doesn't show quit messages if you've recently joined a - // channel, presumably to prevent some kind of abuse. If you're testing - // this, you may need to stay connected to the network for a few minutes - // before it works. If you disconnect too quickly, the server will replace - // your message with a "Client Quit" message. - - $quit = $this->getConfig('quit', pht('Shutting down.')); - $this->write("QUIT :{$quit}"); - - // Flush the write buffer. - while (strlen($this->writeBuffer)) { - $this->getNextMessages(0); - } - - @fclose($this->socket); - $this->socket = null; - } -} diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php deleted file mode 100644 index 89283264b8..0000000000 --- a/src/infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php +++ /dev/null @@ -1,62 +0,0 @@ -config = $config; - return $this; - } - - public function getConfig($key, $default = null) { - return idx($this->config, $key, $default); - } - - /** - * Performs any connection logic necessary for the protocol - */ - abstract public function connect(); - - /** - * Disconnect from the service. - */ - public function disconnect() { - return; - } - - /** - * This is the spout for messages coming in from the protocol. - * This will be called in the main event loop of the bot daemon - * So if if doesn't implement some sort of blocking timeout - * (e.g. select-based socket polling), it should at least sleep - * for some period of time in order to not overwhelm the processor. - * - * @param Int $poll_frequency The number of seconds between polls - */ - abstract public function getNextMessages($poll_frequency); - - /** - * This is the output mechanism for the protocol. - * - * @param PhabricatorBotMessage $message The message to write - */ - abstract public function writeMessage(PhabricatorBotMessage $message); - - /** - * String identifying the service type the adapter provides access to, like - * "irc", "campfire", "flowdock", "hipchat", etc. - */ - abstract public function getServiceType(); - - /** - * String identifying the service name the adapter is connecting to. This is - * used to distinguish between instances of a service. For example, for IRC, - * this should return the IRC network the client is connecting to. - */ - abstract public function getServiceName(); - -} diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php deleted file mode 100644 index 95955c1c2a..0000000000 --- a/src/infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php +++ /dev/null @@ -1,170 +0,0 @@ -server); - return $uri->getDomain(); - } - - public function connect() { - $this->server = $this->getConfig('server'); - $this->authtoken = $this->getConfig('authtoken'); - $rooms = $this->getConfig('join'); - - // First, join the room - if (!$rooms) { - throw new Exception(pht('Not configured to join any rooms!')); - } - - $this->readBuffers = array(); - - // Set up our long poll in a curl multi request so we can - // continue running while it executes in the background - $this->multiHandle = curl_multi_init(); - $this->readHandles = array(); - - foreach ($rooms as $room_id) { - $this->joinRoom($room_id); - - // Set up the curl stream for reading - $url = $this->buildStreamingUrl($room_id); - $ch = $this->readHandles[$url] = curl_init(); - - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt( - $ch, - CURLOPT_USERPWD, - $this->authtoken.':x'); - - curl_setopt( - $ch, - CURLOPT_HTTPHEADER, - array('Content-type: application/json')); - curl_setopt( - $ch, - CURLOPT_WRITEFUNCTION, - array($this, 'read')); - curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); - curl_setopt($ch, CURLOPT_TIMEOUT, 0); - - curl_multi_add_handle($this->multiHandle, $ch); - - // Initialize read buffer - $this->readBuffers[$url] = ''; - } - - $this->active = null; - $this->blockingMultiExec(); - } - - protected function joinRoom($room_id) { - // Optional hook, by default, do nothing - } - - // This is our callback for the background curl multi-request. - // Puts the data read in on the readBuffer for processing. - private function read($ch, $data) { - $info = curl_getinfo($ch); - $length = strlen($data); - $this->readBuffers[$info['url']] .= $data; - return $length; - } - - private function blockingMultiExec() { - do { - $status = curl_multi_exec($this->multiHandle, $this->active); - } while ($status == CURLM_CALL_MULTI_PERFORM); - - // Check for errors - if ($status != CURLM_OK) { - throw new Exception( - pht('Phabricator Bot had a problem reading from stream.')); - } - } - - public function getNextMessages($poll_frequency) { - $messages = array(); - - if (!$this->active) { - throw new Exception(pht('Phabricator Bot stopped reading from stream.')); - } - - // Prod our http request - curl_multi_select($this->multiHandle, $poll_frequency); - $this->blockingMultiExec(); - - // Process anything waiting on the read buffer - while ($m = $this->processReadBuffer()) { - $messages[] = $m; - } - - return $messages; - } - - private function processReadBuffer() { - foreach ($this->readBuffers as $url => &$buffer) { - $until = strpos($buffer, "}\r"); - if ($until == false) { - continue; - } - - $message = substr($buffer, 0, $until + 1); - $buffer = substr($buffer, $until + 2); - - $m_obj = phutil_json_decode($message); - if ($message = $this->processMessage($m_obj)) { - return $message; - } - } - - // If we're here, there's nothing to process - return false; - } - - protected function performPost($endpoint, $data = null) { - $uri = new PhutilURI($this->server); - $uri->setPath($endpoint); - - $payload = json_encode($data); - - list($output) = id(new HTTPSFuture($uri)) - ->setMethod('POST') - ->addHeader('Content-Type', 'application/json') - ->addHeader('Authorization', $this->getAuthorizationHeader()) - ->setData($payload) - ->resolvex(); - - $output = trim($output); - if (strlen($output)) { - return phutil_json_decode($output); - } - - return true; - } - - protected function getAuthorizationHeader() { - return 'Basic '.$this->getEncodedAuthToken(); - } - - protected function getEncodedAuthToken() { - return base64_encode($this->authtoken.':x'); - } - - abstract protected function buildStreamingUrl($channel); - - abstract protected function processMessage(array $raw_object); - -} diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php deleted file mode 100644 index eb4f9b24a9..0000000000 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php +++ /dev/null @@ -1,17 +0,0 @@ -getCommand()) { - case 'LOG': - echo addcslashes( - $message->getBody(), - "\0..\37\177..\377"); - echo "\n"; - break; - } - } -} diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php deleted file mode 100644 index 21eadf1569..0000000000 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php +++ /dev/null @@ -1,180 +0,0 @@ -getConfig('notification.types'); - - if ($show) { - $obj_type = phid_get_type($story_objectphid); - if (!in_array(strtolower($obj_type), $show)) { - return false; - } - } - - $verbosity = $this->getConfig('notification.verbosity', 3); - - $verbs = array(); - - switch ($verbosity) { - case 2: - $verbs[] = array( - 'commented', - 'added', - 'changed', - 'resigned', - 'explained', - 'modified', - 'attached', - 'edited', - 'joined', - 'left', - 'removed', - ); - // fallthrough - case 1: - $verbs[] = array( - 'updated', - 'accepted', - 'requested', - 'planned', - 'claimed', - 'summarized', - 'commandeered', - 'assigned', - ); - // fallthrough - case 0: - $verbs[] = array( - 'created', - 'closed', - 'raised', - 'committed', - 'abandoned', - 'reclaimed', - 'reopened', - 'deleted', - ); - break; - - case 3: - default: - return true; - } - - $verbs = '/('.implode('|', array_mergev($verbs)).')/'; - - if (preg_match($verbs, $story_text)) { - return true; - } - - return false; - } - - public function receiveMessage(PhabricatorBotMessage $message) { - return; - } - - public function runBackgroundTasks() { - if ($this->startupDelay > 0) { - // the event loop runs every 1s so delay enough to fully conenct - $this->startupDelay--; - - return; - } - if ($this->lastSeenChronoKey == 0) { - // Since we only want to post notifications about new stories, skip - // everything that's happened in the past when we start up so we'll - // only process real-time stories. - $latest = $this->getConduit()->callMethodSynchronous( - 'feed.query', - array( - 'limit' => 1, - )); - - foreach ($latest as $story) { - if ($story['chronologicalKey'] > $this->lastSeenChronoKey) { - $this->lastSeenChronoKey = $story['chronologicalKey']; - } - } - - return; - } - - $config_max_pages = $this->getConfig('notification.max_pages', 5); - $config_page_size = $this->getConfig('notification.page_size', 10); - - $last_seen_chrono_key = $this->lastSeenChronoKey; - $chrono_key_cursor = 0; - - // Not efficient but works due to feed.query API - for ($max_pages = $config_max_pages; $max_pages > 0; $max_pages--) { - $stories = $this->getConduit()->callMethodSynchronous( - 'feed.query', - array( - 'limit' => $config_page_size, - 'after' => $chrono_key_cursor, - 'view' => 'text', - )); - - foreach ($stories as $story) { - if ($story['chronologicalKey'] == $last_seen_chrono_key) { - // Caught up on feed - return; - } - if ($story['chronologicalKey'] > $this->lastSeenChronoKey) { - // Keep track of newest seen story - $this->lastSeenChronoKey = $story['chronologicalKey']; - } - if (!$chrono_key_cursor || - $story['chronologicalKey'] < $chrono_key_cursor) { - // Keep track of oldest story on this page - $chrono_key_cursor = $story['chronologicalKey']; - } - - if (!$story['text'] || - !$this->shouldShowStory($story)) { - continue; - } - - $message = $story['text']; - - $story_object_type = phid_get_type($story['objectPHID']); - if (in_array($story_object_type, $this->typesNeedURI)) { - $objects = $this->getConduit()->callMethodSynchronous( - 'phid.lookup', - array( - 'names' => array($story['objectPHID']), - )); - $message .= ' '.$objects[$story['objectPHID']]['uri']; - } - - $channels = $this->getConfig('join'); - foreach ($channels as $channel_name) { - - $channel = id(new PhabricatorBotChannel()) - ->setName($channel_name); - - $this->writeMessage( - id(new PhabricatorBotMessage()) - ->setCommand('MESSAGE') - ->setTarget($channel) - ->setBody($message)); - } - } - } - } - -} diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php deleted file mode 100644 index dad7cbd1be..0000000000 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php +++ /dev/null @@ -1,72 +0,0 @@ -bot = $irc_bot; - } - - final protected function writeMessage(PhabricatorBotMessage $message) { - $this->bot->writeMessage($message); - return $this; - } - - final protected function getConduit() { - return $this->bot->getConduit(); - } - - final protected function getConfig($key, $default = null) { - return $this->bot->getConfig($key, $default); - } - - final protected function getURI($path) { - $base_uri = new PhutilURI($this->bot->getConfig('conduit.uri')); - $base_uri->setPath($path); - return (string)$base_uri; - } - - final protected function getServiceName() { - return $this->bot->getAdapter()->getServiceName(); - } - - final protected function getServiceType() { - return $this->bot->getAdapter()->getServiceType(); - } - - abstract public function receiveMessage(PhabricatorBotMessage $message); - - public function runBackgroundTasks() { - return; - } - - public function replyTo(PhabricatorBotMessage $original_message, $body) { - if ($original_message->getCommand() != 'MESSAGE') { - throw new Exception( - pht('Handler is trying to reply to something which is not a message!')); - } - - $reply = id(new PhabricatorBotMessage()) - ->setCommand('MESSAGE'); - - if ($original_message->getTarget()->isPublic()) { - // This is a public target, like a chatroom. Send the response to the - // chatroom. - $reply->setTarget($original_message->getTarget()); - } else { - // This is a private target, like a private message. Send the response - // back to the sender (presumably, we are the target). - $reply->setTarget($original_message->getSender()); - } - - $reply->setBody($body); - - return $this->writeMessage($reply); - } - -} diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php deleted file mode 100644 index 4f0a9dee35..0000000000 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php +++ /dev/null @@ -1,77 +0,0 @@ -getCommand()) { - case 'MESSAGE': - $target = $message->getTarget(); - if (!$target->isPublic()) { - // Don't log private messages, although maybe we should for debugging? - break; - } - - $target_name = $target->getName(); - - $logs = array( - array( - 'channel' => $target_name, - 'type' => 'mesg', - 'epoch' => time(), - 'author' => $message->getSender()->getName(), - 'message' => $message->getBody(), - 'serviceName' => $this->getServiceName(), - 'serviceType' => $this->getServiceType(), - ), - ); - - $this->futures[] = $this->getConduit()->callMethod( - 'chatlog.record', - array( - 'logs' => $logs, - )); - - $prompts = array( - '/where is the (chat)?log\?/i', - '/where am i\?/i', - '/what year is (this|it)\?/i', - ); - - $tell = false; - foreach ($prompts as $prompt) { - if (preg_match($prompt, $message->getBody())) { - $tell = true; - break; - } - } - - if ($tell) { - $response = $this->getURI( - '/chatlog/channel/'.phutil_escape_uri($target_name).'/'); - - $this->replyTo($message, $response); - } - - break; - } - } - - public function runBackgroundTasks() { - foreach ($this->futures as $key => $future) { - try { - if ($future->isReady()) { - unset($this->futures[$key]); - } - } catch (Exception $ex) { - unset($this->futures[$key]); - phlog($ex); - } - } - } - -} diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php deleted file mode 100644 index 9e7a051a8d..0000000000 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php +++ /dev/null @@ -1,176 +0,0 @@ -macros === false) { - return false; - } - - if ($this->macros !== null) { - return true; - } - - $macros = $this->getConduit()->callMethodSynchronous( - 'macro.query', - array()); - - // If we have no macros, cache `false` (meaning "no macros") and return - // immediately. - if (!$macros) { - $this->macros = false; - return false; - } - - $regexp = array(); - foreach ($macros as $macro_name => $macro) { - $regexp[] = preg_quote($macro_name, '/'); - } - $regexp = '/^('.implode('|', $regexp).')\z/'; - - $this->macros = $macros; - $this->regexp = $regexp; - - return true; - } - - public function receiveMessage(PhabricatorBotMessage $message) { - if (!$this->init()) { - return; - } - - switch ($message->getCommand()) { - case 'MESSAGE': - $message_body = $message->getBody(); - - $matches = null; - if (!preg_match($this->regexp, trim($message_body), $matches)) { - return; - } - - $macro = $matches[1]; - - $ascii = idx($this->macros[$macro], 'ascii'); - if ($ascii === false) { - return; - } - - if (!$ascii) { - $this->macros[$macro]['ascii'] = $this->rasterize( - $this->macros[$macro], - $this->getConfig('macro.size', 48), - $this->getConfig('macro.aspect', 0.66)); - $ascii = $this->macros[$macro]['ascii']; - } - - if ($ascii === false) { - // If we failed to rasterize the macro, bail out. - return; - } - - $target_name = $message->getTarget()->getName(); - foreach ($ascii as $line) { - $this->replyTo($message, $line); - } - break; - } - } - - public function rasterize($macro, $size, $aspect) { - try { - $image = $this->getConduit()->callMethodSynchronous( - 'file.download', - array( - 'phid' => $macro['filePHID'], - )); - $image = base64_decode($image); - } catch (Exception $ex) { - return false; - } - - if (!$image) { - return false; - } - - $img = @imagecreatefromstring($image); - if (!$img) { - return false; - } - - $sx = imagesx($img); - $sy = imagesy($img); - - if ($sx > $size || $sy > $size) { - $scale = max($sx, $sy) / $size; - $dx = floor($sx / $scale); - $dy = floor($sy / $scale); - } else { - $dx = $sx; - $dy = $sy; - } - - $dy = floor($dy * $aspect); - - $dst = imagecreatetruecolor($dx, $dy); - if (!$dst) { - return false; - } - imagealphablending($dst, false); - - $ok = imagecopyresampled( - $dst, $img, - 0, 0, - 0, 0, - $dx, $dy, - $sx, $sy); - - if (!$ok) { - return false; - } - - $map = array( - ' ', - '.', - ',', - ':', - ';', - '!', - '|', - '*', - '=', - '@', - '$', - '#', - ); - - $lines = array(); - - for ($ii = 0; $ii < $dy; $ii++) { - $buf = ''; - for ($jj = 0; $jj < $dx; $jj++) { - $c = imagecolorat($dst, $jj, $ii); - - $a = ($c >> 24) & 0xFF; - $r = ($c >> 16) & 0xFF; - $g = ($c >> 8) & 0xFF; - $b = ($c) & 0xFF; - - $luma = (255 - ((0.30 * $r) + (0.59 * $g) + (0.11 * $b))) / 256; - $luma *= ((127 - $a) / 127); - - $char = $map[max(0, floor($luma * count($map)))]; - $buf .= $char; - } - - $lines[] = $buf; - } - - return $lines; - } - -} diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php deleted file mode 100644 index 11c92af2cf..0000000000 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php +++ /dev/null @@ -1,206 +0,0 @@ -getCommand()) { - case 'MESSAGE': - $message = $original_message->getBody(); - $matches = null; - - $paste_ids = array(); - $commit_names = array(); - $vote_ids = array(); - $file_ids = array(); - $object_names = array(); - $output = array(); - - $pattern = - '@'. - '(?getConduit()->callMethodSynchronous( - 'phid.lookup', - array( - 'names' => $object_names, - )); - foreach ($objects as $object) { - $output[$object['phid']] = $object['fullName'].' - '.$object['uri']; - } - } - - if ($vote_ids) { - foreach ($vote_ids as $vote_id) { - $vote = $this->getConduit()->callMethodSynchronous( - 'slowvote.info', - array( - 'poll_id' => $vote_id, - )); - $output[$vote['phid']] = 'V'.$vote['id'].': '.$vote['question']. - ' '.pht('Come Vote').' '.$vote['uri']; - } - } - - if ($file_ids) { - foreach ($file_ids as $file_id) { - $file = $this->getConduit()->callMethodSynchronous( - 'file.info', - array( - 'id' => $file_id, - )); - $output[$file['phid']] = $file['objectName'].': '. - $file['uri'].' - '.$file['name']; - } - } - - if ($paste_ids) { - foreach ($paste_ids as $paste_id) { - $paste = $this->getConduit()->callMethodSynchronous( - 'paste.query', - array( - 'ids' => array($paste_id), - )); - $paste = head($paste); - - $output[$paste['phid']] = 'P'.$paste['id'].': '.$paste['uri'].' - '. - $paste['title']; - - if ($paste['language']) { - $output[$paste['phid']] .= ' ('.$paste['language'].')'; - } - - $user = $this->getConduit()->callMethodSynchronous( - 'user.query', - array( - 'phids' => array($paste['authorPHID']), - )); - $user = head($user); - if ($user) { - $output[$paste['phid']] .= ' by '.$user['userName']; - } - } - } - - if ($commit_names) { - $commits = $this->getConduit()->callMethodSynchronous( - 'diffusion.querycommits', - array( - 'names' => $commit_names, - )); - foreach ($commits['data'] as $commit) { - $output[$commit['phid']] = $commit['uri']; - } - } - - foreach ($output as $phid => $description) { - - // Don't mention the same object more than once every 10 minutes - // in public channels, so we avoid spamming the chat over and over - // again for discussions of a specific revision, for example. - - $target_name = $original_message->getTarget()->getName(); - if (empty($this->recentlyMentioned[$target_name])) { - $this->recentlyMentioned[$target_name] = array(); - } - - $quiet_until = idx( - $this->recentlyMentioned[$target_name], - $phid, - 0) + (60 * 10); - - if (time() < $quiet_until) { - // Remain quiet on this channel. - continue; - } - - $this->recentlyMentioned[$target_name][$phid] = time(); - $this->replyTo($original_message, $description); - } - break; - } - } - -} diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php deleted file mode 100644 index 2e7ff01bef..0000000000 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php +++ /dev/null @@ -1,50 +0,0 @@ -?" - */ -final class PhabricatorBotSymbolHandler extends PhabricatorBotHandler { - - public function receiveMessage(PhabricatorBotMessage $message) { - switch ($message->getCommand()) { - case 'MESSAGE': - $text = $message->getBody(); - - $matches = null; - if (!preg_match('/where(?: in the world)? is (\S+?)\?/i', - $text, $matches)) { - break; - } - - $symbol = $matches[1]; - $results = $this->getConduit()->callMethodSynchronous( - 'diffusion.findsymbols', - array( - 'name' => $symbol, - )); - - $default_uri = $this->getURI('/diffusion/symbol/'.$symbol.'/'); - - if (count($results) > 1) { - $response = pht( - "Multiple symbols named '%s': %s", - $symbol, - $default_uri); - } else if (count($results) == 1) { - $result = head($results); - $response = - $result['type'].' '. - $result['name'].' '. - '('.$result['language'].'): '. - nonempty($result['uri'], $default_uri); - } else { - $response = pht("No symbol '%s' found anywhere.", $symbol); - } - - $this->replyTo($message, $response); - - break; - } - } - -} diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php deleted file mode 100644 index a7d7ad9051..0000000000 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php +++ /dev/null @@ -1,43 +0,0 @@ -getCommand()) { - case 'MESSAGE': - $message_body = $message->getBody(); - $now = time(); - - $prompt = '~what( i|\')?s new\?~i'; - if (preg_match($prompt, $message_body)) { - if ($now < $this->floodblock) { - return; - } - $this->floodblock = $now + 60; - $this->reportNew($message); - } - break; - } - } - - public function reportNew(PhabricatorBotMessage $message) { - $latest = $this->getConduit()->callMethodSynchronous( - 'feed.query', - array( - 'limit' => 5, - 'view' => 'text', - )); - - foreach ($latest as $feed_item) { - if (isset($feed_item['text'])) { - $this->replyTo($message, html_entity_decode($feed_item['text'])); - } - } - } - -} diff --git a/src/infrastructure/daemon/bot/target/PhabricatorBotChannel.php b/src/infrastructure/daemon/bot/target/PhabricatorBotChannel.php deleted file mode 100644 index 030df925b6..0000000000 --- a/src/infrastructure/daemon/bot/target/PhabricatorBotChannel.php +++ /dev/null @@ -1,12 +0,0 @@ -name = $name; - return $this; - } - - public function getName() { - return $this->name; - } - - abstract public function isPublic(); - -} diff --git a/src/infrastructure/daemon/bot/target/PhabricatorBotUser.php b/src/infrastructure/daemon/bot/target/PhabricatorBotUser.php deleted file mode 100644 index 1bc0c82219..0000000000 --- a/src/infrastructure/daemon/bot/target/PhabricatorBotUser.php +++ /dev/null @@ -1,12 +0,0 @@ - array(), 'db.badges' => array(), 'db.packages' => array(), + 'db.application' => array(), '0000.legacy.sql' => array( 'legacy' => 0, ), diff --git a/src/view/page/PhabricatorBarePageView.php b/src/view/page/PhabricatorBarePageView.php index ca9d432286..c9a3c5c5cd 100644 --- a/src/view/page/PhabricatorBarePageView.php +++ b/src/view/page/PhabricatorBarePageView.php @@ -72,7 +72,7 @@ class PhabricatorBarePageView extends AphrontPageView { 'name' => 'viewport', 'content' => 'width=device-width, '. 'initial-scale=1, '. - 'maximum-scale=1', + 'user-scalable=no', )); } diff --git a/src/view/phui/PHUIListItemView.php b/src/view/phui/PHUIListItemView.php index 1a6d26d917..e8a1940737 100644 --- a/src/view/phui/PHUIListItemView.php +++ b/src/view/phui/PHUIListItemView.php @@ -33,6 +33,7 @@ final class PHUIListItemView extends AphrontTagView { private $tooltip; private $actionIcon; private $actionIconHref; + private $count; public function setOpenInNewWindow($open_in_new_window) { $this->openInNewWindow = $open_in_new_window; @@ -111,6 +112,11 @@ final class PHUIListItemView extends AphrontTagView { return $this->icon; } + public function setCount($count) { + $this->count = $count; + return $this; + } + public function setIndented($indented) { $this->indented = $indented; return $this; @@ -337,6 +343,16 @@ final class PHUIListItemView extends AphrontTagView { $action_icon); } + $count = null; + if ($this->count) { + $count = phutil_tag( + 'span', + array( + 'class' => 'phui-list-item-count', + ), + $this->count); + } + $icons = $this->getIcons(); $list_item = javelin_tag( @@ -354,6 +370,7 @@ final class PHUIListItemView extends AphrontTagView { $icons, $this->renderChildren(), $name, + $count, )); return array($list_item, $action_link); diff --git a/src/view/phui/PHUIObjectBoxView.php b/src/view/phui/PHUIObjectBoxView.php index 1e1d9898ec..26044ef3a0 100644 --- a/src/view/phui/PHUIObjectBoxView.php +++ b/src/view/phui/PHUIObjectBoxView.php @@ -18,6 +18,7 @@ final class PHUIObjectBoxView extends AphrontTagView { private $table; private $collapsed = false; private $anchor; + private $pager; private $showAction; private $hideAction; @@ -126,6 +127,11 @@ final class PHUIObjectBoxView extends AphrontTagView { return $this; } + public function setPager(PHUIPagerView $pager) { + $this->pager = $pager; + return $this; + } + public function setAnchor(PhabricatorAnchorView $anchor) { $this->anchor = $anchor; return $this; @@ -294,6 +300,10 @@ final class PHUIObjectBoxView extends AphrontTagView { $lists = null; } + $pager = null; + if ($this->pager) { + $pager = phutil_tag_div('phui-object-box-pager', $this->pager); + } $content = array( ($this->showHideOpen == false ? $this->anchor : null), @@ -308,6 +318,7 @@ final class PHUIObjectBoxView extends AphrontTagView { ($this->showHideOpen == true ? $this->anchor : null), $lists, $this->table, + $pager, $this->renderChildren(), ); diff --git a/support/aphlict/server/aphlict_server.js b/support/aphlict/server/aphlict_server.js index a4089ef54a..f162e4937c 100644 --- a/support/aphlict/server/aphlict_server.js +++ b/support/aphlict/server/aphlict_server.js @@ -197,3 +197,8 @@ for (ii = 0; ii < aphlict_admins.length; ii++) { admin_server.setClientServers(aphlict_clients); admin_server.setPeerList(peer_list); } + +for (ii = 0; ii < aphlict_clients.length; ii++) { + var client_server = aphlict_clients[ii]; + client_server.setAdminServers(aphlict_admins); +} diff --git a/support/aphlict/server/lib/AphlictAdminServer.js b/support/aphlict/server/lib/AphlictAdminServer.js index 3cac0be3b5..dd428063c2 100644 --- a/support/aphlict/server/lib/AphlictAdminServer.js +++ b/support/aphlict/server/lib/AphlictAdminServer.js @@ -17,6 +17,7 @@ JX.install('AphlictAdminServer', { server.on('request', JX.bind(this, this._onrequest)); this._server = server; this._clientServers = []; + this._messageHistory = []; }, properties: { @@ -30,6 +31,7 @@ JX.install('AphlictAdminServer', { _messagesOut: null, _server: null, _startTime: null, + _messageHistory: null, getListenerLists: function(instance) { var clients = this.getClientServers(); @@ -121,14 +123,24 @@ JX.install('AphlictAdminServer', { total_count += list.getTotalListenerCount(); } + var now = new Date().getTime(); + + var history_size = this._messageHistory.length; + var history_age = null; + if (history_size) { + history_age = (now - this._messageHistory[0].timestamp); + } + var server_status = { 'instance': instance, - 'uptime': (new Date().getTime() - this._startTime), + 'uptime': (now - this._startTime), 'clients.active': active_count, 'clients.total': total_count, 'messages.in': this._messagesIn, 'messages.out': this._messagesOut, - 'version': 7 + 'version': 7, + 'history.size': history_size, + 'history.age': history_age }; response.writeHead(200, {'Content-Type': 'application/json'}); @@ -140,6 +152,16 @@ JX.install('AphlictAdminServer', { * Transmits a message to all subscribed listeners. */ _transmit: function(instance, message, response) { + var now = new Date().getTime(); + + this._messageHistory.push( + { + timestamp: now, + message: message + }); + + this._purgeHistory(); + var peer_list = this.getPeerList(); message = peer_list.addFingerprint(message); @@ -191,7 +213,50 @@ JX.install('AphlictAdminServer', { error); } } + }, + + getHistory: function(min_age) { + var history = this._messageHistory; + var results = []; + + for (var ii = 0; ii < history.length; ii++) { + if (history[ii].timestamp >= min_age) { + results.push(history[ii].message); + } + } + + return results; + }, + + _purgeHistory: function() { + var messages = this._messageHistory; + + // Maximum number of messages to retain. + var size_limit = 4096; + + // Find the index of the first item we're going to keep. If we have too + // many items, this will be somewhere past the beginning of the list. + var keep = Math.max(0, messages.length - size_limit); + + // Maximum number of milliseconds of history to retain. + var age_limit = 60000; + + // Move the index forward until we find an item that is recent enough + // to retain. + var now = new Date().getTime(); + var min_age = (now - age_limit); + for (keep; keep < messages.length; keep++) { + if (messages[keep].timestamp >= min_age) { + break; + } + } + + // Throw away extra messages. + if (keep) { + this._messageHistory.splice(0, keep); + } } + } }); diff --git a/support/aphlict/server/lib/AphlictClientServer.js b/support/aphlict/server/lib/AphlictClientServer.js index 1d4375cbba..0d23d4f4f1 100644 --- a/support/aphlict/server/lib/AphlictClientServer.js +++ b/support/aphlict/server/lib/AphlictClientServer.js @@ -16,10 +16,12 @@ JX.install('AphlictClientServer', { this._server = server; this._lists = {}; + this._adminServers = []; }, properties: { logger: null, + adminServers: null }, members: { @@ -33,6 +35,20 @@ JX.install('AphlictClientServer', { return this._lists[instance]; }, + getHistory: function(age) { + var results = []; + + var servers = this.getAdminServers(); + for (var ii = 0; ii < servers.length; ii++) { + var messages = servers[ii].getHistory(age); + for (var jj = 0; jj < messages.length; jj++) { + results.push(messages[jj]); + } + } + + return results; + }, + log: function() { var logger = this.getLogger(); if (!logger) { @@ -117,6 +133,38 @@ JX.install('AphlictClientServer', { listener.unsubscribe(message.data); break; + case 'replay': + var age = message.data.age || 60000; + var min_age = (new Date().getTime() - age); + + var old_messages = self.getHistory(min_age); + for (var ii = 0; ii < old_messages.length; ii++) { + var old_message = old_messages[ii]; + + if (!listener.isSubscribedToAny(old_message.subscribers)) { + continue; + } + + try { + listener.writeMessage(old_message); + } catch (error) { + break; + } + } + break; + + case 'ping': + var pong = { + type: 'pong' + }; + + try { + listener.writeMessage(pong); + } catch (error) { + // Ignore any issues here, we'll clean up elsewhere. + } + break; + default: log( 'Unrecognized command "%s".', diff --git a/webroot/rsrc/audio/basic/alert.mp3 b/webroot/rsrc/audio/basic/alert.mp3 new file mode 100644 index 0000000000..f296568381 Binary files /dev/null and b/webroot/rsrc/audio/basic/alert.mp3 differ diff --git a/webroot/rsrc/audio/basic/bing.mp3 b/webroot/rsrc/audio/basic/bing.mp3 new file mode 100644 index 0000000000..840990bf80 Binary files /dev/null and b/webroot/rsrc/audio/basic/bing.mp3 differ diff --git a/webroot/rsrc/audio/basic/pock.mp3 b/webroot/rsrc/audio/basic/pock.mp3 new file mode 100644 index 0000000000..009139bf63 Binary files /dev/null and b/webroot/rsrc/audio/basic/pock.mp3 differ diff --git a/webroot/rsrc/audio/basic/tap.mp3 b/webroot/rsrc/audio/basic/tap.mp3 new file mode 100644 index 0000000000..b497cfb13f Binary files /dev/null and b/webroot/rsrc/audio/basic/tap.mp3 differ diff --git a/webroot/rsrc/audio/basic/ting.mp3 b/webroot/rsrc/audio/basic/ting.mp3 new file mode 100644 index 0000000000..311e57642b Binary files /dev/null and b/webroot/rsrc/audio/basic/ting.mp3 differ diff --git a/webroot/rsrc/css/aphront/dark-console.css b/webroot/rsrc/css/aphront/dark-console.css index 40dabd7bf6..e1ddc5a16d 100644 --- a/webroot/rsrc/css/aphront/dark-console.css +++ b/webroot/rsrc/css/aphront/dark-console.css @@ -217,3 +217,21 @@ .dark-console-panel-error-details { display: none; } + +.dark-console-log-frame { + height: 500px; + overflow: auto; + background: #303030; + border: 1px solid #202020; + margin: 8px 0; +} + +.dark-console-log-message { + background: #404040; + padding: 8px; + margin: 2px; +} + +.dark-console-realtime .button { + margin-right: 8px; +} diff --git a/webroot/rsrc/css/application/conpherence/color.css b/webroot/rsrc/css/application/conpherence/color.css new file mode 100644 index 0000000000..5350afebec --- /dev/null +++ b/webroot/rsrc/css/application/conpherence/color.css @@ -0,0 +1,48 @@ +/** + * @provides conpherence-color-css + */ + +.conpherence-theme-blue .conpherence-menu-pane { + background-color: {$lightblue}; +} + +.conpherence-theme-blue .phui-basic-nav .phabricator-side-menu + .phui-list-item-selected { + border-left-color: {$blue}; +} + +.conpherence-theme-indigo .conpherence-menu-pane { + background-color: {$lightindigo}; +} + +.conpherence-theme-indigo .phui-basic-nav .phabricator-side-menu + .phui-list-item-selected { + border-left-color: {$indigo}; +} + +.conpherence-theme-peach .conpherence-menu-pane { + background-color: {$lightred}; +} + +.conpherence-theme-peach .phui-basic-nav .phabricator-side-menu + .phui-list-item-selected { + border-left-color: rgba(0,0,0,.25); +} + +.conpherence-theme-green .conpherence-menu-pane { + background-color: {$lightgreen}; +} + +.conpherence-theme-green .phui-basic-nav .phabricator-side-menu + .phui-list-item-selected { + border-left-color: {$green}; +} + +.conpherence-theme-pink .conpherence-menu-pane { + background-color: {$lightpink}; +} + +.conpherence-theme-pink .phui-basic-nav .phabricator-side-menu + .phui-list-item-selected { + border-left-color: {$pink}; +} diff --git a/webroot/rsrc/css/application/conpherence/header-pane.css b/webroot/rsrc/css/application/conpherence/header-pane.css index 11fe34e3f1..54f58682da 100644 --- a/webroot/rsrc/css/application/conpherence/header-pane.css +++ b/webroot/rsrc/css/application/conpherence/header-pane.css @@ -10,16 +10,27 @@ .conpherence-header-pane .phui-header-header { font-size: 16px; color: #000; + display: block; } .conpherence-header-pane .phui-header-subheader { - color: {$lightgreytext}; padding: 0; - font-size: 12px; margin: 0; +} + +.conpherence-header-pane .conpherence-header-topic .phui-tag-core { + color: {$sh-indigotext}; + padding: 0 4px; + font-size: 12px; + margin: 0 8px 0 0; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; + font-style: italic; +} + +.device-phone .conpherence-header-pane .conpherence-header-topic { + display: none; } .conpherence-header-pane .phui-header-col1 { @@ -36,6 +47,28 @@ left: 0; } +.conpherence-header-pane .policy-header-callout { + background: transparent; + font-size: 12px; + margin: 0 8px 0 0; + padding: 3px 0; + font-style: normal; +} + +.conpherence-header-pane .phui-header-subheader .policy-link { + color: {$lightbluetext}; +} + +.conpherence-header-pane .phui-header-subheader .policy-header-callout + .phui-icon-view { + font-size: 12px; + color: {$lightbluetext}; +} + +.conpherence-header-pane .phui-header-subheader .policy-link:hover { + color: {$darkbluetext}; +} + .conpherence-header-pane .phui-header-image-href { position: inherit; } diff --git a/webroot/rsrc/css/application/conpherence/menu.css b/webroot/rsrc/css/application/conpherence/menu.css index e0719cdcc8..b2702d94fd 100644 --- a/webroot/rsrc/css/application/conpherence/menu.css +++ b/webroot/rsrc/css/application/conpherence/menu.css @@ -21,17 +21,8 @@ background-color: {$page.sidenav}; } -.conpherence-menu-pane .phui-basic-nav .phabricator-side-menu - .phui-list-item-href { - padding: 4px 0 4px 8px; - } - -.conpherence-menu-pane.phabricator-side-menu .phui-list-item-view.hidden { - display: none; -} - -.phui-list-view.conpherence-menu { - margin-bottom: 20px; +.conpherence-menu-pane .phui-basic-nav .phabricator-side-menu { + background-color: transparent; } .conpherence-menu-pane.phabricator-side-menu .room-list-href { @@ -117,20 +108,10 @@ padding: 4px; } -.conpherence-menu .conpherence-selected { - background: rgba({$alphablack},0.05); - border-left: 4px solid {$sky}; -} - .conpherence-menu .phui-list-item-type-link .phui-list-item-href { padding: 8px 0 8px 8px; } -.device-desktop .conpherence-menu - .conpherence-selected.conpherence-menu-item-view:hover { - background-color: rgba({$alphablack},0.07); -} - .conpherence-menu .loading { font-style: italic; } @@ -179,11 +160,19 @@ font-size: {$smallestfontsize}; } -.conpherence-menu .hide-unread-count .conpherence-menu-item-unread-count, -.conpherence-menu .conpherence-selected .conpherence-menu-item-unread-count { +.conpherence-menu .hide-unread-count .phui-list-item-count { display: none; } +.phui-basic-nav .phabricator-side-menu .conpherence-menu + .phui-list-item-icon.phuihead-small { + display: block; + float: left; + height: 20px; + width: 20px; + margin: 0 7px 0 0; +} + .conpherence-menu .conpherence-menu-item-view .conpherence-menu-item-date { position: absolute; top: 15px; diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css index 4f83a19948..ab3c55ba22 100644 --- a/webroot/rsrc/css/application/conpherence/message-pane.css +++ b/webroot/rsrc/css/application/conpherence/message-pane.css @@ -9,7 +9,7 @@ position: fixed; left: 240px; right: 240px; - top: 102px; + top: 106px; bottom: 0px; min-width: 300px; width: auto; @@ -20,8 +20,6 @@ .device .loading .messages-loading-icon, .device .conpherence-layout .conpherence-no-threads { left: 0; - right: 0; - width: 100%; } .conpherence-layout .conpherence-content-pane .conpherence-no-threads { @@ -57,7 +55,7 @@ position: fixed; left: 240px; right: 240px; - top: 103px; + top: 106px; bottom: 142px; overflow-x: hidden; overflow-y: auto; @@ -360,7 +358,7 @@ body .conpherence-message-pane .aphront-form-control { .device .conpherence-message-pane .remarkup-assist-textarea { margin: 0; - padding: 7px 8px 6px 30px; + padding: 7px 8px 6px 38px; width: 100%; height: 34px; resize: none; @@ -447,9 +445,9 @@ body .conpherence-message-pane .aphront-form-control { .show-searchbar .conpherence-search-form-view { display: block; height: 54px; - background: {$lightbluebackground}; + background: #fff; position: absolute; - top: 1px; + top: 0; left: 0; right: 0; } diff --git a/webroot/rsrc/css/application/conpherence/participant-pane.css b/webroot/rsrc/css/application/conpherence/participant-pane.css index c72d098226..f219707e7b 100644 --- a/webroot/rsrc/css/application/conpherence/participant-pane.css +++ b/webroot/rsrc/css/application/conpherence/participant-pane.css @@ -5,7 +5,7 @@ .conpherence-participant-pane { position: fixed; right: 0px; - top: 103px; + top: 106px; bottom: 0; width: 240px; border-width: 0 0 0 1px; @@ -45,7 +45,7 @@ position: fixed; overflow-y: auto; bottom: 0; - top: 102px; + top: 105px; width: 240px; } diff --git a/webroot/rsrc/css/application/diffusion/diffusion-icons.css b/webroot/rsrc/css/application/diffusion/diffusion-icons.css index 0e00f6e46f..8edc034975 100644 --- a/webroot/rsrc/css/application/diffusion/diffusion-icons.css +++ b/webroot/rsrc/css/application/diffusion/diffusion-icons.css @@ -13,6 +13,11 @@ input.diffusion-clone-uri { color: {$lightgreytext}; } +.diffusion-clone-label { + height: 30px; + line-height: 28px; +} + .diffusion-browse-name { margin-left: 8px; } @@ -44,6 +49,6 @@ input.diffusion-clone-uri { } .diffusion-clone-uri-table th a.button .phui-icon-view { - left: 12px; + left: 15px; top: 7px; } diff --git a/webroot/rsrc/css/application/diffusion/diffusion-readme.css b/webroot/rsrc/css/application/diffusion/diffusion-readme.css index bb661bb2c8..b8e20af5cb 100644 --- a/webroot/rsrc/css/application/diffusion/diffusion-readme.css +++ b/webroot/rsrc/css/application/diffusion/diffusion-readme.css @@ -11,4 +11,9 @@ .diffusion-readme-view .phui-document-container { border: none; + padding: 24px 32px; +} + +.diffusion-readme-view .phabricator-remarkup-toc { + display: none; } diff --git a/webroot/rsrc/css/phui/phui-button.css b/webroot/rsrc/css/phui/phui-button.css index f726487a2d..09f2233a6c 100644 --- a/webroot/rsrc/css/phui/phui-button.css +++ b/webroot/rsrc/css/phui/phui-button.css @@ -82,7 +82,7 @@ a.grey, a.grey:visited { background-color: #F7F7F9; background-image: linear-gradient(to bottom, #ffffff, #f1f0f1); - border: 1px solid rgba({$alphablue},.2); + border: 1px solid rgba({$alphablue}, 0.3); color: {$darkgreytext}; } @@ -131,7 +131,7 @@ button:hover { a.button.grey:hover, button.grey:hover { background-image: linear-gradient(to bottom, #ffffff, #eeebec); - border-color: rgba({$alphablue}, 0.3); + border-color: rgba({$alphablue}, 0.4); transition: 0.1s; } @@ -314,7 +314,7 @@ a.policy-control .phui-button-text { } .phui-button-bar .button .phui-icon-view { - left: 9px; + left: 14px; } .button.has-icon .phui-button-text { diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css index 14e0af986f..5c24d5575e 100644 --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -120,7 +120,7 @@ body .phui-header-shell.phui-bleed-header .device-phone .phui-header-action-link .phui-button-text { visibility: hidden; width: 0; - margin-left: 8px; + margin-left: 4px; } .device-phone .phui-header-action-link.button .phui-icon-view { diff --git a/webroot/rsrc/css/phui/phui-object-box.css b/webroot/rsrc/css/phui/phui-object-box.css index dee0525944..4999a4c2c2 100644 --- a/webroot/rsrc/css/phui/phui-object-box.css +++ b/webroot/rsrc/css/phui/phui-object-box.css @@ -146,3 +146,15 @@ div.phui-object-box.phui-object-box-flush { padding: 4px 8px; background-color: {$lightgreybackground}; } + +/* - Pager at the bottom ---------------------------------------------------- */ + +.phui-object-box-pager { + background-color: {$bluebackground}; + border-top: 1px solid {$lightblueborder}; +} + +.phui-object-box-pager a.button { + margin-top: 8px; + margin-bottom: 8px; +} diff --git a/webroot/rsrc/css/phui/phui-remarkup-preview.css b/webroot/rsrc/css/phui/phui-remarkup-preview.css index c045cae703..19464a98f7 100644 --- a/webroot/rsrc/css/phui/phui-remarkup-preview.css +++ b/webroot/rsrc/css/phui/phui-remarkup-preview.css @@ -27,7 +27,8 @@ padding: 16px 0 0 16px; } -.device-phone .phui-panel-preview { +.device-phone .phui-panel-preview, +.device-phone .phui-remarkup-preview { display: none; } diff --git a/webroot/rsrc/externals/javelin/lib/Leader.js b/webroot/rsrc/externals/javelin/lib/Leader.js index 80ba7fcc44..c96e95a339 100644 --- a/webroot/rsrc/externals/javelin/lib/Leader.js +++ b/webroot/rsrc/externals/javelin/lib/Leader.js @@ -33,6 +33,8 @@ JX.install('Leader', { events: ['onBecomeLeader', 'onReceiveBroadcast'], statics: { + _leaseDuration: 1500, + _interval: null, _timeout: null, _broadcastKey: 'JX.Leader.broadcast', @@ -130,8 +132,10 @@ JX.install('Leader', { // If we haven't installed an update timer yet, do so now. This will // renew our lease every 5 seconds, making sure we hold it until the // tab is closed. - if (!self._interval && lease.until > now + 10000) { - self._interval = window.setInterval(self._write, 5000); + var interval = parseInt(self._leaseDuration / 3, 10); + + if (!self._interval && lease.until > now + (interval * 2)) { + self._interval = window.setInterval(self._write, interval); } self._becomeLeader(); @@ -227,7 +231,7 @@ JX.install('Leader', { _write: function() { var self = JX.Leader; - var str = [self._id, ((+new Date()) + 16000)].join(':'); + var str = [self._id, ((+new Date()) + self._leaseDuration)].join(':'); window.localStorage.setItem(self._leaderKey, str); }, @@ -311,8 +315,8 @@ JX.install('Leader', { */ _usurp: function() { var self = JX.Leader; - self.call(JX.bag); self._timeout = null; + self.call(JX.bag); }, diff --git a/webroot/rsrc/externals/javelin/lib/WebSocket.js b/webroot/rsrc/externals/javelin/lib/WebSocket.js index fbb704d762..508218cf3f 100644 --- a/webroot/rsrc/externals/javelin/lib/WebSocket.js +++ b/webroot/rsrc/externals/javelin/lib/WebSocket.js @@ -104,14 +104,40 @@ JX.install('WebSocket', { }, + /** + * Disconnect abruptly, prompting a reconnect. + */ + reconnect: function() { + if (!this._isOpen) { + return; + } + + this._socket.close(); + }, + + + /** + * Get the current reconnect delay (in milliseconds). + */ + getReconnectDelay: function() { + return this._delayUntilReconnect; + }, + + /** * Callback for connection open. */ _onopen: function() { this._isOpen = true; - // Reset the reconnect delay, since we connected successfully. - this._resetDelay(); + // Since we connected successfully, reset the reconnect delay to 0. + + // This will make us try the first reconnect immediately after a + // connection failure. This limits downtime in cases like a service + // restart or a load balancer connection timeout. + + // We only do an immediate retry after a successful connection. + this._delayUntilReconnect = 0; var handler = this.getOpenHandler(); if (handler) { @@ -170,7 +196,11 @@ JX.install('WebSocket', { // Increase the reconnect delay by a factor of 2. If we fail to open the // connection, the close handler will send us back here. We'll reconnect // more and more slowly until we eventually get a valid connection. - this._delayUntilReconnect = this._delayUntilReconnect * 2; + if (!this._delayUntilReconnect) { + this._resetDelay(); + } else { + this._delayUntilReconnect = this._delayUntilReconnect * 2; + } // Max out at 5 minutes between attempts. this._delayUntilReconnect = Math.min(this._delayUntilReconnect, 300000); diff --git a/webroot/rsrc/js/application/aphlict/Aphlict.js b/webroot/rsrc/js/application/aphlict/Aphlict.js index 78d0958c81..e27ff1b4eb 100644 --- a/webroot/rsrc/js/application/aphlict/Aphlict.js +++ b/webroot/rsrc/js/application/aphlict/Aphlict.js @@ -29,6 +29,7 @@ JX.install('Aphlict', { this._uri = uri; this._subscriptions = subscriptions; this._setStatus('setup'); + this._startTime = new Date().getTime(); JX.Aphlict._instance = this; }, @@ -40,6 +41,9 @@ JX.install('Aphlict', { _socket: null, _subscriptions: null, _status: null, + _isReconnect: false, + _keepaliveInterval: false, + _startTime: null, start: function() { JX.Leader.listen('onBecomeLeader', JX.bind(this, this._lead)); @@ -71,6 +75,10 @@ JX.install('Aphlict', { return this._status; }, + getWebsocket: function() { + return this._socket; + }, + _begin: function() { JX.Leader.broadcast( null, @@ -90,11 +98,62 @@ JX.install('Aphlict', { }, _open: function() { + // If this is a reconnect, ask the server to replay recent messages + // after other tabs have had a chance to subscribe. Do this before we + // broadcast that the connection status is now open. + if (this._isReconnect) { + setTimeout(JX.bind(this, this._didReconnect), 100); + } + this._broadcastStatus('open'); JX.Leader.broadcast(null, {type: 'aphlict.getsubscribers'}); + + // By default, ELBs terminate connections after 60 seconds with no + // traffic. Other load balancers may have similar configuration. Send + // a keepalive message every 15 seconds to prevent load balancers from + // deciding they can reap this connection. + + var keepalive = JX.bind(this, this._keepalive); + this._keepaliveInterval = setInterval(keepalive, 15000); + }, + + _didReconnect: function() { + this.replay(); + this.reconnect(); + }, + + replay: function() { + var age = 60000; + + // If the page was loaded a few moments ago, only query for recent + // history. This keeps us from replaying events over and over again as + // a user browses normally. + + // Allow a small margin of error for the actual page load time. It's + // also fine to replay a notification which the user saw for a brief + // moment on the previous page. + var extra_time = 500; + var now = new Date().getTime(); + + age = Math.min(extra_time + (now - this._startTime), age); + + var replay = { + age: age + }; + + JX.Leader.broadcast(null, {type: 'aphlict.replay', data: replay}); + }, + + reconnect: function() { + JX.Leader.broadcast(null, {type: 'aphlict.reconnect', data: null}); }, _close: function() { + if (this._keepaliveInterval) { + clearInterval(this._keepaliveInterval); + this._keepaliveInterval = null; + } + this._broadcastStatus('closed'); }, @@ -104,7 +163,14 @@ JX.install('Aphlict', { _message: function(raw) { var message = JX.JSON.parse(raw); - JX.Leader.broadcast(null, {type: 'aphlict.server', data: message}); + var id = message.uniqueID || null; + + // If this is just a keepalive response, don't bother broadcasting it. + if (message.type == 'pong') { + return; + } + + JX.Leader.broadcast(id, {type: 'aphlict.server', data: message}); }, _receive: function(message, is_leader) { @@ -127,10 +193,13 @@ JX.install('Aphlict', { case 'aphlict.subscribe': if (is_leader) { - this._write({ - command: 'subscribe', - data: message.data - }); + this._writeCommand('subscribe', message.data); + } + break; + + case 'aphlict.replay': + if (is_leader) { + this._writeCommand('replay', message.data); } break; @@ -143,11 +212,31 @@ JX.install('Aphlict', { _setStatus: function(status) { this._status = status; + + // If we've ever seen an open connection, any new connection we make + // is a reconnect and should replay history. + if (status == 'open') { + this._isReconnect = true; + } + this.invoke('didChangeStatus'); }, _write: function(message) { this._socket.send(JX.JSON.stringify(message)); + }, + + _writeCommand: function(command, message) { + var frame = { + command: command, + data: message + }; + + return this._write(frame); + }, + + _keepalive: function() { + this._writeCommand('ping', null); } }, diff --git a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js index 2e381d018c..333e8daac1 100644 --- a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js +++ b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js @@ -53,6 +53,10 @@ JX.behavior('aphlict-listen', function(config) { case 'notification.individual': JX.Stratcom.invoke('aphlict-notification-message', null, message.data); break; + + case 'aphlict.reconnect': + JX.Stratcom.invoke('aphlict-reconnect', null, message.data); + break; } } @@ -62,10 +66,12 @@ JX.behavior('aphlict-listen', function(config) { return; } - JX.Leader.broadcast(null, { - type: 'notification.individual', - data: response - }); + JX.Leader.broadcast( + response.uniqueID, + { + type: 'notification.individual', + data: response + }); } JX.Stratcom.listen('aphlict-notification-message', null, function(e) { @@ -114,9 +120,16 @@ JX.behavior('aphlict-listen', function(config) { config.websocketURI, config.subscriptions); - client - .setHandler(onAphlictMessage) - .start(); + var start_client = function() { + client + .setHandler(onAphlictMessage) + .start(); + }; + + // Don't start the client until other behaviors have had a chance to + // initialize. In particular, we want to capture events into the log for + // the DarkConsole "Realtime" panel. + setTimeout(start_client, 0); JX.Stratcom.listen( 'quicksand-redraw', diff --git a/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js b/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js index a39adb0ee0..81e10bf35b 100644 --- a/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js +++ b/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js @@ -240,6 +240,18 @@ JX.install('ConpherenceThreadManager', { this._updateThread(); })); + // If we see a reconnect, always update the thread state. + JX.Stratcom.listen( + 'aphlict-reconnect', + null, + JX.bind(this, function() { + if (!this._loadedThreadPHID) { + return; + } + + this._updateThread(); + })); + JX.Stratcom.listen( 'click', 'show-older-messages', @@ -305,6 +317,14 @@ JX.install('ConpherenceThreadManager', { this._updating.knownID = r.latest_transaction_id; this._latestTransactionID = r.latest_transaction_id; + + JX.Leader.broadcast( + 'conpherence.message.' + r.latest_transaction_id, + { + type: 'sound', + data: r.sound.receive + }); + JX.Stratcom.invoke( 'conpherence-redraw-aphlict', null, diff --git a/webroot/rsrc/js/application/conpherence/behavior-menu.js b/webroot/rsrc/js/application/conpherence/behavior-menu.js index 1621126a5e..a5566dc5a4 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-menu.js +++ b/webroot/rsrc/js/application/conpherence/behavior-menu.js @@ -25,6 +25,7 @@ JX.behavior('conpherence-menu', function(config) { }; var scrollbar = null; + var cur_theme = config.theme; // TODO - move more logic into the ThreadManager var threadManager = new JX.ConpherenceThreadManager(); @@ -70,7 +71,6 @@ JX.behavior('conpherence-menu', function(config) { var textarea = JX.DOM.find(form_root, 'textarea'); if (!non_update) { _scrollMessageWindow(); - textarea.value = ''; } markThreadLoading(false); @@ -125,10 +125,10 @@ JX.behavior('conpherence-menu', function(config) { function selectThread(node, update_page_data) { if (_thread.node) { - JX.DOM.alterClass(_thread.node, 'conpherence-selected', false); + JX.DOM.alterClass(_thread.node, 'phui-list-item-selected', false); } - JX.DOM.alterClass(node, 'conpherence-selected', true); + JX.DOM.alterClass(node, 'phui-list-item-selected', true); JX.DOM.alterClass(node, 'hide-unread-count', true); _thread.node = node; @@ -145,6 +145,14 @@ JX.behavior('conpherence-menu', function(config) { function updatePageData(data) { var uri = '/Z' + _thread.selected; + var new_theme = data.theme; + + if (new_theme != cur_theme) { + var root = JX.$('conpherence-main-layout'); + JX.DOM.alterClass(root, cur_theme, false); + JX.DOM.alterClass(root, new_theme, true); + cur_theme = new_theme; + } JX.History.replace(uri); if (data.title) { JX.Title.setTitle(data.title); @@ -425,20 +433,6 @@ JX.behavior('conpherence-menu', function(config) { } } - JX.Stratcom.listen( - ['click'], - 'conpherence-menu-see-more', - function (e) { - e.kill(); - var sigil = e.getNodeData('conpherence-menu-see-more').moreSigil; - var root = JX.$('conpherence-menu-pane'); - var more = JX.DOM.scry(root, 'li', sigil); - for (var i = 0; i < more.length; i++) { - JX.DOM.alterClass(more[i], 'hidden', false); - } - JX.DOM.hide(e.getNode('conpherence-menu-see-more')); - }); - JX.Stratcom.listen( ['keydown'], 'conpherence-pontificate', diff --git a/webroot/rsrc/js/application/diffusion/DiffusionLocateFileSource.js b/webroot/rsrc/js/application/diffusion/DiffusionLocateFileSource.js index f824410cfb..14894f6cc0 100644 --- a/webroot/rsrc/js/application/diffusion/DiffusionLocateFileSource.js +++ b/webroot/rsrc/js/application/diffusion/DiffusionLocateFileSource.js @@ -23,6 +23,11 @@ JX.install('DiffusionLocateFileSource', { ondata: function(results) { this.tree = results.tree; + + if (this.lastValue !== null) { + this.matchResults(this.lastValue); + } + this.setReady(true); }, diff --git a/webroot/rsrc/js/application/transactions/behavior-transaction-list.js b/webroot/rsrc/js/application/transactions/behavior-transaction-list.js index 854d97635e..fdcb055872 100644 --- a/webroot/rsrc/js/application/transactions/behavior-transaction-list.js +++ b/webroot/rsrc/js/application/transactions/behavior-transaction-list.js @@ -43,8 +43,10 @@ JX.behavior('phabricator-transaction-list', function() { e.prevent(); var data = e.getNodeData('transaction-quote'); + var ref = data.ref || ''; + new JX.Workflow(data.uri) - .setData({ref: data.ref}) + .setData({ref: ref}) .setHandler(function(r) { var textarea = JX.$(data.targetID); diff --git a/webroot/rsrc/js/core/darkconsole/DarkLog.js b/webroot/rsrc/js/core/darkconsole/DarkLog.js new file mode 100644 index 0000000000..d22f433e3e --- /dev/null +++ b/webroot/rsrc/js/core/darkconsole/DarkLog.js @@ -0,0 +1,47 @@ +/** + * @provides phabricator-darklog + * @javelin + */ + +JX.install('DarkLog', { + + construct: function() { + this._messages = []; + }, + + members: { + _node: null, + _messages: null, + + addMessage: function(message) { + var node = message.getNode(); + + this._messages.push(message); + if (this._node) { + this._append([node]); + } + + return this; + }, + + setNode: function(node) { + var nodes = []; + for (var ii = 0; ii < this._messages.length; ii++) { + nodes.push(this._messages[ii].getNode()); + } + + this._node = node; + this._append(nodes); + + return this; + }, + + _append: function(nodes) { + for (var ii = 0; ii < nodes.length; ii++) { + this._node.appendChild(nodes[ii]); + } + } + + } + +}); diff --git a/webroot/rsrc/js/core/darkconsole/DarkMessage.js b/webroot/rsrc/js/core/darkconsole/DarkMessage.js new file mode 100644 index 0000000000..2fd7697706 --- /dev/null +++ b/webroot/rsrc/js/core/darkconsole/DarkMessage.js @@ -0,0 +1,37 @@ +/** + * @provides phabricator-darkmessage + * @javelin + */ + +JX.install('DarkMessage', { + + construct: function() { + + }, + + members: { + _node: null, + _message: null, + + setMessage: function(message) { + this._message = message; + + JX.DOM.setContent(this.getNode(), message); + + return this; + }, + + getNode: function() { + if (!this._node) { + this._node = JX.$N( + 'div', + { + className: 'dark-console-log-message' + }); + } + + return this._node; + } + } + +}); diff --git a/webroot/rsrc/js/core/behavior-dark-console.js b/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js similarity index 72% rename from webroot/rsrc/js/core/behavior-dark-console.js rename to webroot/rsrc/js/core/darkconsole/behavior-dark-console.js index 3322d31227..13949432ff 100644 --- a/webroot/rsrc/js/core/behavior-dark-console.js +++ b/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js @@ -6,6 +6,8 @@ * javelin-dom * javelin-request * phabricator-keyboard-shortcut + * phabricator-darklog + * phabricator-darkmessage */ JX.behavior('dark-console', function(config, statics) { @@ -246,6 +248,12 @@ JX.behavior('dark-console', function(config, statics) { var div = JX.$N('div', {className: 'dark-console-panel-core'}, JX.$H(html)); JX.DOM.setContent(statics.el.panel, div); + + var params = { + panel: tclass + }; + + JX.Stratcom.invoke('darkconsole.draw', null, params); } function install_shortcut() { @@ -287,4 +295,113 @@ JX.behavior('dark-console', function(config, statics) { } add_request(config); + +/* -( Realtime Panel )----------------------------------------------------- */ + + + if (!statics.realtime) { + statics.realtime = true; + + var realtime_log = new JX.DarkLog(); + var realtime_id = 'dark-console-realtime-log'; + + JX.Stratcom.listen('darkconsole.draw', null, function(e) { + var data = e.getData(); + if (data.panel != 'DarkConsoleRealtimePlugin') { + return; + } + + var node = JX.$(realtime_id); + realtime_log.setNode(node); + }); + + // If the panel is initially visible, try rendering. + try { + var node = JX.$(realtime_id); + realtime_log.setNode(node); + } catch (exception) { + // Ignore. + } + + var leader_log = function(event_name, type, is_leader, details) { + var parts = []; + if (is_leader === true) { + parts.push('+'); + } else if (is_leader === false) { + parts.push('-'); + } else { + parts.push('~'); + } + + parts.push('[Leader/' + event_name + ']'); + + if (type) { + parts.push('(' + type + ')'); + } + + if (details) { + parts.push(details); + } + + parts = parts.join(' '); + + var message = new JX.DarkMessage() + .setMessage(parts); + + realtime_log.addMessage(message); + }; + + JX.Leader.listen('onReceiveBroadcast', function(message, is_leader) { + var json = JX.JSON.stringify(message.data); + + if (message.type == 'aphlict.status') { + if (message.data == 'closed') { + var ws = JX.Aphlict.getInstance().getWebsocket(); + if (ws) { + var delay = ws.getReconnectDelay(); + json += ' [Reconnect: ' + delay + 'ms]'; + } + } + } + + leader_log('onReceiveBroadcast', message.type, is_leader, json); + }); + + JX.Leader.listen('onBecomeLeader', function() { + leader_log('onBecomeLeader'); + }); + + var action_log = function(action) { + var message = new JX.DarkMessage() + .setMessage('> ' + action); + + realtime_log.addMessage(message); + }; + + JX.Stratcom.listen('click', 'dark-console-realtime-action', function(e) { + var node = e.getNode('dark-console-realtime-action'); + var data = JX.Stratcom.getData(node); + + action_log(data.label); + + var action = data.action; + switch (action) { + case 'reconnect': + var ws = JX.Aphlict.getInstance().getWebsocket(); + if (ws) { + ws.reconnect(); + } + break; + case 'replay': + JX.Aphlict.getInstance().replay(); + break; + case 'repaint': + JX.Aphlict.getInstance().reconnect(); + break; + } + + }); + + } + });