diff --git a/.gitignore b/.gitignore index 0b6eeaec463..752a6329a87 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ !/apps/updatenotification !/apps/theming !/apps/twofactor_backupcodes +!/apps/user_status !/apps/workflowengine /apps/files_external/3rdparty/irodsphp/PHPUnitTest /apps/files_external/3rdparty/irodsphp/web diff --git a/COPYING-README b/COPYING-README index 599d4eb469c..53e29ec4771 100644 --- a/COPYING-README +++ b/COPYING-README @@ -9,6 +9,7 @@ Licensing of components: * User: AGPL * XML/RPC: MIT / PHP * Elementary filetype icons: GPL v3+ +* Material UI icons: APACHE LICENSE, VERSION 2.0 All unmodified files from these and other sources retain their original copyright and license notices: see the relevant individual files. diff --git a/Makefile b/Makefile index 63ba3b08baa..b0bfa14d1b1 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,7 @@ clean: rm -rf apps/systemtags/js/systemtags.* rm -rf apps/twofactor_backupcodes/js rm -rf apps/updatenotification/js/updatenotification.* + rm -rf apps/user_status/js/ rm -rf apps/workflowengine/js/ rm -rf core/js/dist @@ -57,5 +58,6 @@ clean-git: clean git checkout -- apps/systemtags/js/systemtags.* git checkout -- apps/twofactor_backupcodes/js git checkout -- apps/updatenotification/js/updatenotification.* + git checkout -- apps/user_status/js/ git checkout -- apps/workflowengine/js/ git checkout -- core/js/dist diff --git a/apps/user_status/appinfo/info.xml b/apps/user_status/appinfo/info.xml new file mode 100644 index 00000000000..04a252ead0c --- /dev/null +++ b/apps/user_status/appinfo/info.xml @@ -0,0 +1,31 @@ + + + user_status + User status + User status + + 0.0.2 + agpl + Georg Ehrke + UserStatus + + social + https://github.com/nextcloud/server + + + user_status-menuitem + User status + + 1 + info.svg + settings + + + + + + + OCA\UserStatus\BackgroundJob\ClearOldStatusesBackgroundJob + + diff --git a/apps/user_status/appinfo/routes.php b/apps/user_status/appinfo/routes.php new file mode 100644 index 00000000000..d9b8d17fe4e --- /dev/null +++ b/apps/user_status/appinfo/routes.php @@ -0,0 +1,43 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +return [ + 'ocs' => [ + // Routes for querying statuses + ['name' => 'Statuses#findAll', 'url' => '/api/v1/statuses', 'verb' => 'GET'], + ['name' => 'Statuses#find', 'url' => '/api/v1/statuses/{userId}', 'verb' => 'GET'], + // Routes for manipulating your own status + ['name' => 'UserStatus#getStatus', 'url' => '/api/v1/user_status', 'verb' => 'GET'], + ['name' => 'UserStatus#setStatus', 'url' => '/api/v1/user_status/status', 'verb' => 'PUT'], + ['name' => 'UserStatus#setPredefinedMessage', 'url' => '/api/v1/user_status/message/predefined', 'verb' => 'PUT'], + ['name' => 'UserStatus#setCustomMessage', 'url' => '/api/v1/user_status/message/custom', 'verb' => 'PUT'], + ['name' => 'UserStatus#clearMessage', 'url' => '/api/v1/user_status/message', 'verb' => 'DELETE'], + // Routes for listing default routes + ['name' => 'PredefinedStatus#findAll', 'url' => '/api/v1/predefined_statuses/', 'verb' => 'GET'] + ], + 'routes' => [ + ['name' => 'Heartbeat#heartbeat', 'url' => '/heartbeat', 'verb' => 'PUT'], + ], +]; diff --git a/apps/user_status/composer/autoload.php b/apps/user_status/composer/autoload.php new file mode 100644 index 00000000000..b22563e6f83 --- /dev/null +++ b/apps/user_status/composer/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/apps/user_status/composer/composer/LICENSE b/apps/user_status/composer/composer/LICENSE new file mode 100644 index 00000000000..f27399a042d --- /dev/null +++ b/apps/user_status/composer/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/apps/user_status/composer/composer/autoload_classmap.php b/apps/user_status/composer/composer/autoload_classmap.php new file mode 100644 index 00000000000..aebcc98bea0 --- /dev/null +++ b/apps/user_status/composer/composer/autoload_classmap.php @@ -0,0 +1,31 @@ + $baseDir . '/../lib/AppInfo/Application.php', + 'OCA\\UserStatus\\BackgroundJob\\ClearOldStatusesBackgroundJob' => $baseDir . '/../lib/BackgroundJob/ClearOldStatusesBackgroundJob.php', + 'OCA\\UserStatus\\Capabilities' => $baseDir . '/../lib/Capabilities.php', + 'OCA\\UserStatus\\Controller\\HeartbeatController' => $baseDir . '/../lib/Controller/HeartbeatController.php', + 'OCA\\UserStatus\\Controller\\PredefinedStatusController' => $baseDir . '/../lib/Controller/PredefinedStatusController.php', + 'OCA\\UserStatus\\Controller\\StatusesController' => $baseDir . '/../lib/Controller/StatusesController.php', + 'OCA\\UserStatus\\Controller\\UserStatusController' => $baseDir . '/../lib/Controller/UserStatusController.php', + 'OCA\\UserStatus\\Db\\UserStatus' => $baseDir . '/../lib/Db/UserStatus.php', + 'OCA\\UserStatus\\Db\\UserStatusMapper' => $baseDir . '/../lib/Db/UserStatusMapper.php', + 'OCA\\UserStatus\\Exception\\InvalidClearAtException' => $baseDir . '/../lib/Exception/InvalidClearAtException.php', + 'OCA\\UserStatus\\Exception\\InvalidMessageIdException' => $baseDir . '/../lib/Exception/InvalidMessageIdException.php', + 'OCA\\UserStatus\\Exception\\InvalidStatusIconException' => $baseDir . '/../lib/Exception/InvalidStatusIconException.php', + 'OCA\\UserStatus\\Exception\\InvalidStatusTypeException' => $baseDir . '/../lib/Exception/InvalidStatusTypeException.php', + 'OCA\\UserStatus\\Exception\\StatusMessageTooLongException' => $baseDir . '/../lib/Exception/StatusMessageTooLongException.php', + 'OCA\\UserStatus\\Listener\\BeforeTemplateRenderedListener' => $baseDir . '/../lib/Listener/BeforeTemplateRenderedListener.php', + 'OCA\\UserStatus\\Listener\\UserDeletedListener' => $baseDir . '/../lib/Listener/UserDeletedListener.php', + 'OCA\\UserStatus\\Listener\\UserLiveStatusListener' => $baseDir . '/../lib/Listener/UserLiveStatusListener.php', + 'OCA\\UserStatus\\Migration\\Version0001Date20200602134824' => $baseDir . '/../lib/Migration/Version0001Date20200602134824.php', + 'OCA\\UserStatus\\Service\\EmojiService' => $baseDir . '/../lib/Service/EmojiService.php', + 'OCA\\UserStatus\\Service\\JSDataService' => $baseDir . '/../lib/Service/JSDataService.php', + 'OCA\\UserStatus\\Service\\PredefinedStatusService' => $baseDir . '/../lib/Service/PredefinedStatusService.php', + 'OCA\\UserStatus\\Service\\StatusService' => $baseDir . '/../lib/Service/StatusService.php', +); diff --git a/apps/user_status/composer/composer/autoload_namespaces.php b/apps/user_status/composer/composer/autoload_namespaces.php new file mode 100644 index 00000000000..71c9e91858d --- /dev/null +++ b/apps/user_status/composer/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($baseDir . '/../lib'), +); diff --git a/apps/user_status/composer/composer/autoload_real.php b/apps/user_status/composer/composer/autoload_real.php new file mode 100644 index 00000000000..a8a7f5ca60e --- /dev/null +++ b/apps/user_status/composer/composer/autoload_real.php @@ -0,0 +1,46 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInitUserStatus::getInitializer($loader)); + } else { + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->setClassMapAuthoritative(true); + $loader->register(true); + + return $loader; + } +} diff --git a/apps/user_status/composer/composer/autoload_static.php b/apps/user_status/composer/composer/autoload_static.php new file mode 100644 index 00000000000..c652caa5ad3 --- /dev/null +++ b/apps/user_status/composer/composer/autoload_static.php @@ -0,0 +1,57 @@ + + array ( + 'OCA\\UserStatus\\' => 15, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'OCA\\UserStatus\\' => + array ( + 0 => __DIR__ . '/..' . '/../lib', + ), + ); + + public static $classMap = array ( + 'OCA\\UserStatus\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', + 'OCA\\UserStatus\\BackgroundJob\\ClearOldStatusesBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/ClearOldStatusesBackgroundJob.php', + 'OCA\\UserStatus\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', + 'OCA\\UserStatus\\Controller\\HeartbeatController' => __DIR__ . '/..' . '/../lib/Controller/HeartbeatController.php', + 'OCA\\UserStatus\\Controller\\PredefinedStatusController' => __DIR__ . '/..' . '/../lib/Controller/PredefinedStatusController.php', + 'OCA\\UserStatus\\Controller\\StatusesController' => __DIR__ . '/..' . '/../lib/Controller/StatusesController.php', + 'OCA\\UserStatus\\Controller\\UserStatusController' => __DIR__ . '/..' . '/../lib/Controller/UserStatusController.php', + 'OCA\\UserStatus\\Db\\UserStatus' => __DIR__ . '/..' . '/../lib/Db/UserStatus.php', + 'OCA\\UserStatus\\Db\\UserStatusMapper' => __DIR__ . '/..' . '/../lib/Db/UserStatusMapper.php', + 'OCA\\UserStatus\\Exception\\InvalidClearAtException' => __DIR__ . '/..' . '/../lib/Exception/InvalidClearAtException.php', + 'OCA\\UserStatus\\Exception\\InvalidMessageIdException' => __DIR__ . '/..' . '/../lib/Exception/InvalidMessageIdException.php', + 'OCA\\UserStatus\\Exception\\InvalidStatusIconException' => __DIR__ . '/..' . '/../lib/Exception/InvalidStatusIconException.php', + 'OCA\\UserStatus\\Exception\\InvalidStatusTypeException' => __DIR__ . '/..' . '/../lib/Exception/InvalidStatusTypeException.php', + 'OCA\\UserStatus\\Exception\\StatusMessageTooLongException' => __DIR__ . '/..' . '/../lib/Exception/StatusMessageTooLongException.php', + 'OCA\\UserStatus\\Listener\\BeforeTemplateRenderedListener' => __DIR__ . '/..' . '/../lib/Listener/BeforeTemplateRenderedListener.php', + 'OCA\\UserStatus\\Listener\\UserDeletedListener' => __DIR__ . '/..' . '/../lib/Listener/UserDeletedListener.php', + 'OCA\\UserStatus\\Listener\\UserLiveStatusListener' => __DIR__ . '/..' . '/../lib/Listener/UserLiveStatusListener.php', + 'OCA\\UserStatus\\Migration\\Version0001Date20200602134824' => __DIR__ . '/..' . '/../lib/Migration/Version0001Date20200602134824.php', + 'OCA\\UserStatus\\Service\\EmojiService' => __DIR__ . '/..' . '/../lib/Service/EmojiService.php', + 'OCA\\UserStatus\\Service\\JSDataService' => __DIR__ . '/..' . '/../lib/Service/JSDataService.php', + 'OCA\\UserStatus\\Service\\PredefinedStatusService' => __DIR__ . '/..' . '/../lib/Service/PredefinedStatusService.php', + 'OCA\\UserStatus\\Service\\StatusService' => __DIR__ . '/..' . '/../lib/Service/StatusService.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitUserStatus::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitUserStatus::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInitUserStatus::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/apps/user_status/css/user-status-menu.scss b/apps/user_status/css/user-status-menu.scss new file mode 100644 index 00000000000..e038425a66f --- /dev/null +++ b/apps/user_status/css/user-status-menu.scss @@ -0,0 +1,37 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +.icon-user-status-away { + @include icon-color('user-status-away', 'user_status', '#F4A331', 1); +} + +.icon-user-status-dnd { + @include icon-color('user-status-dnd', 'user_status', '#ED484C', 1); +} + +.icon-user-status-invisible { + @include icon-color('user-status-invisible', 'user_status', '#000000', 1); +} + +.icon-user-status-online { + @include icon-color('user-status-online', 'user_status', '#49B382', 2); +} diff --git a/apps/user_status/img/app.svg b/apps/user_status/img/app.svg new file mode 100644 index 00000000000..a2044af4d82 --- /dev/null +++ b/apps/user_status/img/app.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/user_status/img/user-status-away.svg b/apps/user_status/img/user-status-away.svg new file mode 100644 index 00000000000..a181a626e81 --- /dev/null +++ b/apps/user_status/img/user-status-away.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/user_status/img/user-status-dnd.svg b/apps/user_status/img/user-status-dnd.svg new file mode 100644 index 00000000000..30f7ee515c8 --- /dev/null +++ b/apps/user_status/img/user-status-dnd.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/user_status/img/user-status-invisible.svg b/apps/user_status/img/user-status-invisible.svg new file mode 100644 index 00000000000..f35034565e0 --- /dev/null +++ b/apps/user_status/img/user-status-invisible.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/user_status/img/user-status-online.svg b/apps/user_status/img/user-status-online.svg new file mode 100644 index 00000000000..bf97c3dc9aa --- /dev/null +++ b/apps/user_status/img/user-status-online.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/user_status/js/user-status-menu.js b/apps/user_status/js/user-status-menu.js new file mode 100644 index 00000000000..cef327966da --- /dev/null +++ b/apps/user_status/js/user-status-menu.js @@ -0,0 +1,2 @@ +!function(e){var t={};function n(a){if(t[a])return t[a].exports;var r=t[a]={i:a,l:!1,exports:{}};return e[a].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,a){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:a})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var a=Object.create(null);if(n.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(a,r,function(t){return e[t]}.bind(null,r));return a},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/js/",n(n.s=385)}([function(e,t,n){(function(e){e.exports=function(){"use strict";var t,a;function r(){return t.apply(null,arguments)}function s(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function i(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function o(e){return void 0===e}function u(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function l(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function c(e,t){var n,a=[];for(n=0;n>>0,a=0;a0)for(n=0;n=0?n?"+":"":"-")+Math.pow(10,Math.max(0,r)).toString().substr(1)+a}var q=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,H=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,z={},U={};function G(e,t,n,a){var r=a;"string"==typeof a&&(r=function(){return this[a]()}),e&&(U[e]=r),t&&(U[t[0]]=function(){return O(r.apply(this,arguments),t[1],t[2])}),n&&(U[n]=function(){return this.localeData().ordinal(r.apply(this,arguments),e)})}function R(e,t){return e.isValid()?(t=W(t,e.localeData()),z[t]=z[t]||function(e){var t,n,a,r=e.match(q);for(t=0,n=r.length;t=0&&H.test(e);)e=e.replace(H,a),H.lastIndex=0,n-=1;return e}var Q=/\d/,J=/\d\d/,V=/\d{3}/,Z=/\d{4}/,$=/[+-]?\d{6}/,K=/\d\d?/,X=/\d\d\d\d?/,ee=/\d\d\d\d\d\d?/,te=/\d{1,3}/,ne=/\d{1,4}/,ae=/[+-]?\d{1,6}/,re=/\d+/,se=/[+-]?\d+/,ie=/Z|[+-]\d\d:?\d\d/gi,oe=/Z|[+-]\d\d(?::?\d\d)?/gi,ue=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i,le={};function ce(e,t,n){le[e]=S(t)?t:function(e,a){return e&&n?n:t}}function me(e,t){return m(le,e)?le[e](t._strict,t._locale):new RegExp(de(e.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,(function(e,t,n,a,r){return t||n||a||r}))))}function de(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var ge={};function fe(e,t){var n,a=t;for("string"==typeof e&&(e=[e]),u(t)&&(a=function(e,n){n[t]=k(e)}),n=0;n68?1900:2e3)};var Ae,ve=be("FullYear",!0);function be(e,t){return function(n){return null!=n?(ke(this,e,n),r.updateOffset(this,t),this):ye(this,e)}}function ye(e,t){return e.isValid()?e._d["get"+(e._isUTC?"UTC":"")+t]():NaN}function ke(e,t,n){e.isValid()&&!isNaN(n)&&("FullYear"===t&&Fe(e.year())&&1===e.month()&&29===e.date()?e._d["set"+(e._isUTC?"UTC":"")+t](n,e.month(),we(n,e.month())):e._d["set"+(e._isUTC?"UTC":"")+t](n))}function we(e,t){if(isNaN(e)||isNaN(t))return NaN;var n,a=(t%(n=12)+n)%n;return e+=(t-a)/12,1===a?Fe(e)?29:28:31-a%7%2}Ae=Array.prototype.indexOf?Array.prototype.indexOf:function(e){var t;for(t=0;t=0?(o=new Date(e+400,t,n,a,r,s,i),isFinite(o.getFullYear())&&o.setFullYear(e)):o=new Date(e,t,n,a,r,s,i),o}function Be(e){var t;if(e<100&&e>=0){var n=Array.prototype.slice.call(arguments);n[0]=e+400,t=new Date(Date.UTC.apply(null,n)),isFinite(t.getUTCFullYear())&&t.setUTCFullYear(e)}else t=new Date(Date.UTC.apply(null,arguments));return t}function Ye(e,t,n){var a=7+t-n;return-(7+Be(e,0,a).getUTCDay()-t)%7+a-1}function Ne(e,t,n,a,r){var s,i,o=1+7*(t-1)+(7+n-a)%7+Ye(e,a,r);return o<=0?i=pe(s=e-1)+o:o>pe(e)?(s=e+1,i=o-pe(e)):(s=e,i=o),{year:s,dayOfYear:i}}function Ie(e,t,n){var a,r,s=Ye(e.year(),t,n),i=Math.floor((e.dayOfYear()-s-1)/7)+1;return i<1?a=i+Oe(r=e.year()-1,t,n):i>Oe(e.year(),t,n)?(a=i-Oe(e.year(),t,n),r=e.year()+1):(r=e.year(),a=i),{week:a,year:r}}function Oe(e,t,n){var a=Ye(e,t,n),r=Ye(e+1,t,n);return(pe(e)-a+r)/7}function qe(e,t){return e.slice(t,7).concat(e.slice(0,t))}G("w",["ww",2],"wo","week"),G("W",["WW",2],"Wo","isoWeek"),P("week","w"),P("isoWeek","W"),I("week",5),I("isoWeek",5),ce("w",K),ce("ww",K,J),ce("W",K),ce("WW",K,J),_e(["w","ww","W","WW"],(function(e,t,n,a){t[a.substr(0,1)]=k(e)})),G("d",0,"do","day"),G("dd",0,0,(function(e){return this.localeData().weekdaysMin(this,e)})),G("ddd",0,0,(function(e){return this.localeData().weekdaysShort(this,e)})),G("dddd",0,0,(function(e){return this.localeData().weekdays(this,e)})),G("e",0,0,"weekday"),G("E",0,0,"isoWeekday"),P("day","d"),P("weekday","e"),P("isoWeekday","E"),I("day",11),I("weekday",11),I("isoWeekday",11),ce("d",K),ce("e",K),ce("E",K),ce("dd",(function(e,t){return t.weekdaysMinRegex(e)})),ce("ddd",(function(e,t){return t.weekdaysShortRegex(e)})),ce("dddd",(function(e,t){return t.weekdaysRegex(e)})),_e(["dd","ddd","dddd"],(function(e,t,n,a){var r=n._locale.weekdaysParse(e,a,n._strict);null!=r?t.d=r:f(n).invalidWeekday=e})),_e(["d","e","E"],(function(e,t,n,a){t[a]=k(e)}));var He="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),ze="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Ue="Su_Mo_Tu_We_Th_Fr_Sa".split("_");function Ge(e,t,n){var a,r,s,i=e.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],a=0;a<7;++a)s=g([2e3,1]).day(a),this._minWeekdaysParse[a]=this.weekdaysMin(s,"").toLocaleLowerCase(),this._shortWeekdaysParse[a]=this.weekdaysShort(s,"").toLocaleLowerCase(),this._weekdaysParse[a]=this.weekdays(s,"").toLocaleLowerCase();return n?"dddd"===t?-1!==(r=Ae.call(this._weekdaysParse,i))?r:null:"ddd"===t?-1!==(r=Ae.call(this._shortWeekdaysParse,i))?r:null:-1!==(r=Ae.call(this._minWeekdaysParse,i))?r:null:"dddd"===t?-1!==(r=Ae.call(this._weekdaysParse,i))||-1!==(r=Ae.call(this._shortWeekdaysParse,i))||-1!==(r=Ae.call(this._minWeekdaysParse,i))?r:null:"ddd"===t?-1!==(r=Ae.call(this._shortWeekdaysParse,i))||-1!==(r=Ae.call(this._weekdaysParse,i))||-1!==(r=Ae.call(this._minWeekdaysParse,i))?r:null:-1!==(r=Ae.call(this._minWeekdaysParse,i))||-1!==(r=Ae.call(this._weekdaysParse,i))||-1!==(r=Ae.call(this._shortWeekdaysParse,i))?r:null}var Re=ue,We=ue,Qe=ue;function Je(){function e(e,t){return t.length-e.length}var t,n,a,r,s,i=[],o=[],u=[],l=[];for(t=0;t<7;t++)n=g([2e3,1]).day(t),a=this.weekdaysMin(n,""),r=this.weekdaysShort(n,""),s=this.weekdays(n,""),i.push(a),o.push(r),u.push(s),l.push(a),l.push(r),l.push(s);for(i.sort(e),o.sort(e),u.sort(e),l.sort(e),t=0;t<7;t++)o[t]=de(o[t]),u[t]=de(u[t]),l[t]=de(l[t]);this._weekdaysRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+i.join("|")+")","i")}function Ve(){return this.hours()%12||12}function Ze(e,t){G(e,0,0,(function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)}))}function $e(e,t){return t._meridiemParse}G("H",["HH",2],0,"hour"),G("h",["hh",2],0,Ve),G("k",["kk",2],0,(function(){return this.hours()||24})),G("hmm",0,0,(function(){return""+Ve.apply(this)+O(this.minutes(),2)})),G("hmmss",0,0,(function(){return""+Ve.apply(this)+O(this.minutes(),2)+O(this.seconds(),2)})),G("Hmm",0,0,(function(){return""+this.hours()+O(this.minutes(),2)})),G("Hmmss",0,0,(function(){return""+this.hours()+O(this.minutes(),2)+O(this.seconds(),2)})),Ze("a",!0),Ze("A",!1),P("hour","h"),I("hour",13),ce("a",$e),ce("A",$e),ce("H",K),ce("h",K),ce("k",K),ce("HH",K,J),ce("hh",K,J),ce("kk",K,J),ce("hmm",X),ce("hmmss",ee),ce("Hmm",X),ce("Hmmss",ee),fe(["H","HH"],3),fe(["k","kk"],(function(e,t,n){var a=k(e);t[3]=24===a?0:a})),fe(["a","A"],(function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e})),fe(["h","hh"],(function(e,t,n){t[3]=k(e),f(n).bigHour=!0})),fe("hmm",(function(e,t,n){var a=e.length-2;t[3]=k(e.substr(0,a)),t[4]=k(e.substr(a)),f(n).bigHour=!0})),fe("hmmss",(function(e,t,n){var a=e.length-4,r=e.length-2;t[3]=k(e.substr(0,a)),t[4]=k(e.substr(a,2)),t[5]=k(e.substr(r)),f(n).bigHour=!0})),fe("Hmm",(function(e,t,n){var a=e.length-2;t[3]=k(e.substr(0,a)),t[4]=k(e.substr(a))})),fe("Hmmss",(function(e,t,n){var a=e.length-4,r=e.length-2;t[3]=k(e.substr(0,a)),t[4]=k(e.substr(a,2)),t[5]=k(e.substr(r))}));var Ke,Xe=be("Hours",!0),et={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:xe,monthsShort:De,week:{dow:0,doy:6},weekdays:He,weekdaysMin:Ue,weekdaysShort:ze,meridiemParse:/[ap]\.?m?\.?/i},tt={},nt={};function at(e){return e?e.toLowerCase().replace("_","-"):e}function rt(t){var a=null;if(!tt[t]&&void 0!==e&&e&&e.exports)try{a=Ke._abbr,n(327)("./"+t),st(a)}catch(e){}return tt[t]}function st(e,t){var n;return e&&((n=o(t)?ot(e):it(e,t))?Ke=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),Ke._abbr}function it(e,t){if(null!==t){var n,a=et;if(t.abbr=e,null!=tt[e])E("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),a=tt[e]._config;else if(null!=t.parentLocale)if(null!=tt[t.parentLocale])a=tt[t.parentLocale]._config;else{if(null==(n=rt(t.parentLocale)))return nt[t.parentLocale]||(nt[t.parentLocale]=[]),nt[t.parentLocale].push({name:e,config:t}),null;a=n._config}return tt[e]=new L(C(a,t)),nt[e]&&nt[e].forEach((function(e){it(e.name,e.config)})),st(e),tt[e]}return delete tt[e],null}function ot(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return Ke;if(!s(e)){if(t=rt(e))return t;e=[e]}return function(e){for(var t,n,a,r,s=0;s0;){if(a=rt(r.slice(0,t).join("-")))return a;if(n&&n.length>=t&&w(r,n,!0)>=t-1)break;t--}s++}return Ke}(e)}function ut(e){var t,n=e._a;return n&&-2===f(e).overflow&&(t=n[1]<0||n[1]>11?1:n[2]<1||n[2]>we(n[0],n[1])?2:n[3]<0||n[3]>24||24===n[3]&&(0!==n[4]||0!==n[5]||0!==n[6])?3:n[4]<0||n[4]>59?4:n[5]<0||n[5]>59?5:n[6]<0||n[6]>999?6:-1,f(e)._overflowDayOfYear&&(t<0||t>2)&&(t=2),f(e)._overflowWeeks&&-1===t&&(t=7),f(e)._overflowWeekday&&-1===t&&(t=8),f(e).overflow=t),e}function lt(e,t,n){return null!=e?e:null!=t?t:n}function ct(e){var t,n,a,s,i,o=[];if(!e._d){for(a=function(e){var t=new Date(r.now());return e._useUTC?[t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate()]:[t.getFullYear(),t.getMonth(),t.getDate()]}(e),e._w&&null==e._a[2]&&null==e._a[1]&&function(e){var t,n,a,r,s,i,o,u;if(null!=(t=e._w).GG||null!=t.W||null!=t.E)s=1,i=4,n=lt(t.GG,e._a[0],Ie(Mt(),1,4).year),a=lt(t.W,1),((r=lt(t.E,1))<1||r>7)&&(u=!0);else{s=e._locale._week.dow,i=e._locale._week.doy;var l=Ie(Mt(),s,i);n=lt(t.gg,e._a[0],l.year),a=lt(t.w,l.week),null!=t.d?((r=t.d)<0||r>6)&&(u=!0):null!=t.e?(r=t.e+s,(t.e<0||t.e>6)&&(u=!0)):r=s}a<1||a>Oe(n,s,i)?f(e)._overflowWeeks=!0:null!=u?f(e)._overflowWeekday=!0:(o=Ne(n,a,r,s,i),e._a[0]=o.year,e._dayOfYear=o.dayOfYear)}(e),null!=e._dayOfYear&&(i=lt(e._a[0],a[0]),(e._dayOfYear>pe(i)||0===e._dayOfYear)&&(f(e)._overflowDayOfYear=!0),n=Be(i,0,e._dayOfYear),e._a[1]=n.getUTCMonth(),e._a[2]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=o[t]=a[t];for(;t<7;t++)e._a[t]=o[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[3]&&0===e._a[4]&&0===e._a[5]&&0===e._a[6]&&(e._nextDay=!0,e._a[3]=0),e._d=(e._useUTC?Be:Pe).apply(null,o),s=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[3]=24),e._w&&void 0!==e._w.d&&e._w.d!==s&&(f(e).weekdayMismatch=!0)}}var mt=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,dt=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,gt=/Z|[+-]\d\d(?::?\d\d)?/,ft=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],_t=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],ht=/^\/?Date\((\-?\d+)/i;function pt(e){var t,n,a,r,s,i,o=e._i,u=mt.exec(o)||dt.exec(o);if(u){for(f(e).iso=!0,t=0,n=ft.length;t0&&f(e).unusedInput.push(i),o=o.slice(o.indexOf(n)+n.length),l+=n.length),U[s]?(n?f(e).empty=!1:f(e).unusedTokens.push(s),he(s,n,e)):e._strict&&!n&&f(e).unusedTokens.push(s);f(e).charsLeftOver=u-l,o.length>0&&f(e).unusedInput.push(o),e._a[3]<=12&&!0===f(e).bigHour&&e._a[3]>0&&(f(e).bigHour=void 0),f(e).parsedDateParts=e._a.slice(0),f(e).meridiem=e._meridiem,e._a[3]=function(e,t,n){var a;return null==n?t:null!=e.meridiemHour?e.meridiemHour(t,n):null!=e.isPM?((a=e.isPM(n))&&t<12&&(t+=12),a||12!==t||(t=0),t):t}(e._locale,e._a[3],e._meridiem),ct(e),ut(e)}else bt(e);else pt(e)}function kt(e){var t=e._i,n=e._f;return e._locale=e._locale||ot(e._l),null===t||void 0===n&&""===t?h({nullInput:!0}):("string"==typeof t&&(e._i=t=e._locale.preparse(t)),b(t)?new v(ut(t)):(l(t)?e._d=t:s(n)?function(e){var t,n,a,r,s;if(0===e._f.length)return f(e).invalidFormat=!0,void(e._d=new Date(NaN));for(r=0;rthis?this:e:h()}));function Tt(e,t){var n,a;if(1===t.length&&s(t[0])&&(t=t[0]),!t.length)return Mt();for(n=t[0],a=1;a=0?new Date(e+400,t,n)-126227808e5:new Date(e,t,n).valueOf()}function en(e,t,n){return e<100&&e>=0?Date.UTC(e+400,t,n)-126227808e5:Date.UTC(e,t,n)}function tn(e,t){G(0,[e,e.length],0,t)}function nn(e,t,n,a,r){var s;return null==e?Ie(this,a,r).year:(t>(s=Oe(e,a,r))&&(t=s),an.call(this,e,t,n,a,r))}function an(e,t,n,a,r){var s=Ne(e,t,n,a,r),i=Be(s.year,0,s.dayOfYear);return this.year(i.getUTCFullYear()),this.month(i.getUTCMonth()),this.date(i.getUTCDate()),this}G(0,["gg",2],0,(function(){return this.weekYear()%100})),G(0,["GG",2],0,(function(){return this.isoWeekYear()%100})),tn("gggg","weekYear"),tn("ggggg","weekYear"),tn("GGGG","isoWeekYear"),tn("GGGGG","isoWeekYear"),P("weekYear","gg"),P("isoWeekYear","GG"),I("weekYear",1),I("isoWeekYear",1),ce("G",se),ce("g",se),ce("GG",K,J),ce("gg",K,J),ce("GGGG",ne,Z),ce("gggg",ne,Z),ce("GGGGG",ae,$),ce("ggggg",ae,$),_e(["gggg","ggggg","GGGG","GGGGG"],(function(e,t,n,a){t[a.substr(0,2)]=k(e)})),_e(["gg","GG"],(function(e,t,n,a){t[a]=r.parseTwoDigitYear(e)})),G("Q",0,"Qo","quarter"),P("quarter","Q"),I("quarter",7),ce("Q",Q),fe("Q",(function(e,t){t[1]=3*(k(e)-1)})),G("D",["DD",2],"Do","date"),P("date","D"),I("date",9),ce("D",K),ce("DD",K,J),ce("Do",(function(e,t){return e?t._dayOfMonthOrdinalParse||t._ordinalParse:t._dayOfMonthOrdinalParseLenient})),fe(["D","DD"],2),fe("Do",(function(e,t){t[2]=k(e.match(K)[0])}));var rn=be("Date",!0);G("DDD",["DDDD",3],"DDDo","dayOfYear"),P("dayOfYear","DDD"),I("dayOfYear",4),ce("DDD",te),ce("DDDD",V),fe(["DDD","DDDD"],(function(e,t,n){n._dayOfYear=k(e)})),G("m",["mm",2],0,"minute"),P("minute","m"),I("minute",14),ce("m",K),ce("mm",K,J),fe(["m","mm"],4);var sn=be("Minutes",!1);G("s",["ss",2],0,"second"),P("second","s"),I("second",15),ce("s",K),ce("ss",K,J),fe(["s","ss"],5);var on,un=be("Seconds",!1);for(G("S",0,0,(function(){return~~(this.millisecond()/100)})),G(0,["SS",2],0,(function(){return~~(this.millisecond()/10)})),G(0,["SSS",3],0,"millisecond"),G(0,["SSSS",4],0,(function(){return 10*this.millisecond()})),G(0,["SSSSS",5],0,(function(){return 100*this.millisecond()})),G(0,["SSSSSS",6],0,(function(){return 1e3*this.millisecond()})),G(0,["SSSSSSS",7],0,(function(){return 1e4*this.millisecond()})),G(0,["SSSSSSSS",8],0,(function(){return 1e5*this.millisecond()})),G(0,["SSSSSSSSS",9],0,(function(){return 1e6*this.millisecond()})),P("millisecond","ms"),I("millisecond",16),ce("S",te,Q),ce("SS",te,J),ce("SSS",te,V),on="SSSS";on.length<=9;on+="S")ce(on,re);function ln(e,t){t[6]=k(1e3*("0."+e))}for(on="S";on.length<=9;on+="S")fe(on,ln);var cn=be("Milliseconds",!1);G("z",0,0,"zoneAbbr"),G("zz",0,0,"zoneName");var mn=v.prototype;function dn(e){return e}mn.add=Wt,mn.calendar=function(e,t){var n=e||Mt(),a=Yt(n,this).startOf("day"),s=r.calendarFormat(this,a)||"sameElse",i=t&&(S(t[s])?t[s].call(this,n):t[s]);return this.format(i||this.localeData().calendar(s,this,Mt(n)))},mn.clone=function(){return new v(this)},mn.diff=function(e,t,n){var a,r,s;if(!this.isValid())return NaN;if(!(a=Yt(e,this)).isValid())return NaN;switch(r=6e4*(a.utcOffset()-this.utcOffset()),t=B(t)){case"year":s=Jt(this,a)/12;break;case"month":s=Jt(this,a);break;case"quarter":s=Jt(this,a)/3;break;case"second":s=(this-a)/1e3;break;case"minute":s=(this-a)/6e4;break;case"hour":s=(this-a)/36e5;break;case"day":s=(this-a-r)/864e5;break;case"week":s=(this-a-r)/6048e5;break;default:s=this-a}return n?s:y(s)},mn.endOf=function(e){var t;if(void 0===(e=B(e))||"millisecond"===e||!this.isValid())return this;var n=this._isUTC?en:Xt;switch(e){case"year":t=n(this.year()+1,0,1)-1;break;case"quarter":t=n(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":t=n(this.year(),this.month()+1,1)-1;break;case"week":t=n(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":t=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":t=n(this.year(),this.month(),this.date()+1)-1;break;case"hour":t=this._d.valueOf(),t+=36e5-Kt(t+(this._isUTC?0:6e4*this.utcOffset()),36e5)-1;break;case"minute":t=this._d.valueOf(),t+=6e4-Kt(t,6e4)-1;break;case"second":t=this._d.valueOf(),t+=1e3-Kt(t,1e3)-1}return this._d.setTime(t),r.updateOffset(this,!0),this},mn.format=function(e){e||(e=this.isUtc()?r.defaultFormatUtc:r.defaultFormat);var t=R(this,e);return this.localeData().postformat(t)},mn.from=function(e,t){return this.isValid()&&(b(e)&&e.isValid()||Mt(e).isValid())?Ht({to:this,from:e}).locale(this.locale()).humanize(!t):this.localeData().invalidDate()},mn.fromNow=function(e){return this.from(Mt(),e)},mn.to=function(e,t){return this.isValid()&&(b(e)&&e.isValid()||Mt(e).isValid())?Ht({from:this,to:e}).locale(this.locale()).humanize(!t):this.localeData().invalidDate()},mn.toNow=function(e){return this.to(Mt(),e)},mn.get=function(e){return S(this[e=B(e)])?this[e]():this},mn.invalidAt=function(){return f(this).overflow},mn.isAfter=function(e,t){var n=b(e)?e:Mt(e);return!(!this.isValid()||!n.isValid())&&("millisecond"===(t=B(t)||"millisecond")?this.valueOf()>n.valueOf():n.valueOf()9999?R(n,t?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):S(Date.prototype.toISOString)?t?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",R(n,"Z")):R(n,t?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},mn.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var e="moment",t="";this.isLocal()||(e=0===this.utcOffset()?"moment.utc":"moment.parseZone",t="Z");var n="["+e+'("]',a=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",r=t+'[")]';return this.format(n+a+"-MM-DD[T]HH:mm:ss.SSS"+r)},mn.toJSON=function(){return this.isValid()?this.toISOString():null},mn.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},mn.unix=function(){return Math.floor(this.valueOf()/1e3)},mn.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},mn.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},mn.year=ve,mn.isLeapYear=function(){return Fe(this.year())},mn.weekYear=function(e){return nn.call(this,e,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},mn.isoWeekYear=function(e){return nn.call(this,e,this.isoWeek(),this.isoWeekday(),1,4)},mn.quarter=mn.quarters=function(e){return null==e?Math.ceil((this.month()+1)/3):this.month(3*(e-1)+this.month()%3)},mn.month=Se,mn.daysInMonth=function(){return we(this.year(),this.month())},mn.week=mn.weeks=function(e){var t=this.localeData().week(this);return null==e?t:this.add(7*(e-t),"d")},mn.isoWeek=mn.isoWeeks=function(e){var t=Ie(this,1,4).week;return null==e?t:this.add(7*(e-t),"d")},mn.weeksInYear=function(){var e=this.localeData()._week;return Oe(this.year(),e.dow,e.doy)},mn.isoWeeksInYear=function(){return Oe(this.year(),1,4)},mn.date=rn,mn.day=mn.days=function(e){if(!this.isValid())return null!=e?this:NaN;var t=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=e?(e=function(e,t){return"string"!=typeof e?e:isNaN(e)?"number"==typeof(e=t.weekdaysParse(e))?e:null:parseInt(e,10)}(e,this.localeData()),this.add(e-t,"d")):t},mn.weekday=function(e){if(!this.isValid())return null!=e?this:NaN;var t=(this.day()+7-this.localeData()._week.dow)%7;return null==e?t:this.add(e-t,"d")},mn.isoWeekday=function(e){if(!this.isValid())return null!=e?this:NaN;if(null!=e){var t=function(e,t){return"string"==typeof e?t.weekdaysParse(e)%7||7:isNaN(e)?null:e}(e,this.localeData());return this.day(this.day()%7?t:t-7)}return this.day()||7},mn.dayOfYear=function(e){var t=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==e?t:this.add(e-t,"d")},mn.hour=mn.hours=Xe,mn.minute=mn.minutes=sn,mn.second=mn.seconds=un,mn.millisecond=mn.milliseconds=cn,mn.utcOffset=function(e,t,n){var a,s=this._offset||0;if(!this.isValid())return null!=e?this:NaN;if(null!=e){if("string"==typeof e){if(null===(e=Bt(oe,e)))return this}else Math.abs(e)<16&&!n&&(e*=60);return!this._isUTC&&t&&(a=Nt(this)),this._offset=e,this._isUTC=!0,null!=a&&this.add(a,"m"),s!==e&&(!t||this._changeInProgress?Rt(this,Ht(e-s,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,r.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?s:Nt(this)},mn.utc=function(e){return this.utcOffset(0,e)},mn.local=function(e){return this._isUTC&&(this.utcOffset(0,e),this._isUTC=!1,e&&this.subtract(Nt(this),"m")),this},mn.parseZone=function(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var e=Bt(ie,this._i);null!=e?this.utcOffset(e):this.utcOffset(0,!0)}return this},mn.hasAlignedHourOffset=function(e){return!!this.isValid()&&(e=e?Mt(e).utcOffset():0,(this.utcOffset()-e)%60==0)},mn.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},mn.isLocal=function(){return!!this.isValid()&&!this._isUTC},mn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},mn.isUtc=It,mn.isUTC=It,mn.zoneAbbr=function(){return this._isUTC?"UTC":""},mn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},mn.dates=x("dates accessor is deprecated. Use date instead.",rn),mn.months=x("months accessor is deprecated. Use month instead",Se),mn.years=x("years accessor is deprecated. Use year instead",ve),mn.zone=x("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",(function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()})),mn.isDSTShifted=x("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",(function(){if(!o(this._isDSTShifted))return this._isDSTShifted;var e={};if(F(e,this),(e=kt(e))._a){var t=e._isUTC?g(e._a):Mt(e._a);this._isDSTShifted=this.isValid()&&w(e._a,t.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}));var gn=L.prototype;function fn(e,t,n,a){var r=ot(),s=g().set(a,t);return r[n](s,e)}function _n(e,t,n){if(u(e)&&(t=e,e=void 0),e=e||"",null!=t)return fn(e,t,n,"month");var a,r=[];for(a=0;a<12;a++)r[a]=fn(e,a,n,"month");return r}function hn(e,t,n,a){"boolean"==typeof e?(u(t)&&(n=t,t=void 0),t=t||""):(n=t=e,e=!1,u(t)&&(n=t,t=void 0),t=t||"");var r,s=ot(),i=e?s._week.dow:0;if(null!=n)return fn(t,(n+i)%7,a,"day");var o=[];for(r=0;r<7;r++)o[r]=fn(t,(r+i)%7,a,"day");return o}gn.calendar=function(e,t,n){var a=this._calendar[e]||this._calendar.sameElse;return S(a)?a.call(t,n):a},gn.longDateFormat=function(e){var t=this._longDateFormat[e],n=this._longDateFormat[e.toUpperCase()];return t||!n?t:(this._longDateFormat[e]=n.replace(/MMMM|MM|DD|dddd/g,(function(e){return e.slice(1)})),this._longDateFormat[e])},gn.invalidDate=function(){return this._invalidDate},gn.ordinal=function(e){return this._ordinal.replace("%d",e)},gn.preparse=dn,gn.postformat=dn,gn.relativeTime=function(e,t,n,a){var r=this._relativeTime[n];return S(r)?r(e,t,n,a):r.replace(/%d/i,e)},gn.pastFuture=function(e,t){var n=this._relativeTime[e>0?"future":"past"];return S(n)?n(t):n.replace(/%s/i,t)},gn.set=function(e){var t,n;for(n in e)S(t=e[n])?this[n]=t:this["_"+n]=t;this._config=e,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},gn.months=function(e,t){return e?s(this._months)?this._months[e.month()]:this._months[(this._months.isFormat||Me).test(t)?"format":"standalone"][e.month()]:s(this._months)?this._months:this._months.standalone},gn.monthsShort=function(e,t){return e?s(this._monthsShort)?this._monthsShort[e.month()]:this._monthsShort[Me.test(t)?"format":"standalone"][e.month()]:s(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},gn.monthsParse=function(e,t,n){var a,r,s;if(this._monthsParseExact)return Te.call(this,e,t,n);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),a=0;a<12;a++){if(r=g([2e3,a]),n&&!this._longMonthsParse[a]&&(this._longMonthsParse[a]=new RegExp("^"+this.months(r,"").replace(".","")+"$","i"),this._shortMonthsParse[a]=new RegExp("^"+this.monthsShort(r,"").replace(".","")+"$","i")),n||this._monthsParse[a]||(s="^"+this.months(r,"")+"|^"+this.monthsShort(r,""),this._monthsParse[a]=new RegExp(s.replace(".",""),"i")),n&&"MMMM"===t&&this._longMonthsParse[a].test(e))return a;if(n&&"MMM"===t&&this._shortMonthsParse[a].test(e))return a;if(!n&&this._monthsParse[a].test(e))return a}},gn.monthsRegex=function(e){return this._monthsParseExact?(m(this,"_monthsRegex")||je.call(this),e?this._monthsStrictRegex:this._monthsRegex):(m(this,"_monthsRegex")||(this._monthsRegex=Le),this._monthsStrictRegex&&e?this._monthsStrictRegex:this._monthsRegex)},gn.monthsShortRegex=function(e){return this._monthsParseExact?(m(this,"_monthsRegex")||je.call(this),e?this._monthsShortStrictRegex:this._monthsShortRegex):(m(this,"_monthsShortRegex")||(this._monthsShortRegex=Ce),this._monthsShortStrictRegex&&e?this._monthsShortStrictRegex:this._monthsShortRegex)},gn.week=function(e){return Ie(e,this._week.dow,this._week.doy).week},gn.firstDayOfYear=function(){return this._week.doy},gn.firstDayOfWeek=function(){return this._week.dow},gn.weekdays=function(e,t){var n=s(this._weekdays)?this._weekdays:this._weekdays[e&&!0!==e&&this._weekdays.isFormat.test(t)?"format":"standalone"];return!0===e?qe(n,this._week.dow):e?n[e.day()]:n},gn.weekdaysMin=function(e){return!0===e?qe(this._weekdaysMin,this._week.dow):e?this._weekdaysMin[e.day()]:this._weekdaysMin},gn.weekdaysShort=function(e){return!0===e?qe(this._weekdaysShort,this._week.dow):e?this._weekdaysShort[e.day()]:this._weekdaysShort},gn.weekdaysParse=function(e,t,n){var a,r,s;if(this._weekdaysParseExact)return Ge.call(this,e,t,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),a=0;a<7;a++){if(r=g([2e3,1]).day(a),n&&!this._fullWeekdaysParse[a]&&(this._fullWeekdaysParse[a]=new RegExp("^"+this.weekdays(r,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[a]=new RegExp("^"+this.weekdaysShort(r,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[a]=new RegExp("^"+this.weekdaysMin(r,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[a]||(s="^"+this.weekdays(r,"")+"|^"+this.weekdaysShort(r,"")+"|^"+this.weekdaysMin(r,""),this._weekdaysParse[a]=new RegExp(s.replace(".",""),"i")),n&&"dddd"===t&&this._fullWeekdaysParse[a].test(e))return a;if(n&&"ddd"===t&&this._shortWeekdaysParse[a].test(e))return a;if(n&&"dd"===t&&this._minWeekdaysParse[a].test(e))return a;if(!n&&this._weekdaysParse[a].test(e))return a}},gn.weekdaysRegex=function(e){return this._weekdaysParseExact?(m(this,"_weekdaysRegex")||Je.call(this),e?this._weekdaysStrictRegex:this._weekdaysRegex):(m(this,"_weekdaysRegex")||(this._weekdaysRegex=Re),this._weekdaysStrictRegex&&e?this._weekdaysStrictRegex:this._weekdaysRegex)},gn.weekdaysShortRegex=function(e){return this._weekdaysParseExact?(m(this,"_weekdaysRegex")||Je.call(this),e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(m(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=We),this._weekdaysShortStrictRegex&&e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},gn.weekdaysMinRegex=function(e){return this._weekdaysParseExact?(m(this,"_weekdaysRegex")||Je.call(this),e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(m(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Qe),this._weekdaysMinStrictRegex&&e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},gn.isPM=function(e){return"p"===(e+"").toLowerCase().charAt(0)},gn.meridiem=function(e,t,n){return e>11?n?"pm":"PM":n?"am":"AM"},st("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var t=e%10;return e+(1===k(e%100/10)?"th":1===t?"st":2===t?"nd":3===t?"rd":"th")}}),r.lang=x("moment.lang is deprecated. Use moment.locale instead.",st),r.langData=x("moment.langData is deprecated. Use moment.localeData instead.",ot);var pn=Math.abs;function Fn(e,t,n,a){var r=Ht(t,n);return e._milliseconds+=a*r._milliseconds,e._days+=a*r._days,e._months+=a*r._months,e._bubble()}function An(e){return e<0?Math.floor(e):Math.ceil(e)}function vn(e){return 4800*e/146097}function bn(e){return 146097*e/4800}function yn(e){return function(){return this.as(e)}}var kn=yn("ms"),wn=yn("s"),Mn=yn("m"),xn=yn("h"),Dn=yn("d"),Tn=yn("w"),En=yn("M"),Sn=yn("Q"),Cn=yn("y");function Ln(e){return function(){return this.isValid()?this._data[e]:NaN}}var jn=Ln("milliseconds"),Pn=Ln("seconds"),Bn=Ln("minutes"),Yn=Ln("hours"),Nn=Ln("days"),In=Ln("months"),On=Ln("years"),qn=Math.round,Hn={ss:44,s:45,m:45,h:22,d:26,M:11};function zn(e,t,n,a,r){return r.relativeTime(t||1,!!n,e,a)}var Un=Math.abs;function Gn(e){return(e>0)-(e<0)||+e}function Rn(){if(!this.isValid())return this.localeData().invalidDate();var e,t,n=Un(this._milliseconds)/1e3,a=Un(this._days),r=Un(this._months);e=y(n/60),t=y(e/60),n%=60,e%=60;var s=y(r/12),i=r%=12,o=a,u=t,l=e,c=n?n.toFixed(3).replace(/\.?0+$/,""):"",m=this.asSeconds();if(!m)return"P0D";var d=m<0?"-":"",g=Gn(this._months)!==Gn(m)?"-":"",f=Gn(this._days)!==Gn(m)?"-":"",_=Gn(this._milliseconds)!==Gn(m)?"-":"";return d+"P"+(s?g+s+"Y":"")+(i?g+i+"M":"")+(o?f+o+"D":"")+(u||l||c?"T":"")+(u?_+u+"H":"")+(l?_+l+"M":"")+(c?_+c+"S":"")}var Wn=St.prototype;return Wn.isValid=function(){return this._isValid},Wn.abs=function(){var e=this._data;return this._milliseconds=pn(this._milliseconds),this._days=pn(this._days),this._months=pn(this._months),e.milliseconds=pn(e.milliseconds),e.seconds=pn(e.seconds),e.minutes=pn(e.minutes),e.hours=pn(e.hours),e.months=pn(e.months),e.years=pn(e.years),this},Wn.add=function(e,t){return Fn(this,e,t,1)},Wn.subtract=function(e,t){return Fn(this,e,t,-1)},Wn.as=function(e){if(!this.isValid())return NaN;var t,n,a=this._milliseconds;if("month"===(e=B(e))||"quarter"===e||"year"===e)switch(t=this._days+a/864e5,n=this._months+vn(t),e){case"month":return n;case"quarter":return n/3;case"year":return n/12}else switch(t=this._days+Math.round(bn(this._months)),e){case"week":return t/7+a/6048e5;case"day":return t+a/864e5;case"hour":return 24*t+a/36e5;case"minute":return 1440*t+a/6e4;case"second":return 86400*t+a/1e3;case"millisecond":return Math.floor(864e5*t)+a;default:throw new Error("Unknown unit "+e)}},Wn.asMilliseconds=kn,Wn.asSeconds=wn,Wn.asMinutes=Mn,Wn.asHours=xn,Wn.asDays=Dn,Wn.asWeeks=Tn,Wn.asMonths=En,Wn.asQuarters=Sn,Wn.asYears=Cn,Wn.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*k(this._months/12):NaN},Wn._bubble=function(){var e,t,n,a,r,s=this._milliseconds,i=this._days,o=this._months,u=this._data;return s>=0&&i>=0&&o>=0||s<=0&&i<=0&&o<=0||(s+=864e5*An(bn(o)+i),i=0,o=0),u.milliseconds=s%1e3,e=y(s/1e3),u.seconds=e%60,t=y(e/60),u.minutes=t%60,n=y(t/60),u.hours=n%24,i+=y(n/24),r=y(vn(i)),o+=r,i-=An(bn(r)),a=y(o/12),o%=12,u.days=i,u.months=o,u.years=a,this},Wn.clone=function(){return Ht(this)},Wn.get=function(e){return e=B(e),this.isValid()?this[e+"s"]():NaN},Wn.milliseconds=jn,Wn.seconds=Pn,Wn.minutes=Bn,Wn.hours=Yn,Wn.days=Nn,Wn.weeks=function(){return y(this.days()/7)},Wn.months=In,Wn.years=On,Wn.humanize=function(e){if(!this.isValid())return this.localeData().invalidDate();var t=this.localeData(),n=function(e,t,n){var a=Ht(e).abs(),r=qn(a.as("s")),s=qn(a.as("m")),i=qn(a.as("h")),o=qn(a.as("d")),u=qn(a.as("M")),l=qn(a.as("y")),c=r<=Hn.ss&&["s",r]||r0,c[4]=n,zn.apply(null,c)}(this,!e,t);return e&&(n=t.pastFuture(+this,n)),t.postformat(n)},Wn.toISOString=Rn,Wn.toString=Rn,Wn.toJSON=Rn,Wn.locale=Vt,Wn.localeData=$t,Wn.toIsoString=x("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Rn),Wn.lang=Zt,G("X",0,0,"unix"),G("x",0,0,"valueOf"),ce("x",se),ce("X",/[+-]?\d+(\.\d{1,3})?/),fe("X",(function(e,t,n){n._d=new Date(1e3*parseFloat(e,10))})),fe("x",(function(e,t,n){n._d=new Date(k(e))})),r.version="2.24.0",t=Mt,r.fn=mn,r.min=function(){var e=[].slice.call(arguments,0);return Tt("isBefore",e)},r.max=function(){var e=[].slice.call(arguments,0);return Tt("isAfter",e)},r.now=function(){return Date.now?Date.now():+new Date},r.utc=g,r.unix=function(e){return Mt(1e3*e)},r.months=function(e,t){return _n(e,t,"months")},r.isDate=l,r.locale=st,r.invalid=h,r.duration=Ht,r.isMoment=b,r.weekdays=function(e,t,n){return hn(e,t,n,"weekdays")},r.parseZone=function(){return Mt.apply(null,arguments).parseZone()},r.localeData=ot,r.isDuration=Ct,r.monthsShort=function(e,t){return _n(e,t,"monthsShort")},r.weekdaysMin=function(e,t,n){return hn(e,t,n,"weekdaysMin")},r.defineLocale=it,r.updateLocale=function(e,t){if(null!=t){var n,a,r=et;null!=(a=rt(e))&&(r=a._config),t=C(r,t),(n=new L(t)).parentLocale=tt[e],tt[e]=n,st(e)}else null!=tt[e]&&(null!=tt[e].parentLocale?tt[e]=tt[e].parentLocale:null!=tt[e]&&delete tt[e]);return tt[e]},r.locales=function(){return D(tt)},r.weekdaysShort=function(e,t,n){return hn(e,t,n,"weekdaysShort")},r.normalizeUnits=B,r.relativeTimeRounding=function(e){return void 0===e?qn:"function"==typeof e&&(qn=e,!0)},r.relativeTimeThreshold=function(e,t){return void 0!==Hn[e]&&(void 0===t?Hn[e]:(Hn[e]=t,"s"===e&&(Hn.ss=t-1),!0))},r.calendarFormat=function(e,t){var n=e.diff(t,"days",!0);return n<-6?"sameElse":n<-1?"lastWeek":n<0?"lastDay":n<1?"sameDay":n<2?"nextDay":n<7?"nextWeek":"sameElse"},r.prototype=mn,r.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},r}()}).call(this,n(140)(e))},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t,n){(function(t){var n=function(e){return e&&e.Math==Math&&e};e.exports=n("object"==typeof globalThis&&globalThis)||n("object"==typeof window&&window)||n("object"==typeof self&&self)||n("object"==typeof t&&t)||Function("return this")()}).call(this,n(20))},function(e,t,n){var a=n(2),r=n(86),s=n(7),i=n(58),o=n(91),u=n(113),l=r("wks"),c=a.Symbol,m=u?c:c&&c.withoutSetter||i;e.exports=function(e){return s(l,e)||(o&&s(c,e)?l[e]=c[e]:l[e]=m("Symbol."+e)),l[e]}},function(e,t,n){var a=n(2),r=n(25).f,s=n(17),i=n(12),o=n(84),u=n(107),l=n(60);e.exports=function(e,t){var n,c,m,d,g,f=e.target,_=e.global,h=e.stat;if(n=_?a:h?a[f]||o(f,{}):(a[f]||{}).prototype)for(c in t){if(d=t[c],m=e.noTargetGet?(g=r(n,c))&&g.value:n[c],!l(_?c:f+(h?".":"#")+c,e.forced)&&void 0!==m){if(typeof d==typeof m)continue;u(d,m)}(e.sham||m&&m.sham)&&s(d,"sham",!0),i(n,c,d,e)}}},function(e,t,n){var a=n(6);e.exports=function(e){if(!a(e))throw TypeError(String(e)+" is not an object");return e}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){"use strict";function a(){return"undefined"==typeof OC?(console.warn("No OC found"),"en"):OC.getLocale()}n(29),n(37),Object.defineProperty(t,"__esModule",{value:!0}),t.getLocale=a,t.getCanonicalLocale=function(){return a().replace(/_/g,"-")},t.getLanguage=function(){if("undefined"==typeof OC)return console.warn("No OC found"),"en";return OC.getLanguage()},t.translate=function(e,t,n,a,r){if("undefined"==typeof OC)return console.warn("No OC found"),t;return OC.L10N.translate(e,t,n,a,r)},t.translatePlural=function(e,t,n,a,r,s){if("undefined"==typeof OC)return console.warn("No OC found"),t;return OC.L10N.translatePlural(e,t,n,a,r,s)},t.getFirstDay=function(){if(void 0===window.firstDay)return console.warn("No firstDay found"),1;return window.firstDay},t.getDayNames=function(){if(void 0===window.dayNames)return console.warn("No dayNames found"),["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];return window.dayNames},t.getDayNamesShort=function(){if(void 0===window.dayNamesShort)return console.warn("No dayNamesShort found"),["Sun.","Mon.","Tue.","Wed.","Thu.","Fri.","Sat."];return window.dayNamesShort},t.getDayNamesMin=function(){if(void 0===window.dayNamesMin)return console.warn("No dayNamesMin found"),["Su","Mo","Tu","We","Th","Fr","Sa"];return window.dayNamesMin},t.getMonthNames=function(){if(void 0===window.monthNames)return console.warn("No monthNames found"),["January","February","March","April","May","June","July","August","September","October","November","December"];return window.monthNames},t.getMonthNamesShort=function(){if(void 0===window.monthNamesShort)return console.warn("No monthNamesShort found"),["Jan.","Feb.","Mar.","Apr.","May.","Jun.","Jul.","Aug.","Sep.","Oct.","Nov.","Dec."];return window.monthNamesShort}},function(e,t,n){var a=n(1);e.exports=!a((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},function(e,t,n){var a=n(9),r=n(105),s=n(5),i=n(42),o=Object.defineProperty;t.f=a?o:function(e,t,n){if(s(e),t=i(t,!0),s(n),r)try{return o(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&&(e[t]=n.value),e}},function(e,t,n){"use strict";var a=n(268),r=Object.prototype.toString;function s(e){return"[object Array]"===r.call(e)}function i(e){return void 0===e}function o(e){return null!==e&&"object"==typeof e}function u(e){return"[object Function]"===r.call(e)}function l(e,t){if(null!=e)if("object"!=typeof e&&(e=[e]),s(e))for(var n=0,a=e.length;n0?r(a(e),9007199254740991):0}},function(e,t,n){"use strict";n(75),n(54),n(24),n(29),n(53),n(37),Object.defineProperty(t,"__esModule",{value:!0}),t.getRootUrl=t.generateFilePath=t.imagePath=t.generateUrl=t.generateOcsUrl=t.generateRemoteUrl=t.linkTo=void 0;t.linkTo=function(e,t){return a(e,"",t)};t.generateRemoteUrl=function(e){return window.location.protocol+"//"+window.location.host+function(e){return r()+"/remote.php/"+e}(e)};t.generateOcsUrl=function(e,t){return t=2!==t?1:2,window.location.protocol+"//"+window.location.host+r()+"/ocs/v"+t+".php/"+e+"/"};t.generateUrl=function(e,t,n){var a=Object.assign({escape:!0,noRewrite:!1},n||{}),s=function(e,t){return t=t||{},e.replace(/{([^{}]*)}/g,(function(e,n){var r=t[n];return a.escape?"string"==typeof r||"number"==typeof r?encodeURIComponent(r.toString()):encodeURIComponent(e):"string"==typeof r||"number"==typeof r?r.toString():e}))};return"/"!==e.charAt(0)&&(e="/"+e),!0!==OC.config.modRewriteWorking||a.noRewrite?r()+"/index.php"+s(e,t||{}):r()+s(e,t||{})};t.imagePath=function(e,t){return-1===t.indexOf(".")?a(e,"img",t+".svg"):a(e,"img",t)};var a=function(e,t,n){var a=-1!==OC.coreApps.indexOf(e),s=r();return"php"!==n.substring(n.length-3)||a?"php"===n.substring(n.length-3)||a?(s+="settings"!==e&&"core"!==e&&"search"!==e||"ajax"!==t?"/":"/index.php/",a||(s+="apps/"),""!==e&&(s+=e+="/"),t&&(s+=t+"/"),s+=n):(s=OC.appswebroots[e],t&&(s+="/"+t+"/"),"/"!==s.substring(s.length-1)&&(s+="/"),s+=n):(s+="/index.php/apps/"+e,"index.php"!==n&&(s+="/",t&&(s+=encodeURI(t+"/")),s+=n)),s};t.generateFilePath=a;var r=function(){return OC.webroot};t.getRootUrl=r},function(e,t,n){"use strict";n(54),Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,r=(a=n(337))&&a.__esModule?a:{default:a},s=n(40);var i=r.default.create({headers:{requesttoken:(0,s.getRequestToken)()}}),o=Object.assign(i,{CancelToken:r.default.CancelToken,isCancel:r.default.isCancel});(0,s.onRequestTokenUpdate)((function(e){return i.defaults.headers.requesttoken=e}));var u=o;t.default=u},function(e,t,n){var a=n(41),r=n(22);e.exports=function(e){return a(r(e))}},function(e,t,n){var a=n(9),r=n(10),s=n(32);e.exports=a?function(e,t,n){return r.f(e,t,s(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t,n){var a=n(22);e.exports=function(e){return Object(a(e))}},function(e,t,n){"use strict";n.r(t),function(e,n){var a=Object.freeze({});function r(e){return null==e}function s(e){return null!=e}function i(e){return!0===e}function o(e){return"string"==typeof e||"number"==typeof e||"symbol"==typeof e||"boolean"==typeof e}function u(e){return null!==e&&"object"==typeof e}var l=Object.prototype.toString;function c(e){return"[object Object]"===l.call(e)}function m(e){return"[object RegExp]"===l.call(e)}function d(e){var t=parseFloat(String(e));return t>=0&&Math.floor(t)===t&&isFinite(e)}function g(e){return s(e)&&"function"==typeof e.then&&"function"==typeof e.catch}function f(e){return null==e?"":Array.isArray(e)||c(e)&&e.toString===l?JSON.stringify(e,null,2):String(e)}function _(e){var t=parseFloat(e);return isNaN(t)?e:t}function h(e,t){for(var n=Object.create(null),a=e.split(","),r=0;r-1)return e.splice(n,1)}}var A=Object.prototype.hasOwnProperty;function v(e,t){return A.call(e,t)}function b(e){var t=Object.create(null);return function(n){return t[n]||(t[n]=e(n))}}var y=/-(\w)/g,k=b((function(e){return e.replace(y,(function(e,t){return t?t.toUpperCase():""}))})),w=b((function(e){return e.charAt(0).toUpperCase()+e.slice(1)})),M=/\B([A-Z])/g,x=b((function(e){return e.replace(M,"-$1").toLowerCase()}));var D=Function.prototype.bind?function(e,t){return e.bind(t)}:function(e,t){function n(n){var a=arguments.length;return a?a>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n};function T(e,t){t=t||0;for(var n=e.length-t,a=new Array(n);n--;)a[n]=e[n+t];return a}function E(e,t){for(var n in t)e[n]=t[n];return e}function S(e){for(var t={},n=0;n0,$=J&&J.indexOf("edge/")>0,K=(J&&J.indexOf("android"),J&&/iphone|ipad|ipod|ios/.test(J)||"ios"===Q),X=(J&&/chrome\/\d+/.test(J),J&&/phantomjs/.test(J),J&&J.match(/firefox\/(\d+)/)),ee={}.watch,te=!1;if(R)try{var ne={};Object.defineProperty(ne,"passive",{get:function(){te=!0}}),window.addEventListener("test-passive",null,ne)}catch(e){}var ae=function(){return void 0===U&&(U=!R&&!W&&void 0!==e&&(e.process&&"server"===e.process.env.VUE_ENV)),U},re=R&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function se(e){return"function"==typeof e&&/native code/.test(e.toString())}var ie,oe="undefined"!=typeof Symbol&&se(Symbol)&&"undefined"!=typeof Reflect&&se(Reflect.ownKeys);ie="undefined"!=typeof Set&&se(Set)?Set:function(){function e(){this.set=Object.create(null)}return e.prototype.has=function(e){return!0===this.set[e]},e.prototype.add=function(e){this.set[e]=!0},e.prototype.clear=function(){this.set=Object.create(null)},e}();var ue=C,le=0,ce=function(){this.id=le++,this.subs=[]};ce.prototype.addSub=function(e){this.subs.push(e)},ce.prototype.removeSub=function(e){F(this.subs,e)},ce.prototype.depend=function(){ce.target&&ce.target.addDep(this)},ce.prototype.notify=function(){var e=this.subs.slice();for(var t=0,n=e.length;t-1)if(s&&!v(r,"default"))i=!1;else if(""===i||i===x(e)){var u=He(String,r.type);(u<0||o0&&(ct((u=e(u,(n||"")+"_"+a))[0])&&ct(c)&&(m[l]=pe(c.text+u[0].text),u.shift()),m.push.apply(m,u)):o(u)?ct(c)?m[l]=pe(c.text+u):""!==u&&m.push(pe(u)):ct(u)&&ct(c)?m[l]=pe(c.text+u.text):(i(t._isVList)&&s(u.tag)&&r(u.key)&&s(n)&&(u.key="__vlist"+n+"_"+a+"__"),m.push(u)));return m}(e):void 0}function ct(e){return s(e)&&s(e.text)&&!1===e.isComment}function mt(e,t){if(e){for(var n=Object.create(null),a=oe?Reflect.ownKeys(e):Object.keys(e),r=0;r0,i=e?!!e.$stable:!s,o=e&&e.$key;if(e){if(e._normalized)return e._normalized;if(i&&n&&n!==a&&o===n.$key&&!s&&!n.$hasNormal)return n;for(var u in r={},e)e[u]&&"$"!==u[0]&&(r[u]=_t(t,u,e[u]))}else r={};for(var l in t)l in r||(r[l]=ht(t,l));return e&&Object.isExtensible(e)&&(e._normalized=r),H(r,"$stable",i),H(r,"$key",o),H(r,"$hasNormal",s),r}function _t(e,t,n){var a=function(){var e=arguments.length?n.apply(null,arguments):n({});return(e=e&&"object"==typeof e&&!Array.isArray(e)?[e]:lt(e))&&(0===e.length||1===e.length&&e[0].isComment)?void 0:e};return n.proxy&&Object.defineProperty(e,t,{get:a,enumerable:!0,configurable:!0}),a}function ht(e,t){return function(){return e[t]}}function pt(e,t){var n,a,r,i,o;if(Array.isArray(e)||"string"==typeof e)for(n=new Array(e.length),a=0,r=e.length;adocument.createEvent("Event").timeStamp&&(un=function(){return ln.now()})}function cn(){var e,t;for(on=un(),rn=!0,en.sort((function(e,t){return e.id-t.id})),sn=0;snsn&&en[n].id>e.id;)n--;en.splice(n+1,0,e)}else en.push(e);an||(an=!0,tt(cn))}}(this)},dn.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||u(e)||this.deep){var t=this.value;if(this.value=e,this.user)try{this.cb.call(this.vm,e,t)}catch(e){ze(e,this.vm,'callback for watcher "'+this.expression+'"')}else this.cb.call(this.vm,e,t)}}},dn.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},dn.prototype.depend=function(){for(var e=this.deps.length;e--;)this.deps[e].depend()},dn.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||F(this.vm._watchers,this);for(var e=this.deps.length;e--;)this.deps[e].removeSub(this);this.active=!1}};var gn={enumerable:!0,configurable:!0,get:C,set:C};function fn(e,t,n){gn.get=function(){return this[t][n]},gn.set=function(e){this[t][n]=e},Object.defineProperty(e,n,gn)}function _n(e){e._watchers=[];var t=e.$options;t.props&&function(e,t){var n=e.$options.propsData||{},a=e._props={},r=e.$options._propKeys=[];e.$parent&&ke(!1);var s=function(s){r.push(s);var i=Ie(s,t,n,e);xe(a,s,i),s in e||fn(e,"_props",s)};for(var i in t)s(i);ke(!0)}(e,t.props),t.methods&&function(e,t){e.$options.props;for(var n in t)e[n]="function"!=typeof t[n]?C:D(t[n],e)}(e,t.methods),t.data?function(e){var t=e.$options.data;c(t=e._data="function"==typeof t?function(e,t){de();try{return e.call(t,t)}catch(e){return ze(e,t,"data()"),{}}finally{ge()}}(t,e):t||{})||(t={});var n=Object.keys(t),a=e.$options.props,r=(e.$options.methods,n.length);for(;r--;){var s=n[r];0,a&&v(a,s)||(i=void 0,36!==(i=(s+"").charCodeAt(0))&&95!==i&&fn(e,"_data",s))}var i;Me(t,!0)}(e):Me(e._data={},!0),t.computed&&function(e,t){var n=e._computedWatchers=Object.create(null),a=ae();for(var r in t){var s=t[r],i="function"==typeof s?s:s.get;0,a||(n[r]=new dn(e,i||C,C,hn)),r in e||pn(e,r,s)}}(e,t.computed),t.watch&&t.watch!==ee&&function(e,t){for(var n in t){var a=t[n];if(Array.isArray(a))for(var r=0;r-1:"string"==typeof e?e.split(",").indexOf(t)>-1:!!m(e)&&e.test(t)}function Dn(e,t){var n=e.cache,a=e.keys,r=e._vnode;for(var s in n){var i=n[s];if(i){var o=Mn(i.componentOptions);o&&!t(o)&&Tn(n,s,a,r)}}}function Tn(e,t,n,a){var r=e[t];!r||a&&r.tag===a.tag||r.componentInstance.$destroy(),e[t]=null,F(n,t)}!function(e){e.prototype._init=function(e){var t=this;t._uid=bn++,t._isVue=!0,e&&e._isComponent?function(e,t){var n=e.$options=Object.create(e.constructor.options),a=t._parentVnode;n.parent=t.parent,n._parentVnode=a;var r=a.componentOptions;n.propsData=r.propsData,n._parentListeners=r.listeners,n._renderChildren=r.children,n._componentTag=r.tag,t.render&&(n.render=t.render,n.staticRenderFns=t.staticRenderFns)}(t,e):t.$options=Ye(yn(t.constructor),e||{},t),t._renderProxy=t,t._self=t,function(e){var t=e.$options,n=t.parent;if(n&&!t.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(e)}e.$parent=n,e.$root=n?n.$root:e,e.$children=[],e.$refs={},e._watcher=null,e._inactive=null,e._directInactive=!1,e._isMounted=!1,e._isDestroyed=!1,e._isBeingDestroyed=!1}(t),function(e){e._events=Object.create(null),e._hasHookEvent=!1;var t=e.$options._parentListeners;t&&Jt(e,t)}(t),function(e){e._vnode=null,e._staticTrees=null;var t=e.$options,n=e.$vnode=t._parentVnode,r=n&&n.context;e.$slots=dt(t._renderChildren,r),e.$scopedSlots=a,e._c=function(t,n,a,r){return Ot(e,t,n,a,r,!1)},e.$createElement=function(t,n,a,r){return Ot(e,t,n,a,r,!0)};var s=n&&n.data;xe(e,"$attrs",s&&s.attrs||a,null,!0),xe(e,"$listeners",t._parentListeners||a,null,!0)}(t),Xt(t,"beforeCreate"),function(e){var t=mt(e.$options.inject,e);t&&(ke(!1),Object.keys(t).forEach((function(n){xe(e,n,t[n])})),ke(!0))}(t),_n(t),function(e){var t=e.$options.provide;t&&(e._provided="function"==typeof t?t.call(e):t)}(t),Xt(t,"created"),t.$options.el&&t.$mount(t.$options.el)}}(kn),function(e){var t={get:function(){return this._data}},n={get:function(){return this._props}};Object.defineProperty(e.prototype,"$data",t),Object.defineProperty(e.prototype,"$props",n),e.prototype.$set=De,e.prototype.$delete=Te,e.prototype.$watch=function(e,t,n){if(c(t))return vn(this,e,t,n);(n=n||{}).user=!0;var a=new dn(this,e,t,n);if(n.immediate)try{t.call(this,a.value)}catch(e){ze(e,this,'callback for immediate watcher "'+a.expression+'"')}return function(){a.teardown()}}}(kn),function(e){var t=/^hook:/;e.prototype.$on=function(e,n){var a=this;if(Array.isArray(e))for(var r=0,s=e.length;r1?T(n):n;for(var a=T(arguments,1),r='event handler for "'+e+'"',s=0,i=n.length;sparseInt(this.max)&&Tn(i,o[0],o,this._vnode)),t.data.keepAlive=!0}return t||e&&e[0]}}};!function(e){var t={get:function(){return O}};Object.defineProperty(e,"config",t),e.util={warn:ue,extend:E,mergeOptions:Ye,defineReactive:xe},e.set=De,e.delete=Te,e.nextTick=tt,e.observable=function(e){return Me(e),e},e.options=Object.create(null),N.forEach((function(t){e.options[t+"s"]=Object.create(null)})),e.options._base=e,E(e.options.components,Sn),function(e){e.use=function(e){var t=this._installedPlugins||(this._installedPlugins=[]);if(t.indexOf(e)>-1)return this;var n=T(arguments,1);return n.unshift(this),"function"==typeof e.install?e.install.apply(e,n):"function"==typeof e&&e.apply(null,n),t.push(e),this}}(e),function(e){e.mixin=function(e){return this.options=Ye(this.options,e),this}}(e),wn(e),function(e){N.forEach((function(t){e[t]=function(e,n){return n?("component"===t&&c(n)&&(n.name=n.name||e,n=this.options._base.extend(n)),"directive"===t&&"function"==typeof n&&(n={bind:n,update:n}),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}}))}(e)}(kn),Object.defineProperty(kn.prototype,"$isServer",{get:ae}),Object.defineProperty(kn.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(kn,"FunctionalRenderContext",{value:Lt}),kn.version="2.6.11";var Cn=h("style,class"),Ln=h("input,textarea,option,select,progress"),jn=h("contenteditable,draggable,spellcheck"),Pn=h("events,caret,typing,plaintext-only"),Bn=h("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"),Yn="http://www.w3.org/1999/xlink",Nn=function(e){return":"===e.charAt(5)&&"xlink"===e.slice(0,5)},In=function(e){return Nn(e)?e.slice(6,e.length):""},On=function(e){return null==e||!1===e};function qn(e){for(var t=e.data,n=e,a=e;s(a.componentInstance);)(a=a.componentInstance._vnode)&&a.data&&(t=Hn(a.data,t));for(;s(n=n.parent);)n&&n.data&&(t=Hn(t,n.data));return function(e,t){if(s(e)||s(t))return zn(e,Un(t));return""}(t.staticClass,t.class)}function Hn(e,t){return{staticClass:zn(e.staticClass,t.staticClass),class:s(e.class)?[e.class,t.class]:t.class}}function zn(e,t){return e?t?e+" "+t:e:t||""}function Un(e){return Array.isArray(e)?function(e){for(var t,n="",a=0,r=e.length;a-1?da(e,t,n):Bn(t)?On(n)?e.removeAttribute(t):(n="allowfullscreen"===t&&"EMBED"===e.tagName?"true":t,e.setAttribute(t,n)):jn(t)?e.setAttribute(t,function(e,t){return On(t)||"false"===t?"false":"contenteditable"===e&&Pn(t)?t:"true"}(t,n)):Nn(t)?On(n)?e.removeAttributeNS(Yn,In(t)):e.setAttributeNS(Yn,t,n):da(e,t,n)}function da(e,t,n){if(On(n))e.removeAttribute(t);else{if(V&&!Z&&"TEXTAREA"===e.tagName&&"placeholder"===t&&""!==n&&!e.__ieph){var a=function(t){t.stopImmediatePropagation(),e.removeEventListener("input",a)};e.addEventListener("input",a),e.__ieph=!0}e.setAttribute(t,n)}}var ga={create:ca,update:ca};function fa(e,t){var n=t.elm,a=t.data,i=e.data;if(!(r(a.staticClass)&&r(a.class)&&(r(i)||r(i.staticClass)&&r(i.class)))){var o=qn(t),u=n._transitionClasses;s(u)&&(o=zn(o,Un(u))),o!==n._prevClass&&(n.setAttribute("class",o),n._prevClass=o)}}var _a,ha={create:fa,update:fa};function pa(e,t,n){var a=_a;return function r(){var s=t.apply(null,arguments);null!==s&&va(e,r,n,a)}}var Fa=Qe&&!(X&&Number(X[1])<=53);function Aa(e,t,n,a){if(Fa){var r=on,s=t;t=s._wrapper=function(e){if(e.target===e.currentTarget||e.timeStamp>=r||e.timeStamp<=0||e.target.ownerDocument!==document)return s.apply(this,arguments)}}_a.addEventListener(e,t,te?{capture:n,passive:a}:n)}function va(e,t,n,a){(a||_a).removeEventListener(e,t._wrapper||t,n)}function ba(e,t){if(!r(e.data.on)||!r(t.data.on)){var n=t.data.on||{},a=e.data.on||{};_a=t.elm,function(e){if(s(e.__r)){var t=V?"change":"input";e[t]=[].concat(e.__r,e[t]||[]),delete e.__r}s(e.__c)&&(e.change=[].concat(e.__c,e.change||[]),delete e.__c)}(n),it(n,a,Aa,va,pa,t.context),_a=void 0}}var ya,ka={create:ba,update:ba};function wa(e,t){if(!r(e.data.domProps)||!r(t.data.domProps)){var n,a,i=t.elm,o=e.data.domProps||{},u=t.data.domProps||{};for(n in s(u.__ob__)&&(u=t.data.domProps=E({},u)),o)n in u||(i[n]="");for(n in u){if(a=u[n],"textContent"===n||"innerHTML"===n){if(t.children&&(t.children.length=0),a===o[n])continue;1===i.childNodes.length&&i.removeChild(i.childNodes[0])}if("value"===n&&"PROGRESS"!==i.tagName){i._value=a;var l=r(a)?"":String(a);Ma(i,l)&&(i.value=l)}else if("innerHTML"===n&&Wn(i.tagName)&&r(i.innerHTML)){(ya=ya||document.createElement("div")).innerHTML=""+a+"";for(var c=ya.firstChild;i.firstChild;)i.removeChild(i.firstChild);for(;c.firstChild;)i.appendChild(c.firstChild)}else if(a!==o[n])try{i[n]=a}catch(e){}}}}function Ma(e,t){return!e.composing&&("OPTION"===e.tagName||function(e,t){var n=!0;try{n=document.activeElement!==e}catch(e){}return n&&e.value!==t}(e,t)||function(e,t){var n=e.value,a=e._vModifiers;if(s(a)){if(a.number)return _(n)!==_(t);if(a.trim)return n.trim()!==t.trim()}return n!==t}(e,t))}var xa={create:wa,update:wa},Da=b((function(e){var t={},n=/:(.+)/;return e.split(/;(?![^(]*\))/g).forEach((function(e){if(e){var a=e.split(n);a.length>1&&(t[a[0].trim()]=a[1].trim())}})),t}));function Ta(e){var t=Ea(e.style);return e.staticStyle?E(e.staticStyle,t):t}function Ea(e){return Array.isArray(e)?S(e):"string"==typeof e?Da(e):e}var Sa,Ca=/^--/,La=/\s*!important$/,ja=function(e,t,n){if(Ca.test(t))e.style.setProperty(t,n);else if(La.test(n))e.style.setProperty(x(t),n.replace(La,""),"important");else{var a=Ba(t);if(Array.isArray(n))for(var r=0,s=n.length;r-1?t.split(Ia).forEach((function(t){return e.classList.add(t)})):e.classList.add(t);else{var n=" "+(e.getAttribute("class")||"")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function qa(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(Ia).forEach((function(t){return e.classList.remove(t)})):e.classList.remove(t),e.classList.length||e.removeAttribute("class");else{for(var n=" "+(e.getAttribute("class")||"")+" ",a=" "+t+" ";n.indexOf(a)>=0;)n=n.replace(a," ");(n=n.trim())?e.setAttribute("class",n):e.removeAttribute("class")}}function Ha(e){if(e){if("object"==typeof e){var t={};return!1!==e.css&&E(t,za(e.name||"v")),E(t,e),t}return"string"==typeof e?za(e):void 0}}var za=b((function(e){return{enterClass:e+"-enter",enterToClass:e+"-enter-to",enterActiveClass:e+"-enter-active",leaveClass:e+"-leave",leaveToClass:e+"-leave-to",leaveActiveClass:e+"-leave-active"}})),Ua=R&&!Z,Ga="transition",Ra="transitionend",Wa="animation",Qa="animationend";Ua&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(Ga="WebkitTransition",Ra="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Wa="WebkitAnimation",Qa="webkitAnimationEnd"));var Ja=R?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(e){return e()};function Va(e){Ja((function(){Ja(e)}))}function Za(e,t){var n=e._transitionClasses||(e._transitionClasses=[]);n.indexOf(t)<0&&(n.push(t),Oa(e,t))}function $a(e,t){e._transitionClasses&&F(e._transitionClasses,t),qa(e,t)}function Ka(e,t,n){var a=er(e,t),r=a.type,s=a.timeout,i=a.propCount;if(!r)return n();var o="transition"===r?Ra:Qa,u=0,l=function(){e.removeEventListener(o,c),n()},c=function(t){t.target===e&&++u>=i&&l()};setTimeout((function(){u0&&(n="transition",c=i,m=s.length):"animation"===t?l>0&&(n="animation",c=l,m=u.length):m=(n=(c=Math.max(i,l))>0?i>l?"transition":"animation":null)?"transition"===n?s.length:u.length:0,{type:n,timeout:c,propCount:m,hasTransform:"transition"===n&&Xa.test(a[Ga+"Property"])}}function tr(e,t){for(;e.length1}function or(e,t){!0!==t.data.show&&ar(t)}var ur=function(e){var t,n,a={},u=e.modules,l=e.nodeOps;for(t=0;tf?A(e,r(n[p+1])?null:n[p+1].elm,n,g,p,a):g>p&&b(t,d,f)}(d,h,p,n,c):s(p)?(s(e.text)&&l.setTextContent(d,""),A(d,null,p,0,p.length-1,n)):s(h)?b(h,0,h.length-1):s(e.text)&&l.setTextContent(d,""):e.text!==t.text&&l.setTextContent(d,t.text),s(f)&&s(g=f.hook)&&s(g=g.postpatch)&&g(e,t)}}}function M(e,t,n){if(i(n)&&s(e.parent))e.parent.data.pendingInsert=t;else for(var a=0;a-1,i.selected!==s&&(i.selected=s);else if(P(gr(i),a))return void(e.selectedIndex!==o&&(e.selectedIndex=o));r||(e.selectedIndex=-1)}}function dr(e,t){return t.every((function(t){return!P(t,e)}))}function gr(e){return"_value"in e?e._value:e.value}function fr(e){e.target.composing=!0}function _r(e){e.target.composing&&(e.target.composing=!1,hr(e.target,"input"))}function hr(e,t){var n=document.createEvent("HTMLEvents");n.initEvent(t,!0,!0),e.dispatchEvent(n)}function pr(e){return!e.componentInstance||e.data&&e.data.transition?e:pr(e.componentInstance._vnode)}var Fr={model:lr,show:{bind:function(e,t,n){var a=t.value,r=(n=pr(n)).data&&n.data.transition,s=e.__vOriginalDisplay="none"===e.style.display?"":e.style.display;a&&r?(n.data.show=!0,ar(n,(function(){e.style.display=s}))):e.style.display=a?s:"none"},update:function(e,t,n){var a=t.value;!a!=!t.oldValue&&((n=pr(n)).data&&n.data.transition?(n.data.show=!0,a?ar(n,(function(){e.style.display=e.__vOriginalDisplay})):rr(n,(function(){e.style.display="none"}))):e.style.display=a?e.__vOriginalDisplay:"none")},unbind:function(e,t,n,a,r){r||(e.style.display=e.__vOriginalDisplay)}}},Ar={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function vr(e){var t=e&&e.componentOptions;return t&&t.Ctor.options.abstract?vr(Gt(t.children)):e}function br(e){var t={},n=e.$options;for(var a in n.propsData)t[a]=e[a];var r=n._parentListeners;for(var s in r)t[k(s)]=r[s];return t}function yr(e,t){if(/\d-keep-alive$/.test(t.tag))return e("keep-alive",{props:t.componentOptions.propsData})}var kr=function(e){return e.tag||Ut(e)},wr=function(e){return"show"===e.name},Mr={name:"transition",props:Ar,abstract:!0,render:function(e){var t=this,n=this.$slots.default;if(n&&(n=n.filter(kr)).length){0;var a=this.mode;0;var r=n[0];if(function(e){for(;e=e.parent;)if(e.data.transition)return!0}(this.$vnode))return r;var s=vr(r);if(!s)return r;if(this._leaving)return yr(e,r);var i="__transition-"+this._uid+"-";s.key=null==s.key?s.isComment?i+"comment":i+s.tag:o(s.key)?0===String(s.key).indexOf(i)?s.key:i+s.key:s.key;var u=(s.data||(s.data={})).transition=br(this),l=this._vnode,c=vr(l);if(s.data.directives&&s.data.directives.some(wr)&&(s.data.show=!0),c&&c.data&&!function(e,t){return t.key===e.key&&t.tag===e.tag}(s,c)&&!Ut(c)&&(!c.componentInstance||!c.componentInstance._vnode.isComment)){var m=c.data.transition=E({},u);if("out-in"===a)return this._leaving=!0,ot(m,"afterLeave",(function(){t._leaving=!1,t.$forceUpdate()})),yr(e,r);if("in-out"===a){if(Ut(s))return l;var d,g=function(){d()};ot(u,"afterEnter",g),ot(u,"enterCancelled",g),ot(m,"delayLeave",(function(e){d=e}))}}return r}}},xr=E({tag:String,moveClass:String},Ar);function Dr(e){e.elm._moveCb&&e.elm._moveCb(),e.elm._enterCb&&e.elm._enterCb()}function Tr(e){e.data.newPos=e.elm.getBoundingClientRect()}function Er(e){var t=e.data.pos,n=e.data.newPos,a=t.left-n.left,r=t.top-n.top;if(a||r){e.data.moved=!0;var s=e.elm.style;s.transform=s.WebkitTransform="translate("+a+"px,"+r+"px)",s.transitionDuration="0s"}}delete xr.mode;var Sr={Transition:Mr,TransitionGroup:{props:xr,beforeMount:function(){var e=this,t=this._update;this._update=function(n,a){var r=Zt(e);e.__patch__(e._vnode,e.kept,!1,!0),e._vnode=e.kept,r(),t.call(e,n,a)}},render:function(e){for(var t=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),a=this.prevChildren=this.children,r=this.$slots.default||[],s=this.children=[],i=br(this),o=0;o-1?Jn[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:Jn[e]=/HTMLUnknownElement/.test(t.toString())},E(kn.options.directives,Fr),E(kn.options.components,Sr),kn.prototype.__patch__=R?ur:C,kn.prototype.$mount=function(e,t){return function(e,t,n){var a;return e.$el=t,e.$options.render||(e.$options.render=he),Xt(e,"beforeMount"),a=function(){e._update(e._render(),n)},new dn(e,a,C,{before:function(){e._isMounted&&!e._isDestroyed&&Xt(e,"beforeUpdate")}},!0),n=!1,null==e.$vnode&&(e._isMounted=!0,Xt(e,"mounted")),e}(this,e=e&&R?function(e){if("string"==typeof e){var t=document.querySelector(e);return t||document.createElement("div")}return e}(e):void 0,t)},R&&setTimeout((function(){O.devtools&&re&&re.emit("init",kn)}),0),t.default=kn}.call(this,n(20),n(318).setImmediate)},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t){e.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){var a,r,s,i=n(297),o=n(2),u=n(6),l=n(17),c=n(7),m=n(57),d=n(43),g=o.WeakMap;if(i){var f=new g,_=f.get,h=f.has,p=f.set;a=function(e,t){return p.call(f,e,t),t},r=function(e){return _.call(f,e)||{}},s=function(e){return h.call(f,e)}}else{var F=m("state");d[F]=!0,a=function(e,t){return l(e,F,t),t},r=function(e){return c(e,F)?e[F]:{}},s=function(e){return c(e,F)}}e.exports={set:a,get:r,has:s,enforce:function(e){return s(e)?r(e):a(e,{})},getterFor:function(e){return function(t){var n;if(!u(t)||(n=r(t)).type!==e)throw TypeError("Incompatible receiver, "+e+" required");return n}}}},function(e,t,n){var a=n(97),r=n(12),s=n(312);a||r(Object.prototype,"toString",s,{unsafe:!0})},function(e,t,n){var a=n(9),r=n(82),s=n(32),i=n(16),o=n(42),u=n(7),l=n(105),c=Object.getOwnPropertyDescriptor;t.f=a?c:function(e,t){if(e=i(e),t=o(t,!0),l)try{return c(e,t)}catch(e){}if(u(e,t))return s(!r.f.call(e,t),e[t])}},function(e,t,n){var a=n(109),r=n(2),s=function(e){return"function"==typeof e?e:void 0};e.exports=function(e,t){return arguments.length<2?s(a[e])||s(r[e]):a[e]&&a[e][t]||r[e]&&r[e][t]}},function(e,t,n){var a=n(9),r=n(1),s=n(7),i=Object.defineProperty,o={},u=function(e){throw e};e.exports=function(e,t){if(s(o,e))return o[e];t||(t={});var n=[][e],l=!!s(t,"ACCESSORS")&&t.ACCESSORS,c=s(t,0)?t[0]:u,m=s(t,1)?t[1]:void 0;return o[e]=!!n&&!r((function(){if(l&&!a)return!0;var e={length:-1};l?i(e,1,{enumerable:!0,get:u}):e[1]=1,n.call(e,c,m)}))}},function(e,t,n){var a=n(10).f,r=n(7),s=n(3)("toStringTag");e.exports=function(e,t,n){e&&!r(e=n?e:e.prototype,s)&&a(e,s,{configurable:!0,value:t})}},function(e,t,n){"use strict";var a=n(4),r=n(69);a({target:"RegExp",proto:!0,forced:/./.exec!==r},{exec:r})},function(e,t,n){"use strict";(function(e){n.d(t,"b",(function(){return v}));var a=("undefined"!=typeof window?window:void 0!==e?e:{}).__VUE_DEVTOOLS_GLOBAL_HOOK__;function r(e,t){if(void 0===t&&(t=[]),null===e||"object"!=typeof e)return e;var n,a=(n=function(t){return t.original===e},t.filter(n)[0]);if(a)return a.copy;var s=Array.isArray(e)?[]:{};return t.push({original:e,copy:s}),Object.keys(e).forEach((function(n){s[n]=r(e[n],t)})),s}function s(e,t){Object.keys(e).forEach((function(n){return t(e[n],n)}))}function i(e){return null!==e&&"object"==typeof e}var o=function(e,t){this.runtime=t,this._children=Object.create(null),this._rawModule=e;var n=e.state;this.state=("function"==typeof n?n():n)||{}},u={namespaced:{configurable:!0}};u.namespaced.get=function(){return!!this._rawModule.namespaced},o.prototype.addChild=function(e,t){this._children[e]=t},o.prototype.removeChild=function(e){delete this._children[e]},o.prototype.getChild=function(e){return this._children[e]},o.prototype.hasChild=function(e){return e in this._children},o.prototype.update=function(e){this._rawModule.namespaced=e.namespaced,e.actions&&(this._rawModule.actions=e.actions),e.mutations&&(this._rawModule.mutations=e.mutations),e.getters&&(this._rawModule.getters=e.getters)},o.prototype.forEachChild=function(e){s(this._children,e)},o.prototype.forEachGetter=function(e){this._rawModule.getters&&s(this._rawModule.getters,e)},o.prototype.forEachAction=function(e){this._rawModule.actions&&s(this._rawModule.actions,e)},o.prototype.forEachMutation=function(e){this._rawModule.mutations&&s(this._rawModule.mutations,e)},Object.defineProperties(o.prototype,u);var l=function(e){this.register([],e,!1)};l.prototype.get=function(e){return e.reduce((function(e,t){return e.getChild(t)}),this.root)},l.prototype.getNamespace=function(e){var t=this.root;return e.reduce((function(e,n){return e+((t=t.getChild(n)).namespaced?n+"/":"")}),"")},l.prototype.update=function(e){!function e(t,n,a){0;if(n.update(a),a.modules)for(var r in a.modules){if(!n.getChild(r))return void 0;e(t.concat(r),n.getChild(r),a.modules[r])}}([],this.root,e)},l.prototype.register=function(e,t,n){var a=this;void 0===n&&(n=!0);var r=new o(t,n);0===e.length?this.root=r:this.get(e.slice(0,-1)).addChild(e[e.length-1],r);t.modules&&s(t.modules,(function(t,r){a.register(e.concat(r),t,n)}))},l.prototype.unregister=function(e){var t=this.get(e.slice(0,-1)),n=e[e.length-1],a=t.getChild(n);a&&a.runtime&&t.removeChild(n)},l.prototype.isRegistered=function(e){var t=this.get(e.slice(0,-1)),n=e[e.length-1];return t.hasChild(n)};var c;var m=function(e){var t=this;void 0===e&&(e={}),!c&&"undefined"!=typeof window&&window.Vue&&A(window.Vue);var n=e.plugins;void 0===n&&(n=[]);var r=e.strict;void 0===r&&(r=!1),this._committing=!1,this._actions=Object.create(null),this._actionSubscribers=[],this._mutations=Object.create(null),this._wrappedGetters=Object.create(null),this._modules=new l(e),this._modulesNamespaceMap=Object.create(null),this._subscribers=[],this._watcherVM=new c,this._makeLocalGettersCache=Object.create(null);var s=this,i=this.dispatch,o=this.commit;this.dispatch=function(e,t){return i.call(s,e,t)},this.commit=function(e,t,n){return o.call(s,e,t,n)},this.strict=r;var u=this._modules.root.state;h(this,u,[],this._modules.root),_(this,u),n.forEach((function(e){return e(t)})),(void 0!==e.devtools?e.devtools:c.config.devtools)&&function(e){a&&(e._devtoolHook=a,a.emit("vuex:init",e),a.on("vuex:travel-to-state",(function(t){e.replaceState(t)})),e.subscribe((function(e,t){a.emit("vuex:mutation",e,t)}),{prepend:!0}),e.subscribeAction((function(e,t){a.emit("vuex:action",e,t)}),{prepend:!0}))}(this)},d={state:{configurable:!0}};function g(e,t,n){return t.indexOf(e)<0&&(n&&n.prepend?t.unshift(e):t.push(e)),function(){var n=t.indexOf(e);n>-1&&t.splice(n,1)}}function f(e,t){e._actions=Object.create(null),e._mutations=Object.create(null),e._wrappedGetters=Object.create(null),e._modulesNamespaceMap=Object.create(null);var n=e.state;h(e,n,[],e._modules.root,!0),_(e,n,t)}function _(e,t,n){var a=e._vm;e.getters={},e._makeLocalGettersCache=Object.create(null);var r=e._wrappedGetters,i={};s(r,(function(t,n){i[n]=function(e,t){return function(){return e(t)}}(t,e),Object.defineProperty(e.getters,n,{get:function(){return e._vm[n]},enumerable:!0})}));var o=c.config.silent;c.config.silent=!0,e._vm=new c({data:{$$state:t},computed:i}),c.config.silent=o,e.strict&&function(e){e._vm.$watch((function(){return this._data.$$state}),(function(){0}),{deep:!0,sync:!0})}(e),a&&(n&&e._withCommit((function(){a._data.$$state=null})),c.nextTick((function(){return a.$destroy()})))}function h(e,t,n,a,r){var s=!n.length,i=e._modules.getNamespace(n);if(a.namespaced&&(e._modulesNamespaceMap[i],e._modulesNamespaceMap[i]=a),!s&&!r){var o=p(t,n.slice(0,-1)),u=n[n.length-1];e._withCommit((function(){c.set(o,u,a.state)}))}var l=a.context=function(e,t,n){var a=""===t,r={dispatch:a?e.dispatch:function(n,a,r){var s=F(n,a,r),i=s.payload,o=s.options,u=s.type;return o&&o.root||(u=t+u),e.dispatch(u,i)},commit:a?e.commit:function(n,a,r){var s=F(n,a,r),i=s.payload,o=s.options,u=s.type;o&&o.root||(u=t+u),e.commit(u,i,o)}};return Object.defineProperties(r,{getters:{get:a?function(){return e.getters}:function(){return function(e,t){if(!e._makeLocalGettersCache[t]){var n={},a=t.length;Object.keys(e.getters).forEach((function(r){if(r.slice(0,a)===t){var s=r.slice(a);Object.defineProperty(n,s,{get:function(){return e.getters[r]},enumerable:!0})}})),e._makeLocalGettersCache[t]=n}return e._makeLocalGettersCache[t]}(e,t)}},state:{get:function(){return p(e.state,n)}}}),r}(e,i,n);a.forEachMutation((function(t,n){!function(e,t,n,a){(e._mutations[t]||(e._mutations[t]=[])).push((function(t){n.call(e,a.state,t)}))}(e,i+n,t,l)})),a.forEachAction((function(t,n){var a=t.root?n:i+n,r=t.handler||t;!function(e,t,n,a){(e._actions[t]||(e._actions[t]=[])).push((function(t){var r,s=n.call(e,{dispatch:a.dispatch,commit:a.commit,getters:a.getters,state:a.state,rootGetters:e.getters,rootState:e.state},t);return(r=s)&&"function"==typeof r.then||(s=Promise.resolve(s)),e._devtoolHook?s.catch((function(t){throw e._devtoolHook.emit("vuex:error",t),t})):s}))}(e,a,r,l)})),a.forEachGetter((function(t,n){!function(e,t,n,a){if(e._wrappedGetters[t])return void 0;e._wrappedGetters[t]=function(e){return n(a.state,a.getters,e.state,e.getters)}}(e,i+n,t,l)})),a.forEachChild((function(a,s){h(e,t,n.concat(s),a,r)}))}function p(e,t){return t.reduce((function(e,t){return e[t]}),e)}function F(e,t,n){return i(e)&&e.type&&(n=t,t=e,e=e.type),{type:e,payload:t,options:n}}function A(e){c&&e===c||function(e){if(Number(e.version.split(".")[0])>=2)e.mixin({beforeCreate:n});else{var t=e.prototype._init;e.prototype._init=function(e){void 0===e&&(e={}),e.init=e.init?[n].concat(e.init):n,t.call(this,e)}}function n(){var e=this.$options;e.store?this.$store="function"==typeof e.store?e.store():e.store:e.parent&&e.parent.$store&&(this.$store=e.parent.$store)}}(c=e)}d.state.get=function(){return this._vm._data.$$state},d.state.set=function(e){0},m.prototype.commit=function(e,t,n){var a=this,r=F(e,t,n),s=r.type,i=r.payload,o=(r.options,{type:s,payload:i}),u=this._mutations[s];u&&(this._withCommit((function(){u.forEach((function(e){e(i)}))})),this._subscribers.slice().forEach((function(e){return e(o,a.state)})))},m.prototype.dispatch=function(e,t){var n=this,a=F(e,t),r=a.type,s=a.payload,i={type:r,payload:s},o=this._actions[r];if(o){try{this._actionSubscribers.slice().filter((function(e){return e.before})).forEach((function(e){return e.before(i,n.state)}))}catch(e){0}var u=o.length>1?Promise.all(o.map((function(e){return e(s)}))):o[0](s);return new Promise((function(e,t){u.then((function(t){try{n._actionSubscribers.filter((function(e){return e.after})).forEach((function(e){return e.after(i,n.state)}))}catch(e){0}e(t)}),(function(e){try{n._actionSubscribers.filter((function(e){return e.error})).forEach((function(t){return t.error(i,n.state,e)}))}catch(e){0}t(e)}))}))}},m.prototype.subscribe=function(e,t){return g(e,this._subscribers,t)},m.prototype.subscribeAction=function(e,t){return g("function"==typeof e?{before:e}:e,this._actionSubscribers,t)},m.prototype.watch=function(e,t,n){var a=this;return this._watcherVM.$watch((function(){return e(a.state,a.getters)}),t,n)},m.prototype.replaceState=function(e){var t=this;this._withCommit((function(){t._vm._data.$$state=e}))},m.prototype.registerModule=function(e,t,n){void 0===n&&(n={}),"string"==typeof e&&(e=[e]),this._modules.register(e,t),h(this,this.state,e,this._modules.get(e),n.preserveState),_(this,this.state)},m.prototype.unregisterModule=function(e){var t=this;"string"==typeof e&&(e=[e]),this._modules.unregister(e),this._withCommit((function(){var n=p(t.state,e.slice(0,-1));c.delete(n,e[e.length-1])})),f(this)},m.prototype.hasModule=function(e){return"string"==typeof e&&(e=[e]),this._modules.isRegistered(e)},m.prototype.hotUpdate=function(e){this._modules.update(e),f(this,!0)},m.prototype._withCommit=function(e){var t=this._committing;this._committing=!0,e(),this._committing=t},Object.defineProperties(m.prototype,d);var v=M((function(e,t){var n={};return w(t).forEach((function(t){var a=t.key,r=t.val;n[a]=function(){var t=this.$store.state,n=this.$store.getters;if(e){var a=x(this.$store,"mapState",e);if(!a)return;t=a.context.state,n=a.context.getters}return"function"==typeof r?r.call(this,t,n):t[r]},n[a].vuex=!0})),n})),b=M((function(e,t){var n={};return w(t).forEach((function(t){var a=t.key,r=t.val;n[a]=function(){for(var t=[],n=arguments.length;n--;)t[n]=arguments[n];var a=this.$store.commit;if(e){var s=x(this.$store,"mapMutations",e);if(!s)return;a=s.context.commit}return"function"==typeof r?r.apply(this,[a].concat(t)):a.apply(this.$store,[r].concat(t))}})),n})),y=M((function(e,t){var n={};return w(t).forEach((function(t){var a=t.key,r=t.val;r=e+r,n[a]=function(){if(!e||x(this.$store,"mapGetters",e))return this.$store.getters[r]},n[a].vuex=!0})),n})),k=M((function(e,t){var n={};return w(t).forEach((function(t){var a=t.key,r=t.val;n[a]=function(){for(var t=[],n=arguments.length;n--;)t[n]=arguments[n];var a=this.$store.dispatch;if(e){var s=x(this.$store,"mapActions",e);if(!s)return;a=s.context.dispatch}return"function"==typeof r?r.apply(this,[a].concat(t)):a.apply(this.$store,[r].concat(t))}})),n}));function w(e){return function(e){return Array.isArray(e)||i(e)}(e)?Array.isArray(e)?e.map((function(e){return{key:e,val:e}})):Object.keys(e).map((function(t){return{key:t,val:e[t]}})):[]}function M(e){return function(t,n){return"string"!=typeof t?(n=t,t=""):"/"!==t.charAt(t.length-1)&&(t+="/"),e(t,n)}}function x(e,t,n){return e._modulesNamespaceMap[n]}function D(e,t,n){var a=n?e.groupCollapsed:e.group;try{a.call(e,t)}catch(n){e.log(t)}}function T(e){try{e.groupEnd()}catch(t){e.log("—— log end ——")}}function E(){var e=new Date;return" @ "+S(e.getHours(),2)+":"+S(e.getMinutes(),2)+":"+S(e.getSeconds(),2)+"."+S(e.getMilliseconds(),3)}function S(e,t){return n="0",a=t-e.toString().length,new Array(a+1).join(n)+e;var n,a}var C={Store:m,install:A,version:"3.5.1",mapState:v,mapMutations:b,mapGetters:y,mapActions:k,createNamespacedHelpers:function(e){return{mapState:v.bind(null,e),mapGetters:y.bind(null,e),mapMutations:b.bind(null,e),mapActions:k.bind(null,e)}},createLogger:function(e){void 0===e&&(e={});var t=e.collapsed;void 0===t&&(t=!0);var n=e.filter;void 0===n&&(n=function(e,t,n){return!0});var a=e.transformer;void 0===a&&(a=function(e){return e});var s=e.mutationTransformer;void 0===s&&(s=function(e){return e});var i=e.actionFilter;void 0===i&&(i=function(e,t){return!0});var o=e.actionTransformer;void 0===o&&(o=function(e){return e});var u=e.logMutations;void 0===u&&(u=!0);var l=e.logActions;void 0===l&&(l=!0);var c=e.logger;return void 0===c&&(c=console),function(e){var m=r(e.state);void 0!==c&&(u&&e.subscribe((function(e,i){var o=r(i);if(n(e,m,o)){var u=E(),l=s(e),d="mutation "+e.type+u;D(c,d,t),c.log("%c prev state","color: #9E9E9E; font-weight: bold",a(m)),c.log("%c mutation","color: #03A9F4; font-weight: bold",l),c.log("%c next state","color: #4CAF50; font-weight: bold",a(o)),T(c)}m=o})),l&&e.subscribeAction((function(e,n){if(i(e,n)){var a=E(),r=o(e),s="action "+e.type+a;D(c,s,t),c.log("%c action","color: #03A9F4; font-weight: bold",r),T(c)}})))}}};t.a=C}).call(this,n(20))},function(e,t,n){window,e.exports=function(e){var t={};function n(a){if(t[a])return t[a].exports;var r=t[a]={i:a,l:!1,exports:{}};return e[a].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,a){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:a})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var a=Object.create(null);if(n.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(a,r,function(t){return e[t]}.bind(null,r));return a},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=3)}([function(e,t){e.exports=n(0)},function(e,t){e.exports=n(328)},function(e,t){e.exports=n(330)},function(e,t,n){"use strict";n.r(t);var a=n(0),r=n.n(a),s=n(1),i=n.n(s),o=n(2),u=new i.a,l=Object(o.getLocale)();[{locale:"ast",json:{charset:"utf-8",headers:{"Last-Translator":"enolp , 2020","Language-Team":"Asturian (https://www.transifex.com/nextcloud/teams/64236/ast/)","Content-Type":"text/plain; charset=UTF-8",Language:"ast","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nenolp , 2020\n"},msgstr:["Last-Translator: enolp , 2020\nLanguage-Team: Asturian (https://www.transifex.com/nextcloud/teams/64236/ast/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: ast\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["segundos"]}}}}},{locale:"cs_CZ",json:{charset:"utf-8",headers:{"Last-Translator":"Pavel Borecki , 2020","Language-Team":"Czech (Czech Republic) (https://www.transifex.com/nextcloud/teams/64236/cs_CZ/)","Content-Type":"text/plain; charset=UTF-8",Language:"cs_CZ","Plural-Forms":"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nPavel Borecki , 2020\n"},msgstr:["Last-Translator: Pavel Borecki , 2020\nLanguage-Team: Czech (Czech Republic) (https://www.transifex.com/nextcloud/teams/64236/cs_CZ/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: cs_CZ\nPlural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["sekund"]}}}}},{locale:"da",json:{charset:"utf-8",headers:{"Last-Translator":"Henrik Troels-Hansen , 2020","Language-Team":"Danish (https://www.transifex.com/nextcloud/teams/64236/da/)","Content-Type":"text/plain; charset=UTF-8",Language:"da","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nHenrik Troels-Hansen , 2020\n"},msgstr:["Last-Translator: Henrik Troels-Hansen , 2020\nLanguage-Team: Danish (https://www.transifex.com/nextcloud/teams/64236/da/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: da\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["sekunder"]}}}}},{locale:"de_DE",json:{charset:"utf-8",headers:{"Last-Translator":"Christoph Wurst , 2020","Language-Team":"German (Germany) (https://www.transifex.com/nextcloud/teams/64236/de_DE/)","Content-Type":"text/plain; charset=UTF-8",Language:"de_DE","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nChristoph Wurst , 2020\n"},msgstr:["Last-Translator: Christoph Wurst , 2020\nLanguage-Team: German (Germany) (https://www.transifex.com/nextcloud/teams/64236/de_DE/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: de_DE\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["Sekunden"]}}}}},{locale:"el",json:{charset:"utf-8",headers:{"Last-Translator":"GRMarksman , 2020","Language-Team":"Greek (https://www.transifex.com/nextcloud/teams/64236/el/)","Content-Type":"text/plain; charset=UTF-8",Language:"el","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nGRMarksman , 2020\n"},msgstr:["Last-Translator: GRMarksman , 2020\nLanguage-Team: Greek (https://www.transifex.com/nextcloud/teams/64236/el/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: el\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["δευτερόλεπτα"]}}}}},{locale:"en_GB",json:{charset:"utf-8",headers:{"Last-Translator":"Oleksa Stasevych , 2020","Language-Team":"English (United Kingdom) (https://www.transifex.com/nextcloud/teams/64236/en_GB/)","Content-Type":"text/plain; charset=UTF-8",Language:"en_GB","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nOleksa Stasevych , 2020\n"},msgstr:["Last-Translator: Oleksa Stasevych , 2020\nLanguage-Team: English (United Kingdom) (https://www.transifex.com/nextcloud/teams/64236/en_GB/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: en_GB\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["seconds"]}}}}},{locale:"es",json:{charset:"utf-8",headers:{"Last-Translator":"Javier San Juan , 2020","Language-Team":"Spanish (https://www.transifex.com/nextcloud/teams/64236/es/)","Content-Type":"text/plain; charset=UTF-8",Language:"es","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nJavier San Juan , 2020\n"},msgstr:["Last-Translator: Javier San Juan , 2020\nLanguage-Team: Spanish (https://www.transifex.com/nextcloud/teams/64236/es/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: es\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["segundos"]}}}}},{locale:"eu",json:{charset:"utf-8",headers:{"Last-Translator":"Asier Iturralde Sarasola , 2020","Language-Team":"Basque (https://www.transifex.com/nextcloud/teams/64236/eu/)","Content-Type":"text/plain; charset=UTF-8",Language:"eu","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nAsier Iturralde Sarasola , 2020\n"},msgstr:["Last-Translator: Asier Iturralde Sarasola , 2020\nLanguage-Team: Basque (https://www.transifex.com/nextcloud/teams/64236/eu/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: eu\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["segundo"]}}}}},{locale:"fr",json:{charset:"utf-8",headers:{"Last-Translator":"Yoplala , 2020","Language-Team":"French (https://www.transifex.com/nextcloud/teams/64236/fr/)","Content-Type":"text/plain; charset=UTF-8",Language:"fr","Plural-Forms":"nplurals=2; plural=(n > 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nYoplala , 2020\n"},msgstr:["Last-Translator: Yoplala , 2020\nLanguage-Team: French (https://www.transifex.com/nextcloud/teams/64236/fr/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: fr\nPlural-Forms: nplurals=2; plural=(n > 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["secondes"]}}}}},{locale:"gl",json:{charset:"utf-8",headers:{"Last-Translator":"Miguel Anxo Bouzada , 2020","Language-Team":"Galician (https://www.transifex.com/nextcloud/teams/64236/gl/)","Content-Type":"text/plain; charset=UTF-8",Language:"gl","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nMiguel Anxo Bouzada , 2020\n"},msgstr:["Last-Translator: Miguel Anxo Bouzada , 2020\nLanguage-Team: Galician (https://www.transifex.com/nextcloud/teams/64236/gl/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: gl\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["segundos"]}}}}},{locale:"he",json:{charset:"utf-8",headers:{"Last-Translator":"Yaron Shahrabani , 2020","Language-Team":"Hebrew (https://www.transifex.com/nextcloud/teams/64236/he/)","Content-Type":"text/plain; charset=UTF-8",Language:"he","Plural-Forms":"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nYaron Shahrabani , 2020\n"},msgstr:["Last-Translator: Yaron Shahrabani , 2020\nLanguage-Team: Hebrew (https://www.transifex.com/nextcloud/teams/64236/he/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: he\nPlural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["שניות"]}}}}},{locale:"hu_HU",json:{charset:"utf-8",headers:{"Last-Translator":"Balázs Meskó , 2020","Language-Team":"Hungarian (Hungary) (https://www.transifex.com/nextcloud/teams/64236/hu_HU/)","Content-Type":"text/plain; charset=UTF-8",Language:"hu_HU","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nBalázs Meskó , 2020\n"},msgstr:["Last-Translator: Balázs Meskó , 2020\nLanguage-Team: Hungarian (Hungary) (https://www.transifex.com/nextcloud/teams/64236/hu_HU/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: hu_HU\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["másodperc"]}}}}},{locale:"is",json:{charset:"utf-8",headers:{"Last-Translator":"Sveinn í Felli , 2020","Language-Team":"Icelandic (https://www.transifex.com/nextcloud/teams/64236/is/)","Content-Type":"text/plain; charset=UTF-8",Language:"is","Plural-Forms":"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nSveinn í Felli , 2020\n"},msgstr:["Last-Translator: Sveinn í Felli , 2020\nLanguage-Team: Icelandic (https://www.transifex.com/nextcloud/teams/64236/is/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: is\nPlural-Forms: nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["sekúndur"]}}}}},{locale:"it",json:{charset:"utf-8",headers:{"Last-Translator":"Random_R, 2020","Language-Team":"Italian (https://www.transifex.com/nextcloud/teams/64236/it/)","Content-Type":"text/plain; charset=UTF-8",Language:"it","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nRandom_R, 2020\n"},msgstr:["Last-Translator: Random_R, 2020\nLanguage-Team: Italian (https://www.transifex.com/nextcloud/teams/64236/it/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: it\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["secondi"]}}}}},{locale:"ja_JP",json:{charset:"utf-8",headers:{"Last-Translator":"YANO Tetsu , 2020","Language-Team":"Japanese (Japan) (https://www.transifex.com/nextcloud/teams/64236/ja_JP/)","Content-Type":"text/plain; charset=UTF-8",Language:"ja_JP","Plural-Forms":"nplurals=1; plural=0;"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nYANO Tetsu , 2020\n"},msgstr:["Last-Translator: YANO Tetsu , 2020\nLanguage-Team: Japanese (Japan) (https://www.transifex.com/nextcloud/teams/64236/ja_JP/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: ja_JP\nPlural-Forms: nplurals=1; plural=0;\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["秒"]}}}}},{locale:"lt_LT",json:{charset:"utf-8",headers:{"Last-Translator":"Moo, 2020","Language-Team":"Lithuanian (Lithuania) (https://www.transifex.com/nextcloud/teams/64236/lt_LT/)","Content-Type":"text/plain; charset=UTF-8",Language:"lt_LT","Plural-Forms":"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nMoo, 2020\n"},msgstr:["Last-Translator: Moo, 2020\nLanguage-Team: Lithuanian (Lithuania) (https://www.transifex.com/nextcloud/teams/64236/lt_LT/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: lt_LT\nPlural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["sek."]}}}}},{locale:"lv",json:{charset:"utf-8",headers:{"Last-Translator":"stendec , 2020","Language-Team":"Latvian (https://www.transifex.com/nextcloud/teams/64236/lv/)","Content-Type":"text/plain; charset=UTF-8",Language:"lv","Plural-Forms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nstendec , 2020\n"},msgstr:["Last-Translator: stendec , 2020\nLanguage-Team: Latvian (https://www.transifex.com/nextcloud/teams/64236/lv/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: lv\nPlural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["sekundes"]}}}}},{locale:"mk",json:{charset:"utf-8",headers:{"Last-Translator":"Сашко Тодоров, 2020","Language-Team":"Macedonian (https://www.transifex.com/nextcloud/teams/64236/mk/)","Content-Type":"text/plain; charset=UTF-8",Language:"mk","Plural-Forms":"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nСашко Тодоров, 2020\n"},msgstr:["Last-Translator: Сашко Тодоров, 2020\nLanguage-Team: Macedonian (https://www.transifex.com/nextcloud/teams/64236/mk/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: mk\nPlural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["секунди"]}}}}},{locale:"nl",json:{charset:"utf-8",headers:{"Last-Translator":"Roeland Jago Douma , 2020","Language-Team":"Dutch (https://www.transifex.com/nextcloud/teams/64236/nl/)","Content-Type":"text/plain; charset=UTF-8",Language:"nl","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nRoeland Jago Douma , 2020\n"},msgstr:["Last-Translator: Roeland Jago Douma , 2020\nLanguage-Team: Dutch (https://www.transifex.com/nextcloud/teams/64236/nl/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: nl\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["seconden"]}}}}},{locale:"oc",json:{charset:"utf-8",headers:{"Last-Translator":"Quentin PAGÈS, 2020","Language-Team":"Occitan (post 1500) (https://www.transifex.com/nextcloud/teams/64236/oc/)","Content-Type":"text/plain; charset=UTF-8",Language:"oc","Plural-Forms":"nplurals=2; plural=(n > 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nQuentin PAGÈS, 2020\n"},msgstr:["Last-Translator: Quentin PAGÈS, 2020\nLanguage-Team: Occitan (post 1500) (https://www.transifex.com/nextcloud/teams/64236/oc/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: oc\nPlural-Forms: nplurals=2; plural=(n > 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["segondas"]}}}}},{locale:"pl",json:{charset:"utf-8",headers:{"Last-Translator":"Janusz Gwiazda , 2020","Language-Team":"Polish (https://www.transifex.com/nextcloud/teams/64236/pl/)","Content-Type":"text/plain; charset=UTF-8",Language:"pl","Plural-Forms":"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nJanusz Gwiazda , 2020\n"},msgstr:["Last-Translator: Janusz Gwiazda , 2020\nLanguage-Team: Polish (https://www.transifex.com/nextcloud/teams/64236/pl/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: pl\nPlural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["sekundy"]}}}}},{locale:"pt_BR",json:{charset:"utf-8",headers:{"Last-Translator":"André Marcelo Alvarenga , 2020","Language-Team":"Portuguese (Brazil) (https://www.transifex.com/nextcloud/teams/64236/pt_BR/)","Content-Type":"text/plain; charset=UTF-8",Language:"pt_BR","Plural-Forms":"nplurals=2; plural=(n > 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nAndré Marcelo Alvarenga , 2020\n"},msgstr:["Last-Translator: André Marcelo Alvarenga , 2020\nLanguage-Team: Portuguese (Brazil) (https://www.transifex.com/nextcloud/teams/64236/pt_BR/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: pt_BR\nPlural-Forms: nplurals=2; plural=(n > 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["segundos"]}}}}},{locale:"pt_PT",json:{charset:"utf-8",headers:{"Last-Translator":"fpapoila , 2020","Language-Team":"Portuguese (Portugal) (https://www.transifex.com/nextcloud/teams/64236/pt_PT/)","Content-Type":"text/plain; charset=UTF-8",Language:"pt_PT","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nfpapoila , 2020\n"},msgstr:["Last-Translator: fpapoila , 2020\nLanguage-Team: Portuguese (Portugal) (https://www.transifex.com/nextcloud/teams/64236/pt_PT/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: pt_PT\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["segundos"]}}}}},{locale:"ru",json:{charset:"utf-8",headers:{"Last-Translator":"Игорь Бондаренко , 2020","Language-Team":"Russian (https://www.transifex.com/nextcloud/teams/64236/ru/)","Content-Type":"text/plain; charset=UTF-8",Language:"ru","Plural-Forms":"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nИгорь Бондаренко , 2020\n"},msgstr:["Last-Translator: Игорь Бондаренко , 2020\nLanguage-Team: Russian (https://www.transifex.com/nextcloud/teams/64236/ru/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: ru\nPlural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["секунды"]}}}}},{locale:"sq",json:{charset:"utf-8",headers:{"Last-Translator":"Greta, 2020","Language-Team":"Albanian (https://www.transifex.com/nextcloud/teams/64236/sq/)","Content-Type":"text/plain; charset=UTF-8",Language:"sq","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nGreta, 2020\n"},msgstr:["Last-Translator: Greta, 2020\nLanguage-Team: Albanian (https://www.transifex.com/nextcloud/teams/64236/sq/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: sq\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["sekonda"]}}}}},{locale:"sr",json:{charset:"utf-8",headers:{"Last-Translator":"Slobodan Simić , 2020","Language-Team":"Serbian (https://www.transifex.com/nextcloud/teams/64236/sr/)","Content-Type":"text/plain; charset=UTF-8",Language:"sr","Plural-Forms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nSlobodan Simić , 2020\n"},msgstr:["Last-Translator: Slobodan Simić , 2020\nLanguage-Team: Serbian (https://www.transifex.com/nextcloud/teams/64236/sr/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: sr\nPlural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["секунде"]}}}}},{locale:"sv",json:{charset:"utf-8",headers:{"Last-Translator":"Magnus Höglund, 2020","Language-Team":"Swedish (https://www.transifex.com/nextcloud/teams/64236/sv/)","Content-Type":"text/plain; charset=UTF-8",Language:"sv","Plural-Forms":"nplurals=2; plural=(n != 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nMagnus Höglund, 2020\n"},msgstr:["Last-Translator: Magnus Höglund, 2020\nLanguage-Team: Swedish (https://www.transifex.com/nextcloud/teams/64236/sv/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: sv\nPlural-Forms: nplurals=2; plural=(n != 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["sekunder"]}}}}},{locale:"tr",json:{charset:"utf-8",headers:{"Last-Translator":"Hüseyin Fahri Uzun , 2020","Language-Team":"Turkish (https://www.transifex.com/nextcloud/teams/64236/tr/)","Content-Type":"text/plain; charset=UTF-8",Language:"tr","Plural-Forms":"nplurals=2; plural=(n > 1);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nHüseyin Fahri Uzun , 2020\n"},msgstr:["Last-Translator: Hüseyin Fahri Uzun , 2020\nLanguage-Team: Turkish (https://www.transifex.com/nextcloud/teams/64236/tr/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: tr\nPlural-Forms: nplurals=2; plural=(n > 1);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["saniye"]}}}}},{locale:"uk",json:{charset:"utf-8",headers:{"Last-Translator":"Oleksa Stasevych , 2020","Language-Team":"Ukrainian (https://www.transifex.com/nextcloud/teams/64236/uk/)","Content-Type":"text/plain; charset=UTF-8",Language:"uk","Plural-Forms":"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nOleksa Stasevych , 2020\n"},msgstr:["Last-Translator: Oleksa Stasevych , 2020\nLanguage-Team: Ukrainian (https://www.transifex.com/nextcloud/teams/64236/uk/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: uk\nPlural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["секунди"]}}}}},{locale:"zh_CN",json:{charset:"utf-8",headers:{"Last-Translator":"Jay Guo , 2020","Language-Team":"Chinese (China) (https://www.transifex.com/nextcloud/teams/64236/zh_CN/)","Content-Type":"text/plain; charset=UTF-8",Language:"zh_CN","Plural-Forms":"nplurals=1; plural=0;"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nJay Guo , 2020\n"},msgstr:["Last-Translator: Jay Guo , 2020\nLanguage-Team: Chinese (China) (https://www.transifex.com/nextcloud/teams/64236/zh_CN/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: zh_CN\nPlural-Forms: nplurals=1; plural=0;\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["秒"]}}}}},{locale:"zh_TW",json:{charset:"utf-8",headers:{"Last-Translator":"Jim Tsai , 2020","Language-Team":"Chinese (Taiwan) (https://www.transifex.com/nextcloud/teams/64236/zh_TW/)","Content-Type":"text/plain; charset=UTF-8",Language:"zh_TW","Plural-Forms":"nplurals=1; plural=0;"},translations:{"":{"":{msgid:"",comments:{translator:"Translators:\nJim Tsai , 2020\n"},msgstr:["Last-Translator: Jim Tsai , 2020\nLanguage-Team: Chinese (Taiwan) (https://www.transifex.com/nextcloud/teams/64236/zh_TW/)\nContent-Type: text/plain; charset=UTF-8\nLanguage: zh_TW\nPlural-Forms: nplurals=1; plural=0;\n"]},seconds:{msgid:"seconds",comments:{reference:"lib/index.ts:22"},msgstr:["秒"]}}}}}].map((function(e){u.addTranslations(e.locale,"messages",e.json)})),u.setLocale(l),r.a.locale(l),r.a.updateLocale(r.a.locale(),{parentLocale:r.a.locale(),relativeTime:Object.assign(r.a.localeData(r.a.locale())._relativeTime,{s:u.gettext("seconds")})}),t.default=r.a}])},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t){e.exports=!1},function(e,t,n){var a=n(45);e.exports=function(e,t,n){if(a(e),void 0===t)return e;switch(n){case 0:return function(){return e.call(t)};case 1:return function(n){return e.call(t,n)};case 2:return function(n,a){return e.call(t,n,a)};case 3:return function(n,a,r){return e.call(t,n,a,r)}}return function(){return e.apply(t,arguments)}}},function(e,t,n){"use strict";var a=n(16),r=n(305),s=n(50),i=n(23),o=n(94),u=i.set,l=i.getterFor("Array Iterator");e.exports=o(Array,"Array",(function(e,t){u(this,{type:"Array Iterator",target:a(e),index:0,kind:t})}),(function(){var e=l(this),t=e.target,n=e.kind,a=e.index++;return!t||a>=t.length?(e.target=void 0,{value:void 0,done:!0}):"keys"==n?{value:a,done:!1}:"values"==n?{value:t[a],done:!1}:{value:[a,t[a]],done:!1}}),"values"),s.Arguments=s.Array,r("keys"),r("values"),r("entries")},function(e,t,n){var a,r=n(5),s=n(118),i=n(88),o=n(43),u=n(119),l=n(83),c=n(57),m=c("IE_PROTO"),d=function(){},g=function(e){return"\n\n\n","/* globals __VUE_SSR_CONTEXT__ */\n\n// IMPORTANT: Do NOT use ES2015 features in this file (except for modules).\n// This module is a runtime utility for cleaner component module output and will\n// be included in the final webpack user bundle.\n\nexport default function normalizeComponent (\n scriptExports,\n render,\n staticRenderFns,\n functionalTemplate,\n injectStyles,\n scopeId,\n moduleIdentifier, /* server only */\n shadowMode /* vue-cli only */\n) {\n // Vue.extend constructor export interop\n var options = typeof scriptExports === 'function'\n ? scriptExports.options\n : scriptExports\n\n // render functions\n if (render) {\n options.render = render\n options.staticRenderFns = staticRenderFns\n options._compiled = true\n }\n\n // functional template\n if (functionalTemplate) {\n options.functional = true\n }\n\n // scopedId\n if (scopeId) {\n options._scopeId = 'data-v-' + scopeId\n }\n\n var hook\n if (moduleIdentifier) { // server build\n hook = function (context) {\n // 2.3 injection\n context =\n context || // cached call\n (this.$vnode && this.$vnode.ssrContext) || // stateful\n (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional\n // 2.2 with runInNewContext: true\n if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {\n context = __VUE_SSR_CONTEXT__\n }\n // inject component styles\n if (injectStyles) {\n injectStyles.call(this, context)\n }\n // register component module identifier for async chunk inferrence\n if (context && context._registeredComponents) {\n context._registeredComponents.add(moduleIdentifier)\n }\n }\n // used by ssr in case component is cached and beforeCreate\n // never gets called\n options._ssrRegister = hook\n } else if (injectStyles) {\n hook = shadowMode\n ? function () {\n injectStyles.call(\n this,\n (options.functional ? this.parent : this).$root.$options.shadowRoot\n )\n }\n : injectStyles\n }\n\n if (hook) {\n if (options.functional) {\n // for template-only hot-reload because in that case the render fn doesn't\n // go through the normalizer\n options._injectStyles = hook\n // register for functional component in vue file\n var originalRender = options.render\n options.render = function renderWithStyleInjection (h, context) {\n hook.call(context)\n return originalRender(h, context)\n }\n } else {\n // inject component registration as beforeCreate hook\n var existing = options.beforeCreate\n options.beforeCreate = existing\n ? [].concat(existing, hook)\n : [hook]\n }\n }\n\n return {\n exports: scriptExports,\n options: options\n }\n}\n","import { render, staticRenderFns } from \"./PredefinedStatus.vue?vue&type=template&id=2af0cabf&scoped=true&\"\nimport script from \"./PredefinedStatus.vue?vue&type=script&lang=js&\"\nexport * from \"./PredefinedStatus.vue?vue&type=script&lang=js&\"\nimport style0 from \"./PredefinedStatus.vue?vue&type=style&index=0&id=2af0cabf&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"2af0cabf\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"predefined-status\",attrs:{\"tabindex\":\"0\"},on:{\"keyup\":[function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,\"enter\",13,$event.key,\"Enter\")){ return null; }return _vm.select($event)},function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,\"space\",32,$event.key,[\" \",\"Spacebar\"])){ return null; }return _vm.select($event)}],\"click\":_vm.select}},[_c('span',{staticClass:\"predefined-status__icon\"},[_vm._v(\"\\n\\t\\t\"+_vm._s(_vm.icon)+\"\\n\\t\")]),_vm._v(\" \"),_c('span',{staticClass:\"predefined-status__message\"},[_vm._v(\"\\n\\t\\t\"+_vm._s(_vm.message)+\"\\n\\t\")]),_vm._v(\" \"),_c('span',{staticClass:\"predefined-status__clear-at\"},[_vm._v(\"\\n\\t\\t\"+_vm._s(_vm._f(\"clearAtFilter\")(_vm.clearAt))+\"\\n\\t\")])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n","import mod from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./PredefinedStatusesList.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./PredefinedStatusesList.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./PredefinedStatusesList.vue?vue&type=template&id=3b99f880&scoped=true&\"\nimport script from \"./PredefinedStatusesList.vue?vue&type=script&lang=js&\"\nexport * from \"./PredefinedStatusesList.vue?vue&type=script&lang=js&\"\nimport style0 from \"./PredefinedStatusesList.vue?vue&type=style&index=0&id=3b99f880&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"3b99f880\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.hasLoaded)?_c('div',{staticClass:\"predefined-statuses-list\"},_vm._l((_vm.predefinedStatuses),function(status){return _c('PredefinedStatus',{key:status.id,attrs:{\"message-id\":status.id,\"icon\":status.icon,\"message\":status.message,\"clear-at\":status.clearAt},on:{\"select\":function($event){return _vm.selectStatus(status)}}})}),1):_c('div',{staticClass:\"predefined-statuses-list\"},[_c('div',{staticClass:\"icon icon-loading-small\"})])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./CustomMessageInput.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./CustomMessageInput.vue?vue&type=script&lang=js&\"","\n\n\n\n\n\n","import { render, staticRenderFns } from \"./CustomMessageInput.vue?vue&type=template&id=08feb764&scoped=true&\"\nimport script from \"./CustomMessageInput.vue?vue&type=script&lang=js&\"\nexport * from \"./CustomMessageInput.vue?vue&type=script&lang=js&\"\nimport style0 from \"./CustomMessageInput.vue?vue&type=style&index=0&id=08feb764&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"08feb764\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('form',{staticClass:\"custom-input__form\",on:{\"submit\":function($event){$event.preventDefault();}}},[_c('input',{attrs:{\"placeholder\":_vm.$t('user_status', 'What\\'s your status?'),\"type\":\"text\"},domProps:{\"value\":_vm.message},on:{\"change\":_vm.change}})])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./ClearAtSelect.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./ClearAtSelect.vue?vue&type=script&lang=js&\"","\n\n\n\n\n\n\n","/**\n * @copyright Copyright (c) 2020 Georg Ehrke\n *\n * @author Georg Ehrke \n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\nimport { translate as t } from '@nextcloud/l10n'\n\n/**\n * Returns an array\n *\n * @returns {Object[]}\n */\nconst getAllClearAtOptions = () => {\n\treturn [{\n\t\tlabel: t('user_status', 'Don\\'t clear'),\n\t\tclearAt: null,\n\t}, {\n\t\tlabel: t('user_status', '30 minutes'),\n\t\tclearAt: {\n\t\t\ttype: 'period',\n\t\t\ttime: 1800,\n\t\t},\n\t}, {\n\t\tlabel: t('user_status', '1 hour'),\n\t\tclearAt: {\n\t\t\ttype: 'period',\n\t\t\ttime: 3600,\n\t\t},\n\t}, {\n\t\tlabel: t('user_status', '4 hours'),\n\t\tclearAt: {\n\t\t\ttype: 'period',\n\t\t\ttime: 14400,\n\t\t},\n\t}, {\n\t\tlabel: t('user_status', 'Today'),\n\t\tclearAt: {\n\t\t\ttype: 'end-of',\n\t\t\ttime: 'day',\n\t\t},\n\t}, {\n\t\tlabel: t('user_status', 'This week'),\n\t\tclearAt: {\n\t\t\ttype: 'end-of',\n\t\t\ttime: 'week',\n\t\t},\n\t}]\n}\n\nexport {\n\tgetAllClearAtOptions,\n}\n","import { render, staticRenderFns } from \"./ClearAtSelect.vue?vue&type=template&id=56360fea&scoped=true&\"\nimport script from \"./ClearAtSelect.vue?vue&type=script&lang=js&\"\nexport * from \"./ClearAtSelect.vue?vue&type=script&lang=js&\"\nimport style0 from \"./ClearAtSelect.vue?vue&type=style&index=0&id=56360fea&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"56360fea\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"clear-at-select\"},[_c('span',{staticClass:\"clear-at-select__label\"},[_vm._v(\"\\n\\t\\t\"+_vm._s(_vm.$t('user_select', 'Clear status after'))+\"\\n\\t\")]),_vm._v(\" \"),_c('Multiselect',{attrs:{\"label\":\"label\",\"value\":_vm.option,\"options\":_vm.options,\"open-direction\":\"top\"},on:{\"select\":_vm.select}})],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n","import mod from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./SetStatusModal.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./SetStatusModal.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./SetStatusModal.vue?vue&type=template&id=9f0c326c&scoped=true&\"\nimport script from \"./SetStatusModal.vue?vue&type=script&lang=js&\"\nexport * from \"./SetStatusModal.vue?vue&type=script&lang=js&\"\nimport style0 from \"./SetStatusModal.vue?vue&type=style&index=0&id=9f0c326c&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"9f0c326c\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('Modal',{attrs:{\"size\":\"normal\",\"title\":_vm.$t('user_status', 'Set a custom status')},on:{\"close\":_vm.closeModal}},[_c('div',{staticClass:\"set-status-modal\"},[_c('div',{staticClass:\"set-status-modal__header\"},[_c('h3',[_vm._v(_vm._s(_vm.$t('user_status', 'Set a custom status')))])]),_vm._v(\" \"),_c('div',{staticClass:\"set-status-modal__custom-input\"},[_c('EmojiPicker',{on:{\"select\":_vm.setIcon}},[_c('button',{staticClass:\"custom-input__emoji-button\"},[_vm._v(\"\\n\\t\\t\\t\\t\\t\"+_vm._s(_vm.visibleIcon)+\"\\n\\t\\t\\t\\t\")])]),_vm._v(\" \"),_c('CustomMessageInput',{attrs:{\"message\":_vm.message},on:{\"change\":_vm.setMessage}})],1),_vm._v(\" \"),_c('PredefinedStatusesList',{on:{\"selectStatus\":_vm.selectPredefinedMessage}}),_vm._v(\" \"),_c('ClearAtSelect',{attrs:{\"clear-at\":_vm.clearAt},on:{\"selectClearAt\":_vm.setClearAt}}),_vm._v(\" \"),_c('div',{staticClass:\"status-buttons\"},[_c('button',{staticClass:\"status-buttons__select\",on:{\"click\":_vm.clearStatus}},[_vm._v(\"\\n\\t\\t\\t\\t\"+_vm._s(_vm.$t('user_status', 'Clear custom status'))+\"\\n\\t\\t\\t\")]),_vm._v(\" \"),_c('button',{staticClass:\"status-buttons__primary primary\",on:{\"click\":_vm.saveStatus}},[_vm._v(\"\\n\\t\\t\\t\\t\"+_vm._s(_vm.$t('user_status', 'Set status'))+\"\\n\\t\\t\\t\")])])],1)])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","/**\n * @copyright Copyright (c) 2020 Georg Ehrke\n *\n * @author Georg Ehrke \n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\nimport HttpClient from '@nextcloud/axios'\nimport { generateUrl } from '@nextcloud/router'\n\n/**\n * Sends a heartbeat\n *\n * @param {Boolean} isAway Whether or not the user is active\n * @returns {Promise}\n */\nconst sendHeartbeat = async(isAway) => {\n\tconst url = generateUrl('/apps/user_status/heartbeat')\n\tawait HttpClient.put(url, {\n\t\tstatus: isAway ? 'away' : 'online',\n\t})\n}\n\nexport {\n\tsendHeartbeat,\n}\n","\n\n\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js&\"","/**\n * @copyright Copyright (c) 2020 Georg Ehrke\n *\n * @author Georg Ehrke \n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\nimport { translate as t } from '@nextcloud/l10n'\n\n/**\n * Returns a list of all user-definable statuses\n *\n * @returns {Object[]}\n */\nconst getAllStatusOptions = () => {\n\treturn [{\n\t\ttype: 'online',\n\t\tlabel: t('user_status', 'Online'),\n\t\ticon: 'icon-user-status-online',\n\t}, {\n\t\ttype: 'away',\n\t\tlabel: t('user_status', 'Away'),\n\t\ticon: 'icon-user-status-away',\n\t}, {\n\t\ttype: 'dnd',\n\t\tlabel: t('user_status', 'Do not disturb'),\n\t\ticon: 'icon-user-status-dnd',\n\n\t}, {\n\t\ttype: 'invisible',\n\t\tlabel: t('user_status', 'Invisible'),\n\t\ticon: 'icon-user-status-invisible',\n\t}]\n}\n\nexport {\n\tgetAllStatusOptions,\n}\n","import { render, staticRenderFns } from \"./App.vue?vue&type=template&id=236d0262&\"\nimport script from \"./App.vue?vue&type=script&lang=js&\"\nexport * from \"./App.vue?vue&type=script&lang=js&\"\nimport style0 from \"./App.vue?vue&type=style&index=0&lang=scss&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('li',[_c('div',{attrs:{\"id\":\"user-status-menu-item\"}},[_c('span',{attrs:{\"id\":\"user-status-menu-item__header\"}},[_vm._v(_vm._s(_vm.displayName))]),_vm._v(\" \"),_c('Actions',{attrs:{\"id\":\"user-status-menu-item__subheader\",\"default-icon\":_vm.statusIcon,\"menu-title\":_vm.visibleMessage}},[_vm._l((_vm.statuses),function(status){return _c('ActionButton',{key:status.type,attrs:{\"icon\":status.icon,\"close-after-click\":true},on:{\"click\":function($event){$event.preventDefault();$event.stopPropagation();return _vm.changeStatus(status.type)}}},[_vm._v(\"\\n\\t\\t\\t\\t\"+_vm._s(status.label)+\"\\n\\t\\t\\t\")])}),_vm._v(\" \"),_c('ActionButton',{attrs:{\"icon\":\"icon-rename\",\"close-after-click\":true},on:{\"click\":function($event){$event.preventDefault();$event.stopPropagation();return _vm.openModal($event)}}},[_vm._v(\"\\n\\t\\t\\t\\t\"+_vm._s(_vm.$t('user_status', 'Set custom status'))+\"\\n\\t\\t\\t\")])],2),_vm._v(\" \"),(_vm.isModalOpen)?_c('SetStatusModal',{on:{\"close\":_vm.closeModal}}):_vm._e()],1)])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","/**\n * @copyright Copyright (c) 2020 Georg Ehrke\n *\n * @author Georg Ehrke \n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\nimport HttpClient from '@nextcloud/axios'\nimport { generateOcsUrl } from '@nextcloud/router'\n\n/**\n * Fetches all predefined statuses from the server\n *\n * @returns {Promise}\n */\nconst fetchAllPredefinedStatuses = async() => {\n\tconst url = generateOcsUrl('apps/user_status/api/v1', 2) + '/predefined_statuses?format=json'\n\tconst response = await HttpClient.get(url)\n\n\treturn response.data.ocs.data\n}\n\nexport {\n\tfetchAllPredefinedStatuses,\n}\n","/**\n * @copyright Copyright (c) 2020 Georg Ehrke\n *\n * @author Georg Ehrke \n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\nimport { fetchAllPredefinedStatuses } from '../services/predefinedStatusService'\n\nconst state = {\n\tpredefinedStatuses: [],\n}\n\nconst mutations = {\n\n\t/**\n\t * Adds a predefined status to the state\n\t *\n\t * @param {Object} state The Vuex state\n\t * @param {Object} status The status to add\n\t */\n\taddPredefinedStatus(state, status) {\n\t\tstate.predefinedStatuses.push(status)\n\t},\n}\n\nconst getters = {}\n\nconst actions = {\n\n\t/**\n\t * Loads all predefined statuses from the server\n\t *\n\t * @param {Object} vuex The Vuex components\n\t * @param {Function} vuex.commit The Vuex commit function\n\t */\n\tasync loadAllPredefinedStatuses({ state, commit }) {\n\t\tif (state.predefinedStatuses.length > 0) {\n\t\t\treturn\n\t\t}\n\n\t\tconst statuses = await fetchAllPredefinedStatuses()\n\t\tfor (const status of statuses) {\n\t\t\tcommit('addPredefinedStatus', status)\n\t\t}\n\t},\n\n}\n\nexport default { state, mutations, getters, actions }\n","/**\n * @copyright Copyright (c) 2020 Georg Ehrke\n *\n * @author Georg Ehrke \n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\nimport HttpClient from '@nextcloud/axios'\nimport { generateOcsUrl } from '@nextcloud/router'\n\n/**\n * Fetches the current user-status\n *\n * @returns {Promise}\n */\nconst fetchCurrentStatus = async() => {\n\tconst url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status'\n\tconst response = await HttpClient.get(url)\n\n\treturn response.data.ocs.data\n}\n\n/**\n * Sets the status\n *\n * @param {String} statusType The status (online / away / dnd / invisible)\n * @returns {Promise}\n */\nconst setStatus = async(statusType) => {\n\tconst url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status/status'\n\tawait HttpClient.put(url, {\n\t\tstatusType,\n\t})\n}\n\n/**\n * Sets a message based on our predefined statuses\n *\n * @param {String} messageId The id of the message, taken from predefined status service\n * @param {Number|null} clearAt When to automatically clean the status\n * @returns {Promise}\n */\nconst setPredefinedMessage = async(messageId, clearAt = null) => {\n\tconst url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status/message/predefined?format=json'\n\tawait HttpClient.put(url, {\n\t\tmessageId,\n\t\tclearAt,\n\t})\n}\n\n/**\n * Sets a custom message\n *\n * @param {String} message The user-defined message\n * @param {String|null} statusIcon The user-defined icon\n * @param {Number|null} clearAt When to automatically clean the status\n * @returns {Promise}\n */\nconst setCustomMessage = async(message, statusIcon = null, clearAt = null) => {\n\tconst url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status/message/custom?format=json'\n\tawait HttpClient.put(url, {\n\t\tmessage,\n\t\tstatusIcon,\n\t\tclearAt,\n\t})\n}\n\n/**\n * Clears the current status of the user\n *\n * @returns {Promise}\n */\nconst clearMessage = async() => {\n\tconst url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status/message?format=json'\n\tawait HttpClient.delete(url)\n}\n\nexport {\n\tfetchCurrentStatus,\n\tsetStatus,\n\tsetCustomMessage,\n\tsetPredefinedMessage,\n\tclearMessage,\n}\n","/**\n * @copyright Copyright (c) 2020 Georg Ehrke\n *\n * @author Georg Ehrke \n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\nimport {\n\tdateFactory,\n} from './dateService'\nimport moment from '@nextcloud/moment'\n\n/**\n * Calculates the actual clearAt timestamp\n *\n * @param {Object|null} clearAt The clear-at config\n * @returns {Number|null}\n */\nconst getTimestampForClearAt = (clearAt) => {\n\tif (clearAt === null) {\n\t\treturn null\n\t}\n\n\tconst date = dateFactory()\n\n\tif (clearAt.type === 'period') {\n\t\tdate.setSeconds(date.getSeconds() + clearAt.time)\n\t\treturn Math.floor(date.getTime() / 1000)\n\t}\n\tif (clearAt.type === 'end-of') {\n\t\tswitch (clearAt.time) {\n\t\tcase 'day':\n\t\tcase 'week':\n\t\t\treturn Number(moment(date).endOf(clearAt.time).format('X'))\n\t\t}\n\t}\n\t// This is not an officially supported type\n\t// but only used internally to show the remaining time\n\t// in the Set Status Modal\n\tif (clearAt.type === '_time') {\n\t\treturn clearAt.time\n\t}\n\n\treturn null\n}\n\nexport {\n\tgetTimestampForClearAt,\n}\n","/**\n * @copyright Copyright (c) 2020 Georg Ehrke\n *\n * @author Georg Ehrke \n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\nimport {\n\tfetchCurrentStatus,\n\tsetStatus,\n\tsetPredefinedMessage,\n\tsetCustomMessage,\n\tclearMessage,\n} from '../services/statusService'\nimport { loadState } from '@nextcloud/initial-state'\nimport { getTimestampForClearAt } from '../services/clearAtService'\n\nconst state = {\n\t// Status (online / away / dnd / invisible / offline)\n\tstatus: null,\n\t// Whether or not the status is user-defined\n\tstatusIsUserDefined: null,\n\t// A custom message set by the user\n\tmessage: null,\n\t// The icon selected by the user\n\ticon: null,\n\t// When to automatically clean the status\n\tclearAt: null,\n\t// Whether or not the message is predefined\n\t// (and can automatically be translated by Nextcloud)\n\tmessageIsPredefined: null,\n\t// The id of the message in case it's predefined\n\tmessageId: null,\n}\n\nconst mutations = {\n\n\t/**\n\t * Sets a new status\n\t *\n\t * @param {Object} state The Vuex state\n\t * @param {Object} data The destructuring object\n\t * @param {String} data.statusType The new status type\n\t */\n\tsetStatus(state, { statusType }) {\n\t\tstate.status = statusType\n\t\tstate.statusIsUserDefined = true\n\t},\n\n\t/**\n\t * Sets a message using a predefined message\n\t *\n\t * @param {Object} state The Vuex state\n\t * @param {Object} data The destructuring object\n\t * @param {String} data.messageId The messageId\n\t * @param {Number|null} data.clearAt When to automatically clear the status\n\t * @param {String} data.message The message\n\t * @param {String} data.icon The icon\n\t */\n\tsetPredefinedMessage(state, { messageId, clearAt, message, icon }) {\n\t\tstate.messageId = messageId\n\t\tstate.messageIsPredefined = true\n\n\t\tstate.message = message\n\t\tstate.icon = icon\n\t\tstate.clearAt = clearAt\n\t},\n\n\t/**\n\t * Sets a custom message\n\t *\n\t * @param {Object} state The Vuex state\n\t * @param {Object} data The destructuring object\n\t * @param {String} data.message The message\n\t * @param {String} data.icon The icon\n\t * @param {Number} data.clearAt When to automatically clear the status\n\t */\n\tsetCustomMessage(state, { message, icon, clearAt }) {\n\t\tstate.messageId = null\n\t\tstate.messageIsPredefined = false\n\n\t\tstate.message = message\n\t\tstate.icon = icon\n\t\tstate.clearAt = clearAt\n\t},\n\n\t/**\n\t * Clears the status\n\t *\n\t * @param {Object} state The Vuex state\n\t */\n\tclearMessage(state) {\n\t\tstate.messageId = null\n\t\tstate.messageIsPredefined = false\n\n\t\tstate.message = null\n\t\tstate.icon = null\n\t\tstate.clearAt = null\n\t},\n\n\t/**\n\t * Loads the status from initial state\n\t *\n\t * @param {Object} state The Vuex state\n\t * @param {Object} data The destructuring object\n\t * @param {String} data.status The status type\n\t * @param {Boolean} data.statusIsUserDefined Whether or not this status is user-defined\n\t * @param {String} data.message The message\n\t * @param {String} data.icon The icon\n\t * @param {Number} data.clearAt When to automatically clear the status\n\t * @param {Boolean} data.messageIsPredefined Whether or not the message is predefined\n\t * @param {string} data.messageId The id of the predefined message\n\t */\n\tloadStatusFromServer(state, { status, statusIsUserDefined, message, icon, clearAt, messageIsPredefined, messageId }) {\n\t\tstate.status = status\n\t\tstate.statusIsUserDefined = statusIsUserDefined\n\t\tstate.message = message\n\t\tstate.icon = icon\n\t\tstate.clearAt = clearAt\n\t\tstate.messageIsPredefined = messageIsPredefined\n\t\tstate.messageId = messageId\n\t},\n}\n\nconst getters = {}\n\nconst actions = {\n\n\t/**\n\t * Sets a new status\n\t *\n\t * @param {Object} vuex The Vuex destructuring object\n\t * @param {Function} vuex.commit The Vuex commit function\n\t * @param {Object} data The data destructuring object\n\t * @param {String} data.statusType The new status type\n\t * @returns {Promise}\n\t */\n\tasync setStatus({ commit }, { statusType }) {\n\t\tawait setStatus(statusType)\n\t\tcommit('setStatus', { statusType })\n\t},\n\n\t/**\n\t * Sets a message using a predefined message\n\t *\n\t * @param {Object} vuex The Vuex destructuring object\n\t * @param {Function} vuex.commit The Vuex commit function\n\t * @param {Object} vuex.rootState The Vuex root state\n\t * @param {Object} data The data destructuring object\n\t * @param {String} data.messageId The messageId\n\t * @param {Object|null} data.clearAt When to automatically clear the status\n\t * @returns {Promise}\n\t */\n\tasync setPredefinedMessage({ commit, rootState }, { messageId, clearAt }) {\n\t\tconst resolvedClearAt = getTimestampForClearAt(clearAt)\n\n\t\tawait setPredefinedMessage(messageId, resolvedClearAt)\n\t\tconst status = rootState.predefinedStatuses.predefinedStatuses.find((status) => status.id === messageId)\n\t\tconst { message, icon } = status\n\n\t\tcommit('setPredefinedMessage', { messageId, clearAt: resolvedClearAt, message, icon })\n\t},\n\n\t/**\n\t * Sets a custom message\n\t *\n\t * @param {Object} vuex The Vuex destructuring object\n\t * @param {Function} vuex.commit The Vuex commit function\n\t * @param {Object} data The data destructuring object\n\t * @param {String} data.message The message\n\t * @param {String} data.icon The icon\n\t * @param {Object|null} data.clearAt When to automatically clear the status\n\t * @returns {Promise}\n\t */\n\tasync setCustomMessage({ commit }, { message, icon, clearAt }) {\n\t\tconst resolvedClearAt = getTimestampForClearAt(clearAt)\n\n\t\tawait setCustomMessage(message, icon, resolvedClearAt)\n\t\tcommit('setCustomMessage', { message, icon, clearAt: resolvedClearAt })\n\t},\n\n\t/**\n\t * Clears the status\n\t *\n\t * @param {Object} vuex The Vuex destructuring object\n\t * @param {Function} vuex.commit The Vuex commit function\n\t * @returns {Promise}\n\t */\n\tasync clearMessage({ commit }) {\n\t\tawait clearMessage()\n\t\tcommit('clearMessage')\n\t},\n\n\t/**\n\t * Re-fetches the status from the server\n\t *\n\t * @param {Object} vuex The Vuex destructuring object\n\t * @param {Function} vuex.commit The Vuex commit function\n\t * @returns {Promise}\n\t */\n\tasync reFetchStatusFromServer({ commit }) {\n\t\tconst status = await fetchCurrentStatus()\n\t\tcommit('loadStatusFromServer', status)\n\t},\n\n\t/**\n\t * Loads the server from the initial state\n\t *\n\t * @param {Object} vuex The Vuex destructuring object\n\t * @param {Function} vuex.commit The Vuex commit function\n\t */\n\tloadStatusFromInitialState({ commit }) {\n\t\tconst status = loadState('user_status', 'status')\n\t\tcommit('loadStatusFromServer', status)\n\t},\n}\n\nexport default { state, mutations, getters, actions }\n","/**\n * @copyright Copyright (c) 2020 Georg Ehrke\n *\n * @author Georg Ehrke \n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\nimport Vue from 'vue'\nimport Vuex from 'vuex'\nimport predefinedStatuses from './predefinedStatuses'\nimport userStatus from './userStatus'\n\nVue.use(Vuex)\n\nexport default new Vuex.Store({\n\tmodules: {\n\t\tpredefinedStatuses,\n\t\tuserStatus,\n\t},\n\tstrict: true,\n})\n","import Vue from 'vue'\nimport { getRequestToken } from '@nextcloud/auth'\nimport App from './App'\nimport store from './store'\n\n// eslint-disable-next-line camelcase\n__webpack_nonce__ = btoa(getRequestToken())\n\n// Correct the root of the app for chunk loading\n// OC.linkTo matches the apps folders\n// OC.generateUrl ensure the index.php (or not)\n// eslint-disable-next-line\n__webpack_public_path__ = OC.linkTo('user_status', 'js/')\n\nVue.prototype.t = t\nVue.prototype.$t = t\n\nconst app = new Vue({\n\trender: h => h(App),\n\tstore,\n}).$mount('li[data-id=\"user_status-menuitem\"]')\n\nexport { app }\n"],"sourceRoot":""} \ No newline at end of file diff --git a/apps/user_status/lib/AppInfo/Application.php b/apps/user_status/lib/AppInfo/Application.php new file mode 100644 index 00000000000..6de72e01839 --- /dev/null +++ b/apps/user_status/lib/AppInfo/Application.php @@ -0,0 +1,74 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\AppInfo; + +use OCA\UserStatus\Capabilities; +use OCA\UserStatus\Listener\BeforeTemplateRenderedListener; +use OCA\UserStatus\Listener\UserDeletedListener; +use OCA\UserStatus\Listener\UserLiveStatusListener; +use OCP\AppFramework\App; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\User\Events\UserDeletedEvent; +use OCP\User\Events\UserLiveStatusEvent; + +/** + * Class Application + * + * @package OCA\UserStatus\AppInfo + */ +class Application extends App implements IBootstrap { + + /** @var string */ + public const APP_ID = 'user_status'; + + /** + * Application constructor. + * + * @param array $urlParams + */ + public function __construct(array $urlParams = []) { + parent::__construct(self::APP_ID, $urlParams); + } + + /** + * @inheritDoc + */ + public function register(IRegistrationContext $context): void { + // Register OCS Capabilities + $context->registerCapability(Capabilities::class); + + // Register Event Listeners + $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); + $context->registerEventListener(UserLiveStatusEvent::class, UserLiveStatusListener::class); + $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); + } + + public function boot(IBootContext $context): void { + } +} diff --git a/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php b/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php new file mode 100644 index 00000000000..40639048843 --- /dev/null +++ b/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php @@ -0,0 +1,63 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\BackgroundJob; + +use OCA\UserStatus\Db\UserStatusMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; + +/** + * Class ClearOldStatusesBackgroundJob + * + * @package OCA\UserStatus\BackgroundJob + */ +class ClearOldStatusesBackgroundJob extends TimedJob { + + /** @var UserStatusMapper */ + private $mapper; + + /** + * ClearOldStatusesBackgroundJob constructor. + * + * @param ITimeFactory $time + * @param UserStatusMapper $mapper + */ + public function __construct(ITimeFactory $time, + UserStatusMapper $mapper) { + parent::__construct($time); + $this->mapper = $mapper; + + // Run every time the cron is run + $this->setInterval(60); + } + + /** + * @inheritDoc + */ + protected function run($argument) { + $this->mapper->clearOlderThan($this->time->getTime()); + } +} diff --git a/apps/user_status/lib/Capabilities.php b/apps/user_status/lib/Capabilities.php new file mode 100644 index 00000000000..ada65402a30 --- /dev/null +++ b/apps/user_status/lib/Capabilities.php @@ -0,0 +1,60 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\UserStatus; + +use OCA\UserStatus\Service\EmojiService; +use OCP\Capabilities\ICapability; + +/** + * Class Capabilities + * + * @package OCA\UserStatus + */ +class Capabilities implements ICapability { + + /** @var EmojiService */ + private $emojiService; + + /** + * Capabilities constructor. + * + * @param EmojiService $emojiService + */ + public function __construct(EmojiService $emojiService) { + $this->emojiService = $emojiService; + } + + /** + * @inheritDoc + */ + public function getCapabilities() { + return [ + 'user_status' => [ + 'enabled' => true, + 'supports_emoji' => $this->emojiService->doesPlatformSupportEmoji(), + ], + ]; + } +} diff --git a/apps/user_status/lib/Controller/HeartbeatController.php b/apps/user_status/lib/Controller/HeartbeatController.php new file mode 100644 index 00000000000..fb8259a2ad7 --- /dev/null +++ b/apps/user_status/lib/Controller/HeartbeatController.php @@ -0,0 +1,92 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Controller; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IRequest; +use OCP\IUserSession; +use OCP\User\Events\UserLiveStatusEvent; + +class HeartbeatController extends Controller { + + /** @var IEventDispatcher */ + private $eventDispatcher; + + /** @var IUserSession */ + private $userSession; + + /** @var ITimeFactory */ + private $timeFactory; + + /** + * HeartbeatController constructor. + * + * @param string $appName + * @param IRequest $request + * @param IEventDispatcher $eventDispatcher + */ + public function __construct(string $appName, + IRequest $request, + IEventDispatcher $eventDispatcher, + IUserSession $userSession, + ITimeFactory $timeFactory) { + parent::__construct($appName, $request); + $this->eventDispatcher = $eventDispatcher; + $this->userSession = $userSession; + $this->timeFactory = $timeFactory; + } + + /** + * @NoAdminRequired + * + * @param string $status + * @return JSONResponse + */ + public function heartbeat(string $status): JSONResponse { + if (!\in_array($status, ['online', 'away'])) { + return new JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + $this->eventDispatcher->dispatchTyped( + new UserLiveStatusEvent( + $user, + $status, + $this->timeFactory->getTime() + ) + ); + + return new JSONResponse([], Http::STATUS_NO_CONTENT); + } +} diff --git a/apps/user_status/lib/Controller/PredefinedStatusController.php b/apps/user_status/lib/Controller/PredefinedStatusController.php new file mode 100644 index 00000000000..4c3530624f3 --- /dev/null +++ b/apps/user_status/lib/Controller/PredefinedStatusController.php @@ -0,0 +1,65 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Controller; + +use OCA\UserStatus\Service\PredefinedStatusService; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\IRequest; + +/** + * Class DefaultStatusController + * + * @package OCA\UserStatus\Controller + */ +class PredefinedStatusController extends OCSController { + + /** @var PredefinedStatusService */ + private $predefinedStatusService; + + /** + * AStatusController constructor. + * + * @param string $appName + * @param IRequest $request + * @param PredefinedStatusService $predefinedStatusService + */ + public function __construct(string $appName, + IRequest $request, + PredefinedStatusService $predefinedStatusService) { + parent::__construct($appName, $request); + $this->predefinedStatusService = $predefinedStatusService; + } + + /** + * @NoAdminRequired + * + * @return DataResponse + */ + public function findAll():DataResponse { + return new DataResponse($this->predefinedStatusService->getDefaultStatuses()); + } +} diff --git a/apps/user_status/lib/Controller/StatusesController.php b/apps/user_status/lib/Controller/StatusesController.php new file mode 100644 index 00000000000..b707708f46a --- /dev/null +++ b/apps/user_status/lib/Controller/StatusesController.php @@ -0,0 +1,107 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Controller; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\IRequest; + +class StatusesController extends OCSController { + + /** @var StatusService */ + private $service; + + /** + * StatusesController constructor. + * + * @param string $appName + * @param IRequest $request + * @param StatusService $service + */ + public function __construct(string $appName, + IRequest $request, + StatusService $service) { + parent::__construct($appName, $request); + $this->service = $service; + } + + /** + * @NoAdminRequired + * + * @param int|null $limit + * @param int|null $offset + * @return DataResponse + */ + public function findAll(?int $limit=null, ?int $offset=null): DataResponse { + $allStatuses = $this->service->findAll($limit, $offset); + + return new DataResponse(array_map(function ($userStatus) { + return $this->formatStatus($userStatus); + }, $allStatuses)); + } + + /** + * @NoAdminRequired + * + * @param string $userId + * @return DataResponse + * @throws OCSNotFoundException + */ + public function find(string $userId): DataResponse { + try { + $userStatus = $this->service->findByUserId($userId); + } catch (DoesNotExistException $ex) { + throw new OCSNotFoundException('No status for the requested userId'); + } + + return new DataResponse($this->formatStatus($userStatus)); + } + + /** + * @NoAdminRequired + * + * @param UserStatus $status + * @return array + */ + private function formatStatus(UserStatus $status): array { + $visibleStatus = $status->getStatus(); + if ($visibleStatus === 'invisible') { + $visibleStatus = 'offline'; + } + + return [ + 'userId' => $status->getUserId(), + 'message' => $status->getCustomMessage(), + 'icon' => $status->getCustomIcon(), + 'clearAt' => $status->getClearAt(), + 'status' => $visibleStatus, + ]; + } +} diff --git a/apps/user_status/lib/Controller/UserStatusController.php b/apps/user_status/lib/Controller/UserStatusController.php new file mode 100644 index 00000000000..ffbe1e753ef --- /dev/null +++ b/apps/user_status/lib/Controller/UserStatusController.php @@ -0,0 +1,191 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\UserStatus\Controller; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Exception\InvalidClearAtException; +use OCA\UserStatus\Exception\InvalidMessageIdException; +use OCA\UserStatus\Exception\InvalidStatusIconException; +use OCA\UserStatus\Exception\InvalidStatusTypeException; +use OCA\UserStatus\Exception\StatusMessageTooLongException; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\ILogger; +use OCP\IRequest; + +class UserStatusController extends OCSController { + + /** @var string */ + private $userId; + + /** @var ILogger */ + private $logger; + + /** @var StatusService */ + private $service; + + /** + * StatusesController constructor. + * + * @param string $appName + * @param IRequest $request + * @param string $userId + * @param ILogger $logger; + * @param StatusService $service + */ + public function __construct(string $appName, + IRequest $request, + string $userId, + ILogger $logger, + StatusService $service) { + parent::__construct($appName, $request); + $this->userId = $userId; + $this->logger = $logger; + $this->service = $service; + } + + /** + * @NoAdminRequired + * + * @return DataResponse + * @throws OCSNotFoundException + */ + public function getStatus(): DataResponse { + try { + $userStatus = $this->service->findByUserId($this->userId); + } catch (DoesNotExistException $ex) { + throw new OCSNotFoundException('No status for the current user'); + } + + return new DataResponse($this->formatStatus($userStatus)); + } + + /** + * @NoAdminRequired + * + * @param string $statusType + * @return DataResponse + * @throws OCSBadRequestException + */ + public function setStatus(string $statusType): DataResponse { + try { + $status = $this->service->setStatus($this->userId, $statusType, null, true); + return new DataResponse($this->formatStatus($status)); + } catch (InvalidStatusTypeException $ex) { + $this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to an invalid status type "' . $statusType . '"'); + throw new OCSBadRequestException($ex->getMessage(), $ex); + } + } + + /** + * @NoAdminRequired + * + * @param string $messageId + * @param int|null $clearAt + * @return DataResponse + * @throws OCSBadRequestException + */ + public function setPredefinedMessage(string $messageId, + ?int $clearAt): DataResponse { + try { + $status = $this->service->setPredefinedMessage($this->userId, $messageId, $clearAt); + return new DataResponse($this->formatStatus($status)); + } catch (InvalidClearAtException $ex) { + $this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to an invalid clearAt value "' . $clearAt . '"'); + throw new OCSBadRequestException($ex->getMessage(), $ex); + } catch (InvalidMessageIdException $ex) { + $this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to an invalid message-id "' . $messageId . '"'); + throw new OCSBadRequestException($ex->getMessage(), $ex); + } + } + + /** + * @NoAdminRequired + * + * @param string|null $statusIcon + * @param string $message + * @param int|null $clearAt + * @return DataResponse + * @throws OCSBadRequestException + */ + public function setCustomMessage(?string $statusIcon, + string $message, + ?int $clearAt): DataResponse { + try { + $status = $this->service->setCustomMessage($this->userId, $statusIcon, $message, $clearAt); + return new DataResponse($this->formatStatus($status)); + } catch (InvalidClearAtException $ex) { + $this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to an invalid clearAt value "' . $clearAt . '"'); + throw new OCSBadRequestException($ex->getMessage(), $ex); + } catch (InvalidStatusIconException $ex) { + $this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to an invalid icon value "' . $statusIcon . '"'); + throw new OCSBadRequestException($ex->getMessage(), $ex); + } catch (StatusMessageTooLongException $ex) { + $this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to a too long status message.'); + throw new OCSBadRequestException($ex->getMessage(), $ex); + } + } + + /** + * @NoAdminRequired + * + * @return DataResponse + */ + public function clearStatus(): DataResponse { + $this->service->clearStatus($this->userId); + return new DataResponse([]); + } + + /** + * @NoAdminRequired + * + * @return DataResponse + */ + public function clearMessage(): DataResponse { + $this->service->clearMessage($this->userId); + return new DataResponse([]); + } + + /** + * @param UserStatus $status + * @return array + */ + private function formatStatus(UserStatus $status): array { + return [ + 'userId' => $status->getUserId(), + 'message' => $status->getCustomMessage(), + 'messageId' => $status->getMessageId(), + 'messageIsPredefined' => $status->getMessageId() !== null, + 'icon' => $status->getCustomIcon(), + 'clearAt' => $status->getClearAt(), + 'status' => $status->getStatus(), + 'statusIsUserDefined' => $status->getIsUserDefined(), + ]; + } +} diff --git a/apps/user_status/lib/Db/UserStatus.php b/apps/user_status/lib/Db/UserStatus.php new file mode 100644 index 00000000000..6faea6e0ecd --- /dev/null +++ b/apps/user_status/lib/Db/UserStatus.php @@ -0,0 +1,90 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Db; + +use OCP\AppFramework\Db\Entity; + +/** + * Class UserStatus + * + * @package OCA\UserStatus\Db + * + * @method int getId() + * @method void setId(int $id) + * @method string getUserId() + * @method void setUserId(string $userId) + * @method string getStatus() + * @method void setStatus(string $status) + * @method int getStatusTimestamp() + * @method void setStatusTimestamp(int $statusTimestamp) + * @method bool getIsUserDefined() + * @method void setIsUserDefined(bool $isUserDefined) + * @method string getMessageId() + * @method void setMessageId(string|null $messageId) + * @method string getCustomIcon() + * @method void setCustomIcon(string|null $customIcon) + * @method string getCustomMessage() + * @method void setCustomMessage(string|null $customMessage) + * @method int getClearAt() + * @method void setClearAt(int|null $clearAt) + */ +class UserStatus extends Entity { + + /** @var string */ + public $userId; + + /** @var string */ + public $status; + + /** @var int */ + public $statusTimestamp; + + /** @var boolean */ + public $isUserDefined; + + /** @var string|null */ + public $messageId; + + /** @var string|null */ + public $customIcon; + + /** @var string|null */ + public $customMessage; + + /** @var int|null */ + public $clearAt; + + public function __construct() { + $this->addType('userId', 'string'); + $this->addType('status', 'string'); + $this->addType('statusTimestamp', 'int'); + $this->addType('isUserDefined', 'boolean'); + $this->addType('messageId', 'string'); + $this->addType('customIcon', 'string'); + $this->addType('customMessage', 'string'); + $this->addType('clearAt', 'int'); + } +} diff --git a/apps/user_status/lib/Db/UserStatusMapper.php b/apps/user_status/lib/Db/UserStatusMapper.php new file mode 100644 index 00000000000..4e78ef11e05 --- /dev/null +++ b/apps/user_status/lib/Db/UserStatusMapper.php @@ -0,0 +1,104 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Db; + +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Class UserStatusMapper + * + * @package OCA\UserStatus\Db + * + * @method UserStatus insert(UserStatus $entity) + * @method UserStatus update(UserStatus $entity) + * @method UserStatus insertOrUpdate(UserStatus $entity) + * @method UserStatus delete(UserStatus $entity) + */ +class UserStatusMapper extends QBMapper { + + /** + * @param IDBConnection $db + */ + public function __construct(IDBConnection $db) { + parent::__construct($db, 'user_status'); + } + + /** + * @param int|null $limit + * @param int|null $offset + * @return UserStatus[] + */ + public function findAll(?int $limit = null, ?int $offset = null):array { + $qb = $this->db->getQueryBuilder(); + $qb + ->select('*') + ->from($this->tableName); + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + return $this->findEntities($qb); + } + + /** + * @param string $userId + * @return UserStatus + * @throws \OCP\AppFramework\Db\DoesNotExistException + */ + public function findByUserId(string $userId):UserStatus { + $qb = $this->db->getQueryBuilder(); + $qb + ->select('*') + ->from($this->tableName) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))); + + return $this->findEntity($qb); + } + + /** + * Clear all statuses older than a given timestamp + * + * @param int $timestamp + */ + public function clearOlderThan(int $timestamp): void { + $qb = $this->db->getQueryBuilder(); + $qb->update($this->tableName) + ->set('message_id', $qb->createNamedParameter(null)) + ->set('custom_icon', $qb->createNamedParameter(null)) + ->set('custom_message', $qb->createNamedParameter(null)) + ->set('clear_at', $qb->createNamedParameter(null)) + ->where($qb->expr()->isNotNull('clear_at')) + ->andWhere($qb->expr()->lte('clear_at', $qb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))); + + $qb->execute(); + } +} diff --git a/apps/user_status/lib/Exception/InvalidClearAtException.php b/apps/user_status/lib/Exception/InvalidClearAtException.php new file mode 100644 index 00000000000..da49a8ee672 --- /dev/null +++ b/apps/user_status/lib/Exception/InvalidClearAtException.php @@ -0,0 +1,29 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Exception; + +class InvalidClearAtException extends \Exception { +} diff --git a/apps/user_status/lib/Exception/InvalidMessageIdException.php b/apps/user_status/lib/Exception/InvalidMessageIdException.php new file mode 100644 index 00000000000..e08045aa8d2 --- /dev/null +++ b/apps/user_status/lib/Exception/InvalidMessageIdException.php @@ -0,0 +1,29 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Exception; + +class InvalidMessageIdException extends \Exception { +} diff --git a/apps/user_status/lib/Exception/InvalidStatusIconException.php b/apps/user_status/lib/Exception/InvalidStatusIconException.php new file mode 100644 index 00000000000..e8dc089327f --- /dev/null +++ b/apps/user_status/lib/Exception/InvalidStatusIconException.php @@ -0,0 +1,29 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Exception; + +class InvalidStatusIconException extends \Exception { +} diff --git a/apps/user_status/lib/Exception/InvalidStatusTypeException.php b/apps/user_status/lib/Exception/InvalidStatusTypeException.php new file mode 100644 index 00000000000..f11b899e7bd --- /dev/null +++ b/apps/user_status/lib/Exception/InvalidStatusTypeException.php @@ -0,0 +1,29 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Exception; + +class InvalidStatusTypeException extends \Exception { +} diff --git a/apps/user_status/lib/Exception/StatusMessageTooLongException.php b/apps/user_status/lib/Exception/StatusMessageTooLongException.php new file mode 100644 index 00000000000..675d7417b06 --- /dev/null +++ b/apps/user_status/lib/Exception/StatusMessageTooLongException.php @@ -0,0 +1,29 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Exception; + +class StatusMessageTooLongException extends \Exception { +} diff --git a/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php b/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php new file mode 100644 index 00000000000..fa3d8ce9e68 --- /dev/null +++ b/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php @@ -0,0 +1,75 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Listener; + +use OCA\UserStatus\AppInfo\Application; +use OCA\UserStatus\Service\JSDataService; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IInitialStateService; + +class BeforeTemplateRenderedListener implements IEventListener { + + /** @var IInitialStateService */ + private $initialState; + + /** @var JSDataService */ + private $jsDataService; + + /** + * BeforeTemplateRenderedListener constructor. + * + * @param IInitialStateService $initialState + * @param JSDataService $jsDataService + */ + public function __construct(IInitialStateService $initialState, + JSDataService $jsDataService) { + $this->initialState = $initialState; + $this->jsDataService = $jsDataService; + } + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + if (!($event instanceof BeforeTemplateRenderedEvent)) { + // Unrelated + return; + } + + if (!$event->isLoggedIn()) { + return; + } + + $this->initialState->provideLazyInitialState(Application::APP_ID, 'status', function () { + return $this->jsDataService; + }); + + \OCP\Util::addScript('user_status', 'user-status-menu'); + \OCP\Util::addStyle('user_status', 'user-status-menu'); + } +} diff --git a/apps/user_status/lib/Listener/UserDeletedListener.php b/apps/user_status/lib/Listener/UserDeletedListener.php new file mode 100644 index 00000000000..b376be00949 --- /dev/null +++ b/apps/user_status/lib/Listener/UserDeletedListener.php @@ -0,0 +1,65 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Listener; + +use OCA\UserStatus\Service\StatusService; +use OCP\EventDispatcher\IEventListener; +use OCP\EventDispatcher\Event; +use OCP\User\Events\UserDeletedEvent; + +/** + * Class UserDeletedListener + * + * @package OCA\UserStatus\Listener + */ +class UserDeletedListener implements IEventListener { + + /** @var StatusService */ + private $service; + + /** + * UserDeletedListener constructor. + * + * @param StatusService $service + */ + public function __construct(StatusService $service) { + $this->service = $service; + } + + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + if (!($event instanceof UserDeletedEvent)) { + // Unrelated + return; + } + + $user = $event->getUser(); + $this->service->removeUserStatus($user->getUID()); + } +} diff --git a/apps/user_status/lib/Listener/UserLiveStatusListener.php b/apps/user_status/lib/Listener/UserLiveStatusListener.php new file mode 100644 index 00000000000..ce97841d9ad --- /dev/null +++ b/apps/user_status/lib/Listener/UserLiveStatusListener.php @@ -0,0 +1,133 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Listener; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Db\UserStatusMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventListener; +use OCP\EventDispatcher\Event; +use OCP\User\Events\UserLiveStatusEvent; + +/** + * Class UserDeletedListener + * + * @package OCA\UserStatus\Listener + */ +class UserLiveStatusListener implements IEventListener { + + /** @var UserStatusMapper */ + private $mapper; + + /** @var ITimeFactory */ + private $timeFactory; + + /** @var string[] */ + private $priorityOrderedStatuses = [ + 'online', + 'away', + 'dnd', + 'invisible', + 'offline' + ]; + + /** @var string[] */ + private $persistentUserStatuses = [ + 'away', + 'dnd', + 'invisible', + ]; + + /** @var int */ + private $offlineThreshold = 300; + + /** + * UserLiveStatusListener constructor. + * + * @param UserStatusMapper $mapper + * @param ITimeFactory $timeFactory + */ + public function __construct(UserStatusMapper $mapper, + ITimeFactory $timeFactory) { + $this->mapper = $mapper; + $this->timeFactory = $timeFactory; + } + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + if (!($event instanceof UserLiveStatusEvent)) { + // Unrelated + return; + } + + $user = $event->getUser(); + try { + $userStatus = $this->mapper->findByUserId($user->getUID()); + } catch (DoesNotExistException $ex) { + $userStatus = new UserStatus(); + $userStatus->setUserId($user->getUID()); + $userStatus->setStatus('offline'); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + } + + // If the status is user-defined and one of the persistent statuses, we + // will not override it. + if ($userStatus->getIsUserDefined() && + \in_array($userStatus->getStatus(), $this->persistentUserStatuses, true)) { + return; + } + + $needsUpdate = false; + + // If the current status is older than 5 minutes, + // treat it as outdated and update + if ($userStatus->getStatusTimestamp() < ($this->timeFactory->getTime() - $this->offlineThreshold)) { + $needsUpdate = true; + } + + // If the emitted status is more important than the current status + // treat it as outdated and update + if (array_search($event->getStatus(), $this->priorityOrderedStatuses) < array_search($userStatus->getStatus(), $this->priorityOrderedStatuses)) { + $needsUpdate = true; + } + + if ($needsUpdate) { + $userStatus->setStatus($event->getStatus()); + $userStatus->setStatusTimestamp($event->getTimestamp()); + $userStatus->setIsUserDefined(false); + + if ($userStatus->getId() === null) { + $this->mapper->insert($userStatus); + } else { + $this->mapper->update($userStatus); + } + } + } +} diff --git a/apps/user_status/lib/Migration/Version0001Date20200602134824.php b/apps/user_status/lib/Migration/Version0001Date20200602134824.php new file mode 100644 index 00000000000..82b33f815b7 --- /dev/null +++ b/apps/user_status/lib/Migration/Version0001Date20200602134824.php @@ -0,0 +1,97 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Migration; + +use Doctrine\DBAL\Types\Types; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Class Version0001Date20200602134824 + * + * @package OCA\UserStatus\Migration + */ +class Version0001Date20200602134824 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + * @since 20.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $statusTable = $schema->createTable('user_status'); + $statusTable->addColumn('id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 20, + 'unsigned' => true, + ]); + $statusTable->addColumn('user_id', Types::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + $statusTable->addColumn('status', Types::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + $statusTable->addColumn('status_timestamp', Types::INTEGER, [ + 'notnull' => true, + 'length' => 11, + 'unsigned' => true, + ]); + $statusTable->addColumn('is_user_defined', Types::BOOLEAN, [ + 'notnull' => true, + ]); + $statusTable->addColumn('message_id', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + ]); + $statusTable->addColumn('custom_icon', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + ]); + $statusTable->addColumn('custom_message', Types::TEXT, [ + 'notnull' => false, + ]); + $statusTable->addColumn('clear_at', Types::INTEGER, [ + 'notnull' => false, + 'length' => 11, + 'unsigned' => true, + ]); + + $statusTable->setPrimaryKey(['id']); + $statusTable->addUniqueIndex(['user_id'], 'user_status_uid_ix'); + $statusTable->addIndex(['clear_at'], 'user_status_clr_ix'); + + return $schema; + } +} diff --git a/apps/user_status/lib/Service/EmojiService.php b/apps/user_status/lib/Service/EmojiService.php new file mode 100644 index 00000000000..bb0506d242f --- /dev/null +++ b/apps/user_status/lib/Service/EmojiService.php @@ -0,0 +1,100 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Service; + +use OCP\IDBConnection; + +/** + * Class EmojiService + * + * @package OCA\UserStatus\Service + */ +class EmojiService { + + /** @var IDBConnection */ + private $db; + + /** + * EmojiService constructor. + * + * @param IDBConnection $db + */ + public function __construct(IDBConnection $db) { + $this->db = $db; + } + + /** + * @return bool + */ + public function doesPlatformSupportEmoji(): bool { + return $this->db->supports4ByteText() && + \class_exists(\IntlBreakIterator::class); + } + + /** + * @param string $emoji + * @return bool + */ + public function isValidEmoji(string $emoji): bool { + $intlBreakIterator = \IntlBreakIterator::createCharacterInstance(); + $intlBreakIterator->setText($emoji); + + $characterCount = 0; + while ($intlBreakIterator->next() !== \IntlBreakIterator::DONE) { + $characterCount++; + } + + if ($characterCount !== 1) { + return false; + } + + $codePointIterator = \IntlBreakIterator::createCodePointInstance(); + $codePointIterator->setText($emoji); + + foreach ($codePointIterator->getPartsIterator() as $codePoint) { + $codePointType = \IntlChar::charType($codePoint); + + // If the current code-point is an emoji or a modifier (like a skin-tone) + // just continue and check the next character + if ($codePointType === \IntlChar::CHAR_CATEGORY_MODIFIER_SYMBOL || + $codePointType === \IntlChar::CHAR_CATEGORY_MODIFIER_LETTER || + $codePointType === \IntlChar::CHAR_CATEGORY_OTHER_SYMBOL) { + continue; + } + + // If it's neither a modifier nor an emoji, we only allow + // a zero-width-joiner or a variation selector 16 + $codePointValue = \IntlChar::ord($codePoint); + if ($codePointValue === 8205 || $codePointValue === 65039) { + continue; + } + + return false; + } + + return true; + } +} diff --git a/apps/user_status/lib/Service/JSDataService.php b/apps/user_status/lib/Service/JSDataService.php new file mode 100644 index 00000000000..ebe801cd57a --- /dev/null +++ b/apps/user_status/lib/Service/JSDataService.php @@ -0,0 +1,84 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Service; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\IUserSession; + +class JSDataService implements \JsonSerializable { + + /** @var IUserSession */ + private $userSession; + + /** @var StatusService */ + private $statusService; + + /** + * JSDataService constructor. + * + * @param IUserSession $userSession + * @param StatusService $statusService + */ + public function __construct(IUserSession $userSession, + StatusService $statusService) { + $this->userSession = $userSession; + $this->statusService = $statusService; + } + + public function jsonSerialize() { + $user = $this->userSession->getUser(); + + if ($user === null) { + return []; + } + + try { + $status = $this->statusService->findByUserId($user->getUID()); + } catch (DoesNotExistException $ex) { + return [ + 'userId' => $user->getUID(), + 'message' => null, + 'messageId' => null, + 'messageIsPredefined' => false, + 'icon' => null, + 'clearAt' => null, + 'status' => 'offline', + 'statusIsUserDefined' => false, + ]; + } + + return [ + 'userId' => $status->getUserId(), + 'message' => $status->getCustomMessage(), + 'messageId' => $status->getMessageId(), + 'messageIsPredefined' => $status->getMessageId() !== null, + 'icon' => $status->getCustomIcon(), + 'clearAt' => $status->getClearAt(), + 'status' => $status->getStatus(), + 'statusIsUserDefined' => $status->getIsUserDefined(), + ]; + } +} diff --git a/apps/user_status/lib/Service/PredefinedStatusService.php b/apps/user_status/lib/Service/PredefinedStatusService.php new file mode 100644 index 00000000000..e8a82014b7b --- /dev/null +++ b/apps/user_status/lib/Service/PredefinedStatusService.php @@ -0,0 +1,187 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Service; + +use OCP\IL10N; + +/** + * Class DefaultStatusService + * + * We are offering a set of default statuses, so we can + * translate them into different languages. + * + * @package OCA\UserStatus\Service + */ +class PredefinedStatusService { + private const MEETING = 'meeting'; + private const COMMUTING = 'commuting'; + private const SICK_LEAVE = 'sick-leave'; + private const VACATIONING = 'vacationing'; + private const REMOTE_WORK = 'remote-work'; + + /** @var IL10N */ + private $l10n; + + /** + * DefaultStatusService constructor. + * + * @param IL10N $l10n + */ + public function __construct(IL10N $l10n) { + $this->l10n = $l10n; + } + + /** + * @return array + */ + public function getDefaultStatuses(): array { + return [ + [ + 'id' => self::MEETING, + 'icon' => '📅', + 'message' => $this->getTranslatedStatusForId(self::MEETING), + 'clearAt' => [ + 'type' => 'period', + 'time' => 3600, + ], + ], + [ + 'id' => self::COMMUTING, + 'icon' => '🚌', + 'message' => $this->getTranslatedStatusForId(self::COMMUTING), + 'clearAt' => [ + 'type' => 'period', + 'time' => 1800, + ], + ], + [ + 'id' => self::REMOTE_WORK, + 'icon' => '🏡', + 'message' => $this->getTranslatedStatusForId(self::REMOTE_WORK), + 'clearAt' => [ + 'type' => 'end-of', + 'time' => 'day', + ], + ], + [ + 'id' => self::SICK_LEAVE, + 'icon' => '🤒', + 'message' => $this->getTranslatedStatusForId(self::SICK_LEAVE), + 'clearAt' => [ + 'type' => 'end-of', + 'time' => 'day', + ], + ], + [ + 'id' => self::VACATIONING, + 'icon' => '🌴', + 'message' => $this->getTranslatedStatusForId(self::VACATIONING), + 'clearAt' => null, + ], + ]; + } + + /** + * @param string $id + * @return array|null + */ + public function getDefaultStatusById(string $id): ?array { + foreach ($this->getDefaultStatuses() as $status) { + if ($status['id'] === $id) { + return $status; + } + } + + return null; + } + + /** + * @param string $id + * @return string|null + */ + public function getIconForId(string $id): ?string { + switch ($id) { + case self::MEETING: + return '📅'; + + case self::COMMUTING: + return '🚌'; + + case self::SICK_LEAVE: + return '🤒'; + + case self::VACATIONING: + return '🌴'; + + case self::REMOTE_WORK: + return '🏡'; + + default: + return null; + } + } + + /** + * @param string $lang + * @param string $id + * @return string|null + */ + public function getTranslatedStatusForId(string $id): ?string { + switch ($id) { + case self::MEETING: + return $this->l10n->t('In a meeting'); + + case self::COMMUTING: + return $this->l10n->t('Commuting'); + + case self::SICK_LEAVE: + return $this->l10n->t('Out sick'); + + case self::VACATIONING: + return $this->l10n->t('Vacationing'); + + case self::REMOTE_WORK: + return $this->l10n->t('Working remotely'); + + default: + return null; + } + } + + /** + * @param string $id + * @return bool + */ + public function isValidId(string $id): bool { + return \in_array($id, [ + self::MEETING, + self::COMMUTING, + self::SICK_LEAVE, + self::VACATIONING, + self::REMOTE_WORK, + ], true); + } +} diff --git a/apps/user_status/lib/Service/StatusService.php b/apps/user_status/lib/Service/StatusService.php new file mode 100644 index 00000000000..83fcd0a8f02 --- /dev/null +++ b/apps/user_status/lib/Service/StatusService.php @@ -0,0 +1,335 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Service; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Db\UserStatusMapper; +use OCA\UserStatus\Exception\InvalidClearAtException; +use OCA\UserStatus\Exception\InvalidMessageIdException; +use OCA\UserStatus\Exception\InvalidStatusIconException; +use OCA\UserStatus\Exception\InvalidStatusTypeException; +use OCA\UserStatus\Exception\StatusMessageTooLongException; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; + +/** + * Class StatusService + * + * @package OCA\UserStatus\Service + */ +class StatusService { + + /** @var UserStatusMapper */ + private $mapper; + + /** @var ITimeFactory */ + private $timeFactory; + + /** @var PredefinedStatusService */ + private $predefinedStatusService; + + /** @var EmojiService */ + private $emojiService; + + /** @var string[] */ + private $allowedStatusTypes = [ + 'online', + 'away', + 'dnd', + 'invisible', + 'offline' + ]; + + /** @var int */ + private $maximumMessageLength = 80; + + /** + * StatusService constructor. + * + * @param UserStatusMapper $mapper + * @param ITimeFactory $timeFactory + * @param PredefinedStatusService $defaultStatusService, + * @param EmojiService $emojiService + */ + public function __construct(UserStatusMapper $mapper, + ITimeFactory $timeFactory, + PredefinedStatusService $defaultStatusService, + EmojiService $emojiService) { + $this->mapper = $mapper; + $this->timeFactory = $timeFactory; + $this->predefinedStatusService = $defaultStatusService; + $this->emojiService = $emojiService; + } + + /** + * @param int|null $limit + * @param int|null $offset + * @return UserStatus[] + */ + public function findAll(?int $limit = null, ?int $offset = null): array { + return array_map(function ($status) { + return $this->processStatus($status); + }, $this->mapper->findAll($limit, $offset)); + } + + /** + * @param string $userId + * @return UserStatus + * @throws DoesNotExistException + */ + public function findByUserId(string $userId):UserStatus { + return $this->processStatus($this->mapper->findByUserId($userId)); + } + + /** + * @param string $userId + * @param string $status + * @param int|null $statusTimestamp + * @param bool $isUserDefined + * @return UserStatus + * @throws InvalidStatusTypeException + */ + public function setStatus(string $userId, + string $status, + ?int $statusTimestamp, + bool $isUserDefined): UserStatus { + try { + $userStatus = $this->mapper->findByUserId($userId); + } catch (DoesNotExistException $ex) { + $userStatus = new UserStatus(); + $userStatus->setUserId($userId); + } + + // Check if status-type is valid + if (!\in_array($status, $this->allowedStatusTypes, true)) { + throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported'); + } + if ($statusTimestamp === null) { + $statusTimestamp = $this->timeFactory->getTime(); + } + + $userStatus->setStatus($status); + $userStatus->setStatusTimestamp($statusTimestamp); + $userStatus->setIsUserDefined($isUserDefined); + + if ($userStatus->getId() === null) { + return $this->mapper->insert($userStatus); + } + + return $this->mapper->update($userStatus); + } + + /** + * @param string $userId + * @param string $messageId + * @param int|null $clearAt + * @return UserStatus + * @throws InvalidMessageIdException + * @throws InvalidClearAtException + */ + public function setPredefinedMessage(string $userId, + string $messageId, + ?int $clearAt): UserStatus { + try { + $userStatus = $this->mapper->findByUserId($userId); + } catch (DoesNotExistException $ex) { + $userStatus = new UserStatus(); + $userStatus->setUserId($userId); + $userStatus->setStatus('offline'); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + } + + if (!$this->predefinedStatusService->isValidId($messageId)) { + throw new InvalidMessageIdException('Message-Id "' . $messageId . '" is not supported'); + } + + // Check that clearAt is in the future + if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) { + throw new InvalidClearAtException('ClearAt is in the past'); + } + + $userStatus->setMessageId($messageId); + $userStatus->setCustomIcon(null); + $userStatus->setCustomMessage(null); + $userStatus->setClearAt($clearAt); + + if ($userStatus->getId() === null) { + return $this->mapper->insert($userStatus); + } + + return $this->mapper->update($userStatus); + } + + /** + * @param string $userId + * @param string|null $statusIcon + * @param string|null $message + * @param int|null $clearAt + * @return UserStatus + * @throws InvalidClearAtException + * @throws InvalidStatusIconException + * @throws StatusMessageTooLongException + */ + public function setCustomMessage(string $userId, + ?string $statusIcon, + string $message, + ?int $clearAt): UserStatus { + try { + $userStatus = $this->mapper->findByUserId($userId); + } catch (DoesNotExistException $ex) { + $userStatus = new UserStatus(); + $userStatus->setUserId($userId); + $userStatus->setStatus('offline'); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + } + + // Check if statusIcon contains only one character + if ($statusIcon !== null && !$this->emojiService->isValidEmoji($statusIcon)) { + throw new InvalidStatusIconException('Status-Icon is longer than one character'); + } + // Check for maximum length of custom message + if (\mb_strlen($message) > $this->maximumMessageLength) { + throw new StatusMessageTooLongException('Message is longer than supported length of ' . $this->maximumMessageLength . ' characters'); + } + // Check that clearAt is in the future + if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) { + throw new InvalidClearAtException('ClearAt is in the past'); + } + + $userStatus->setMessageId(null); + $userStatus->setCustomIcon($statusIcon); + $userStatus->setCustomMessage($message); + $userStatus->setClearAt($clearAt); + + if ($userStatus->getId() === null) { + return $this->mapper->insert($userStatus); + } + + return $this->mapper->update($userStatus); + } + + /** + * @param string $userId + * @return bool + */ + public function clearStatus(string $userId): bool { + try { + $userStatus = $this->mapper->findByUserId($userId); + } catch (DoesNotExistException $ex) { + // if there is no status to remove, just return + return false; + } + + $userStatus->setStatus('offline'); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + + $this->mapper->update($userStatus); + return true; + } + + /** + * @param string $userId + * @return bool + */ + public function clearMessage(string $userId): bool { + try { + $userStatus = $this->mapper->findByUserId($userId); + } catch (DoesNotExistException $ex) { + // if there is no status to remove, just return + return false; + } + + $userStatus->setMessageId(null); + $userStatus->setCustomMessage(null); + $userStatus->setCustomIcon(null); + $userStatus->setClearAt(null); + + $this->mapper->update($userStatus); + return true; + } + + /** + * @param string $userId + * @return bool + */ + public function removeUserStatus(string $userId): bool { + try { + $userStatus = $this->mapper->findByUserId($userId); + } catch (DoesNotExistException $ex) { + // if there is no status to remove, just return + return false; + } + + $this->mapper->delete($userStatus); + return true; + } + + /** + * Processes a status to check if custom message is still + * up to date and provides translated default status if needed + * + * @param UserStatus $status + * @returns UserStatus + */ + private function processStatus(UserStatus $status): UserStatus { + $clearAt = $status->getClearAt(); + if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) { + $this->cleanStatus($status); + } + if ($status->getMessageId() !== null) { + $this->addDefaultMessage($status); + } + + return $status; + } + + /** + * @param UserStatus $status + */ + private function cleanStatus(UserStatus $status): void { + $status->setMessageId(null); + $status->setCustomIcon(null); + $status->setCustomMessage(null); + $status->setClearAt(null); + + $this->mapper->update($status); + } + + /** + * @param UserStatus $status + */ + private function addDefaultMessage(UserStatus $status): void { + // If the message is predefined, insert the translated message and icon + $predefinedMessage = $this->predefinedStatusService->getDefaultStatusById($status->getMessageId()); + if ($predefinedMessage !== null) { + $status->setCustomMessage($predefinedMessage['message']); + $status->setCustomIcon($predefinedMessage['icon']); + } + } +} diff --git a/apps/user_status/src/App.vue b/apps/user_status/src/App.vue new file mode 100644 index 00000000000..e8c3021c7ec --- /dev/null +++ b/apps/user_status/src/App.vue @@ -0,0 +1,271 @@ + + + + + + + diff --git a/apps/user_status/src/components/ClearAtSelect.vue b/apps/user_status/src/components/ClearAtSelect.vue new file mode 100644 index 00000000000..af0db698ad9 --- /dev/null +++ b/apps/user_status/src/components/ClearAtSelect.vue @@ -0,0 +1,102 @@ + + + + + + + diff --git a/apps/user_status/src/components/CustomMessageInput.vue b/apps/user_status/src/components/CustomMessageInput.vue new file mode 100644 index 00000000000..04bc2f026da --- /dev/null +++ b/apps/user_status/src/components/CustomMessageInput.vue @@ -0,0 +1,65 @@ + + + + + + diff --git a/apps/user_status/src/components/PredefinedStatus.vue b/apps/user_status/src/components/PredefinedStatus.vue new file mode 100644 index 00000000000..c7fd4d63fed --- /dev/null +++ b/apps/user_status/src/components/PredefinedStatus.vue @@ -0,0 +1,111 @@ + + + + + + diff --git a/apps/user_status/src/components/PredefinedStatusesList.vue b/apps/user_status/src/components/PredefinedStatusesList.vue new file mode 100644 index 00000000000..844fdbbdfe3 --- /dev/null +++ b/apps/user_status/src/components/PredefinedStatusesList.vue @@ -0,0 +1,90 @@ + + + + + + + diff --git a/apps/user_status/src/components/SetStatusModal.vue b/apps/user_status/src/components/SetStatusModal.vue new file mode 100644 index 00000000000..46c289d9e81 --- /dev/null +++ b/apps/user_status/src/components/SetStatusModal.vue @@ -0,0 +1,236 @@ + + + + + + + diff --git a/apps/user_status/src/filters/clearAtFilter.js b/apps/user_status/src/filters/clearAtFilter.js new file mode 100644 index 00000000000..22579baa82a --- /dev/null +++ b/apps/user_status/src/filters/clearAtFilter.js @@ -0,0 +1,68 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { translate as t } from '@nextcloud/l10n' +import moment from '@nextcloud/moment' +import { dateFactory } from '../services/dateService' + +/** + * Formats a clearAt object to be human readable + * + * @param {Object} clearAt The clearAt object + * @returns {string|null} + */ +const clearAtFilter = (clearAt) => { + if (clearAt === null) { + return t('user_status', 'Don\'t clear') + } + + if (clearAt.type === 'end-of') { + switch (clearAt.time) { + case 'day': + return t('user_status', 'Today') + case 'week': + return t('user_status', 'This week') + + default: + return null + } + } + + if (clearAt.type === 'period') { + return moment.duration(clearAt.time * 1000).humanize() + } + + // This is not an officially supported type + // but only used internally to show the remaining time + // in the Set Status Modal + if (clearAt.type === '_time') { + const momentNow = moment(dateFactory()) + const momentClearAt = moment(clearAt.time, 'X') + + return moment.duration(momentNow.diff(momentClearAt)).humanize() + } + + return null +} + +export { + clearAtFilter, +} diff --git a/apps/user_status/src/main-user-status-menu.js b/apps/user_status/src/main-user-status-menu.js new file mode 100644 index 00000000000..795f41df4e7 --- /dev/null +++ b/apps/user_status/src/main-user-status-menu.js @@ -0,0 +1,23 @@ +import Vue from 'vue' +import { getRequestToken } from '@nextcloud/auth' +import App from './App' +import store from './store' + +// eslint-disable-next-line camelcase +__webpack_nonce__ = btoa(getRequestToken()) + +// Correct the root of the app for chunk loading +// OC.linkTo matches the apps folders +// OC.generateUrl ensure the index.php (or not) +// eslint-disable-next-line +__webpack_public_path__ = OC.linkTo('user_status', 'js/') + +Vue.prototype.t = t +Vue.prototype.$t = t + +const app = new Vue({ + render: h => h(App), + store, +}).$mount('li[data-id="user_status-menuitem"]') + +export { app } diff --git a/apps/user_status/src/services/clearAtOptionsService.js b/apps/user_status/src/services/clearAtOptionsService.js new file mode 100644 index 00000000000..83289f9059f --- /dev/null +++ b/apps/user_status/src/services/clearAtOptionsService.js @@ -0,0 +1,68 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { translate as t } from '@nextcloud/l10n' + +/** + * Returns an array + * + * @returns {Object[]} + */ +const getAllClearAtOptions = () => { + return [{ + label: t('user_status', 'Don\'t clear'), + clearAt: null, + }, { + label: t('user_status', '30 minutes'), + clearAt: { + type: 'period', + time: 1800, + }, + }, { + label: t('user_status', '1 hour'), + clearAt: { + type: 'period', + time: 3600, + }, + }, { + label: t('user_status', '4 hours'), + clearAt: { + type: 'period', + time: 14400, + }, + }, { + label: t('user_status', 'Today'), + clearAt: { + type: 'end-of', + time: 'day', + }, + }, { + label: t('user_status', 'This week'), + clearAt: { + type: 'end-of', + time: 'week', + }, + }] +} + +export { + getAllClearAtOptions, +} diff --git a/apps/user_status/src/services/clearAtService.js b/apps/user_status/src/services/clearAtService.js new file mode 100644 index 00000000000..12328d3b399 --- /dev/null +++ b/apps/user_status/src/services/clearAtService.js @@ -0,0 +1,63 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { + dateFactory, +} from './dateService' +import moment from '@nextcloud/moment' + +/** + * Calculates the actual clearAt timestamp + * + * @param {Object|null} clearAt The clear-at config + * @returns {Number|null} + */ +const getTimestampForClearAt = (clearAt) => { + if (clearAt === null) { + return null + } + + const date = dateFactory() + + if (clearAt.type === 'period') { + date.setSeconds(date.getSeconds() + clearAt.time) + return Math.floor(date.getTime() / 1000) + } + if (clearAt.type === 'end-of') { + switch (clearAt.time) { + case 'day': + case 'week': + return Number(moment(date).endOf(clearAt.time).format('X')) + } + } + // This is not an officially supported type + // but only used internally to show the remaining time + // in the Set Status Modal + if (clearAt.type === '_time') { + return clearAt.time + } + + return null +} + +export { + getTimestampForClearAt, +} diff --git a/apps/user_status/src/services/dateService.js b/apps/user_status/src/services/dateService.js new file mode 100644 index 00000000000..641244dada3 --- /dev/null +++ b/apps/user_status/src/services/dateService.js @@ -0,0 +1,34 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +/** + * Returns a new Date object + * + * @returns {Date} + */ +const dateFactory = () => { + return new Date() +} + +export { + dateFactory, +} diff --git a/apps/user_status/src/services/heartbeatService.js b/apps/user_status/src/services/heartbeatService.js new file mode 100644 index 00000000000..ca3a7de6d03 --- /dev/null +++ b/apps/user_status/src/services/heartbeatService.js @@ -0,0 +1,40 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import HttpClient from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' + +/** + * Sends a heartbeat + * + * @param {Boolean} isAway Whether or not the user is active + * @returns {Promise} + */ +const sendHeartbeat = async(isAway) => { + const url = generateUrl('/apps/user_status/heartbeat') + await HttpClient.put(url, { + status: isAway ? 'away' : 'online', + }) +} + +export { + sendHeartbeat, +} diff --git a/apps/user_status/src/services/predefinedStatusService.js b/apps/user_status/src/services/predefinedStatusService.js new file mode 100644 index 00000000000..116fccb0c56 --- /dev/null +++ b/apps/user_status/src/services/predefinedStatusService.js @@ -0,0 +1,39 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import HttpClient from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +/** + * Fetches all predefined statuses from the server + * + * @returns {Promise} + */ +const fetchAllPredefinedStatuses = async() => { + const url = generateOcsUrl('apps/user_status/api/v1', 2) + '/predefined_statuses?format=json' + const response = await HttpClient.get(url) + + return response.data.ocs.data +} + +export { + fetchAllPredefinedStatuses, +} diff --git a/apps/user_status/src/services/statusOptionsService.js b/apps/user_status/src/services/statusOptionsService.js new file mode 100644 index 00000000000..f429d6b189f --- /dev/null +++ b/apps/user_status/src/services/statusOptionsService.js @@ -0,0 +1,52 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { translate as t } from '@nextcloud/l10n' + +/** + * Returns a list of all user-definable statuses + * + * @returns {Object[]} + */ +const getAllStatusOptions = () => { + return [{ + type: 'online', + label: t('user_status', 'Online'), + icon: 'icon-user-status-online', + }, { + type: 'away', + label: t('user_status', 'Away'), + icon: 'icon-user-status-away', + }, { + type: 'dnd', + label: t('user_status', 'Do not disturb'), + icon: 'icon-user-status-dnd', + + }, { + type: 'invisible', + label: t('user_status', 'Invisible'), + icon: 'icon-user-status-invisible', + }] +} + +export { + getAllStatusOptions, +} diff --git a/apps/user_status/src/services/statusService.js b/apps/user_status/src/services/statusService.js new file mode 100644 index 00000000000..206ff4ee647 --- /dev/null +++ b/apps/user_status/src/services/statusService.js @@ -0,0 +1,98 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import HttpClient from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +/** + * Fetches the current user-status + * + * @returns {Promise} + */ +const fetchCurrentStatus = async() => { + const url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status' + const response = await HttpClient.get(url) + + return response.data.ocs.data +} + +/** + * Sets the status + * + * @param {String} statusType The status (online / away / dnd / invisible) + * @returns {Promise} + */ +const setStatus = async(statusType) => { + const url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status/status' + await HttpClient.put(url, { + statusType, + }) +} + +/** + * Sets a message based on our predefined statuses + * + * @param {String} messageId The id of the message, taken from predefined status service + * @param {Number|null} clearAt When to automatically clean the status + * @returns {Promise} + */ +const setPredefinedMessage = async(messageId, clearAt = null) => { + const url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status/message/predefined?format=json' + await HttpClient.put(url, { + messageId, + clearAt, + }) +} + +/** + * Sets a custom message + * + * @param {String} message The user-defined message + * @param {String|null} statusIcon The user-defined icon + * @param {Number|null} clearAt When to automatically clean the status + * @returns {Promise} + */ +const setCustomMessage = async(message, statusIcon = null, clearAt = null) => { + const url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status/message/custom?format=json' + await HttpClient.put(url, { + message, + statusIcon, + clearAt, + }) +} + +/** + * Clears the current status of the user + * + * @returns {Promise} + */ +const clearMessage = async() => { + const url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status/message?format=json' + await HttpClient.delete(url) +} + +export { + fetchCurrentStatus, + setStatus, + setCustomMessage, + setPredefinedMessage, + clearMessage, +} diff --git a/apps/user_status/src/store/index.js b/apps/user_status/src/store/index.js new file mode 100644 index 00000000000..d810cae5444 --- /dev/null +++ b/apps/user_status/src/store/index.js @@ -0,0 +1,35 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import Vue from 'vue' +import Vuex from 'vuex' +import predefinedStatuses from './predefinedStatuses' +import userStatus from './userStatus' + +Vue.use(Vuex) + +export default new Vuex.Store({ + modules: { + predefinedStatuses, + userStatus, + }, + strict: true, +}) diff --git a/apps/user_status/src/store/predefinedStatuses.js b/apps/user_status/src/store/predefinedStatuses.js new file mode 100644 index 00000000000..f7174bf8bfc --- /dev/null +++ b/apps/user_status/src/store/predefinedStatuses.js @@ -0,0 +1,64 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { fetchAllPredefinedStatuses } from '../services/predefinedStatusService' + +const state = { + predefinedStatuses: [], +} + +const mutations = { + + /** + * Adds a predefined status to the state + * + * @param {Object} state The Vuex state + * @param {Object} status The status to add + */ + addPredefinedStatus(state, status) { + state.predefinedStatuses.push(status) + }, +} + +const getters = {} + +const actions = { + + /** + * Loads all predefined statuses from the server + * + * @param {Object} vuex The Vuex components + * @param {Function} vuex.commit The Vuex commit function + */ + async loadAllPredefinedStatuses({ state, commit }) { + if (state.predefinedStatuses.length > 0) { + return + } + + const statuses = await fetchAllPredefinedStatuses() + for (const status of statuses) { + commit('addPredefinedStatus', status) + } + }, + +} + +export default { state, mutations, getters, actions } diff --git a/apps/user_status/src/store/userStatus.js b/apps/user_status/src/store/userStatus.js new file mode 100644 index 00000000000..ebe2aea2047 --- /dev/null +++ b/apps/user_status/src/store/userStatus.js @@ -0,0 +1,232 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { + fetchCurrentStatus, + setStatus, + setPredefinedMessage, + setCustomMessage, + clearMessage, +} from '../services/statusService' +import { loadState } from '@nextcloud/initial-state' +import { getTimestampForClearAt } from '../services/clearAtService' + +const state = { + // Status (online / away / dnd / invisible / offline) + status: null, + // Whether or not the status is user-defined + statusIsUserDefined: null, + // A custom message set by the user + message: null, + // The icon selected by the user + icon: null, + // When to automatically clean the status + clearAt: null, + // Whether or not the message is predefined + // (and can automatically be translated by Nextcloud) + messageIsPredefined: null, + // The id of the message in case it's predefined + messageId: null, +} + +const mutations = { + + /** + * Sets a new status + * + * @param {Object} state The Vuex state + * @param {Object} data The destructuring object + * @param {String} data.statusType The new status type + */ + setStatus(state, { statusType }) { + state.status = statusType + state.statusIsUserDefined = true + }, + + /** + * Sets a message using a predefined message + * + * @param {Object} state The Vuex state + * @param {Object} data The destructuring object + * @param {String} data.messageId The messageId + * @param {Number|null} data.clearAt When to automatically clear the status + * @param {String} data.message The message + * @param {String} data.icon The icon + */ + setPredefinedMessage(state, { messageId, clearAt, message, icon }) { + state.messageId = messageId + state.messageIsPredefined = true + + state.message = message + state.icon = icon + state.clearAt = clearAt + }, + + /** + * Sets a custom message + * + * @param {Object} state The Vuex state + * @param {Object} data The destructuring object + * @param {String} data.message The message + * @param {String} data.icon The icon + * @param {Number} data.clearAt When to automatically clear the status + */ + setCustomMessage(state, { message, icon, clearAt }) { + state.messageId = null + state.messageIsPredefined = false + + state.message = message + state.icon = icon + state.clearAt = clearAt + }, + + /** + * Clears the status + * + * @param {Object} state The Vuex state + */ + clearMessage(state) { + state.messageId = null + state.messageIsPredefined = false + + state.message = null + state.icon = null + state.clearAt = null + }, + + /** + * Loads the status from initial state + * + * @param {Object} state The Vuex state + * @param {Object} data The destructuring object + * @param {String} data.status The status type + * @param {Boolean} data.statusIsUserDefined Whether or not this status is user-defined + * @param {String} data.message The message + * @param {String} data.icon The icon + * @param {Number} data.clearAt When to automatically clear the status + * @param {Boolean} data.messageIsPredefined Whether or not the message is predefined + * @param {string} data.messageId The id of the predefined message + */ + loadStatusFromServer(state, { status, statusIsUserDefined, message, icon, clearAt, messageIsPredefined, messageId }) { + state.status = status + state.statusIsUserDefined = statusIsUserDefined + state.message = message + state.icon = icon + state.clearAt = clearAt + state.messageIsPredefined = messageIsPredefined + state.messageId = messageId + }, +} + +const getters = {} + +const actions = { + + /** + * Sets a new status + * + * @param {Object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @param {Object} data The data destructuring object + * @param {String} data.statusType The new status type + * @returns {Promise} + */ + async setStatus({ commit }, { statusType }) { + await setStatus(statusType) + commit('setStatus', { statusType }) + }, + + /** + * Sets a message using a predefined message + * + * @param {Object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @param {Object} vuex.rootState The Vuex root state + * @param {Object} data The data destructuring object + * @param {String} data.messageId The messageId + * @param {Object|null} data.clearAt When to automatically clear the status + * @returns {Promise} + */ + async setPredefinedMessage({ commit, rootState }, { messageId, clearAt }) { + const resolvedClearAt = getTimestampForClearAt(clearAt) + + await setPredefinedMessage(messageId, resolvedClearAt) + const status = rootState.predefinedStatuses.predefinedStatuses.find((status) => status.id === messageId) + const { message, icon } = status + + commit('setPredefinedMessage', { messageId, clearAt: resolvedClearAt, message, icon }) + }, + + /** + * Sets a custom message + * + * @param {Object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @param {Object} data The data destructuring object + * @param {String} data.message The message + * @param {String} data.icon The icon + * @param {Object|null} data.clearAt When to automatically clear the status + * @returns {Promise} + */ + async setCustomMessage({ commit }, { message, icon, clearAt }) { + const resolvedClearAt = getTimestampForClearAt(clearAt) + + await setCustomMessage(message, icon, resolvedClearAt) + commit('setCustomMessage', { message, icon, clearAt: resolvedClearAt }) + }, + + /** + * Clears the status + * + * @param {Object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @returns {Promise} + */ + async clearMessage({ commit }) { + await clearMessage() + commit('clearMessage') + }, + + /** + * Re-fetches the status from the server + * + * @param {Object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @returns {Promise} + */ + async reFetchStatusFromServer({ commit }) { + const status = await fetchCurrentStatus() + commit('loadStatusFromServer', status) + }, + + /** + * Loads the server from the initial state + * + * @param {Object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + */ + loadStatusFromInitialState({ commit }) { + const status = loadState('user_status', 'status') + commit('loadStatusFromServer', status) + }, +} + +export default { state, mutations, getters, actions } diff --git a/apps/user_status/tests/Unit/BackgroundJob/ClearOldStatusesBackgroundJobTest.php b/apps/user_status/tests/Unit/BackgroundJob/ClearOldStatusesBackgroundJobTest.php new file mode 100644 index 00000000000..6c5f15d47e9 --- /dev/null +++ b/apps/user_status/tests/Unit/BackgroundJob/ClearOldStatusesBackgroundJobTest.php @@ -0,0 +1,63 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Tests\BackgroundJob; + +use OCA\UserStatus\BackgroundJob\ClearOldStatusesBackgroundJob; +use OCA\UserStatus\Db\UserStatusMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use Test\TestCase; + +class ClearOldStatusesBackgroundJobTest extends TestCase { + + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $time; + + /** @var UserStatusMapper|\PHPUnit\Framework\MockObject\MockObject */ + private $mapper; + + /** @var ClearOldStatusesBackgroundJob */ + private $job; + + protected function setUp(): void { + parent::setUp(); + + $this->time = $this->createMock(ITimeFactory::class); + $this->mapper = $this->createMock(UserStatusMapper::class); + + $this->job = new ClearOldStatusesBackgroundJob($this->time, $this->mapper); + } + + public function testRun() { + $this->mapper->expects($this->once()) + ->method('clearOlderThan') + ->with(1337); + + $this->time->method('getTime') + ->willReturn(1337); + + self::invokePrivate($this->job, 'run', [[]]); + } +} diff --git a/apps/user_status/tests/Unit/CapabilitiesTest.php b/apps/user_status/tests/Unit/CapabilitiesTest.php new file mode 100644 index 00000000000..80f6765a694 --- /dev/null +++ b/apps/user_status/tests/Unit/CapabilitiesTest.php @@ -0,0 +1,71 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Tests; + +use OCA\UserStatus\Capabilities; +use OCA\UserStatus\Service\EmojiService; +use Test\TestCase; + +class CapabilitiesTest extends TestCase { + + /** @var EmojiService|\PHPUnit\Framework\MockObject\MockObject */ + private $emojiService; + + /** @var Capabilities */ + private $capabilities; + + protected function setUp(): void { + parent::setUp(); + + $this->emojiService = $this->createMock(EmojiService::class); + $this->capabilities = new Capabilities($this->emojiService); + } + + /** + * @param bool $supportsEmojis + * + * @dataProvider getCapabilitiesDataProvider + */ + public function testGetCapabilities(bool $supportsEmojis): void { + $this->emojiService->expects($this->once()) + ->method('doesPlatformSupportEmoji') + ->willReturn($supportsEmojis); + + $this->assertEquals([ + 'user_status' => [ + 'enabled' => true, + 'supports_emoji' => $supportsEmojis, + ] + ], $this->capabilities->getCapabilities()); + } + + public function getCapabilitiesDataProvider(): array { + return [ + [true], + [false], + ]; + } +} diff --git a/apps/user_status/tests/Unit/Controller/PredefinedStatusControllerTest.php b/apps/user_status/tests/Unit/Controller/PredefinedStatusControllerTest.php new file mode 100644 index 00000000000..44e3af5c0d2 --- /dev/null +++ b/apps/user_status/tests/Unit/Controller/PredefinedStatusControllerTest.php @@ -0,0 +1,74 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Tests\Controller; + +use OCA\UserStatus\Controller\PredefinedStatusController; +use OCA\UserStatus\Service\PredefinedStatusService; +use OCP\IRequest; +use Test\TestCase; + +class PredefinedStatusControllerTest extends TestCase { + + /** @var PredefinedStatusService|\PHPUnit\Framework\MockObject\MockObject */ + private $service; + + /** @var PredefinedStatusController */ + private $controller; + + protected function setUp(): void { + parent::setUp(); + + $request = $this->createMock(IRequest::class); + $this->service = $this->createMock(PredefinedStatusService::class); + + $this->controller = new PredefinedStatusController('user_status', $request, + $this->service); + } + + public function testFindAll() { + $this->service->expects($this->once()) + ->method('getDefaultStatuses') + ->with() + ->willReturn([ + [ + 'id' => 'predefined-status-one', + ], + [ + 'id' => 'predefined-status-two', + ], + ]); + + $actual = $this->controller->findAll(); + $this->assertEquals([ + [ + 'id' => 'predefined-status-one', + ], + [ + 'id' => 'predefined-status-two', + ], + ], $actual->getData()); + } +} diff --git a/apps/user_status/tests/Unit/Controller/StatusesControllerTest.php b/apps/user_status/tests/Unit/Controller/StatusesControllerTest.php new file mode 100644 index 00000000000..1dd6c0c4ae1 --- /dev/null +++ b/apps/user_status/tests/Unit/Controller/StatusesControllerTest.php @@ -0,0 +1,114 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Tests\Controller; + +use OCA\UserStatus\Controller\StatusesController; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\IRequest; +use Test\TestCase; + +class StatusesControllerTest extends TestCase { + + /** @var StatusService|\PHPUnit\Framework\MockObject\MockObject */ + private $service; + + /** @var StatusesController */ + private $controller; + + protected function setUp(): void { + parent::setUp(); + + $request = $this->createMock(IRequest::class); + $this->service = $this->createMock(StatusService::class); + + $this->controller = new StatusesController('user_status', $request, $this->service); + } + + public function testFindAll(): void { + $userStatus = $this->getUserStatus(); + + $this->service->expects($this->once()) + ->method('findAll') + ->with(20, 40) + ->willReturn([$userStatus]); + + $response = $this->controller->findAll(20, 40); + $this->assertEquals([[ + 'userId' => 'john.doe', + 'status' => 'offline', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + ]], $response->getData()); + } + + public function testFind(): void { + $userStatus = $this->getUserStatus(); + + $this->service->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($userStatus); + + $response = $this->controller->find('john.doe'); + $this->assertEquals([ + 'userId' => 'john.doe', + 'status' => 'offline', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + ], $response->getData()); + } + + public function testFindDoesNotExist(): void { + $this->service->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->expectException(OCSNotFoundException::class); + $this->expectExceptionMessage('No status for the requested userId'); + + $this->controller->find('john.doe'); + } + + private function getUserStatus(): UserStatus { + $userStatus = new UserStatus(); + $userStatus->setId(1337); + $userStatus->setUserId('john.doe'); + $userStatus->setStatus('invisible'); + $userStatus->setStatusTimestamp(5000); + $userStatus->setIsUserDefined(true); + $userStatus->setCustomIcon('🏝'); + $userStatus->setCustomMessage('On vacation'); + $userStatus->setClearAt(60000); + + return $userStatus; + } +} diff --git a/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php b/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php new file mode 100644 index 00000000000..5d1e15b0d3e --- /dev/null +++ b/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php @@ -0,0 +1,340 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Tests\Controller; + +use OCA\UserStatus\Controller\UserStatusController; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Exception\InvalidClearAtException; +use OCA\UserStatus\Exception\InvalidMessageIdException; +use OCA\UserStatus\Exception\InvalidStatusIconException; +use OCA\UserStatus\Exception\InvalidStatusTypeException; +use OCA\UserStatus\Exception\StatusMessageTooLongException; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\ILogger; +use OCP\IRequest; +use Test\TestCase; +use Throwable; + +class UserStatusControllerTest extends TestCase { + + /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */ + private $logger; + + /** @var StatusService|\PHPUnit\Framework\MockObject\MockObject */ + private $service; + + /** @var UserStatusController */ + private $controller; + + protected function setUp(): void { + parent::setUp(); + + $request = $this->createMock(IRequest::class); + $userId = 'john.doe'; + $this->logger = $this->createMock(ILogger::class); + $this->service = $this->createMock(StatusService::class); + + $this->controller = new UserStatusController('user_status', $request, $userId, $this->logger, $this->service); + } + + public function testGetStatus(): void { + $userStatus = $this->getUserStatus(); + + $this->service->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($userStatus); + + $response = $this->controller->getStatus(); + $this->assertEquals([ + 'userId' => 'john.doe', + 'status' => 'invisible', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + 'statusIsUserDefined' => true, + 'messageIsPredefined' => false, + 'messageId' => null, + ], $response->getData()); + } + + public function testGetStatusDoesNotExist(): void { + $this->service->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->expectException(OCSNotFoundException::class); + $this->expectExceptionMessage('No status for the current user'); + + $this->controller->getStatus(); + } + + /** + * @param string $statusType + * @param string|null $statusIcon + * @param string|null $message + * @param int|null $clearAt + * @param bool $expectSuccess + * @param bool $expectException + * @param Throwable|null $exception + * @param bool $expectLogger + * @param string|null $expectedLogMessage + * + * @dataProvider setStatusDataProvider + */ + public function testSetStatus(string $statusType, + ?string $statusIcon, + ?string $message, + ?int $clearAt, + bool $expectSuccess, + bool $expectException, + ?Throwable $exception, + bool $expectLogger, + ?string $expectedLogMessage): void { + $userStatus = $this->getUserStatus(); + + if ($expectException) { + $this->service->expects($this->once()) + ->method('setStatus') + ->with('john.doe', $statusType, null, true) + ->willThrowException($exception); + } else { + $this->service->expects($this->once()) + ->method('setStatus') + ->with('john.doe', $statusType, null, true) + ->willReturn($userStatus); + } + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('debug') + ->with($expectedLogMessage); + } + if ($expectException) { + $this->expectException(OCSBadRequestException::class); + $this->expectExceptionMessage('Original exception message'); + } + + $response = $this->controller->setStatus($statusType); + + if ($expectSuccess) { + $this->assertEquals([ + 'userId' => 'john.doe', + 'status' => 'invisible', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + 'statusIsUserDefined' => true, + 'messageIsPredefined' => false, + 'messageId' => null, + ], $response->getData()); + } + } + + public function setStatusDataProvider(): array { + return [ + ['busy', '👨🏽‍💻', 'Busy developing the status feature', 500, true, false, null, false, null], + ['busy', '👨🏽‍💻', 'Busy developing the status feature', 500, false, true, new InvalidStatusTypeException('Original exception message'), true, + 'New user-status for "john.doe" was rejected due to an invalid status type "busy"'], + ]; + } + + /** + * @param string $messageId + * @param int|null $clearAt + * @param bool $expectSuccess + * @param bool $expectException + * @param Throwable|null $exception + * @param bool $expectLogger + * @param string|null $expectedLogMessage + * + * @dataProvider setPredefinedMessageDataProvider + */ + public function testSetPredefinedMessage(string $messageId, + ?int $clearAt, + bool $expectSuccess, + bool $expectException, + ?Throwable $exception, + bool $expectLogger, + ?string $expectedLogMessage): void { + $userStatus = $this->getUserStatus(); + + if ($expectException) { + $this->service->expects($this->once()) + ->method('setPredefinedMessage') + ->with('john.doe', $messageId, $clearAt) + ->willThrowException($exception); + } else { + $this->service->expects($this->once()) + ->method('setPredefinedMessage') + ->with('john.doe', $messageId, $clearAt) + ->willReturn($userStatus); + } + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('debug') + ->with($expectedLogMessage); + } + if ($expectException) { + $this->expectException(OCSBadRequestException::class); + $this->expectExceptionMessage('Original exception message'); + } + + $response = $this->controller->setPredefinedMessage($messageId, $clearAt); + + if ($expectSuccess) { + $this->assertEquals([ + 'userId' => 'john.doe', + 'status' => 'invisible', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + 'statusIsUserDefined' => true, + 'messageIsPredefined' => false, + 'messageId' => null, + ], $response->getData()); + } + } + + public function setPredefinedMessageDataProvider(): array { + return [ + ['messageId-42', 500, true, false, null, false, null], + ['messageId-42', 500, false, true, new InvalidClearAtException('Original exception message'), true, + 'New user-status for "john.doe" was rejected due to an invalid clearAt value "500"'], + ['messageId-42', 500, false, true, new InvalidMessageIdException('Original exception message'), true, + 'New user-status for "john.doe" was rejected due to an invalid message-id "messageId-42"'], + ]; + } + + /** + * @param string|null $statusIcon + * @param string $message + * @param int|null $clearAt + * @param bool $expectSuccess + * @param bool $expectException + * @param Throwable|null $exception + * @param bool $expectLogger + * @param string|null $expectedLogMessage + * + * @dataProvider setCustomMessageDataProvider + */ + public function testSetCustomMessage(?string $statusIcon, + string $message, + ?int $clearAt, + bool $expectSuccess, + bool $expectException, + ?Throwable $exception, + bool $expectLogger, + ?string $expectedLogMessage): void { + $userStatus = $this->getUserStatus(); + + if ($expectException) { + $this->service->expects($this->once()) + ->method('setCustomMessage') + ->with('john.doe', $statusIcon, $message, $clearAt) + ->willThrowException($exception); + } else { + $this->service->expects($this->once()) + ->method('setCustomMessage') + ->with('john.doe', $statusIcon, $message, $clearAt) + ->willReturn($userStatus); + } + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('debug') + ->with($expectedLogMessage); + } + if ($expectException) { + $this->expectException(OCSBadRequestException::class); + $this->expectExceptionMessage('Original exception message'); + } + + $response = $this->controller->setCustomMessage($statusIcon, $message, $clearAt); + + if ($expectSuccess) { + $this->assertEquals([ + 'userId' => 'john.doe', + 'status' => 'invisible', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + 'statusIsUserDefined' => true, + 'messageIsPredefined' => false, + 'messageId' => null, + ], $response->getData()); + } + } + + public function setCustomMessageDataProvider(): array { + return [ + ['👨🏽‍💻', 'Busy developing the status feature', 500, true, false, null, false, null], + ['👨🏽‍💻', 'Busy developing the status feature', 500, false, true, new InvalidClearAtException('Original exception message'), true, + 'New user-status for "john.doe" was rejected due to an invalid clearAt value "500"'], + ['👨🏽‍💻', 'Busy developing the status feature', 500, false, true, new InvalidStatusIconException('Original exception message'), true, + 'New user-status for "john.doe" was rejected due to an invalid icon value "👨🏽‍💻"'], + ['👨🏽‍💻', 'Busy developing the status feature', 500, false, true, new StatusMessageTooLongException('Original exception message'), true, + 'New user-status for "john.doe" was rejected due to a too long status message.'], + ]; + } + + public function testClearStatus(): void { + $this->service->expects($this->once()) + ->method('clearStatus') + ->with('john.doe'); + + $response = $this->controller->clearStatus(); + $this->assertEquals([], $response->getData()); + } + + public function testClearMessage(): void { + $this->service->expects($this->once()) + ->method('clearMessage') + ->with('john.doe'); + + $response = $this->controller->clearMessage(); + $this->assertEquals([], $response->getData()); + } + + private function getUserStatus(): UserStatus { + $userStatus = new UserStatus(); + $userStatus->setId(1337); + $userStatus->setUserId('john.doe'); + $userStatus->setStatus('invisible'); + $userStatus->setStatusTimestamp(5000); + $userStatus->setIsUserDefined(true); + $userStatus->setCustomIcon('🏝'); + $userStatus->setCustomMessage('On vacation'); + $userStatus->setClearAt(60000); + + return $userStatus; + } +} diff --git a/apps/user_status/tests/Unit/Db/UserStatusMapperTest.php b/apps/user_status/tests/Unit/Db/UserStatusMapperTest.php new file mode 100644 index 00000000000..16699d2ae27 --- /dev/null +++ b/apps/user_status/tests/Unit/Db/UserStatusMapperTest.php @@ -0,0 +1,168 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Tests\Db; + +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Db\UserStatusMapper; +use Test\TestCase; + +class UserStatusMapperTest extends TestCase { + + /** @var UserStatusMapper */ + private $mapper; + + protected function setUp(): void { + parent::setUp(); + + // make sure that DB is empty + $qb = self::$realDatabase->getQueryBuilder(); + $qb->delete('user_status')->execute(); + + $this->mapper = new UserStatusMapper(self::$realDatabase); + } + + public function testGetTableName(): void { + $this->assertEquals('user_status', $this->mapper->getTableName()); + } + + public function testGetFindAll(): void { + $this->insertSampleStatuses(); + + $allResults = $this->mapper->findAll(); + $this->assertCount(3, $allResults); + + $limitedResults = $this->mapper->findAll(2); + $this->assertCount(2, $limitedResults); + $this->assertEquals('admin', $limitedResults[0]->getUserId()); + $this->assertEquals('user1', $limitedResults[1]->getUserId()); + + $offsetResults = $this->mapper->findAll(null, 2); + $this->assertCount(1, $offsetResults); + $this->assertEquals('user2', $offsetResults[0]->getUserId()); + } + + public function testGetFind(): void { + $this->insertSampleStatuses(); + + $adminStatus = $this->mapper->findByUserId('admin'); + $this->assertEquals('admin', $adminStatus->getUserId()); + $this->assertEquals('offline', $adminStatus->getStatus()); + $this->assertEquals(0, $adminStatus->getStatusTimestamp()); + $this->assertEquals(false, $adminStatus->getIsUserDefined()); + $this->assertEquals(null, $adminStatus->getCustomIcon()); + $this->assertEquals(null, $adminStatus->getCustomMessage()); + $this->assertEquals(null, $adminStatus->getClearAt()); + + $user1Status = $this->mapper->findByUserId('user1'); + $this->assertEquals('user1', $user1Status->getUserId()); + $this->assertEquals('dnd', $user1Status->getStatus()); + $this->assertEquals(5000, $user1Status->getStatusTimestamp()); + $this->assertEquals(true, $user1Status->getIsUserDefined()); + $this->assertEquals('💩', $user1Status->getCustomIcon()); + $this->assertEquals('Do not disturb', $user1Status->getCustomMessage()); + $this->assertEquals(50000, $user1Status->getClearAt()); + + $user2Status = $this->mapper->findByUserId('user2'); + $this->assertEquals('user2', $user2Status->getUserId()); + $this->assertEquals('away', $user2Status->getStatus()); + $this->assertEquals(5000, $user2Status->getStatusTimestamp()); + $this->assertEquals(false, $user2Status->getIsUserDefined()); + $this->assertEquals('🏝', $user2Status->getCustomIcon()); + $this->assertEquals('On vacation', $user2Status->getCustomMessage()); + $this->assertEquals(60000, $user2Status->getClearAt()); + } + + public function testUserIdUnique(): void { + // Test that inserting a second status for a user is throwing an exception + + $userStatus1 = new UserStatus(); + $userStatus1->setUserId('admin'); + $userStatus1->setStatus('dnd'); + $userStatus1->setStatusTimestamp(5000); + $userStatus1->setIsUserDefined(true); + + $this->mapper->insert($userStatus1); + + $userStatus2 = new UserStatus(); + $userStatus2->setUserId('admin'); + $userStatus2->setStatus('away'); + $userStatus2->setStatusTimestamp(6000); + $userStatus2->setIsUserDefined(false); + + $this->expectException(UniqueConstraintViolationException::class); + + $this->mapper->insert($userStatus2); + } + + public function testClearOlderThan(): void { + $this->insertSampleStatuses(); + + $this->mapper->clearOlderThan(55000); + + $allStatuses = $this->mapper->findAll(); + $this->assertCount(3, $allStatuses); + + $user1Status = $this->mapper->findByUserId('user1'); + $this->assertEquals('user1', $user1Status->getUserId()); + $this->assertEquals('dnd', $user1Status->getStatus()); + $this->assertEquals(5000, $user1Status->getStatusTimestamp()); + $this->assertEquals(true, $user1Status->getIsUserDefined()); + $this->assertEquals(null, $user1Status->getCustomIcon()); + $this->assertEquals(null, $user1Status->getCustomMessage()); + $this->assertEquals(null, $user1Status->getClearAt()); + } + + private function insertSampleStatuses(): void { + $userStatus1 = new UserStatus(); + $userStatus1->setUserId('admin'); + $userStatus1->setStatus('offline'); + $userStatus1->setStatusTimestamp(0); + $userStatus1->setIsUserDefined(false); + + $userStatus2 = new UserStatus(); + $userStatus2->setUserId('user1'); + $userStatus2->setStatus('dnd'); + $userStatus2->setStatusTimestamp(5000); + $userStatus2->setIsUserDefined(true); + $userStatus2->setCustomIcon('💩'); + $userStatus2->setCustomMessage('Do not disturb'); + $userStatus2->setClearAt(50000); + + $userStatus3 = new UserStatus(); + $userStatus3->setUserId('user2'); + $userStatus3->setStatus('away'); + $userStatus3->setStatusTimestamp(5000); + $userStatus3->setIsUserDefined(false); + $userStatus3->setCustomIcon('🏝'); + $userStatus3->setCustomMessage('On vacation'); + $userStatus3->setClearAt(60000); + + $this->mapper->insert($userStatus1); + $this->mapper->insert($userStatus2); + $this->mapper->insert($userStatus3); + } +} diff --git a/apps/user_status/tests/Unit/Listener/UserDeletedListenerTest.php b/apps/user_status/tests/Unit/Listener/UserDeletedListenerTest.php new file mode 100644 index 00000000000..6267bf1d185 --- /dev/null +++ b/apps/user_status/tests/Unit/Listener/UserDeletedListenerTest.php @@ -0,0 +1,71 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Tests\Listener; + +use OCA\UserStatus\Listener\UserDeletedListener; +use OCA\UserStatus\Service\StatusService; +use OCP\EventDispatcher\GenericEvent; +use OCP\IUser; +use OCP\User\Events\UserDeletedEvent; +use Test\TestCase; + +class UserDeletedListenerTest extends TestCase { + + /** @var StatusService|\PHPUnit\Framework\MockObject\MockObject */ + private $service; + + /** @var UserDeletedListener */ + private $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->service = $this->createMock(StatusService::class); + $this->listener = new UserDeletedListener($this->service); + } + + public function testHandleWithCorrectEvent(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('john.doe'); + + $this->service->expects($this->once()) + ->method('removeUserStatus') + ->with('john.doe'); + + $event = new UserDeletedEvent($user); + $this->listener->handle($event); + } + + public function testHandleWithWrongEvent(): void { + $this->service->expects($this->never()) + ->method('removeUserStatus'); + + $event = new GenericEvent(); + $this->listener->handle($event); + } +} diff --git a/apps/user_status/tests/Unit/Listener/UserLiveStatusListenerTest.php b/apps/user_status/tests/Unit/Listener/UserLiveStatusListenerTest.php new file mode 100644 index 00000000000..29f444ece0b --- /dev/null +++ b/apps/user_status/tests/Unit/Listener/UserLiveStatusListenerTest.php @@ -0,0 +1,162 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Tests\Listener; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Db\UserStatusMapper; +use OCA\UserStatus\Listener\UserDeletedListener; +use OCA\UserStatus\Listener\UserLiveStatusListener; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\GenericEvent; +use OCP\IUser; +use OCP\User\Events\UserLiveStatusEvent; +use Test\TestCase; + +class UserLiveStatusListenerTest extends TestCase { + + /** @var UserStatusMapper|\PHPUnit\Framework\MockObject\MockObject */ + private $mapper; + + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $timeFactory; + + /** @var UserDeletedListener */ + private $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->mapper = $this->createMock(UserStatusMapper::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->listener = new UserLiveStatusListener($this->mapper, $this->timeFactory); + } + + /** + * @param string $userId + * @param string $previousStatus + * @param int $previousTimestamp + * @param bool $previousIsUserDefined + * @param string $eventStatus + * @param int $eventTimestamp + * @param bool $expectExisting + * @param bool $expectUpdate + * + * @dataProvider handleEventWithCorrectEventDataProvider + */ + public function testHandleWithCorrectEvent(string $userId, + string $previousStatus, + int $previousTimestamp, + bool $previousIsUserDefined, + string $eventStatus, + int $eventTimestamp, + bool $expectExisting, + bool $expectUpdate): void { + $userStatus = new UserStatus(); + + if ($expectExisting) { + $userStatus->setId(42); + $userStatus->setUserId($userId); + $userStatus->setStatus($previousStatus); + $userStatus->setStatusTimestamp($previousTimestamp); + $userStatus->setIsUserDefined($previousIsUserDefined); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willReturn($userStatus); + } else { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willThrowException(new DoesNotExistException('')); + } + + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn($userId); + $event = new UserLiveStatusEvent($user, $eventStatus, $eventTimestamp); + + $this->timeFactory->expects($this->once()) + ->method('getTime') + ->willReturn(5000); + + if ($expectUpdate) { + if ($expectExisting) { + $this->mapper->expects($this->never()) + ->method('insert'); + $this->mapper->expects($this->once()) + ->method('update') + ->with($this->callback(function ($userStatus) use ($eventStatus, $eventTimestamp) { + $this->assertEquals($eventStatus, $userStatus->getStatus()); + $this->assertEquals($eventTimestamp, $userStatus->getStatusTimestamp()); + $this->assertFalse($userStatus->getIsUserDefined()); + + return true; + })); + } else { + $this->mapper->expects($this->once()) + ->method('insert') + ->with($this->callback(function ($userStatus) use ($eventStatus, $eventTimestamp) { + $this->assertEquals($eventStatus, $userStatus->getStatus()); + $this->assertEquals($eventTimestamp, $userStatus->getStatusTimestamp()); + $this->assertFalse($userStatus->getIsUserDefined()); + + return true; + })); + $this->mapper->expects($this->never()) + ->method('update'); + } + + $this->listener->handle($event); + } else { + $this->mapper->expects($this->never()) + ->method('insert'); + $this->mapper->expects($this->never()) + ->method('update'); + + $this->listener->handle($event); + } + } + + public function handleEventWithCorrectEventDataProvider(): array { + return [ + ['john.doe', 'offline', 0, false, 'online', 5000, true, true], + ['john.doe', 'offline', 0, false, 'online', 5000, false, true], + ['john.doe', 'online', 5000, false, 'online', 5000, true, false], + ['john.doe', 'online', 5000, false, 'online', 5000, false, true], + ['john.doe', 'away', 5000, false, 'online', 5000, true, true], + ['john.doe', 'online', 5000, false, 'away', 5000, true, false], + ]; + } + + public function testHandleWithWrongEvent(): void { + $this->mapper->expects($this->never()) + ->method('insertOrUpdate'); + + $event = new GenericEvent(); + $this->listener->handle($event); + } +} diff --git a/apps/user_status/tests/Unit/Service/EmojiServiceTest.php b/apps/user_status/tests/Unit/Service/EmojiServiceTest.php new file mode 100644 index 00000000000..e622a7eabcd --- /dev/null +++ b/apps/user_status/tests/Unit/Service/EmojiServiceTest.php @@ -0,0 +1,100 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Tests\Service; + +use OCA\UserStatus\Service\EmojiService; +use OCP\IDBConnection; +use Test\TestCase; + +class EmojiServiceTest extends TestCase { + + /** @var IDBConnection|\PHPUnit\Framework\MockObject\MockObject */ + private $db; + + /** @var EmojiService */ + private $service; + + protected function setUp(): void { + parent::setUp(); + + $this->db = $this->createMock(IDBConnection::class); + $this->service = new EmojiService($this->db); + } + + /** + * @param bool $supports4ByteText + * @param bool $expected + * + * @dataProvider doesPlatformSupportEmojiDataProvider + */ + public function testDoesPlatformSupportEmoji(bool $supports4ByteText, bool $expected): void { + $this->db->expects($this->once()) + ->method('supports4ByteText') + ->willReturn($supports4ByteText); + + $this->assertEquals($expected, $this->service->doesPlatformSupportEmoji()); + } + + /** + * @return array + */ + public function doesPlatformSupportEmojiDataProvider(): array { + return [ + [true, true], + [false, false], + ]; + } + + /** + * @param string $emoji + * @param bool $expected + * + * @dataProvider isValidEmojiDataProvider + */ + public function testIsValidEmoji(string $emoji, bool $expected): void { + $actual = $this->service->isValidEmoji($emoji); + + $this->assertEquals($expected, $actual); + } + + public function isValidEmojiDataProvider(): array { + return [ + ['🏝', true], + ['📱', true], + ['🏢', true], + ['📱📠', false], + ['a', false], + ['0', false], + ['$', false], + // Test some more complex emojis with modifiers and zero-width-joiner + ['👩🏿‍💻', true], + ['🤷🏼‍♀️', true], + ['🏳️‍🌈', true], + ['👨‍👨‍👦‍👦', true], + ['👩‍❤️‍👩', true] + ]; + } +} diff --git a/apps/user_status/tests/Unit/Service/PredefinedStatusServiceTest.php b/apps/user_status/tests/Unit/Service/PredefinedStatusServiceTest.php new file mode 100644 index 00000000000..2f58b5b1df8 --- /dev/null +++ b/apps/user_status/tests/Unit/Service/PredefinedStatusServiceTest.php @@ -0,0 +1,184 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Tests\Service; + +use OCA\UserStatus\Service\PredefinedStatusService; +use OCP\IL10N; +use Test\TestCase; + +class PredefinedStatusServiceTest extends TestCase { + + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + protected $l10n; + + /** @var PredefinedStatusService */ + protected $service; + + protected function setUp(): void { + parent::setUp(); + + $this->l10n = $this->createMock(IL10N::class); + + $this->service = new PredefinedStatusService($this->l10n); + } + + public function testGetDefaultStatuses(): void { + $this->l10n->expects($this->exactly(5)) + ->method('t') + ->withConsecutive( + ['In a meeting'], + ['Commuting'], + ['Working remotely'], + ['Out sick'], + ['Vacationing'] + ) + ->willReturnArgument(0); + + $actual = $this->service->getDefaultStatuses(); + $this->assertEquals([ + [ + 'id' => 'meeting', + 'icon' => '📅', + 'message' => 'In a meeting', + 'clearAt' => [ + 'type' => 'period', + 'time' => 3600, + ], + ], + [ + 'id' => 'commuting', + 'icon' => '🚌', + 'message' => 'Commuting', + 'clearAt' => [ + 'type' => 'period', + 'time' => 1800, + ], + ], + [ + 'id' => 'remote-work', + 'icon' => '🏡', + 'message' => 'Working remotely', + 'clearAt' => [ + 'type' => 'end-of', + 'time' => 'day', + ], + ], + [ + 'id' => 'sick-leave', + 'icon' => '🤒', + 'message' => 'Out sick', + 'clearAt' => [ + 'type' => 'end-of', + 'time' => 'day', + ], + ], + [ + 'id' => 'vacationing', + 'icon' => '🌴', + 'message' => 'Vacationing', + 'clearAt' => null, + ], + ], $actual); + } + + /** + * @param string $id + * @param string|null $expectedIcon + * + * @dataProvider getIconForIdDataProvider + */ + public function testGetIconForId(string $id, ?string $expectedIcon): void { + $actual = $this->service->getIconForId($id); + $this->assertEquals($expectedIcon, $actual); + } + + /** + * @return array + */ + public function getIconForIdDataProvider(): array { + return [ + ['meeting', '📅'], + ['commuting', '🚌'], + ['sick-leave', '🤒'], + ['vacationing', '🌴'], + ['remote-work', '🏡'], + ['unknown-id', null], + ]; + } + + /** + * @param string $id + * @param string|null $expected + * + * @dataProvider getTranslatedStatusForIdDataProvider + */ + public function testGetTranslatedStatusForId(string $id, ?string $expected): void { + $this->l10n->method('t') + ->willReturnArgument(0); + + $actual = $this->service->getTranslatedStatusForId($id); + $this->assertEquals($expected, $actual); + } + + /** + * @return array + */ + public function getTranslatedStatusForIdDataProvider(): array { + return [ + ['meeting', 'In a meeting'], + ['commuting', 'Commuting'], + ['sick-leave', 'Out sick'], + ['vacationing', 'Vacationing'], + ['remote-work', 'Working remotely'], + ['unknown-id', null], + ]; + } + + /** + * @param string $id + * @param bool $expected + * + * @dataProvider isValidIdDataProvider + */ + public function testIsValidId(string $id, bool $expected): void { + $actual = $this->service->isValidId($id); + $this->assertEquals($expected, $actual); + } + + /** + * @return array + */ + public function isValidIdDataProvider(): array { + return [ + ['meeting', true], + ['commuting', true], + ['sick-leave', true], + ['vacationing', true], + ['remote-work', true], + ['unknown-id', false], + ]; + } +} diff --git a/apps/user_status/tests/Unit/Service/StatusServiceTest.php b/apps/user_status/tests/Unit/Service/StatusServiceTest.php new file mode 100644 index 00000000000..647c1089db8 --- /dev/null +++ b/apps/user_status/tests/Unit/Service/StatusServiceTest.php @@ -0,0 +1,592 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserStatus\Tests\Service; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Db\UserStatusMapper; +use OCA\UserStatus\Exception\InvalidClearAtException; +use OCA\UserStatus\Exception\InvalidMessageIdException; +use OCA\UserStatus\Exception\InvalidStatusIconException; +use OCA\UserStatus\Exception\InvalidStatusTypeException; +use OCA\UserStatus\Exception\StatusMessageTooLongException; +use OCA\UserStatus\Service\EmojiService; +use OCA\UserStatus\Service\PredefinedStatusService; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use Test\TestCase; + +class StatusServiceTest extends TestCase { + + /** @var UserStatusMapper|\PHPUnit\Framework\MockObject\MockObject */ + private $mapper; + + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $timeFactory; + + /** @var PredefinedStatusService|\PHPUnit\Framework\MockObject\MockObject */ + private $predefinedStatusService; + + /** @var EmojiService|\PHPUnit\Framework\MockObject\MockObject */ + private $emojiService; + + /** @var StatusService */ + private $service; + + protected function setUp(): void { + parent::setUp(); + + $this->mapper = $this->createMock(UserStatusMapper::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->predefinedStatusService = $this->createMock(PredefinedStatusService::class); + $this->emojiService = $this->createMock(EmojiService::class); + $this->service = new StatusService($this->mapper, + $this->timeFactory, + $this->predefinedStatusService, + $this->emojiService); + } + + public function testFindAll(): void { + $status1 = $this->createMock(UserStatus::class); + $status2 = $this->createMock(UserStatus::class); + + $this->mapper->expects($this->once()) + ->method('findAll') + ->with(20, 50) + ->willReturn([$status1, $status2]); + + $this->assertEquals([ + $status1, + $status2, + ], $this->service->findAll(20, 50)); + } + + public function testFindByUserId(): void { + $status = $this->createMock(UserStatus::class); + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + + $this->assertEquals($status, $this->service->findByUserId('john.doe')); + } + + public function testFindByUserIdDoesNotExist(): void { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->expectException(DoesNotExistException::class); + $this->service->findByUserId('john.doe'); + } + + public function testFindAllAddDefaultMessage(): void { + $status = new UserStatus(); + $status->setMessageId('commuting'); + + $this->predefinedStatusService->expects($this->once()) + ->method('getDefaultStatusById') + ->with('commuting') + ->willReturn([ + 'id' => 'commuting', + 'icon' => '🚌', + 'message' => 'Commuting', + 'clearAt' => [ + 'type' => 'period', + 'time' => 1800, + ], + ]); + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + + $this->assertEquals($status, $this->service->findByUserId('john.doe')); + $this->assertEquals('🚌', $status->getCustomIcon()); + $this->assertEquals('Commuting', $status->getCustomMessage()); + } + + public function testFindAllClearStatus(): void { + $status = new UserStatus(); + $status->setClearAt(50); + $status->setMessageId('commuting'); + + $this->timeFactory->expects($this->once()) + ->method('getTime') + ->willReturn(60); + $this->predefinedStatusService->expects($this->never()) + ->method('getDefaultStatusById'); + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + $this->assertEquals($status, $this->service->findByUserId('john.doe')); + $this->assertNull($status->getClearAt()); + $this->assertNull($status->getMessageId()); + } + + /** + * @param string $userId + * @param string $status + * @param int|null $statusTimestamp + * @param bool $isUserDefined + * @param bool $expectExisting + * @param bool $expectSuccess + * @param bool $expectTimeFactory + * @param bool $expectException + * @param string|null $expectedExceptionClass + * @param string|null $expectedExceptionMessage + * + * @dataProvider setStatusDataProvider + */ + public function testSetStatus(string $userId, + string $status, + ?int $statusTimestamp, + bool $isUserDefined, + bool $expectExisting, + bool $expectSuccess, + bool $expectTimeFactory, + bool $expectException, + ?string $expectedExceptionClass, + ?string $expectedExceptionMessage): void { + $userStatus = new UserStatus(); + + if ($expectExisting) { + $userStatus->setId(42); + $userStatus->setUserId($userId); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willReturn($userStatus); + } else { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willThrowException(new DoesNotExistException('')); + } + + if ($expectTimeFactory) { + $this->timeFactory + ->method('getTime') + ->willReturn(40); + } + + if ($expectException) { + $this->expectException($expectedExceptionClass); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->service->setStatus($userId, $status, $statusTimestamp, $isUserDefined); + } + + if ($expectSuccess) { + if ($expectExisting) { + $this->mapper->expects($this->once()) + ->method('update') + ->willReturnArgument(0); + } else { + $this->mapper->expects($this->once()) + ->method('insert') + ->willReturnArgument(0); + } + + $actual = $this->service->setStatus($userId, $status, $statusTimestamp, $isUserDefined); + + $this->assertEquals('john.doe', $actual->getUserId()); + $this->assertEquals($status, $actual->getStatus()); + $this->assertEquals($statusTimestamp ?? 40, $actual->getStatusTimestamp()); + $this->assertEquals($isUserDefined, $actual->getIsUserDefined()); + } + } + + public function setStatusDataProvider(): array { + return [ + ['john.doe', 'online', 50, true, true, true, false, false, null, null], + ['john.doe', 'online', 50, true, false, true, false, false, null, null], + ['john.doe', 'online', 50, false, true, true, false, false, null, null], + ['john.doe', 'online', 50, false, false, true, false, false, null, null], + ['john.doe', 'online', null, true, true, true, true, false, null, null], + ['john.doe', 'online', null, true, false, true, true, false, null, null], + ['john.doe', 'online', null, false, true, true, true, false, null, null], + ['john.doe', 'online', null, false, false, true, true, false, null, null], + + ['john.doe', 'away', 50, true, true, true, false, false, null, null], + ['john.doe', 'away', 50, true, false, true, false, false, null, null], + ['john.doe', 'away', 50, false, true, true, false, false, null, null], + ['john.doe', 'away', 50, false, false, true, false, false, null, null], + ['john.doe', 'away', null, true, true, true, true, false, null, null], + ['john.doe', 'away', null, true, false, true, true, false, null, null], + ['john.doe', 'away', null, false, true, true, true, false, null, null], + ['john.doe', 'away', null, false, false, true, true, false, null, null], + + ['john.doe', 'dnd', 50, true, true, true, false, false, null, null], + ['john.doe', 'dnd', 50, true, false, true, false, false, null, null], + ['john.doe', 'dnd', 50, false, true, true, false, false, null, null], + ['john.doe', 'dnd', 50, false, false, true, false, false, null, null], + ['john.doe', 'dnd', null, true, true, true, true, false, null, null], + ['john.doe', 'dnd', null, true, false, true, true, false, null, null], + ['john.doe', 'dnd', null, false, true, true, true, false, null, null], + ['john.doe', 'dnd', null, false, false, true, true, false, null, null], + + ['john.doe', 'invisible', 50, true, true, true, false, false, null, null], + ['john.doe', 'invisible', 50, true, false, true, false, false, null, null], + ['john.doe', 'invisible', 50, false, true, true, false, false, null, null], + ['john.doe', 'invisible', 50, false, false, true, false, false, null, null], + ['john.doe', 'invisible', null, true, true, true, true, false, null, null], + ['john.doe', 'invisible', null, true, false, true, true, false, null, null], + ['john.doe', 'invisible', null, false, true, true, true, false, null, null], + ['john.doe', 'invisible', null, false, false, true, true, false, null, null], + + ['john.doe', 'offline', 50, true, true, true, false, false, null, null], + ['john.doe', 'offline', 50, true, false, true, false, false, null, null], + ['john.doe', 'offline', 50, false, true, true, false, false, null, null], + ['john.doe', 'offline', 50, false, false, true, false, false, null, null], + ['john.doe', 'offline', null, true, true, true, true, false, null, null], + ['john.doe', 'offline', null, true, false, true, true, false, null, null], + ['john.doe', 'offline', null, false, true, true, true, false, null, null], + ['john.doe', 'offline', null, false, false, true, true, false, null, null], + + ['john.doe', 'illegal-status', 50, true, true, false, false, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', 50, true, false, false, false, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', 50, false, true, false, false, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', 50, false, false, false, false, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', null, true, true, false, true, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', null, true, false, false, true, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', null, false, true, false, true, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', null, false, false, false, true, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ]; + } + + /** + * @param string $userId + * @param string $messageId + * @param bool $isValidMessageId + * @param int|null $clearAt + * @param bool $expectExisting + * @param bool $expectSuccess + * @param bool $expectException + * @param string|null $expectedExceptionClass + * @param string|null $expectedExceptionMessage + * + * @dataProvider setPredefinedMessageDataProvider + */ + public function testSetPredefinedMessage(string $userId, + string $messageId, + bool $isValidMessageId, + ?int $clearAt, + bool $expectExisting, + bool $expectSuccess, + bool $expectException, + ?string $expectedExceptionClass, + ?string $expectedExceptionMessage): void { + $userStatus = new UserStatus(); + + if ($expectExisting) { + $userStatus->setId(42); + $userStatus->setUserId($userId); + $userStatus->setStatus('offline'); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + $userStatus->setCustomIcon('😀'); + $userStatus->setCustomMessage('Foo'); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willReturn($userStatus); + } else { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willThrowException(new DoesNotExistException('')); + } + + $this->predefinedStatusService->expects($this->once()) + ->method('isValidId') + ->with($messageId) + ->willReturn($isValidMessageId); + + $this->timeFactory + ->method('getTime') + ->willReturn(40); + + if ($expectException) { + $this->expectException($expectedExceptionClass); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->service->setPredefinedMessage($userId, $messageId, $clearAt); + } + + if ($expectSuccess) { + if ($expectExisting) { + $this->mapper->expects($this->once()) + ->method('update') + ->willReturnArgument(0); + } else { + $this->mapper->expects($this->once()) + ->method('insert') + ->willReturnArgument(0); + } + + $actual = $this->service->setPredefinedMessage($userId, $messageId, $clearAt); + + $this->assertEquals('john.doe', $actual->getUserId()); + $this->assertEquals('offline', $actual->getStatus()); + $this->assertEquals(0, $actual->getStatusTimestamp()); + $this->assertEquals(false, $actual->getIsUserDefined()); + $this->assertEquals($messageId, $actual->getMessageId()); + $this->assertNull($actual->getCustomIcon()); + $this->assertNull($actual->getCustomMessage()); + $this->assertEquals($clearAt, $actual->getClearAt()); + } + } + + public function setPredefinedMessageDataProvider(): array { + return [ + ['john.doe', 'sick-leave', true, null, true, true, false, null, null], + ['john.doe', 'sick-leave', true, null, false, true, false, null, null], + ['john.doe', 'sick-leave', true, 20, true, false, true, InvalidClearAtException::class, 'ClearAt is in the past'], + ['john.doe', 'sick-leave', true, 20, false, false, true, InvalidClearAtException::class, 'ClearAt is in the past'], + ['john.doe', 'sick-leave', true, 60, true, true, false, null, null], + ['john.doe', 'sick-leave', true, 60, false, true, false, null, null], + ['john.doe', 'illegal-message-id', false, null, true, false, true, InvalidMessageIdException::class, 'Message-Id "illegal-message-id" is not supported'], + ['john.doe', 'illegal-message-id', false, null, false, false, true, InvalidMessageIdException::class, 'Message-Id "illegal-message-id" is not supported'], + ]; + } + + /** + * @param string $userId + * @param string|null $statusIcon + * @param bool $supportsEmoji + * @param string $message + * @param int|null $clearAt + * @param bool $expectExisting + * @param bool $expectSuccess + * @param bool $expectException + * @param string|null $expectedExceptionClass + * @param string|null $expectedExceptionMessage + * + * @dataProvider setCustomMessageDataProvider + */ + public function testSetCustomMessage(string $userId, + ?string $statusIcon, + bool $supportsEmoji, + string $message, + ?int $clearAt, + bool $expectExisting, + bool $expectSuccess, + bool $expectException, + ?string $expectedExceptionClass, + ?string $expectedExceptionMessage): void { + $userStatus = new UserStatus(); + + if ($expectExisting) { + $userStatus->setId(42); + $userStatus->setUserId($userId); + $userStatus->setStatus('offline'); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + $userStatus->setMessageId('messageId-42'); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willReturn($userStatus); + } else { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willThrowException(new DoesNotExistException('')); + } + + $this->emojiService->method('isValidEmoji') + ->with($statusIcon) + ->willReturn($supportsEmoji); + + $this->timeFactory + ->method('getTime') + ->willReturn(40); + + if ($expectException) { + $this->expectException($expectedExceptionClass); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->service->setCustomMessage($userId, $statusIcon, $message, $clearAt); + } + + if ($expectSuccess) { + if ($expectExisting) { + $this->mapper->expects($this->once()) + ->method('update') + ->willReturnArgument(0); + } else { + $this->mapper->expects($this->once()) + ->method('insert') + ->willReturnArgument(0); + } + + $actual = $this->service->setCustomMessage($userId, $statusIcon, $message, $clearAt); + + $this->assertEquals('john.doe', $actual->getUserId()); + $this->assertEquals('offline', $actual->getStatus()); + $this->assertEquals(0, $actual->getStatusTimestamp()); + $this->assertEquals(false, $actual->getIsUserDefined()); + $this->assertNull($actual->getMessageId()); + $this->assertEquals($statusIcon, $actual->getCustomIcon()); + $this->assertEquals($message, $actual->getCustomMessage()); + $this->assertEquals($clearAt, $actual->getClearAt()); + } + } + + public function setCustomMessageDataProvider(): array { + return [ + ['john.doe', '😁', true, 'Custom message', null, true, true, false, null, null], + ['john.doe', '😁', true, 'Custom message', null, false, true, false, null, null], + ['john.doe', null, false, 'Custom message', null, true, true, false, null, null], + ['john.doe', null, false, 'Custom message', null, false, true, false, null, null], + ['john.doe', '😁', false, 'Custom message', null, true, false, true, InvalidStatusIconException::class, 'Status-Icon is longer than one character'], + ['john.doe', '😁', false, 'Custom message', null, false, false, true, InvalidStatusIconException::class, 'Status-Icon is longer than one character'], + ['john.doe', null, false, 'Custom message that is way too long and violates the maximum length and hence should be rejected', null, true, false, true, StatusMessageTooLongException::class, 'Message is longer than supported length of 80 characters'], + ['john.doe', null, false, 'Custom message that is way too long and violates the maximum length and hence should be rejected', null, false, false, true, StatusMessageTooLongException::class, 'Message is longer than supported length of 80 characters'], + ['john.doe', '😁', true, 'Custom message', 80, true, true, false, null, null], + ['john.doe', '😁', true, 'Custom message', 80, false, true, false, null, null], + ['john.doe', '😁', true, 'Custom message', 20, true, false, true, InvalidClearAtException::class, 'ClearAt is in the past'], + ['john.doe', '😁', true, 'Custom message', 20, false, false, true, InvalidClearAtException::class, 'ClearAt is in the past'], + ]; + } + + public function testClearStatus(): void { + $status = new UserStatus(); + $status->setId(1); + $status->setUserId('john.doe'); + $status->setStatus('dnd'); + $status->setStatusTimestamp(1337); + $status->setIsUserDefined(true); + $status->setMessageId('messageId-42'); + $status->setCustomIcon('🙊'); + $status->setCustomMessage('My custom status message'); + $status->setClearAt(42); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + + $this->mapper->expects($this->once()) + ->method('update') + ->with($status); + + $actual = $this->service->clearStatus('john.doe'); + $this->assertTrue($actual); + $this->assertEquals('offline', $status->getStatus()); + $this->assertEquals(0, $status->getStatusTimestamp()); + $this->assertFalse($status->getIsUserDefined()); + } + + public function testClearStatusDoesNotExist(): void { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->mapper->expects($this->never()) + ->method('update'); + + $actual = $this->service->clearStatus('john.doe'); + $this->assertFalse($actual); + } + + public function testClearMessage(): void { + $status = new UserStatus(); + $status->setId(1); + $status->setUserId('john.doe'); + $status->setStatus('dnd'); + $status->setStatusTimestamp(1337); + $status->setIsUserDefined(true); + $status->setMessageId('messageId-42'); + $status->setCustomIcon('🙊'); + $status->setCustomMessage('My custom status message'); + $status->setClearAt(42); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + + $this->mapper->expects($this->once()) + ->method('update') + ->with($status); + + $actual = $this->service->clearMessage('john.doe'); + $this->assertTrue($actual); + $this->assertNull($status->getMessageId()); + $this->assertNull($status->getCustomMessage()); + $this->assertNull($status->getCustomIcon()); + $this->assertNull($status->getClearAt()); + } + + public function testClearMessageDoesNotExist(): void { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->mapper->expects($this->never()) + ->method('update'); + + $actual = $this->service->clearMessage('john.doe'); + $this->assertFalse($actual); + } + + public function testRemoveUserStatus(): void { + $status = $this->createMock(UserStatus::class); + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + + $this->mapper->expects($this->once()) + ->method('delete') + ->with($status); + + $actual = $this->service->removeUserStatus('john.doe'); + $this->assertTrue($actual); + } + + public function testRemoveUserStatusDoesNotExist(): void { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->mapper->expects($this->never()) + ->method('delete'); + + $actual = $this->service->removeUserStatus('john.doe'); + $this->assertFalse($actual); + } +} diff --git a/apps/user_status/tests/bootstrap.php b/apps/user_status/tests/bootstrap.php new file mode 100644 index 00000000000..5ef5008fed2 --- /dev/null +++ b/apps/user_status/tests/bootstrap.php @@ -0,0 +1,36 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +if (!defined('PHPUNIT_RUN')) { + define('PHPUNIT_RUN', 1); +} + +require_once __DIR__.'/../../../lib/base.php'; + +\OC::$composerAutoloader->addPsr4('Test\\', OC::$SERVERROOT . '/tests/lib/', true); + +\OC_App::loadApp('user_status'); + +OC_Hook::clear(); diff --git a/apps/user_status/webpack.js b/apps/user_status/webpack.js new file mode 100644 index 00000000000..b3a9b4f312d --- /dev/null +++ b/apps/user_status/webpack.js @@ -0,0 +1,18 @@ +const path = require('path') + +module.exports = { + entry: { + 'user-status-menu': path.join(__dirname, 'src', 'main-user-status-menu') + }, + output: { + path: path.resolve(__dirname, './js'), + publicPath: '/js/', + filename: '[name].js?v=[chunkhash]', + jsonpFunction: 'webpackJsonpUserStatus' + }, + optimization: { + splitChunks: { + automaticNameDelimiter: '-', + } + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index ae8d6349dc2..854ef66f23a 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -519,6 +519,7 @@ return array( 'OCP\\User\\Events\\UserChangedEvent' => $baseDir . '/lib/public/User/Events/UserChangedEvent.php', 'OCP\\User\\Events\\UserCreatedEvent' => $baseDir . '/lib/public/User/Events/UserCreatedEvent.php', 'OCP\\User\\Events\\UserDeletedEvent' => $baseDir . '/lib/public/User/Events/UserDeletedEvent.php', + 'OCP\\User\\Events\\UserLiveStatusEvent' => $baseDir . '/lib/public/User/Events/UserLiveStatusEvent.php', 'OCP\\User\\Events\\UserLoggedInEvent' => $baseDir . '/lib/public/User/Events/UserLoggedInEvent.php', 'OCP\\User\\Events\\UserLoggedInWithCookieEvent' => $baseDir . '/lib/public/User/Events/UserLoggedInWithCookieEvent.php', 'OCP\\User\\Events\\UserLoggedOutEvent' => $baseDir . '/lib/public/User/Events/UserLoggedOutEvent.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 37771cafa78..4a857abae35 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -548,6 +548,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\User\\Events\\UserChangedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserChangedEvent.php', 'OCP\\User\\Events\\UserCreatedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserCreatedEvent.php', 'OCP\\User\\Events\\UserDeletedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserDeletedEvent.php', + 'OCP\\User\\Events\\UserLiveStatusEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserLiveStatusEvent.php', 'OCP\\User\\Events\\UserLoggedInEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserLoggedInEvent.php', 'OCP\\User\\Events\\UserLoggedInWithCookieEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserLoggedInWithCookieEvent.php', 'OCP\\User\\Events\\UserLoggedOutEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserLoggedOutEvent.php', diff --git a/lib/private/NavigationManager.php b/lib/private/NavigationManager.php index b40f403c056..81642fac234 100644 --- a/lib/private/NavigationManager.php +++ b/lib/private/NavigationManager.php @@ -200,7 +200,7 @@ class NavigationManager implements INavigationManager { $this->add([ 'type' => 'settings', 'id' => 'help', - 'order' => 5, + 'order' => 6, 'href' => $this->urlGenerator->linkToRoute('settings.Help.help'), 'name' => $l->t('Help'), 'icon' => $this->urlGenerator->imagePath('settings', 'help.svg'), @@ -213,7 +213,7 @@ class NavigationManager implements INavigationManager { $this->add([ 'type' => 'settings', 'id' => 'core_apps', - 'order' => 3, + 'order' => 4, 'href' => $this->urlGenerator->linkToRoute('settings.AppSettings.viewApps'), 'icon' => $this->urlGenerator->imagePath('settings', 'apps.svg'), 'name' => $l->t('Apps'), @@ -224,7 +224,7 @@ class NavigationManager implements INavigationManager { $this->add([ 'type' => 'settings', 'id' => 'settings', - 'order' => 1, + 'order' => 2, 'href' => $this->urlGenerator->linkToRoute('settings.PersonalSettings.index'), 'name' => $l->t('Settings'), 'icon' => $this->urlGenerator->imagePath('settings', 'admin.svg'), @@ -248,7 +248,7 @@ class NavigationManager implements INavigationManager { $this->add([ 'type' => 'settings', 'id' => 'core_users', - 'order' => 4, + 'order' => 5, 'href' => $this->urlGenerator->linkToRoute('settings.Users.usersList'), 'name' => $l->t('Users'), 'icon' => $this->urlGenerator->imagePath('settings', 'users.svg'), diff --git a/lib/public/User/Events/UserLiveStatusEvent.php b/lib/public/User/Events/UserLiveStatusEvent.php new file mode 100644 index 00000000000..6c836a28ddf --- /dev/null +++ b/lib/public/User/Events/UserLiveStatusEvent.php @@ -0,0 +1,101 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCP\User\Events; + +use OCP\EventDispatcher\Event; +use OCP\IUser; + +/** + * @since 20.0.0 + */ +class UserLiveStatusEvent extends Event { + + /** + * @var string + * @since 20.0.0 + */ + public const STATUS_ONLINE = 'online'; + + /** + * @var string + * @since 20.0.0 + */ + public const STATUS_AWAY = 'away'; + + /** + * @var string + * @since 20.0.0 + */ + public const STATUS_OFFLINE = 'offline'; + + /** @var IUser */ + private $user; + + /** @var string */ + private $status; + + /** @var int */ + private $timestamp; + + /** + * @param IUser $user + * @param string $status + * @param int $timestamp + * @since 20.0.0 + */ + public function __construct(IUser $user, + string $status, + int $timestamp) { + parent::__construct(); + $this->user = $user; + $this->status = $status; + $this->timestamp = $timestamp; + } + + /** + * @return IUser + * @since 20.0.0 + */ + public function getUser(): IUser { + return $this->user; + } + + /** + * @return string + * @since 20.0.0 + */ + public function getStatus(): string { + return $this->status; + } + + /** + * @return int + * @since 20.0.0 + */ + public function getTimestamp(): int { + return $this->timestamp; + } +} diff --git a/package-lock.json b/package-lock.json index 3f6a585dee2..0b93bb8b7d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1586,6 +1586,57 @@ "core-js": "^3.6.4" } }, + "@nextcloud/moment": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@nextcloud/moment/-/moment-1.1.1.tgz", + "integrity": "sha512-lh7Xn9Ver12pLfE0rpjxE6x/ipscAV+7fw1u+7TJak1QR1T1UDRMZ9dA7z77W8mZH2C3yveTh/VEHZIflKBrng==", + "requires": { + "@nextcloud/l10n": "1.2.0", + "core-js": "3.6.4", + "jed": "^1.1.1", + "moment": "2.24.0", + "node-gettext": "^2.0.0" + }, + "dependencies": { + "@nextcloud/l10n": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-1.2.0.tgz", + "integrity": "sha512-aPsVAewCYMNe2h0yse3Fj7LofvnvFPimojw24K47ip1+I1gawMIsQL+BYAnN8wzlcbsDTEc7I1FxtOh+8dHHIA==", + "requires": { + "core-js": "^3.6.4", + "node-gettext": "^3.0.0" + }, + "dependencies": { + "node-gettext": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-gettext/-/node-gettext-3.0.0.tgz", + "integrity": "sha512-/VRYibXmVoN6tnSAY2JWhNRhWYJ8Cd844jrZU/DwLVoI4vBI6ceYbd8i42sYZ9uOgDH3S7vslIKOWV/ZrT2YBA==", + "requires": { + "lodash.get": "^4.4.2" + } + } + } + }, + "core-js": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", + "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==" + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "node-gettext": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-gettext/-/node-gettext-2.1.0.tgz", + "integrity": "sha512-vsHImHl+Py0vB7M2UXcFEJ5NJ3950gcja45YclBFtYxYeZiqdfQdcu+G9s4L7jpRFSh/J/7VoS3upR4JM1nS+g==", + "requires": { + "lodash.get": "^4.4.2" + } + } + } + }, "@nextcloud/password-confirmation": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@nextcloud/password-confirmation/-/password-confirmation-1.0.1.tgz", @@ -6033,6 +6084,11 @@ "resize-observer-polyfill": "^1.5.0" } }, + "jed": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz", + "integrity": "sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ=" + }, "jquery": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz", diff --git a/package.json b/package.json index 9409d32671e..520c84b098e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@nextcloud/initial-state": "^1.1.2", "@nextcloud/l10n": "^1.3.0", "@nextcloud/logger": "^1.1.2", + "@nextcloud/moment": "^1.1.1", "@nextcloud/password-confirmation": "^1.0.1", "@nextcloud/paths": "^1.1.2", "@nextcloud/router": "^1.1.0", diff --git a/tests/lib/NavigationManagerTest.php b/tests/lib/NavigationManagerTest.php index 4f1cabc3cba..a33a06635c9 100644 --- a/tests/lib/NavigationManagerTest.php +++ b/tests/lib/NavigationManagerTest.php @@ -244,7 +244,7 @@ class NavigationManagerTest extends TestCase { $apps = [ 'core_apps' => [ 'id' => 'core_apps', - 'order' => 3, + 'order' => 4, 'href' => '/apps/test/', 'icon' => '/apps/settings/img/apps.svg', 'name' => 'Apps', @@ -256,7 +256,7 @@ class NavigationManagerTest extends TestCase { $defaults = [ 'settings' => [ 'id' => 'settings', - 'order' => 1, + 'order' => 2, 'href' => '/apps/test/', 'icon' => '/apps/settings/img/admin.svg', 'name' => 'Settings', diff --git a/webpack.common.js b/webpack.common.js index f61c0233874..6cd7c789084 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -15,6 +15,7 @@ const files_versions = require('./apps/files_versions/webpack') const oauth2 = require('./apps/oauth2/webpack') const settings = require('./apps/settings/webpack') const systemtags = require('./apps/systemtags/webpack') +const user_status = require('./apps/user_status/webpack') const twofactor_backupscodes = require('./apps/twofactor_backupcodes/webpack') const updatenotification = require('./apps/updatenotification/webpack') const workflowengine = require('./apps/workflowengine/webpack') @@ -31,6 +32,7 @@ const modules = { oauth2, settings, systemtags, + user_status, twofactor_backupscodes, updatenotification, workflowengine