Signed-off-by: Georg Ehrke <developer@georgehrke.com>tags/v20.0.0beta1
@@ -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 |
@@ -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. | |||
@@ -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 |
@@ -0,0 +1,31 @@ | |||
<?xml version="1.0"?> | |||
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" | |||
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd"> | |||
<id>user_status</id> | |||
<name>User status</name> | |||
<summary>User status</summary> | |||
<description><![CDATA[User status]]></description> | |||
<version>0.0.2</version> | |||
<licence>agpl</licence> | |||
<author mail="oc.list@georgehrke.com" >Georg Ehrke</author> | |||
<namespace>UserStatus</namespace> | |||
<default_enable/> | |||
<category>social</category> | |||
<bugs>https://github.com/nextcloud/server</bugs> | |||
<navigations> | |||
<navigation> | |||
<id>user_status-menuitem</id> | |||
<name>User status</name> | |||
<route /> | |||
<order>1</order> | |||
<icon>info.svg</icon> | |||
<type>settings</type> | |||
</navigation> | |||
</navigations> | |||
<dependencies> | |||
<nextcloud min-version="20" max-version="20"/> | |||
</dependencies> | |||
<background-jobs> | |||
<job>OCA\UserStatus\BackgroundJob\ClearOldStatusesBackgroundJob</job> | |||
</background-jobs> | |||
</info> |
@@ -0,0 +1,43 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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'], | |||
], | |||
]; |
@@ -0,0 +1,7 @@ | |||
<?php | |||
// autoload.php @generated by Composer | |||
require_once __DIR__ . '/composer/autoload_real.php'; | |||
return ComposerAutoloaderInitUserStatus::getLoader(); |
@@ -0,0 +1,13 @@ | |||
{ | |||
"config" : { | |||
"vendor-dir": ".", | |||
"optimize-autoloader": true, | |||
"classmap-authoritative": true, | |||
"autoloader-suffix": "UserStatus" | |||
}, | |||
"autoload" : { | |||
"psr-4": { | |||
"OCA\\UserStatus\\": "../lib/" | |||
} | |||
} | |||
} |
@@ -0,0 +1,445 @@ | |||
<?php | |||
/* | |||
* This file is part of Composer. | |||
* | |||
* (c) Nils Adermann <naderman@naderman.de> | |||
* Jordi Boggiano <j.boggiano@seld.be> | |||
* | |||
* 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 <fabien@symfony.com> | |||
* @author Jordi Boggiano <j.boggiano@seld.be> | |||
* @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; | |||
} |
@@ -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. | |||
@@ -0,0 +1,31 @@ | |||
<?php | |||
// autoload_classmap.php @generated by Composer | |||
$vendorDir = dirname(dirname(__FILE__)); | |||
$baseDir = $vendorDir; | |||
return array( | |||
'OCA\\UserStatus\\AppInfo\\Application' => $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', | |||
); |
@@ -0,0 +1,9 @@ | |||
<?php | |||
// autoload_namespaces.php @generated by Composer | |||
$vendorDir = dirname(dirname(__FILE__)); | |||
$baseDir = $vendorDir; | |||
return array( | |||
); |
@@ -0,0 +1,10 @@ | |||
<?php | |||
// autoload_psr4.php @generated by Composer | |||
$vendorDir = dirname(dirname(__FILE__)); | |||
$baseDir = $vendorDir; | |||
return array( | |||
'OCA\\UserStatus\\' => array($baseDir . '/../lib'), | |||
); |
@@ -0,0 +1,46 @@ | |||
<?php | |||
// autoload_real.php @generated by Composer | |||
class ComposerAutoloaderInitUserStatus | |||
{ | |||
private static $loader; | |||
public static function loadClassLoader($class) | |||
{ | |||
if ('Composer\Autoload\ClassLoader' === $class) { | |||
require __DIR__ . '/ClassLoader.php'; | |||
} | |||
} | |||
/** | |||
* @return \Composer\Autoload\ClassLoader | |||
*/ | |||
public static function getLoader() | |||
{ | |||
if (null !== self::$loader) { | |||
return self::$loader; | |||
} | |||
spl_autoload_register(array('ComposerAutoloaderInitUserStatus', 'loadClassLoader'), true, true); | |||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(); | |||
spl_autoload_unregister(array('ComposerAutoloaderInitUserStatus', 'loadClassLoader')); | |||
$useStaticLoader = PHP_VERSION_ID >= 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; | |||
} | |||
} |
@@ -0,0 +1,57 @@ | |||
<?php | |||
// autoload_static.php @generated by Composer | |||
namespace Composer\Autoload; | |||
class ComposerStaticInitUserStatus | |||
{ | |||
public static $prefixLengthsPsr4 = array ( | |||
'O' => | |||
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); | |||
} | |||
} |
@@ -0,0 +1,37 @@ | |||
/** | |||
* @copyright Copyright (c) 2020 Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
.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); | |||
} |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><g><path d="M12,2C6.5,2,2,6.5,2,12s4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z M16.2,16.2L11,13V7h1.5v5.2l4.5,2.7L16.2,16.2z"/></g></g></g></svg> |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><g><path fill="#F4A331" d="M12,2C6.5,2,2,6.5,2,12s4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z M16.2,16.2L11,13V7h1.5v5.2l4.5,2.7L16.2,16.2z"/></g></g></g></svg> |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#ED484C" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11H7v-2h10v2z"/></svg> |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg> |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><path fill="#49B382" d="M8,16h8V8H8V16z M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10 S17.52,2,12,2L12,2z"/></g></svg> |
@@ -0,0 +1,74 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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 { | |||
} | |||
} |
@@ -0,0 +1,63 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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()); | |||
} | |||
} |
@@ -0,0 +1,60 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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(), | |||
], | |||
]; | |||
} | |||
} |
@@ -0,0 +1,92 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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); | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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()); | |||
} | |||
} |
@@ -0,0 +1,107 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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, | |||
]; | |||
} | |||
} |
@@ -0,0 +1,191 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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(), | |||
]; | |||
} | |||
} |
@@ -0,0 +1,90 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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'); | |||
} | |||
} |
@@ -0,0 +1,104 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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(); | |||
} | |||
} |
@@ -0,0 +1,29 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
namespace OCA\UserStatus\Exception; | |||
class InvalidClearAtException extends \Exception { | |||
} |
@@ -0,0 +1,29 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
namespace OCA\UserStatus\Exception; | |||
class InvalidMessageIdException extends \Exception { | |||
} |
@@ -0,0 +1,29 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
namespace OCA\UserStatus\Exception; | |||
class InvalidStatusIconException extends \Exception { | |||
} |
@@ -0,0 +1,29 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
namespace OCA\UserStatus\Exception; | |||
class InvalidStatusTypeException extends \Exception { | |||
} |
@@ -0,0 +1,29 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
namespace OCA\UserStatus\Exception; | |||
class StatusMessageTooLongException extends \Exception { | |||
} |
@@ -0,0 +1,75 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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'); | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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()); | |||
} | |||
} |
@@ -0,0 +1,133 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,97 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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; | |||
} | |||
} |
@@ -0,0 +1,100 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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; | |||
} | |||
} |
@@ -0,0 +1,84 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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(), | |||
]; | |||
} | |||
} |
@@ -0,0 +1,187 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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); | |||
} | |||
} |
@@ -0,0 +1,335 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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']); | |||
} | |||
} | |||
} |
@@ -0,0 +1,271 @@ | |||
<!-- | |||
- @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> | |||
- @author Georg Ehrke <oc.list@georgehrke.com> | |||
- | |||
- @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 <http://www.gnu.org/licenses/>. | |||
- | |||
--> | |||
<template> | |||
<li> | |||
<div id="user-status-menu-item"> | |||
<span id="user-status-menu-item__header">{{ displayName }}</span> | |||
<Actions | |||
id="user-status-menu-item__subheader" | |||
:default-icon="statusIcon" | |||
:menu-title="visibleMessage"> | |||
<ActionButton | |||
v-for="status in statuses" | |||
:key="status.type" | |||
:icon="status.icon" | |||
:close-after-click="true" | |||
@click.prevent.stop="changeStatus(status.type)"> | |||
{{ status.label }} | |||
</ActionButton> | |||
<ActionButton | |||
icon="icon-rename" | |||
:close-after-click="true" | |||
@click.prevent.stop="openModal"> | |||
{{ $t('user_status', 'Set custom status') }} | |||
</ActionButton> | |||
</Actions> | |||
<SetStatusModal | |||
v-if="isModalOpen" | |||
@close="closeModal" /> | |||
</div> | |||
</li> | |||
</template> | |||
<script> | |||
import { getCurrentUser } from '@nextcloud/auth' | |||
import SetStatusModal from './components/SetStatusModal' | |||
import Actions from '@nextcloud/vue/dist/Components/Actions' | |||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' | |||
import { mapState } from 'vuex' | |||
import { showError } from '@nextcloud/dialogs' | |||
import { getAllStatusOptions } from './services/statusOptionsService' | |||
import { sendHeartbeat } from './services/heartbeatService' | |||
import debounce from 'debounce' | |||
export default { | |||
name: 'App', | |||
components: { | |||
Actions, | |||
ActionButton, | |||
SetStatusModal, | |||
}, | |||
data() { | |||
return { | |||
isModalOpen: false, | |||
statuses: getAllStatusOptions(), | |||
heartbeatInterval: null, | |||
setAwayTimeout: null, | |||
mouseMoveListener: null, | |||
isAway: false, | |||
} | |||
}, | |||
computed: { | |||
...mapState({ | |||
statusType: state => state.userStatus.status, | |||
statusIsUserDefined: state => state.userStatus.statusIsUserDefined, | |||
customIcon: state => state.userStatus.icon, | |||
customMessage: state => state.userStatus.message, | |||
}), | |||
/** | |||
* The display-name of the current user | |||
* | |||
* @returns {String} | |||
*/ | |||
displayName() { | |||
return getCurrentUser().displayName | |||
}, | |||
/** | |||
* The message displayed in the top right corner | |||
* | |||
* @returns {String} | |||
*/ | |||
visibleMessage() { | |||
if (this.customIcon && this.customMessage) { | |||
return `${this.customIcon} ${this.customMessage}` | |||
} | |||
if (this.customMessage) { | |||
return this.customMessage | |||
} | |||
if (this.statusIsUserDefined) { | |||
switch (this.statusType) { | |||
case 'online': | |||
return this.$t('user_status', 'Online') | |||
case 'away': | |||
return this.$t('user_status', 'Away') | |||
case 'dnd': | |||
return this.$t('user_status', 'Do not disturb') | |||
case 'invisible': | |||
return this.$t('user_status', 'Invisible') | |||
case 'offline': | |||
return this.$t('user_status', 'Offline') | |||
} | |||
} | |||
return this.$t('user_status', 'Set status') | |||
}, | |||
/** | |||
* The status indicator icon | |||
* | |||
* @returns {String|null} | |||
*/ | |||
statusIcon() { | |||
switch (this.statusType) { | |||
case 'online': | |||
return 'icon-user-status-online' | |||
case 'away': | |||
return 'icon-user-status-away' | |||
case 'dnd': | |||
return 'icon-user-status-dnd' | |||
case 'invisible': | |||
case 'offline': | |||
return 'icon-user-status-invisible' | |||
} | |||
return '' | |||
}, | |||
}, | |||
/** | |||
* Loads the current user's status from initial state | |||
* and stores it in Vuex | |||
*/ | |||
mounted() { | |||
this.$store.dispatch('loadStatusFromInitialState') | |||
if (OC.config.session_keepalive) { | |||
// Send the latest status to the server every 5 minutes | |||
this.heartbeatInterval = setInterval(this._backgroundHeartbeat.bind(this), 1000 * 60 * 5) | |||
this.setAwayTimeout = () => { | |||
this.isAway = true | |||
} | |||
// Catch mouse movements, but debounce to once every 30 seconds | |||
this.mouseMoveListener = debounce(() => { | |||
const wasAway = this.isAway | |||
this.isAway = false | |||
// Reset the two minute counter | |||
clearTimeout(this.setAwayTimeout) | |||
// If the user did not move the mouse within two minutes, | |||
// mark them as away | |||
setTimeout(this.setAwayTimeout, 1000 * 60 * 2) | |||
if (wasAway) { | |||
this._backgroundHeartbeat() | |||
} | |||
}, 1000 * 2, true) | |||
window.addEventListener('mousemove', this.mouseMoveListener, { | |||
capture: true, | |||
passive: true, | |||
}) | |||
this._backgroundHeartbeat() | |||
} | |||
}, | |||
/** | |||
* Some housekeeping before destroying the component | |||
*/ | |||
beforeDestroy() { | |||
window.removeEventListener('mouseMove', this.mouseMoveListener) | |||
clearInterval(this.heartbeatInterval) | |||
}, | |||
methods: { | |||
/** | |||
* Opens the modal to set a custom status | |||
*/ | |||
openModal() { | |||
this.isModalOpen = true | |||
}, | |||
/** | |||
* Closes the modal | |||
*/ | |||
closeModal() { | |||
this.isModalOpen = false | |||
}, | |||
/** | |||
* Changes the user-status | |||
* | |||
* @param {String} statusType (online / away / dnd / invisible) | |||
*/ | |||
async changeStatus(statusType) { | |||
try { | |||
await this.$store.dispatch('setStatus', { statusType }) | |||
} catch (err) { | |||
showError(this.$t('user_status', 'There was an error saving the new status')) | |||
console.debug(err) | |||
} | |||
}, | |||
/** | |||
* Sends the status heartbeat to the server | |||
* | |||
* @returns {Promise<void>} | |||
* @private | |||
*/ | |||
async _backgroundHeartbeat() { | |||
await sendHeartbeat(this.isAway) | |||
await this.$store.dispatch('reFetchStatusFromServer') | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss"> | |||
#user-status-menu-item { | |||
&__header { | |||
display: block; | |||
align-items: center; | |||
color: var(--color-main-text); | |||
padding: 10px 12px 5px 12px; | |||
box-sizing: border-box; | |||
opacity: 1; | |||
white-space: nowrap; | |||
width: 100%; | |||
text-align: center; | |||
max-width: 250px; | |||
text-overflow: ellipsis; | |||
min-width: 175px; | |||
} | |||
&__subheader { | |||
width: 100%; | |||
> button { | |||
background-color: var(--color-main-background); | |||
background-size: 16px; | |||
border: 0; | |||
border-radius: 0; | |||
font-weight: normal; | |||
font-size: 0.875em; | |||
padding-left: 40px; | |||
&:hover, | |||
&:focus { | |||
box-shadow: inset 4px 0 var(--color-primary-element); | |||
} | |||
} | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,102 @@ | |||
<!-- | |||
- @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> | |||
- @author Georg Ehrke <oc.list@georgehrke.com> | |||
- | |||
- @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 <http://www.gnu.org/licenses/>. | |||
- | |||
--> | |||
<template> | |||
<div class="clear-at-select"> | |||
<span | |||
class="clear-at-select__label"> | |||
{{ $t('user_select', 'Clear status after') }} | |||
</span> | |||
<Multiselect | |||
label="label" | |||
:value="option" | |||
:options="options" | |||
open-direction="top" | |||
@select="select" /> | |||
</div> | |||
</template> | |||
<script> | |||
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect' | |||
import { getAllClearAtOptions } from '../services/clearAtOptionsService' | |||
import { clearAtFilter } from '../filters/clearAtFilter' | |||
export default { | |||
name: 'ClearAtSelect', | |||
components: { | |||
Multiselect, | |||
}, | |||
props: { | |||
clearAt: { | |||
type: Object, | |||
default: null, | |||
}, | |||
}, | |||
data() { | |||
return { | |||
options: getAllClearAtOptions(), | |||
} | |||
}, | |||
computed: { | |||
/** | |||
* Returns an object of the currently selected option | |||
* | |||
* @returns {Object} | |||
*/ | |||
option() { | |||
return { | |||
clearAt: this.clearAt, | |||
label: clearAtFilter(this.clearAt), | |||
} | |||
}, | |||
}, | |||
methods: { | |||
/** | |||
* Triggered when the user selects a new option. | |||
* | |||
* @param {Object=} option The new selected option | |||
*/ | |||
select(option) { | |||
if (!option) { | |||
return | |||
} | |||
this.$emit('selectClearAt', option.clearAt) | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.clear-at-select { | |||
display: flex; | |||
margin-bottom: 10px; | |||
align-items: center; | |||
&__label { | |||
margin-right: 10px; | |||
} | |||
.multiselect { | |||
flex-grow: 1; | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,65 @@ | |||
<!-- | |||
- @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> | |||
- @author Georg Ehrke <oc.list@georgehrke.com> | |||
- | |||
- @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 <http://www.gnu.org/licenses/>. | |||
- | |||
--> | |||
<template> | |||
<form | |||
class="custom-input__form" | |||
@submit.prevent> | |||
<input | |||
:placeholder="$t('user_status', 'What\'s your status?')" | |||
type="text" | |||
:value="message" | |||
@change="change"> | |||
</form> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'CustomMessageInput', | |||
props: { | |||
message: { | |||
type: String, | |||
required: true, | |||
default: () => '', | |||
}, | |||
}, | |||
methods: { | |||
/** | |||
* Notifies the parent component about a changed input | |||
* | |||
* @param {Event} event The Change Event | |||
*/ | |||
change(event) { | |||
this.$emit('change', event.target.value) | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.custom-input__form { | |||
flex-grow: 1; | |||
input { | |||
width: 100%; | |||
border-radius: 0 var(--border-radius) var(--border-radius) 0; | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,111 @@ | |||
<!-- | |||
- @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> | |||
- @author Georg Ehrke <oc.list@georgehrke.com> | |||
- | |||
- @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 <http://www.gnu.org/licenses/>. | |||
- | |||
--> | |||
<template> | |||
<div | |||
class="predefined-status" | |||
tabindex="0" | |||
@keyup.enter="select" | |||
@keyup.space="select" | |||
@click="select"> | |||
<span class="predefined-status__icon"> | |||
{{ icon }} | |||
</span> | |||
<span class="predefined-status__message"> | |||
{{ message }} | |||
</span> | |||
<span class="predefined-status__clear-at"> | |||
{{ clearAt | clearAtFilter }} | |||
</span> | |||
</div> | |||
</template> | |||
<script> | |||
import { clearAtFilter } from '../filters/clearAtFilter' | |||
export default { | |||
name: 'PredefinedStatus', | |||
filters: { | |||
clearAtFilter, | |||
}, | |||
props: { | |||
messageId: { | |||
type: String, | |||
required: true, | |||
}, | |||
icon: { | |||
type: String, | |||
required: true, | |||
}, | |||
message: { | |||
type: String, | |||
required: true, | |||
}, | |||
clearAt: { | |||
type: Object, | |||
required: false, | |||
default: null, | |||
}, | |||
}, | |||
methods: { | |||
/** | |||
* Emits an event when the user clicks the row | |||
*/ | |||
select() { | |||
this.$emit('select') | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.predefined-status { | |||
display: flex; | |||
flex-wrap: nowrap; | |||
justify-content: flex-start; | |||
flex-basis: 100%; | |||
border-radius: var(--border-radius); | |||
align-items: center; | |||
min-height: 44px; | |||
&:hover, | |||
&:focus { | |||
background-color: var(--color-background-hover); | |||
} | |||
&__icon { | |||
flex-basis: 40px; | |||
text-align: center; | |||
} | |||
&__message { | |||
font-weight: bold; | |||
padding: 0 6px; | |||
} | |||
&__clear-at { | |||
opacity: .7; | |||
&::before { | |||
content: ' - '; | |||
} | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,90 @@ | |||
<!-- | |||
- @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> | |||
- @author Georg Ehrke <oc.list@georgehrke.com> | |||
- | |||
- @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 <http://www.gnu.org/licenses/>. | |||
- | |||
--> | |||
<template> | |||
<div | |||
v-if="hasLoaded" | |||
class="predefined-statuses-list"> | |||
<PredefinedStatus | |||
v-for="status in predefinedStatuses" | |||
:key="status.id" | |||
:message-id="status.id" | |||
:icon="status.icon" | |||
:message="status.message" | |||
:clear-at="status.clearAt" | |||
@select="selectStatus(status)" /> | |||
</div> | |||
<div | |||
v-else | |||
class="predefined-statuses-list"> | |||
<div class="icon icon-loading-small" /> | |||
</div> | |||
</template> | |||
<script> | |||
import PredefinedStatus from './PredefinedStatus' | |||
import { mapState } from 'vuex' | |||
export default { | |||
name: 'PredefinedStatusesList', | |||
components: { | |||
PredefinedStatus, | |||
}, | |||
computed: { | |||
...mapState({ | |||
predefinedStatuses: state => state.predefinedStatuses.predefinedStatuses, | |||
}), | |||
/** | |||
* Indicator whether the predefined statuses have already been loaded | |||
* | |||
* @returns {boolean} | |||
*/ | |||
hasLoaded() { | |||
return this.predefinedStatuses.length > 0 | |||
}, | |||
}, | |||
/** | |||
* Loads all predefined statuses from the server | |||
* when this component is mounted | |||
*/ | |||
mounted() { | |||
this.$store.dispatch('loadAllPredefinedStatuses') | |||
}, | |||
methods: { | |||
/** | |||
* Emits an event when the user selects a status | |||
* | |||
* @param {Object} status The selected status | |||
*/ | |||
selectStatus(status) { | |||
this.$emit('selectStatus', status) | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.predefined-statuses-list { | |||
display: flex; | |||
flex-direction: column; | |||
margin-bottom: 10px; | |||
} | |||
</style> |
@@ -0,0 +1,236 @@ | |||
<!-- | |||
- @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> | |||
- @author Georg Ehrke <oc.list@georgehrke.com> | |||
- | |||
- @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 <http://www.gnu.org/licenses/>. | |||
- | |||
--> | |||
<template> | |||
<Modal | |||
size="normal" | |||
:title="$t('user_status', 'Set a custom status')" | |||
@close="closeModal"> | |||
<div class="set-status-modal"> | |||
<div class="set-status-modal__header"> | |||
<h3>{{ $t('user_status', 'Set a custom status') }}</h3> | |||
</div> | |||
<div class="set-status-modal__custom-input"> | |||
<EmojiPicker @select="setIcon"> | |||
<button | |||
class="custom-input__emoji-button"> | |||
{{ visibleIcon }} | |||
</button> | |||
</EmojiPicker> | |||
<CustomMessageInput | |||
:message="message" | |||
@change="setMessage" /> | |||
</div> | |||
<PredefinedStatusesList | |||
@selectStatus="selectPredefinedMessage" /> | |||
<ClearAtSelect | |||
:clear-at="clearAt" | |||
@selectClearAt="setClearAt" /> | |||
<div class="status-buttons"> | |||
<button class="status-buttons__select" @click="clearStatus"> | |||
{{ $t('user_status', 'Clear custom status') }} | |||
</button> | |||
<button class="status-buttons__primary primary" @click="saveStatus"> | |||
{{ $t('user_status', 'Set status') }} | |||
</button> | |||
</div> | |||
</div> | |||
</Modal> | |||
</template> | |||
<script> | |||
import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker' | |||
import Modal from '@nextcloud/vue/dist/Components/Modal' | |||
import PredefinedStatusesList from './PredefinedStatusesList' | |||
import CustomMessageInput from './CustomMessageInput' | |||
import ClearAtSelect from './ClearAtSelect' | |||
import { showError } from '@nextcloud/dialogs' | |||
export default { | |||
name: 'SetStatusModal', | |||
components: { | |||
EmojiPicker, | |||
Modal, | |||
CustomMessageInput, | |||
PredefinedStatusesList, | |||
ClearAtSelect, | |||
}, | |||
data() { | |||
return { | |||
icon: null, | |||
message: null, | |||
clearAt: null, | |||
} | |||
}, | |||
computed: { | |||
/** | |||
* Returns the user-set icon or a smiley in case no icon is set | |||
* | |||
* @returns {String} | |||
*/ | |||
visibleIcon() { | |||
return this.icon || '😀' | |||
}, | |||
}, | |||
/** | |||
* Loads the current status when a user opens dialog | |||
*/ | |||
mounted() { | |||
this.messageId = this.$store.state.userStatus.messageId | |||
this.icon = this.$store.state.userStatus.icon | |||
this.message = this.$store.state.userStatus.message | |||
if (this.$store.state.userStatus.clearAt !== null) { | |||
this.clearAt = { | |||
type: '_time', | |||
time: this.$store.state.userStatus.clearAt, | |||
} | |||
} | |||
}, | |||
methods: { | |||
/** | |||
* Closes the Set Status modal | |||
*/ | |||
closeModal() { | |||
this.$emit('close') | |||
}, | |||
/** | |||
* Sets a new icon | |||
* | |||
* @param {String} icon The new icon | |||
*/ | |||
setIcon(icon) { | |||
this.messageId = null | |||
this.icon = icon | |||
}, | |||
/** | |||
* Sets a new message | |||
* | |||
* @param {String} message The new message | |||
*/ | |||
setMessage(message) { | |||
this.messageId = null | |||
this.message = message | |||
}, | |||
/** | |||
* Sets a new clearAt value | |||
* | |||
* @param {Object} clearAt The new clearAt object | |||
*/ | |||
setClearAt(clearAt) { | |||
this.clearAt = clearAt | |||
}, | |||
/** | |||
* Sets new icon/message/clearAt based on a predefined message | |||
* | |||
* @param {Object} status The predefined status object | |||
*/ | |||
selectPredefinedMessage(status) { | |||
this.messageId = status.id | |||
this.clearAt = status.clearAt | |||
this.icon = status.icon | |||
this.message = status.message | |||
}, | |||
/** | |||
* Saves the status and closes the | |||
* | |||
* @returns {Promise<void>} | |||
*/ | |||
async saveStatus() { | |||
try { | |||
this.isSavingStatus = true | |||
if (this.messageId !== null) { | |||
await this.$store.dispatch('setPredefinedMessage', { | |||
messageId: this.messageId, | |||
clearAt: this.clearAt, | |||
}) | |||
} else { | |||
await this.$store.dispatch('setCustomMessage', { | |||
message: this.message, | |||
icon: this.icon, | |||
clearAt: this.clearAt, | |||
}) | |||
} | |||
} catch (err) { | |||
showError(this.$t('user_status', 'There was an error saving the status')) | |||
console.debug(err) | |||
this.isSavingStatus = false | |||
return | |||
} | |||
this.isSavingStatus = false | |||
this.closeModal() | |||
}, | |||
/** | |||
* | |||
* @returns {Promise<void>} | |||
*/ | |||
async clearStatus() { | |||
try { | |||
this.isSavingStatus = true | |||
await this.$store.dispatch('clearMessage') | |||
} catch (err) { | |||
showError(this.$t('user_status', 'There was an error clearing the status')) | |||
console.debug(err) | |||
this.isSavingStatus = false | |||
return | |||
} | |||
this.isSavingStatus = false | |||
this.closeModal() | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.set-status-modal { | |||
min-width: 500px; | |||
min-height: 200px; | |||
padding: 8px 20px 20px 20px; | |||
&__custom-input { | |||
display: flex; | |||
width: 100%; | |||
margin-bottom: 10px; | |||
.custom-input__emoji-button { | |||
flex-basis: 40px; | |||
width: 40px; | |||
flex-grow: 0; | |||
border-radius: var(--border-radius) 0 0 var(--border-radius); | |||
height: 34px; | |||
margin-right: 0; | |||
border-right: none; | |||
} | |||
} | |||
.status-buttons { | |||
display: flex; | |||
button { | |||
flex-basis: 50%; | |||
} | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,68 @@ | |||
/** | |||
* @copyright Copyright (c) 2020 Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
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, | |||
} |
@@ -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 } |
@@ -0,0 +1,68 @@ | |||
/** | |||
* @copyright Copyright (c) 2020 Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
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, | |||
} |
@@ -0,0 +1,63 @@ | |||
/** | |||
* @copyright Copyright (c) 2020 Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
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, | |||
} |
@@ -0,0 +1,34 @@ | |||
/** | |||
* @copyright Copyright (c) 2020 Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
/** | |||
* Returns a new Date object | |||
* | |||
* @returns {Date} | |||
*/ | |||
const dateFactory = () => { | |||
return new Date() | |||
} | |||
export { | |||
dateFactory, | |||
} |
@@ -0,0 +1,40 @@ | |||
/** | |||
* @copyright Copyright (c) 2020 Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
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<void>} | |||
*/ | |||
const sendHeartbeat = async(isAway) => { | |||
const url = generateUrl('/apps/user_status/heartbeat') | |||
await HttpClient.put(url, { | |||
status: isAway ? 'away' : 'online', | |||
}) | |||
} | |||
export { | |||
sendHeartbeat, | |||
} |
@@ -0,0 +1,39 @@ | |||
/** | |||
* @copyright Copyright (c) 2020 Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
import HttpClient from '@nextcloud/axios' | |||
import { generateOcsUrl } from '@nextcloud/router' | |||
/** | |||
* Fetches all predefined statuses from the server | |||
* | |||
* @returns {Promise<void>} | |||
*/ | |||
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, | |||
} |
@@ -0,0 +1,52 @@ | |||
/** | |||
* @copyright Copyright (c) 2020 Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
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, | |||
} |
@@ -0,0 +1,98 @@ | |||
/** | |||
* @copyright Copyright (c) 2020 Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
import HttpClient from '@nextcloud/axios' | |||
import { generateOcsUrl } from '@nextcloud/router' | |||
/** | |||
* Fetches the current user-status | |||
* | |||
* @returns {Promise<Object>} | |||
*/ | |||
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<void>} | |||
*/ | |||
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<void>} | |||
*/ | |||
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<void>} | |||
*/ | |||
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<void>} | |||
*/ | |||
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, | |||
} |
@@ -0,0 +1,35 @@ | |||
/** | |||
* @copyright Copyright (c) 2020 Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
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, | |||
}) |
@@ -0,0 +1,64 @@ | |||
/** | |||
* @copyright Copyright (c) 2020 Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
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 } |
@@ -0,0 +1,232 @@ | |||
/** | |||
* @copyright Copyright (c) 2020 Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
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<void>} | |||
*/ | |||
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<void>} | |||
*/ | |||
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<void>} | |||
*/ | |||
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<void>} | |||
*/ | |||
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<void>} | |||
*/ | |||
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 } |
@@ -0,0 +1,63 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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', [[]]); | |||
} | |||
} |
@@ -0,0 +1,71 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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], | |||
]; | |||
} | |||
} |
@@ -0,0 +1,74 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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()); | |||
} | |||
} |
@@ -0,0 +1,114 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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; | |||
} | |||
} |
@@ -0,0 +1,340 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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; | |||
} | |||
} |
@@ -0,0 +1,168 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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); | |||
} | |||
} |
@@ -0,0 +1,71 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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); | |||
} | |||
} |
@@ -0,0 +1,162 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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); | |||
} | |||
} |
@@ -0,0 +1,100 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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] | |||
]; | |||
} | |||
} |
@@ -0,0 +1,184 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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], | |||
]; | |||
} | |||
} |
@@ -0,0 +1,592 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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); | |||
} | |||
} |
@@ -0,0 +1,36 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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(); |
@@ -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: '-', | |||
} | |||
} | |||
} |
@@ -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', |
@@ -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', |
@@ -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'), |
@@ -0,0 +1,101 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2020, Georg Ehrke | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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; | |||
} | |||
} |
@@ -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", |
@@ -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", |
@@ -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', |
@@ -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 |