diff options
Diffstat (limited to 'apps/user_status')
200 files changed, 14973 insertions, 0 deletions
diff --git a/apps/user_status/.l10nignore b/apps/user_status/.l10nignore new file mode 100644 index 00000000000..91aefac85dc --- /dev/null +++ b/apps/user_status/.l10nignore @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +#webpack bundled files +js/ diff --git a/apps/user_status/appinfo/info.xml b/apps/user_status/appinfo/info.xml new file mode 100644 index 00000000000..0f9b5235f7c --- /dev/null +++ b/apps/user_status/appinfo/info.xml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later + --> +<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>1.12.0</version> + <licence>agpl</licence> + <author mail="oc.list@georgehrke.com" >Georg Ehrke</author> + <namespace>UserStatus</namespace> + <category>social</category> + <bugs>https://github.com/nextcloud/server</bugs> + <navigations> + <navigation> + <id>user_status-menu-entry</id> + <name>User status</name> + <order>1</order> + <type>settings</type> + </navigation> + </navigations> + <dependencies> + <nextcloud min-version="32" max-version="32"/> + </dependencies> + <background-jobs> + <job>OCA\UserStatus\BackgroundJob\ClearOldStatusesBackgroundJob</job> + </background-jobs> + <contactsmenu> + <provider>OCA\UserStatus\ContactsMenu\StatusProvider</provider> + </contactsmenu> +</info> diff --git a/apps/user_status/composer/autoload.php b/apps/user_status/composer/autoload.php new file mode 100644 index 00000000000..afd560d3ae9 --- /dev/null +++ b/apps/user_status/composer/autoload.php @@ -0,0 +1,25 @@ +<?php + +// autoload.php @generated by Composer + +if (PHP_VERSION_ID < 50600) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, $err); + } elseif (!headers_sent()) { + echo $err; + } + } + trigger_error( + $err, + E_USER_ERROR + ); +} + +require_once __DIR__ . '/composer/autoload_real.php'; + +return ComposerAutoloaderInitUserStatus::getLoader(); diff --git a/apps/user_status/composer/composer.json b/apps/user_status/composer/composer.json new file mode 100644 index 00000000000..fbb78312088 --- /dev/null +++ b/apps/user_status/composer/composer.json @@ -0,0 +1,13 @@ +{ + "config" : { + "vendor-dir": ".", + "optimize-autoloader": true, + "classmap-authoritative": true, + "autoloader-suffix": "UserStatus" + }, + "autoload" : { + "psr-4": { + "OCA\\UserStatus\\": "../lib/" + } + } +} diff --git a/apps/user_status/composer/composer.lock b/apps/user_status/composer/composer.lock new file mode 100644 index 00000000000..fd0bcbcb753 --- /dev/null +++ b/apps/user_status/composer/composer.lock @@ -0,0 +1,18 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d751713988987e9331980363e24189ce", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.1.0" +} diff --git a/apps/user_status/composer/composer/ClassLoader.php b/apps/user_status/composer/composer/ClassLoader.php new file mode 100644 index 00000000000..7824d8f7eaf --- /dev/null +++ b/apps/user_status/composer/composer/ClassLoader.php @@ -0,0 +1,579 @@ +<?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 https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array<string, array<string, int>> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array<string, list<string>> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list<string> + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array<string, array<string, list<string>>> + */ + private $prefixesPsr0 = array(); + /** + * @var list<string> + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array<string, string> + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array<string, bool> + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array<string, self> + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array<string, list<string>> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array<string, list<string>> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list<string> + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list<string> + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array<string, string> Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array<string, string> $classMap Class to filename map + * + * @return void + */ + 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 list<string>|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $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 list<string>|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $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] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $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 list<string>|string $paths The PSR-0 base directories + * + * @return void + */ + 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 list<string>|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + 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 + * + * @return void + */ + 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 + * + * @return void + */ + 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 + * + * @return void + */ + 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 + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * 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; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array<string, self> + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + 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; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/apps/user_status/composer/composer/InstalledVersions.php b/apps/user_status/composer/composer/InstalledVersions.php new file mode 100644 index 00000000000..51e734a774b --- /dev/null +++ b/apps/user_status/composer/composer/InstalledVersions.php @@ -0,0 +1,359 @@ +<?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; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list<string> + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list<string> + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/apps/user_status/composer/composer/LICENSE b/apps/user_status/composer/composer/LICENSE new file mode 100644 index 00000000000..f27399a042d --- /dev/null +++ b/apps/user_status/composer/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/apps/user_status/composer/composer/autoload_classmap.php b/apps/user_status/composer/composer/autoload_classmap.php new file mode 100644 index 00000000000..b57df813bc9 --- /dev/null +++ b/apps/user_status/composer/composer/autoload_classmap.php @@ -0,0 +1,41 @@ +<?php + +// autoload_classmap.php @generated by Composer + +$vendorDir = dirname(__DIR__); +$baseDir = $vendorDir; + +return array( + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + '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\\Connector\\UserStatus' => $baseDir . '/../lib/Connector/UserStatus.php', + 'OCA\\UserStatus\\Connector\\UserStatusProvider' => $baseDir . '/../lib/Connector/UserStatusProvider.php', + 'OCA\\UserStatus\\ContactsMenu\\StatusProvider' => $baseDir . '/../lib/ContactsMenu/StatusProvider.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\\Dashboard\\UserStatusWidget' => $baseDir . '/../lib/Dashboard/UserStatusWidget.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\\OutOfOfficeStatusListener' => $baseDir . '/../lib/Listener/OutOfOfficeStatusListener.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\\Migration\\Version0002Date20200902144824' => $baseDir . '/../lib/Migration/Version0002Date20200902144824.php', + 'OCA\\UserStatus\\Migration\\Version1000Date20201111130204' => $baseDir . '/../lib/Migration/Version1000Date20201111130204.php', + 'OCA\\UserStatus\\Migration\\Version1003Date20210809144824' => $baseDir . '/../lib/Migration/Version1003Date20210809144824.php', + 'OCA\\UserStatus\\Migration\\Version1008Date20230921144701' => $baseDir . '/../lib/Migration/Version1008Date20230921144701.php', + 'OCA\\UserStatus\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', + 'OCA\\UserStatus\\Service\\JSDataService' => $baseDir . '/../lib/Service/JSDataService.php', + 'OCA\\UserStatus\\Service\\PredefinedStatusService' => $baseDir . '/../lib/Service/PredefinedStatusService.php', + 'OCA\\UserStatus\\Service\\StatusService' => $baseDir . '/../lib/Service/StatusService.php', +); diff --git a/apps/user_status/composer/composer/autoload_namespaces.php b/apps/user_status/composer/composer/autoload_namespaces.php new file mode 100644 index 00000000000..3f5c9296251 --- /dev/null +++ b/apps/user_status/composer/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ +<?php + +// autoload_namespaces.php @generated by Composer + +$vendorDir = dirname(__DIR__); +$baseDir = $vendorDir; + +return array( +); diff --git a/apps/user_status/composer/composer/autoload_psr4.php b/apps/user_status/composer/composer/autoload_psr4.php new file mode 100644 index 00000000000..746ed232b66 --- /dev/null +++ b/apps/user_status/composer/composer/autoload_psr4.php @@ -0,0 +1,10 @@ +<?php + +// autoload_psr4.php @generated by Composer + +$vendorDir = dirname(__DIR__); +$baseDir = $vendorDir; + +return array( + 'OCA\\UserStatus\\' => array($baseDir . '/../lib'), +); diff --git a/apps/user_status/composer/composer/autoload_real.php b/apps/user_status/composer/composer/autoload_real.php new file mode 100644 index 00000000000..205d9780930 --- /dev/null +++ b/apps/user_status/composer/composer/autoload_real.php @@ -0,0 +1,37 @@ +<?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(\dirname(__DIR__)); + spl_autoload_unregister(array('ComposerAutoloaderInitUserStatus', 'loadClassLoader')); + + require __DIR__ . '/autoload_static.php'; + call_user_func(\Composer\Autoload\ComposerStaticInitUserStatus::getInitializer($loader)); + + $loader->setClassMapAuthoritative(true); + $loader->register(true); + + return $loader; + } +} diff --git a/apps/user_status/composer/composer/autoload_static.php b/apps/user_status/composer/composer/autoload_static.php new file mode 100644 index 00000000000..7e494344490 --- /dev/null +++ b/apps/user_status/composer/composer/autoload_static.php @@ -0,0 +1,67 @@ +<?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 ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + '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\\Connector\\UserStatus' => __DIR__ . '/..' . '/../lib/Connector/UserStatus.php', + 'OCA\\UserStatus\\Connector\\UserStatusProvider' => __DIR__ . '/..' . '/../lib/Connector/UserStatusProvider.php', + 'OCA\\UserStatus\\ContactsMenu\\StatusProvider' => __DIR__ . '/..' . '/../lib/ContactsMenu/StatusProvider.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\\Dashboard\\UserStatusWidget' => __DIR__ . '/..' . '/../lib/Dashboard/UserStatusWidget.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\\OutOfOfficeStatusListener' => __DIR__ . '/..' . '/../lib/Listener/OutOfOfficeStatusListener.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\\Migration\\Version0002Date20200902144824' => __DIR__ . '/..' . '/../lib/Migration/Version0002Date20200902144824.php', + 'OCA\\UserStatus\\Migration\\Version1000Date20201111130204' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20201111130204.php', + 'OCA\\UserStatus\\Migration\\Version1003Date20210809144824' => __DIR__ . '/..' . '/../lib/Migration/Version1003Date20210809144824.php', + 'OCA\\UserStatus\\Migration\\Version1008Date20230921144701' => __DIR__ . '/..' . '/../lib/Migration/Version1008Date20230921144701.php', + 'OCA\\UserStatus\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', + 'OCA\\UserStatus\\Service\\JSDataService' => __DIR__ . '/..' . '/../lib/Service/JSDataService.php', + 'OCA\\UserStatus\\Service\\PredefinedStatusService' => __DIR__ . '/..' . '/../lib/Service/PredefinedStatusService.php', + 'OCA\\UserStatus\\Service\\StatusService' => __DIR__ . '/..' . '/../lib/Service/StatusService.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitUserStatus::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitUserStatus::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInitUserStatus::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/apps/user_status/composer/composer/installed.json b/apps/user_status/composer/composer/installed.json new file mode 100644 index 00000000000..f20a6c47c6d --- /dev/null +++ b/apps/user_status/composer/composer/installed.json @@ -0,0 +1,5 @@ +{ + "packages": [], + "dev": false, + "dev-package-names": [] +} diff --git a/apps/user_status/composer/composer/installed.php b/apps/user_status/composer/composer/installed.php new file mode 100644 index 00000000000..1a66c7f2416 --- /dev/null +++ b/apps/user_status/composer/composer/installed.php @@ -0,0 +1,23 @@ +<?php return array( + 'root' => array( + 'name' => '__root__', + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../', + 'aliases' => array(), + 'dev' => false, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/apps/user_status/css/user-status-menu.css b/apps/user_status/css/user-status-menu.css new file mode 100644 index 00000000000..5bdbdf01cb4 --- /dev/null +++ b/apps/user_status/css/user-status-menu.css @@ -0,0 +1,4 @@ +/*! + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */.icon-user-status{background-image:url("../img/app.svg")}.icon-user-status-dark{background-image:url("../img/app-dark.svg");filter:var(--background-invert-if-dark)}/*# sourceMappingURL=user-status-menu.css.map */ diff --git a/apps/user_status/css/user-status-menu.css.map b/apps/user_status/css/user-status-menu.css.map new file mode 100644 index 00000000000..d8e862f7108 --- /dev/null +++ b/apps/user_status/css/user-status-menu.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["user-status-menu.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA,GAIA,kBACC,uCAGD,uBACC,4CACA","file":"user-status-menu.css"}
\ No newline at end of file diff --git a/apps/user_status/css/user-status-menu.css.map.license b/apps/user_status/css/user-status-menu.css.map.license new file mode 100644 index 00000000000..7e235b60091 --- /dev/null +++ b/apps/user_status/css/user-status-menu.css.map.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/user_status/css/user-status-menu.scss b/apps/user_status/css/user-status-menu.scss new file mode 100644 index 00000000000..10d761e5dff --- /dev/null +++ b/apps/user_status/css/user-status-menu.scss @@ -0,0 +1,12 @@ +/*! + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +.icon-user-status { + background-image: url("../img/app.svg"); +} + +.icon-user-status-dark { + background-image: url("../img/app-dark.svg"); + filter: var(--background-invert-if-dark); +} diff --git a/apps/user_status/img/app-dark.svg b/apps/user_status/img/app-dark.svg new file mode 100644 index 00000000000..292424a68ec --- /dev/null +++ b/apps/user_status/img/app-dark.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px"><path d="M479.96-144Q340-144 242-242t-98-238q0-140 97.93-238t237.83-98q13.06 0 25.65 1 12.59 1 25.59 3-39 29-62 72t-23 92q0 85 58.5 143.5T648-446q49 0 92-23t72-62q2 13 3 25.59t1 25.65q0 139.9-98.04 237.83t-238 97.93Z"/></svg>
\ No newline at end of file diff --git a/apps/user_status/img/app.svg b/apps/user_status/img/app.svg new file mode 100644 index 00000000000..d7fdef622fd --- /dev/null +++ b/apps/user_status/img/app.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#fff"><path d="M479.96-144Q340-144 242-242t-98-238q0-140 97.93-238t237.83-98q13.06 0 25.65 1 12.59 1 25.59 3-39 29-62 72t-23 92q0 85 58.5 143.5T648-446q49 0 92-23t72-62q2 13 3 25.59t1 25.65q0 139.9-98.04 237.83t-238 97.93Z"/></svg>
\ No newline at end of file diff --git a/apps/user_status/l10n/.gitkeep b/apps/user_status/l10n/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/apps/user_status/l10n/.gitkeep diff --git a/apps/user_status/l10n/af.js b/apps/user_status/l10n/af.js new file mode 100644 index 00000000000..8a4bdf0be20 --- /dev/null +++ b/apps/user_status/l10n/af.js @@ -0,0 +1,35 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Onlangse statusse", + "No recent status changes" : "Geen onlangse statusverandering nie", + "In a meeting" : "In ’n vergadering", + "Commuting" : "In die verkeer", + "Out sick" : "Siek tuis", + "Vacationing" : "Met vakansie", + "Working remotely" : "Werk in die veld", + "User status" : "Gebruikerstatus", + "Clear status after" : "Wis status na", + "There was an error saving the status" : "Daar was ’n fout toe status bewaar is", + "There was an error clearing the status" : "Daar was ’n fout toe die status gewis is", + "Online status" : "Aanlyn status", + "Status message" : "Statusboodskap", + "Clear status message" : "Wis statusboodskap", + "Set status message" : "Stel statusboodskap", + "Don't clear" : "Moenie wis nie", + "Today" : "Vandag", + "This week" : "Vandeesweek", + "Online" : "Aanlyn", + "Away" : "Weg", + "Do not disturb" : "Moenie pla nie", + "Invisible" : "Onsigbaar", + "Offline" : "Vanlyn", + "Set status" : "Stel status", + "There was an error saving the new status" : "Daar was ’n fout toe nuwe status bewaar is", + "30 minutes" : "30 minute", + "1 hour" : "1 uur", + "4 hours" : "4 uur", + "Mute all notifications" : "Demp alle kennisgewings", + "Appear offline" : "Toon as vanlyn" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/af.json b/apps/user_status/l10n/af.json new file mode 100644 index 00000000000..0a92dd9ab7d --- /dev/null +++ b/apps/user_status/l10n/af.json @@ -0,0 +1,33 @@ +{ "translations": { + "Recent statuses" : "Onlangse statusse", + "No recent status changes" : "Geen onlangse statusverandering nie", + "In a meeting" : "In ’n vergadering", + "Commuting" : "In die verkeer", + "Out sick" : "Siek tuis", + "Vacationing" : "Met vakansie", + "Working remotely" : "Werk in die veld", + "User status" : "Gebruikerstatus", + "Clear status after" : "Wis status na", + "There was an error saving the status" : "Daar was ’n fout toe status bewaar is", + "There was an error clearing the status" : "Daar was ’n fout toe die status gewis is", + "Online status" : "Aanlyn status", + "Status message" : "Statusboodskap", + "Clear status message" : "Wis statusboodskap", + "Set status message" : "Stel statusboodskap", + "Don't clear" : "Moenie wis nie", + "Today" : "Vandag", + "This week" : "Vandeesweek", + "Online" : "Aanlyn", + "Away" : "Weg", + "Do not disturb" : "Moenie pla nie", + "Invisible" : "Onsigbaar", + "Offline" : "Vanlyn", + "Set status" : "Stel status", + "There was an error saving the new status" : "Daar was ’n fout toe nuwe status bewaar is", + "30 minutes" : "30 minute", + "1 hour" : "1 uur", + "4 hours" : "4 uur", + "Mute all notifications" : "Demp alle kennisgewings", + "Appear offline" : "Toon as vanlyn" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/ar.js b/apps/user_status/l10n/ar.js new file mode 100644 index 00000000000..9557ff74546 --- /dev/null +++ b/apps/user_status/l10n/ar.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "آخر الحالات", + "No recent status changes" : "لم يتم تغيير الحالة مؤخراً", + "In a meeting" : "في اجتماع", + "Commuting" : "تجوال", + "Out sick" : "إجازة مرضية", + "Vacationing" : "في اجازة", + "Out of office" : "خارج المكتب", + "Working remotely" : "عمل عن بعد", + "In a call" : "على الهاتف", + "User status" : "حالة العضو", + "Clear status after" : "مسح رسالة الحالة بعد", + "Emoji for your status message" : "رسم تعبيري \"إيموجي\" لرسالة الحالة الخاصة بك", + "What is your status?" : "ماهي حالتك؟", + "Predefined statuses" : "حالات مُعرّفة مُسبقاً", + "Previously set" : "سبق تعيينها", + "Reset status" : "إعادة تعيين الحالة", + "Reset status to \"{icon} {message}\"" : "إعادة تعيين الحالة إلى \"{icon} {message}\"", + "Reset status to \"{message}\"" : "إعادة تعيين الحالة إلى \"{message}\"", + "Reset status to \"{icon}\"" : "إعادة تعيين الحالة إلى \"{icon}\"", + "There was an error saving the status" : "حدث خطأ اثناء حفظ الحالة", + "There was an error clearing the status" : "حدث خطأ اثناء حذف الحالة", + "There was an error reverting the status" : "حدث خطأ أثناء استرجاع الحالة", + "Online status" : "حالة الاتصال", + "Status message" : "رسالة الحالة", + "Set absence period" : "تعيين فترة التغيّب", + "Set absence period and replacement" : "تعيين فترة التّغيُّب و البديل", + "Your status was set automatically" : "تمّ تعيين حالتك تلقائيّاً", + "Clear status message" : "حذف رسالة الحالة", + "Set status message" : "تعيين رسالة الحالة", + "Don't clear" : "غير محدد", + "Today" : "اليوم", + "This week" : "هذا الأسبوع", + "Online" : "متصل", + "Away" : "بالخارج", + "Do not disturb" : "عدم الازعاج", + "Invisible" : "عدم الظهور", + "Offline" : "غير متصل", + "Set status" : "تعيين الحالة", + "There was an error saving the new status" : "حدث خطأ اثناء حفظ الحالة الجديدة", + "30 minutes" : "30 دقيقة", + "1 hour" : "1 ساعة", + "4 hours" : "4 ساعات", + "Busy" : "مشغول", + "Mute all notifications" : "عدم إظهار جميع التنبيهات", + "Appear offline" : "الحالة غير متصل" +}, +"nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;"); diff --git a/apps/user_status/l10n/ar.json b/apps/user_status/l10n/ar.json new file mode 100644 index 00000000000..79c8c2d184c --- /dev/null +++ b/apps/user_status/l10n/ar.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "آخر الحالات", + "No recent status changes" : "لم يتم تغيير الحالة مؤخراً", + "In a meeting" : "في اجتماع", + "Commuting" : "تجوال", + "Out sick" : "إجازة مرضية", + "Vacationing" : "في اجازة", + "Out of office" : "خارج المكتب", + "Working remotely" : "عمل عن بعد", + "In a call" : "على الهاتف", + "User status" : "حالة العضو", + "Clear status after" : "مسح رسالة الحالة بعد", + "Emoji for your status message" : "رسم تعبيري \"إيموجي\" لرسالة الحالة الخاصة بك", + "What is your status?" : "ماهي حالتك؟", + "Predefined statuses" : "حالات مُعرّفة مُسبقاً", + "Previously set" : "سبق تعيينها", + "Reset status" : "إعادة تعيين الحالة", + "Reset status to \"{icon} {message}\"" : "إعادة تعيين الحالة إلى \"{icon} {message}\"", + "Reset status to \"{message}\"" : "إعادة تعيين الحالة إلى \"{message}\"", + "Reset status to \"{icon}\"" : "إعادة تعيين الحالة إلى \"{icon}\"", + "There was an error saving the status" : "حدث خطأ اثناء حفظ الحالة", + "There was an error clearing the status" : "حدث خطأ اثناء حذف الحالة", + "There was an error reverting the status" : "حدث خطأ أثناء استرجاع الحالة", + "Online status" : "حالة الاتصال", + "Status message" : "رسالة الحالة", + "Set absence period" : "تعيين فترة التغيّب", + "Set absence period and replacement" : "تعيين فترة التّغيُّب و البديل", + "Your status was set automatically" : "تمّ تعيين حالتك تلقائيّاً", + "Clear status message" : "حذف رسالة الحالة", + "Set status message" : "تعيين رسالة الحالة", + "Don't clear" : "غير محدد", + "Today" : "اليوم", + "This week" : "هذا الأسبوع", + "Online" : "متصل", + "Away" : "بالخارج", + "Do not disturb" : "عدم الازعاج", + "Invisible" : "عدم الظهور", + "Offline" : "غير متصل", + "Set status" : "تعيين الحالة", + "There was an error saving the new status" : "حدث خطأ اثناء حفظ الحالة الجديدة", + "30 minutes" : "30 دقيقة", + "1 hour" : "1 ساعة", + "4 hours" : "4 ساعات", + "Busy" : "مشغول", + "Mute all notifications" : "عدم إظهار جميع التنبيهات", + "Appear offline" : "الحالة غير متصل" +},"pluralForm" :"nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/ast.js b/apps/user_status/l10n/ast.js new file mode 100644 index 00000000000..ac09b5ac733 --- /dev/null +++ b/apps/user_status/l10n/ast.js @@ -0,0 +1,48 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Estaos de recién", + "No recent status changes" : "Nun hai nengún cambéu d'estáu recién", + "In a meeting" : "Nuna reunión", + "Commuting" : "En desplazamientu", + "Out sick" : "Non disponible por enfermedá", + "Vacationing" : "De vacaciones", + "Out of office" : "Fuera de la oficina", + "Working remotely" : "Trabayando en remoto", + "In a call" : "Nuna llamada", + "User status" : "Estáu del usuariu", + "Clear status after" : "Borrar l'estúa dempués de", + "Emoji for your status message" : "Fustaxes pa los mensaxes d'estáu", + "What is your status?" : "¿Cuál ye'l to estáu?", + "Predefined statuses" : "Estaos predefiníos", + "Previously set" : "Afitóse con anterioridá", + "Reset status" : "Reafitar l'estáu", + "Reset status to \"{icon} {message}\"" : "Reafitar l'estáu a «{icon} {message}»", + "Reset status to \"{message}\"" : "Reafitar l'estáu a «{message}»", + "Reset status to \"{icon}\"" : "Reafitar l'estáu a «{icon}»", + "There was an error saving the status" : "Hebo un error al guardar l'estáu", + "There was an error clearing the status" : "Hebo un error al borrar l'estáu", + "There was an error reverting the status" : "Hebo un error al recuperar l'estáu anterior", + "Online status" : "Estáu en llinia", + "Status message" : "Mensaxe del estáu", + "Your status was set automatically" : "L'estáu afitóse automáticamente", + "Clear status message" : "Borrar el mensaxe del estáu", + "Set status message" : "Afitar el mensaxe del estáu", + "Don't clear" : "Nun borrar", + "Today" : "Güei", + "This week" : "Esta selmana", + "Online" : "En llinia", + "Away" : "Ausente", + "Do not disturb" : "Nun molestar", + "Invisible" : "Invisible", + "Offline" : "Desconectáu", + "Set status" : "Afitar l'estáu", + "There was an error saving the new status" : "Hebo un error al guardar l'estáu nuevu", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 hores", + "Busy" : "Ocupáu", + "Mute all notifications" : "Desactivar tolos avisos", + "Appear offline" : "Apaecer desconectáu" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/ast.json b/apps/user_status/l10n/ast.json new file mode 100644 index 00000000000..0a6e2ee9681 --- /dev/null +++ b/apps/user_status/l10n/ast.json @@ -0,0 +1,46 @@ +{ "translations": { + "Recent statuses" : "Estaos de recién", + "No recent status changes" : "Nun hai nengún cambéu d'estáu recién", + "In a meeting" : "Nuna reunión", + "Commuting" : "En desplazamientu", + "Out sick" : "Non disponible por enfermedá", + "Vacationing" : "De vacaciones", + "Out of office" : "Fuera de la oficina", + "Working remotely" : "Trabayando en remoto", + "In a call" : "Nuna llamada", + "User status" : "Estáu del usuariu", + "Clear status after" : "Borrar l'estúa dempués de", + "Emoji for your status message" : "Fustaxes pa los mensaxes d'estáu", + "What is your status?" : "¿Cuál ye'l to estáu?", + "Predefined statuses" : "Estaos predefiníos", + "Previously set" : "Afitóse con anterioridá", + "Reset status" : "Reafitar l'estáu", + "Reset status to \"{icon} {message}\"" : "Reafitar l'estáu a «{icon} {message}»", + "Reset status to \"{message}\"" : "Reafitar l'estáu a «{message}»", + "Reset status to \"{icon}\"" : "Reafitar l'estáu a «{icon}»", + "There was an error saving the status" : "Hebo un error al guardar l'estáu", + "There was an error clearing the status" : "Hebo un error al borrar l'estáu", + "There was an error reverting the status" : "Hebo un error al recuperar l'estáu anterior", + "Online status" : "Estáu en llinia", + "Status message" : "Mensaxe del estáu", + "Your status was set automatically" : "L'estáu afitóse automáticamente", + "Clear status message" : "Borrar el mensaxe del estáu", + "Set status message" : "Afitar el mensaxe del estáu", + "Don't clear" : "Nun borrar", + "Today" : "Güei", + "This week" : "Esta selmana", + "Online" : "En llinia", + "Away" : "Ausente", + "Do not disturb" : "Nun molestar", + "Invisible" : "Invisible", + "Offline" : "Desconectáu", + "Set status" : "Afitar l'estáu", + "There was an error saving the new status" : "Hebo un error al guardar l'estáu nuevu", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 hores", + "Busy" : "Ocupáu", + "Mute all notifications" : "Desactivar tolos avisos", + "Appear offline" : "Apaecer desconectáu" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/bg.js b/apps/user_status/l10n/bg.js new file mode 100644 index 00000000000..e799fa1f212 --- /dev/null +++ b/apps/user_status/l10n/bg.js @@ -0,0 +1,48 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Последни състояния", + "No recent status changes" : "Няма скорошни промени в състоянието", + "In a meeting" : "В среща", + "Commuting" : "Пътувам до работа", + "Out sick" : "Болничен", + "Vacationing" : "Отпуск", + "Out of office" : "Извън офиса", + "Working remotely" : "Работа от разстояние", + "In a call" : "В обаждане", + "User status" : "Потребителско състояние", + "Clear status after" : "Изчистване на състоянието след", + "What is your status?" : "Какво е вашето състояние?", + "Previously set" : "Предишно зададени", + "Reset status" : "Възстановяване на състоянието", + "Reset status to \"{icon} {message}\"" : "Възстановяване на състоянието на „{icon} {message}“", + "Reset status to \"{message}\"" : "Възстановяване на състоянието на „{message}“", + "Reset status to \"{icon}\"" : "Възстановяване на състоянието на „{icon}“", + "There was an error saving the status" : "Възникна грешка при запазване на състоянието", + "There was an error clearing the status" : "Възникна грешка при изчистване на състоянието", + "There was an error reverting the status" : "Имаше грешка при връщане на състоянието", + "Online status" : "Състояние", + "Status message" : "Съобщение за състояние", + "Set absence period" : "Задай период на отсъствие", + "Set absence period and replacement" : "Задай период на отсъствие и заместник.", + "Your status was set automatically" : "Състоянието ви беше зададено автоматично", + "Clear status message" : "Изчисти състоянието", + "Set status message" : "Задай състояние", + "Don't clear" : "Да не се изчиства", + "Today" : "Днес", + "This week" : "Тази седмица", + "Online" : "На линия", + "Away" : "Отсъстващ", + "Do not disturb" : "Не безпокойте", + "Invisible" : "Невидим", + "Offline" : "Офлайн", + "Set status" : "Задаване на състояние", + "There was an error saving the new status" : "Възникна грешка при запазване на новото състояние", + "30 minutes" : "30 минути", + "1 hour" : "1 час", + "4 hours" : "4 чàса", + "Busy" : "Зает", + "Mute all notifications" : "Заглушаване на всички известия", + "Appear offline" : "Показване като офлайн" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/bg.json b/apps/user_status/l10n/bg.json new file mode 100644 index 00000000000..639aab4a411 --- /dev/null +++ b/apps/user_status/l10n/bg.json @@ -0,0 +1,46 @@ +{ "translations": { + "Recent statuses" : "Последни състояния", + "No recent status changes" : "Няма скорошни промени в състоянието", + "In a meeting" : "В среща", + "Commuting" : "Пътувам до работа", + "Out sick" : "Болничен", + "Vacationing" : "Отпуск", + "Out of office" : "Извън офиса", + "Working remotely" : "Работа от разстояние", + "In a call" : "В обаждане", + "User status" : "Потребителско състояние", + "Clear status after" : "Изчистване на състоянието след", + "What is your status?" : "Какво е вашето състояние?", + "Previously set" : "Предишно зададени", + "Reset status" : "Възстановяване на състоянието", + "Reset status to \"{icon} {message}\"" : "Възстановяване на състоянието на „{icon} {message}“", + "Reset status to \"{message}\"" : "Възстановяване на състоянието на „{message}“", + "Reset status to \"{icon}\"" : "Възстановяване на състоянието на „{icon}“", + "There was an error saving the status" : "Възникна грешка при запазване на състоянието", + "There was an error clearing the status" : "Възникна грешка при изчистване на състоянието", + "There was an error reverting the status" : "Имаше грешка при връщане на състоянието", + "Online status" : "Състояние", + "Status message" : "Съобщение за състояние", + "Set absence period" : "Задай период на отсъствие", + "Set absence period and replacement" : "Задай период на отсъствие и заместник.", + "Your status was set automatically" : "Състоянието ви беше зададено автоматично", + "Clear status message" : "Изчисти състоянието", + "Set status message" : "Задай състояние", + "Don't clear" : "Да не се изчиства", + "Today" : "Днес", + "This week" : "Тази седмица", + "Online" : "На линия", + "Away" : "Отсъстващ", + "Do not disturb" : "Не безпокойте", + "Invisible" : "Невидим", + "Offline" : "Офлайн", + "Set status" : "Задаване на състояние", + "There was an error saving the new status" : "Възникна грешка при запазване на новото състояние", + "30 minutes" : "30 минути", + "1 hour" : "1 час", + "4 hours" : "4 чàса", + "Busy" : "Зает", + "Mute all notifications" : "Заглушаване на всички известия", + "Appear offline" : "Показване като офлайн" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/ca.js b/apps/user_status/l10n/ca.js new file mode 100644 index 00000000000..8fa53f7f41b --- /dev/null +++ b/apps/user_status/l10n/ca.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Estats recents", + "No recent status changes" : "No hi ha cap canvi d'estat recent", + "In a meeting" : "En una reunió", + "Commuting" : "En desplaçament", + "Out sick" : "No disponible per malaltia", + "Vacationing" : "De vacances", + "Out of office" : "Fora de l'oficina", + "Working remotely" : "Treballant en remot", + "In a call" : "En una trucada", + "User status" : "Estat de l'usuari", + "Clear status after" : "Esborra l'estat després de", + "Emoji for your status message" : "Emoji per al missatge d'estat", + "What is your status?" : "Quin és el vostre estat?", + "Predefined statuses" : "Estats predefinits", + "Previously set" : "Definits anteriorment", + "Reset status" : "Reinicialitza l'estat", + "Reset status to \"{icon} {message}\"" : "Reinicialitza l'estat a «{icon} {message}»", + "Reset status to \"{message}\"" : "Reinicialitza l'estat a «{message}»", + "Reset status to \"{icon}\"" : "Reinicialitza l'estat a «{icon}»", + "There was an error saving the status" : "S'ha produït un error en desar l'estat", + "There was an error clearing the status" : "S'ha produït un error en esborrar l'estat", + "There was an error reverting the status" : "S'ha produït un error en recuperar l'estat anterior", + "Online status" : "Estat en línia", + "Status message" : "Missatge d'estat", + "Set absence period" : "Establir període d'absència", + "Set absence period and replacement" : "Establir període d'absència i substitució", + "Your status was set automatically" : "S'ha indicat l'estat automàticament", + "Clear status message" : "Esborra el missatge d'estat", + "Set status message" : "Indica el missatge d'estat", + "Don't clear" : "No l'esborris", + "Today" : "Avui", + "This week" : "Aquesta setmana", + "Online" : "En línia", + "Away" : "Absent", + "Do not disturb" : "No molesteu", + "Invisible" : "Invisible", + "Offline" : "Fora de línia", + "Set status" : "Indica l'estat", + "There was an error saving the new status" : "S'ha produït un error en desar l'estat nou", + "30 minutes" : "30 minuts", + "1 hour" : "1 hora", + "4 hours" : "4 hores", + "Busy" : "Ocupat", + "Mute all notifications" : "Silencieu totes les notificacions", + "Appear offline" : "Apareixeu fora de línia" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/ca.json b/apps/user_status/l10n/ca.json new file mode 100644 index 00000000000..c40c658b992 --- /dev/null +++ b/apps/user_status/l10n/ca.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Estats recents", + "No recent status changes" : "No hi ha cap canvi d'estat recent", + "In a meeting" : "En una reunió", + "Commuting" : "En desplaçament", + "Out sick" : "No disponible per malaltia", + "Vacationing" : "De vacances", + "Out of office" : "Fora de l'oficina", + "Working remotely" : "Treballant en remot", + "In a call" : "En una trucada", + "User status" : "Estat de l'usuari", + "Clear status after" : "Esborra l'estat després de", + "Emoji for your status message" : "Emoji per al missatge d'estat", + "What is your status?" : "Quin és el vostre estat?", + "Predefined statuses" : "Estats predefinits", + "Previously set" : "Definits anteriorment", + "Reset status" : "Reinicialitza l'estat", + "Reset status to \"{icon} {message}\"" : "Reinicialitza l'estat a «{icon} {message}»", + "Reset status to \"{message}\"" : "Reinicialitza l'estat a «{message}»", + "Reset status to \"{icon}\"" : "Reinicialitza l'estat a «{icon}»", + "There was an error saving the status" : "S'ha produït un error en desar l'estat", + "There was an error clearing the status" : "S'ha produït un error en esborrar l'estat", + "There was an error reverting the status" : "S'ha produït un error en recuperar l'estat anterior", + "Online status" : "Estat en línia", + "Status message" : "Missatge d'estat", + "Set absence period" : "Establir període d'absència", + "Set absence period and replacement" : "Establir període d'absència i substitució", + "Your status was set automatically" : "S'ha indicat l'estat automàticament", + "Clear status message" : "Esborra el missatge d'estat", + "Set status message" : "Indica el missatge d'estat", + "Don't clear" : "No l'esborris", + "Today" : "Avui", + "This week" : "Aquesta setmana", + "Online" : "En línia", + "Away" : "Absent", + "Do not disturb" : "No molesteu", + "Invisible" : "Invisible", + "Offline" : "Fora de línia", + "Set status" : "Indica l'estat", + "There was an error saving the new status" : "S'ha produït un error en desar l'estat nou", + "30 minutes" : "30 minuts", + "1 hour" : "1 hora", + "4 hours" : "4 hores", + "Busy" : "Ocupat", + "Mute all notifications" : "Silencieu totes les notificacions", + "Appear offline" : "Apareixeu fora de línia" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/cs.js b/apps/user_status/l10n/cs.js new file mode 100644 index 00000000000..9d21e89f539 --- /dev/null +++ b/apps/user_status/l10n/cs.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Nedávné stavy", + "No recent status changes" : "Žádné nedávné změny stavu", + "In a meeting" : "Na poradě", + "Commuting" : "Dojíždění", + "Out sick" : "Nemoc", + "Vacationing" : "Dovolená", + "Out of office" : "Mimo kancelář", + "Working remotely" : "Pracuje na dálku", + "In a call" : "Má hovor", + "User status" : "Stav uživatele", + "Clear status after" : "Vyčistit stav po uplynutí", + "Emoji for your status message" : "Emotikona pro vaší stavovou zprávu", + "What is your status?" : "Jaký je váš stav?", + "Predefined statuses" : "Předdefinované stavy", + "Previously set" : "Dříve nastavené", + "Reset status" : "Resetovat stav", + "Reset status to \"{icon} {message}\"" : "Resetovat stav na „{icon} {message}“", + "Reset status to \"{message}\"" : "Resetovat stav na „{message}“", + "Reset status to \"{icon}\"" : "Resetovat stav na „{icon}“", + "There was an error saving the status" : "Došlo k chybě při ukládání stavu", + "There was an error clearing the status" : "Při čištění stavu došlo k chybě", + "There was an error reverting the status" : "Při vracení stavu nazpět došlo k chybě", + "Online status" : "Stav online", + "Status message" : "Stavová zpráva", + "Set absence period" : "Nastavit období nepřítomnosti", + "Set absence period and replacement" : "Nastavit období nepřítomnosti a zástup", + "Your status was set automatically" : "Váš stav byl nastaven automaticky", + "Clear status message" : "Vyčistit stavovou zprávu", + "Set status message" : "Nastavit stavovou zprávu", + "Don't clear" : "Do odvolání", + "Today" : "Dnes", + "This week" : "Tento týden", + "Online" : "Online", + "Away" : "Pryč", + "Do not disturb" : "Nerušit", + "Invisible" : "Není vidět", + "Offline" : "Offline", + "Set status" : "Nastavit stav", + "There was an error saving the new status" : "Při ukládání nového stavu došlo k chybě", + "30 minutes" : "30 minut", + "1 hour" : "1 hodina", + "4 hours" : "4 hodiny", + "Busy" : "Zaneprázdněn(a)", + "Mute all notifications" : "Ztlumit veškerá upozornění", + "Appear offline" : "Jevit se offline" +}, +"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;"); diff --git a/apps/user_status/l10n/cs.json b/apps/user_status/l10n/cs.json new file mode 100644 index 00000000000..c6c875d3454 --- /dev/null +++ b/apps/user_status/l10n/cs.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Nedávné stavy", + "No recent status changes" : "Žádné nedávné změny stavu", + "In a meeting" : "Na poradě", + "Commuting" : "Dojíždění", + "Out sick" : "Nemoc", + "Vacationing" : "Dovolená", + "Out of office" : "Mimo kancelář", + "Working remotely" : "Pracuje na dálku", + "In a call" : "Má hovor", + "User status" : "Stav uživatele", + "Clear status after" : "Vyčistit stav po uplynutí", + "Emoji for your status message" : "Emotikona pro vaší stavovou zprávu", + "What is your status?" : "Jaký je váš stav?", + "Predefined statuses" : "Předdefinované stavy", + "Previously set" : "Dříve nastavené", + "Reset status" : "Resetovat stav", + "Reset status to \"{icon} {message}\"" : "Resetovat stav na „{icon} {message}“", + "Reset status to \"{message}\"" : "Resetovat stav na „{message}“", + "Reset status to \"{icon}\"" : "Resetovat stav na „{icon}“", + "There was an error saving the status" : "Došlo k chybě při ukládání stavu", + "There was an error clearing the status" : "Při čištění stavu došlo k chybě", + "There was an error reverting the status" : "Při vracení stavu nazpět došlo k chybě", + "Online status" : "Stav online", + "Status message" : "Stavová zpráva", + "Set absence period" : "Nastavit období nepřítomnosti", + "Set absence period and replacement" : "Nastavit období nepřítomnosti a zástup", + "Your status was set automatically" : "Váš stav byl nastaven automaticky", + "Clear status message" : "Vyčistit stavovou zprávu", + "Set status message" : "Nastavit stavovou zprávu", + "Don't clear" : "Do odvolání", + "Today" : "Dnes", + "This week" : "Tento týden", + "Online" : "Online", + "Away" : "Pryč", + "Do not disturb" : "Nerušit", + "Invisible" : "Není vidět", + "Offline" : "Offline", + "Set status" : "Nastavit stav", + "There was an error saving the new status" : "Při ukládání nového stavu došlo k chybě", + "30 minutes" : "30 minut", + "1 hour" : "1 hodina", + "4 hours" : "4 hodiny", + "Busy" : "Zaneprázdněn(a)", + "Mute all notifications" : "Ztlumit veškerá upozornění", + "Appear offline" : "Jevit se offline" +},"pluralForm" :"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/da.js b/apps/user_status/l10n/da.js new file mode 100644 index 00000000000..43b4560d086 --- /dev/null +++ b/apps/user_status/l10n/da.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Senestestatus", + "No recent status changes" : "Ingen ændringer i status", + "In a meeting" : "I et møde", + "Commuting" : "Undervejs", + "Out sick" : "Sygemeldt", + "Vacationing" : "Holder ferie", + "Out of office" : "Ikke på kontoret", + "Working remotely" : "Arbejder hjemmefra", + "In a call" : "Taler i telefon", + "User status" : "Brugerstatus", + "Clear status after" : "Ryd status efter", + "Emoji for your status message" : "Emoji til din statusbesked", + "What is your status?" : "Hvad er din status", + "Predefined statuses" : "Foruddefinerede status", + "Previously set" : "Tidligere sat", + "Reset status" : "Nulstil status", + "Reset status to \"{icon} {message}\"" : "Nulstil status til \"{icon}{message}\"", + "Reset status to \"{message}\"" : "Nulstil status til \"{message}\"", + "Reset status to \"{icon}\"" : "Nulstil status til \"{icon}\"", + "There was an error saving the status" : "Der opstod en fejl ved lagring af status", + "There was an error clearing the status" : "Der opstod en fejl ved rydning af status", + "There was an error reverting the status" : "Der opstod en fejl ved indstillingen af status", + "Online status" : "Online status", + "Status message" : "Statusbesked", + "Set absence period" : "Indstil fraværs periode", + "Set absence period and replacement" : "Indstil fraværs periode og angiv afløser", + "Your status was set automatically" : "Status sat automatisk", + "Clear status message" : "Ryd statusnotifikation", + "Set status message" : "Sæt statusbesked", + "Don't clear" : "Ryd ikke", + "Today" : "I dag", + "This week" : "Denne uge", + "Online" : "Online", + "Away" : "Ikke tilstede", + "Do not disturb" : "Forstyr ikke", + "Invisible" : "Usynlig", + "Offline" : "Offline", + "Set status" : "Sæt status", + "There was an error saving the new status" : "Der opstod en fejl ved lagring af den nye status", + "30 minutes" : "30 minutter", + "1 hour" : "1 time", + "4 hours" : "4 timer", + "Busy" : "Optaget", + "Mute all notifications" : "Vis ikke notifikationer", + "Appear offline" : "Er offline" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/da.json b/apps/user_status/l10n/da.json new file mode 100644 index 00000000000..67708311d7b --- /dev/null +++ b/apps/user_status/l10n/da.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Senestestatus", + "No recent status changes" : "Ingen ændringer i status", + "In a meeting" : "I et møde", + "Commuting" : "Undervejs", + "Out sick" : "Sygemeldt", + "Vacationing" : "Holder ferie", + "Out of office" : "Ikke på kontoret", + "Working remotely" : "Arbejder hjemmefra", + "In a call" : "Taler i telefon", + "User status" : "Brugerstatus", + "Clear status after" : "Ryd status efter", + "Emoji for your status message" : "Emoji til din statusbesked", + "What is your status?" : "Hvad er din status", + "Predefined statuses" : "Foruddefinerede status", + "Previously set" : "Tidligere sat", + "Reset status" : "Nulstil status", + "Reset status to \"{icon} {message}\"" : "Nulstil status til \"{icon}{message}\"", + "Reset status to \"{message}\"" : "Nulstil status til \"{message}\"", + "Reset status to \"{icon}\"" : "Nulstil status til \"{icon}\"", + "There was an error saving the status" : "Der opstod en fejl ved lagring af status", + "There was an error clearing the status" : "Der opstod en fejl ved rydning af status", + "There was an error reverting the status" : "Der opstod en fejl ved indstillingen af status", + "Online status" : "Online status", + "Status message" : "Statusbesked", + "Set absence period" : "Indstil fraværs periode", + "Set absence period and replacement" : "Indstil fraværs periode og angiv afløser", + "Your status was set automatically" : "Status sat automatisk", + "Clear status message" : "Ryd statusnotifikation", + "Set status message" : "Sæt statusbesked", + "Don't clear" : "Ryd ikke", + "Today" : "I dag", + "This week" : "Denne uge", + "Online" : "Online", + "Away" : "Ikke tilstede", + "Do not disturb" : "Forstyr ikke", + "Invisible" : "Usynlig", + "Offline" : "Offline", + "Set status" : "Sæt status", + "There was an error saving the new status" : "Der opstod en fejl ved lagring af den nye status", + "30 minutes" : "30 minutter", + "1 hour" : "1 time", + "4 hours" : "4 timer", + "Busy" : "Optaget", + "Mute all notifications" : "Vis ikke notifikationer", + "Appear offline" : "Er offline" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/de.js b/apps/user_status/l10n/de.js new file mode 100644 index 00000000000..b59906d6135 --- /dev/null +++ b/apps/user_status/l10n/de.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Letzter Status", + "No recent status changes" : "Keine kürzlichen Statusänderungen", + "In a meeting" : "In einer Besprechung", + "Commuting" : "Pendelt", + "Out sick" : "Krankgeschrieben", + "Vacationing" : "Im Urlaub", + "Out of office" : "Nicht im Büro", + "Working remotely" : "Arbeitet aus der Ferne", + "In a call" : "In einem Anruf", + "Be right back" : "Bin gleich zurück", + "User status" : "Benutzerstatus", + "Clear status after" : "Status löschen nach", + "Emoji for your status message" : "Emoji für deine Statusnachricht", + "What is your status?" : "Wie ist dein Status?", + "Predefined statuses" : "Vordefinierte Status", + "Previously set" : "Zuvor eingestellt", + "Reset status" : "Status zurücksetzen", + "Reset status to \"{icon} {message}\"" : "Status auf \"{icon} {message}\" zurücksetzen", + "Reset status to \"{message}\"" : "Status auf \"{message}\" zurücksetzen", + "Reset status to \"{icon}\"" : "Status auf \"{icon}\" zurücksetzen", + "There was an error saving the status" : "Es gab einen Fehler beim Speichern des Status", + "There was an error clearing the status" : "Es gab einen Fehler beim Löschen des Status", + "There was an error reverting the status" : "Es ist ein Fehler beim Zurücksetzen des Status aufgetreten", + "Online status" : "Online-Status", + "Status message" : "Statusnachricht", + "Set absence period" : "Abwesenheitszeitraum festlegen", + "Set absence period and replacement" : "Abwesenheitszeitraum und Vertretung festlegen", + "Your status was set automatically" : "Dein Status wurde automatisch gesetzt", + "Clear status message" : "Statusnachricht löschen", + "Set status message" : "Statusnachricht setzen", + "Don't clear" : "Nicht löschen", + "Today" : "Heute", + "This week" : "Diese Woche", + "Online" : "Online", + "Away" : "Abwesend", + "Do not disturb" : "Bitte nicht stören", + "Invisible" : "Unsichtbar", + "Offline" : "Offline", + "Set status" : "Status setzen", + "There was an error saving the new status" : "Es gab einen Fehler beim Speichern des neuen Status", + "30 minutes" : "30 Minuten", + "1 hour" : "1 Stunde", + "4 hours" : "4 Stunden", + "Busy" : "Beschäftigt", + "Mute all notifications" : "Alle Benachrichtigungen stummschalten", + "Appear offline" : "Offline erscheinen" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/de.json b/apps/user_status/l10n/de.json new file mode 100644 index 00000000000..2badd82476c --- /dev/null +++ b/apps/user_status/l10n/de.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "Letzter Status", + "No recent status changes" : "Keine kürzlichen Statusänderungen", + "In a meeting" : "In einer Besprechung", + "Commuting" : "Pendelt", + "Out sick" : "Krankgeschrieben", + "Vacationing" : "Im Urlaub", + "Out of office" : "Nicht im Büro", + "Working remotely" : "Arbeitet aus der Ferne", + "In a call" : "In einem Anruf", + "Be right back" : "Bin gleich zurück", + "User status" : "Benutzerstatus", + "Clear status after" : "Status löschen nach", + "Emoji for your status message" : "Emoji für deine Statusnachricht", + "What is your status?" : "Wie ist dein Status?", + "Predefined statuses" : "Vordefinierte Status", + "Previously set" : "Zuvor eingestellt", + "Reset status" : "Status zurücksetzen", + "Reset status to \"{icon} {message}\"" : "Status auf \"{icon} {message}\" zurücksetzen", + "Reset status to \"{message}\"" : "Status auf \"{message}\" zurücksetzen", + "Reset status to \"{icon}\"" : "Status auf \"{icon}\" zurücksetzen", + "There was an error saving the status" : "Es gab einen Fehler beim Speichern des Status", + "There was an error clearing the status" : "Es gab einen Fehler beim Löschen des Status", + "There was an error reverting the status" : "Es ist ein Fehler beim Zurücksetzen des Status aufgetreten", + "Online status" : "Online-Status", + "Status message" : "Statusnachricht", + "Set absence period" : "Abwesenheitszeitraum festlegen", + "Set absence period and replacement" : "Abwesenheitszeitraum und Vertretung festlegen", + "Your status was set automatically" : "Dein Status wurde automatisch gesetzt", + "Clear status message" : "Statusnachricht löschen", + "Set status message" : "Statusnachricht setzen", + "Don't clear" : "Nicht löschen", + "Today" : "Heute", + "This week" : "Diese Woche", + "Online" : "Online", + "Away" : "Abwesend", + "Do not disturb" : "Bitte nicht stören", + "Invisible" : "Unsichtbar", + "Offline" : "Offline", + "Set status" : "Status setzen", + "There was an error saving the new status" : "Es gab einen Fehler beim Speichern des neuen Status", + "30 minutes" : "30 Minuten", + "1 hour" : "1 Stunde", + "4 hours" : "4 Stunden", + "Busy" : "Beschäftigt", + "Mute all notifications" : "Alle Benachrichtigungen stummschalten", + "Appear offline" : "Offline erscheinen" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/de_DE.js b/apps/user_status/l10n/de_DE.js new file mode 100644 index 00000000000..23d86b06643 --- /dev/null +++ b/apps/user_status/l10n/de_DE.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Letzter Status", + "No recent status changes" : "Keine kürzlichen Statusänderungen", + "In a meeting" : "In einer Besprechung", + "Commuting" : "Pendelt", + "Out sick" : "Krank geschrieben", + "Vacationing" : "Im Urlaub", + "Out of office" : "Nicht im Büro", + "Working remotely" : "Arbeitet aus der Ferne", + "In a call" : "In einem Anruf", + "Be right back" : "Bin gleich zurück", + "User status" : "Benutzerstatus", + "Clear status after" : "Status löschen nach", + "Emoji for your status message" : "Emoji für Ihre Statusnachricht", + "What is your status?" : "Wie ist Ihr Status?", + "Predefined statuses" : "Vordefinierte Status", + "Previously set" : "Zuvor eingestellt", + "Reset status" : "Status zurücksetzen", + "Reset status to \"{icon} {message}\"" : "Status auf \"{icon} {message}\" zurücksetzen", + "Reset status to \"{message}\"" : "Status auf \"{message}\" zurücksetzen", + "Reset status to \"{icon}\"" : "Status auf \"{icon}\" zurücksetzen", + "There was an error saving the status" : "Es gab einen Fehler beim Speichern des Status", + "There was an error clearing the status" : "Es gab einen Fehler beim Löschen des Status", + "There was an error reverting the status" : "Es ist ein Fehler beim Zurücksetzen des Status aufgetreten", + "Online status" : "Online-Status", + "Status message" : "Statusnachricht", + "Set absence period" : "Abwesenheitszeitraum festlegen", + "Set absence period and replacement" : "Abwesenheitszeitraum und Vertretung festlegen", + "Your status was set automatically" : "Ihr Status wurde automatisch gesetzt", + "Clear status message" : "Statusnachricht löschen", + "Set status message" : "Statusnachricht setzen", + "Don't clear" : "Nicht löschen", + "Today" : "Heute", + "This week" : "Diese Woche", + "Online" : "Online", + "Away" : "Abwesend", + "Do not disturb" : "Nicht stören", + "Invisible" : "Unsichtbar", + "Offline" : "Offline", + "Set status" : "Status setzen", + "There was an error saving the new status" : "Es gab einen Fehler beim Speichern des neuen Status", + "30 minutes" : "30 Minuten", + "1 hour" : "1 Stunde", + "4 hours" : "4 Stunden", + "Busy" : "Beschäftigt", + "Mute all notifications" : "Alle Benachrichtigungen stummschalten", + "Appear offline" : "Offline erscheinen" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/de_DE.json b/apps/user_status/l10n/de_DE.json new file mode 100644 index 00000000000..9be300f0e29 --- /dev/null +++ b/apps/user_status/l10n/de_DE.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "Letzter Status", + "No recent status changes" : "Keine kürzlichen Statusänderungen", + "In a meeting" : "In einer Besprechung", + "Commuting" : "Pendelt", + "Out sick" : "Krank geschrieben", + "Vacationing" : "Im Urlaub", + "Out of office" : "Nicht im Büro", + "Working remotely" : "Arbeitet aus der Ferne", + "In a call" : "In einem Anruf", + "Be right back" : "Bin gleich zurück", + "User status" : "Benutzerstatus", + "Clear status after" : "Status löschen nach", + "Emoji for your status message" : "Emoji für Ihre Statusnachricht", + "What is your status?" : "Wie ist Ihr Status?", + "Predefined statuses" : "Vordefinierte Status", + "Previously set" : "Zuvor eingestellt", + "Reset status" : "Status zurücksetzen", + "Reset status to \"{icon} {message}\"" : "Status auf \"{icon} {message}\" zurücksetzen", + "Reset status to \"{message}\"" : "Status auf \"{message}\" zurücksetzen", + "Reset status to \"{icon}\"" : "Status auf \"{icon}\" zurücksetzen", + "There was an error saving the status" : "Es gab einen Fehler beim Speichern des Status", + "There was an error clearing the status" : "Es gab einen Fehler beim Löschen des Status", + "There was an error reverting the status" : "Es ist ein Fehler beim Zurücksetzen des Status aufgetreten", + "Online status" : "Online-Status", + "Status message" : "Statusnachricht", + "Set absence period" : "Abwesenheitszeitraum festlegen", + "Set absence period and replacement" : "Abwesenheitszeitraum und Vertretung festlegen", + "Your status was set automatically" : "Ihr Status wurde automatisch gesetzt", + "Clear status message" : "Statusnachricht löschen", + "Set status message" : "Statusnachricht setzen", + "Don't clear" : "Nicht löschen", + "Today" : "Heute", + "This week" : "Diese Woche", + "Online" : "Online", + "Away" : "Abwesend", + "Do not disturb" : "Nicht stören", + "Invisible" : "Unsichtbar", + "Offline" : "Offline", + "Set status" : "Status setzen", + "There was an error saving the new status" : "Es gab einen Fehler beim Speichern des neuen Status", + "30 minutes" : "30 Minuten", + "1 hour" : "1 Stunde", + "4 hours" : "4 Stunden", + "Busy" : "Beschäftigt", + "Mute all notifications" : "Alle Benachrichtigungen stummschalten", + "Appear offline" : "Offline erscheinen" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/el.js b/apps/user_status/l10n/el.js new file mode 100644 index 00000000000..0818d00d791 --- /dev/null +++ b/apps/user_status/l10n/el.js @@ -0,0 +1,39 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Πρόσφατες καταστάσεις", + "No recent status changes" : "Δεν υπάρχουν πρόσφατες αλλαγές κατάστασης", + "In a meeting" : "Σε συνάντηση", + "Commuting" : "Μετακίνηση προς την εργασία", + "Out sick" : "Αναρρωτική άδεια", + "Vacationing" : "Διακοπάρω", + "Out of office" : "Εκτός γραφείου", + "Working remotely" : "Εργασία εξ αποστάσεως", + "In a call" : "Σε μια κλήση", + "User status" : "Κατάσταση χρήστη", + "Clear status after" : "Εκκαθάριση κατάστασης μετά από", + "What is your status?" : "Ποια είναι η κατάστασή σας;", + "There was an error saving the status" : "Παρουσιάστηκε σφάλμα κατά την αποθήκευση της κατάστασης", + "There was an error clearing the status" : "Παρουσιάστηκε σφάλμα κατά την εκκαθάριση της κατάστασης", + "Online status" : "Κατάσταση σε σύνδεση", + "Status message" : "Μήνυμα κατάστασης", + "Clear status message" : "Εκκαθάριση μηνύματος κατάστασης", + "Set status message" : "Ορισμός μηνύματος κατάστασης", + "Don't clear" : "Να μη γίνεται εκκαθάριση", + "Today" : "Σήμερα", + "This week" : "Αυτή την εβδομάδα", + "Online" : "Σε σύνδεση", + "Away" : "Λείπω", + "Do not disturb" : "Μην ενοχλείτε", + "Invisible" : "Αόρατος", + "Offline" : "Εκτός σύνδεσης", + "Set status" : "Ορισμός κατάστασης", + "There was an error saving the new status" : "Παρουσιάστηκε σφάλμα κατά την αποθήκευση της νέας κατάστασης", + "30 minutes" : "30 λεπτά", + "1 hour" : "1 ώρα", + "4 hours" : "4 ώρες", + "Busy" : "Απασχολημένος", + "Mute all notifications" : "Σίγαση όλων των ειδοποιήσεων", + "Appear offline" : "Εμφάνιση εκτός σύνδεσης" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/el.json b/apps/user_status/l10n/el.json new file mode 100644 index 00000000000..bef6486de08 --- /dev/null +++ b/apps/user_status/l10n/el.json @@ -0,0 +1,37 @@ +{ "translations": { + "Recent statuses" : "Πρόσφατες καταστάσεις", + "No recent status changes" : "Δεν υπάρχουν πρόσφατες αλλαγές κατάστασης", + "In a meeting" : "Σε συνάντηση", + "Commuting" : "Μετακίνηση προς την εργασία", + "Out sick" : "Αναρρωτική άδεια", + "Vacationing" : "Διακοπάρω", + "Out of office" : "Εκτός γραφείου", + "Working remotely" : "Εργασία εξ αποστάσεως", + "In a call" : "Σε μια κλήση", + "User status" : "Κατάσταση χρήστη", + "Clear status after" : "Εκκαθάριση κατάστασης μετά από", + "What is your status?" : "Ποια είναι η κατάστασή σας;", + "There was an error saving the status" : "Παρουσιάστηκε σφάλμα κατά την αποθήκευση της κατάστασης", + "There was an error clearing the status" : "Παρουσιάστηκε σφάλμα κατά την εκκαθάριση της κατάστασης", + "Online status" : "Κατάσταση σε σύνδεση", + "Status message" : "Μήνυμα κατάστασης", + "Clear status message" : "Εκκαθάριση μηνύματος κατάστασης", + "Set status message" : "Ορισμός μηνύματος κατάστασης", + "Don't clear" : "Να μη γίνεται εκκαθάριση", + "Today" : "Σήμερα", + "This week" : "Αυτή την εβδομάδα", + "Online" : "Σε σύνδεση", + "Away" : "Λείπω", + "Do not disturb" : "Μην ενοχλείτε", + "Invisible" : "Αόρατος", + "Offline" : "Εκτός σύνδεσης", + "Set status" : "Ορισμός κατάστασης", + "There was an error saving the new status" : "Παρουσιάστηκε σφάλμα κατά την αποθήκευση της νέας κατάστασης", + "30 minutes" : "30 λεπτά", + "1 hour" : "1 ώρα", + "4 hours" : "4 ώρες", + "Busy" : "Απασχολημένος", + "Mute all notifications" : "Σίγαση όλων των ειδοποιήσεων", + "Appear offline" : "Εμφάνιση εκτός σύνδεσης" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/en_GB.js b/apps/user_status/l10n/en_GB.js new file mode 100644 index 00000000000..2ef82ebed8a --- /dev/null +++ b/apps/user_status/l10n/en_GB.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Recent statuses", + "No recent status changes" : "No recent status changes", + "In a meeting" : "In a meeting", + "Commuting" : "Commuting", + "Out sick" : "Out sick", + "Vacationing" : "Vacationing", + "Out of office" : "Out of office", + "Working remotely" : "Working remotely", + "In a call" : "In a call", + "Be right back" : "Be right back", + "User status" : "User status", + "Clear status after" : "Clear status after", + "Emoji for your status message" : "Emoji for your status message", + "What is your status?" : "What is your status?", + "Predefined statuses" : "Predefined statuses", + "Previously set" : "Previously set", + "Reset status" : "Reset status", + "Reset status to \"{icon} {message}\"" : "Reset status to \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Reset status to \"{message}\"", + "Reset status to \"{icon}\"" : "Reset status to \"{icon}\"", + "There was an error saving the status" : "There was an error saving the status", + "There was an error clearing the status" : "There was an error clearing the status", + "There was an error reverting the status" : "There was an error reverting the status", + "Online status" : "Online status", + "Status message" : "Status message", + "Set absence period" : "Set absence period", + "Set absence period and replacement" : "Set absence period and replacement", + "Your status was set automatically" : "Your status was set automatically", + "Clear status message" : "Clear status message", + "Set status message" : "Set status message", + "Don't clear" : "Don't clear", + "Today" : "Today", + "This week" : "This week", + "Online" : "Online", + "Away" : "Away", + "Do not disturb" : "Do not disturb", + "Invisible" : "Invisible", + "Offline" : "Offline", + "Set status" : "Set status", + "There was an error saving the new status" : "There was an error saving the new status", + "30 minutes" : "30 minutes", + "1 hour" : "1 hour", + "4 hours" : "4 hours", + "Busy" : "Busy", + "Mute all notifications" : "Mute all notifications", + "Appear offline" : "Appear offline" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/en_GB.json b/apps/user_status/l10n/en_GB.json new file mode 100644 index 00000000000..0e646a02599 --- /dev/null +++ b/apps/user_status/l10n/en_GB.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "Recent statuses", + "No recent status changes" : "No recent status changes", + "In a meeting" : "In a meeting", + "Commuting" : "Commuting", + "Out sick" : "Out sick", + "Vacationing" : "Vacationing", + "Out of office" : "Out of office", + "Working remotely" : "Working remotely", + "In a call" : "In a call", + "Be right back" : "Be right back", + "User status" : "User status", + "Clear status after" : "Clear status after", + "Emoji for your status message" : "Emoji for your status message", + "What is your status?" : "What is your status?", + "Predefined statuses" : "Predefined statuses", + "Previously set" : "Previously set", + "Reset status" : "Reset status", + "Reset status to \"{icon} {message}\"" : "Reset status to \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Reset status to \"{message}\"", + "Reset status to \"{icon}\"" : "Reset status to \"{icon}\"", + "There was an error saving the status" : "There was an error saving the status", + "There was an error clearing the status" : "There was an error clearing the status", + "There was an error reverting the status" : "There was an error reverting the status", + "Online status" : "Online status", + "Status message" : "Status message", + "Set absence period" : "Set absence period", + "Set absence period and replacement" : "Set absence period and replacement", + "Your status was set automatically" : "Your status was set automatically", + "Clear status message" : "Clear status message", + "Set status message" : "Set status message", + "Don't clear" : "Don't clear", + "Today" : "Today", + "This week" : "This week", + "Online" : "Online", + "Away" : "Away", + "Do not disturb" : "Do not disturb", + "Invisible" : "Invisible", + "Offline" : "Offline", + "Set status" : "Set status", + "There was an error saving the new status" : "There was an error saving the new status", + "30 minutes" : "30 minutes", + "1 hour" : "1 hour", + "4 hours" : "4 hours", + "Busy" : "Busy", + "Mute all notifications" : "Mute all notifications", + "Appear offline" : "Appear offline" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/es.js b/apps/user_status/l10n/es.js new file mode 100644 index 00000000000..952bd011543 --- /dev/null +++ b/apps/user_status/l10n/es.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Estados recientes", + "No recent status changes" : "No hay cambios de estado recientes", + "In a meeting" : "En una reunión", + "Commuting" : "De viaje", + "Out sick" : "Ausente por enfermedad", + "Vacationing" : "De vacaciones", + "Out of office" : "Fuera de la oficina", + "Working remotely" : "Teletrabajando", + "In a call" : "En una llamada", + "Be right back" : "Vuelvo ahora mismo", + "User status" : "Estado del usuario", + "Clear status after" : "Eliminar el estado después de", + "Emoji for your status message" : "Emoji para sus mensaje de estado", + "What is your status?" : "¿Cuál es su estado?", + "Predefined statuses" : "Estados predefinidos", + "Previously set" : "Previamente definido", + "Reset status" : "Re-inicializar estado", + "Reset status to \"{icon} {message}\"" : "Re-inicializar estado a \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Re-inicializar estado a \"{message}\"", + "Reset status to \"{icon}\"" : "Re-inicializar estado a \"{icon}\"", + "There was an error saving the status" : "Ha habido un error al guardar el estado", + "There was an error clearing the status" : "Ha habido un error al eliminar el estado", + "There was an error reverting the status" : "Ocurrió un error al revertir el estado", + "Online status" : "Estado en línea", + "Status message" : "Mensaje de estado", + "Set absence period" : "Establecer período de ausencia", + "Set absence period and replacement" : "Establecer período de ausencia y el sustituto", + "Your status was set automatically" : "Su estado fue definido automáticamente", + "Clear status message" : "Borrar mensaje de estado", + "Set status message" : "Ajustar el mensaje de estado", + "Don't clear" : "No eliminar", + "Today" : "Hoy", + "This week" : "Esta semana", + "Online" : "En línea", + "Away" : "Ausente", + "Do not disturb" : "No molestar", + "Invisible" : "Invisible", + "Offline" : "Sin conexión", + "Set status" : "Configurar estado", + "There was an error saving the new status" : "Ha habido un error al guardar el nuevo estado", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 horas", + "Busy" : "Ocupado", + "Mute all notifications" : "Silenciar todas las notificaciones", + "Appear offline" : "Aparecer sin conexión" +}, +"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/user_status/l10n/es.json b/apps/user_status/l10n/es.json new file mode 100644 index 00000000000..93299df5b27 --- /dev/null +++ b/apps/user_status/l10n/es.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "Estados recientes", + "No recent status changes" : "No hay cambios de estado recientes", + "In a meeting" : "En una reunión", + "Commuting" : "De viaje", + "Out sick" : "Ausente por enfermedad", + "Vacationing" : "De vacaciones", + "Out of office" : "Fuera de la oficina", + "Working remotely" : "Teletrabajando", + "In a call" : "En una llamada", + "Be right back" : "Vuelvo ahora mismo", + "User status" : "Estado del usuario", + "Clear status after" : "Eliminar el estado después de", + "Emoji for your status message" : "Emoji para sus mensaje de estado", + "What is your status?" : "¿Cuál es su estado?", + "Predefined statuses" : "Estados predefinidos", + "Previously set" : "Previamente definido", + "Reset status" : "Re-inicializar estado", + "Reset status to \"{icon} {message}\"" : "Re-inicializar estado a \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Re-inicializar estado a \"{message}\"", + "Reset status to \"{icon}\"" : "Re-inicializar estado a \"{icon}\"", + "There was an error saving the status" : "Ha habido un error al guardar el estado", + "There was an error clearing the status" : "Ha habido un error al eliminar el estado", + "There was an error reverting the status" : "Ocurrió un error al revertir el estado", + "Online status" : "Estado en línea", + "Status message" : "Mensaje de estado", + "Set absence period" : "Establecer período de ausencia", + "Set absence period and replacement" : "Establecer período de ausencia y el sustituto", + "Your status was set automatically" : "Su estado fue definido automáticamente", + "Clear status message" : "Borrar mensaje de estado", + "Set status message" : "Ajustar el mensaje de estado", + "Don't clear" : "No eliminar", + "Today" : "Hoy", + "This week" : "Esta semana", + "Online" : "En línea", + "Away" : "Ausente", + "Do not disturb" : "No molestar", + "Invisible" : "Invisible", + "Offline" : "Sin conexión", + "Set status" : "Configurar estado", + "There was an error saving the new status" : "Ha habido un error al guardar el nuevo estado", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 horas", + "Busy" : "Ocupado", + "Mute all notifications" : "Silenciar todas las notificaciones", + "Appear offline" : "Aparecer sin conexión" +},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/es_EC.js b/apps/user_status/l10n/es_EC.js new file mode 100644 index 00000000000..6500afa7c5a --- /dev/null +++ b/apps/user_status/l10n/es_EC.js @@ -0,0 +1,48 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Estados recientes", + "No recent status changes" : "No hay cambios recientes de estado", + "In a meeting" : "En una reunión", + "Commuting" : "Desplazamiento", + "Out sick" : "Ausente por enfermedad", + "Vacationing" : "De vacaciones", + "Out of office" : "Fuera de la oficina", + "Working remotely" : "Trabajando de forma remota", + "In a call" : "En una llamada", + "User status" : "Estado de usuario", + "Clear status after" : "Borrar estado después de", + "Emoji for your status message" : "Emoji para tu mensaje de estado", + "What is your status?" : "¿Cuál es tu estado?", + "Predefined statuses" : "Estados predefinidos", + "Previously set" : "Previamente establecido", + "Reset status" : "Restablecer estado", + "Reset status to \"{icon} {message}\"" : "Restablecer estado a \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Restablecer estado a \"{message}\"", + "Reset status to \"{icon}\"" : "Restablecer estado a \"{icon}\"", + "There was an error saving the status" : "Hubo un error al guardar el estado", + "There was an error clearing the status" : "Hubo un error al borrar el estado", + "There was an error reverting the status" : "Hubo un error al revertir el estado", + "Online status" : "Estado en línea", + "Status message" : "Mensaje de estado", + "Your status was set automatically" : "Tu estado se estableció automáticamente", + "Clear status message" : "Borrar mensaje de estado", + "Set status message" : "Establecer mensaje de estado", + "Don't clear" : "No borrar", + "Today" : "Hoy", + "This week" : "Esta semana", + "Online" : "En línea", + "Away" : "Ausente", + "Do not disturb" : "No molestar", + "Invisible" : "Invisible", + "Offline" : "Sin conexión", + "Set status" : "Establecer estado", + "There was an error saving the new status" : "Hubo un error al guardar el nuevo estado", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 horas", + "Busy" : "Ocupado", + "Mute all notifications" : "Silenciar todas las notificaciones", + "Appear offline" : "Aparecer como desconectado" +}, +"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/user_status/l10n/es_EC.json b/apps/user_status/l10n/es_EC.json new file mode 100644 index 00000000000..4d6f3df54ec --- /dev/null +++ b/apps/user_status/l10n/es_EC.json @@ -0,0 +1,46 @@ +{ "translations": { + "Recent statuses" : "Estados recientes", + "No recent status changes" : "No hay cambios recientes de estado", + "In a meeting" : "En una reunión", + "Commuting" : "Desplazamiento", + "Out sick" : "Ausente por enfermedad", + "Vacationing" : "De vacaciones", + "Out of office" : "Fuera de la oficina", + "Working remotely" : "Trabajando de forma remota", + "In a call" : "En una llamada", + "User status" : "Estado de usuario", + "Clear status after" : "Borrar estado después de", + "Emoji for your status message" : "Emoji para tu mensaje de estado", + "What is your status?" : "¿Cuál es tu estado?", + "Predefined statuses" : "Estados predefinidos", + "Previously set" : "Previamente establecido", + "Reset status" : "Restablecer estado", + "Reset status to \"{icon} {message}\"" : "Restablecer estado a \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Restablecer estado a \"{message}\"", + "Reset status to \"{icon}\"" : "Restablecer estado a \"{icon}\"", + "There was an error saving the status" : "Hubo un error al guardar el estado", + "There was an error clearing the status" : "Hubo un error al borrar el estado", + "There was an error reverting the status" : "Hubo un error al revertir el estado", + "Online status" : "Estado en línea", + "Status message" : "Mensaje de estado", + "Your status was set automatically" : "Tu estado se estableció automáticamente", + "Clear status message" : "Borrar mensaje de estado", + "Set status message" : "Establecer mensaje de estado", + "Don't clear" : "No borrar", + "Today" : "Hoy", + "This week" : "Esta semana", + "Online" : "En línea", + "Away" : "Ausente", + "Do not disturb" : "No molestar", + "Invisible" : "Invisible", + "Offline" : "Sin conexión", + "Set status" : "Establecer estado", + "There was an error saving the new status" : "Hubo un error al guardar el nuevo estado", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 horas", + "Busy" : "Ocupado", + "Mute all notifications" : "Silenciar todas las notificaciones", + "Appear offline" : "Aparecer como desconectado" +},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/es_MX.js b/apps/user_status/l10n/es_MX.js new file mode 100644 index 00000000000..1b5aae7f116 --- /dev/null +++ b/apps/user_status/l10n/es_MX.js @@ -0,0 +1,48 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Estados recientes", + "No recent status changes" : "No hay cambios recientes de estado", + "In a meeting" : "En una reunión", + "Commuting" : "Trasladándose", + "Out sick" : "Enfermo", + "Vacationing" : "De vacaciones", + "Out of office" : "Fuera de la oficina", + "Working remotely" : "Trabajando remotamente", + "In a call" : "En una llamada", + "User status" : "Estado de usuario", + "Clear status after" : "Limpiar el estado después de", + "Emoji for your status message" : "Emoji para su mensaje de estado", + "What is your status?" : "¿Cuál es su estado?", + "Predefined statuses" : "Estados predefinidos", + "Previously set" : "Previamente establecido", + "Reset status" : "Restablecer estado", + "Reset status to \"{icon} {message}\"" : "Restablecer estado a \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Restablecer estado a \"{message}\"", + "Reset status to \"{icon}\"" : "Restablecer estado a \"{icon}\"", + "There was an error saving the status" : "Hubo un error al guardar el estado", + "There was an error clearing the status" : "Hubo un error al limpiar el estado", + "There was an error reverting the status" : "Hubo un error al revertir el estado", + "Online status" : "Estado en línea", + "Status message" : "Mensaje de estado", + "Your status was set automatically" : "Su estado se estableció automáticamente", + "Clear status message" : "Borrar mensaje de estado", + "Set status message" : "Establecer mensaje de estado", + "Don't clear" : "No borrar", + "Today" : "Hoy", + "This week" : "Esta semana", + "Online" : "En línea", + "Away" : "Ausente", + "Do not disturb" : "No molestar", + "Invisible" : "Invisible", + "Offline" : "Sin conexión", + "Set status" : "Establecer estado", + "There was an error saving the new status" : "Hubo un error al guardar el nuevo estado", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 horas", + "Busy" : "Ocupado", + "Mute all notifications" : "Silenciar todas las notificaciones", + "Appear offline" : "Aparecer como desconectado" +}, +"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/user_status/l10n/es_MX.json b/apps/user_status/l10n/es_MX.json new file mode 100644 index 00000000000..26f418adc7f --- /dev/null +++ b/apps/user_status/l10n/es_MX.json @@ -0,0 +1,46 @@ +{ "translations": { + "Recent statuses" : "Estados recientes", + "No recent status changes" : "No hay cambios recientes de estado", + "In a meeting" : "En una reunión", + "Commuting" : "Trasladándose", + "Out sick" : "Enfermo", + "Vacationing" : "De vacaciones", + "Out of office" : "Fuera de la oficina", + "Working remotely" : "Trabajando remotamente", + "In a call" : "En una llamada", + "User status" : "Estado de usuario", + "Clear status after" : "Limpiar el estado después de", + "Emoji for your status message" : "Emoji para su mensaje de estado", + "What is your status?" : "¿Cuál es su estado?", + "Predefined statuses" : "Estados predefinidos", + "Previously set" : "Previamente establecido", + "Reset status" : "Restablecer estado", + "Reset status to \"{icon} {message}\"" : "Restablecer estado a \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Restablecer estado a \"{message}\"", + "Reset status to \"{icon}\"" : "Restablecer estado a \"{icon}\"", + "There was an error saving the status" : "Hubo un error al guardar el estado", + "There was an error clearing the status" : "Hubo un error al limpiar el estado", + "There was an error reverting the status" : "Hubo un error al revertir el estado", + "Online status" : "Estado en línea", + "Status message" : "Mensaje de estado", + "Your status was set automatically" : "Su estado se estableció automáticamente", + "Clear status message" : "Borrar mensaje de estado", + "Set status message" : "Establecer mensaje de estado", + "Don't clear" : "No borrar", + "Today" : "Hoy", + "This week" : "Esta semana", + "Online" : "En línea", + "Away" : "Ausente", + "Do not disturb" : "No molestar", + "Invisible" : "Invisible", + "Offline" : "Sin conexión", + "Set status" : "Establecer estado", + "There was an error saving the new status" : "Hubo un error al guardar el nuevo estado", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 horas", + "Busy" : "Ocupado", + "Mute all notifications" : "Silenciar todas las notificaciones", + "Appear offline" : "Aparecer como desconectado" +},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/et_EE.js b/apps/user_status/l10n/et_EE.js new file mode 100644 index 00000000000..79d1c125875 --- /dev/null +++ b/apps/user_status/l10n/et_EE.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Hiljutised olekud", + "No recent status changes" : "Pole hiljutisi olekumuudatusi", + "In a meeting" : "Koosolekul", + "Commuting" : "Sõidus", + "Out sick" : "Haige", + "Vacationing" : "Puhkusel", + "Out of office" : "Kontorist väljas", + "Working remotely" : "Kaugtööl", + "In a call" : "Kõnes", + "Be right back" : "Kohe jõuan tagasi", + "User status" : "Kasutaja olek", + "Clear status after" : "Eemalda olekuteade peale", + "Emoji for your status message" : "Sinu olekuteate emoji", + "What is your status?" : "Mis on su olek?", + "Predefined statuses" : "Eeldefineeritud olekud", + "Previously set" : "Varasemalt seatud", + "Reset status" : "Lähesta olek", + "Reset status to \"{icon} {message}\"" : "Lähesta olek „{icon} {message}“-ks", + "Reset status to \"{message}\"" : "Lähesta olek „{message}“-ks", + "Reset status to \"{icon}\"" : "Lähesta olek „{icon}“-ks", + "There was an error saving the status" : "Oleku salvestamisel tekkis viga", + "There was an error clearing the status" : "Oleku eemaldamisel tekkis viga", + "There was an error reverting the status" : "Oleku taastamisel tekkis viga", + "Online status" : "Olek võrgus", + "Status message" : "Olekuteade", + "Set absence period" : "Määra eemaloleku periood", + "Set absence period and replacement" : "Määra eemaloleku periood ja asendaja", + "Your status was set automatically" : "Su olek määrati automaatselt", + "Clear status message" : "Eemalda olekuteade", + "Set status message" : "Lisa olekusõnum", + "Don't clear" : "Ära kustuta", + "Today" : "Täna", + "This week" : "Käesoleval nädalal", + "Online" : "Võrgus", + "Away" : "Eemal", + "Do not disturb" : "Ära sega", + "Invisible" : "Nähtamatu", + "Offline" : "Pole võrgus", + "Set status" : "Määra olek", + "There was an error saving the new status" : "Uue oleku salvestamisel esines viga", + "30 minutes" : "30 minutit", + "1 hour" : "1 tundi", + "4 hours" : "4 tundi", + "Busy" : "Hõivatud", + "Mute all notifications" : "Sellega summutad teavitused", + "Appear offline" : "Sellega paistad olema võrgust väljas" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/et_EE.json b/apps/user_status/l10n/et_EE.json new file mode 100644 index 00000000000..38c0880aa79 --- /dev/null +++ b/apps/user_status/l10n/et_EE.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "Hiljutised olekud", + "No recent status changes" : "Pole hiljutisi olekumuudatusi", + "In a meeting" : "Koosolekul", + "Commuting" : "Sõidus", + "Out sick" : "Haige", + "Vacationing" : "Puhkusel", + "Out of office" : "Kontorist väljas", + "Working remotely" : "Kaugtööl", + "In a call" : "Kõnes", + "Be right back" : "Kohe jõuan tagasi", + "User status" : "Kasutaja olek", + "Clear status after" : "Eemalda olekuteade peale", + "Emoji for your status message" : "Sinu olekuteate emoji", + "What is your status?" : "Mis on su olek?", + "Predefined statuses" : "Eeldefineeritud olekud", + "Previously set" : "Varasemalt seatud", + "Reset status" : "Lähesta olek", + "Reset status to \"{icon} {message}\"" : "Lähesta olek „{icon} {message}“-ks", + "Reset status to \"{message}\"" : "Lähesta olek „{message}“-ks", + "Reset status to \"{icon}\"" : "Lähesta olek „{icon}“-ks", + "There was an error saving the status" : "Oleku salvestamisel tekkis viga", + "There was an error clearing the status" : "Oleku eemaldamisel tekkis viga", + "There was an error reverting the status" : "Oleku taastamisel tekkis viga", + "Online status" : "Olek võrgus", + "Status message" : "Olekuteade", + "Set absence period" : "Määra eemaloleku periood", + "Set absence period and replacement" : "Määra eemaloleku periood ja asendaja", + "Your status was set automatically" : "Su olek määrati automaatselt", + "Clear status message" : "Eemalda olekuteade", + "Set status message" : "Lisa olekusõnum", + "Don't clear" : "Ära kustuta", + "Today" : "Täna", + "This week" : "Käesoleval nädalal", + "Online" : "Võrgus", + "Away" : "Eemal", + "Do not disturb" : "Ära sega", + "Invisible" : "Nähtamatu", + "Offline" : "Pole võrgus", + "Set status" : "Määra olek", + "There was an error saving the new status" : "Uue oleku salvestamisel esines viga", + "30 minutes" : "30 minutit", + "1 hour" : "1 tundi", + "4 hours" : "4 tundi", + "Busy" : "Hõivatud", + "Mute all notifications" : "Sellega summutad teavitused", + "Appear offline" : "Sellega paistad olema võrgust väljas" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/eu.js b/apps/user_status/l10n/eu.js new file mode 100644 index 00000000000..b10e38ec09e --- /dev/null +++ b/apps/user_status/l10n/eu.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Azken egoerak", + "No recent status changes" : "Azken egoera-aldaketarik ez", + "In a meeting" : "Bilera batean", + "Commuting" : "Lanerako bidean", + "Out sick" : "Gaixorik", + "Vacationing" : "Oporretan", + "Out of office" : "Bulegotik kanpo", + "Working remotely" : "Urrutitik lanean", + "In a call" : "Dei batean", + "User status" : "Erabiltzaile-egoera", + "Clear status after" : "Garbitu egoera honen ondoren", + "Emoji for your status message" : "Zure egoera-mezurako emojia", + "What is your status?" : "Zein da zure egoera?", + "Predefined statuses" : "Aurrez definitutako egoerak", + "Previously set" : "Lehendik ezarrita", + "Reset status" : "Berrezarri egoera", + "Reset status to \"{icon} {message}\"" : "Berrezarri egoera \"{icon} {message}\"(e)ra", + "Reset status to \"{message}\"" : "Berrezarri egoera \"{message}\"(e)ra", + "Reset status to \"{icon}\"" : "Berrezarri egoera \"{icon}\"(e)ra", + "There was an error saving the status" : "Errore bat gertatu da egoera gordetzean", + "There was an error clearing the status" : "Errore bat gertatu da egoera garbitzean", + "There was an error reverting the status" : "Errore bat gertatu da egoera berrezartzean", + "Online status" : "Lineako egoera", + "Status message" : "Egoera-mezua", + "Set absence period" : "Ezarri absentzia aldia", + "Set absence period and replacement" : "Ezarri absentzia aldia eta ordezkoa", + "Your status was set automatically" : "Zure egoera automatikoki ezarriko dira", + "Clear status message" : "Garbitu egoera-mezua", + "Set status message" : "Ezarri egoera-mezua", + "Don't clear" : "Ez garbitu", + "Today" : "Gaur", + "This week" : "Aste honetan", + "Online" : "Linean", + "Away" : "Kanpoan", + "Do not disturb" : "Ez molestatu", + "Invisible" : "Ikusezina", + "Offline" : "Lineaz kanpo", + "Set status" : "Ezarri egoera", + "There was an error saving the new status" : "Errore bat gertatu da egoera berria gordetzean", + "30 minutes" : "30 minutu", + "1 hour" : "Ordu 1", + "4 hours" : "4 ordu", + "Busy" : "Lanpetua", + "Mute all notifications" : "Mututu jakinarazpen guztiak", + "Appear offline" : "Lineaz kanpo agertu" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/eu.json b/apps/user_status/l10n/eu.json new file mode 100644 index 00000000000..3362581d9f5 --- /dev/null +++ b/apps/user_status/l10n/eu.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Azken egoerak", + "No recent status changes" : "Azken egoera-aldaketarik ez", + "In a meeting" : "Bilera batean", + "Commuting" : "Lanerako bidean", + "Out sick" : "Gaixorik", + "Vacationing" : "Oporretan", + "Out of office" : "Bulegotik kanpo", + "Working remotely" : "Urrutitik lanean", + "In a call" : "Dei batean", + "User status" : "Erabiltzaile-egoera", + "Clear status after" : "Garbitu egoera honen ondoren", + "Emoji for your status message" : "Zure egoera-mezurako emojia", + "What is your status?" : "Zein da zure egoera?", + "Predefined statuses" : "Aurrez definitutako egoerak", + "Previously set" : "Lehendik ezarrita", + "Reset status" : "Berrezarri egoera", + "Reset status to \"{icon} {message}\"" : "Berrezarri egoera \"{icon} {message}\"(e)ra", + "Reset status to \"{message}\"" : "Berrezarri egoera \"{message}\"(e)ra", + "Reset status to \"{icon}\"" : "Berrezarri egoera \"{icon}\"(e)ra", + "There was an error saving the status" : "Errore bat gertatu da egoera gordetzean", + "There was an error clearing the status" : "Errore bat gertatu da egoera garbitzean", + "There was an error reverting the status" : "Errore bat gertatu da egoera berrezartzean", + "Online status" : "Lineako egoera", + "Status message" : "Egoera-mezua", + "Set absence period" : "Ezarri absentzia aldia", + "Set absence period and replacement" : "Ezarri absentzia aldia eta ordezkoa", + "Your status was set automatically" : "Zure egoera automatikoki ezarriko dira", + "Clear status message" : "Garbitu egoera-mezua", + "Set status message" : "Ezarri egoera-mezua", + "Don't clear" : "Ez garbitu", + "Today" : "Gaur", + "This week" : "Aste honetan", + "Online" : "Linean", + "Away" : "Kanpoan", + "Do not disturb" : "Ez molestatu", + "Invisible" : "Ikusezina", + "Offline" : "Lineaz kanpo", + "Set status" : "Ezarri egoera", + "There was an error saving the new status" : "Errore bat gertatu da egoera berria gordetzean", + "30 minutes" : "30 minutu", + "1 hour" : "Ordu 1", + "4 hours" : "4 ordu", + "Busy" : "Lanpetua", + "Mute all notifications" : "Mututu jakinarazpen guztiak", + "Appear offline" : "Lineaz kanpo agertu" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/fa.js b/apps/user_status/l10n/fa.js new file mode 100644 index 00000000000..e3e600eb696 --- /dev/null +++ b/apps/user_status/l10n/fa.js @@ -0,0 +1,48 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "وضعیت های اخیر", + "No recent status changes" : "هیچ تغییر وضعیت جدیدی وجود ندارد", + "In a meeting" : "در جلسه", + "Commuting" : "در رفت و آمد", + "Out sick" : "مرخصی استعلاجی", + "Vacationing" : "تعطیلات", + "Out of office" : "بیرون از دفتر", + "Working remotely" : "دورکاری", + "In a call" : "در حال تماس تلفنی", + "User status" : "وضعبت کاربر", + "Clear status after" : "پاک کردن وضعیت بعدی", + "Emoji for your status message" : "Emoji for your status message", + "What is your status?" : "وضعیت شما چیست؟", + "Predefined statuses" : "Predefined statuses", + "Previously set" : "Previously set", + "Reset status" : "Reset status", + "Reset status to \"{icon} {message}\"" : "Reset status to \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Reset status to \"{message}\"", + "Reset status to \"{icon}\"" : "Reset status to \"{icon}\"", + "There was an error saving the status" : "مشکلی در ذخیره سازی وضعیت پیش آمده", + "There was an error clearing the status" : "مشکلی در پاک کردن وضعیت پیش آمده", + "There was an error reverting the status" : "There was an error reverting the status", + "Online status" : "وضعیت آنلاین", + "Status message" : "پیغام وضعیت", + "Your status was set automatically" : "Your status was set automatically", + "Clear status message" : "پیام وضعیت را پاک کن", + "Set status message" : "تنظیم پیام وضعیت", + "Don't clear" : "پاک نکن", + "Today" : "امروز", + "This week" : "این هفته", + "Online" : "آنلاین", + "Away" : "بیرون", + "Do not disturb" : "مزاحم نشوید", + "Invisible" : "غیر قابل مشاهده", + "Offline" : "آفلاین", + "Set status" : "تنظیم وضعیت", + "There was an error saving the new status" : "مشکلی در ذخیره سازی وضعیت جدید پیش آمده", + "30 minutes" : "۳۰ دقیقه", + "1 hour" : "۱ ساعت", + "4 hours" : "۴ ساعت", + "Busy" : "مشغول", + "Mute all notifications" : "خاموش کردن همه اعلانات", + "Appear offline" : "نمایش آفلاین" +}, +"nplurals=2; plural=(n > 1);"); diff --git a/apps/user_status/l10n/fa.json b/apps/user_status/l10n/fa.json new file mode 100644 index 00000000000..ab997e0a8a7 --- /dev/null +++ b/apps/user_status/l10n/fa.json @@ -0,0 +1,46 @@ +{ "translations": { + "Recent statuses" : "وضعیت های اخیر", + "No recent status changes" : "هیچ تغییر وضعیت جدیدی وجود ندارد", + "In a meeting" : "در جلسه", + "Commuting" : "در رفت و آمد", + "Out sick" : "مرخصی استعلاجی", + "Vacationing" : "تعطیلات", + "Out of office" : "بیرون از دفتر", + "Working remotely" : "دورکاری", + "In a call" : "در حال تماس تلفنی", + "User status" : "وضعبت کاربر", + "Clear status after" : "پاک کردن وضعیت بعدی", + "Emoji for your status message" : "Emoji for your status message", + "What is your status?" : "وضعیت شما چیست؟", + "Predefined statuses" : "Predefined statuses", + "Previously set" : "Previously set", + "Reset status" : "Reset status", + "Reset status to \"{icon} {message}\"" : "Reset status to \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Reset status to \"{message}\"", + "Reset status to \"{icon}\"" : "Reset status to \"{icon}\"", + "There was an error saving the status" : "مشکلی در ذخیره سازی وضعیت پیش آمده", + "There was an error clearing the status" : "مشکلی در پاک کردن وضعیت پیش آمده", + "There was an error reverting the status" : "There was an error reverting the status", + "Online status" : "وضعیت آنلاین", + "Status message" : "پیغام وضعیت", + "Your status was set automatically" : "Your status was set automatically", + "Clear status message" : "پیام وضعیت را پاک کن", + "Set status message" : "تنظیم پیام وضعیت", + "Don't clear" : "پاک نکن", + "Today" : "امروز", + "This week" : "این هفته", + "Online" : "آنلاین", + "Away" : "بیرون", + "Do not disturb" : "مزاحم نشوید", + "Invisible" : "غیر قابل مشاهده", + "Offline" : "آفلاین", + "Set status" : "تنظیم وضعیت", + "There was an error saving the new status" : "مشکلی در ذخیره سازی وضعیت جدید پیش آمده", + "30 minutes" : "۳۰ دقیقه", + "1 hour" : "۱ ساعت", + "4 hours" : "۴ ساعت", + "Busy" : "مشغول", + "Mute all notifications" : "خاموش کردن همه اعلانات", + "Appear offline" : "نمایش آفلاین" +},"pluralForm" :"nplurals=2; plural=(n > 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/fi.js b/apps/user_status/l10n/fi.js new file mode 100644 index 00000000000..db936384ff9 --- /dev/null +++ b/apps/user_status/l10n/fi.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Viimeisimmät tilatiedot", + "No recent status changes" : "Ei viimeisimpiä tilatietomuutoksia", + "In a meeting" : "Tapaamisessa", + "Commuting" : "Työmatkalla", + "Out sick" : "Sairaana", + "Vacationing" : "Lomailemassa", + "Out of office" : "Poissa työpaikalta", + "Working remotely" : "Etätyössä", + "In a call" : "Puhelussa", + "User status" : "Käyttäjän tilatieto", + "Clear status after" : "Tyhjennä tilatieto", + "Emoji for your status message" : "Emoji tilaviestiisi", + "What is your status?" : "Mikä on tilatietosi?", + "Predefined statuses" : "Ennalta määritellyt tilatiedot", + "Previously set" : "Aiemmin asetettu", + "Reset status" : "Palauta tilatieto", + "Reset status to \"{icon} {message}\"" : "Palauta tilatiedoksi \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Palauta tilatiedoksi \"{message}\"", + "Reset status to \"{icon}\"" : "Palauta tilatiedoksi \"{icon}\"", + "There was an error saving the status" : "Tilatiedon tallentamisessa tapahtui virhe", + "There was an error clearing the status" : "Tilatietoa tyhjentäessä tapahtui virhe", + "There was an error reverting the status" : "Tilatietoa palauttaessa tapahtui virhe", + "Online status" : "Online-tila", + "Status message" : "Tilaviesti", + "Set absence period" : "Aseta poissaoloaika", + "Set absence period and replacement" : "Aseta poissaoloaika ja sijainen", + "Your status was set automatically" : "Tilatietosi asetettiin automaattisesti", + "Clear status message" : "Tyhjennä tilaviesti", + "Set status message" : "Aseta tilaviesti", + "Don't clear" : "Älä tyhjennä", + "Today" : "Tänään", + "This week" : "Tällä viikolla", + "Online" : "Paikalla", + "Away" : "Poissa", + "Do not disturb" : "Älä häiritse", + "Invisible" : "Näkymätön", + "Offline" : "Poissa", + "Set status" : "Aseta tilatieto", + "There was an error saving the new status" : "Uuden tilatiedon tallentamisessa tapahtui virhe", + "30 minutes" : "30 minuuttia", + "1 hour" : "1 tunti", + "4 hours" : "4 tuntia", + "Busy" : "Varattu", + "Mute all notifications" : "Mykistä kaikki ilmoitukset", + "Appear offline" : "Näytä olevan poissa" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/fi.json b/apps/user_status/l10n/fi.json new file mode 100644 index 00000000000..5a7ad4fa685 --- /dev/null +++ b/apps/user_status/l10n/fi.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Viimeisimmät tilatiedot", + "No recent status changes" : "Ei viimeisimpiä tilatietomuutoksia", + "In a meeting" : "Tapaamisessa", + "Commuting" : "Työmatkalla", + "Out sick" : "Sairaana", + "Vacationing" : "Lomailemassa", + "Out of office" : "Poissa työpaikalta", + "Working remotely" : "Etätyössä", + "In a call" : "Puhelussa", + "User status" : "Käyttäjän tilatieto", + "Clear status after" : "Tyhjennä tilatieto", + "Emoji for your status message" : "Emoji tilaviestiisi", + "What is your status?" : "Mikä on tilatietosi?", + "Predefined statuses" : "Ennalta määritellyt tilatiedot", + "Previously set" : "Aiemmin asetettu", + "Reset status" : "Palauta tilatieto", + "Reset status to \"{icon} {message}\"" : "Palauta tilatiedoksi \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Palauta tilatiedoksi \"{message}\"", + "Reset status to \"{icon}\"" : "Palauta tilatiedoksi \"{icon}\"", + "There was an error saving the status" : "Tilatiedon tallentamisessa tapahtui virhe", + "There was an error clearing the status" : "Tilatietoa tyhjentäessä tapahtui virhe", + "There was an error reverting the status" : "Tilatietoa palauttaessa tapahtui virhe", + "Online status" : "Online-tila", + "Status message" : "Tilaviesti", + "Set absence period" : "Aseta poissaoloaika", + "Set absence period and replacement" : "Aseta poissaoloaika ja sijainen", + "Your status was set automatically" : "Tilatietosi asetettiin automaattisesti", + "Clear status message" : "Tyhjennä tilaviesti", + "Set status message" : "Aseta tilaviesti", + "Don't clear" : "Älä tyhjennä", + "Today" : "Tänään", + "This week" : "Tällä viikolla", + "Online" : "Paikalla", + "Away" : "Poissa", + "Do not disturb" : "Älä häiritse", + "Invisible" : "Näkymätön", + "Offline" : "Poissa", + "Set status" : "Aseta tilatieto", + "There was an error saving the new status" : "Uuden tilatiedon tallentamisessa tapahtui virhe", + "30 minutes" : "30 minuuttia", + "1 hour" : "1 tunti", + "4 hours" : "4 tuntia", + "Busy" : "Varattu", + "Mute all notifications" : "Mykistä kaikki ilmoitukset", + "Appear offline" : "Näytä olevan poissa" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/fr.js b/apps/user_status/l10n/fr.js new file mode 100644 index 00000000000..a00b780a33d --- /dev/null +++ b/apps/user_status/l10n/fr.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Statuts récents", + "No recent status changes" : "Aucun changement de statut récent", + "In a meeting" : "En réunion", + "Commuting" : "Trajet", + "Out sick" : "En congé de maladie", + "Vacationing" : "En vacances", + "Out of office" : "Absent du bureau", + "Working remotely" : "Travail à distance", + "In a call" : "En communication", + "User status" : "Statut utilisateur", + "Clear status after" : "Effacer l'état après", + "Emoji for your status message" : "Emoji pour votre message de statut", + "What is your status?" : "Quel est votre statut ?", + "Predefined statuses" : "Statuts prédéfinis", + "Previously set" : "Précédemment défini", + "Reset status" : "Réinitialiser l'état", + "Reset status to \"{icon} {message}\"" : "Réinitialiser l'état en \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Réinitialiser l'état en \"{message}\"", + "Reset status to \"{icon}\"" : "Réinitialiser l'état en \"{icon}\"", + "There was an error saving the status" : "Une erreur s'est produite lors de l'enregistrement de l'état", + "There was an error clearing the status" : "Une erreur s'est produite lors de l'effacement de l'état", + "There was an error reverting the status" : "Une erreur est survenue dans le rétablissement d'état", + "Online status" : "Statut en ligne", + "Status message" : "Message d'état", + "Set absence period" : "Définir une période d'absence", + "Set absence period and replacement" : "Définir une période d'absence et un remplaçant", + "Your status was set automatically" : "Votre état a été automatiquement défini", + "Clear status message" : "Effacer le message d'état", + "Set status message" : "Enregistrer le message d'état", + "Don't clear" : "Ne pas effacer", + "Today" : "Aujourd'hui", + "This week" : "Cette semaine", + "Online" : "En ligne", + "Away" : "Absent(e)", + "Do not disturb" : "Ne pas déranger", + "Invisible" : "Invisible", + "Offline" : "Hors-ligne", + "Set status" : "Définir le statut", + "There was an error saving the new status" : "Une erreur s'est produite lors de l'enregistrement du nouveau statut", + "30 minutes" : "30 minutes", + "1 hour" : "1 heure", + "4 hours" : "4 heures", + "Busy" : "Occupé", + "Mute all notifications" : "Désactiver les notifications", + "Appear offline" : "Apparaitre hors-ligne" +}, +"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/user_status/l10n/fr.json b/apps/user_status/l10n/fr.json new file mode 100644 index 00000000000..98c873a2060 --- /dev/null +++ b/apps/user_status/l10n/fr.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Statuts récents", + "No recent status changes" : "Aucun changement de statut récent", + "In a meeting" : "En réunion", + "Commuting" : "Trajet", + "Out sick" : "En congé de maladie", + "Vacationing" : "En vacances", + "Out of office" : "Absent du bureau", + "Working remotely" : "Travail à distance", + "In a call" : "En communication", + "User status" : "Statut utilisateur", + "Clear status after" : "Effacer l'état après", + "Emoji for your status message" : "Emoji pour votre message de statut", + "What is your status?" : "Quel est votre statut ?", + "Predefined statuses" : "Statuts prédéfinis", + "Previously set" : "Précédemment défini", + "Reset status" : "Réinitialiser l'état", + "Reset status to \"{icon} {message}\"" : "Réinitialiser l'état en \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Réinitialiser l'état en \"{message}\"", + "Reset status to \"{icon}\"" : "Réinitialiser l'état en \"{icon}\"", + "There was an error saving the status" : "Une erreur s'est produite lors de l'enregistrement de l'état", + "There was an error clearing the status" : "Une erreur s'est produite lors de l'effacement de l'état", + "There was an error reverting the status" : "Une erreur est survenue dans le rétablissement d'état", + "Online status" : "Statut en ligne", + "Status message" : "Message d'état", + "Set absence period" : "Définir une période d'absence", + "Set absence period and replacement" : "Définir une période d'absence et un remplaçant", + "Your status was set automatically" : "Votre état a été automatiquement défini", + "Clear status message" : "Effacer le message d'état", + "Set status message" : "Enregistrer le message d'état", + "Don't clear" : "Ne pas effacer", + "Today" : "Aujourd'hui", + "This week" : "Cette semaine", + "Online" : "En ligne", + "Away" : "Absent(e)", + "Do not disturb" : "Ne pas déranger", + "Invisible" : "Invisible", + "Offline" : "Hors-ligne", + "Set status" : "Définir le statut", + "There was an error saving the new status" : "Une erreur s'est produite lors de l'enregistrement du nouveau statut", + "30 minutes" : "30 minutes", + "1 hour" : "1 heure", + "4 hours" : "4 heures", + "Busy" : "Occupé", + "Mute all notifications" : "Désactiver les notifications", + "Appear offline" : "Apparaitre hors-ligne" +},"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/ga.js b/apps/user_status/l10n/ga.js new file mode 100644 index 00000000000..d976c272537 --- /dev/null +++ b/apps/user_status/l10n/ga.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Stádais le déanaí", + "No recent status changes" : "Níl aon athrú stádais le déanaí", + "In a meeting" : "I gcruinniú", + "Commuting" : "Comaitéireacht", + "Out sick" : "Amach tinn", + "Vacationing" : "Laethanta saoire", + "Out of office" : "As oifig", + "Working remotely" : "Ag obair go cianda", + "In a call" : "I nglao", + "Be right back" : "Ar ais láithreach", + "User status" : "Stádas úsáideora", + "Clear status after" : "Stádas soiléir tar éis", + "Emoji for your status message" : "Emoji do do theachtaireacht stádais", + "What is your status?" : "Cad é do stádas?", + "Predefined statuses" : "Stádais réamhshainithe", + "Previously set" : "Socraíodh roimhe seo", + "Reset status" : "Stádas a athshocrú", + "Reset status to \"{icon} {message}\"" : "Athshocraigh stádas go \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Athshocraigh stádas go \"{message}\"", + "Reset status to \"{icon}\"" : "Athshocraigh stádas go \"{icon}\"", + "There was an error saving the status" : "Tharla earráid agus an stádas á shábháil", + "There was an error clearing the status" : "Tharla earráid agus an stádas á ghlanadh", + "There was an error reverting the status" : "Tharla earráid agus an stádas á chur ar ais", + "Online status" : "Stádas ar líne", + "Status message" : "Teachtaireacht stádais", + "Set absence period" : "Socraigh tréimhse neamhláithreachta", + "Set absence period and replacement" : "Socraigh tréimhse neamhláithreachta agus athsholáthar", + "Your status was set automatically" : "Socraíodh do stádas go huathoibríoch", + "Clear status message" : "Glan teachtaireacht stádais", + "Set status message" : "Socraigh teachtaireacht stádais", + "Don't clear" : "Ná soiléir", + "Today" : "Inniu", + "This week" : "An tseachtain seo", + "Online" : "Ar líne", + "Away" : "Amach", + "Do not disturb" : "Ná cur as", + "Invisible" : "Dofheicthe", + "Offline" : "As líne", + "Set status" : "Socraigh stádas", + "There was an error saving the new status" : "Tharla earráid agus an stádas nua á shábháil", + "30 minutes" : "30 nóiméad", + "1 hour" : "1 uair", + "4 hours" : "4 uair an chloig", + "Busy" : "Gnóthach", + "Mute all notifications" : "Balbhaigh gach fógra", + "Appear offline" : "Le feiceáil as líne" +}, +"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);"); diff --git a/apps/user_status/l10n/ga.json b/apps/user_status/l10n/ga.json new file mode 100644 index 00000000000..c672231aab3 --- /dev/null +++ b/apps/user_status/l10n/ga.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "Stádais le déanaí", + "No recent status changes" : "Níl aon athrú stádais le déanaí", + "In a meeting" : "I gcruinniú", + "Commuting" : "Comaitéireacht", + "Out sick" : "Amach tinn", + "Vacationing" : "Laethanta saoire", + "Out of office" : "As oifig", + "Working remotely" : "Ag obair go cianda", + "In a call" : "I nglao", + "Be right back" : "Ar ais láithreach", + "User status" : "Stádas úsáideora", + "Clear status after" : "Stádas soiléir tar éis", + "Emoji for your status message" : "Emoji do do theachtaireacht stádais", + "What is your status?" : "Cad é do stádas?", + "Predefined statuses" : "Stádais réamhshainithe", + "Previously set" : "Socraíodh roimhe seo", + "Reset status" : "Stádas a athshocrú", + "Reset status to \"{icon} {message}\"" : "Athshocraigh stádas go \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Athshocraigh stádas go \"{message}\"", + "Reset status to \"{icon}\"" : "Athshocraigh stádas go \"{icon}\"", + "There was an error saving the status" : "Tharla earráid agus an stádas á shábháil", + "There was an error clearing the status" : "Tharla earráid agus an stádas á ghlanadh", + "There was an error reverting the status" : "Tharla earráid agus an stádas á chur ar ais", + "Online status" : "Stádas ar líne", + "Status message" : "Teachtaireacht stádais", + "Set absence period" : "Socraigh tréimhse neamhláithreachta", + "Set absence period and replacement" : "Socraigh tréimhse neamhláithreachta agus athsholáthar", + "Your status was set automatically" : "Socraíodh do stádas go huathoibríoch", + "Clear status message" : "Glan teachtaireacht stádais", + "Set status message" : "Socraigh teachtaireacht stádais", + "Don't clear" : "Ná soiléir", + "Today" : "Inniu", + "This week" : "An tseachtain seo", + "Online" : "Ar líne", + "Away" : "Amach", + "Do not disturb" : "Ná cur as", + "Invisible" : "Dofheicthe", + "Offline" : "As líne", + "Set status" : "Socraigh stádas", + "There was an error saving the new status" : "Tharla earráid agus an stádas nua á shábháil", + "30 minutes" : "30 nóiméad", + "1 hour" : "1 uair", + "4 hours" : "4 uair an chloig", + "Busy" : "Gnóthach", + "Mute all notifications" : "Balbhaigh gach fógra", + "Appear offline" : "Le feiceáil as líne" +},"pluralForm" :"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/gl.js b/apps/user_status/l10n/gl.js new file mode 100644 index 00000000000..3045c22fb4a --- /dev/null +++ b/apps/user_status/l10n/gl.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Estados recentes", + "No recent status changes" : "Non hai cambios de estado recentes", + "In a meeting" : "Nunha xuntanza", + "Commuting" : "De casa ao traballo ou ao revés", + "Out sick" : "Enfermo", + "Vacationing" : "De vacacións", + "Out of office" : "Fóra da oficina", + "Working remotely" : "Traballando en remoto", + "In a call" : "Nunha chamada", + "User status" : "Estado do usuario", + "Clear status after" : "Limpar o estado após", + "Emoji for your status message" : "«Emoji» para a súa mensaxe de estado", + "What is your status?" : "Cal é o seu estado?", + "Predefined statuses" : "Estados predefinidos", + "Previously set" : "Estabelecido previamente", + "Reset status" : "Restabelecer o estado", + "Reset status to \"{icon} {message}\"" : "Restabelecer o estado a «{icon} {message}»", + "Reset status to \"{message}\"" : "Restabelecer o estado a «{message}»", + "Reset status to \"{icon}\"" : "Restabelecer o estado a «{icon}»", + "There was an error saving the status" : "Produciuse un erro ao gardar o estado", + "There was an error clearing the status" : "Produciuse un erro ao limpar o estado", + "There was an error reverting the status" : "Produciuse un erro ao reverter o estado", + "Online status" : "Estado en liña", + "Status message" : "Mensaxe de estado", + "Set absence period" : "Definir o período de ausencia", + "Set absence period and replacement" : "Definir o período de ausencia e substitución", + "Your status was set automatically" : "O seu estado foi estabelecido automaticamente", + "Clear status message" : "Limpar a mensaxe de estado", + "Set status message" : "Definir a mensaxe de estado", + "Don't clear" : "Non limpar", + "Today" : "Hoxe", + "This week" : "Esta semana", + "Online" : "En liña", + "Away" : "Ausente", + "Do not disturb" : "Non molestar", + "Invisible" : "Invisíbel", + "Offline" : "Sen conexión", + "Set status" : "Definir o estado", + "There was an error saving the new status" : "Produciuse un erro ao gardar o novo estado", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 horas", + "Busy" : "Ocupado", + "Mute all notifications" : "Enmudecer todas as notificacións", + "Appear offline" : "Aparece coma sen conexión" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/gl.json b/apps/user_status/l10n/gl.json new file mode 100644 index 00000000000..c90de4d4f13 --- /dev/null +++ b/apps/user_status/l10n/gl.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Estados recentes", + "No recent status changes" : "Non hai cambios de estado recentes", + "In a meeting" : "Nunha xuntanza", + "Commuting" : "De casa ao traballo ou ao revés", + "Out sick" : "Enfermo", + "Vacationing" : "De vacacións", + "Out of office" : "Fóra da oficina", + "Working remotely" : "Traballando en remoto", + "In a call" : "Nunha chamada", + "User status" : "Estado do usuario", + "Clear status after" : "Limpar o estado após", + "Emoji for your status message" : "«Emoji» para a súa mensaxe de estado", + "What is your status?" : "Cal é o seu estado?", + "Predefined statuses" : "Estados predefinidos", + "Previously set" : "Estabelecido previamente", + "Reset status" : "Restabelecer o estado", + "Reset status to \"{icon} {message}\"" : "Restabelecer o estado a «{icon} {message}»", + "Reset status to \"{message}\"" : "Restabelecer o estado a «{message}»", + "Reset status to \"{icon}\"" : "Restabelecer o estado a «{icon}»", + "There was an error saving the status" : "Produciuse un erro ao gardar o estado", + "There was an error clearing the status" : "Produciuse un erro ao limpar o estado", + "There was an error reverting the status" : "Produciuse un erro ao reverter o estado", + "Online status" : "Estado en liña", + "Status message" : "Mensaxe de estado", + "Set absence period" : "Definir o período de ausencia", + "Set absence period and replacement" : "Definir o período de ausencia e substitución", + "Your status was set automatically" : "O seu estado foi estabelecido automaticamente", + "Clear status message" : "Limpar a mensaxe de estado", + "Set status message" : "Definir a mensaxe de estado", + "Don't clear" : "Non limpar", + "Today" : "Hoxe", + "This week" : "Esta semana", + "Online" : "En liña", + "Away" : "Ausente", + "Do not disturb" : "Non molestar", + "Invisible" : "Invisíbel", + "Offline" : "Sen conexión", + "Set status" : "Definir o estado", + "There was an error saving the new status" : "Produciuse un erro ao gardar o novo estado", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 horas", + "Busy" : "Ocupado", + "Mute all notifications" : "Enmudecer todas as notificacións", + "Appear offline" : "Aparece coma sen conexión" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/he.js b/apps/user_status/l10n/he.js new file mode 100644 index 00000000000..c14e661e90c --- /dev/null +++ b/apps/user_status/l10n/he.js @@ -0,0 +1,38 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "מצבים אחרונים", + "No recent status changes" : "אין שינויים אחרונים למצב", + "In a meeting" : "בפגישה", + "Commuting" : "בדרכים", + "Out sick" : "בחופשת מחלה", + "Vacationing" : "בחופש", + "Out of office" : "מחוץ למשרד", + "Working remotely" : "בעבודה מרחוק", + "User status" : "מצב משתמש", + "Clear status after" : "לפנות את המצב לאחר", + "What is your status?" : "מה המצב שלך?", + "There was an error saving the status" : "אירעה שגיאה בשמירת המצב", + "There was an error clearing the status" : "אירעה שגיאה בפינוי המצב", + "Online status" : "מצב מקוון", + "Status message" : "הודעת מצב", + "Clear status message" : "פינוי הודעת המצב", + "Set status message" : "הגדרת הודעת מצב", + "Don't clear" : "לא לפנות", + "Today" : "היום", + "This week" : "השבוע", + "Online" : "מקוון", + "Away" : "לא פה", + "Do not disturb" : "לא להפריע", + "Invisible" : "נסתרת", + "Offline" : "בלתי מקוון", + "Set status" : "הגדרת מצב", + "There was an error saving the new status" : "אירעה שגיאה בשמירת המצב החדש", + "30 minutes" : "30 דקות", + "1 hour" : "שעה", + "4 hours" : "4 שעות", + "Busy" : "עסוק", + "Mute all notifications" : "השתקת כל ההתראות", + "Appear offline" : "להופיע במצב בלתי מקוון" +}, +"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;"); diff --git a/apps/user_status/l10n/he.json b/apps/user_status/l10n/he.json new file mode 100644 index 00000000000..1475c5c48e9 --- /dev/null +++ b/apps/user_status/l10n/he.json @@ -0,0 +1,36 @@ +{ "translations": { + "Recent statuses" : "מצבים אחרונים", + "No recent status changes" : "אין שינויים אחרונים למצב", + "In a meeting" : "בפגישה", + "Commuting" : "בדרכים", + "Out sick" : "בחופשת מחלה", + "Vacationing" : "בחופש", + "Out of office" : "מחוץ למשרד", + "Working remotely" : "בעבודה מרחוק", + "User status" : "מצב משתמש", + "Clear status after" : "לפנות את המצב לאחר", + "What is your status?" : "מה המצב שלך?", + "There was an error saving the status" : "אירעה שגיאה בשמירת המצב", + "There was an error clearing the status" : "אירעה שגיאה בפינוי המצב", + "Online status" : "מצב מקוון", + "Status message" : "הודעת מצב", + "Clear status message" : "פינוי הודעת המצב", + "Set status message" : "הגדרת הודעת מצב", + "Don't clear" : "לא לפנות", + "Today" : "היום", + "This week" : "השבוע", + "Online" : "מקוון", + "Away" : "לא פה", + "Do not disturb" : "לא להפריע", + "Invisible" : "נסתרת", + "Offline" : "בלתי מקוון", + "Set status" : "הגדרת מצב", + "There was an error saving the new status" : "אירעה שגיאה בשמירת המצב החדש", + "30 minutes" : "30 דקות", + "1 hour" : "שעה", + "4 hours" : "4 שעות", + "Busy" : "עסוק", + "Mute all notifications" : "השתקת כל ההתראות", + "Appear offline" : "להופיע במצב בלתי מקוון" +},"pluralForm" :"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/hr.js b/apps/user_status/l10n/hr.js new file mode 100644 index 00000000000..32026e39816 --- /dev/null +++ b/apps/user_status/l10n/hr.js @@ -0,0 +1,39 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Nedavni statusi", + "No recent status changes" : "Nema nedavnih promjena statusa", + "In a meeting" : "Na sastanku", + "Commuting" : "Na putu", + "Out sick" : "Na bolovanju", + "Vacationing" : "Na odmoru", + "Out of office" : "Izvan ureda", + "Working remotely" : "Rad na daljinu", + "In a call" : "U pozivu", + "User status" : "Status korisnika", + "Clear status after" : "Izbriši status nakon", + "What is your status?" : "Koji je vaš status?", + "There was an error saving the status" : "Došlo je do pogreške pri spremanju statusa", + "There was an error clearing the status" : "Došlo je do pogreške pri brisanju statusa", + "Online status" : "Status na mreži", + "Status message" : "Poruka statusa", + "Clear status message" : "Izbriši poruku statusa", + "Set status message" : "Postavi poruku statusa", + "Don't clear" : "Ne briši", + "Today" : "Danas", + "This week" : "Ovaj tjedan", + "Online" : "Na mreži", + "Away" : "Odsutan", + "Do not disturb" : "Ne ometaj", + "Invisible" : "Nevidljiva", + "Offline" : "Izvanmrežno", + "Set status" : "Postavi status", + "There was an error saving the new status" : "Došlo je do pogreške pri spremanju novog statusa", + "30 minutes" : "30 minuta", + "1 hour" : "1 sat", + "4 hours" : "4 sata", + "Busy" : "Zauzeto", + "Mute all notifications" : "Utišaj sve obavijesti", + "Appear offline" : "Prikaži izvanmrežno" +}, +"nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;"); diff --git a/apps/user_status/l10n/hr.json b/apps/user_status/l10n/hr.json new file mode 100644 index 00000000000..ba0a7d987f4 --- /dev/null +++ b/apps/user_status/l10n/hr.json @@ -0,0 +1,37 @@ +{ "translations": { + "Recent statuses" : "Nedavni statusi", + "No recent status changes" : "Nema nedavnih promjena statusa", + "In a meeting" : "Na sastanku", + "Commuting" : "Na putu", + "Out sick" : "Na bolovanju", + "Vacationing" : "Na odmoru", + "Out of office" : "Izvan ureda", + "Working remotely" : "Rad na daljinu", + "In a call" : "U pozivu", + "User status" : "Status korisnika", + "Clear status after" : "Izbriši status nakon", + "What is your status?" : "Koji je vaš status?", + "There was an error saving the status" : "Došlo je do pogreške pri spremanju statusa", + "There was an error clearing the status" : "Došlo je do pogreške pri brisanju statusa", + "Online status" : "Status na mreži", + "Status message" : "Poruka statusa", + "Clear status message" : "Izbriši poruku statusa", + "Set status message" : "Postavi poruku statusa", + "Don't clear" : "Ne briši", + "Today" : "Danas", + "This week" : "Ovaj tjedan", + "Online" : "Na mreži", + "Away" : "Odsutan", + "Do not disturb" : "Ne ometaj", + "Invisible" : "Nevidljiva", + "Offline" : "Izvanmrežno", + "Set status" : "Postavi status", + "There was an error saving the new status" : "Došlo je do pogreške pri spremanju novog statusa", + "30 minutes" : "30 minuta", + "1 hour" : "1 sat", + "4 hours" : "4 sata", + "Busy" : "Zauzeto", + "Mute all notifications" : "Utišaj sve obavijesti", + "Appear offline" : "Prikaži izvanmrežno" +},"pluralForm" :"nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/hu.js b/apps/user_status/l10n/hu.js new file mode 100644 index 00000000000..9610f72b24e --- /dev/null +++ b/apps/user_status/l10n/hu.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Legutóbbi állapotok", + "No recent status changes" : "Nincsenek legutóbbi állapotváltozások", + "In a meeting" : "Találkozón", + "Commuting" : "Ingázás", + "Out sick" : "Betegszabadságon", + "Vacationing" : "Szabadságon", + "Out of office" : "Irodán kívül", + "Working remotely" : "Távoli munkavégzés", + "In a call" : "Hívásban", + "User status" : "Felhasználói állapot", + "Clear status after" : "Állapot törlése ennyi idő után", + "Emoji for your status message" : "Emodzsi az állapotüzenetéhez", + "What is your status?" : "Mi az állapota?", + "Predefined statuses" : "Előre meghatározott állapotok", + "Previously set" : "Előzőleg beállított", + "Reset status" : "Állapot visszaállítása", + "Reset status to \"{icon} {message}\"" : "Állapot visszaállítása erre: „{icon} {message}”", + "Reset status to \"{message}\"" : "Állapot visszaállítása erre: „{message}”", + "Reset status to \"{icon}\"" : "Állapot visszaállítása erre: „{icon}”", + "There was an error saving the status" : "Hiba történt az állapot mentése során", + "There was an error clearing the status" : "Hiba történt az állapot törlése sorá", + "There was an error reverting the status" : "Hiba történt az állapot visszaállítása során", + "Online status" : "Elérhető állapot", + "Status message" : "Állapotüzenet", + "Set absence period" : "Távolléti időszak beállítása", + "Set absence period and replacement" : "Távolléti időszak és helyettes beállítása", + "Your status was set automatically" : "Az állapota automatikusan lett beállítva", + "Clear status message" : "Állapotüzenet törlése", + "Set status message" : "Állapotüzenet beállítása", + "Don't clear" : "Ne törölje", + "Today" : "Ma", + "This week" : "Ezen a héten", + "Online" : "Elérhető", + "Away" : "Távol", + "Do not disturb" : "Ne zavarjanak", + "Invisible" : "Láthatatlan", + "Offline" : "Nem kapcsolódott", + "Set status" : "Állapot beállítása", + "There was an error saving the new status" : "Hiba történt az új állapot mentése sorá", + "30 minutes" : "30 perc", + "1 hour" : "1 óra", + "4 hours" : "4 óra", + "Busy" : "Foglalt", + "Mute all notifications" : "Összes értesítés némítása", + "Appear offline" : "Megjelenés nem kapcsolódottként" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/hu.json b/apps/user_status/l10n/hu.json new file mode 100644 index 00000000000..9dbf642fc0d --- /dev/null +++ b/apps/user_status/l10n/hu.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Legutóbbi állapotok", + "No recent status changes" : "Nincsenek legutóbbi állapotváltozások", + "In a meeting" : "Találkozón", + "Commuting" : "Ingázás", + "Out sick" : "Betegszabadságon", + "Vacationing" : "Szabadságon", + "Out of office" : "Irodán kívül", + "Working remotely" : "Távoli munkavégzés", + "In a call" : "Hívásban", + "User status" : "Felhasználói állapot", + "Clear status after" : "Állapot törlése ennyi idő után", + "Emoji for your status message" : "Emodzsi az állapotüzenetéhez", + "What is your status?" : "Mi az állapota?", + "Predefined statuses" : "Előre meghatározott állapotok", + "Previously set" : "Előzőleg beállított", + "Reset status" : "Állapot visszaállítása", + "Reset status to \"{icon} {message}\"" : "Állapot visszaállítása erre: „{icon} {message}”", + "Reset status to \"{message}\"" : "Állapot visszaállítása erre: „{message}”", + "Reset status to \"{icon}\"" : "Állapot visszaállítása erre: „{icon}”", + "There was an error saving the status" : "Hiba történt az állapot mentése során", + "There was an error clearing the status" : "Hiba történt az állapot törlése sorá", + "There was an error reverting the status" : "Hiba történt az állapot visszaállítása során", + "Online status" : "Elérhető állapot", + "Status message" : "Állapotüzenet", + "Set absence period" : "Távolléti időszak beállítása", + "Set absence period and replacement" : "Távolléti időszak és helyettes beállítása", + "Your status was set automatically" : "Az állapota automatikusan lett beállítva", + "Clear status message" : "Állapotüzenet törlése", + "Set status message" : "Állapotüzenet beállítása", + "Don't clear" : "Ne törölje", + "Today" : "Ma", + "This week" : "Ezen a héten", + "Online" : "Elérhető", + "Away" : "Távol", + "Do not disturb" : "Ne zavarjanak", + "Invisible" : "Láthatatlan", + "Offline" : "Nem kapcsolódott", + "Set status" : "Állapot beállítása", + "There was an error saving the new status" : "Hiba történt az új állapot mentése sorá", + "30 minutes" : "30 perc", + "1 hour" : "1 óra", + "4 hours" : "4 óra", + "Busy" : "Foglalt", + "Mute all notifications" : "Összes értesítés némítása", + "Appear offline" : "Megjelenés nem kapcsolódottként" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/is.js b/apps/user_status/l10n/is.js new file mode 100644 index 00000000000..1f17415040b --- /dev/null +++ b/apps/user_status/l10n/is.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Nýlegar stöður", + "No recent status changes" : "Engar nýlegar breytingar á stöðu", + "In a meeting" : "Á fundi", + "Commuting" : "Á ferðinni", + "Out sick" : "Veikindi", + "Vacationing" : "Í fríi", + "Out of office" : "Ekki á staðnum", + "Working remotely" : "Fjarvinna", + "In a call" : "Er í símtali", + "User status" : "Staða notanda", + "Clear status after" : "Hreinsa stöðu eftir", + "Emoji for your status message" : "Tákn fyrir stöðufærsluna þína", + "What is your status?" : "Hver er staðan á þér?", + "Predefined statuses" : "Forákvarðaðar stöður", + "Previously set" : "Áður stillt", + "Reset status" : "Endurstilla stöðu", + "Reset status to \"{icon} {message}\"" : "Endurstilla stöðu sem \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Endurstilla stöðu sem \"{message}\"", + "Reset status to \"{icon}\"" : "Endurstilla stöðu sem \"{icon}\"", + "There was an error saving the status" : "Það kom upp villa við að vista stöðuna", + "There was an error clearing the status" : "Það kom upp villa við að hreinsa stöðuna", + "There was an error reverting the status" : "Það kom upp villa við að afturkalla stöðuna", + "Online status" : "Staða á netinu", + "Status message" : "Stöðuskilaboð", + "Set absence period" : "Setja tímabil fjarveru", + "Set absence period and replacement" : "Setja tímabil fjarveru og afleysingu", + "Your status was set automatically" : "Staðan þín var stillt sjálfvirkt", + "Clear status message" : "Hreinsa stöðuskilaboð", + "Set status message" : "Setja stöðuskilaboð", + "Don't clear" : "Ekki hreinsa", + "Today" : "Í dag", + "This week" : "Í þessari viku", + "Online" : "Á netinu", + "Away" : "Fjarverandi", + "Do not disturb" : "Ónáðið ekki", + "Invisible" : "Ósýnilegt", + "Offline" : "Ótengdur neti", + "Set status" : "Setja stöðu", + "There was an error saving the new status" : "Það kom upp villa við að vista nýju stöðuna", + "30 minutes" : "30 mínútur", + "1 hour" : "1 klukkustund", + "4 hours" : "4 klukkustundir", + "Busy" : "Upptekinn", + "Mute all notifications" : "Þagga allar tilkynningar", + "Appear offline" : "Birtast ótengt" +}, +"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);"); diff --git a/apps/user_status/l10n/is.json b/apps/user_status/l10n/is.json new file mode 100644 index 00000000000..f081ed2745f --- /dev/null +++ b/apps/user_status/l10n/is.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Nýlegar stöður", + "No recent status changes" : "Engar nýlegar breytingar á stöðu", + "In a meeting" : "Á fundi", + "Commuting" : "Á ferðinni", + "Out sick" : "Veikindi", + "Vacationing" : "Í fríi", + "Out of office" : "Ekki á staðnum", + "Working remotely" : "Fjarvinna", + "In a call" : "Er í símtali", + "User status" : "Staða notanda", + "Clear status after" : "Hreinsa stöðu eftir", + "Emoji for your status message" : "Tákn fyrir stöðufærsluna þína", + "What is your status?" : "Hver er staðan á þér?", + "Predefined statuses" : "Forákvarðaðar stöður", + "Previously set" : "Áður stillt", + "Reset status" : "Endurstilla stöðu", + "Reset status to \"{icon} {message}\"" : "Endurstilla stöðu sem \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Endurstilla stöðu sem \"{message}\"", + "Reset status to \"{icon}\"" : "Endurstilla stöðu sem \"{icon}\"", + "There was an error saving the status" : "Það kom upp villa við að vista stöðuna", + "There was an error clearing the status" : "Það kom upp villa við að hreinsa stöðuna", + "There was an error reverting the status" : "Það kom upp villa við að afturkalla stöðuna", + "Online status" : "Staða á netinu", + "Status message" : "Stöðuskilaboð", + "Set absence period" : "Setja tímabil fjarveru", + "Set absence period and replacement" : "Setja tímabil fjarveru og afleysingu", + "Your status was set automatically" : "Staðan þín var stillt sjálfvirkt", + "Clear status message" : "Hreinsa stöðuskilaboð", + "Set status message" : "Setja stöðuskilaboð", + "Don't clear" : "Ekki hreinsa", + "Today" : "Í dag", + "This week" : "Í þessari viku", + "Online" : "Á netinu", + "Away" : "Fjarverandi", + "Do not disturb" : "Ónáðið ekki", + "Invisible" : "Ósýnilegt", + "Offline" : "Ótengdur neti", + "Set status" : "Setja stöðu", + "There was an error saving the new status" : "Það kom upp villa við að vista nýju stöðuna", + "30 minutes" : "30 mínútur", + "1 hour" : "1 klukkustund", + "4 hours" : "4 klukkustundir", + "Busy" : "Upptekinn", + "Mute all notifications" : "Þagga allar tilkynningar", + "Appear offline" : "Birtast ótengt" +},"pluralForm" :"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/it.js b/apps/user_status/l10n/it.js new file mode 100644 index 00000000000..9917b09972e --- /dev/null +++ b/apps/user_status/l10n/it.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Stati recenti", + "No recent status changes" : "Nessun cambio di stato recente", + "In a meeting" : "In una riunione", + "Commuting" : "Pendolare", + "Out sick" : "In malattia", + "Vacationing" : "In vacanza", + "Out of office" : "Fuori sede", + "Working remotely" : "Lavoro da remoto", + "In a call" : "In una chiamata", + "User status" : "Stato utente", + "Clear status after" : "Togli lo stato dopo", + "Emoji for your status message" : "Emoji per il tuo messaggio di stato", + "What is your status?" : "Qual è il tuo stato?", + "Predefined statuses" : "Stati predefiniti", + "Previously set" : "Impostato in precedenza", + "Reset status" : "Ripristina stato", + "Reset status to \"{icon} {message}\"" : "Ripristina stato a \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Ripristina stato a \"{message}\"", + "Reset status to \"{icon}\"" : "Ripristina stato a \"{icon}\"", + "There was an error saving the status" : "Si è verificato un errore durante il salvataggio dello stato", + "There was an error clearing the status" : "Si è verificato un errore durante la rimozione dello stato", + "There was an error reverting the status" : "Si è verificato un errore ripristinando lo stato", + "Online status" : "Stato in linea", + "Status message" : "Messaggio di stato", + "Set absence period" : "Imposta periodo di assenza", + "Set absence period and replacement" : "Imposta periodo di assenza e sostituzione", + "Your status was set automatically" : "Stato impostato automaticamente", + "Clear status message" : "Cancella il messaggio di stato", + "Set status message" : "Imposta messaggio di stato", + "Don't clear" : "Non togliere", + "Today" : "Oggi", + "This week" : "Questa settimana", + "Online" : "In linea", + "Away" : "Assente", + "Do not disturb" : "Non disturbare", + "Invisible" : "Invisibile", + "Offline" : "Non in linea", + "Set status" : "Imposta stato", + "There was an error saving the new status" : "Si è verificato un errore durante il salvataggio del nuovo stato", + "30 minutes" : "30 minuti", + "1 hour" : "1 ora", + "4 hours" : "4 ore", + "Busy" : "Occupato", + "Mute all notifications" : "Silenzia tutte le notifiche", + "Appear offline" : "Mostrati non in linea" +}, +"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/user_status/l10n/it.json b/apps/user_status/l10n/it.json new file mode 100644 index 00000000000..96a6af919a5 --- /dev/null +++ b/apps/user_status/l10n/it.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Stati recenti", + "No recent status changes" : "Nessun cambio di stato recente", + "In a meeting" : "In una riunione", + "Commuting" : "Pendolare", + "Out sick" : "In malattia", + "Vacationing" : "In vacanza", + "Out of office" : "Fuori sede", + "Working remotely" : "Lavoro da remoto", + "In a call" : "In una chiamata", + "User status" : "Stato utente", + "Clear status after" : "Togli lo stato dopo", + "Emoji for your status message" : "Emoji per il tuo messaggio di stato", + "What is your status?" : "Qual è il tuo stato?", + "Predefined statuses" : "Stati predefiniti", + "Previously set" : "Impostato in precedenza", + "Reset status" : "Ripristina stato", + "Reset status to \"{icon} {message}\"" : "Ripristina stato a \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Ripristina stato a \"{message}\"", + "Reset status to \"{icon}\"" : "Ripristina stato a \"{icon}\"", + "There was an error saving the status" : "Si è verificato un errore durante il salvataggio dello stato", + "There was an error clearing the status" : "Si è verificato un errore durante la rimozione dello stato", + "There was an error reverting the status" : "Si è verificato un errore ripristinando lo stato", + "Online status" : "Stato in linea", + "Status message" : "Messaggio di stato", + "Set absence period" : "Imposta periodo di assenza", + "Set absence period and replacement" : "Imposta periodo di assenza e sostituzione", + "Your status was set automatically" : "Stato impostato automaticamente", + "Clear status message" : "Cancella il messaggio di stato", + "Set status message" : "Imposta messaggio di stato", + "Don't clear" : "Non togliere", + "Today" : "Oggi", + "This week" : "Questa settimana", + "Online" : "In linea", + "Away" : "Assente", + "Do not disturb" : "Non disturbare", + "Invisible" : "Invisibile", + "Offline" : "Non in linea", + "Set status" : "Imposta stato", + "There was an error saving the new status" : "Si è verificato un errore durante il salvataggio del nuovo stato", + "30 minutes" : "30 minuti", + "1 hour" : "1 ora", + "4 hours" : "4 ore", + "Busy" : "Occupato", + "Mute all notifications" : "Silenzia tutte le notifiche", + "Appear offline" : "Mostrati non in linea" +},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/ja.js b/apps/user_status/l10n/ja.js new file mode 100644 index 00000000000..74f480e0f36 --- /dev/null +++ b/apps/user_status/l10n/ja.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "最近のステータス", + "No recent status changes" : "最近のステータスの変更はありません", + "In a meeting" : "会議中", + "Commuting" : "通勤中", + "Out sick" : "体調不良", + "Vacationing" : "休暇", + "Out of office" : "オフィス外", + "Working remotely" : "リモートワーク中", + "In a call" : "通話中", + "Be right back" : "すぐ戻ります", + "User status" : "ユーザーステータス", + "Clear status after" : "ステータスの有効期限", + "Emoji for your status message" : "あなたのステータスメッセージに絵文字を", + "What is your status?" : "現在のオンラインステータスは?", + "Predefined statuses" : "事前定義されたステータス", + "Previously set" : "以前の設定", + "Reset status" : "ステータスをリセット", + "Reset status to \"{icon} {message}\"" : "ステータスを \"{icon} {message}\" にリセット", + "Reset status to \"{message}\"" : "ステータスを \"{message}\" にリセット", + "Reset status to \"{icon}\"" : "ステータスを \"{icon}\" にリセット", + "There was an error saving the status" : "ステータスの保存中にエラーが発生しました", + "There was an error clearing the status" : "ステータスの消去中にエラーが発生しました", + "There was an error reverting the status" : "ステータスを戻す際にエラーが発生しました", + "Online status" : "オンラインステータス", + "Status message" : "状態メッセージ", + "Set absence period" : "不在設定をセットする", + "Set absence period and replacement" : "不在期間と交代要員をセットする", + "Your status was set automatically" : "あなたのステータスは自動的に設定されました", + "Clear status message" : "ステータスメッセージを消去", + "Set status message" : "ステータスメッセージを設定", + "Don't clear" : "消去しない", + "Today" : "今日", + "This week" : "今週", + "Online" : "オンライン", + "Away" : "離席中", + "Do not disturb" : "取り込み中", + "Invisible" : "ステータスを隠す", + "Offline" : "オフライン", + "Set status" : "ステータスを設定", + "There was an error saving the new status" : "新しいステータスの保存中にエラーが発生しました", + "30 minutes" : "30分", + "1 hour" : "1時間", + "4 hours" : "4時間", + "Busy" : "ビジー", + "Mute all notifications" : "全ての通知をミュート", + "Appear offline" : "オフライン" +}, +"nplurals=1; plural=0;"); diff --git a/apps/user_status/l10n/ja.json b/apps/user_status/l10n/ja.json new file mode 100644 index 00000000000..183ed4f1c1e --- /dev/null +++ b/apps/user_status/l10n/ja.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "最近のステータス", + "No recent status changes" : "最近のステータスの変更はありません", + "In a meeting" : "会議中", + "Commuting" : "通勤中", + "Out sick" : "体調不良", + "Vacationing" : "休暇", + "Out of office" : "オフィス外", + "Working remotely" : "リモートワーク中", + "In a call" : "通話中", + "Be right back" : "すぐ戻ります", + "User status" : "ユーザーステータス", + "Clear status after" : "ステータスの有効期限", + "Emoji for your status message" : "あなたのステータスメッセージに絵文字を", + "What is your status?" : "現在のオンラインステータスは?", + "Predefined statuses" : "事前定義されたステータス", + "Previously set" : "以前の設定", + "Reset status" : "ステータスをリセット", + "Reset status to \"{icon} {message}\"" : "ステータスを \"{icon} {message}\" にリセット", + "Reset status to \"{message}\"" : "ステータスを \"{message}\" にリセット", + "Reset status to \"{icon}\"" : "ステータスを \"{icon}\" にリセット", + "There was an error saving the status" : "ステータスの保存中にエラーが発生しました", + "There was an error clearing the status" : "ステータスの消去中にエラーが発生しました", + "There was an error reverting the status" : "ステータスを戻す際にエラーが発生しました", + "Online status" : "オンラインステータス", + "Status message" : "状態メッセージ", + "Set absence period" : "不在設定をセットする", + "Set absence period and replacement" : "不在期間と交代要員をセットする", + "Your status was set automatically" : "あなたのステータスは自動的に設定されました", + "Clear status message" : "ステータスメッセージを消去", + "Set status message" : "ステータスメッセージを設定", + "Don't clear" : "消去しない", + "Today" : "今日", + "This week" : "今週", + "Online" : "オンライン", + "Away" : "離席中", + "Do not disturb" : "取り込み中", + "Invisible" : "ステータスを隠す", + "Offline" : "オフライン", + "Set status" : "ステータスを設定", + "There was an error saving the new status" : "新しいステータスの保存中にエラーが発生しました", + "30 minutes" : "30分", + "1 hour" : "1時間", + "4 hours" : "4時間", + "Busy" : "ビジー", + "Mute all notifications" : "全ての通知をミュート", + "Appear offline" : "オフライン" +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/ko.js b/apps/user_status/l10n/ko.js new file mode 100644 index 00000000000..3afaa412ee2 --- /dev/null +++ b/apps/user_status/l10n/ko.js @@ -0,0 +1,39 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "최근 상태", + "No recent status changes" : "최근 상태 변경 없음", + "In a meeting" : "회의 중", + "Commuting" : "이동 중", + "Out sick" : "병가", + "Vacationing" : "휴가 중", + "Out of office" : "자리에 없음", + "Working remotely" : "원격 근무 중", + "In a call" : "통화중", + "User status" : "사용자 상태", + "Clear status after" : "상태 메시지 지우기 예약", + "What is your status?" : "당신의 상태는?", + "There was an error saving the status" : "상태 저장에 오류가 발생했습니다.", + "There was an error clearing the status" : "상태 해제에 오류가 발생했습니다.", + "Online status" : "접속 상태", + "Status message" : "상태 메시지", + "Clear status message" : "상태 메시지 지움", + "Set status message" : "상태 메시지 설정", + "Don't clear" : "지우지 않음", + "Today" : "오늘", + "This week" : "이번 주", + "Online" : "접속 중", + "Away" : "자리 비움", + "Do not disturb" : "방해 금지", + "Invisible" : "숨겨짐", + "Offline" : "오프라인", + "Set status" : "상태 설정", + "There was an error saving the new status" : "새로운 상태 저장에 오류가 발생했습니다.", + "30 minutes" : "30 분", + "1 hour" : "한 시간", + "4 hours" : "4 시간", + "Busy" : "바쁨", + "Mute all notifications" : "모든 알림을 음소거", + "Appear offline" : "접속 안함으로 표시" +}, +"nplurals=1; plural=0;"); diff --git a/apps/user_status/l10n/ko.json b/apps/user_status/l10n/ko.json new file mode 100644 index 00000000000..4858bccd4e0 --- /dev/null +++ b/apps/user_status/l10n/ko.json @@ -0,0 +1,37 @@ +{ "translations": { + "Recent statuses" : "최근 상태", + "No recent status changes" : "최근 상태 변경 없음", + "In a meeting" : "회의 중", + "Commuting" : "이동 중", + "Out sick" : "병가", + "Vacationing" : "휴가 중", + "Out of office" : "자리에 없음", + "Working remotely" : "원격 근무 중", + "In a call" : "통화중", + "User status" : "사용자 상태", + "Clear status after" : "상태 메시지 지우기 예약", + "What is your status?" : "당신의 상태는?", + "There was an error saving the status" : "상태 저장에 오류가 발생했습니다.", + "There was an error clearing the status" : "상태 해제에 오류가 발생했습니다.", + "Online status" : "접속 상태", + "Status message" : "상태 메시지", + "Clear status message" : "상태 메시지 지움", + "Set status message" : "상태 메시지 설정", + "Don't clear" : "지우지 않음", + "Today" : "오늘", + "This week" : "이번 주", + "Online" : "접속 중", + "Away" : "자리 비움", + "Do not disturb" : "방해 금지", + "Invisible" : "숨겨짐", + "Offline" : "오프라인", + "Set status" : "상태 설정", + "There was an error saving the new status" : "새로운 상태 저장에 오류가 발생했습니다.", + "30 minutes" : "30 분", + "1 hour" : "한 시간", + "4 hours" : "4 시간", + "Busy" : "바쁨", + "Mute all notifications" : "모든 알림을 음소거", + "Appear offline" : "접속 안함으로 표시" +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/lt_LT.js b/apps/user_status/l10n/lt_LT.js new file mode 100644 index 00000000000..b440b3f1c05 --- /dev/null +++ b/apps/user_status/l10n/lt_LT.js @@ -0,0 +1,41 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Paskiausios būsenos", + "No recent status changes" : "Jokių paskiausių būsenos pasikeitimų", + "In a meeting" : "Susitikime", + "Commuting" : "Važinėju", + "Out sick" : "Sergu", + "Vacationing" : "Poilsiauju", + "Out of office" : "Ne darbo vietoje", + "Working remotely" : "Dirbu nuotoliniu būdu", + "In a call" : "Dalyvauju skambutyje", + "User status" : "Naudotojo būsena", + "Clear status after" : "Išvalyti būseną po", + "What is your status?" : "Kokia jūsų būsena?", + "Predefined statuses" : "Iš anksto apibrėžtos būsenos", + "There was an error saving the status" : "Įrašant būseną, įvyko klaida", + "There was an error clearing the status" : "Išvalant būseną, įvyko klaida", + "Online status" : "Prisijungimo būsena", + "Status message" : "Būsenos žinutė", + "Your status was set automatically" : "Jūsų būsena buvo nustatyta automatiškai", + "Clear status message" : "Išvalyti būsenos žinutę", + "Set status message" : "Nustatyti būsenos žinutę", + "Don't clear" : "Neišvalyti", + "Today" : "Šiandien", + "This week" : "Šią savaitę", + "Online" : "Prisijungęs", + "Away" : "Atsitraukęs", + "Do not disturb" : "Netrukdyti", + "Invisible" : "Nematomas", + "Offline" : "Atsijungęs", + "Set status" : "Nustatyti būseną", + "There was an error saving the new status" : "Įrašant naują būseną, įvyko klaida", + "30 minutes" : "30 minučių", + "1 hour" : "1 valanda", + "4 hours" : "4 valandos", + "Busy" : "Užimtas laikas", + "Mute all notifications" : "Išjungti visus pranešimus", + "Appear offline" : "Atrodyti atsijungusiu" +}, +"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);"); diff --git a/apps/user_status/l10n/lt_LT.json b/apps/user_status/l10n/lt_LT.json new file mode 100644 index 00000000000..d1df46a90f2 --- /dev/null +++ b/apps/user_status/l10n/lt_LT.json @@ -0,0 +1,39 @@ +{ "translations": { + "Recent statuses" : "Paskiausios būsenos", + "No recent status changes" : "Jokių paskiausių būsenos pasikeitimų", + "In a meeting" : "Susitikime", + "Commuting" : "Važinėju", + "Out sick" : "Sergu", + "Vacationing" : "Poilsiauju", + "Out of office" : "Ne darbo vietoje", + "Working remotely" : "Dirbu nuotoliniu būdu", + "In a call" : "Dalyvauju skambutyje", + "User status" : "Naudotojo būsena", + "Clear status after" : "Išvalyti būseną po", + "What is your status?" : "Kokia jūsų būsena?", + "Predefined statuses" : "Iš anksto apibrėžtos būsenos", + "There was an error saving the status" : "Įrašant būseną, įvyko klaida", + "There was an error clearing the status" : "Išvalant būseną, įvyko klaida", + "Online status" : "Prisijungimo būsena", + "Status message" : "Būsenos žinutė", + "Your status was set automatically" : "Jūsų būsena buvo nustatyta automatiškai", + "Clear status message" : "Išvalyti būsenos žinutę", + "Set status message" : "Nustatyti būsenos žinutę", + "Don't clear" : "Neišvalyti", + "Today" : "Šiandien", + "This week" : "Šią savaitę", + "Online" : "Prisijungęs", + "Away" : "Atsitraukęs", + "Do not disturb" : "Netrukdyti", + "Invisible" : "Nematomas", + "Offline" : "Atsijungęs", + "Set status" : "Nustatyti būseną", + "There was an error saving the new status" : "Įrašant naują būseną, įvyko klaida", + "30 minutes" : "30 minučių", + "1 hour" : "1 valanda", + "4 hours" : "4 valandos", + "Busy" : "Užimtas laikas", + "Mute all notifications" : "Išjungti visus pranešimus", + "Appear offline" : "Atrodyti atsijungusiu" +},"pluralForm" :"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/mk.js b/apps/user_status/l10n/mk.js new file mode 100644 index 00000000000..64501b2f6df --- /dev/null +++ b/apps/user_status/l10n/mk.js @@ -0,0 +1,39 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Неодамнешни статуси", + "No recent status changes" : "Нема неодамнешна промена на статус", + "In a meeting" : "На состанок", + "Commuting" : "На пат", + "Out sick" : "На боледување", + "Vacationing" : "На одмор", + "Out of office" : "Надвор од канцеларија", + "Working remotely" : "Присутен од дома", + "In a call" : "Во разговор", + "User status" : "Статус на корисникот", + "Clear status after" : "Тргни го статусот после", + "What is your status?" : "Кој е вашиот статус?", + "There was an error saving the status" : "Грешка при зачувување на статус", + "There was an error clearing the status" : "Грешка при отстранување на статус", + "Online status" : "Присутен", + "Status message" : "Статус порака", + "Clear status message" : "Тргни ја статус пораката", + "Set status message" : "Постави статус порака", + "Don't clear" : "Не го тргај", + "Today" : "Денес", + "This week" : "Оваа недела", + "Online" : "Приклучен", + "Away" : "Неактивен", + "Do not disturb" : "Не вознемирувај", + "Invisible" : "Невидливо", + "Offline" : "Исклучен", + "Set status" : "Постави статус", + "There was an error saving the new status" : "Настана грешка при зачувување на нов статус", + "30 minutes" : "30 минути", + "1 hour" : "1 час", + "4 hours" : "4 часа", + "Busy" : "Зафатен", + "Mute all notifications" : "Занеми (Mute) ги сите известувања", + "Appear offline" : "Прикажи исклучен" +}, +"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;"); diff --git a/apps/user_status/l10n/mk.json b/apps/user_status/l10n/mk.json new file mode 100644 index 00000000000..393500ad5c5 --- /dev/null +++ b/apps/user_status/l10n/mk.json @@ -0,0 +1,37 @@ +{ "translations": { + "Recent statuses" : "Неодамнешни статуси", + "No recent status changes" : "Нема неодамнешна промена на статус", + "In a meeting" : "На состанок", + "Commuting" : "На пат", + "Out sick" : "На боледување", + "Vacationing" : "На одмор", + "Out of office" : "Надвор од канцеларија", + "Working remotely" : "Присутен од дома", + "In a call" : "Во разговор", + "User status" : "Статус на корисникот", + "Clear status after" : "Тргни го статусот после", + "What is your status?" : "Кој е вашиот статус?", + "There was an error saving the status" : "Грешка при зачувување на статус", + "There was an error clearing the status" : "Грешка при отстранување на статус", + "Online status" : "Присутен", + "Status message" : "Статус порака", + "Clear status message" : "Тргни ја статус пораката", + "Set status message" : "Постави статус порака", + "Don't clear" : "Не го тргај", + "Today" : "Денес", + "This week" : "Оваа недела", + "Online" : "Приклучен", + "Away" : "Неактивен", + "Do not disturb" : "Не вознемирувај", + "Invisible" : "Невидливо", + "Offline" : "Исклучен", + "Set status" : "Постави статус", + "There was an error saving the new status" : "Настана грешка при зачувување на нов статус", + "30 minutes" : "30 минути", + "1 hour" : "1 час", + "4 hours" : "4 часа", + "Busy" : "Зафатен", + "Mute all notifications" : "Занеми (Mute) ги сите известувања", + "Appear offline" : "Прикажи исклучен" +},"pluralForm" :"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/nb.js b/apps/user_status/l10n/nb.js new file mode 100644 index 00000000000..f627aaa1e60 --- /dev/null +++ b/apps/user_status/l10n/nb.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Nylige statuser", + "No recent status changes" : "Ingen nylige statusendringer", + "In a meeting" : "I et møte", + "Commuting" : "Pendler", + "Out sick" : "Syk", + "Vacationing" : "På ferie", + "Out of office" : "Fraværende", + "Working remotely" : "Jobber utenfra", + "In a call" : "I en samtale", + "User status" : "Brukerstatus", + "Clear status after" : "Fjern status etter", + "Emoji for your status message" : "Emoji for statusmeldingene dine", + "What is your status?" : "Hva er din status?", + "Predefined statuses" : "Forhåndsdefinerte statuser", + "Previously set" : "Tidligere angitt", + "Reset status" : "Tilbakestill status", + "Reset status to \"{icon} {message}\"" : "Tilbakestill status til \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Tilbakestill status til \"{message}\"", + "Reset status to \"{icon}\"" : "Tilbakestill status til \"{icon}\"", + "There was an error saving the status" : "Det oppsto en feil ved lagring av status", + "There was an error clearing the status" : "Det oppsto en feil ved fjerning av status", + "There was an error reverting the status" : "Det oppstod en feil under tilbakestilling av statusen", + "Online status" : "Online-status", + "Status message" : "Statusmelding", + "Set absence period" : "Angi fraværsperiode", + "Set absence period and replacement" : "Angi fraværsperiode og erstatter", + "Your status was set automatically" : "Statusen din ble satt", + "Clear status message" : "Fjern statusmelding", + "Set status message" : "Velg statusmelding", + "Don't clear" : "Ikke fjern", + "Today" : "I dag", + "This week" : "Denne uken", + "Online" : "Pålogget", + "Away" : "Borte", + "Do not disturb" : "Ikke forstyrr", + "Invisible" : "Usynlig", + "Offline" : "Frakoblet", + "Set status" : "Velg status", + "There was an error saving the new status" : "Det oppsto en feil ved lagring av ny status", + "30 minutes" : "30 minutter", + "1 hour" : "1 time", + "4 hours" : "4 timer", + "Busy" : "Opptatt", + "Mute all notifications" : "Demp alle varslinger", + "Appear offline" : "Vis som frakoblet" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/nb.json b/apps/user_status/l10n/nb.json new file mode 100644 index 00000000000..4e47d91a20b --- /dev/null +++ b/apps/user_status/l10n/nb.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Nylige statuser", + "No recent status changes" : "Ingen nylige statusendringer", + "In a meeting" : "I et møte", + "Commuting" : "Pendler", + "Out sick" : "Syk", + "Vacationing" : "På ferie", + "Out of office" : "Fraværende", + "Working remotely" : "Jobber utenfra", + "In a call" : "I en samtale", + "User status" : "Brukerstatus", + "Clear status after" : "Fjern status etter", + "Emoji for your status message" : "Emoji for statusmeldingene dine", + "What is your status?" : "Hva er din status?", + "Predefined statuses" : "Forhåndsdefinerte statuser", + "Previously set" : "Tidligere angitt", + "Reset status" : "Tilbakestill status", + "Reset status to \"{icon} {message}\"" : "Tilbakestill status til \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Tilbakestill status til \"{message}\"", + "Reset status to \"{icon}\"" : "Tilbakestill status til \"{icon}\"", + "There was an error saving the status" : "Det oppsto en feil ved lagring av status", + "There was an error clearing the status" : "Det oppsto en feil ved fjerning av status", + "There was an error reverting the status" : "Det oppstod en feil under tilbakestilling av statusen", + "Online status" : "Online-status", + "Status message" : "Statusmelding", + "Set absence period" : "Angi fraværsperiode", + "Set absence period and replacement" : "Angi fraværsperiode og erstatter", + "Your status was set automatically" : "Statusen din ble satt", + "Clear status message" : "Fjern statusmelding", + "Set status message" : "Velg statusmelding", + "Don't clear" : "Ikke fjern", + "Today" : "I dag", + "This week" : "Denne uken", + "Online" : "Pålogget", + "Away" : "Borte", + "Do not disturb" : "Ikke forstyrr", + "Invisible" : "Usynlig", + "Offline" : "Frakoblet", + "Set status" : "Velg status", + "There was an error saving the new status" : "Det oppsto en feil ved lagring av ny status", + "30 minutes" : "30 minutter", + "1 hour" : "1 time", + "4 hours" : "4 timer", + "Busy" : "Opptatt", + "Mute all notifications" : "Demp alle varslinger", + "Appear offline" : "Vis som frakoblet" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/nl.js b/apps/user_status/l10n/nl.js new file mode 100644 index 00000000000..745507c57c0 --- /dev/null +++ b/apps/user_status/l10n/nl.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Recente statussen", + "No recent status changes" : "Geen recente statuswijzigingen", + "In a meeting" : "In een vergadering", + "Commuting" : "Woon-werk", + "Out sick" : "Ziek", + "Vacationing" : "Op vakantie", + "Out of office" : "Niet op kantoor", + "Working remotely" : "Thuiswerken", + "In a call" : "In gesprek", + "Be right back" : "Zo weer terug", + "User status" : "Gebruikersstatus", + "Clear status after" : "Maak de status leeg na", + "Emoji for your status message" : "Emoji voor je statusbericht", + "What is your status?" : "Wat is jouw status?", + "Predefined statuses" : "Voorgedefinieerde statussen", + "Previously set" : "Eerder ingesteld", + "Reset status" : "Reset status", + "Reset status to \"{icon} {message}\"" : "Status terugzetten naar \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Status terugzetten naar \"{message}\"", + "Reset status to \"{icon}\"" : "Status terugzetten naar \"{icon}\"", + "There was an error saving the status" : "Er is een fout opgetreden bij het bewaren van de status", + "There was an error clearing the status" : "Er is een fout opgetreden bij het leegmaken van de status", + "There was an error reverting the status" : "Er was een fout bij het terugdraaien van de status", + "Online status" : "Online status", + "Status message" : "Statusbericht", + "Set absence period" : "Afwezigheidsperiode instellen", + "Set absence period and replacement" : "Afwezigheidsperiode en vervanging instellen", + "Your status was set automatically" : "Uw status is automatisch ingesteld", + "Clear status message" : "Statusbericht wissen", + "Set status message" : "Statusbericht instellen", + "Don't clear" : "Niet schoonmaken", + "Today" : "Vandaag", + "This week" : "Deze week", + "Online" : "Online", + "Away" : "Afwezig", + "Do not disturb" : "Niet storen", + "Invisible" : "Verborgen", + "Offline" : "Off-line", + "Set status" : "Status instellen", + "There was an error saving the new status" : "Er is een fout opgetreden bij het bewaren van de nieuwe status", + "30 minutes" : "30 minuten", + "1 hour" : "1 uur", + "4 hours" : "4 uur", + "Busy" : "Bezet", + "Mute all notifications" : "Onderdruk alle meldingen", + "Appear offline" : "Toon afwezig" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/nl.json b/apps/user_status/l10n/nl.json new file mode 100644 index 00000000000..643cf4c27fb --- /dev/null +++ b/apps/user_status/l10n/nl.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "Recente statussen", + "No recent status changes" : "Geen recente statuswijzigingen", + "In a meeting" : "In een vergadering", + "Commuting" : "Woon-werk", + "Out sick" : "Ziek", + "Vacationing" : "Op vakantie", + "Out of office" : "Niet op kantoor", + "Working remotely" : "Thuiswerken", + "In a call" : "In gesprek", + "Be right back" : "Zo weer terug", + "User status" : "Gebruikersstatus", + "Clear status after" : "Maak de status leeg na", + "Emoji for your status message" : "Emoji voor je statusbericht", + "What is your status?" : "Wat is jouw status?", + "Predefined statuses" : "Voorgedefinieerde statussen", + "Previously set" : "Eerder ingesteld", + "Reset status" : "Reset status", + "Reset status to \"{icon} {message}\"" : "Status terugzetten naar \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Status terugzetten naar \"{message}\"", + "Reset status to \"{icon}\"" : "Status terugzetten naar \"{icon}\"", + "There was an error saving the status" : "Er is een fout opgetreden bij het bewaren van de status", + "There was an error clearing the status" : "Er is een fout opgetreden bij het leegmaken van de status", + "There was an error reverting the status" : "Er was een fout bij het terugdraaien van de status", + "Online status" : "Online status", + "Status message" : "Statusbericht", + "Set absence period" : "Afwezigheidsperiode instellen", + "Set absence period and replacement" : "Afwezigheidsperiode en vervanging instellen", + "Your status was set automatically" : "Uw status is automatisch ingesteld", + "Clear status message" : "Statusbericht wissen", + "Set status message" : "Statusbericht instellen", + "Don't clear" : "Niet schoonmaken", + "Today" : "Vandaag", + "This week" : "Deze week", + "Online" : "Online", + "Away" : "Afwezig", + "Do not disturb" : "Niet storen", + "Invisible" : "Verborgen", + "Offline" : "Off-line", + "Set status" : "Status instellen", + "There was an error saving the new status" : "Er is een fout opgetreden bij het bewaren van de nieuwe status", + "30 minutes" : "30 minuten", + "1 hour" : "1 uur", + "4 hours" : "4 uur", + "Busy" : "Bezet", + "Mute all notifications" : "Onderdruk alle meldingen", + "Appear offline" : "Toon afwezig" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/oc.js b/apps/user_status/l10n/oc.js new file mode 100644 index 00000000000..bbd00e9e551 --- /dev/null +++ b/apps/user_status/l10n/oc.js @@ -0,0 +1,39 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Estats recents", + "No recent status changes" : "Cap de cambiament recent d’estat", + "In a meeting" : "En reünion", + "Commuting" : "En comunicacion", + "Out sick" : "Malaut", + "Vacationing" : "En vacanças", + "Out of office" : "Fòra del burèu", + "Working remotely" : "En teletrabalh", + "In a call" : "Al telefòn", + "User status" : "Estat utilizaire", + "Clear status after" : "Escafar l’estat aprèp", + "What is your status?" : "Quin es vòstre estat ?", + "There was an error saving the status" : "Error en enregistrant l’estat", + "There was an error clearing the status" : "Error en escafant l’estat", + "Online status" : "Estat en linha", + "Status message" : "Messatge d’estat", + "Clear status message" : "Escafar messatge d’estat", + "Set status message" : "Definir messatge d’estat", + "Don't clear" : "Escafar pas", + "Today" : "Uèi", + "This week" : "Aquesta setmana", + "Online" : "En linha", + "Away" : "Absent", + "Do not disturb" : "Me desrengar pas", + "Invisible" : "Invisible", + "Offline" : "Fòra linha", + "Set status" : "Definir estat", + "There was an error saving the new status" : "Error en enregistrant l’estat novèl", + "30 minutes" : "30 minutas", + "1 hour" : "1 ora", + "4 hours" : "4 oras", + "Busy" : "Ocupat", + "Mute all notifications" : "Amudir totas las notificacions", + "Appear offline" : "Aparéisser fòra linha" +}, +"nplurals=2; plural=(n > 1);"); diff --git a/apps/user_status/l10n/oc.json b/apps/user_status/l10n/oc.json new file mode 100644 index 00000000000..388df700e75 --- /dev/null +++ b/apps/user_status/l10n/oc.json @@ -0,0 +1,37 @@ +{ "translations": { + "Recent statuses" : "Estats recents", + "No recent status changes" : "Cap de cambiament recent d’estat", + "In a meeting" : "En reünion", + "Commuting" : "En comunicacion", + "Out sick" : "Malaut", + "Vacationing" : "En vacanças", + "Out of office" : "Fòra del burèu", + "Working remotely" : "En teletrabalh", + "In a call" : "Al telefòn", + "User status" : "Estat utilizaire", + "Clear status after" : "Escafar l’estat aprèp", + "What is your status?" : "Quin es vòstre estat ?", + "There was an error saving the status" : "Error en enregistrant l’estat", + "There was an error clearing the status" : "Error en escafant l’estat", + "Online status" : "Estat en linha", + "Status message" : "Messatge d’estat", + "Clear status message" : "Escafar messatge d’estat", + "Set status message" : "Definir messatge d’estat", + "Don't clear" : "Escafar pas", + "Today" : "Uèi", + "This week" : "Aquesta setmana", + "Online" : "En linha", + "Away" : "Absent", + "Do not disturb" : "Me desrengar pas", + "Invisible" : "Invisible", + "Offline" : "Fòra linha", + "Set status" : "Definir estat", + "There was an error saving the new status" : "Error en enregistrant l’estat novèl", + "30 minutes" : "30 minutas", + "1 hour" : "1 ora", + "4 hours" : "4 oras", + "Busy" : "Ocupat", + "Mute all notifications" : "Amudir totas las notificacions", + "Appear offline" : "Aparéisser fòra linha" +},"pluralForm" :"nplurals=2; plural=(n > 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/pl.js b/apps/user_status/l10n/pl.js new file mode 100644 index 00000000000..c25c92e27e1 --- /dev/null +++ b/apps/user_status/l10n/pl.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Najnowsze statusy", + "No recent status changes" : "Brak ostatnich zmian statusu", + "In a meeting" : "Na spotkaniu", + "Commuting" : "W drodze", + "Out sick" : "Chory", + "Vacationing" : "Na wakacjach", + "Out of office" : "Biuro nie funkcjonuje", + "Working remotely" : "Praca zdalna", + "In a call" : "Rozmawia", + "Be right back" : "Zaraz wracam", + "User status" : "Status użytkownika", + "Clear status after" : "Wyczyść status po", + "Emoji for your status message" : "Emoji dla komunikatu o statusie", + "What is your status?" : "Jaki jest Twój status?", + "Predefined statuses" : "Predefiniowane statusy", + "Previously set" : "Ustawione wcześniej", + "Reset status" : "Zresetuj status", + "Reset status to \"{icon} {message}\"" : "Zresetuj status do \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Zresetuj status do \"{message}\"", + "Reset status to \"{icon}\"" : "Zresetuj status do \"{icon}\"", + "There was an error saving the status" : "Wystąpił błąd podczas zapisywania statusu", + "There was an error clearing the status" : "Wystąpił błąd podczas usuwania statusu", + "There was an error reverting the status" : "Podczas przywracania statusu wystąpił błąd", + "Online status" : "Status online", + "Status message" : "Komunikat statusu", + "Set absence period" : "Ustaw okres nieobecności", + "Set absence period and replacement" : "Ustaw okres nieobecności i zastępstwo", + "Your status was set automatically" : "Twój status został ustawiony automatycznie", + "Clear status message" : "Wyczyść komunikat statusu", + "Set status message" : "Ustaw komunikat statusu", + "Don't clear" : "Nie czyść", + "Today" : "Dzisiaj", + "This week" : "W tym tygodniu", + "Online" : "Online", + "Away" : "Bezczynny", + "Do not disturb" : "Nie przeszkadzać", + "Invisible" : "Niewidoczny", + "Offline" : "Offline", + "Set status" : "Ustaw status", + "There was an error saving the new status" : "Wystąpił błąd podczas zapisywania nowego statusu", + "30 minutes" : "30 minut", + "1 hour" : "1 godzina", + "4 hours" : "4 godziny", + "Busy" : "Brak dostępności", + "Mute all notifications" : "Wycisz wszystkie powiadomienia", + "Appear offline" : "Widnieje jako offline" +}, +"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);"); diff --git a/apps/user_status/l10n/pl.json b/apps/user_status/l10n/pl.json new file mode 100644 index 00000000000..48079696e54 --- /dev/null +++ b/apps/user_status/l10n/pl.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "Najnowsze statusy", + "No recent status changes" : "Brak ostatnich zmian statusu", + "In a meeting" : "Na spotkaniu", + "Commuting" : "W drodze", + "Out sick" : "Chory", + "Vacationing" : "Na wakacjach", + "Out of office" : "Biuro nie funkcjonuje", + "Working remotely" : "Praca zdalna", + "In a call" : "Rozmawia", + "Be right back" : "Zaraz wracam", + "User status" : "Status użytkownika", + "Clear status after" : "Wyczyść status po", + "Emoji for your status message" : "Emoji dla komunikatu o statusie", + "What is your status?" : "Jaki jest Twój status?", + "Predefined statuses" : "Predefiniowane statusy", + "Previously set" : "Ustawione wcześniej", + "Reset status" : "Zresetuj status", + "Reset status to \"{icon} {message}\"" : "Zresetuj status do \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Zresetuj status do \"{message}\"", + "Reset status to \"{icon}\"" : "Zresetuj status do \"{icon}\"", + "There was an error saving the status" : "Wystąpił błąd podczas zapisywania statusu", + "There was an error clearing the status" : "Wystąpił błąd podczas usuwania statusu", + "There was an error reverting the status" : "Podczas przywracania statusu wystąpił błąd", + "Online status" : "Status online", + "Status message" : "Komunikat statusu", + "Set absence period" : "Ustaw okres nieobecności", + "Set absence period and replacement" : "Ustaw okres nieobecności i zastępstwo", + "Your status was set automatically" : "Twój status został ustawiony automatycznie", + "Clear status message" : "Wyczyść komunikat statusu", + "Set status message" : "Ustaw komunikat statusu", + "Don't clear" : "Nie czyść", + "Today" : "Dzisiaj", + "This week" : "W tym tygodniu", + "Online" : "Online", + "Away" : "Bezczynny", + "Do not disturb" : "Nie przeszkadzać", + "Invisible" : "Niewidoczny", + "Offline" : "Offline", + "Set status" : "Ustaw status", + "There was an error saving the new status" : "Wystąpił błąd podczas zapisywania nowego statusu", + "30 minutes" : "30 minut", + "1 hour" : "1 godzina", + "4 hours" : "4 godziny", + "Busy" : "Brak dostępności", + "Mute all notifications" : "Wycisz wszystkie powiadomienia", + "Appear offline" : "Widnieje jako offline" +},"pluralForm" :"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/pt_BR.js b/apps/user_status/l10n/pt_BR.js new file mode 100644 index 00000000000..3844bd746f7 --- /dev/null +++ b/apps/user_status/l10n/pt_BR.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Status recentes", + "No recent status changes" : "Sem alterações de status recentes", + "In a meeting" : "Em reunião", + "Commuting" : "Em trânsito", + "Out sick" : "Doente", + "Vacationing" : "Férias", + "Out of office" : "Fora do escritório", + "Working remotely" : "Em trabalho remoto", + "In a call" : "Em uma chamada", + "Be right back" : "Volto já", + "User status" : "Status do usuário", + "Clear status after" : "Limpar status após", + "Emoji for your status message" : "Emoji para sua mensagem de status", + "What is your status?" : "Qual é o seu status?", + "Predefined statuses" : "Status predefinidos", + "Previously set" : "Definido anteriormente", + "Reset status" : "Redefinir status", + "Reset status to \"{icon} {message}\"" : "Redefinir status para \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Redefinir status para \"{message}\"", + "Reset status to \"{icon}\"" : "Redefinir status para \"{icon}\"", + "There was an error saving the status" : "Ocorreu um erro ao salvar o status", + "There was an error clearing the status" : "Ocorreu um erro ao limpar o status", + "There was an error reverting the status" : "Ocorreu um erro ao reverter o status", + "Online status" : "Status on-line", + "Status message" : "Mensagem de status", + "Set absence period" : "Definir período de ausência", + "Set absence period and replacement" : "Definir período de ausência e substituição", + "Your status was set automatically" : "Seu status foi definido automaticamente", + "Clear status message" : "Limpar mensagem de status", + "Set status message" : "Definir mensagem de status", + "Don't clear" : "Não limpe", + "Today" : "Hoje", + "This week" : "Esta semana", + "Online" : "On-line", + "Away" : "Fora", + "Do not disturb" : "Não perturbe", + "Invisible" : "Invisível", + "Offline" : "Off-line", + "Set status" : "Definir status", + "There was an error saving the new status" : "Ocorreu um erro ao salvar o novo status", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 horas", + "Busy" : "Ocupado", + "Mute all notifications" : "Silenciar todas as notificações", + "Appear offline" : "Aparecer off-line" +}, +"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/user_status/l10n/pt_BR.json b/apps/user_status/l10n/pt_BR.json new file mode 100644 index 00000000000..e596a39e2bf --- /dev/null +++ b/apps/user_status/l10n/pt_BR.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "Status recentes", + "No recent status changes" : "Sem alterações de status recentes", + "In a meeting" : "Em reunião", + "Commuting" : "Em trânsito", + "Out sick" : "Doente", + "Vacationing" : "Férias", + "Out of office" : "Fora do escritório", + "Working remotely" : "Em trabalho remoto", + "In a call" : "Em uma chamada", + "Be right back" : "Volto já", + "User status" : "Status do usuário", + "Clear status after" : "Limpar status após", + "Emoji for your status message" : "Emoji para sua mensagem de status", + "What is your status?" : "Qual é o seu status?", + "Predefined statuses" : "Status predefinidos", + "Previously set" : "Definido anteriormente", + "Reset status" : "Redefinir status", + "Reset status to \"{icon} {message}\"" : "Redefinir status para \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Redefinir status para \"{message}\"", + "Reset status to \"{icon}\"" : "Redefinir status para \"{icon}\"", + "There was an error saving the status" : "Ocorreu um erro ao salvar o status", + "There was an error clearing the status" : "Ocorreu um erro ao limpar o status", + "There was an error reverting the status" : "Ocorreu um erro ao reverter o status", + "Online status" : "Status on-line", + "Status message" : "Mensagem de status", + "Set absence period" : "Definir período de ausência", + "Set absence period and replacement" : "Definir período de ausência e substituição", + "Your status was set automatically" : "Seu status foi definido automaticamente", + "Clear status message" : "Limpar mensagem de status", + "Set status message" : "Definir mensagem de status", + "Don't clear" : "Não limpe", + "Today" : "Hoje", + "This week" : "Esta semana", + "Online" : "On-line", + "Away" : "Fora", + "Do not disturb" : "Não perturbe", + "Invisible" : "Invisível", + "Offline" : "Off-line", + "Set status" : "Definir status", + "There was an error saving the new status" : "Ocorreu um erro ao salvar o novo status", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 horas", + "Busy" : "Ocupado", + "Mute all notifications" : "Silenciar todas as notificações", + "Appear offline" : "Aparecer off-line" +},"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/pt_PT.js b/apps/user_status/l10n/pt_PT.js new file mode 100644 index 00000000000..e6b64625619 --- /dev/null +++ b/apps/user_status/l10n/pt_PT.js @@ -0,0 +1,39 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Estados recentes", + "No recent status changes" : "Sem alterações de estado recentes", + "In a meeting" : "Numa reunião", + "Commuting" : "Em trânsito", + "Out sick" : "Doente", + "Vacationing" : "Férias", + "Out of office" : "Fora do escritório", + "Working remotely" : "A trabalhar à distância", + "In a call" : "Numa chamada", + "User status" : "Estado do utilizador", + "Clear status after" : "Limpar mensagem de estado após", + "What is your status?" : "Qual é o seu estado?", + "There was an error saving the status" : "Ocorreu um erro ao guardar o estado", + "There was an error clearing the status" : "Ocorreu um erro ao apagar o estado", + "Online status" : "Estado online", + "Status message" : "Mensagem de estado", + "Clear status message" : "Limpar mensagem de estado", + "Set status message" : "Definir mensagem de estado", + "Don't clear" : "Não apagar", + "Today" : "Hoje", + "This week" : "Esta semana", + "Online" : "Online", + "Away" : "Ausente", + "Do not disturb" : "Não incomodar", + "Invisible" : "Invisível ", + "Offline" : "Offline", + "Set status" : "Definir estado", + "There was an error saving the new status" : "Ocorreu um erro ao guardar o novo estado", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 horas", + "Busy" : "Ocupado", + "Mute all notifications" : "Desativar todas as notificações", + "Appear offline" : "Aparecer offline" +}, +"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/user_status/l10n/pt_PT.json b/apps/user_status/l10n/pt_PT.json new file mode 100644 index 00000000000..17762a3db45 --- /dev/null +++ b/apps/user_status/l10n/pt_PT.json @@ -0,0 +1,37 @@ +{ "translations": { + "Recent statuses" : "Estados recentes", + "No recent status changes" : "Sem alterações de estado recentes", + "In a meeting" : "Numa reunião", + "Commuting" : "Em trânsito", + "Out sick" : "Doente", + "Vacationing" : "Férias", + "Out of office" : "Fora do escritório", + "Working remotely" : "A trabalhar à distância", + "In a call" : "Numa chamada", + "User status" : "Estado do utilizador", + "Clear status after" : "Limpar mensagem de estado após", + "What is your status?" : "Qual é o seu estado?", + "There was an error saving the status" : "Ocorreu um erro ao guardar o estado", + "There was an error clearing the status" : "Ocorreu um erro ao apagar o estado", + "Online status" : "Estado online", + "Status message" : "Mensagem de estado", + "Clear status message" : "Limpar mensagem de estado", + "Set status message" : "Definir mensagem de estado", + "Don't clear" : "Não apagar", + "Today" : "Hoje", + "This week" : "Esta semana", + "Online" : "Online", + "Away" : "Ausente", + "Do not disturb" : "Não incomodar", + "Invisible" : "Invisível ", + "Offline" : "Offline", + "Set status" : "Definir estado", + "There was an error saving the new status" : "Ocorreu um erro ao guardar o novo estado", + "30 minutes" : "30 minutos", + "1 hour" : "1 hora", + "4 hours" : "4 horas", + "Busy" : "Ocupado", + "Mute all notifications" : "Desativar todas as notificações", + "Appear offline" : "Aparecer offline" +},"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/ro.js b/apps/user_status/l10n/ro.js new file mode 100644 index 00000000000..3214bdf6d73 --- /dev/null +++ b/apps/user_status/l10n/ro.js @@ -0,0 +1,48 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Statusuri recente", + "No recent status changes" : "Nu există modificări recente ale statutului", + "In a meeting" : "În cadrul unei întâlniri", + "Commuting" : "În deplasare", + "Out sick" : "Bolnav", + "Vacationing" : "În vacanță", + "Out of office" : "În afara serviciului", + "Working remotely" : "Lucru la distanță", + "In a call" : "Într-un apel", + "User status" : "Statusul utilizatorului", + "Clear status after" : "Șterge statusul după", + "Emoji for your status message" : "Emoji pentru mesajul de status", + "What is your status?" : "Care este statusul dumneavoastră?", + "Predefined statuses" : "Stări predefinite", + "Previously set" : "Setat anterior", + "Reset status" : "Resetează starea", + "Reset status to \"{icon} {message}\"" : "Resetează statusul la \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Resetează statusul la \"{message}\"", + "Reset status to \"{icon}\"" : "Resetează statusul la \"{icon}\"", + "There was an error saving the status" : "S-a produs o eroare la salvarea stării", + "There was an error clearing the status" : "S-a produs o eroare de ștergere a statutului", + "There was an error reverting the status" : "Eroare la revenirea la statusul anterior", + "Online status" : "Status online", + "Status message" : "Mesaj de status", + "Your status was set automatically" : "Statusul a fost setat automat", + "Clear status message" : "Șterge mesajul de stare", + "Set status message" : "Setează mesajul de status", + "Don't clear" : "Nu curăța", + "Today" : "Azi", + "This week" : "Săptămâna asta", + "Online" : "Online", + "Away" : "Plecat", + "Do not disturb" : "Nu deranja", + "Invisible" : "Invizibil", + "Offline" : "Offline", + "Set status" : "Setează status", + "There was an error saving the new status" : "S-a produs o eroare de salvare a noului status", + "30 minutes" : "30 minute", + "1 hour" : "1 oră", + "4 hours" : "4 ore", + "Busy" : "Ocupat", + "Mute all notifications" : "Dezactivați toate notificările", + "Appear offline" : "Apari deconectat" +}, +"nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));"); diff --git a/apps/user_status/l10n/ro.json b/apps/user_status/l10n/ro.json new file mode 100644 index 00000000000..292a2aaac70 --- /dev/null +++ b/apps/user_status/l10n/ro.json @@ -0,0 +1,46 @@ +{ "translations": { + "Recent statuses" : "Statusuri recente", + "No recent status changes" : "Nu există modificări recente ale statutului", + "In a meeting" : "În cadrul unei întâlniri", + "Commuting" : "În deplasare", + "Out sick" : "Bolnav", + "Vacationing" : "În vacanță", + "Out of office" : "În afara serviciului", + "Working remotely" : "Lucru la distanță", + "In a call" : "Într-un apel", + "User status" : "Statusul utilizatorului", + "Clear status after" : "Șterge statusul după", + "Emoji for your status message" : "Emoji pentru mesajul de status", + "What is your status?" : "Care este statusul dumneavoastră?", + "Predefined statuses" : "Stări predefinite", + "Previously set" : "Setat anterior", + "Reset status" : "Resetează starea", + "Reset status to \"{icon} {message}\"" : "Resetează statusul la \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Resetează statusul la \"{message}\"", + "Reset status to \"{icon}\"" : "Resetează statusul la \"{icon}\"", + "There was an error saving the status" : "S-a produs o eroare la salvarea stării", + "There was an error clearing the status" : "S-a produs o eroare de ștergere a statutului", + "There was an error reverting the status" : "Eroare la revenirea la statusul anterior", + "Online status" : "Status online", + "Status message" : "Mesaj de status", + "Your status was set automatically" : "Statusul a fost setat automat", + "Clear status message" : "Șterge mesajul de stare", + "Set status message" : "Setează mesajul de status", + "Don't clear" : "Nu curăța", + "Today" : "Azi", + "This week" : "Săptămâna asta", + "Online" : "Online", + "Away" : "Plecat", + "Do not disturb" : "Nu deranja", + "Invisible" : "Invizibil", + "Offline" : "Offline", + "Set status" : "Setează status", + "There was an error saving the new status" : "S-a produs o eroare de salvare a noului status", + "30 minutes" : "30 minute", + "1 hour" : "1 oră", + "4 hours" : "4 ore", + "Busy" : "Ocupat", + "Mute all notifications" : "Dezactivați toate notificările", + "Appear offline" : "Apari deconectat" +},"pluralForm" :"nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/ru.js b/apps/user_status/l10n/ru.js new file mode 100644 index 00000000000..32b784b5e0c --- /dev/null +++ b/apps/user_status/l10n/ru.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Недавние статусы", + "No recent status changes" : "Нет недавних изменений статуса", + "In a meeting" : "На встрече", + "Commuting" : "В пути", + "Out sick" : "Болен", + "Vacationing" : "В отпуске", + "Out of office" : "Вне офиса", + "Working remotely" : "Удалённо", + "In a call" : "В вызове", + "Be right back" : "Скоро вернусь", + "User status" : "Статус пользователя", + "Clear status after" : "Очистить статус после", + "Emoji for your status message" : "Эмодзи для вашего сообщения к статусу", + "What is your status?" : "Какой у Вас статус?", + "Predefined statuses" : "Предопределенные статусы", + "Previously set" : "Установлено ранее", + "Reset status" : "Сбросить статус", + "Reset status to \"{icon} {message}\"" : "Сбросить статус на \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Сбросить статус на \"{message}\"", + "Reset status to \"{icon}\"" : "Сбросить статус на \"{icon}\"", + "There was an error saving the status" : "Произошла ошибка при сохранении статуса", + "There was an error clearing the status" : "Произошла ошибка при удалении статуса", + "There was an error reverting the status" : "Произошла ошибка при сбросе статуса", + "Online status" : "Онлайн статус", + "Status message" : "Описание статуса", + "Set absence period" : "Задать период отсутствия", + "Set absence period and replacement" : "Задать период отсутствия и замену", + "Your status was set automatically" : "Ваш статус был установлен автоматически", + "Clear status message" : "Удалить сообщение к статусу", + "Set status message" : "Установить сообщение к статусу", + "Don't clear" : "Не очищать", + "Today" : "Сегодня", + "This week" : "Эта неделя", + "Online" : "В сети", + "Away" : "Неактивен", + "Do not disturb" : "Не беспокоить", + "Invisible" : "Невидимый", + "Offline" : "Не в сети", + "Set status" : "Установить статус", + "There was an error saving the new status" : "Произошла ошибка при сохранении нового статуса", + "30 minutes" : "30 минут", + "1 hour" : "1 час", + "4 hours" : "4 часа", + "Busy" : "Занят", + "Mute all notifications" : "Отключить все уведомления", + "Appear offline" : "\"Не в сети\" для остальных" +}, +"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"); diff --git a/apps/user_status/l10n/ru.json b/apps/user_status/l10n/ru.json new file mode 100644 index 00000000000..f6b8d241ac4 --- /dev/null +++ b/apps/user_status/l10n/ru.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "Недавние статусы", + "No recent status changes" : "Нет недавних изменений статуса", + "In a meeting" : "На встрече", + "Commuting" : "В пути", + "Out sick" : "Болен", + "Vacationing" : "В отпуске", + "Out of office" : "Вне офиса", + "Working remotely" : "Удалённо", + "In a call" : "В вызове", + "Be right back" : "Скоро вернусь", + "User status" : "Статус пользователя", + "Clear status after" : "Очистить статус после", + "Emoji for your status message" : "Эмодзи для вашего сообщения к статусу", + "What is your status?" : "Какой у Вас статус?", + "Predefined statuses" : "Предопределенные статусы", + "Previously set" : "Установлено ранее", + "Reset status" : "Сбросить статус", + "Reset status to \"{icon} {message}\"" : "Сбросить статус на \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Сбросить статус на \"{message}\"", + "Reset status to \"{icon}\"" : "Сбросить статус на \"{icon}\"", + "There was an error saving the status" : "Произошла ошибка при сохранении статуса", + "There was an error clearing the status" : "Произошла ошибка при удалении статуса", + "There was an error reverting the status" : "Произошла ошибка при сбросе статуса", + "Online status" : "Онлайн статус", + "Status message" : "Описание статуса", + "Set absence period" : "Задать период отсутствия", + "Set absence period and replacement" : "Задать период отсутствия и замену", + "Your status was set automatically" : "Ваш статус был установлен автоматически", + "Clear status message" : "Удалить сообщение к статусу", + "Set status message" : "Установить сообщение к статусу", + "Don't clear" : "Не очищать", + "Today" : "Сегодня", + "This week" : "Эта неделя", + "Online" : "В сети", + "Away" : "Неактивен", + "Do not disturb" : "Не беспокоить", + "Invisible" : "Невидимый", + "Offline" : "Не в сети", + "Set status" : "Установить статус", + "There was an error saving the new status" : "Произошла ошибка при сохранении нового статуса", + "30 minutes" : "30 минут", + "1 hour" : "1 час", + "4 hours" : "4 часа", + "Busy" : "Занят", + "Mute all notifications" : "Отключить все уведомления", + "Appear offline" : "\"Не в сети\" для остальных" +},"pluralForm" :"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/sc.js b/apps/user_status/l10n/sc.js new file mode 100644 index 00000000000..d4836447296 --- /dev/null +++ b/apps/user_status/l10n/sc.js @@ -0,0 +1,38 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Istados reghentes", + "No recent status changes" : "Perunu càmbiu de istadu reghente", + "In a meeting" : "In riunione", + "Commuting" : "Biagende", + "Out sick" : "In maladia", + "Vacationing" : "In vacàntzia", + "Out of office" : "Foras de serbìtziu", + "Working remotely" : "Traballende in remotu", + "User status" : "Istadu de s'utente", + "Clear status after" : "Lìmpia s'istadu a pustis", + "What is your status?" : "Cale est s'istadu tuo?", + "There was an error saving the status" : "B'at àpidu un'errore sarvende s'istadu", + "There was an error clearing the status" : "B'at àpidu un'errore limpiende s'istadu", + "Online status" : "Istadu in lìnia", + "Status message" : "Messàgiu de istadu", + "Clear status message" : "Lìmpia su messàgiu de istadu", + "Set status message" : "Cunfigura su messàgiu de istadu", + "Don't clear" : "Non nche ddu lìmpies", + "Today" : "Oe", + "This week" : "Custa chida", + "Online" : "In lìnia", + "Away" : "Ausente", + "Do not disturb" : "No istorbes", + "Invisible" : "Invisìbile", + "Offline" : "Fora de lìnia", + "Set status" : "Cunfigura un'istadu", + "There was an error saving the new status" : "B'at àpidu un'errore sarvende s'istadu nou", + "30 minutes" : "30 minutos", + "1 hour" : "1 ora", + "4 hours" : "4 oras", + "Busy" : "Impinnadu", + "Mute all notifications" : "Istuda totu is notìficas", + "Appear offline" : "Mustra•ti foras de lìnia" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/sc.json b/apps/user_status/l10n/sc.json new file mode 100644 index 00000000000..9bec309186e --- /dev/null +++ b/apps/user_status/l10n/sc.json @@ -0,0 +1,36 @@ +{ "translations": { + "Recent statuses" : "Istados reghentes", + "No recent status changes" : "Perunu càmbiu de istadu reghente", + "In a meeting" : "In riunione", + "Commuting" : "Biagende", + "Out sick" : "In maladia", + "Vacationing" : "In vacàntzia", + "Out of office" : "Foras de serbìtziu", + "Working remotely" : "Traballende in remotu", + "User status" : "Istadu de s'utente", + "Clear status after" : "Lìmpia s'istadu a pustis", + "What is your status?" : "Cale est s'istadu tuo?", + "There was an error saving the status" : "B'at àpidu un'errore sarvende s'istadu", + "There was an error clearing the status" : "B'at àpidu un'errore limpiende s'istadu", + "Online status" : "Istadu in lìnia", + "Status message" : "Messàgiu de istadu", + "Clear status message" : "Lìmpia su messàgiu de istadu", + "Set status message" : "Cunfigura su messàgiu de istadu", + "Don't clear" : "Non nche ddu lìmpies", + "Today" : "Oe", + "This week" : "Custa chida", + "Online" : "In lìnia", + "Away" : "Ausente", + "Do not disturb" : "No istorbes", + "Invisible" : "Invisìbile", + "Offline" : "Fora de lìnia", + "Set status" : "Cunfigura un'istadu", + "There was an error saving the new status" : "B'at àpidu un'errore sarvende s'istadu nou", + "30 minutes" : "30 minutos", + "1 hour" : "1 ora", + "4 hours" : "4 oras", + "Busy" : "Impinnadu", + "Mute all notifications" : "Istuda totu is notìficas", + "Appear offline" : "Mustra•ti foras de lìnia" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/sk.js b/apps/user_status/l10n/sk.js new file mode 100644 index 00000000000..9d91578b0f2 --- /dev/null +++ b/apps/user_status/l10n/sk.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Nedávne stavy", + "No recent status changes" : "Žiadne nedávne zmeny stavu", + "In a meeting" : "Na schôdzke", + "Commuting" : "Na ceste", + "Out sick" : "Choroba", + "Vacationing" : "Na dovolenke", + "Out of office" : "Mimo kancelárie", + "Working remotely" : "Pracujem na diaľku", + "In a call" : "práve telefonuje", + "User status" : "Stav užívateľa", + "Clear status after" : "Vyčistiť správu o stave po", + "Emoji for your status message" : "Emoji pre vašu statusovú správu", + "What is your status?" : "Aký je váš stav?", + "Predefined statuses" : "Preddefinované statusy", + "Previously set" : "Predtým nastavené", + "Reset status" : "Obnoviť status", + "Reset status to \"{icon} {message}\"" : "Obnoviť stav na „{icon} {message}“", + "Reset status to \"{message}\"" : "Obnoviť stav na „{message}“", + "Reset status to \"{icon}\"" : "Obnoviť stav na „{icon}“", + "There was an error saving the status" : "Pri ukladaní stavu sa vyskytla chyba", + "There was an error clearing the status" : "Pri čistení stavu sa vyskytla chyba", + "There was an error reverting the status" : "Pri zmene statusu sa vyskytla chyba", + "Online status" : "Stav pripojenia", + "Status message" : "Správa o stave", + "Set absence period" : "Nastaviť dobu neprítomnosti", + "Set absence period and replacement" : "Nastaviť dobu neprítomnosti a svoju náhradu", + "Your status was set automatically" : "Váš status bol nastavený automaticky", + "Clear status message" : "Vyčistiť správu o stave", + "Set status message" : "Nastaviť správu o stave", + "Don't clear" : "Nemazať", + "Today" : "Dnes", + "This week" : "Tento týždeň", + "Online" : "Pripojené", + "Away" : "Preč", + "Do not disturb" : "Nerušiť", + "Invisible" : "Neviditeľnosť", + "Offline" : "Offline", + "Set status" : "Nastaviť stav", + "There was an error saving the new status" : "Pri ukladaní nového stavu sa vyskytla chyba", + "30 minutes" : "30 minút", + "1 hour" : "1 hodina", + "4 hours" : "4 hodiny", + "Busy" : "Zaneprázdnený", + "Mute all notifications" : "Stíšiť všetky upozornenia", + "Appear offline" : "V odpojenom režime" +}, +"nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);"); diff --git a/apps/user_status/l10n/sk.json b/apps/user_status/l10n/sk.json new file mode 100644 index 00000000000..dffd39b9e8c --- /dev/null +++ b/apps/user_status/l10n/sk.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Nedávne stavy", + "No recent status changes" : "Žiadne nedávne zmeny stavu", + "In a meeting" : "Na schôdzke", + "Commuting" : "Na ceste", + "Out sick" : "Choroba", + "Vacationing" : "Na dovolenke", + "Out of office" : "Mimo kancelárie", + "Working remotely" : "Pracujem na diaľku", + "In a call" : "práve telefonuje", + "User status" : "Stav užívateľa", + "Clear status after" : "Vyčistiť správu o stave po", + "Emoji for your status message" : "Emoji pre vašu statusovú správu", + "What is your status?" : "Aký je váš stav?", + "Predefined statuses" : "Preddefinované statusy", + "Previously set" : "Predtým nastavené", + "Reset status" : "Obnoviť status", + "Reset status to \"{icon} {message}\"" : "Obnoviť stav na „{icon} {message}“", + "Reset status to \"{message}\"" : "Obnoviť stav na „{message}“", + "Reset status to \"{icon}\"" : "Obnoviť stav na „{icon}“", + "There was an error saving the status" : "Pri ukladaní stavu sa vyskytla chyba", + "There was an error clearing the status" : "Pri čistení stavu sa vyskytla chyba", + "There was an error reverting the status" : "Pri zmene statusu sa vyskytla chyba", + "Online status" : "Stav pripojenia", + "Status message" : "Správa o stave", + "Set absence period" : "Nastaviť dobu neprítomnosti", + "Set absence period and replacement" : "Nastaviť dobu neprítomnosti a svoju náhradu", + "Your status was set automatically" : "Váš status bol nastavený automaticky", + "Clear status message" : "Vyčistiť správu o stave", + "Set status message" : "Nastaviť správu o stave", + "Don't clear" : "Nemazať", + "Today" : "Dnes", + "This week" : "Tento týždeň", + "Online" : "Pripojené", + "Away" : "Preč", + "Do not disturb" : "Nerušiť", + "Invisible" : "Neviditeľnosť", + "Offline" : "Offline", + "Set status" : "Nastaviť stav", + "There was an error saving the new status" : "Pri ukladaní nového stavu sa vyskytla chyba", + "30 minutes" : "30 minút", + "1 hour" : "1 hodina", + "4 hours" : "4 hodiny", + "Busy" : "Zaneprázdnený", + "Mute all notifications" : "Stíšiť všetky upozornenia", + "Appear offline" : "V odpojenom režime" +},"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/sl.js b/apps/user_status/l10n/sl.js new file mode 100644 index 00000000000..0eb3b4a6d14 --- /dev/null +++ b/apps/user_status/l10n/sl.js @@ -0,0 +1,48 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Nedavna stanja", + "No recent status changes" : "Ni nedavnih sprememb stanja", + "In a meeting" : "Na sestanku", + "Commuting" : "Med vožnjo", + "Out sick" : "Na bolniški", + "Vacationing" : "Na dopustu", + "Out of office" : "Službena odsotnost", + "Working remotely" : "Delam od doma", + "In a call" : "V klicu", + "User status" : "Stanje uporabnika", + "Clear status after" : "Počisti stanje", + "Emoji for your status message" : "Izrazne ikone za stanje sporočila", + "What is your status?" : "Kako želite nastaviti stanje?", + "Predefined statuses" : "Pripravljena stanja", + "Previously set" : "Predhodno nastavljeno", + "Reset status" : "Ponastavi stanje", + "Reset status to \"{icon} {message}\"" : "Ponastavi stanje na »{icon} {message}«", + "Reset status to \"{message}\"" : "Ponastavi stanje na »{message}«", + "Reset status to \"{icon}\"" : "Ponastavi stanje na »{icon}«", + "There was an error saving the status" : "Prišlo je do napake med shranjevanjem stanja", + "There was an error clearing the status" : "Prišlo je do napake med odstranjevanjem stanja", + "There was an error reverting the status" : "Prišlo je do napake spreminjanja stanja", + "Online status" : "Povezano stanje", + "Status message" : "Sporočilo stanja", + "Your status was set automatically" : "Stanje je določeno samodejno", + "Clear status message" : "Počisti sporočilo stanja", + "Set status message" : "Nastavi sporočilo stanja", + "Don't clear" : "ne počisti", + "Today" : "enkrat danes", + "This week" : "še ta teden", + "Online" : "Na spletu", + "Away" : "Trenutno ne spremljam", + "Do not disturb" : "Ne pustim se motiti", + "Invisible" : "Drugim neviden", + "Offline" : "Brez povezave", + "Set status" : "Nastavi stanje", + "There was an error saving the new status" : "Prišlo je do napake med shranjevanjem novega stanja", + "30 minutes" : "po 30 minutah", + "1 hour" : "po 1 uri", + "4 hours" : "po 4 urah", + "Busy" : "Zasedeno", + "Mute all notifications" : "Utiša vsa obvestila", + "Appear offline" : "Pokaže kot brez povezave" +}, +"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"); diff --git a/apps/user_status/l10n/sl.json b/apps/user_status/l10n/sl.json new file mode 100644 index 00000000000..4e0c8582227 --- /dev/null +++ b/apps/user_status/l10n/sl.json @@ -0,0 +1,46 @@ +{ "translations": { + "Recent statuses" : "Nedavna stanja", + "No recent status changes" : "Ni nedavnih sprememb stanja", + "In a meeting" : "Na sestanku", + "Commuting" : "Med vožnjo", + "Out sick" : "Na bolniški", + "Vacationing" : "Na dopustu", + "Out of office" : "Službena odsotnost", + "Working remotely" : "Delam od doma", + "In a call" : "V klicu", + "User status" : "Stanje uporabnika", + "Clear status after" : "Počisti stanje", + "Emoji for your status message" : "Izrazne ikone za stanje sporočila", + "What is your status?" : "Kako želite nastaviti stanje?", + "Predefined statuses" : "Pripravljena stanja", + "Previously set" : "Predhodno nastavljeno", + "Reset status" : "Ponastavi stanje", + "Reset status to \"{icon} {message}\"" : "Ponastavi stanje na »{icon} {message}«", + "Reset status to \"{message}\"" : "Ponastavi stanje na »{message}«", + "Reset status to \"{icon}\"" : "Ponastavi stanje na »{icon}«", + "There was an error saving the status" : "Prišlo je do napake med shranjevanjem stanja", + "There was an error clearing the status" : "Prišlo je do napake med odstranjevanjem stanja", + "There was an error reverting the status" : "Prišlo je do napake spreminjanja stanja", + "Online status" : "Povezano stanje", + "Status message" : "Sporočilo stanja", + "Your status was set automatically" : "Stanje je določeno samodejno", + "Clear status message" : "Počisti sporočilo stanja", + "Set status message" : "Nastavi sporočilo stanja", + "Don't clear" : "ne počisti", + "Today" : "enkrat danes", + "This week" : "še ta teden", + "Online" : "Na spletu", + "Away" : "Trenutno ne spremljam", + "Do not disturb" : "Ne pustim se motiti", + "Invisible" : "Drugim neviden", + "Offline" : "Brez povezave", + "Set status" : "Nastavi stanje", + "There was an error saving the new status" : "Prišlo je do napake med shranjevanjem novega stanja", + "30 minutes" : "po 30 minutah", + "1 hour" : "po 1 uri", + "4 hours" : "po 4 urah", + "Busy" : "Zasedeno", + "Mute all notifications" : "Utiša vsa obvestila", + "Appear offline" : "Pokaže kot brez povezave" +},"pluralForm" :"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/sr.js b/apps/user_status/l10n/sr.js new file mode 100644 index 00000000000..8c94ab10e2e --- /dev/null +++ b/apps/user_status/l10n/sr.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Скорашњи статуси", + "No recent status changes" : "Нема скорашњих измена статуса", + "In a meeting" : "На састанку", + "Commuting" : "На путу до посла", + "Out sick" : "На боловању", + "Vacationing" : "На одмору", + "Out of office" : "Ван канцеларије", + "Working remotely" : "Радим од куће", + "In a call" : "У позиву", + "User status" : "Корисников статус", + "Clear status after" : "Обриши статус након", + "Emoji for your status message" : "Емођи за вашу статусну поруку", + "What is your status?" : "Који је ваш статус?", + "Predefined statuses" : "Предефинисани статуси", + "Previously set" : "Претходно постављено", + "Reset status" : "Ресетуј статус", + "Reset status to \"{icon} {message}\"" : "Ресетуј статус на „{icon} {message}”", + "Reset status to \"{message}\"" : "Ресетуј статус на „{message}”", + "Reset status to \"{icon}\"" : "Ресетуј статус на „{icon}”", + "There was an error saving the status" : "Дошло је до грешке приликом чувања статуса", + "There was an error clearing the status" : "Дошло је до грешке приликом брисања статуса", + "There was an error reverting the status" : "Дошло је до грешке приликом враћања претходног статуса", + "Online status" : "Мрежни статус", + "Status message" : "Порука стања", + "Set absence period" : "Постави период одсутности", + "Set absence period and replacement" : "Постави период одсутности и замену", + "Your status was set automatically" : "Ваш статус је аутоматски постављен", + "Clear status message" : "Обриши статусну поруку", + "Set status message" : "Постављање статусне поруке", + "Don't clear" : "Не бриши", + "Today" : "Данас", + "This week" : "Ове недеље", + "Online" : "На мрежи", + "Away" : "Одсутан", + "Do not disturb" : "Не узнемиравај", + "Invisible" : "Невидљива", + "Offline" : "Ван мреже", + "Set status" : "Постави статус", + "There was an error saving the new status" : "Дошло је до грешке приликом чувања новог статуса", + "30 minutes" : "30 минута", + "1 hour" : "1 сат", + "4 hours" : "4 сата", + "Busy" : "Заузет", + "Mute all notifications" : "Искључи сва обавештења", + "Appear offline" : "Прикажи као ван мреже" +}, +"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"); diff --git a/apps/user_status/l10n/sr.json b/apps/user_status/l10n/sr.json new file mode 100644 index 00000000000..a7953e5e0a3 --- /dev/null +++ b/apps/user_status/l10n/sr.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Скорашњи статуси", + "No recent status changes" : "Нема скорашњих измена статуса", + "In a meeting" : "На састанку", + "Commuting" : "На путу до посла", + "Out sick" : "На боловању", + "Vacationing" : "На одмору", + "Out of office" : "Ван канцеларије", + "Working remotely" : "Радим од куће", + "In a call" : "У позиву", + "User status" : "Корисников статус", + "Clear status after" : "Обриши статус након", + "Emoji for your status message" : "Емођи за вашу статусну поруку", + "What is your status?" : "Који је ваш статус?", + "Predefined statuses" : "Предефинисани статуси", + "Previously set" : "Претходно постављено", + "Reset status" : "Ресетуј статус", + "Reset status to \"{icon} {message}\"" : "Ресетуј статус на „{icon} {message}”", + "Reset status to \"{message}\"" : "Ресетуј статус на „{message}”", + "Reset status to \"{icon}\"" : "Ресетуј статус на „{icon}”", + "There was an error saving the status" : "Дошло је до грешке приликом чувања статуса", + "There was an error clearing the status" : "Дошло је до грешке приликом брисања статуса", + "There was an error reverting the status" : "Дошло је до грешке приликом враћања претходног статуса", + "Online status" : "Мрежни статус", + "Status message" : "Порука стања", + "Set absence period" : "Постави период одсутности", + "Set absence period and replacement" : "Постави период одсутности и замену", + "Your status was set automatically" : "Ваш статус је аутоматски постављен", + "Clear status message" : "Обриши статусну поруку", + "Set status message" : "Постављање статусне поруке", + "Don't clear" : "Не бриши", + "Today" : "Данас", + "This week" : "Ове недеље", + "Online" : "На мрежи", + "Away" : "Одсутан", + "Do not disturb" : "Не узнемиравај", + "Invisible" : "Невидљива", + "Offline" : "Ван мреже", + "Set status" : "Постави статус", + "There was an error saving the new status" : "Дошло је до грешке приликом чувања новог статуса", + "30 minutes" : "30 минута", + "1 hour" : "1 сат", + "4 hours" : "4 сата", + "Busy" : "Заузет", + "Mute all notifications" : "Искључи сва обавештења", + "Appear offline" : "Прикажи као ван мреже" +},"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/sr@latin.js b/apps/user_status/l10n/sr@latin.js new file mode 100644 index 00000000000..532e6651761 --- /dev/null +++ b/apps/user_status/l10n/sr@latin.js @@ -0,0 +1,47 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Poslednji statusi", + "No recent status changes" : "Nema skorašnjih promena statusa", + "In a meeting" : "Na sastanku", + "Commuting" : "Na putu do posla", + "Out sick" : "Na bolovanju", + "Vacationing" : "Na odmoru", + "Out of office" : "Van kancelarije", + "Working remotely" : "Radim od kuće", + "In a call" : "U pozivu", + "User status" : "Status korisnika", + "Clear status after" : "Obriši status nakon", + "Emoji for your status message" : "Emoji za vašu statusnu poruku", + "What is your status?" : "Koji je vaš status?", + "Predefined statuses" : "Predefinisani statusi", + "Previously set" : "Prethodno postavljeno", + "Reset status" : "Resetuj status", + "Reset status to \"{icon} {message}\"" : "Resetuj status na „{icon} {message}”", + "Reset status to \"{message}\"" : "Resertuj status na „{message}”", + "Reset status to \"{icon}\"" : "Resetuj status na „{icon}”", + "There was an error saving the status" : "Greška u snimanju statusa", + "There was an error clearing the status" : "Greška u brisanju statusa", + "There was an error reverting the status" : "Greška u vraćanju statusa", + "Online status" : "Mrežni status", + "Status message" : "Poruka stanja", + "Your status was set automatically" : "Vaš status je postavljen automatski", + "Clear status message" : "Obriši statusnu poruku", + "Set status message" : "Postavi statusnu poruku", + "Don't clear" : "Ne briši", + "Today" : "Danas", + "This week" : "Ove sedmice", + "Online" : "Na mreži", + "Away" : "Odsutan", + "Do not disturb" : "Ne uznemiravaj", + "Invisible" : "Nevidljiv", + "Offline" : "Van mreže", + "Set status" : "Postavi status", + "There was an error saving the new status" : "Greška u snimanju novog statusa", + "30 minutes" : "30 minuta", + "1 hour" : "1 sat", + "4 hours" : "4 sata", + "Mute all notifications" : "Isključi sva obaveštenja", + "Appear offline" : "Prikaži kao van mreže" +}, +"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"); diff --git a/apps/user_status/l10n/sr@latin.json b/apps/user_status/l10n/sr@latin.json new file mode 100644 index 00000000000..bb642d5708c --- /dev/null +++ b/apps/user_status/l10n/sr@latin.json @@ -0,0 +1,45 @@ +{ "translations": { + "Recent statuses" : "Poslednji statusi", + "No recent status changes" : "Nema skorašnjih promena statusa", + "In a meeting" : "Na sastanku", + "Commuting" : "Na putu do posla", + "Out sick" : "Na bolovanju", + "Vacationing" : "Na odmoru", + "Out of office" : "Van kancelarije", + "Working remotely" : "Radim od kuće", + "In a call" : "U pozivu", + "User status" : "Status korisnika", + "Clear status after" : "Obriši status nakon", + "Emoji for your status message" : "Emoji za vašu statusnu poruku", + "What is your status?" : "Koji je vaš status?", + "Predefined statuses" : "Predefinisani statusi", + "Previously set" : "Prethodno postavljeno", + "Reset status" : "Resetuj status", + "Reset status to \"{icon} {message}\"" : "Resetuj status na „{icon} {message}”", + "Reset status to \"{message}\"" : "Resertuj status na „{message}”", + "Reset status to \"{icon}\"" : "Resetuj status na „{icon}”", + "There was an error saving the status" : "Greška u snimanju statusa", + "There was an error clearing the status" : "Greška u brisanju statusa", + "There was an error reverting the status" : "Greška u vraćanju statusa", + "Online status" : "Mrežni status", + "Status message" : "Poruka stanja", + "Your status was set automatically" : "Vaš status je postavljen automatski", + "Clear status message" : "Obriši statusnu poruku", + "Set status message" : "Postavi statusnu poruku", + "Don't clear" : "Ne briši", + "Today" : "Danas", + "This week" : "Ove sedmice", + "Online" : "Na mreži", + "Away" : "Odsutan", + "Do not disturb" : "Ne uznemiravaj", + "Invisible" : "Nevidljiv", + "Offline" : "Van mreže", + "Set status" : "Postavi status", + "There was an error saving the new status" : "Greška u snimanju novog statusa", + "30 minutes" : "30 minuta", + "1 hour" : "1 sat", + "4 hours" : "4 sata", + "Mute all notifications" : "Isključi sva obaveštenja", + "Appear offline" : "Prikaži kao van mreže" +},"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/sv.js b/apps/user_status/l10n/sv.js new file mode 100644 index 00000000000..acd1b224d4b --- /dev/null +++ b/apps/user_status/l10n/sv.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Senaste statusuppdateringar", + "No recent status changes" : "Inga statusuppdateringar den sista tiden", + "In a meeting" : "På ett möte", + "Commuting" : "Reser", + "Out sick" : "Sjuk", + "Vacationing" : "På semester", + "Out of office" : "Ej på plats", + "Working remotely" : "Arbetar hemifrån", + "In a call" : "I ett samtal", + "User status" : "Användarstatus", + "Clear status after" : "Rensa status efter", + "Emoji for your status message" : "Emoji för ditt statusmeddelande", + "What is your status?" : "Vad är din status?", + "Predefined statuses" : "Fördefinierade statusar", + "Previously set" : "Tidigare inställd", + "Reset status" : "Återställ status", + "Reset status to \"{icon} {message}\"" : "Återställ status till \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Återställ status till \"{message}\"", + "Reset status to \"{icon}\"" : "Återställ status till \"{icon}\"", + "There was an error saving the status" : "Ett fel inträffade när statusen skulle ändras", + "There was an error clearing the status" : "Ett fel inträffade när statusen skulle rensas", + "There was an error reverting the status" : "Det gick inte att återställa statusen", + "Online status" : "Online-status", + "Status message" : "Statusmeddelande", + "Set absence period" : "Ställ in frånvaroperiod", + "Set absence period and replacement" : "Ställ in frånvaroperiod och ersättare", + "Your status was set automatically" : "Din status ställdes in automatiskt", + "Clear status message" : "Rensa statusmeddelande", + "Set status message" : "Sätt statusmeddelande", + "Don't clear" : "Rensa inte", + "Today" : "Idag", + "This week" : "Denna vecka", + "Online" : "Online", + "Away" : "Iväg", + "Do not disturb" : "Stör ej", + "Invisible" : "Osynlig", + "Offline" : "Frånkopplad", + "Set status" : "Sätt status", + "There was an error saving the new status" : "Ett fel inträffade när den nya statusen skulle sparas", + "30 minutes" : "30 minuter", + "1 hour" : "1 timme", + "4 hours" : "4 timmar", + "Busy" : "Upptagen", + "Mute all notifications" : "Dölj alla aviseringar", + "Appear offline" : "Visa som frånkopplad" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/sv.json b/apps/user_status/l10n/sv.json new file mode 100644 index 00000000000..502baef321b --- /dev/null +++ b/apps/user_status/l10n/sv.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Senaste statusuppdateringar", + "No recent status changes" : "Inga statusuppdateringar den sista tiden", + "In a meeting" : "På ett möte", + "Commuting" : "Reser", + "Out sick" : "Sjuk", + "Vacationing" : "På semester", + "Out of office" : "Ej på plats", + "Working remotely" : "Arbetar hemifrån", + "In a call" : "I ett samtal", + "User status" : "Användarstatus", + "Clear status after" : "Rensa status efter", + "Emoji for your status message" : "Emoji för ditt statusmeddelande", + "What is your status?" : "Vad är din status?", + "Predefined statuses" : "Fördefinierade statusar", + "Previously set" : "Tidigare inställd", + "Reset status" : "Återställ status", + "Reset status to \"{icon} {message}\"" : "Återställ status till \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Återställ status till \"{message}\"", + "Reset status to \"{icon}\"" : "Återställ status till \"{icon}\"", + "There was an error saving the status" : "Ett fel inträffade när statusen skulle ändras", + "There was an error clearing the status" : "Ett fel inträffade när statusen skulle rensas", + "There was an error reverting the status" : "Det gick inte att återställa statusen", + "Online status" : "Online-status", + "Status message" : "Statusmeddelande", + "Set absence period" : "Ställ in frånvaroperiod", + "Set absence period and replacement" : "Ställ in frånvaroperiod och ersättare", + "Your status was set automatically" : "Din status ställdes in automatiskt", + "Clear status message" : "Rensa statusmeddelande", + "Set status message" : "Sätt statusmeddelande", + "Don't clear" : "Rensa inte", + "Today" : "Idag", + "This week" : "Denna vecka", + "Online" : "Online", + "Away" : "Iväg", + "Do not disturb" : "Stör ej", + "Invisible" : "Osynlig", + "Offline" : "Frånkopplad", + "Set status" : "Sätt status", + "There was an error saving the new status" : "Ett fel inträffade när den nya statusen skulle sparas", + "30 minutes" : "30 minuter", + "1 hour" : "1 timme", + "4 hours" : "4 timmar", + "Busy" : "Upptagen", + "Mute all notifications" : "Dölj alla aviseringar", + "Appear offline" : "Visa som frånkopplad" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/sw.js b/apps/user_status/l10n/sw.js new file mode 100644 index 00000000000..69e9430b920 --- /dev/null +++ b/apps/user_status/l10n/sw.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Hali za hivi karibuni", + "No recent status changes" : "Hakuna mabadiliko ya hali ya hivi karibuni", + "In a meeting" : "Katika mkutano", + "Commuting" : "Kuelekea", + "Out sick" : "Nje mgonjwa", + "Vacationing" : "Likizo", + "Out of office" : "Nje ya ofisi", + "Working remotely" : "Kufanyia kazi mbali", + "In a call" : "Katika simu", + "Be right back" : "Rudi mara moja", + "User status" : "Hadhi ya mtumiaji", + "Clear status after" : "Futa hali baada ya", + "Emoji for your status message" : "Emoji kwa hali yako ya ujumbe", + "What is your status?" : "Hadhi yako ni nini?", + "Predefined statuses" : "Hali zilizoainishwa awali", + "Previously set" : "Imepangiliwa mwanzo", + "Reset status" : "Pangilia hali", + "Reset status to \"{icon} {message}\"" : " Weka upya hali kuwa \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Weka upya hali kuwa \"{message}\"", + "Reset status to \"{icon}\"" : "Weka upya hali kuwa \"{icon}\"", + "There was an error saving the status" : "Kulikuwa na hitilafu katika kuhifadhi hali", + "There was an error clearing the status" : "Kulikuwa na hitilafu katika kufuta hali", + "There was an error reverting the status" : "Kulikuwa na hitilafu katika kurejesha hali", + "Online status" : "Hali ya mtandaoni", + "Status message" : "Hali ya ujumbe", + "Set absence period" : "Weka kipindi cha kutokuwepo", + "Set absence period and replacement" : " Weka kipindi cha kutokuwepo na uingizwaji mbadala", + "Your status was set automatically" : "Hadhi yako ilipangiliwa moja kwa moja", + "Clear status message" : "Futa jumbe za wadhifa", + "Set status message" : "Pangilia hali ya ujumbe", + "Don't clear" : "Usifute", + "Today" : "Leo", + "This week" : "Wiki hii", + "Online" : "Mtandaoni", + "Away" : "Mbali", + "Do not disturb" : "Acha kusumbua", + "Invisible" : "Haionekani", + "Offline" : "Nje ya mtandao", + "Set status" : "Panglia hali", + "There was an error saving the new status" : "Kulikuwa na hitilafu katika kuhifadhi hali mpya", + "30 minutes" : "Dakika 30", + "1 hour" : "Saa 1", + "4 hours" : "Masaa 4", + "Busy" : "Bize", + "Mute all notifications" : "Zima arifu zote", + "Appear offline" : "Tokea nje ya mtandao" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/sw.json b/apps/user_status/l10n/sw.json new file mode 100644 index 00000000000..a106159c6fb --- /dev/null +++ b/apps/user_status/l10n/sw.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "Hali za hivi karibuni", + "No recent status changes" : "Hakuna mabadiliko ya hali ya hivi karibuni", + "In a meeting" : "Katika mkutano", + "Commuting" : "Kuelekea", + "Out sick" : "Nje mgonjwa", + "Vacationing" : "Likizo", + "Out of office" : "Nje ya ofisi", + "Working remotely" : "Kufanyia kazi mbali", + "In a call" : "Katika simu", + "Be right back" : "Rudi mara moja", + "User status" : "Hadhi ya mtumiaji", + "Clear status after" : "Futa hali baada ya", + "Emoji for your status message" : "Emoji kwa hali yako ya ujumbe", + "What is your status?" : "Hadhi yako ni nini?", + "Predefined statuses" : "Hali zilizoainishwa awali", + "Previously set" : "Imepangiliwa mwanzo", + "Reset status" : "Pangilia hali", + "Reset status to \"{icon} {message}\"" : " Weka upya hali kuwa \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Weka upya hali kuwa \"{message}\"", + "Reset status to \"{icon}\"" : "Weka upya hali kuwa \"{icon}\"", + "There was an error saving the status" : "Kulikuwa na hitilafu katika kuhifadhi hali", + "There was an error clearing the status" : "Kulikuwa na hitilafu katika kufuta hali", + "There was an error reverting the status" : "Kulikuwa na hitilafu katika kurejesha hali", + "Online status" : "Hali ya mtandaoni", + "Status message" : "Hali ya ujumbe", + "Set absence period" : "Weka kipindi cha kutokuwepo", + "Set absence period and replacement" : " Weka kipindi cha kutokuwepo na uingizwaji mbadala", + "Your status was set automatically" : "Hadhi yako ilipangiliwa moja kwa moja", + "Clear status message" : "Futa jumbe za wadhifa", + "Set status message" : "Pangilia hali ya ujumbe", + "Don't clear" : "Usifute", + "Today" : "Leo", + "This week" : "Wiki hii", + "Online" : "Mtandaoni", + "Away" : "Mbali", + "Do not disturb" : "Acha kusumbua", + "Invisible" : "Haionekani", + "Offline" : "Nje ya mtandao", + "Set status" : "Panglia hali", + "There was an error saving the new status" : "Kulikuwa na hitilafu katika kuhifadhi hali mpya", + "30 minutes" : "Dakika 30", + "1 hour" : "Saa 1", + "4 hours" : "Masaa 4", + "Busy" : "Bize", + "Mute all notifications" : "Zima arifu zote", + "Appear offline" : "Tokea nje ya mtandao" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/th.js b/apps/user_status/l10n/th.js new file mode 100644 index 00000000000..00bdced011d --- /dev/null +++ b/apps/user_status/l10n/th.js @@ -0,0 +1,36 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "สถานะล่าสุด", + "No recent status changes" : "ไม่มีการเปลี่ยนสถานะล่าสุด", + "In a meeting" : "กำลังประชุม", + "Commuting" : "กำลังเดินทาง", + "Out sick" : "ป่วย", + "Vacationing" : "วันหยุดพักผ่อน", + "Working remotely" : "ทำงานจากระยะไกล", + "User status" : "สถานะผู้ใช้", + "Clear status after" : "ล้างสถานะหลังจาก", + "What is your status?" : "สถานะของคุณ", + "There was an error saving the status" : "เกิดข้อผิดพลาดในการบันทึกสถานะ", + "There was an error clearing the status" : "เกิดข้อผิดพลาดในการลบสถานะ", + "Online status" : "สถานะออนไลน์", + "Status message" : "ข้อความสถานะ", + "Clear status message" : "ล้างข้อความสถานะ", + "Set status message" : "กำหนดข้อความสถานะ", + "Don't clear" : "ไม่ต้องล้าง", + "Today" : "วันนี้", + "This week" : "สัปดาห์นี้", + "Online" : "ออนไลน์", + "Away" : "ไม่อยู่", + "Do not disturb" : "ห้ามรบกวน", + "Invisible" : "ไม่แสดงสถานะ", + "Offline" : "ออฟไลน์", + "Set status" : "กำหนดสถานะ", + "There was an error saving the new status" : "เกิดข้อผิดพลาดในการบันทึกสถานะใหม่", + "30 minutes" : "30 นาที", + "1 hour" : "1 ชั่วโมง", + "4 hours" : "4 ชั่วโมง", + "Mute all notifications" : "ปิดการแจ้งเตือนทั้งหมด", + "Appear offline" : "แสดงเป็นออฟไลน์" +}, +"nplurals=1; plural=0;"); diff --git a/apps/user_status/l10n/th.json b/apps/user_status/l10n/th.json new file mode 100644 index 00000000000..36ca7503b17 --- /dev/null +++ b/apps/user_status/l10n/th.json @@ -0,0 +1,34 @@ +{ "translations": { + "Recent statuses" : "สถานะล่าสุด", + "No recent status changes" : "ไม่มีการเปลี่ยนสถานะล่าสุด", + "In a meeting" : "กำลังประชุม", + "Commuting" : "กำลังเดินทาง", + "Out sick" : "ป่วย", + "Vacationing" : "วันหยุดพักผ่อน", + "Working remotely" : "ทำงานจากระยะไกล", + "User status" : "สถานะผู้ใช้", + "Clear status after" : "ล้างสถานะหลังจาก", + "What is your status?" : "สถานะของคุณ", + "There was an error saving the status" : "เกิดข้อผิดพลาดในการบันทึกสถานะ", + "There was an error clearing the status" : "เกิดข้อผิดพลาดในการลบสถานะ", + "Online status" : "สถานะออนไลน์", + "Status message" : "ข้อความสถานะ", + "Clear status message" : "ล้างข้อความสถานะ", + "Set status message" : "กำหนดข้อความสถานะ", + "Don't clear" : "ไม่ต้องล้าง", + "Today" : "วันนี้", + "This week" : "สัปดาห์นี้", + "Online" : "ออนไลน์", + "Away" : "ไม่อยู่", + "Do not disturb" : "ห้ามรบกวน", + "Invisible" : "ไม่แสดงสถานะ", + "Offline" : "ออฟไลน์", + "Set status" : "กำหนดสถานะ", + "There was an error saving the new status" : "เกิดข้อผิดพลาดในการบันทึกสถานะใหม่", + "30 minutes" : "30 นาที", + "1 hour" : "1 ชั่วโมง", + "4 hours" : "4 ชั่วโมง", + "Mute all notifications" : "ปิดการแจ้งเตือนทั้งหมด", + "Appear offline" : "แสดงเป็นออฟไลน์" +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/tr.js b/apps/user_status/l10n/tr.js new file mode 100644 index 00000000000..c63ff93c164 --- /dev/null +++ b/apps/user_status/l10n/tr.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Son durumlar", + "No recent status changes" : "Son zamanlarda durum değiştirilmemiş", + "In a meeting" : "Toplantıda", + "Commuting" : "İşe gidiyor/geliyor", + "Out sick" : "Hasta", + "Vacationing" : "Tatilde", + "Out of office" : "İş yeri dışında", + "Working remotely" : "Uzaktan çalışıyor", + "In a call" : "Bir çağrıda", + "User status" : "Kullanıcı durumu", + "Clear status after" : "Durum şu kadar sonra kaldırılsın", + "Emoji for your status message" : "Durum iletiniz için emoji", + "What is your status?" : "Durumunuz nedir?", + "Predefined statuses" : "Hazır durumlar", + "Previously set" : "Önceden ayarlanmış", + "Reset status" : "Durumu sıfırla", + "Reset status to \"{icon} {message}\"" : "Durumu \"{icon} {message}\" olarak sıfırla", + "Reset status to \"{message}\"" : "Durumu \"{message}\" olarak sıfırla", + "Reset status to \"{icon}\"" : "Durumu \"{icon}\" olarak sıfırla", + "There was an error saving the status" : "Durum kaydedilirken bir sorun çıktı", + "There was an error clearing the status" : "Durum kaldırılırken bir sorun çıktı", + "There was an error reverting the status" : "Durum geri alınırken bir sorun çıktı", + "Online status" : "Çevrim içi durumu", + "Status message" : "Durum iletisi", + "Set absence period" : "Bulunmama aralığını ayarla", + "Set absence period and replacement" : "Bulunmama aralığını ve yedek kişiyi ayarla", + "Your status was set automatically" : "Durumunuz otomatik olarak ayarlanmış", + "Clear status message" : "Durum iletisini temizle", + "Set status message" : "Durum iletisini ayarla", + "Don't clear" : "Kaldırılmasın", + "Today" : "Bugün", + "This week" : "Bu hafta", + "Online" : "Çevrim içi", + "Away" : "Uzakta", + "Do not disturb" : "Rahatsız etmeyin", + "Invisible" : "Gizli", + "Offline" : "Çevrim dışı", + "Set status" : "Durumu ayarla", + "There was an error saving the new status" : "Yeni durum kaydedilirken bir sorun çıktı", + "30 minutes" : "30 dakika", + "1 hour" : "1 saat", + "4 hours" : "4 saat", + "Busy" : "Meşgul", + "Mute all notifications" : "Tüm bildirimleri kapat", + "Appear offline" : "Çevrim dışı görün" +}, +"nplurals=2; plural=(n > 1);"); diff --git a/apps/user_status/l10n/tr.json b/apps/user_status/l10n/tr.json new file mode 100644 index 00000000000..c601fbbd635 --- /dev/null +++ b/apps/user_status/l10n/tr.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Son durumlar", + "No recent status changes" : "Son zamanlarda durum değiştirilmemiş", + "In a meeting" : "Toplantıda", + "Commuting" : "İşe gidiyor/geliyor", + "Out sick" : "Hasta", + "Vacationing" : "Tatilde", + "Out of office" : "İş yeri dışında", + "Working remotely" : "Uzaktan çalışıyor", + "In a call" : "Bir çağrıda", + "User status" : "Kullanıcı durumu", + "Clear status after" : "Durum şu kadar sonra kaldırılsın", + "Emoji for your status message" : "Durum iletiniz için emoji", + "What is your status?" : "Durumunuz nedir?", + "Predefined statuses" : "Hazır durumlar", + "Previously set" : "Önceden ayarlanmış", + "Reset status" : "Durumu sıfırla", + "Reset status to \"{icon} {message}\"" : "Durumu \"{icon} {message}\" olarak sıfırla", + "Reset status to \"{message}\"" : "Durumu \"{message}\" olarak sıfırla", + "Reset status to \"{icon}\"" : "Durumu \"{icon}\" olarak sıfırla", + "There was an error saving the status" : "Durum kaydedilirken bir sorun çıktı", + "There was an error clearing the status" : "Durum kaldırılırken bir sorun çıktı", + "There was an error reverting the status" : "Durum geri alınırken bir sorun çıktı", + "Online status" : "Çevrim içi durumu", + "Status message" : "Durum iletisi", + "Set absence period" : "Bulunmama aralığını ayarla", + "Set absence period and replacement" : "Bulunmama aralığını ve yedek kişiyi ayarla", + "Your status was set automatically" : "Durumunuz otomatik olarak ayarlanmış", + "Clear status message" : "Durum iletisini temizle", + "Set status message" : "Durum iletisini ayarla", + "Don't clear" : "Kaldırılmasın", + "Today" : "Bugün", + "This week" : "Bu hafta", + "Online" : "Çevrim içi", + "Away" : "Uzakta", + "Do not disturb" : "Rahatsız etmeyin", + "Invisible" : "Gizli", + "Offline" : "Çevrim dışı", + "Set status" : "Durumu ayarla", + "There was an error saving the new status" : "Yeni durum kaydedilirken bir sorun çıktı", + "30 minutes" : "30 dakika", + "1 hour" : "1 saat", + "4 hours" : "4 saat", + "Busy" : "Meşgul", + "Mute all notifications" : "Tüm bildirimleri kapat", + "Appear offline" : "Çevrim dışı görün" +},"pluralForm" :"nplurals=2; plural=(n > 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/ug.js b/apps/user_status/l10n/ug.js new file mode 100644 index 00000000000..43c620037e3 --- /dev/null +++ b/apps/user_status/l10n/ug.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "يېقىنقى ھالەت", + "No recent status changes" : "يېقىنقى ھالەت ئۆزگەرمىدى", + "In a meeting" : "بىر يىغىندا", + "Commuting" : "سەپەرگە چىقىش", + "Out sick" : "كېسەل", + "Vacationing" : "دەم ئېلىش", + "Out of office" : "ئىشخانىدىن چىقتى", + "Working remotely" : "يىراقتىن ئىشلەش", + "In a call" : "تېلېفوندا", + "User status" : "ئىشلەتكۈچى ھالىتى", + "Clear status after" : "كېيىنكى ھالەتنى ئېنىقلاش", + "Emoji for your status message" : "ھالەت ئۇچۇرىڭىز ئۈچۈن Emoji", + "What is your status?" : "ئەھۋالىڭىز نېمە؟", + "Predefined statuses" : "ئالدىن بېكىتىلگەن ھالەت", + "Previously set" : "ئىلگىرى تەڭشەلگەن", + "Reset status" : "ھالىتىنى ئەسلىگە كەلتۈرۈش", + "Reset status to \"{icon} {message}\"" : "ھالەتنى \"{icon} {message}\" غا قايتۇرۇڭ", + "Reset status to \"{message}\"" : "ھالەتنى \"{message}\" غا قايتۇرۇڭ", + "Reset status to \"{icon}\"" : "ھالەتنى \"{icon}\" گە قايتۇرۇڭ", + "There was an error saving the status" : "ھالەتنى ساقلاشتا خاتالىق كۆرۈلدى", + "There was an error clearing the status" : "ھالەتنى تازىلاشتا خاتالىق كۆرۈلدى", + "There was an error reverting the status" : "ھالەتنى ئەسلىگە كەلتۈرۈشتە خاتالىق كۆرۈلدى", + "Online status" : "توردىكى ئورنى", + "Status message" : "ھالەت ئۇچۇرى", + "Set absence period" : "يوقلۇق ۋاقتىنى بەلگىلەڭ", + "Set absence period and replacement" : "يوقلۇق ۋاقتى ۋە ئورنىنى بەلگىلەڭ", + "Your status was set automatically" : "ھالىتىڭىز ئاپتوماتىك تەڭشەلدى", + "Clear status message" : "ھالەت ئۇچۇرىنى تازىلاش", + "Set status message" : "ھالەت ئۇچۇرىنى بەلگىلەڭ", + "Don't clear" : "ئېنىق ئەمەس", + "Today" : "بۈگۈن", + "This week" : "بۇ ھەپتە", + "Online" : "توردا", + "Away" : "يىراق", + "Do not disturb" : "ئاۋارە قىلماڭ", + "Invisible" : "كۆرۈنمەيدۇ", + "Offline" : "تورسىز", + "Set status" : "ھالەت بەلگىلەڭ", + "There was an error saving the new status" : "يېڭى ھالەتنى ساقلاشتا خاتالىق كۆرۈلدى", + "30 minutes" : "30 مىنۇت", + "1 hour" : "1 سائەت", + "4 hours" : "4 سائەت", + "Busy" : "ئالدىراش", + "Mute all notifications" : "بارلىق ئۇقتۇرۇشلارنى ئاۋازسىز قىلىڭ", + "Appear offline" : "تورسىز كۆرۈنۈش" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/user_status/l10n/ug.json b/apps/user_status/l10n/ug.json new file mode 100644 index 00000000000..eb439948185 --- /dev/null +++ b/apps/user_status/l10n/ug.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "يېقىنقى ھالەت", + "No recent status changes" : "يېقىنقى ھالەت ئۆزگەرمىدى", + "In a meeting" : "بىر يىغىندا", + "Commuting" : "سەپەرگە چىقىش", + "Out sick" : "كېسەل", + "Vacationing" : "دەم ئېلىش", + "Out of office" : "ئىشخانىدىن چىقتى", + "Working remotely" : "يىراقتىن ئىشلەش", + "In a call" : "تېلېفوندا", + "User status" : "ئىشلەتكۈچى ھالىتى", + "Clear status after" : "كېيىنكى ھالەتنى ئېنىقلاش", + "Emoji for your status message" : "ھالەت ئۇچۇرىڭىز ئۈچۈن Emoji", + "What is your status?" : "ئەھۋالىڭىز نېمە؟", + "Predefined statuses" : "ئالدىن بېكىتىلگەن ھالەت", + "Previously set" : "ئىلگىرى تەڭشەلگەن", + "Reset status" : "ھالىتىنى ئەسلىگە كەلتۈرۈش", + "Reset status to \"{icon} {message}\"" : "ھالەتنى \"{icon} {message}\" غا قايتۇرۇڭ", + "Reset status to \"{message}\"" : "ھالەتنى \"{message}\" غا قايتۇرۇڭ", + "Reset status to \"{icon}\"" : "ھالەتنى \"{icon}\" گە قايتۇرۇڭ", + "There was an error saving the status" : "ھالەتنى ساقلاشتا خاتالىق كۆرۈلدى", + "There was an error clearing the status" : "ھالەتنى تازىلاشتا خاتالىق كۆرۈلدى", + "There was an error reverting the status" : "ھالەتنى ئەسلىگە كەلتۈرۈشتە خاتالىق كۆرۈلدى", + "Online status" : "توردىكى ئورنى", + "Status message" : "ھالەت ئۇچۇرى", + "Set absence period" : "يوقلۇق ۋاقتىنى بەلگىلەڭ", + "Set absence period and replacement" : "يوقلۇق ۋاقتى ۋە ئورنىنى بەلگىلەڭ", + "Your status was set automatically" : "ھالىتىڭىز ئاپتوماتىك تەڭشەلدى", + "Clear status message" : "ھالەت ئۇچۇرىنى تازىلاش", + "Set status message" : "ھالەت ئۇچۇرىنى بەلگىلەڭ", + "Don't clear" : "ئېنىق ئەمەس", + "Today" : "بۈگۈن", + "This week" : "بۇ ھەپتە", + "Online" : "توردا", + "Away" : "يىراق", + "Do not disturb" : "ئاۋارە قىلماڭ", + "Invisible" : "كۆرۈنمەيدۇ", + "Offline" : "تورسىز", + "Set status" : "ھالەت بەلگىلەڭ", + "There was an error saving the new status" : "يېڭى ھالەتنى ساقلاشتا خاتالىق كۆرۈلدى", + "30 minutes" : "30 مىنۇت", + "1 hour" : "1 سائەت", + "4 hours" : "4 سائەت", + "Busy" : "ئالدىراش", + "Mute all notifications" : "بارلىق ئۇقتۇرۇشلارنى ئاۋازسىز قىلىڭ", + "Appear offline" : "تورسىز كۆرۈنۈش" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/uk.js b/apps/user_status/l10n/uk.js new file mode 100644 index 00000000000..b73417b164b --- /dev/null +++ b/apps/user_status/l10n/uk.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Останні статуси", + "No recent status changes" : "Статус не змінювався", + "In a meeting" : "На зустрічі", + "Commuting" : "В дорозі на роботу", + "Out sick" : "Хворію", + "Vacationing" : "У відпустці", + "Out of office" : "Недоступний(-а)", + "Working remotely" : "Працюю віддалено", + "In a call" : "На дзвінку", + "Be right back" : "Зараз повернуся", + "User status" : "Статус користувача", + "Clear status after" : "Очистити статус після", + "Emoji for your status message" : "Емоційки для повідомлення вашого статусу", + "What is your status?" : "Який твій статус?", + "Predefined statuses" : "Попередньо визначені статуси", + "Previously set" : "Раніше встановлений", + "Reset status" : "Скинути статус", + "Reset status to \"{icon} {message}\"" : "Скинути статус на \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Скинути статус на \"{message}\"", + "Reset status to \"{icon}\"" : "Скинути статус на \"{icon}\"", + "There was an error saving the status" : "Помилка під час збереження статусу", + "There was an error clearing the status" : "Помилка під час очищення статусу", + "There was an error reverting the status" : "Помилка при скиданні статусу", + "Online status" : "Мій статус доступності", + "Status message" : "Повідомлення про статус", + "Set absence period" : "Встановити період відсутности", + "Set absence period and replacement" : "Встановити період відсутности та тимчасово виконуючого обов'язки", + "Your status was set automatically" : "Ваш статус встановлено автоматично", + "Clear status message" : "Прибрати статус", + "Set status message" : "Оновити статус", + "Don't clear" : "Залишити поточний", + "Today" : "Сьогодні", + "This week" : "Цього тижня", + "Online" : "Доступний(-а)", + "Away" : "Відсутній(-я)", + "Do not disturb" : "Не турбувати", + "Invisible" : "Невидимка", + "Offline" : "Поза мережею", + "Set status" : "Встановити статус", + "There was an error saving the new status" : "Помилка під час збереження статусу", + "30 minutes" : "30 хвилин", + "1 hour" : "1 година", + "4 hours" : "4 години", + "Busy" : "Зайнято", + "Mute all notifications" : "Вимкнути всі сповіщення", + "Appear offline" : "Перебуваю поза мережею" +}, +"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);"); diff --git a/apps/user_status/l10n/uk.json b/apps/user_status/l10n/uk.json new file mode 100644 index 00000000000..e115bf09f5f --- /dev/null +++ b/apps/user_status/l10n/uk.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "Останні статуси", + "No recent status changes" : "Статус не змінювався", + "In a meeting" : "На зустрічі", + "Commuting" : "В дорозі на роботу", + "Out sick" : "Хворію", + "Vacationing" : "У відпустці", + "Out of office" : "Недоступний(-а)", + "Working remotely" : "Працюю віддалено", + "In a call" : "На дзвінку", + "Be right back" : "Зараз повернуся", + "User status" : "Статус користувача", + "Clear status after" : "Очистити статус після", + "Emoji for your status message" : "Емоційки для повідомлення вашого статусу", + "What is your status?" : "Який твій статус?", + "Predefined statuses" : "Попередньо визначені статуси", + "Previously set" : "Раніше встановлений", + "Reset status" : "Скинути статус", + "Reset status to \"{icon} {message}\"" : "Скинути статус на \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Скинути статус на \"{message}\"", + "Reset status to \"{icon}\"" : "Скинути статус на \"{icon}\"", + "There was an error saving the status" : "Помилка під час збереження статусу", + "There was an error clearing the status" : "Помилка під час очищення статусу", + "There was an error reverting the status" : "Помилка при скиданні статусу", + "Online status" : "Мій статус доступності", + "Status message" : "Повідомлення про статус", + "Set absence period" : "Встановити період відсутности", + "Set absence period and replacement" : "Встановити період відсутности та тимчасово виконуючого обов'язки", + "Your status was set automatically" : "Ваш статус встановлено автоматично", + "Clear status message" : "Прибрати статус", + "Set status message" : "Оновити статус", + "Don't clear" : "Залишити поточний", + "Today" : "Сьогодні", + "This week" : "Цього тижня", + "Online" : "Доступний(-а)", + "Away" : "Відсутній(-я)", + "Do not disturb" : "Не турбувати", + "Invisible" : "Невидимка", + "Offline" : "Поза мережею", + "Set status" : "Встановити статус", + "There was an error saving the new status" : "Помилка під час збереження статусу", + "30 minutes" : "30 хвилин", + "1 hour" : "1 година", + "4 hours" : "4 години", + "Busy" : "Зайнято", + "Mute all notifications" : "Вимкнути всі сповіщення", + "Appear offline" : "Перебуваю поза мережею" +},"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/uz.js b/apps/user_status/l10n/uz.js new file mode 100644 index 00000000000..c2ada8c65d0 --- /dev/null +++ b/apps/user_status/l10n/uz.js @@ -0,0 +1,50 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Oxirgi holatlar", + "No recent status changes" : "Oxirgi holatda o'zgarish mavjud emas", + "In a meeting" : "Uchrashuvda", + "Commuting" : "Qatnov", + "Out sick" : "Kasal", + "Vacationing" : "Dam olish", + "Out of office" : "Ofisda emas", + "Working remotely" : "Masofadan ishlash", + "In a call" : "Qo'ng'iroqda", + "User status" : "Foydalanuvchi holati", + "Clear status after" : "Holatni tozalashdan keyin", + "Emoji for your status message" : "Xabar holati uchun emoji", + "What is your status?" : "Sizning holatingiz qanday?", + "Predefined statuses" : "Oldindan belgilangan holatlar", + "Previously set" : "Ilgari o'rnatilgan", + "Reset status" : "Holatni tiklash", + "Reset status to \"{icon} {message}\"" : " \"{icon} {message}\" uchun holatni tiklash", + "Reset status to \"{message}\"" : " \"{message}\" uchun holatni tiklash", + "Reset status to \"{icon}\"" : " \"{icon}\" uchun holatni tiklash", + "There was an error saving the status" : "Holatni saqlashda xatolik yuz berdi", + "There was an error clearing the status" : "Holatni tozalashda xatolik yuz berdi", + "There was an error reverting the status" : "Holatni qaytarishda xatolik yuz berdi", + "Online status" : "Onlayn holat", + "Status message" : "Holat xabari", + "Set absence period" : "Aloqadan o`chirilgan muddatini belgilang", + "Set absence period and replacement" : "Aloqadan o`chirilgan muddatini va almashtirish belgilang", + "Your status was set automatically" : "Sizning holatingiz avtomatik ravishda o'rnatildi", + "Clear status message" : "Holat xabarini tozalash", + "Set status message" : "Holat xabarini o'rnatish", + "Don't clear" : "Aniq emas", + "Today" : "Bugun", + "This week" : "Shu hafta", + "Online" : "Online", + "Away" : "Uzoqda", + "Do not disturb" : "Bezovta qilmang", + "Invisible" : "Ko'rinmas", + "Offline" : "Offline", + "Set status" : "Holatni belgilash", + "There was an error saving the new status" : "Yangi holatni saqlashda xatolik yuz berdi", + "30 minutes" : "30 minut", + "1 hour" : "1 soat", + "4 hours" : "4 soat", + "Busy" : "Band", + "Mute all notifications" : "Barcha bildirishnomalarni o'chirish", + "Appear offline" : "Oflayn ko'rinishda" +}, +"nplurals=1; plural=0;"); diff --git a/apps/user_status/l10n/uz.json b/apps/user_status/l10n/uz.json new file mode 100644 index 00000000000..8d4d073a89f --- /dev/null +++ b/apps/user_status/l10n/uz.json @@ -0,0 +1,48 @@ +{ "translations": { + "Recent statuses" : "Oxirgi holatlar", + "No recent status changes" : "Oxirgi holatda o'zgarish mavjud emas", + "In a meeting" : "Uchrashuvda", + "Commuting" : "Qatnov", + "Out sick" : "Kasal", + "Vacationing" : "Dam olish", + "Out of office" : "Ofisda emas", + "Working remotely" : "Masofadan ishlash", + "In a call" : "Qo'ng'iroqda", + "User status" : "Foydalanuvchi holati", + "Clear status after" : "Holatni tozalashdan keyin", + "Emoji for your status message" : "Xabar holati uchun emoji", + "What is your status?" : "Sizning holatingiz qanday?", + "Predefined statuses" : "Oldindan belgilangan holatlar", + "Previously set" : "Ilgari o'rnatilgan", + "Reset status" : "Holatni tiklash", + "Reset status to \"{icon} {message}\"" : " \"{icon} {message}\" uchun holatni tiklash", + "Reset status to \"{message}\"" : " \"{message}\" uchun holatni tiklash", + "Reset status to \"{icon}\"" : " \"{icon}\" uchun holatni tiklash", + "There was an error saving the status" : "Holatni saqlashda xatolik yuz berdi", + "There was an error clearing the status" : "Holatni tozalashda xatolik yuz berdi", + "There was an error reverting the status" : "Holatni qaytarishda xatolik yuz berdi", + "Online status" : "Onlayn holat", + "Status message" : "Holat xabari", + "Set absence period" : "Aloqadan o`chirilgan muddatini belgilang", + "Set absence period and replacement" : "Aloqadan o`chirilgan muddatini va almashtirish belgilang", + "Your status was set automatically" : "Sizning holatingiz avtomatik ravishda o'rnatildi", + "Clear status message" : "Holat xabarini tozalash", + "Set status message" : "Holat xabarini o'rnatish", + "Don't clear" : "Aniq emas", + "Today" : "Bugun", + "This week" : "Shu hafta", + "Online" : "Online", + "Away" : "Uzoqda", + "Do not disturb" : "Bezovta qilmang", + "Invisible" : "Ko'rinmas", + "Offline" : "Offline", + "Set status" : "Holatni belgilash", + "There was an error saving the new status" : "Yangi holatni saqlashda xatolik yuz berdi", + "30 minutes" : "30 minut", + "1 hour" : "1 soat", + "4 hours" : "4 soat", + "Busy" : "Band", + "Mute all notifications" : "Barcha bildirishnomalarni o'chirish", + "Appear offline" : "Oflayn ko'rinishda" +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/vi.js b/apps/user_status/l10n/vi.js new file mode 100644 index 00000000000..0c2e8081a8e --- /dev/null +++ b/apps/user_status/l10n/vi.js @@ -0,0 +1,47 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "Trạng thái gần đây", + "No recent status changes" : "Không có thay đổi trạng thái gần đây", + "In a meeting" : "Trong một cuộc họp", + "Commuting" : "Đang di chuyển", + "Out sick" : "Bị ốm", + "Vacationing" : "Đi nghỉ", + "Out of office" : "Không ở văn phòng", + "Working remotely" : "Làm việc từ xa", + "User status" : "Trạng thái người dùng", + "Clear status after" : "Xóa trạng thái sau", + "Emoji for your status message" : "Biểu tượng cảm xúc cho thông báo trạng thái của bạn", + "What is your status?" : "Trạng thái của bạn là gì?", + "Predefined statuses" : "Trạng thái được xác định trước", + "Previously set" : "Đã đặt trước đó", + "Reset status" : "Thiết lập trạng thái", + "Reset status to \"{icon} {message}\"" : "Đặt lại trạng thái thành \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Đặt lại trạng thái thành \"{message}\"", + "Reset status to \"{icon}\"" : "Đặt lại trạng thái thành \"{icon}\"", + "There was an error saving the status" : "Đã xảy ra lỗi khi lưu trạng thái", + "There was an error clearing the status" : "Đã xảy ra lỗi khi xóa trạng thái", + "There was an error reverting the status" : "Đã xảy ra lỗi khi hoàn nguyên trạng thái", + "Online status" : "Trạng thái trực tuyến", + "Status message" : "Thông báo trạng thái", + "Your status was set automatically" : "Trạng thái của bạn đã được đặt tự động", + "Clear status message" : "Xoá thông báo trạng thái", + "Set status message" : "Đặt thông báo trạng thái", + "Don't clear" : "Không xoá", + "Today" : "Hôm nay", + "This week" : "Tuần này", + "Online" : "Trực tuyến", + "Away" : "Tạm vắng", + "Do not disturb" : "Đừng làm phiền", + "Invisible" : "Vô hình", + "Offline" : "Ngoại tuyến", + "Set status" : "Đặt trạng thái", + "There was an error saving the new status" : "Đã xảy ra lỗi khi lưu trạng thái mới", + "30 minutes" : "30 phút", + "1 hour" : "1 tiếng", + "4 hours" : "4 tiếng", + "Busy" : "Bận", + "Mute all notifications" : "Tắt tiếng tất cả thông báo", + "Appear offline" : "Đang offline" +}, +"nplurals=1; plural=0;"); diff --git a/apps/user_status/l10n/vi.json b/apps/user_status/l10n/vi.json new file mode 100644 index 00000000000..daf7d940656 --- /dev/null +++ b/apps/user_status/l10n/vi.json @@ -0,0 +1,45 @@ +{ "translations": { + "Recent statuses" : "Trạng thái gần đây", + "No recent status changes" : "Không có thay đổi trạng thái gần đây", + "In a meeting" : "Trong một cuộc họp", + "Commuting" : "Đang di chuyển", + "Out sick" : "Bị ốm", + "Vacationing" : "Đi nghỉ", + "Out of office" : "Không ở văn phòng", + "Working remotely" : "Làm việc từ xa", + "User status" : "Trạng thái người dùng", + "Clear status after" : "Xóa trạng thái sau", + "Emoji for your status message" : "Biểu tượng cảm xúc cho thông báo trạng thái của bạn", + "What is your status?" : "Trạng thái của bạn là gì?", + "Predefined statuses" : "Trạng thái được xác định trước", + "Previously set" : "Đã đặt trước đó", + "Reset status" : "Thiết lập trạng thái", + "Reset status to \"{icon} {message}\"" : "Đặt lại trạng thái thành \"{icon} {message}\"", + "Reset status to \"{message}\"" : "Đặt lại trạng thái thành \"{message}\"", + "Reset status to \"{icon}\"" : "Đặt lại trạng thái thành \"{icon}\"", + "There was an error saving the status" : "Đã xảy ra lỗi khi lưu trạng thái", + "There was an error clearing the status" : "Đã xảy ra lỗi khi xóa trạng thái", + "There was an error reverting the status" : "Đã xảy ra lỗi khi hoàn nguyên trạng thái", + "Online status" : "Trạng thái trực tuyến", + "Status message" : "Thông báo trạng thái", + "Your status was set automatically" : "Trạng thái của bạn đã được đặt tự động", + "Clear status message" : "Xoá thông báo trạng thái", + "Set status message" : "Đặt thông báo trạng thái", + "Don't clear" : "Không xoá", + "Today" : "Hôm nay", + "This week" : "Tuần này", + "Online" : "Trực tuyến", + "Away" : "Tạm vắng", + "Do not disturb" : "Đừng làm phiền", + "Invisible" : "Vô hình", + "Offline" : "Ngoại tuyến", + "Set status" : "Đặt trạng thái", + "There was an error saving the new status" : "Đã xảy ra lỗi khi lưu trạng thái mới", + "30 minutes" : "30 phút", + "1 hour" : "1 tiếng", + "4 hours" : "4 tiếng", + "Busy" : "Bận", + "Mute all notifications" : "Tắt tiếng tất cả thông báo", + "Appear offline" : "Đang offline" +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/zh_CN.js b/apps/user_status/l10n/zh_CN.js new file mode 100644 index 00000000000..c36ad38c713 --- /dev/null +++ b/apps/user_status/l10n/zh_CN.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "最近状态", + "No recent status changes" : "最近状态没有改变", + "In a meeting" : "开会中", + "Commuting" : "通勤中", + "Out sick" : "生病了", + "Vacationing" : "度假中", + "Out of office" : "不在办公室", + "Working remotely" : "远程办公中", + "In a call" : "通话中", + "Be right back" : "马上回来", + "User status" : "用户状态", + "Clear status after" : "清除状态于", + "Emoji for your status message" : "状态消息的表情符号", + "What is your status?" : "您的状态如何?", + "Predefined statuses" : "预定义状态", + "Previously set" : "先前设置", + "Reset status" : "重置状态", + "Reset status to \"{icon} {message}\"" : "将状态重置为“{icon} {message}”", + "Reset status to \"{message}\"" : "将状态重置为“{message}”", + "Reset status to \"{icon}\"" : "将状态重置为“{icon}”", + "There was an error saving the status" : "保存状态时出错", + "There was an error clearing the status" : "清除状态时出错", + "There was an error reverting the status" : "恢复状态时出错", + "Online status" : "在线状态", + "Status message" : "状态消息", + "Set absence period" : "设置缺勤时段", + "Set absence period and replacement" : "设置缺勤时段和接替者", + "Your status was set automatically" : "您的状态已自动设置", + "Clear status message" : "清除状态消息", + "Set status message" : "设置状态消息", + "Don't clear" : "不要清除", + "Today" : "今天", + "This week" : "本周", + "Online" : "在线", + "Away" : "离开", + "Do not disturb" : "勿扰", + "Invisible" : "隐身", + "Offline" : "离线", + "Set status" : "设置状态", + "There was an error saving the new status" : "保存新状态时出错", + "30 minutes" : "30 分钟", + "1 hour" : "1 小时", + "4 hours" : "4 小时", + "Busy" : "忙碌", + "Mute all notifications" : "静音所有通知", + "Appear offline" : "显示为离线" +}, +"nplurals=1; plural=0;"); diff --git a/apps/user_status/l10n/zh_CN.json b/apps/user_status/l10n/zh_CN.json new file mode 100644 index 00000000000..8546482d238 --- /dev/null +++ b/apps/user_status/l10n/zh_CN.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "最近状态", + "No recent status changes" : "最近状态没有改变", + "In a meeting" : "开会中", + "Commuting" : "通勤中", + "Out sick" : "生病了", + "Vacationing" : "度假中", + "Out of office" : "不在办公室", + "Working remotely" : "远程办公中", + "In a call" : "通话中", + "Be right back" : "马上回来", + "User status" : "用户状态", + "Clear status after" : "清除状态于", + "Emoji for your status message" : "状态消息的表情符号", + "What is your status?" : "您的状态如何?", + "Predefined statuses" : "预定义状态", + "Previously set" : "先前设置", + "Reset status" : "重置状态", + "Reset status to \"{icon} {message}\"" : "将状态重置为“{icon} {message}”", + "Reset status to \"{message}\"" : "将状态重置为“{message}”", + "Reset status to \"{icon}\"" : "将状态重置为“{icon}”", + "There was an error saving the status" : "保存状态时出错", + "There was an error clearing the status" : "清除状态时出错", + "There was an error reverting the status" : "恢复状态时出错", + "Online status" : "在线状态", + "Status message" : "状态消息", + "Set absence period" : "设置缺勤时段", + "Set absence period and replacement" : "设置缺勤时段和接替者", + "Your status was set automatically" : "您的状态已自动设置", + "Clear status message" : "清除状态消息", + "Set status message" : "设置状态消息", + "Don't clear" : "不要清除", + "Today" : "今天", + "This week" : "本周", + "Online" : "在线", + "Away" : "离开", + "Do not disturb" : "勿扰", + "Invisible" : "隐身", + "Offline" : "离线", + "Set status" : "设置状态", + "There was an error saving the new status" : "保存新状态时出错", + "30 minutes" : "30 分钟", + "1 hour" : "1 小时", + "4 hours" : "4 小时", + "Busy" : "忙碌", + "Mute all notifications" : "静音所有通知", + "Appear offline" : "显示为离线" +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/zh_HK.js b/apps/user_status/l10n/zh_HK.js new file mode 100644 index 00000000000..66fcd087abe --- /dev/null +++ b/apps/user_status/l10n/zh_HK.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "最近的狀態", + "No recent status changes" : "最近沒有狀態變更", + "In a meeting" : "會議中", + "Commuting" : "通勤中", + "Out sick" : "生病了 ", + "Vacationing" : "休假中", + "Out of office" : "不在辦公室", + "Working remotely" : "遠程工作中", + "In a call" : "通話中", + "Be right back" : "馬上回來", + "User status" : "用戶狀態", + "Clear status after" : "繼此之後清空狀態", + "Emoji for your status message" : "狀態訊息的表情符號", + "What is your status?" : "您目前的狀態是什麼呢?", + "Predefined statuses" : "預先定義的狀態", + "Previously set" : "先前設定", + "Reset status" : "重設狀態", + "Reset status to \"{icon} {message}\"" : "將狀態重置為 “{icon} {message}”", + "Reset status to \"{message}\"" : "將狀態重置為“{message}”", + "Reset status to \"{icon}\"" : "將狀態重置為“{icon}”", + "There was an error saving the status" : "儲存狀態時發生錯誤", + "There was an error clearing the status" : "變更狀態時發生錯誤", + "There was an error reverting the status" : "恢復狀態時出錯", + "Online status" : "線上狀態", + "Status message" : "狀態訊息", + "Set absence period" : "設定缺席時間", + "Set absence period and replacement" : "設定缺席時間與職務代理人", + "Your status was set automatically" : "您的狀態是自動設置的", + "Clear status message" : "清空狀態訊息", + "Set status message" : "設定狀態訊息", + "Don't clear" : "不要清空", + "Today" : "今日", + "This week" : "本星期", + "Online" : "在線", + "Away" : "離開", + "Do not disturb" : "請勿打擾", + "Invisible" : "隱藏", + "Offline" : "離線", + "Set status" : "設定狀態", + "There was an error saving the new status" : "儲存新狀態時發生錯誤", + "30 minutes" : "30分鐘", + "1 hour" : "1 小時", + "4 hours" : "4 小時", + "Busy" : "忙碌", + "Mute all notifications" : "靜音所有通知", + "Appear offline" : "顯示為離線" +}, +"nplurals=1; plural=0;"); diff --git a/apps/user_status/l10n/zh_HK.json b/apps/user_status/l10n/zh_HK.json new file mode 100644 index 00000000000..a95da1fa45c --- /dev/null +++ b/apps/user_status/l10n/zh_HK.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "最近的狀態", + "No recent status changes" : "最近沒有狀態變更", + "In a meeting" : "會議中", + "Commuting" : "通勤中", + "Out sick" : "生病了 ", + "Vacationing" : "休假中", + "Out of office" : "不在辦公室", + "Working remotely" : "遠程工作中", + "In a call" : "通話中", + "Be right back" : "馬上回來", + "User status" : "用戶狀態", + "Clear status after" : "繼此之後清空狀態", + "Emoji for your status message" : "狀態訊息的表情符號", + "What is your status?" : "您目前的狀態是什麼呢?", + "Predefined statuses" : "預先定義的狀態", + "Previously set" : "先前設定", + "Reset status" : "重設狀態", + "Reset status to \"{icon} {message}\"" : "將狀態重置為 “{icon} {message}”", + "Reset status to \"{message}\"" : "將狀態重置為“{message}”", + "Reset status to \"{icon}\"" : "將狀態重置為“{icon}”", + "There was an error saving the status" : "儲存狀態時發生錯誤", + "There was an error clearing the status" : "變更狀態時發生錯誤", + "There was an error reverting the status" : "恢復狀態時出錯", + "Online status" : "線上狀態", + "Status message" : "狀態訊息", + "Set absence period" : "設定缺席時間", + "Set absence period and replacement" : "設定缺席時間與職務代理人", + "Your status was set automatically" : "您的狀態是自動設置的", + "Clear status message" : "清空狀態訊息", + "Set status message" : "設定狀態訊息", + "Don't clear" : "不要清空", + "Today" : "今日", + "This week" : "本星期", + "Online" : "在線", + "Away" : "離開", + "Do not disturb" : "請勿打擾", + "Invisible" : "隱藏", + "Offline" : "離線", + "Set status" : "設定狀態", + "There was an error saving the new status" : "儲存新狀態時發生錯誤", + "30 minutes" : "30分鐘", + "1 hour" : "1 小時", + "4 hours" : "4 小時", + "Busy" : "忙碌", + "Mute all notifications" : "靜音所有通知", + "Appear offline" : "顯示為離線" +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/user_status/l10n/zh_TW.js b/apps/user_status/l10n/zh_TW.js new file mode 100644 index 00000000000..c4cd18345a5 --- /dev/null +++ b/apps/user_status/l10n/zh_TW.js @@ -0,0 +1,51 @@ +OC.L10N.register( + "user_status", + { + "Recent statuses" : "最近的狀態", + "No recent status changes" : "最近沒有狀態變更", + "In a meeting" : "會議中", + "Commuting" : "通勤中", + "Out sick" : "病假", + "Vacationing" : "休假中", + "Out of office" : "不在辦公室", + "Working remotely" : "遠端工作", + "In a call" : "通話中", + "Be right back" : "馬上回來", + "User status" : "使用者狀態", + "Clear status after" : "多久後清除狀態", + "Emoji for your status message" : "狀態訊息的表情符號", + "What is your status?" : "您目前的狀態是什麼呢?", + "Predefined statuses" : "預先定義的狀態", + "Previously set" : "先前設定", + "Reset status" : "重設狀態", + "Reset status to \"{icon} {message}\"" : "重設狀態為「{icon} {message}」", + "Reset status to \"{message}\"" : "重設狀態為「{message}」", + "Reset status to \"{icon}\"" : "重設狀態為「{icon}」", + "There was an error saving the status" : "儲存狀態時發生錯誤", + "There was an error clearing the status" : "變更狀態時發生錯誤", + "There was an error reverting the status" : "還原狀態時發生錯誤", + "Online status" : "線上狀態", + "Status message" : "狀態訊息", + "Set absence period" : "設定缺席時間", + "Set absence period and replacement" : "設定缺席時間與職務代理人", + "Your status was set automatically" : "您的狀態為自動設定", + "Clear status message" : "清除狀態訊息", + "Set status message" : "設定狀態訊息", + "Don't clear" : "不要清除", + "Today" : "今天", + "This week" : "本週", + "Online" : "上線", + "Away" : "離開", + "Do not disturb" : "請勿打擾", + "Invisible" : "隱藏", + "Offline" : "離線", + "Set status" : "設定狀態", + "There was an error saving the new status" : "儲存新狀態時發生錯誤", + "30 minutes" : "30 分鐘", + "1 hour" : "1 小時", + "4 hours" : "4 小時", + "Busy" : "忙碌", + "Mute all notifications" : "靜音所有通知", + "Appear offline" : "顯示為離線" +}, +"nplurals=1; plural=0;"); diff --git a/apps/user_status/l10n/zh_TW.json b/apps/user_status/l10n/zh_TW.json new file mode 100644 index 00000000000..9e99204b682 --- /dev/null +++ b/apps/user_status/l10n/zh_TW.json @@ -0,0 +1,49 @@ +{ "translations": { + "Recent statuses" : "最近的狀態", + "No recent status changes" : "最近沒有狀態變更", + "In a meeting" : "會議中", + "Commuting" : "通勤中", + "Out sick" : "病假", + "Vacationing" : "休假中", + "Out of office" : "不在辦公室", + "Working remotely" : "遠端工作", + "In a call" : "通話中", + "Be right back" : "馬上回來", + "User status" : "使用者狀態", + "Clear status after" : "多久後清除狀態", + "Emoji for your status message" : "狀態訊息的表情符號", + "What is your status?" : "您目前的狀態是什麼呢?", + "Predefined statuses" : "預先定義的狀態", + "Previously set" : "先前設定", + "Reset status" : "重設狀態", + "Reset status to \"{icon} {message}\"" : "重設狀態為「{icon} {message}」", + "Reset status to \"{message}\"" : "重設狀態為「{message}」", + "Reset status to \"{icon}\"" : "重設狀態為「{icon}」", + "There was an error saving the status" : "儲存狀態時發生錯誤", + "There was an error clearing the status" : "變更狀態時發生錯誤", + "There was an error reverting the status" : "還原狀態時發生錯誤", + "Online status" : "線上狀態", + "Status message" : "狀態訊息", + "Set absence period" : "設定缺席時間", + "Set absence period and replacement" : "設定缺席時間與職務代理人", + "Your status was set automatically" : "您的狀態為自動設定", + "Clear status message" : "清除狀態訊息", + "Set status message" : "設定狀態訊息", + "Don't clear" : "不要清除", + "Today" : "今天", + "This week" : "本週", + "Online" : "上線", + "Away" : "離開", + "Do not disturb" : "請勿打擾", + "Invisible" : "隱藏", + "Offline" : "離線", + "Set status" : "設定狀態", + "There was an error saving the new status" : "儲存新狀態時發生錯誤", + "30 minutes" : "30 分鐘", + "1 hour" : "1 小時", + "4 hours" : "4 小時", + "Busy" : "忙碌", + "Mute all notifications" : "靜音所有通知", + "Appear offline" : "顯示為離線" +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/user_status/lib/AppInfo/Application.php b/apps/user_status/lib/AppInfo/Application.php new file mode 100644 index 00000000000..5199c3fdbf0 --- /dev/null +++ b/apps/user_status/lib/AppInfo/Application.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\AppInfo; + +use OCA\UserStatus\Capabilities; +use OCA\UserStatus\Connector\UserStatusProvider; +use OCA\UserStatus\Dashboard\UserStatusWidget; +use OCA\UserStatus\Listener\BeforeTemplateRenderedListener; +use OCA\UserStatus\Listener\OutOfOfficeStatusListener; +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\IConfig; +use OCP\User\Events\OutOfOfficeChangedEvent; +use OCP\User\Events\OutOfOfficeClearedEvent; +use OCP\User\Events\OutOfOfficeEndedEvent; +use OCP\User\Events\OutOfOfficeScheduledEvent; +use OCP\User\Events\OutOfOfficeStartedEvent; +use OCP\User\Events\UserDeletedEvent; +use OCP\User\Events\UserLiveStatusEvent; +use OCP\UserStatus\IManager; + +/** + * 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); + $context->registerEventListener(OutOfOfficeChangedEvent::class, OutOfOfficeStatusListener::class); + $context->registerEventListener(OutOfOfficeScheduledEvent::class, OutOfOfficeStatusListener::class); + $context->registerEventListener(OutOfOfficeClearedEvent::class, OutOfOfficeStatusListener::class); + $context->registerEventListener(OutOfOfficeStartedEvent::class, OutOfOfficeStatusListener::class); + $context->registerEventListener(OutOfOfficeEndedEvent::class, OutOfOfficeStatusListener::class); + + $config = $this->getContainer()->query(IConfig::class); + $shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; + $shareeEnumerationInGroupOnly = $shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; + $shareeEnumerationPhone = $shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; + + // Register the Dashboard panel if user enumeration is enabled and not limited + if ($shareeEnumeration && !$shareeEnumerationInGroupOnly && !$shareeEnumerationPhone) { + $context->registerDashboardWidget(UserStatusWidget::class); + } + } + + public function boot(IBootContext $context): void { + /** @var IManager $userStatusManager */ + $userStatusManager = $context->getServerContainer()->get(IManager::class); + $userStatusManager->registerProvider(UserStatusProvider::class); + } +} diff --git a/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php b/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php new file mode 100644 index 00000000000..51a9c623a03 --- /dev/null +++ b/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\BackgroundJob; + +use OCA\UserStatus\Db\UserStatusMapper; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; + +/** + * Class ClearOldStatusesBackgroundJob + * + * @package OCA\UserStatus\BackgroundJob + */ +class ClearOldStatusesBackgroundJob extends TimedJob { + + /** + * ClearOldStatusesBackgroundJob constructor. + * + * @param ITimeFactory $time + * @param UserStatusMapper $mapper + */ + public function __construct( + ITimeFactory $time, + private UserStatusMapper $mapper, + ) { + parent::__construct($time); + + $this->setInterval(60); + } + + /** + * @inheritDoc + */ + protected function run($argument) { + $now = $this->time->getTime(); + + $this->mapper->clearOlderThanClearAt($now); + $this->mapper->clearStatusesOlderThan($now - StatusService::INVALIDATE_STATUS_THRESHOLD, $now); + } +} diff --git a/apps/user_status/lib/Capabilities.php b/apps/user_status/lib/Capabilities.php new file mode 100644 index 00000000000..c3edbc032d6 --- /dev/null +++ b/apps/user_status/lib/Capabilities.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus; + +use OCP\Capabilities\ICapability; +use OCP\IEmojiHelper; + +/** + * Class Capabilities + * + * @package OCA\UserStatus + */ +class Capabilities implements ICapability { + public function __construct( + private IEmojiHelper $emojiHelper, + ) { + } + + /** + * @return array{user_status: array{enabled: bool, restore: bool, supports_emoji: bool, supports_busy: bool}} + */ + public function getCapabilities() { + return [ + 'user_status' => [ + 'enabled' => true, + 'restore' => true, + 'supports_emoji' => $this->emojiHelper->doesPlatformSupportEmoji(), + 'supports_busy' => true, + ], + ]; + } +} diff --git a/apps/user_status/lib/Connector/UserStatus.php b/apps/user_status/lib/Connector/UserStatus.php new file mode 100644 index 00000000000..04467a99e5e --- /dev/null +++ b/apps/user_status/lib/Connector/UserStatus.php @@ -0,0 +1,86 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Connector; + +use DateTimeImmutable; +use OCA\UserStatus\Db; +use OCP\UserStatus\IUserStatus; + +class UserStatus implements IUserStatus { + + /** @var string */ + private $userId; + + /** @var string */ + private $status; + + /** @var string|null */ + private $message; + + /** @var string|null */ + private $icon; + + /** @var DateTimeImmutable|null */ + private $clearAt; + + public function __construct( + private Db\UserStatus $internalStatus, + ) { + $this->userId = $this->internalStatus->getUserId(); + $this->status = $this->internalStatus->getStatus(); + $this->message = $this->internalStatus->getCustomMessage(); + $this->icon = $this->internalStatus->getCustomIcon(); + + if ($this->internalStatus->getStatus() === IUserStatus::INVISIBLE) { + $this->status = IUserStatus::OFFLINE; + } + if ($this->internalStatus->getClearAt() !== null) { + $this->clearAt = DateTimeImmutable::createFromFormat('U', (string)$this->internalStatus->getClearAt()); + } + } + + /** + * @inheritDoc + */ + public function getUserId(): string { + return $this->userId; + } + + /** + * @inheritDoc + */ + public function getStatus(): string { + return $this->status; + } + + /** + * @inheritDoc + */ + public function getMessage(): ?string { + return $this->message; + } + + /** + * @inheritDoc + */ + public function getIcon(): ?string { + return $this->icon; + } + + /** + * @inheritDoc + */ + public function getClearAt(): ?DateTimeImmutable { + return $this->clearAt; + } + + public function getInternal(): Db\UserStatus { + return $this->internalStatus; + } +} diff --git a/apps/user_status/lib/Connector/UserStatusProvider.php b/apps/user_status/lib/Connector/UserStatusProvider.php new file mode 100644 index 00000000000..e84d69d1eb2 --- /dev/null +++ b/apps/user_status/lib/Connector/UserStatusProvider.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Connector; + +use OC\UserStatus\ISettableProvider; +use OCA\UserStatus\Service\StatusService; +use OCP\UserStatus\IProvider; + +class UserStatusProvider implements IProvider, ISettableProvider { + + /** + * UserStatusProvider constructor. + * + * @param StatusService $service + */ + public function __construct( + private StatusService $service, + ) { + } + + /** + * @inheritDoc + */ + public function getUserStatuses(array $userIds): array { + $statuses = $this->service->findByUserIds($userIds); + + $userStatuses = []; + foreach ($statuses as $status) { + $userStatuses[$status->getUserId()] = new UserStatus($status); + } + + return $userStatuses; + } + + public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup, ?string $customMessage = null): void { + $this->service->setUserStatus($userId, $status, $messageId, $createBackup, $customMessage); + } + + public function revertUserStatus(string $userId, string $messageId, string $status): void { + $this->service->revertUserStatus($userId, $messageId); + } + + public function revertMultipleUserStatus(array $userIds, string $messageId, string $status): void { + $this->service->revertMultipleUserStatus($userIds, $messageId); + } +} diff --git a/apps/user_status/lib/ContactsMenu/StatusProvider.php b/apps/user_status/lib/ContactsMenu/StatusProvider.php new file mode 100644 index 00000000000..6a6949b46ba --- /dev/null +++ b/apps/user_status/lib/ContactsMenu/StatusProvider.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UserStatus\ContactsMenu; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Service\StatusService; +use OCP\Contacts\ContactsMenu\IBulkProvider; +use OCP\Contacts\ContactsMenu\IEntry; +use function array_combine; +use function array_filter; +use function array_map; + +class StatusProvider implements IBulkProvider { + + public function __construct( + private StatusService $statusService, + ) { + } + + public function process(array $entries): void { + $uids = array_filter( + array_map(fn (IEntry $entry): ?string => $entry->getProperty('UID'), $entries) + ); + + $statuses = $this->statusService->findByUserIds($uids); + /** @var array<string, UserStatus> $indexed */ + $indexed = array_combine( + array_map(fn (UserStatus $status) => $status->getUserId(), $statuses), + $statuses + ); + + foreach ($entries as $entry) { + $uid = $entry->getProperty('UID'); + if ($uid !== null && isset($indexed[$uid])) { + $status = $indexed[$uid]; + $entry->setStatus( + $status->getStatus(), + $status->getCustomMessage(), + $status->getStatusMessageTimestamp(), + $status->getCustomIcon(), + ); + } + } + } + +} diff --git a/apps/user_status/lib/Controller/HeartbeatController.php b/apps/user_status/lib/Controller/HeartbeatController.php new file mode 100644 index 00000000000..30f4af6572a --- /dev/null +++ b/apps/user_status/lib/Controller/HeartbeatController.php @@ -0,0 +1,94 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Controller; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\ResponseDefinitions; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IRequest; +use OCP\IUserSession; +use OCP\User\Events\UserLiveStatusEvent; +use OCP\UserStatus\IUserStatus; + +/** + * @psalm-import-type UserStatusPrivate from ResponseDefinitions + */ +class HeartbeatController extends OCSController { + + public function __construct( + string $appName, + IRequest $request, + private IEventDispatcher $eventDispatcher, + private IUserSession $userSession, + private ITimeFactory $timeFactory, + private StatusService $service, + ) { + parent::__construct($appName, $request); + } + + /** + * Keep the status alive + * + * @param string $status Only online, away + * + * @return DataResponse<Http::STATUS_OK, UserStatusPrivate, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NO_CONTENT, list<empty>, array{}> + * + * 200: Status successfully updated + * 204: User has no status to keep alive + * 400: Invalid status to update + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'PUT', url: '/api/v1/heartbeat')] + public function heartbeat(string $status): DataResponse { + if (!\in_array($status, [IUserStatus::ONLINE, IUserStatus::AWAY], true)) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + $user = $this->userSession->getUser(); + if ($user === null) { + return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + $event = new UserLiveStatusEvent( + $user, + $status, + $this->timeFactory->getTime() + ); + + $this->eventDispatcher->dispatchTyped($event); + + $userStatus = $event->getUserStatus(); + if (!$userStatus) { + return new DataResponse([], Http::STATUS_NO_CONTENT); + } + + /** @psalm-suppress UndefinedInterfaceMethod */ + return new DataResponse($this->formatStatus($userStatus->getInternal())); + } + + private function formatStatus(UserStatus $status): array { + return [ + 'userId' => $status->getUserId(), + 'message' => $status->getCustomMessage(), + 'messageId' => $status->getMessageId(), + 'messageIsPredefined' => $status->getMessageId() !== null, + 'icon' => $status->getCustomIcon(), + 'clearAt' => $status->getClearAt(), + 'status' => $status->getStatus(), + 'statusIsUserDefined' => $status->getIsUserDefined(), + ]; + } +} diff --git a/apps/user_status/lib/Controller/PredefinedStatusController.php b/apps/user_status/lib/Controller/PredefinedStatusController.php new file mode 100644 index 00000000000..70262d1108a --- /dev/null +++ b/apps/user_status/lib/Controller/PredefinedStatusController.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Controller; + +use OCA\UserStatus\ResponseDefinitions; +use OCA\UserStatus\Service\PredefinedStatusService; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\IRequest; + +/** + * @package OCA\UserStatus\Controller + * + * @psalm-import-type UserStatusPredefined from ResponseDefinitions + */ +class PredefinedStatusController extends OCSController { + + /** + * AStatusController constructor. + * + * @param string $appName + * @param IRequest $request + * @param PredefinedStatusService $predefinedStatusService + */ + public function __construct( + string $appName, + IRequest $request, + private PredefinedStatusService $predefinedStatusService, + ) { + parent::__construct($appName, $request); + } + + /** + * Get all predefined messages + * + * @return DataResponse<Http::STATUS_OK, list<UserStatusPredefined>, array{}> + * + * 200: Predefined statuses returned + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/api/v1/predefined_statuses/')] + public function findAll():DataResponse { + // Filtering out the invisible one, that should only be set by API + return new DataResponse(array_values(array_filter($this->predefinedStatusService->getDefaultStatuses(), function (array $status) { + return !array_key_exists('visible', $status) || $status['visible'] === true; + }))); + } +} diff --git a/apps/user_status/lib/Controller/StatusesController.php b/apps/user_status/lib/Controller/StatusesController.php new file mode 100644 index 00000000000..44688c39023 --- /dev/null +++ b/apps/user_status/lib/Controller/StatusesController.php @@ -0,0 +1,104 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Controller; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\ResponseDefinitions; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\IRequest; +use OCP\UserStatus\IUserStatus; + +/** + * @psalm-import-type UserStatusType from ResponseDefinitions + * @psalm-import-type UserStatusPublic from ResponseDefinitions + */ +class StatusesController extends OCSController { + + /** + * StatusesController constructor. + * + * @param string $appName + * @param IRequest $request + * @param StatusService $service + */ + public function __construct( + string $appName, + IRequest $request, + private StatusService $service, + ) { + parent::__construct($appName, $request); + } + + /** + * Find statuses of users + * + * @param int|null $limit Maximum number of statuses to find + * @param non-negative-int|null $offset Offset for finding statuses + * @return DataResponse<Http::STATUS_OK, list<UserStatusPublic>, array{}> + * + * 200: Statuses returned + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/api/v1/statuses')] + public function findAll(?int $limit = null, ?int $offset = null): DataResponse { + $allStatuses = $this->service->findAll($limit, $offset); + + return new DataResponse(array_values(array_map(function ($userStatus) { + return $this->formatStatus($userStatus); + }, $allStatuses))); + } + + /** + * Find the status of a user + * + * @param string $userId ID of the user + * @return DataResponse<Http::STATUS_OK, UserStatusPublic, array{}> + * @throws OCSNotFoundException The user was not found + * + * 200: Status returned + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/api/v1/statuses/{userId}')] + 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)); + } + + /** + * @param UserStatus $status + * @return UserStatusPublic + */ + private function formatStatus(UserStatus $status): array { + /** @var UserStatusType $visibleStatus */ + $visibleStatus = $status->getStatus(); + if ($visibleStatus === IUserStatus::INVISIBLE) { + $visibleStatus = IUserStatus::OFFLINE; + } + + return [ + 'userId' => $status->getUserId(), + 'message' => $status->getCustomMessage(), + 'icon' => $status->getCustomIcon(), + 'clearAt' => $status->getClearAt(), + 'status' => $visibleStatus, + ]; + } +} diff --git a/apps/user_status/lib/Controller/UserStatusController.php b/apps/user_status/lib/Controller/UserStatusController.php new file mode 100644 index 00000000000..9b3807ce86e --- /dev/null +++ b/apps/user_status/lib/Controller/UserStatusController.php @@ -0,0 +1,209 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Controller; + +use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService; +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\ResponseDefinitions; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\IRequest; +use Psr\Log\LoggerInterface; + +/** + * @psalm-import-type UserStatusType from ResponseDefinitions + * @psalm-import-type UserStatusPrivate from ResponseDefinitions + */ +class UserStatusController extends OCSController { + public function __construct( + string $appName, + IRequest $request, + private ?string $userId, + private LoggerInterface $logger, + private StatusService $service, + private CalendarStatusService $calendarStatusService, + ) { + parent::__construct($appName, $request); + } + + /** + * Get the status of the current user + * + * @return DataResponse<Http::STATUS_OK, UserStatusPrivate, array{}> + * @throws OCSNotFoundException The user was not found + * + * 200: The status was found successfully + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/api/v1/user_status')] + public function getStatus(): DataResponse { + try { + $this->calendarStatusService->processCalendarStatus($this->userId); + $userStatus = $this->service->findByUserId($this->userId); + } catch (DoesNotExistException $ex) { + throw new OCSNotFoundException('No status for the current user'); + } + + return new DataResponse($this->formatStatus($userStatus)); + } + + /** + * Update the status type of the current user + * + * @param string $statusType The new status type + * @return DataResponse<Http::STATUS_OK, UserStatusPrivate, array{}> + * @throws OCSBadRequestException The status type is invalid + * + * 200: The status was updated successfully + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'PUT', url: '/api/v1/user_status/status')] + public function setStatus(string $statusType): DataResponse { + try { + $status = $this->service->setStatus($this->userId, $statusType, null, true); + + $this->service->removeBackupUserStatus($this->userId); + 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); + } + } + + /** + * Set the message to a predefined message for the current user + * + * @param string $messageId ID of the predefined message + * @param int|null $clearAt When the message should be cleared + * @return DataResponse<Http::STATUS_OK, UserStatusPrivate, array{}> + * @throws OCSBadRequestException The clearAt or message-id is invalid + * + * 200: The message was updated successfully + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'PUT', url: '/api/v1/user_status/message/predefined')] + public function setPredefinedMessage(string $messageId, + ?int $clearAt): DataResponse { + try { + $status = $this->service->setPredefinedMessage($this->userId, $messageId, $clearAt); + $this->service->removeBackupUserStatus($this->userId); + 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); + } + } + + /** + * Set the message to a custom message for the current user + * + * @param string|null $statusIcon Icon of the status + * @param string|null $message Message of the status + * @param int|null $clearAt When the message should be cleared + * @return DataResponse<Http::STATUS_OK, UserStatusPrivate, array{}> + * @throws OCSBadRequestException The clearAt or icon is invalid or the message is too long + * @throws OCSNotFoundException No status for the current user + * + * 200: The message was updated successfully + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'PUT', url: '/api/v1/user_status/message/custom')] + public function setCustomMessage(?string $statusIcon, + ?string $message, + ?int $clearAt): DataResponse { + try { + if (($statusIcon !== null && $statusIcon !== '') || ($message !== null && $message !== '') || ($clearAt !== null && $clearAt !== 0)) { + $status = $this->service->setCustomMessage($this->userId, $statusIcon, $message, $clearAt); + } else { + $this->service->clearMessage($this->userId); + $status = $this->service->findByUserId($this->userId); + } + $this->service->removeBackupUserStatus($this->userId); + 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); + } catch (DoesNotExistException $ex) { + throw new OCSNotFoundException('No status for the current user'); + } + } + + /** + * Clear the message of the current user + * + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> + * + * 200: Message cleared successfully + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'DELETE', url: '/api/v1/user_status/message')] + public function clearMessage(): DataResponse { + $this->service->clearMessage($this->userId); + return new DataResponse([]); + } + + /** + * Revert the status to the previous status + * + * @param string $messageId ID of the message to delete + * + * @return DataResponse<Http::STATUS_OK, UserStatusPrivate|list<empty>, array{}> + * + * 200: Status reverted + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'DELETE', url: '/api/v1/user_status/revert/{messageId}')] + public function revertStatus(string $messageId): DataResponse { + $backupStatus = $this->service->revertUserStatus($this->userId, $messageId, true); + if ($backupStatus) { + return new DataResponse($this->formatStatus($backupStatus)); + } + return new DataResponse([]); + } + + /** + * @param UserStatus $status + * @return UserStatusPrivate + */ + private function formatStatus(UserStatus $status): array { + /** @var UserStatusType $visibleStatus */ + $visibleStatus = $status->getStatus(); + return [ + 'userId' => $status->getUserId(), + 'message' => $status->getCustomMessage(), + 'messageId' => $status->getMessageId(), + 'messageIsPredefined' => $status->getMessageId() !== null, + 'icon' => $status->getCustomIcon(), + 'clearAt' => $status->getClearAt(), + 'status' => $visibleStatus, + 'statusIsUserDefined' => $status->getIsUserDefined(), + ]; + } +} diff --git a/apps/user_status/lib/Dashboard/UserStatusWidget.php b/apps/user_status/lib/Dashboard/UserStatusWidget.php new file mode 100644 index 00000000000..2870a2c1907 --- /dev/null +++ b/apps/user_status/lib/Dashboard/UserStatusWidget.php @@ -0,0 +1,177 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Dashboard; + +use OCA\UserStatus\AppInfo\Application; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Services\IInitialState; +use OCP\Dashboard\IAPIWidget; +use OCP\Dashboard\IAPIWidgetV2; +use OCP\Dashboard\IIconWidget; +use OCP\Dashboard\IOptionWidget; +use OCP\Dashboard\Model\WidgetItem; +use OCP\Dashboard\Model\WidgetItems; +use OCP\Dashboard\Model\WidgetOptions; +use OCP\IDateTimeFormatter; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\UserStatus\IUserStatus; + +/** + * Class UserStatusWidget + * + * @package OCA\UserStatus + */ +class UserStatusWidget implements IAPIWidget, IAPIWidgetV2, IIconWidget, IOptionWidget { + /** + * UserStatusWidget constructor + * + * @param IL10N $l10n + * @param IDateTimeFormatter $dateTimeFormatter + * @param IURLGenerator $urlGenerator + * @param IInitialState $initialStateService + * @param IUserManager $userManager + * @param IUserSession $userSession + * @param StatusService $service + */ + public function __construct( + private IL10N $l10n, + private IDateTimeFormatter $dateTimeFormatter, + private IURLGenerator $urlGenerator, + private IInitialState $initialStateService, + private IUserManager $userManager, + private IUserSession $userSession, + private StatusService $service, + ) { + } + + /** + * @inheritDoc + */ + public function getId(): string { + return Application::APP_ID; + } + + /** + * @inheritDoc + */ + public function getTitle(): string { + return $this->l10n->t('Recent statuses'); + } + + /** + * @inheritDoc + */ + public function getOrder(): int { + return 5; + } + + /** + * @inheritDoc + */ + public function getIconClass(): string { + return 'icon-user-status-dark'; + } + + /** + * @inheritDoc + */ + public function getIconUrl(): string { + return $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->imagePath(Application::APP_ID, 'app-dark.svg') + ); + } + + /** + * @inheritDoc + */ + public function getUrl(): ?string { + return null; + } + + /** + * @inheritDoc + */ + public function load(): void { + } + + private function getWidgetData(string $userId, ?string $since = null, int $limit = 7): array { + // Fetch status updates and filter current user + $recentStatusUpdates = array_slice( + array_filter( + $this->service->findAllRecentStatusChanges($limit + 1, 0), + static function (UserStatus $status) use ($userId, $since): bool { + return $status->getUserId() !== $userId + && ($since === null || $status->getStatusTimestamp() > (int)$since); + } + ), + 0, + $limit + ); + return array_map(function (UserStatus $status): array { + $user = $this->userManager->get($status->getUserId()); + $displayName = $status->getUserId(); + if ($user !== null) { + $displayName = $user->getDisplayName(); + } + + return [ + 'userId' => $status->getUserId(), + 'displayName' => $displayName, + 'status' => $status->getStatus() === IUserStatus::INVISIBLE + ? IUserStatus::OFFLINE + : $status->getStatus(), + 'icon' => $status->getCustomIcon(), + 'message' => $status->getCustomMessage(), + 'timestamp' => $status->getStatusMessageTimestamp(), + ]; + }, $recentStatusUpdates); + } + + /** + * @inheritDoc + */ + public function getItems(string $userId, ?string $since = null, int $limit = 7): array { + $widgetItemsData = $this->getWidgetData($userId, $since, $limit); + + return array_values(array_map(function (array $widgetData) { + $formattedDate = $this->dateTimeFormatter->formatTimeSpan($widgetData['timestamp']); + return new WidgetItem( + $widgetData['displayName'], + $widgetData['icon'] . ($widgetData['icon'] ? ' ' : '') . $widgetData['message'] . ', ' . $formattedDate, + // https://nextcloud.local/index.php/u/julien + $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->linkToRoute('profile.ProfilePage.index', ['targetUserId' => $widgetData['userId']]) + ), + $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->linkToRoute('core.avatar.getAvatar', ['userId' => $widgetData['userId'], 'size' => 44]) + ), + (string)$widgetData['timestamp'] + ); + }, $widgetItemsData)); + } + + /** + * @inheritDoc + */ + public function getItemsV2(string $userId, ?string $since = null, int $limit = 7): WidgetItems { + $items = $this->getItems($userId, $since, $limit); + return new WidgetItems( + $items, + count($items) === 0 ? $this->l10n->t('No recent status changes') : '', + ); + } + + public function getWidgetOptions(): WidgetOptions { + return new WidgetOptions(true); + } +} diff --git a/apps/user_status/lib/Db/UserStatus.php b/apps/user_status/lib/Db/UserStatus.php new file mode 100644 index 00000000000..b2da4a9e07a --- /dev/null +++ b/apps/user_status/lib/Db/UserStatus.php @@ -0,0 +1,86 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Db; + +use OCP\AppFramework\Db\Entity; +use OCP\DB\Types; + +/** + * 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|null getMessageId() + * @method void setMessageId(string|null $messageId) + * @method string|null getCustomIcon() + * @method void setCustomIcon(string|null $customIcon) + * @method string|null getCustomMessage() + * @method void setCustomMessage(string|null $customMessage) + * @method int|null getClearAt() + * @method void setClearAt(int|null $clearAt) + * @method setIsBackup(bool $isBackup): void + * @method getIsBackup(): bool + * @method int getStatusMessageTimestamp() + * @method void setStatusMessageTimestamp(int $statusTimestamp) + */ +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; + + /** @var bool $isBackup */ + public $isBackup; + + /** @var int */ + protected $statusMessageTimestamp = 0; + + public function __construct() { + $this->addType('userId', 'string'); + $this->addType('status', 'string'); + $this->addType('statusTimestamp', Types::INTEGER); + $this->addType('isUserDefined', 'boolean'); + $this->addType('messageId', 'string'); + $this->addType('customIcon', 'string'); + $this->addType('customMessage', 'string'); + $this->addType('clearAt', Types::INTEGER); + $this->addType('isBackup', 'boolean'); + $this->addType('statusMessageTimestamp', Types::INTEGER); + } +} diff --git a/apps/user_status/lib/Db/UserStatusMapper.php b/apps/user_status/lib/Db/UserStatusMapper.php new file mode 100644 index 00000000000..15982d44fd8 --- /dev/null +++ b/apps/user_status/lib/Db/UserStatusMapper.php @@ -0,0 +1,197 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UserStatus\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\UserStatus\IUserStatus; + +/** + * @template-extends QBMapper<UserStatus> + */ +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 int|null $limit + * @param int|null $offset + * @return array + */ + public function findAllRecent(?int $limit = null, ?int $offset = null): array { + $qb = $this->db->getQueryBuilder(); + + $qb + ->select('*') + ->from($this->tableName) + ->orderBy('status_message_timestamp', 'DESC') + ->where($qb->expr()->andX( + $qb->expr()->neq('status_message_timestamp', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT), + $qb->expr()->orX( + $qb->expr()->notIn('status', $qb->createNamedParameter([IUserStatus::ONLINE, IUserStatus::AWAY, IUserStatus::OFFLINE], IQueryBuilder::PARAM_STR_ARRAY)), + $qb->expr()->isNotNull('message_id'), + $qb->expr()->isNotNull('custom_icon'), + $qb->expr()->isNotNull('custom_message'), + ), + $qb->expr()->notLike('user_id', $qb->createNamedParameter($this->db->escapeLikeParameter('_') . '%')) + )); + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + return $this->findEntities($qb); + } + + /** + * @param string $userId + * @return UserStatus + * @throws DoesNotExistException + */ + public function findByUserId(string $userId, bool $isBackup = false): UserStatus { + $qb = $this->db->getQueryBuilder(); + $qb + ->select('*') + ->from($this->tableName) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($isBackup ? '_' . $userId : $userId, IQueryBuilder::PARAM_STR))); + + return $this->findEntity($qb); + } + + /** + * @param array $userIds + * @return array + */ + public function findByUserIds(array $userIds): array { + $qb = $this->db->getQueryBuilder(); + $qb + ->select('*') + ->from($this->tableName) + ->where($qb->expr()->in('user_id', $qb->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY))); + + return $this->findEntities($qb); + } + + /** + * @param int $olderThan + * @param int $now + */ + public function clearStatusesOlderThan(int $olderThan, int $now): void { + $qb = $this->db->getQueryBuilder(); + $qb->update($this->tableName) + ->set('status', $qb->createNamedParameter(IUserStatus::OFFLINE)) + ->set('is_user_defined', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)) + ->set('status_timestamp', $qb->createNamedParameter($now, IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->lte('status_timestamp', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->neq('status', $qb->createNamedParameter(IUserStatus::OFFLINE))) + ->andWhere($qb->expr()->orX( + $qb->expr()->eq('is_user_defined', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL), IQueryBuilder::PARAM_BOOL), + $qb->expr()->eq('status', $qb->createNamedParameter(IUserStatus::ONLINE)) + )); + + $qb->executeStatement(); + } + + /** + * Clear all statuses older than a given timestamp + * + * @param int $timestamp + */ + public function clearOlderThanClearAt(int $timestamp): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where($qb->expr()->isNotNull('clear_at')) + ->andWhere($qb->expr()->lte('clear_at', $qb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))); + + $qb->executeStatement(); + } + + + /** + * Deletes a user status so we can restore the backup + * + * @param string $userId + * @param string $messageId + * @return bool True if an entry was deleted + */ + public function deleteCurrentStatusToRestoreBackup(string $userId, string $messageId): bool { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('message_id', $qb->createNamedParameter($messageId))) + ->andWhere($qb->expr()->eq('is_backup', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))); + return $qb->executeStatement() > 0; + } + + public function deleteByIds(array $ids): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where($qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))); + $qb->executeStatement(); + } + + /** + * @param string $userId + * @return bool + * @throws \OCP\DB\Exception + */ + public function createBackupStatus(string $userId): bool { + // Prefix user account with an underscore because user_id is marked as unique + // in the table. Starting a username with an underscore is not allowed so this + // shouldn't create any trouble. + $qb = $this->db->getQueryBuilder(); + $qb->update($this->tableName) + ->set('is_backup', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)) + ->set('user_id', $qb->createNamedParameter('_' . $userId)) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); + return $qb->executeStatement() > 0; + } + + public function restoreBackupStatuses(array $ids): void { + $qb = $this->db->getQueryBuilder(); + $qb->update($this->tableName) + ->set('is_backup', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)) + ->set('user_id', $qb->func()->substring('user_id', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT))) + ->where($qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))); + + $qb->executeStatement(); + } +} diff --git a/apps/user_status/lib/Exception/InvalidClearAtException.php b/apps/user_status/lib/Exception/InvalidClearAtException.php new file mode 100644 index 00000000000..a3bd4dfa0d0 --- /dev/null +++ b/apps/user_status/lib/Exception/InvalidClearAtException.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Exception; + +class InvalidClearAtException extends \Exception { +} diff --git a/apps/user_status/lib/Exception/InvalidMessageIdException.php b/apps/user_status/lib/Exception/InvalidMessageIdException.php new file mode 100644 index 00000000000..1feb36a916a --- /dev/null +++ b/apps/user_status/lib/Exception/InvalidMessageIdException.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Exception; + +class InvalidMessageIdException extends \Exception { +} diff --git a/apps/user_status/lib/Exception/InvalidStatusIconException.php b/apps/user_status/lib/Exception/InvalidStatusIconException.php new file mode 100644 index 00000000000..80dff2a7666 --- /dev/null +++ b/apps/user_status/lib/Exception/InvalidStatusIconException.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Exception; + +class InvalidStatusIconException extends \Exception { +} diff --git a/apps/user_status/lib/Exception/InvalidStatusTypeException.php b/apps/user_status/lib/Exception/InvalidStatusTypeException.php new file mode 100644 index 00000000000..a09284be40e --- /dev/null +++ b/apps/user_status/lib/Exception/InvalidStatusTypeException.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Exception; + +class InvalidStatusTypeException extends \Exception { +} diff --git a/apps/user_status/lib/Exception/StatusMessageTooLongException.php b/apps/user_status/lib/Exception/StatusMessageTooLongException.php new file mode 100644 index 00000000000..03d578abf46 --- /dev/null +++ b/apps/user_status/lib/Exception/StatusMessageTooLongException.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Exception; + +class StatusMessageTooLongException extends \Exception { +} diff --git a/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php b/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php new file mode 100644 index 00000000000..ab3a1e62beb --- /dev/null +++ b/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php @@ -0,0 +1,75 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UserStatus\Listener; + +use OC\Profile\ProfileManager; +use OCA\UserStatus\AppInfo\Application; +use OCA\UserStatus\Service\JSDataService; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IInitialStateService; +use OCP\IUserSession; +use OCP\Util; + +/** @template-implements IEventListener<BeforeTemplateRenderedEvent> */ +class BeforeTemplateRenderedListener implements IEventListener { + + /** @var ProfileManager */ + private $profileManager; + + /** + * BeforeTemplateRenderedListener constructor. + * + * @param ProfileManager $profileManager + * @param IUserSession $userSession + * @param IInitialStateService $initialState + * @param JSDataService $jsDataService + */ + public function __construct( + ProfileManager $profileManager, + private IUserSession $userSession, + private IInitialStateService $initialState, + private JSDataService $jsDataService, + ) { + $this->profileManager = $profileManager; + } + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + $user = $this->userSession->getUser(); + if ($user === null) { + return; + } + + if (!($event instanceof BeforeTemplateRenderedEvent)) { + // Unrelated + return; + } + + if (!$event->isLoggedIn() || $event->getResponse()->getRenderAs() !== TemplateResponse::RENDER_AS_USER) { + return; + } + + $this->initialState->provideLazyInitialState(Application::APP_ID, 'status', function () { + return $this->jsDataService; + }); + + $this->initialState->provideLazyInitialState(Application::APP_ID, 'profileEnabled', function () use ($user) { + return ['profileEnabled' => $this->profileManager->isProfileEnabled($user)]; + }); + + Util::addScript('user_status', 'menu'); + Util::addStyle('user_status', 'user-status-menu'); + } +} diff --git a/apps/user_status/lib/Listener/OutOfOfficeStatusListener.php b/apps/user_status/lib/Listener/OutOfOfficeStatusListener.php new file mode 100644 index 00000000000..6337d637896 --- /dev/null +++ b/apps/user_status/lib/Listener/OutOfOfficeStatusListener.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Listener; + +use OCA\DAV\BackgroundJob\UserStatusAutomation; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\User\Events\OutOfOfficeChangedEvent; +use OCP\User\Events\OutOfOfficeClearedEvent; +use OCP\User\Events\OutOfOfficeEndedEvent; +use OCP\User\Events\OutOfOfficeScheduledEvent; +use OCP\User\Events\OutOfOfficeStartedEvent; +use OCP\UserStatus\IManager; +use OCP\UserStatus\IUserStatus; + +/** + * Class UserDeletedListener + * + * @template-implements IEventListener<OutOfOfficeScheduledEvent|OutOfOfficeChangedEvent|OutOfOfficeClearedEvent|OutOfOfficeStartedEvent|OutOfOfficeEndedEvent> + * + */ +class OutOfOfficeStatusListener implements IEventListener { + public function __construct( + private IJobList $jobsList, + private ITimeFactory $time, + private IManager $manager, + ) { + } + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + if ($event instanceof OutOfOfficeClearedEvent) { + $this->manager->revertUserStatus($event->getData()->getUser()->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND); + $this->jobsList->scheduleAfter(UserStatusAutomation::class, $this->time->getTime(), ['userId' => $event->getData()->getUser()->getUID()]); + return; + } + + if ($event instanceof OutOfOfficeScheduledEvent + || $event instanceof OutOfOfficeChangedEvent + || $event instanceof OutOfOfficeStartedEvent + || $event instanceof OutOfOfficeEndedEvent + ) { + // This might be overwritten by the office hours automation, but that is ok. This is just in case no office hours are set + $this->jobsList->scheduleAfter(UserStatusAutomation::class, $this->time->getTime(), ['userId' => $event->getData()->getUser()->getUID()]); + } + } +} diff --git a/apps/user_status/lib/Listener/UserDeletedListener.php b/apps/user_status/lib/Listener/UserDeletedListener.php new file mode 100644 index 00000000000..bf021635156 --- /dev/null +++ b/apps/user_status/lib/Listener/UserDeletedListener.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Listener; + +use OCA\UserStatus\Service\StatusService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\User\Events\UserDeletedEvent; + +/** + * Class UserDeletedListener + * + * @package OCA\UserStatus\Listener + * @template-implements IEventListener<UserDeletedEvent> + */ +class UserDeletedListener implements IEventListener { + + /** + * UserDeletedListener constructor. + * + * @param StatusService $service + */ + public function __construct( + private StatusService $service, + ) { + } + + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + if (!($event instanceof UserDeletedEvent)) { + // Unrelated + return; + } + + $user = $event->getUser(); + $this->service->removeUserStatus($user->getUID()); + } +} diff --git a/apps/user_status/lib/Listener/UserLiveStatusListener.php b/apps/user_status/lib/Listener/UserLiveStatusListener.php new file mode 100644 index 00000000000..2db999d3712 --- /dev/null +++ b/apps/user_status/lib/Listener/UserLiveStatusListener.php @@ -0,0 +1,115 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Listener; + +use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService; +use OCA\UserStatus\Connector\UserStatus as ConnectorUserStatus; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Db\UserStatusMapper; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\Exception; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\User\Events\UserLiveStatusEvent; +use OCP\UserStatus\IUserStatus; +use Psr\Log\LoggerInterface; + +/** + * Class UserDeletedListener + * + * @package OCA\UserStatus\Listener + * @template-implements IEventListener<UserLiveStatusEvent> + */ +class UserLiveStatusListener implements IEventListener { + public function __construct( + private UserStatusMapper $mapper, + private StatusService $statusService, + private ITimeFactory $timeFactory, + private CalendarStatusService $calendarStatusService, + private LoggerInterface $logger, + ) { + } + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + if (!($event instanceof UserLiveStatusEvent)) { + // Unrelated + return; + } + + $user = $event->getUser(); + try { + $this->calendarStatusService->processCalendarStatus($user->getUID()); + $userStatus = $this->statusService->findByUserId($user->getUID()); + } catch (DoesNotExistException $ex) { + $userStatus = new UserStatus(); + $userStatus->setUserId($user->getUID()); + $userStatus->setStatus(IUserStatus::OFFLINE); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + } + + // If the status is user-defined and one of the persistent status, we + // will not override it. + if ($userStatus->getIsUserDefined() + && \in_array($userStatus->getStatus(), StatusService::PERSISTENT_STATUSES, true)) { + return; + } + + // Don't overwrite the "away" calendar status if it's set + if ($userStatus->getMessageId() === IUserStatus::MESSAGE_CALENDAR_BUSY) { + $event->setUserStatus(new ConnectorUserStatus($userStatus)); + return; + } + + $needsUpdate = false; + + // If the current status is older than 5 minutes, + // treat it as outdated and update + if ($userStatus->getStatusTimestamp() < ($this->timeFactory->getTime() - StatusService::INVALIDATE_STATUS_THRESHOLD)) { + $needsUpdate = true; + } + + // If the emitted status is more important than the current status + // treat it as outdated and update + if (array_search($event->getStatus(), StatusService::PRIORITY_ORDERED_STATUSES) < array_search($userStatus->getStatus(), StatusService::PRIORITY_ORDERED_STATUSES)) { + $needsUpdate = true; + } + + if ($needsUpdate) { + $userStatus->setStatus($event->getStatus()); + $userStatus->setStatusTimestamp($event->getTimestamp()); + $userStatus->setIsUserDefined(false); + + if ($userStatus->getId() === null) { + try { + $this->mapper->insert($userStatus); + } catch (Exception $e) { + if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + // A different process might have written another status + // update to the DB while we're processing our stuff. + // We can safely ignore it as we're only changing between AWAY and ONLINE + // and not doing anything with the message or icon. + $this->logger->debug('Unique constraint violation for live user status', ['exception' => $e]); + return; + } + throw $e; + } + } else { + $this->mapper->update($userStatus); + } + } + + $event->setUserStatus(new ConnectorUserStatus($userStatus)); + } +} diff --git a/apps/user_status/lib/Migration/Version0001Date20200602134824.php b/apps/user_status/lib/Migration/Version0001Date20200602134824.php new file mode 100644 index 00000000000..678c2ec245a --- /dev/null +++ b/apps/user_status/lib/Migration/Version0001Date20200602134824.php @@ -0,0 +1,80 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Migration; + +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +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' => false, + ]); + $statusTable->addColumn('message_id', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + ]); + $statusTable->addColumn('custom_icon', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + ]); + $statusTable->addColumn('custom_message', Types::TEXT, [ + 'notnull' => false, + ]); + $statusTable->addColumn('clear_at', Types::INTEGER, [ + 'notnull' => false, + 'length' => 11, + 'unsigned' => true, + ]); + + $statusTable->setPrimaryKey(['id']); + $statusTable->addUniqueIndex(['user_id'], 'user_status_uid_ix'); + $statusTable->addIndex(['clear_at'], 'user_status_clr_ix'); + + return $schema; + } +} diff --git a/apps/user_status/lib/Migration/Version0002Date20200902144824.php b/apps/user_status/lib/Migration/Version0002Date20200902144824.php new file mode 100644 index 00000000000..199d2a4cc6b --- /dev/null +++ b/apps/user_status/lib/Migration/Version0002Date20200902144824.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Migration; + +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Class Version0001Date20200602134824 + * + * @package OCA\UserStatus\Migration + */ +class Version0002Date20200902144824 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->getTable('user_status'); + + $statusTable->addIndex(['status_timestamp'], 'user_status_tstmp_ix'); + $statusTable->addIndex(['is_user_defined', 'status'], 'user_status_iud_ix'); + + return $schema; + } +} diff --git a/apps/user_status/lib/Migration/Version1000Date20201111130204.php b/apps/user_status/lib/Migration/Version1000Date20201111130204.php new file mode 100644 index 00000000000..b0789684da0 --- /dev/null +++ b/apps/user_status/lib/Migration/Version1000Date20201111130204.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1000Date20201111130204 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $result = $this->ensureColumnIsNullable($schema, 'user_status', 'is_user_defined'); + + return $result ? $schema : null; + } + + protected function ensureColumnIsNullable(ISchemaWrapper $schema, string $tableName, string $columnName): bool { + $table = $schema->getTable($tableName); + $column = $table->getColumn($columnName); + + if ($column->getNotnull()) { + $column->setNotnull(false); + return true; + } + + return false; + } +} diff --git a/apps/user_status/lib/Migration/Version1003Date20210809144824.php b/apps/user_status/lib/Migration/Version1003Date20210809144824.php new file mode 100644 index 00000000000..7c6cf76adbe --- /dev/null +++ b/apps/user_status/lib/Migration/Version1003Date20210809144824.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Migration; + +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * @package OCA\UserStatus\Migration + */ +class Version1003Date20210809144824 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + * @since 23.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $statusTable = $schema->getTable('user_status'); + + if (!$statusTable->hasColumn('is_backup')) { + $statusTable->addColumn('is_backup', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false, + ]); + } + + return $schema; + } +} diff --git a/apps/user_status/lib/Migration/Version1008Date20230921144701.php b/apps/user_status/lib/Migration/Version1008Date20230921144701.php new file mode 100644 index 00000000000..30ebbf37b0e --- /dev/null +++ b/apps/user_status/lib/Migration/Version1008Date20230921144701.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UserStatus\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1008Date20230921144701 extends SimpleMigrationStep { + + public function __construct( + private IDBConnection $connection, + ) { + } + + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $statusTable = $schema->getTable('user_status'); + if (!($statusTable->hasColumn('status_message_timestamp'))) { + $statusTable->addColumn('status_message_timestamp', Types::INTEGER, [ + 'notnull' => true, + 'length' => 11, + 'unsigned' => true, + 'default' => 0, + ]); + } + if (!$statusTable->hasIndex('user_status_mtstmp_ix')) { + $statusTable->addIndex(['status_message_timestamp'], 'user_status_mtstmp_ix'); + } + + return $schema; + } + + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $qb = $this->connection->getQueryBuilder(); + + $update = $qb->update('user_status') + ->set('status_message_timestamp', 'status_timestamp'); + + $update->executeStatement(); + } +} diff --git a/apps/user_status/lib/ResponseDefinitions.php b/apps/user_status/lib/ResponseDefinitions.php new file mode 100644 index 00000000000..82f606dd301 --- /dev/null +++ b/apps/user_status/lib/ResponseDefinitions.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UserStatus; + +/** + * @psalm-type UserStatusClearAtTimeType = "day"|"week" + * + * @psalm-type UserStatusClearAt = array{ + * type: "period"|"end-of", + * time: int|UserStatusClearAtTimeType, + * } + * + * @psalm-type UserStatusPredefined = array{ + * id: string, + * icon: string, + * message: string, + * clearAt: ?UserStatusClearAt, + * } + * + * @psalm-type UserStatusType = "online"|"away"|"dnd"|"busy"|"offline"|"invisible" + * + * @psalm-type UserStatusPublic = array{ + * userId: string, + * message: ?string, + * icon: ?string, + * clearAt: ?int, + * status: UserStatusType, + * } + * + * @psalm-type UserStatusPrivate = UserStatusPublic&array{ + * messageId: ?string, + * messageIsPredefined: bool, + * statusIsUserDefined: bool, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/user_status/lib/Service/JSDataService.php b/apps/user_status/lib/Service/JSDataService.php new file mode 100644 index 00000000000..a777e97fe57 --- /dev/null +++ b/apps/user_status/lib/Service/JSDataService.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Service; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\IUserSession; +use OCP\UserStatus\IUserStatus; + +class JSDataService implements \JsonSerializable { + + /** + * JSDataService constructor. + * + * @param IUserSession $userSession + * @param StatusService $statusService + */ + public function __construct( + private IUserSession $userSession, + private StatusService $statusService, + ) { + } + + public function jsonSerialize(): array { + $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' => IUserStatus::OFFLINE, + 'statusIsUserDefined' => false, + ]; + } + + return [ + 'userId' => $status->getUserId(), + 'message' => $status->getCustomMessage(), + 'messageId' => $status->getMessageId(), + 'messageIsPredefined' => $status->getMessageId() !== null, + 'icon' => $status->getCustomIcon(), + 'clearAt' => $status->getClearAt(), + 'status' => $status->getStatus(), + 'statusIsUserDefined' => $status->getIsUserDefined(), + ]; + } +} diff --git a/apps/user_status/lib/Service/PredefinedStatusService.php b/apps/user_status/lib/Service/PredefinedStatusService.php new file mode 100644 index 00000000000..599d5b8b52f --- /dev/null +++ b/apps/user_status/lib/Service/PredefinedStatusService.php @@ -0,0 +1,223 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Service; + +use OCP\IL10N; +use OCP\UserStatus\IUserStatus; + +/** + * 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 BE_RIGHT_BACK = 'be-right-back'; + private const MEETING = 'meeting'; + private const COMMUTING = 'commuting'; + private const SICK_LEAVE = 'sick-leave'; + private const VACATIONING = 'vacationing'; + private const REMOTE_WORK = 'remote-work'; + /** + * @deprecated See \OCP\UserStatus\IUserStatus::MESSAGE_CALL + */ + public const CALL = 'call'; + public const OUT_OF_OFFICE = 'out-of-office'; + + /** + * DefaultStatusService constructor. + * + * @param IL10N $l10n + */ + public function __construct( + private IL10N $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::BE_RIGHT_BACK, + 'icon' => '⏳', + 'message' => $this->getTranslatedStatusForId(self::BE_RIGHT_BACK), + 'clearAt' => [ + 'type' => 'period', + 'time' => 900, + ], + ], + [ + '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, + ], + [ + 'id' => self::CALL, + 'icon' => '💬', + 'message' => $this->getTranslatedStatusForId(self::CALL), + 'clearAt' => null, + 'visible' => false, + ], + [ + 'id' => self::OUT_OF_OFFICE, + 'icon' => '🛑', + 'message' => $this->getTranslatedStatusForId(self::OUT_OF_OFFICE), + 'clearAt' => null, + 'visible' => false, + ], + ]; + } + + /** + * @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::OUT_OF_OFFICE: + return '🛑'; + + case self::REMOTE_WORK: + return '🏡'; + + case self::BE_RIGHT_BACK: + return '⏳'; + + case self::CALL: + 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::OUT_OF_OFFICE: + return $this->l10n->t('Out of office'); + + case self::REMOTE_WORK: + return $this->l10n->t('Working remotely'); + + case self::CALL: + return $this->l10n->t('In a call'); + + case self::BE_RIGHT_BACK: + return $this->l10n->t('Be right back'); + + 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::OUT_OF_OFFICE, + self::BE_RIGHT_BACK, + self::REMOTE_WORK, + IUserStatus::MESSAGE_CALL, + IUserStatus::MESSAGE_AVAILABILITY, + IUserStatus::MESSAGE_VACATION, + IUserStatus::MESSAGE_CALENDAR_BUSY, + IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE, + ], true); + } +} diff --git a/apps/user_status/lib/Service/StatusService.php b/apps/user_status/lib/Service/StatusService.php new file mode 100644 index 00000000000..188eb26d1d7 --- /dev/null +++ b/apps/user_status/lib/Service/StatusService.php @@ -0,0 +1,599 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +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; +use OCP\DB\Exception; +use OCP\IConfig; +use OCP\IEmojiHelper; +use OCP\IUserManager; +use OCP\UserStatus\IUserStatus; +use Psr\Log\LoggerInterface; +use function in_array; + +/** + * Class StatusService + * + * @package OCA\UserStatus\Service + */ +class StatusService { + private bool $shareeEnumeration; + private bool $shareeEnumerationInGroupOnly; + private bool $shareeEnumerationPhone; + + /** + * List of priorities ordered by their priority + */ + public const PRIORITY_ORDERED_STATUSES = [ + IUserStatus::ONLINE, + IUserStatus::AWAY, + IUserStatus::DND, + IUserStatus::BUSY, + IUserStatus::INVISIBLE, + IUserStatus::OFFLINE, + ]; + + /** + * List of statuses that persist the clear-up + * or UserLiveStatusEvents + */ + public const PERSISTENT_STATUSES = [ + IUserStatus::AWAY, + IUserStatus::BUSY, + IUserStatus::DND, + IUserStatus::INVISIBLE, + ]; + + /** @var int */ + public const INVALIDATE_STATUS_THRESHOLD = 15 /* minutes */ * 60 /* seconds */; + + /** @var int */ + public const MAXIMUM_MESSAGE_LENGTH = 80; + + public function __construct( + private UserStatusMapper $mapper, + private ITimeFactory $timeFactory, + private PredefinedStatusService $predefinedStatusService, + private IEmojiHelper $emojiHelper, + private IConfig $config, + private IUserManager $userManager, + private LoggerInterface $logger, + ) { + $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; + $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; + $this->shareeEnumerationPhone = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; + } + + /** + * @param int|null $limit + * @param int|null $offset + * @return UserStatus[] + */ + public function findAll(?int $limit = null, ?int $offset = null): array { + // Return empty array if user enumeration is disabled or limited to groups + // TODO: find a solution that scales to get only users from common groups if user enumeration is limited to + // groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936 + if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) { + return []; + } + + return array_map(function ($status) { + return $this->processStatus($status); + }, $this->mapper->findAll($limit, $offset)); + } + + /** + * @param int|null $limit + * @param int|null $offset + * @return array + */ + public function findAllRecentStatusChanges(?int $limit = null, ?int $offset = null): array { + // Return empty array if user enumeration is disabled or limited to groups + // TODO: find a solution that scales to get only users from common groups if user enumeration is limited to + // groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936 + if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) { + return []; + } + + return array_map(function ($status) { + return $this->processStatus($status); + }, $this->mapper->findAllRecent($limit, $offset)); + } + + /** + * @param string $userId + * @return UserStatus + * @throws DoesNotExistException + */ + public function findByUserId(string $userId): UserStatus { + return $this->processStatus($this->mapper->findByUserId($userId)); + } + + /** + * @param array $userIds + * @return UserStatus[] + */ + public function findByUserIds(array $userIds):array { + return array_map(function ($status) { + return $this->processStatus($status); + }, $this->mapper->findByUserIds($userIds)); + } + + /** + * @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, self::PRIORITY_ORDERED_STATUSES, 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); + $userStatus->setIsBackup(false); + + if ($userStatus->getId() === null) { + return $this->insertWithoutThrowingUniqueConstrain($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(IUserStatus::OFFLINE); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + $userStatus->setIsBackup(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); + $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp()); + + if ($userStatus->getId() === null) { + return $this->insertWithoutThrowingUniqueConstrain($userStatus); + } + + return $this->mapper->update($userStatus); + } + + /** + * @param string $userId + * @param string $status + * @param string $messageId + * @param bool $createBackup + * @param string|null $customMessage + * @throws InvalidStatusTypeException + * @throws InvalidMessageIdException + */ + public function setUserStatus(string $userId, + string $status, + string $messageId, + bool $createBackup, + ?string $customMessage = null): ?UserStatus { + // Check if status-type is valid + if (!in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { + throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported'); + } + + if (!$this->predefinedStatusService->isValidId($messageId)) { + throw new InvalidMessageIdException('Message-Id "' . $messageId . '" is not supported'); + } + + try { + $userStatus = $this->mapper->findByUserId($userId); + } catch (DoesNotExistException $e) { + // We don't need to do anything + $userStatus = new UserStatus(); + $userStatus->setUserId($userId); + } + + $updateStatus = false; + if ($messageId === IUserStatus::MESSAGE_OUT_OF_OFFICE) { + // OUT_OF_OFFICE trumps AVAILABILITY, CALL and CALENDAR status + $updateStatus = $userStatus->getMessageId() === IUserStatus::MESSAGE_AVAILABILITY || $userStatus->getMessageId() === IUserStatus::MESSAGE_CALL || $userStatus->getMessageId() === IUserStatus::MESSAGE_CALENDAR_BUSY; + } elseif ($messageId === IUserStatus::MESSAGE_AVAILABILITY) { + // AVAILABILITY trumps CALL and CALENDAR status + $updateStatus = $userStatus->getMessageId() === IUserStatus::MESSAGE_CALL || $userStatus->getMessageId() === IUserStatus::MESSAGE_CALENDAR_BUSY; + } elseif ($messageId === IUserStatus::MESSAGE_CALL) { + // CALL trumps CALENDAR status + $updateStatus = $userStatus->getMessageId() === IUserStatus::MESSAGE_CALENDAR_BUSY; + } + + if ($messageId === IUserStatus::MESSAGE_OUT_OF_OFFICE || $messageId === IUserStatus::MESSAGE_AVAILABILITY || $messageId === IUserStatus::MESSAGE_CALL || $messageId === IUserStatus::MESSAGE_CALENDAR_BUSY) { + if ($updateStatus) { + $this->logger->debug('User ' . $userId . ' is currently NOT available, overwriting status [status: ' . $userStatus->getStatus() . ', messageId: ' . json_encode($userStatus->getMessageId()) . ']', ['app' => 'dav']); + } else { + $this->logger->debug('User ' . $userId . ' is currently NOT available, but we are NOT overwriting status [status: ' . $userStatus->getStatus() . ', messageId: ' . json_encode($userStatus->getMessageId()) . ']', ['app' => 'dav']); + } + } + + // There should be a backup already or none is needed. So we take a shortcut. + if ($updateStatus) { + $userStatus->setStatus($status); + $userStatus->setStatusTimestamp($this->timeFactory->getTime()); + $userStatus->setIsUserDefined(true); + $userStatus->setIsBackup(false); + $userStatus->setMessageId($messageId); + $userStatus->setCustomIcon(null); + $userStatus->setCustomMessage($customMessage); + $userStatus->setClearAt(null); + $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp()); + return $this->mapper->update($userStatus); + } + + if ($createBackup) { + if ($this->backupCurrentStatus($userId) === false) { + return null; // Already a status set automatically => abort. + } + + // If we just created the backup + // we need to create a new status to insert + // Unfortunately there's no way to unset the DB ID on an Entity + $userStatus = new UserStatus(); + $userStatus->setUserId($userId); + } + + $userStatus->setStatus($status); + $userStatus->setStatusTimestamp($this->timeFactory->getTime()); + $userStatus->setIsUserDefined(true); + $userStatus->setIsBackup(false); + $userStatus->setMessageId($messageId); + $userStatus->setCustomIcon(null); + $userStatus->setCustomMessage($customMessage); + $userStatus->setClearAt(null); + if ($this->predefinedStatusService->getTranslatedStatusForId($messageId) !== null + || ($customMessage !== null && $customMessage !== '')) { + // Only track status message ID if there is one + $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp()); + } else { + $userStatus->setStatusMessageTimestamp(0); + } + + if ($userStatus->getId() !== null) { + return $this->mapper->update($userStatus); + } + return $this->insertWithoutThrowingUniqueConstrain($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(IUserStatus::OFFLINE); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + } + + // Check if statusIcon contains only one character + if ($statusIcon !== null && !$this->emojiHelper->isValidSingleEmoji($statusIcon)) { + throw new InvalidStatusIconException('Status-Icon is longer than one character'); + } + // Check for maximum length of custom message + if ($message !== null && \mb_strlen($message) > self::MAXIMUM_MESSAGE_LENGTH) { + throw new StatusMessageTooLongException('Message is longer than supported length of ' . self::MAXIMUM_MESSAGE_LENGTH . ' 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); + $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp()); + + if ($userStatus->getId() === null) { + return $this->insertWithoutThrowingUniqueConstrain($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(IUserStatus::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); + $userStatus->setStatusMessageTimestamp(0); + + $this->mapper->update($userStatus); + return true; + } + + /** + * @param string $userId + * @return bool + */ + public function removeUserStatus(string $userId): bool { + try { + $userStatus = $this->mapper->findByUserId($userId, false); + } catch (DoesNotExistException $ex) { + // if there is no status to remove, just return + return false; + } + + $this->mapper->delete($userStatus); + return true; + } + + public function removeBackupUserStatus(string $userId): bool { + try { + $userStatus = $this->mapper->findByUserId($userId, true); + } 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 + * @return UserStatus + */ + private function processStatus(UserStatus $status): UserStatus { + $clearAt = $status->getClearAt(); + + if ($status->getStatusTimestamp() < $this->timeFactory->getTime() - self::INVALIDATE_STATUS_THRESHOLD + && (!$status->getIsUserDefined() || $status->getStatus() === IUserStatus::ONLINE)) { + $this->cleanStatus($status); + } + if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) { + $this->cleanStatus($status); + $this->cleanStatusMessage($status); + } + if ($status->getMessageId() !== null) { + $this->addDefaultMessage($status); + } + + return $status; + } + + /** + * @param UserStatus $status + */ + private function cleanStatus(UserStatus $status): void { + if ($status->getStatus() === IUserStatus::OFFLINE && !$status->getIsUserDefined()) { + return; + } + + $status->setStatus(IUserStatus::OFFLINE); + $status->setStatusTimestamp($this->timeFactory->getTime()); + $status->setIsUserDefined(false); + + $this->mapper->update($status); + } + + /** + * @param UserStatus $status + */ + private function cleanStatusMessage(UserStatus $status): void { + $status->setMessageId(null); + $status->setCustomIcon(null); + $status->setCustomMessage(null); + $status->setClearAt(null); + $status->setStatusMessageTimestamp(0); + + $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) { + return; + } + // If there is a custom message, don't overwrite it + if (empty($status->getCustomMessage())) { + $status->setCustomMessage($predefinedMessage['message']); + } + if (empty($status->getCustomIcon())) { + $status->setCustomIcon($predefinedMessage['icon']); + } + } + + /** + * @return bool false if there is already a backup. In this case abort the procedure. + */ + public function backupCurrentStatus(string $userId): bool { + try { + $this->mapper->createBackupStatus($userId); + return true; + } catch (Exception $ex) { + if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + return false; + } + throw $ex; + } + } + + public function revertUserStatus(string $userId, string $messageId, bool $revertedManually = false): ?UserStatus { + try { + /** @var UserStatus $userStatus */ + $backupUserStatus = $this->mapper->findByUserId($userId, true); + } catch (DoesNotExistException $ex) { + // No user status to revert, do nothing + return null; + } + + $deleted = $this->mapper->deleteCurrentStatusToRestoreBackup($userId, $messageId); + if (!$deleted) { + // Another status is set automatically or no status, do nothing + return null; + } + + if ($revertedManually) { + if ($backupUserStatus->getStatus() === IUserStatus::OFFLINE) { + // When the user reverts the status manually they are online + $backupUserStatus->setStatus(IUserStatus::ONLINE); + } + $backupUserStatus->setStatusTimestamp($this->timeFactory->getTime()); + } + + $backupUserStatus->setIsBackup(false); + // Remove the underscore prefix added when creating the backup + $backupUserStatus->setUserId(substr($backupUserStatus->getUserId(), 1)); + $this->mapper->update($backupUserStatus); + + return $backupUserStatus; + } + + public function revertMultipleUserStatus(array $userIds, string $messageId): void { + // Get all user statuses and the backups + $findById = $userIds; + foreach ($userIds as $userId) { + $findById[] = '_' . $userId; + } + $userStatuses = $this->mapper->findByUserIds($findById); + + $backups = $restoreIds = $statuesToDelete = []; + foreach ($userStatuses as $userStatus) { + if (!$userStatus->getIsBackup() + && $userStatus->getMessageId() === $messageId) { + $statuesToDelete[$userStatus->getUserId()] = $userStatus->getId(); + } elseif ($userStatus->getIsBackup()) { + $backups[$userStatus->getUserId()] = $userStatus->getId(); + } + } + + // For users with both (normal and backup) delete the status when matching + foreach ($statuesToDelete as $userId => $statusId) { + $backupUserId = '_' . $userId; + if (isset($backups[$backupUserId])) { + $restoreIds[] = $backups[$backupUserId]; + } + } + + $this->mapper->deleteByIds(array_values($statuesToDelete)); + + // For users that matched restore the previous status + $this->mapper->restoreBackupStatuses($restoreIds); + } + + protected function insertWithoutThrowingUniqueConstrain(UserStatus $userStatus): UserStatus { + try { + return $this->mapper->insert($userStatus); + } catch (Exception $e) { + // Ignore if a parallel request already set the status + if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + throw $e; + } + } + return $userStatus; + } +} diff --git a/apps/user_status/openapi.json b/apps/user_status/openapi.json new file mode 100644 index 00000000000..e48d4970b96 --- /dev/null +++ b/apps/user_status/openapi.json @@ -0,0 +1,1195 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "user_status", + "version": "0.0.1", + "description": "User status", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "Capabilities": { + "type": "object", + "required": [ + "user_status" + ], + "properties": { + "user_status": { + "type": "object", + "required": [ + "enabled", + "restore", + "supports_emoji", + "supports_busy" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "restore": { + "type": "boolean" + }, + "supports_emoji": { + "type": "boolean" + }, + "supports_busy": { + "type": "boolean" + } + } + } + } + }, + "ClearAt": { + "type": "object", + "required": [ + "type", + "time" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "period", + "end-of" + ] + }, + "time": { + "anyOf": [ + { + "type": "integer", + "format": "int64" + }, + { + "$ref": "#/components/schemas/ClearAtTimeType" + } + ] + } + } + }, + "ClearAtTimeType": { + "type": "string", + "enum": [ + "day", + "week" + ] + }, + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + }, + "Predefined": { + "type": "object", + "required": [ + "id", + "icon", + "message", + "clearAt" + ], + "properties": { + "id": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "message": { + "type": "string" + }, + "clearAt": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ClearAt" + } + ] + } + } + }, + "Private": { + "allOf": [ + { + "$ref": "#/components/schemas/Public" + }, + { + "type": "object", + "required": [ + "messageId", + "messageIsPredefined", + "statusIsUserDefined" + ], + "properties": { + "messageId": { + "type": "string", + "nullable": true + }, + "messageIsPredefined": { + "type": "boolean" + }, + "statusIsUserDefined": { + "type": "boolean" + } + } + } + ] + }, + "Public": { + "type": "object", + "required": [ + "userId", + "message", + "icon", + "clearAt", + "status" + ], + "properties": { + "userId": { + "type": "string" + }, + "message": { + "type": "string", + "nullable": true + }, + "icon": { + "type": "string", + "nullable": true + }, + "clearAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/Type" + } + } + }, + "Type": { + "type": "string", + "enum": [ + "online", + "away", + "dnd", + "busy", + "offline", + "invisible" + ] + } + } + }, + "paths": { + "/ocs/v2.php/apps/user_status/api/v1/heartbeat": { + "put": { + "operationId": "heartbeat-heartbeat", + "summary": "Keep the status alive", + "tags": [ + "heartbeat" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "description": "Only online, away" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Status successfully updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Private" + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid status to update", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "204": { + "description": "User has no status to keep alive" + } + } + } + }, + "/ocs/v2.php/apps/user_status/api/v1/predefined_statuses": { + "get": { + "operationId": "predefined_status-find-all", + "summary": "Get all predefined messages", + "tags": [ + "predefined_status" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Predefined statuses returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Predefined" + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/user_status/api/v1/statuses": { + "get": { + "operationId": "statuses-find-all", + "summary": "Find statuses of users", + "tags": [ + "statuses" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Maximum number of statuses to find", + "schema": { + "type": "integer", + "format": "int64", + "nullable": true, + "default": null + } + }, + { + "name": "offset", + "in": "query", + "description": "Offset for finding statuses", + "schema": { + "type": "integer", + "format": "int64", + "nullable": true, + "default": null, + "minimum": 0 + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Statuses returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Public" + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/user_status/api/v1/statuses/{userId}": { + "get": { + "operationId": "statuses-find", + "summary": "Find the status of a user", + "tags": [ + "statuses" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "ID of the user", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Status returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Public" + } + } + } + } + } + } + } + }, + "404": { + "description": "The user was not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/user_status/api/v1/user_status": { + "get": { + "operationId": "user_status-get-status", + "summary": "Get the status of the current user", + "tags": [ + "user_status" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The status was found successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Private" + } + } + } + } + } + } + } + }, + "404": { + "description": "The user was not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/user_status/api/v1/user_status/status": { + "put": { + "operationId": "user_status-set-status", + "summary": "Update the status type of the current user", + "tags": [ + "user_status" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "statusType" + ], + "properties": { + "statusType": { + "type": "string", + "description": "The new status type" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The status was updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Private" + } + } + } + } + } + } + } + }, + "400": { + "description": "The status type is invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/user_status/api/v1/user_status/message/predefined": { + "put": { + "operationId": "user_status-set-predefined-message", + "summary": "Set the message to a predefined message for the current user", + "tags": [ + "user_status" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "messageId" + ], + "properties": { + "messageId": { + "type": "string", + "description": "ID of the predefined message" + }, + "clearAt": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "When the message should be cleared" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The message was updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Private" + } + } + } + } + } + } + } + }, + "400": { + "description": "The clearAt or message-id is invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/user_status/api/v1/user_status/message/custom": { + "put": { + "operationId": "user_status-set-custom-message", + "summary": "Set the message to a custom message for the current user", + "tags": [ + "user_status" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "statusIcon": { + "type": "string", + "nullable": true, + "description": "Icon of the status" + }, + "message": { + "type": "string", + "nullable": true, + "description": "Message of the status" + }, + "clearAt": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "When the message should be cleared" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The message was updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Private" + } + } + } + } + } + } + } + }, + "400": { + "description": "The clearAt or icon is invalid or the message is too long", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "No status for the current user", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/user_status/api/v1/user_status/message": { + "delete": { + "operationId": "user_status-clear-message", + "summary": "Clear the message of the current user", + "tags": [ + "user_status" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Message cleared successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/user_status/api/v1/user_status/revert/{messageId}": { + "delete": { + "operationId": "user_status-revert-status", + "summary": "Revert the status to the previous status", + "tags": [ + "user_status" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "messageId", + "in": "path", + "description": "ID of the message to delete", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Status reverted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "anyOf": [ + { + "$ref": "#/components/schemas/Private" + }, + { + "type": "array", + "maxItems": 0 + } + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "tags": [] +} diff --git a/apps/user_status/openapi.json.license b/apps/user_status/openapi.json.license new file mode 100644 index 00000000000..83559daa9dc --- /dev/null +++ b/apps/user_status/openapi.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later
\ No newline at end of file diff --git a/apps/user_status/src/UserStatus.vue b/apps/user_status/src/UserStatus.vue new file mode 100644 index 00000000000..07d81aad95c --- /dev/null +++ b/apps/user_status/src/UserStatus.vue @@ -0,0 +1,184 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <Fragment> + <NcListItem v-if="!inline" + class="user-status-menu-item" + compact + :name="visibleMessage" + @click.stop="openModal"> + <template #icon> + <NcUserStatusIcon class="user-status-icon" + :status="statusType" + aria-hidden="true" /> + </template> + </NcListItem> + + <div v-else> + <!-- Dashboard Status --> + <NcButton @click.stop="openModal"> + <template #icon> + <NcUserStatusIcon class="user-status-icon" + :status="statusType" + aria-hidden="true" /> + </template> + {{ visibleMessage }} + </NcButton> + </div> + <!-- Status management modal --> + <SetStatusModal v-if="isModalOpen" + :inline="inline" + @close="closeModal" /> + </Fragment> +</template> + +<script> +import { getCurrentUser } from '@nextcloud/auth' +import { subscribe, unsubscribe } from '@nextcloud/event-bus' +import { Fragment } from 'vue-frag' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcListItem from '@nextcloud/vue/components/NcListItem' +import NcUserStatusIcon from '@nextcloud/vue/components/NcUserStatusIcon' +import debounce from 'debounce' + +import { sendHeartbeat } from './services/heartbeatService.js' +import OnlineStatusMixin from './mixins/OnlineStatusMixin.js' + +export default { + name: 'UserStatus', + + components: { + Fragment, + NcButton, + NcListItem, + NcUserStatusIcon, + SetStatusModal: () => import(/* webpackChunkName: 'user-status-modal' */'./components/SetStatusModal.vue'), + }, + mixins: [OnlineStatusMixin], + + props: { + /** + * Whether the component should be rendered as a Dashboard Status or a User Menu Entries + * true = Dashboard Status + * false = User Menu Entries + */ + inline: { + type: Boolean, + default: false, + }, + }, + + data() { + return { + heartbeatInterval: null, + isAway: false, + isModalOpen: false, + mouseMoveListener: null, + setAwayTimeout: null, + } + }, + + /** + * 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() + } + subscribe('user_status:status.updated', this.handleUserStatusUpdated) + }, + + /** + * Some housekeeping before destroying the component + */ + beforeDestroy() { + window.removeEventListener('mouseMove', this.mouseMoveListener) + clearInterval(this.heartbeatInterval) + unsubscribe('user_status:status.updated', this.handleUserStatusUpdated) + }, + + methods: { + /** + * Opens the modal to set a custom status + */ + openModal() { + this.isModalOpen = true + }, + /** + * Closes the modal + */ + closeModal() { + this.isModalOpen = false + }, + + /** + * Sends the status heartbeat to the server + * + * @return {Promise<void>} + * @private + */ + async _backgroundHeartbeat() { + try { + const status = await sendHeartbeat(this.isAway) + if (status?.userId) { + this.$store.dispatch('setStatusFromHeartbeat', status) + } else { + await this.$store.dispatch('reFetchStatusFromServer') + } + } catch (error) { + console.debug('Failed sending heartbeat, got: ' + error.response?.status) + } + }, + handleUserStatusUpdated(state) { + if (getCurrentUser()?.uid === state.userId) { + this.$store.dispatch('setStatusFromObject', { + status: state.status, + icon: state.icon, + message: state.message, + }) + } + }, + }, +} +</script> + +<style lang="scss" scoped> +.user-status-icon { + width: 20px; + height: 20px; + margin: calc((var(--default-clickable-area) - 20px) / 2); // 20px icon size + opacity: 1 !important; + background-size: 20px; + vertical-align: middle !important; +} +</style> diff --git a/apps/user_status/src/components/ClearAtSelect.vue b/apps/user_status/src/components/ClearAtSelect.vue new file mode 100644 index 00000000000..91b816dc04a --- /dev/null +++ b/apps/user_status/src/components/ClearAtSelect.vue @@ -0,0 +1,85 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <div class="clear-at-select"> + <label class="clear-at-select__label" for="clearStatus"> + {{ $t('user_status', 'Clear status after') }} + </label> + <NcSelect input-id="clearStatus" + class="clear-at-select__select" + :options="options" + :value="option" + :clearable="false" + placement="top" + label-outside + @option:selected="select" /> + </div> +</template> + +<script> +import NcSelect from '@nextcloud/vue/components/NcSelect' +import { getAllClearAtOptions } from '../services/clearAtOptionsService.js' +import { clearAtFilter } from '../filters/clearAtFilter.js' + +export default { + name: 'ClearAtSelect', + components: { + NcSelect, + }, + props: { + clearAt: { + type: Object, + default: null, + }, + }, + data() { + return { + options: getAllClearAtOptions(), + } + }, + computed: { + /** + * Returns an object of the currently selected option + * + * @return {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('select-clear-at', option.clearAt) + }, + }, +} +</script> + +<style lang="scss" scoped> +.clear-at-select { + display: flex; + gap: calc(2 * var(--default-grid-baseline)); + align-items: center; + margin-block: 0 calc(2 * var(--default-grid-baseline)); + + &__select { + flex-grow: 1; + min-width: 215px; + } +} +</style> diff --git a/apps/user_status/src/components/CustomMessageInput.vue b/apps/user_status/src/components/CustomMessageInput.vue new file mode 100644 index 00000000000..fb129281430 --- /dev/null +++ b/apps/user_status/src/components/CustomMessageInput.vue @@ -0,0 +1,106 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div class="custom-input" role="group"> + <NcEmojiPicker container=".custom-input" @select="setIcon"> + <NcButton type="tertiary" + :aria-label="t('user_status', 'Emoji for your status message')"> + <template #icon> + {{ visibleIcon }} + </template> + </NcButton> + </NcEmojiPicker> + <div class="custom-input__container"> + <NcTextField ref="input" + maxlength="80" + :disabled="disabled" + :placeholder="t('user_status', 'What is your status?')" + :value="message" + type="text" + :label="t('user_status', 'What is your status?')" + @input="onChange" /> + </div> + </div> +</template> + +<script> +import NcButton from '@nextcloud/vue/components/NcButton' +import NcEmojiPicker from '@nextcloud/vue/components/NcEmojiPicker' +import NcTextField from '@nextcloud/vue/components/NcTextField' + +export default { + name: 'CustomMessageInput', + + components: { + NcTextField, + NcButton, + NcEmojiPicker, + }, + + props: { + icon: { + type: String, + default: '😀', + }, + message: { + type: String, + required: true, + default: () => '', + }, + disabled: { + type: Boolean, + default: false, + }, + }, + + emits: [ + 'change', + 'select-icon', + ], + + computed: { + /** + * Returns the user-set icon or a smiley in case no icon is set + * + * @return {string} + */ + visibleIcon() { + return this.icon || '😀' + }, + }, + + methods: { + focus() { + this.$refs.input.focus() + }, + + /** + * Notifies the parent component about a changed input + * + * @param {Event} event The Change Event + */ + onChange(event) { + this.$emit('change', event.target.value) + }, + + setIcon(icon) { + this.$emit('select-icon', icon) + }, + }, +} +</script> + +<style lang="scss" scoped> +.custom-input { + display: flex; + align-items: flex-end; + gap: var(--default-grid-baseline); + width: 100%; + + &__container { + width: 100%; + } +} +</style> diff --git a/apps/user_status/src/components/OnlineStatusSelect.vue b/apps/user_status/src/components/OnlineStatusSelect.vue new file mode 100644 index 00000000000..0abcc8d68e6 --- /dev/null +++ b/apps/user_status/src/components/OnlineStatusSelect.vue @@ -0,0 +1,110 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div class="user-status-online-select"> + <input :id="id" + :checked="checked" + class="hidden-visually user-status-online-select__input" + type="radio" + name="user-status-online" + @change="onChange"> + <label :for="id" class="user-status-online-select__label"> + <NcUserStatusIcon :status="type" + class="user-status-online-select__icon" + aria-hidden="true" /> + {{ label }} + <em class="user-status-online-select__subline">{{ subline }}</em> + </label> + </div> +</template> + +<script> +import NcUserStatusIcon from '@nextcloud/vue/components/NcUserStatusIcon' + +export default { + name: 'OnlineStatusSelect', + + components: { + NcUserStatusIcon, + }, + + props: { + checked: { + type: Boolean, + default: false, + }, + type: { + type: String, + required: true, + }, + label: { + type: String, + required: true, + }, + subline: { + type: String, + default: null, + }, + }, + + computed: { + id() { + return `user-status-online-status-${this.type}` + }, + }, + + methods: { + onChange() { + this.$emit('select', this.type) + }, + }, +} +</script> + +<style lang="scss" scoped> +.user-status-online-select { + &__label { + box-sizing: inherit; + display: grid; + grid-template-columns: var(--default-clickable-area) 1fr 2fr; + align-items: center; + gap: var(--default-grid-baseline); + min-height: var(--default-clickable-area); + padding: var(--default-grid-baseline); + border-radius: var(--border-radius-large); + background-color: var(--color-background-hover); + + &, & * { + cursor: pointer; + } + + &:hover { + background-color: var(--color-background-dark); + } + } + + &__icon { + flex-shrink: 0; + max-width: 34px; + max-height: 100%; + } + + &__input:checked + &__label { + outline: 2px solid var(--color-main-text); + background-color: var(--color-background-dark); + box-shadow: 0 0 0 4px var(--color-main-background); + } + + &__input:focus-visible + &__label { + outline: 2px solid var(--color-primary-element) !important; + background-color: var(--color-background-dark); + } + + &__subline { + display: block; + color: var(--color-text-lighter); + } +} +</style> diff --git a/apps/user_status/src/components/PredefinedStatus.vue b/apps/user_status/src/components/PredefinedStatus.vue new file mode 100644 index 00000000000..b12892d4add --- /dev/null +++ b/apps/user_status/src/components/PredefinedStatus.vue @@ -0,0 +1,128 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <li class="predefined-status"> + <input :id="id" + class="hidden-visually predefined-status__input" + type="radio" + name="predefined-status" + :checked="selected" + @change="select"> + <label class="predefined-status__label" :for="id"> + <span aria-hidden="true" class="predefined-status__label--icon"> + {{ icon }} + </span> + <span class="predefined-status__label--message"> + {{ message }} + </span> + <span class="predefined-status__label--clear-at"> + {{ clearAt | clearAtFilter }} + </span> + </label> + </li> +</template> + +<script> +import { clearAtFilter } from '../filters/clearAtFilter.js' + +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, + }, + selected: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + id() { + return `user-status-predefined-status-${this.messageId}` + }, + }, + methods: { + /** + * Emits an event when the user clicks the row + */ + select() { + this.$emit('select') + }, + }, +} +</script> + +<style lang="scss" scoped> +.predefined-status { + &__label { + display: flex; + flex-wrap: nowrap; + justify-content: flex-start; + flex-basis: 100%; + border-radius: var(--border-radius); + align-items: center; + min-height: var(--default-clickable-area); + padding-inline: var(--default-grid-baseline); + + &, & * { + cursor: pointer; + } + + &:hover { + background-color: var(--color-background-dark); + } + + &--icon { + flex-basis: var(--default-clickable-area); + text-align: center; + } + + &--message { + font-weight: bold; + padding: 0 6px; + } + + &--clear-at { + color: var(--color-text-maxcontrast); + + &::before { + content: ' – '; + } + } + } + + &__input:checked + &__label, + &__label:active { + outline: 2px solid var(--color-main-text); + box-shadow: 0 0 0 4px var(--color-main-background); + background-color: var(--color-background-dark); + border-radius: var(--border-radius-large); + } + + &__input:focus-visible + &__label { + outline: 2px solid var(--color-primary-element) !important; + background-color: var(--color-background-dark); + border-radius: var(--border-radius-large); + } +} +</style> diff --git a/apps/user_status/src/components/PredefinedStatusesList.vue b/apps/user_status/src/components/PredefinedStatusesList.vue new file mode 100644 index 00000000000..cdf359dce76 --- /dev/null +++ b/apps/user_status/src/components/PredefinedStatusesList.vue @@ -0,0 +1,84 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <ul v-if="statusesHaveLoaded" + class="predefined-statuses-list" + :aria-label="t('user_status', 'Predefined statuses')"> + <PredefinedStatus v-for="status in predefinedStatuses" + :key="status.id" + :message-id="status.id" + :icon="status.icon" + :message="status.message" + :clear-at="status.clearAt" + :selected="lastSelected === status.id" + @select="selectStatus(status)" /> + </ul> + <div v-else + class="predefined-statuses-list"> + <div class="icon icon-loading-small" /> + </div> +</template> + +<script> +import PredefinedStatus from './PredefinedStatus.vue' +import { mapGetters, mapState } from 'vuex' + +export default { + name: 'PredefinedStatusesList', + components: { + PredefinedStatus, + }, + data() { + return { + lastSelected: null, + } + }, + computed: { + ...mapState({ + predefinedStatuses: state => state.predefinedStatuses.predefinedStatuses, + messageId: state => state.userStatus.messageId, + }), + ...mapGetters(['statusesHaveLoaded']), + }, + + watch: { + messageId: { + immediate: true, + handler() { + this.lastSelected = this.messageId + }, + }, + }, + + /** + * Loads all predefined statuses from the server + * when this component is mounted + */ + created() { + this.$store.dispatch('loadAllPredefinedStatuses') + }, + methods: { + /** + * Emits an event when the user selects a status + * + * @param {object} status The selected status + */ + selectStatus(status) { + this.lastSelected = status.id + this.$emit('select-status', status) + }, + }, +} +</script> + +<style lang="scss" scoped> +.predefined-statuses-list { + display: flex; + flex-direction: column; + gap: var(--default-grid-baseline); + margin-block: 0 calc(2 * var(--default-grid-baseline)); +} +</style> diff --git a/apps/user_status/src/components/PreviousStatus.vue b/apps/user_status/src/components/PreviousStatus.vue new file mode 100644 index 00000000000..58d6ebd294b --- /dev/null +++ b/apps/user_status/src/components/PreviousStatus.vue @@ -0,0 +1,106 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div class="predefined-status backup-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"> + {{ $t('user_status', 'Previously set') }} + </span> + + <div class="backup-status__reset-button"> + <NcButton @click="select"> + {{ $t('user_status', 'Reset status') }} + </NcButton> + </div> + </div> +</template> + +<script> +import NcButton from '@nextcloud/vue/components/NcButton' + +export default { + name: 'PreviousStatus', + + components: { + NcButton, + }, + + props: { + icon: { + type: [String, null], + required: true, + }, + message: { + type: String, + required: true, + }, + }, + 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: var(--default-clickable-area); + padding-inline: var(--default-grid-baseline); + + &:hover, + &:focus { + background-color: var(--color-background-hover); + } + + &:active{ + background-color: var(--color-background-dark); + } + + &__icon { + flex-basis: var(--default-clickable-area); + text-align: center; + } + + &__message { + font-weight: bold; + padding: 0 6px; + } + + &__clear-at { + color: var(--color-text-maxcontrast); + + &::before { + content: ' – '; + } + } +} + +.backup-status { + &__reset-button { + justify-content: flex-end; + display: flex; + flex-grow: 1; + } +} +</style> diff --git a/apps/user_status/src/components/SetStatusModal.vue b/apps/user_status/src/components/SetStatusModal.vue new file mode 100644 index 00000000000..8624ed19e94 --- /dev/null +++ b/apps/user_status/src/components/SetStatusModal.vue @@ -0,0 +1,391 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <NcModal size="normal" + label-id="user_status-set-dialog" + dark + :set-return-focus="setReturnFocus" + @close="closeModal"> + <div class="set-status-modal"> + <!-- Status selector --> + <h2 id="user_status-set-dialog" class="set-status-modal__header"> + {{ $t('user_status', 'Online status') }} + </h2> + <div class="set-status-modal__online-status" + role="radiogroup" + :aria-label="$t('user_status', 'Online status')"> + <OnlineStatusSelect v-for="status in statuses" + :key="status.type" + v-bind="status" + :checked="status.type === statusType" + @select="changeStatus" /> + </div> + + <!-- Status message form --> + <form @submit.prevent="saveStatus" @reset="clearStatus"> + <h3 class="set-status-modal__header"> + {{ $t('user_status', 'Status message') }} + </h3> + <div class="set-status-modal__custom-input"> + <CustomMessageInput ref="customMessageInput" + :icon="icon" + :message="editedMessage" + @change="setMessage" + @select-icon="setIcon" /> + <NcButton v-if="messageId === 'vacationing'" + :href="absencePageUrl" + target="_blank" + type="secondary" + :aria-label="$t('user_status', 'Set absence period')"> + {{ $t('user_status', 'Set absence period and replacement') + ' ↗' }} + </NcButton> + </div> + <div v-if="hasBackupStatus" + class="set-status-modal__automation-hint"> + {{ $t('user_status', 'Your status was set automatically') }} + </div> + <PreviousStatus v-if="hasBackupStatus" + :icon="backupIcon" + :message="backupMessage" + @select="revertBackupFromServer" /> + <PredefinedStatusesList @select-status="selectPredefinedMessage" /> + <ClearAtSelect :clear-at="clearAt" + @select-clear-at="setClearAt" /> + <div class="status-buttons"> + <NcButton :wide="true" + type="tertiary" + native-type="reset" + :aria-label="$t('user_status', 'Clear status message')" + :disabled="isSavingStatus"> + {{ $t('user_status', 'Clear status message') }} + </NcButton> + <NcButton :wide="true" + type="primary" + native-type="submit" + :aria-label="$t('user_status', 'Set status message')" + :disabled="isSavingStatus"> + {{ $t('user_status', 'Set status message') }} + </NcButton> + </div> + </form> + </div> + </NcModal> +</template> + +<script> +import { showError } from '@nextcloud/dialogs' +import { generateUrl } from '@nextcloud/router' +import NcModal from '@nextcloud/vue/components/NcModal' +import NcButton from '@nextcloud/vue/components/NcButton' +import { getAllStatusOptions } from '../services/statusOptionsService.js' +import OnlineStatusMixin from '../mixins/OnlineStatusMixin.js' +import PredefinedStatusesList from './PredefinedStatusesList.vue' +import PreviousStatus from './PreviousStatus.vue' +import CustomMessageInput from './CustomMessageInput.vue' +import ClearAtSelect from './ClearAtSelect.vue' +import OnlineStatusSelect from './OnlineStatusSelect.vue' + +export default { + name: 'SetStatusModal', + + components: { + ClearAtSelect, + CustomMessageInput, + NcModal, + OnlineStatusSelect, + PredefinedStatusesList, + PreviousStatus, + NcButton, + }, + mixins: [OnlineStatusMixin], + + props: { + /** + * Whether the component should be rendered as a Dashboard Status or a User Menu Entries + * true = Dashboard Status + * false = User Menu Entries + */ + inline: { + type: Boolean, + default: false, + }, + }, + + data() { + return { + clearAt: null, + editedMessage: '', + predefinedMessageId: null, + isSavingStatus: false, + statuses: getAllStatusOptions(), + } + }, + + computed: { + messageId() { + return this.$store.state.userStatus.messageId + }, + icon() { + return this.$store.state.userStatus.icon + }, + message() { + return this.$store.state.userStatus.message || '' + }, + hasBackupStatus() { + return this.messageId && (this.backupIcon || this.backupMessage) + }, + backupIcon() { + return this.$store.state.userBackupStatus.icon || '' + }, + backupMessage() { + return this.$store.state.userBackupStatus.message || '' + }, + + absencePageUrl() { + return generateUrl('settings/user/availability#absence') + }, + + resetButtonText() { + if (this.backupIcon && this.backupMessage) { + return this.$t('user_status', 'Reset status to "{icon} {message}"', { + icon: this.backupIcon, + message: this.backupMessage, + }) + } else if (this.backupMessage) { + return this.$t('user_status', 'Reset status to "{message}"', { + message: this.backupMessage, + }) + } else if (this.backupIcon) { + return this.$t('user_status', 'Reset status to "{icon}"', { + icon: this.backupIcon, + }) + } + + return this.$t('user_status', 'Reset status') + }, + + setReturnFocus() { + if (this.inline) { + return undefined + } + return document.querySelector('[aria-controls="header-menu-user-menu"]') ?? undefined + }, + }, + + watch: { + message: { + immediate: true, + handler(newValue) { + this.editedMessage = newValue + }, + }, + }, + + /** + * Loads the current status when a user opens dialog + */ + mounted() { + this.$store.dispatch('fetchBackupFromServer') + + this.predefinedMessageId = this.$store.state.userStatus.messageId + 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.predefinedMessageId = null + this.$store.dispatch('setCustomMessage', { + message: this.message, + icon, + clearAt: this.clearAt, + }) + this.$nextTick(() => { + this.$refs.customMessageInput.focus() + }) + }, + /** + * Sets a new message + * + * @param {string} message The new message + */ + setMessage(message) { + this.predefinedMessageId = null + this.editedMessage = 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.predefinedMessageId = status.id + this.clearAt = status.clearAt + this.$store.dispatch('setPredefinedMessage', { + messageId: status.id, + clearAt: status.clearAt, + }) + }, + /** + * Saves the status and closes the + * + * @return {Promise<void>} + */ + async saveStatus() { + if (this.isSavingStatus) { + return + } + + try { + this.isSavingStatus = true + + if (this.predefinedMessageId === null) { + await this.$store.dispatch('setCustomMessage', { + message: this.editedMessage, + icon: this.icon, + clearAt: this.clearAt, + }) + } else { + this.$store.dispatch('setPredefinedMessage', { + messageId: this.predefinedMessageId, + 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() + }, + /** + * + * @return {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.predefinedMessageId = null + this.closeModal() + }, + /** + * + * @return {Promise<void>} + */ + async revertBackupFromServer() { + try { + this.isSavingStatus = true + + await this.$store.dispatch('revertBackupFromServer', { + messageId: this.messageId, + }) + } catch (err) { + showError(this.$t('user_status', 'There was an error reverting the status')) + console.debug(err) + this.isSavingStatus = false + return + } + + this.isSavingStatus = false + this.predefinedMessageId = this.$store.state.userStatus?.messageId + }, + }, +} +</script> + +<style lang="scss" scoped> + +.set-status-modal { + padding: 8px 20px 20px 20px; + + &, & * { + box-sizing: border-box; + } + + &__header { + font-size: 21px; + text-align: center; + height: fit-content; + min-height: var(--default-clickable-area); + line-height: var(--default-clickable-area); + overflow-wrap: break-word; + margin-block: 0 calc(2 * var(--default-grid-baseline)); + } + + &__online-status { + display: flex; + flex-direction: column; + gap: calc(2 * var(--default-grid-baseline)); + margin-block: 0 calc(2 * var(--default-grid-baseline)); + } + + &__custom-input { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--default-grid-baseline); + width: 100%; + padding-inline-start: var(--default-grid-baseline); + margin-block: 0 calc(2 * var(--default-grid-baseline)); + } + + &__automation-hint { + display: flex; + width: 100%; + margin-block: 0 calc(2 * var(--default-grid-baseline)); + color: var(--color-text-maxcontrast); + } + + .status-buttons { + display: flex; + padding: 3px; + padding-inline-start:0; + gap: 3px; + } +} + +@media only screen and (max-width: 500px) { + .set-status-modal__online-status { + grid-template-columns: none !important; + } +} + +</style> diff --git a/apps/user_status/src/filters/clearAtFilter.js b/apps/user_status/src/filters/clearAtFilter.js new file mode 100644 index 00000000000..5f62385a978 --- /dev/null +++ b/apps/user_status/src/filters/clearAtFilter.js @@ -0,0 +1,52 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { translate as t } from '@nextcloud/l10n' +import moment from '@nextcloud/moment' +import { dateFactory } from '../services/dateService.js' + +/** + * Formats a clearAt object to be human readable + * + * @param {object} clearAt The clearAt object + * @return {string|null} + */ +const clearAtFilter = (clearAt) => { + if (clearAt === null) { + return t('user_status', 'Don\'t clear') + } + + if (clearAt.type === 'end-of') { + switch (clearAt.time) { + case 'day': + return t('user_status', 'Today') + case 'week': + return t('user_status', 'This week') + + default: + return null + } + } + + if (clearAt.type === 'period') { + return moment.duration(clearAt.time * 1000).humanize() + } + + // This is not an officially supported type + // but only used internally to show the remaining time + // in the Set Status Modal + if (clearAt.type === '_time') { + const momentNow = moment(dateFactory()) + const momentClearAt = moment(clearAt.time, 'X') + + return moment.duration(momentNow.diff(momentClearAt)).humanize() + } + + return null +} + +export { + clearAtFilter, +} diff --git a/apps/user_status/src/menu.js b/apps/user_status/src/menu.js new file mode 100644 index 00000000000..34e5e6eabb1 --- /dev/null +++ b/apps/user_status/src/menu.js @@ -0,0 +1,52 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { getCSPNonce } from '@nextcloud/auth' +import { subscribe } from '@nextcloud/event-bus' +import Vue from 'vue' + +import UserStatus from './UserStatus.vue' +import store from './store/index.js' + +// eslint-disable-next-line camelcase +__webpack_nonce__ = getCSPNonce() + +Vue.prototype.t = t +Vue.prototype.$t = t + +const mountPoint = document.getElementById('user_status-menu-entry') + +const mountMenuEntry = () => { + const mountPoint = document.getElementById('user_status-menu-entry') + // eslint-disable-next-line no-new + new Vue({ + el: mountPoint, + render: h => h(UserStatus), + store, + }) +} + +if (mountPoint) { + mountMenuEntry() +} else { + subscribe('core:user-menu:mounted', mountMenuEntry) +} + +// Register dashboard status +document.addEventListener('DOMContentLoaded', function() { + if (!OCA.Dashboard) { + return + } + + OCA.Dashboard.registerStatus('status', (el) => { + const Dashboard = Vue.extend(UserStatus) + return new Dashboard({ + propsData: { + inline: true, + }, + store, + }).$mount(el) + }) +}) diff --git a/apps/user_status/src/mixins/OnlineStatusMixin.js b/apps/user_status/src/mixins/OnlineStatusMixin.js new file mode 100644 index 00000000000..5670eb4dc06 --- /dev/null +++ b/apps/user_status/src/mixins/OnlineStatusMixin.js @@ -0,0 +1,71 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { mapState } from 'vuex' +import { showError } from '@nextcloud/dialogs' + +export default { + computed: { + ...mapState({ + statusType: state => state.userStatus.status, + statusIsUserDefined: state => state.userStatus.statusIsUserDefined, + customIcon: state => state.userStatus.icon, + customMessage: state => state.userStatus.message, + }), + + /** + * The message displayed in the top right corner + * + * @return {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': + case 'busy': + 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') + }, + }, + + methods: { + /** + * 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) + } + }, + }, +} diff --git a/apps/user_status/src/services/clearAtOptionsService.js b/apps/user_status/src/services/clearAtOptionsService.js new file mode 100644 index 00000000000..af0059bfb7f --- /dev/null +++ b/apps/user_status/src/services/clearAtOptionsService.js @@ -0,0 +1,52 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { translate as t } from '@nextcloud/l10n' + +/** + * Returns an array + * + * @return {object[]} + */ +const getAllClearAtOptions = () => { + return [{ + label: t('user_status', 'Don\'t clear'), + clearAt: null, + }, { + label: t('user_status', '30 minutes'), + clearAt: { + type: 'period', + time: 1800, + }, + }, { + label: t('user_status', '1 hour'), + clearAt: { + type: 'period', + time: 3600, + }, + }, { + label: t('user_status', '4 hours'), + clearAt: { + type: 'period', + time: 14400, + }, + }, { + label: t('user_status', 'Today'), + clearAt: { + type: 'end-of', + time: 'day', + }, + }, { + label: t('user_status', 'This week'), + clearAt: { + type: 'end-of', + time: 'week', + }, + }] +} + +export { + getAllClearAtOptions, +} diff --git a/apps/user_status/src/services/clearAtService.js b/apps/user_status/src/services/clearAtService.js new file mode 100644 index 00000000000..f23d267ad02 --- /dev/null +++ b/apps/user_status/src/services/clearAtService.js @@ -0,0 +1,47 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { + dateFactory, +} from './dateService.js' +import moment from '@nextcloud/moment' + +/** + * Calculates the actual clearAt timestamp + * + * @param {object | null} clearAt The clear-at config + * @return {number | null} + */ +const getTimestampForClearAt = (clearAt) => { + if (clearAt === null) { + return null + } + + const date = dateFactory() + + if (clearAt.type === 'period') { + date.setSeconds(date.getSeconds() + clearAt.time) + return Math.floor(date.getTime() / 1000) + } + if (clearAt.type === 'end-of') { + switch (clearAt.time) { + case 'day': + case 'week': + return Number(moment(date).endOf(clearAt.time).format('X')) + } + } + // This is not an officially supported type + // but only used internally to show the remaining time + // in the Set Status Modal + if (clearAt.type === '_time') { + return clearAt.time + } + + return null +} + +export { + getTimestampForClearAt, +} diff --git a/apps/user_status/src/services/dateService.js b/apps/user_status/src/services/dateService.js new file mode 100644 index 00000000000..26a61d4a3e2 --- /dev/null +++ b/apps/user_status/src/services/dateService.js @@ -0,0 +1,12 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +const dateFactory = () => { + return new Date() +} + +export { + dateFactory, +} diff --git a/apps/user_status/src/services/heartbeatService.js b/apps/user_status/src/services/heartbeatService.js new file mode 100644 index 00000000000..fda1a1ffc9f --- /dev/null +++ b/apps/user_status/src/services/heartbeatService.js @@ -0,0 +1,25 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import HttpClient from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +/** + * Sends a heartbeat + * + * @param {boolean} isAway Whether or not the user is active + * @return {Promise<void>} + */ +const sendHeartbeat = async (isAway) => { + const url = generateOcsUrl('apps/user_status/api/v1/heartbeat?format=json') + const response = await HttpClient.put(url, { + status: isAway ? 'away' : 'online', + }) + return response.data.ocs.data +} + +export { + sendHeartbeat, +} diff --git a/apps/user_status/src/services/predefinedStatusService.js b/apps/user_status/src/services/predefinedStatusService.js new file mode 100644 index 00000000000..b423c6e0cc4 --- /dev/null +++ b/apps/user_status/src/services/predefinedStatusService.js @@ -0,0 +1,23 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import HttpClient from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +/** + * Fetches all predefined statuses from the server + * + * @return {Promise<void>} + */ +const fetchAllPredefinedStatuses = async () => { + const url = generateOcsUrl('apps/user_status/api/v1/predefined_statuses?format=json') + const response = await HttpClient.get(url) + + return response.data.ocs.data +} + +export { + fetchAllPredefinedStatuses, +} diff --git a/apps/user_status/src/services/statusOptionsService.js b/apps/user_status/src/services/statusOptionsService.js new file mode 100644 index 00000000000..6c23645e5be --- /dev/null +++ b/apps/user_status/src/services/statusOptionsService.js @@ -0,0 +1,36 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { translate as t } from '@nextcloud/l10n' + +/** + * Returns a list of all user-definable statuses + * + * @return {object[]} + */ +const getAllStatusOptions = () => { + return [{ + type: 'online', + label: t('user_status', 'Online'), + }, { + type: 'away', + label: t('user_status', 'Away'), + }, { + type: 'busy', + label: t('user_status', 'Busy'), + }, { + type: 'dnd', + label: t('user_status', 'Do not disturb'), + subline: t('user_status', 'Mute all notifications'), + }, { + type: 'invisible', + label: t('user_status', 'Invisible'), + subline: t('user_status', 'Appear offline'), + }] +} + +export { + getAllStatusOptions, +} diff --git a/apps/user_status/src/services/statusService.js b/apps/user_status/src/services/statusService.js new file mode 100644 index 00000000000..6504411c996 --- /dev/null +++ b/apps/user_status/src/services/statusService.js @@ -0,0 +1,110 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import HttpClient from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +/** + * Fetches the current user-status + * + * @return {Promise<object>} + */ +const fetchCurrentStatus = async () => { + const url = generateOcsUrl('apps/user_status/api/v1/user_status') + const response = await HttpClient.get(url) + + return response.data.ocs.data +} + +/** + * Fetches the current user-status + * + * @param {string} userId Id of the user to fetch the status + * @return {Promise<object>} + */ +const fetchBackupStatus = async (userId) => { + const url = generateOcsUrl('apps/user_status/api/v1/statuses/{userId}', { userId: '_' + userId }) + const response = await HttpClient.get(url) + + return response.data.ocs.data +} + +/** + * Sets the status + * + * @param {string} statusType The status (online / away / dnd / invisible) + * @return {Promise<void>} + */ +const setStatus = async (statusType) => { + const url = generateOcsUrl('apps/user_status/api/v1/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 + * @return {Promise<void>} + */ +const setPredefinedMessage = async (messageId, clearAt = null) => { + const url = generateOcsUrl('apps/user_status/api/v1/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 + * @return {Promise<void>} + */ +const setCustomMessage = async (message, statusIcon = null, clearAt = null) => { + const url = generateOcsUrl('apps/user_status/api/v1/user_status/message/custom?format=json') + await HttpClient.put(url, { + message, + statusIcon, + clearAt, + }) +} + +/** + * Clears the current status of the user + * + * @return {Promise<void>} + */ +const clearMessage = async () => { + const url = generateOcsUrl('apps/user_status/api/v1/user_status/message?format=json') + await HttpClient.delete(url) +} + +/** + * Revert the automated status + * + * @param {string} messageId ID of the message to revert + * @return {Promise<object>} + */ +const revertToBackupStatus = async (messageId) => { + const url = generateOcsUrl('apps/user_status/api/v1/user_status/revert/{messageId}', { messageId }) + const response = await HttpClient.delete(url) + + return response.data.ocs.data +} + +export { + fetchCurrentStatus, + fetchBackupStatus, + setStatus, + setCustomMessage, + setPredefinedMessage, + clearMessage, + revertToBackupStatus, +} diff --git a/apps/user_status/src/store/index.js b/apps/user_status/src/store/index.js new file mode 100644 index 00000000000..d9cfe674165 --- /dev/null +++ b/apps/user_status/src/store/index.js @@ -0,0 +1,21 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import Vue from 'vue' +import Vuex, { Store } from 'vuex' +import predefinedStatuses from './predefinedStatuses.js' +import userStatus from './userStatus.js' +import userBackupStatus from './userBackupStatus.js' + +Vue.use(Vuex) + +export default new Store({ + modules: { + predefinedStatuses, + userStatus, + userBackupStatus, + }, + strict: true, +}) diff --git a/apps/user_status/src/store/predefinedStatuses.js b/apps/user_status/src/store/predefinedStatuses.js new file mode 100644 index 00000000000..6d592ca627e --- /dev/null +++ b/apps/user_status/src/store/predefinedStatuses.js @@ -0,0 +1,53 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { fetchAllPredefinedStatuses } from '../services/predefinedStatusService.js' + +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 = [...state.predefinedStatuses, status] + }, +} + +const getters = { + statusesHaveLoaded(state) { + return state.predefinedStatuses.length > 0 + }, +} + +const actions = { + + /** + * Loads all predefined statuses from the server + * + * @param {object} vuex The Vuex components + * @param {Function} vuex.commit The Vuex commit function + * @param {object} vuex.state - + */ + async loadAllPredefinedStatuses({ state, commit }) { + if (state.predefinedStatuses.length > 0) { + return + } + + const statuses = await fetchAllPredefinedStatuses() + for (const status of statuses) { + commit('addPredefinedStatus', status) + } + }, + +} + +export default { state, mutations, getters, actions } diff --git a/apps/user_status/src/store/userBackupStatus.js b/apps/user_status/src/store/userBackupStatus.js new file mode 100644 index 00000000000..78e5318de9d --- /dev/null +++ b/apps/user_status/src/store/userBackupStatus.js @@ -0,0 +1,102 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { + fetchBackupStatus, + revertToBackupStatus, +} from '../services/statusService.js' +import { getCurrentUser } from '@nextcloud/auth' +import { emit } from '@nextcloud/event-bus' + +const state = { + // Status (online / away / dnd / invisible / offline) + status: null, + // Whether 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 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 = { + /** + * 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 + */ + loadBackupStatusFromServer(state, { status, statusIsUserDefined, message, icon, clearAt, messageIsPredefined, messageId }) { + state.status = status + state.message = message + state.icon = icon + + // Don't overwrite certain values if the refreshing comes in via short updates + // E.g. from talk participant list which only has the status, message and icon + if (typeof statusIsUserDefined !== 'undefined') { + state.statusIsUserDefined = statusIsUserDefined + } + if (typeof clearAt !== 'undefined') { + state.clearAt = clearAt + } + if (typeof messageIsPredefined !== 'undefined') { + state.messageIsPredefined = messageIsPredefined + } + if (typeof messageId !== 'undefined') { + state.messageId = messageId + } + }, +} + +const getters = {} + +const actions = { + /** + * Re-fetches the status from the server + * + * @param {object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @return {Promise<void>} + */ + async fetchBackupFromServer({ commit }) { + try { + const status = await fetchBackupStatus(getCurrentUser()?.uid) + commit('loadBackupStatusFromServer', status) + } catch (e) { + // Ignore missing user backup status + } + }, + + async revertBackupFromServer({ commit }, { messageId }) { + const status = await revertToBackupStatus(messageId) + if (status) { + commit('loadBackupStatusFromServer', {}) + commit('loadStatusFromServer', status) + emit('user_status:status.updated', { + status: status.status, + message: status.message, + icon: status.icon, + clearAt: status.clearAt, + userId: getCurrentUser()?.uid, + }) + } + }, +} + +export default { state, mutations, getters, actions } diff --git a/apps/user_status/src/store/userStatus.js b/apps/user_status/src/store/userStatus.js new file mode 100644 index 00000000000..9bc86ab5062 --- /dev/null +++ b/apps/user_status/src/store/userStatus.js @@ -0,0 +1,295 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { + fetchCurrentStatus, + setStatus, + setPredefinedMessage, + setCustomMessage, + clearMessage, +} from '../services/statusService.js' +import { loadState } from '@nextcloud/initial-state' +import { getCurrentUser } from '@nextcloud/auth' +import { getTimestampForClearAt } from '../services/clearAtService.js' +import { emit } from '@nextcloud/event-bus' + +const state = { + // Status (online / away / dnd / invisible / offline) + status: null, + // Whether 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 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.message = message + state.icon = icon + + // Don't overwrite certain values if the refreshing comes in via short updates + // E.g. from talk participant list which only has the status, message and icon + if (typeof statusIsUserDefined !== 'undefined') { + state.statusIsUserDefined = statusIsUserDefined + } + if (typeof clearAt !== 'undefined') { + state.clearAt = clearAt + } + if (typeof messageIsPredefined !== 'undefined') { + state.messageIsPredefined = messageIsPredefined + } + if (typeof messageId !== 'undefined') { + 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} vuex.state The Vuex state object + * @param {object} data The data destructuring object + * @param {string} data.statusType The new status type + * @return {Promise<void>} + */ + async setStatus({ commit, state }, { statusType }) { + await setStatus(statusType) + commit('setStatus', { statusType }) + emit('user_status:status.updated', { + status: state.status, + message: state.message, + icon: state.icon, + clearAt: state.clearAt, + userId: getCurrentUser()?.uid, + }) + }, + + /** + * Update status from 'user_status:status.updated' update. + * This doesn't trigger another 'user_status:status.updated' + * event. + * + * @param {object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @param {object} vuex.state The Vuex state object + * @param {string} status The new status + * @return {Promise<void>} + */ + async setStatusFromObject({ commit, state }, status) { + commit('loadStatusFromServer', status) + }, + + /** + * 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.state The Vuex state object + * @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 + * @return {Promise<void>} + */ + async setPredefinedMessage({ commit, rootState, state }, { 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 }) + emit('user_status:status.updated', { + status: state.status, + message: state.message, + icon: state.icon, + clearAt: state.clearAt, + userId: getCurrentUser()?.uid, + }) + }, + + /** + * Sets a custom message + * + * @param {object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @param {object} vuex.state The Vuex state object + * @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 + * @return {Promise<void>} + */ + async setCustomMessage({ commit, state }, { message, icon, clearAt }) { + const resolvedClearAt = getTimestampForClearAt(clearAt) + + await setCustomMessage(message, icon, resolvedClearAt) + commit('setCustomMessage', { message, icon, clearAt: resolvedClearAt }) + emit('user_status:status.updated', { + status: state.status, + message: state.message, + icon: state.icon, + clearAt: state.clearAt, + userId: getCurrentUser()?.uid, + }) + }, + + /** + * Clears the status + * + * @param {object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @param {object} vuex.state The Vuex state object + * @return {Promise<void>} + */ + async clearMessage({ commit, state }) { + await clearMessage() + commit('clearMessage') + emit('user_status:status.updated', { + status: state.status, + message: state.message, + icon: state.icon, + clearAt: state.clearAt, + userId: getCurrentUser()?.uid, + }) + }, + + /** + * Re-fetches the status from the server + * + * @param {object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @return {Promise<void>} + */ + async reFetchStatusFromServer({ commit }) { + const status = await fetchCurrentStatus() + commit('loadStatusFromServer', status) + }, + + /** + * Stores the status we got in the reply of the heartbeat + * + * @param {object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @param {object} status The data destructuring object + * @param {string} status.status The status type + * @param {boolean} status.statusIsUserDefined Whether or not this status is user-defined + * @param {string} status.message The message + * @param {string} status.icon The icon + * @param {number} status.clearAt When to automatically clear the status + * @param {boolean} status.messageIsPredefined Whether or not the message is predefined + * @param {string} status.messageId The id of the predefined message + * @return {Promise<void>} + */ + async setStatusFromHeartbeat({ commit }, status) { + commit('loadStatusFromServer', status) + }, + + /** + * Loads the server from the initial state + * + * @param {object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + */ + loadStatusFromInitialState({ commit }) { + const status = loadState('user_status', 'status') + commit('loadStatusFromServer', status) + }, +} + +export default { state, mutations, getters, actions } diff --git a/apps/user_status/tests/Integration/Service/StatusServiceIntegrationTest.php b/apps/user_status/tests/Integration/Service/StatusServiceIntegrationTest.php new file mode 100644 index 00000000000..8a21052b09f --- /dev/null +++ b/apps/user_status/tests/Integration/Service/StatusServiceIntegrationTest.php @@ -0,0 +1,196 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UserStatus\Tests\Integration\Service; + +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\IDBConnection; +use OCP\Server; +use OCP\UserStatus\IUserStatus; +use Test\TestCase; +use function sleep; +use function time; + +/** + * @group DB + */ +class StatusServiceIntegrationTest extends TestCase { + + private StatusService $service; + + protected function setUp(): void { + parent::setUp(); + + $this->service = Server::get(StatusService::class); + + $db = Server::get(IDBConnection::class); + $qb = $db->getQueryBuilder(); + $qb->delete('user_status')->executeStatement(); + } + + public function testNoStatusYet(): void { + $this->expectException(DoesNotExistException::class); + + $this->service->findByUserId('test123'); + } + + public function testCustomStatusMessageTimestamp(): void { + $before = time(); + $this->service->setCustomMessage( + 'test123', + '🍕', + 'Lunch', + null, + ); + $after = time(); + + $status = $this->service->findByUserId('test123'); + + self::assertSame('Lunch', $status->getCustomMessage()); + self::assertGreaterThanOrEqual($before, $status->getStatusMessageTimestamp()); + self::assertLessThanOrEqual($after, $status->getStatusMessageTimestamp()); + } + + public function testOnlineStatusKeepsMessageTimestamp(): void { + $this->service->setStatus( + 'test123', + IUserStatus::OFFLINE, + time() + 1000, + false, + ); + $this->service->setCustomMessage( + 'test123', + '🍕', + 'Lunch', + null, + ); + $timeAfterInsert = time(); + sleep(1); + $this->service->setStatus( + 'test123', + IUserStatus::ONLINE, + time() + 2000, + false, + ); + $status = $this->service->findByUserId('test123'); + + self::assertSame('Lunch', $status->getCustomMessage()); + self::assertLessThanOrEqual($timeAfterInsert, $status->getStatusMessageTimestamp()); + } + + public function testCreateRestoreBackupAutomatically(): void { + $this->service->setStatus( + 'test123', + IUserStatus::ONLINE, + null, + false, + ); + $this->service->setUserStatus( + 'test123', + IUserStatus::DND, + 'meeting', + true, + ); + + self::assertSame( + 'meeting', + $this->service->findByUserId('test123')->getMessageId(), + ); + self::assertSame( + IUserStatus::ONLINE, + $this->service->findByUserId('_test123')->getStatus(), + ); + + $revertedStatus = $this->service->revertUserStatus( + 'test123', + 'meeting', + ); + + self::assertNotNull($revertedStatus, 'Status should have been reverted'); + + try { + $this->service->findByUserId('_test123'); + $this->fail('Expected DoesNotExistException() to be thrown when finding backup status after reverting'); + } catch (DoesNotExistException) { + } + + self::assertSame( + IUserStatus::ONLINE, + $this->service->findByUserId('test123')->getStatus(), + ); + } + + public function testCallOverwritesMeetingStatus(): void { + $this->service->setStatus( + 'test123', + IUserStatus::ONLINE, + null, + false, + ); + $this->service->setUserStatus( + 'test123', + IUserStatus::BUSY, + IUserStatus::MESSAGE_CALENDAR_BUSY, + true, + ); + self::assertSame( + 'meeting', + $this->service->findByUserId('test123')->getMessageId(), + ); + + $this->service->setUserStatus( + 'test123', + IUserStatus::BUSY, + IUserStatus::MESSAGE_CALL, + true, + ); + self::assertSame( + IUserStatus::BUSY, + $this->service->findByUserId('test123')->getStatus(), + ); + + self::assertSame( + IUserStatus::MESSAGE_CALL, + $this->service->findByUserId('test123')->getMessageId(), + ); + } + + public function testOtherAutomationsDoNotOverwriteEachOther(): void { + $this->service->setStatus( + 'test123', + IUserStatus::ONLINE, + null, + false, + ); + $this->service->setUserStatus( + 'test123', + IUserStatus::DND, + IUserStatus::MESSAGE_AVAILABILITY, + true, + ); + self::assertSame( + 'availability', + $this->service->findByUserId('test123')->getMessageId(), + ); + + $nostatus = $this->service->setUserStatus( + 'test123', + IUserStatus::BUSY, + IUserStatus::MESSAGE_CALENDAR_BUSY, + true, + ); + + self::assertNull($nostatus); + self::assertSame( + IUserStatus::MESSAGE_AVAILABILITY, + $this->service->findByUserId('test123')->getMessageId(), + ); + } +} diff --git a/apps/user_status/tests/Unit/BackgroundJob/ClearOldStatusesBackgroundJobTest.php b/apps/user_status/tests/Unit/BackgroundJob/ClearOldStatusesBackgroundJobTest.php new file mode 100644 index 00000000000..66142082343 --- /dev/null +++ b/apps/user_status/tests/Unit/BackgroundJob/ClearOldStatusesBackgroundJobTest.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\BackgroundJob; + +use OCA\UserStatus\BackgroundJob\ClearOldStatusesBackgroundJob; +use OCA\UserStatus\Db\UserStatusMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class ClearOldStatusesBackgroundJobTest extends TestCase { + private ITimeFactory&MockObject $time; + private UserStatusMapper&MockObject $mapper; + private ClearOldStatusesBackgroundJob $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(): void { + $this->mapper->expects($this->once()) + ->method('clearOlderThanClearAt') + ->with(1337); + $this->mapper->expects($this->once()) + ->method('clearStatusesOlderThan') + ->with(437, 1337); + + $this->time->method('getTime') + ->willReturn(1337); + + self::invokePrivate($this->job, 'run', [[]]); + } +} diff --git a/apps/user_status/tests/Unit/CapabilitiesTest.php b/apps/user_status/tests/Unit/CapabilitiesTest.php new file mode 100644 index 00000000000..601fb207df4 --- /dev/null +++ b/apps/user_status/tests/Unit/CapabilitiesTest.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests; + +use OCA\UserStatus\Capabilities; +use OCP\IEmojiHelper; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class CapabilitiesTest extends TestCase { + private IEmojiHelper&MockObject $emojiHelper; + private Capabilities $capabilities; + + protected function setUp(): void { + parent::setUp(); + + $this->emojiHelper = $this->createMock(IEmojiHelper::class); + $this->capabilities = new Capabilities($this->emojiHelper); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('getCapabilitiesDataProvider')] + public function testGetCapabilities(bool $supportsEmojis): void { + $this->emojiHelper->expects($this->once()) + ->method('doesPlatformSupportEmoji') + ->willReturn($supportsEmojis); + + $this->assertEquals([ + 'user_status' => [ + 'enabled' => true, + 'restore' => true, + 'supports_emoji' => $supportsEmojis, + 'supports_busy' => true, + ] + ], $this->capabilities->getCapabilities()); + } + + public static function getCapabilitiesDataProvider(): array { + return [ + [true], + [false], + ]; + } +} diff --git a/apps/user_status/tests/Unit/Connector/UserStatusProviderTest.php b/apps/user_status/tests/Unit/Connector/UserStatusProviderTest.php new file mode 100644 index 00000000000..df6c55488d5 --- /dev/null +++ b/apps/user_status/tests/Unit/Connector/UserStatusProviderTest.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Connector; + +use OCA\UserStatus\Connector\UserStatusProvider; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Service\StatusService; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class UserStatusProviderTest extends TestCase { + private StatusService&MockObject $service; + private UserStatusProvider $provider; + + protected function setUp(): void { + parent::setUp(); + + $this->service = $this->createMock(StatusService::class); + $this->provider = new UserStatusProvider($this->service); + } + + public function testGetUserStatuses(): void { + $userStatus2 = new UserStatus(); + $userStatus2->setUserId('userId2'); + $userStatus2->setStatus('dnd'); + $userStatus2->setStatusTimestamp(5000); + $userStatus2->setIsUserDefined(true); + $userStatus2->setCustomIcon('💩'); + $userStatus2->setCustomMessage('Do not disturb'); + $userStatus2->setClearAt(50000); + + $userStatus3 = new UserStatus(); + $userStatus3->setUserId('userId3'); + $userStatus3->setStatus('away'); + $userStatus3->setStatusTimestamp(5000); + $userStatus3->setIsUserDefined(false); + $userStatus3->setCustomIcon('🏝'); + $userStatus3->setCustomMessage('On vacation'); + $userStatus3->setClearAt(60000); + + $this->service->expects($this->once()) + ->method('findByUserIds') + ->with(['userId1', 'userId2', 'userId3']) + ->willReturn([$userStatus2, $userStatus3]); + + $actual = $this->provider->getUserStatuses(['userId1', 'userId2', 'userId3']); + + $this->assertCount(2, $actual); + $status2 = $actual['userId2']; + $this->assertEquals('userId2', $status2->getUserId()); + $this->assertEquals('dnd', $status2->getStatus()); + $this->assertEquals('Do not disturb', $status2->getMessage()); + $this->assertEquals('💩', $status2->getIcon()); + $dateTime2 = $status2->getClearAt(); + $this->assertInstanceOf(\DateTimeImmutable::class, $dateTime2); + $this->assertEquals('50000', $dateTime2->format('U')); + + $status3 = $actual['userId3']; + $this->assertEquals('userId3', $status3->getUserId()); + $this->assertEquals('away', $status3->getStatus()); + $this->assertEquals('On vacation', $status3->getMessage()); + $this->assertEquals('🏝', $status3->getIcon()); + $dateTime3 = $status3->getClearAt(); + $this->assertInstanceOf(\DateTimeImmutable::class, $dateTime3); + $this->assertEquals('60000', $dateTime3->format('U')); + } +} diff --git a/apps/user_status/tests/Unit/Connector/UserStatusTest.php b/apps/user_status/tests/Unit/Connector/UserStatusTest.php new file mode 100644 index 00000000000..fee9b4e4b89 --- /dev/null +++ b/apps/user_status/tests/Unit/Connector/UserStatusTest.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Connector; + +use OCA\UserStatus\Connector\UserStatus; +use OCA\UserStatus\Db; +use Test\TestCase; + +class UserStatusTest extends TestCase { + public function testUserStatus(): void { + $status = new Db\UserStatus(); + $status->setUserId('user2'); + $status->setStatus('away'); + $status->setStatusTimestamp(5000); + $status->setIsUserDefined(false); + $status->setCustomIcon('🏝'); + $status->setCustomMessage('On vacation'); + $status->setClearAt(60000); + + $userStatus = new UserStatus($status); + $this->assertEquals('user2', $userStatus->getUserId()); + $this->assertEquals('away', $userStatus->getStatus()); + $this->assertEquals('On vacation', $userStatus->getMessage()); + $this->assertEquals('🏝', $userStatus->getIcon()); + + $dateTime = $userStatus->getClearAt(); + $this->assertInstanceOf(\DateTimeImmutable::class, $dateTime); + $this->assertEquals('60000', $dateTime->format('U')); + } + + public function testUserStatusInvisible(): void { + $status = new Db\UserStatus(); + $status->setUserId('user2'); + $status->setStatus('invisible'); + $status->setStatusTimestamp(5000); + $status->setIsUserDefined(false); + $status->setCustomIcon('🏝'); + $status->setCustomMessage('On vacation'); + $status->setClearAt(60000); + + $userStatus = new UserStatus($status); + $this->assertEquals('user2', $userStatus->getUserId()); + $this->assertEquals('offline', $userStatus->getStatus()); + $this->assertEquals('On vacation', $userStatus->getMessage()); + $this->assertEquals('🏝', $userStatus->getIcon()); + } +} diff --git a/apps/user_status/tests/Unit/Controller/PredefinedStatusControllerTest.php b/apps/user_status/tests/Unit/Controller/PredefinedStatusControllerTest.php new file mode 100644 index 00000000000..0f96f41a524 --- /dev/null +++ b/apps/user_status/tests/Unit/Controller/PredefinedStatusControllerTest.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Controller; + +use OCA\UserStatus\Controller\PredefinedStatusController; +use OCA\UserStatus\Service\PredefinedStatusService; +use OCP\IRequest; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class PredefinedStatusControllerTest extends TestCase { + private PredefinedStatusService&MockObject $service; + private PredefinedStatusController $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(): void { + $this->service->expects($this->once()) + ->method('getDefaultStatuses') + ->with() + ->willReturn([ + [ + 'id' => 'predefined-status-one', + ], + [ + 'id' => 'predefined-status-two', + ], + ]); + + $actual = $this->controller->findAll(); + $this->assertEquals([ + [ + 'id' => 'predefined-status-one', + ], + [ + 'id' => 'predefined-status-two', + ], + ], $actual->getData()); + } +} diff --git a/apps/user_status/tests/Unit/Controller/StatusesControllerTest.php b/apps/user_status/tests/Unit/Controller/StatusesControllerTest.php new file mode 100644 index 00000000000..76d337879c3 --- /dev/null +++ b/apps/user_status/tests/Unit/Controller/StatusesControllerTest.php @@ -0,0 +1,94 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +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 PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class StatusesControllerTest extends TestCase { + private StatusService&MockObject $service; + private StatusesController $controller; + + protected function setUp(): void { + parent::setUp(); + + $request = $this->createMock(IRequest::class); + $this->service = $this->createMock(StatusService::class); + + $this->controller = new StatusesController('user_status', $request, $this->service); + } + + public function testFindAll(): void { + $userStatus = $this->getUserStatus(); + + $this->service->expects($this->once()) + ->method('findAll') + ->with(20, 40) + ->willReturn([$userStatus]); + + $response = $this->controller->findAll(20, 40); + $this->assertEquals([[ + 'userId' => 'john.doe', + 'status' => 'offline', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + ]], $response->getData()); + } + + public function testFind(): void { + $userStatus = $this->getUserStatus(); + + $this->service->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($userStatus); + + $response = $this->controller->find('john.doe'); + $this->assertEquals([ + 'userId' => 'john.doe', + 'status' => 'offline', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + ], $response->getData()); + } + + public function testFindDoesNotExist(): void { + $this->service->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->expectException(OCSNotFoundException::class); + $this->expectExceptionMessage('No status for the requested userId'); + + $this->controller->find('john.doe'); + } + + private function getUserStatus(): UserStatus { + $userStatus = new UserStatus(); + $userStatus->setId(1337); + $userStatus->setUserId('john.doe'); + $userStatus->setStatus('invisible'); + $userStatus->setStatusTimestamp(5000); + $userStatus->setIsUserDefined(true); + $userStatus->setCustomIcon('🏝'); + $userStatus->setCustomMessage('On vacation'); + $userStatus->setClearAt(60000); + + return $userStatus; + } +} diff --git a/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php b/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php new file mode 100644 index 00000000000..e99290319ed --- /dev/null +++ b/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php @@ -0,0 +1,313 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Controller; + +use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService; +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\IRequest; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; +use Throwable; + +class UserStatusControllerTest extends TestCase { + private LoggerInterface&MockObject $logger; + private StatusService&MockObject $statusService; + private CalendarStatusService&MockObject $calendarStatusService; + private UserStatusController $controller; + + protected function setUp(): void { + parent::setUp(); + + $request = $this->createMock(IRequest::class); + $userId = 'john.doe'; + $this->logger = $this->createMock(LoggerInterface::class); + $this->statusService = $this->createMock(StatusService::class); + $this->calendarStatusService = $this->createMock(CalendarStatusService::class); + + $this->controller = new UserStatusController( + 'user_status', + $request, + $userId, + $this->logger, + $this->statusService, + $this->calendarStatusService, + ); + } + + public function testGetStatus(): void { + $userStatus = $this->getUserStatus(); + + $this->statusService->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->calendarStatusService->expects(self::once()) + ->method('processCalendarStatus') + ->with('john.doe'); + $this->statusService->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(); + } + + #[\PHPUnit\Framework\Attributes\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->statusService->expects($this->once()) + ->method('setStatus') + ->with('john.doe', $statusType, null, true) + ->willThrowException($exception); + } else { + $this->statusService->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 static 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"'], + ]; + } + + #[\PHPUnit\Framework\Attributes\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->statusService->expects($this->once()) + ->method('setPredefinedMessage') + ->with('john.doe', $messageId, $clearAt) + ->willThrowException($exception); + } else { + $this->statusService->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 static 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"'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('setCustomMessageDataProvider')] + public function testSetCustomMessage( + ?string $statusIcon, + string $message, + ?int $clearAt, + bool $expectSuccess, + bool $expectException, + ?Throwable $exception, + bool $expectLogger, + ?string $expectedLogMessage, + bool $expectSuccessAsReset = false, + ): void { + $userStatus = $this->getUserStatus(); + + if ($expectException) { + $this->statusService->expects($this->once()) + ->method('setCustomMessage') + ->with('john.doe', $statusIcon, $message, $clearAt) + ->willThrowException($exception); + } else { + if ($expectSuccessAsReset) { + $this->statusService->expects($this->never()) + ->method('setCustomMessage'); + $this->statusService->expects($this->once()) + ->method('clearMessage') + ->with('john.doe'); + $this->statusService->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($userStatus); + } else { + $this->statusService->expects($this->once()) + ->method('setCustomMessage') + ->with('john.doe', $statusIcon, $message, $clearAt) + ->willReturn($userStatus); + + $this->statusService->expects($this->never()) + ->method('clearMessage'); + } + } + + 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 static function setCustomMessageDataProvider(): array { + return [ + ['👨🏽💻', 'Busy developing the status feature', 500, true, false, null, false, null], + ['👨🏽💻', '', 500, true, false, null, false, null, false], + ['👨🏽💻', '', 0, true, false, null, false, null, false], + ['👨🏽💻', '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 testClearMessage(): void { + $this->statusService->expects($this->once()) + ->method('clearMessage') + ->with('john.doe'); + + $response = $this->controller->clearMessage(); + $this->assertEquals([], $response->getData()); + } + + private function getUserStatus(): UserStatus { + $userStatus = new UserStatus(); + $userStatus->setId(1337); + $userStatus->setUserId('john.doe'); + $userStatus->setStatus('invisible'); + $userStatus->setStatusTimestamp(5000); + $userStatus->setIsUserDefined(true); + $userStatus->setCustomIcon('🏝'); + $userStatus->setCustomMessage('On vacation'); + $userStatus->setClearAt(60000); + + return $userStatus; + } +} diff --git a/apps/user_status/tests/Unit/Dashboard/UserStatusWidgetTest.php b/apps/user_status/tests/Unit/Dashboard/UserStatusWidgetTest.php new file mode 100644 index 00000000000..8773b04c95f --- /dev/null +++ b/apps/user_status/tests/Unit/Dashboard/UserStatusWidgetTest.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Dashboard; + +use OCA\UserStatus\Dashboard\UserStatusWidget; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Services\IInitialState; +use OCP\IDateTimeFormatter; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class UserStatusWidgetTest extends TestCase { + private IL10N&MockObject $l10n; + private IDateTimeFormatter&MockObject $dateTimeFormatter; + private IURLGenerator&MockObject $urlGenerator; + private IInitialState&MockObject $initialState; + private IUserManager&MockObject $userManager; + private IUserSession&MockObject $userSession; + private StatusService&MockObject $service; + private UserStatusWidget $widget; + + protected function setUp(): void { + parent::setUp(); + + $this->l10n = $this->createMock(IL10N::class); + $this->dateTimeFormatter = $this->createMock(IDateTimeFormatter::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->initialState = $this->createMock(IInitialState::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->service = $this->createMock(StatusService::class); + + $this->widget = new UserStatusWidget($this->l10n, $this->dateTimeFormatter, $this->urlGenerator, $this->initialState, $this->userManager, $this->userSession, $this->service); + } + + public function testGetId(): void { + $this->assertEquals('user_status', $this->widget->getId()); + } + + public function testGetTitle(): void { + $this->l10n->expects($this->exactly(1)) + ->method('t') + ->willReturnArgument(0); + + $this->assertEquals('Recent statuses', $this->widget->getTitle()); + } + + public function testGetOrder(): void { + $this->assertEquals(5, $this->widget->getOrder()); + } + + public function testGetIconClass(): void { + $this->assertEquals('icon-user-status-dark', $this->widget->getIconClass()); + } + + public function testGetUrl(): void { + $this->assertNull($this->widget->getUrl()); + } +} diff --git a/apps/user_status/tests/Unit/Db/UserStatusMapperTest.php b/apps/user_status/tests/Unit/Db/UserStatusMapperTest.php new file mode 100644 index 00000000000..ea4480489c7 --- /dev/null +++ b/apps/user_status/tests/Unit/Db/UserStatusMapperTest.php @@ -0,0 +1,332 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Db; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Db\UserStatusMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\DB\Exception; +use Test\TestCase; + +class UserStatusMapperTest extends TestCase { + private UserStatusMapper $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 testFindAllRecent(): void { + $this->insertSampleStatuses(); + + $allResults = $this->mapper->findAllRecent(2, 0); + $this->assertCount(2, $allResults); + $this->assertEquals('user2', $allResults[0]->getUserId()); + $this->assertEquals('user1', $allResults[1]->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(6000, $user2Status->getStatusTimestamp()); + $this->assertEquals(false, $user2Status->getIsUserDefined()); + $this->assertEquals('🏝', $user2Status->getCustomIcon()); + $this->assertEquals('On vacation', $user2Status->getCustomMessage()); + $this->assertEquals(60000, $user2Status->getClearAt()); + } + + public function testFindByUserIds(): void { + $this->insertSampleStatuses(); + + $statuses = $this->mapper->findByUserIds(['admin', 'user2']); + $this->assertCount(2, $statuses); + + $adminStatus = $statuses[0]; + $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()); + + $user2Status = $statuses[1]; + $this->assertEquals('user2', $user2Status->getUserId()); + $this->assertEquals('away', $user2Status->getStatus()); + $this->assertEquals(6000, $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(Exception::class); + + $this->mapper->insert($userStatus2); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('clearStatusesOlderThanDataProvider')] + public function testClearStatusesOlderThan(string $status, bool $isUserDefined, int $timestamp, bool $expectsClean): void { + $oldStatus = UserStatus::fromParams([ + 'userId' => 'john.doe', + 'status' => $status, + 'isUserDefined' => $isUserDefined, + 'statusTimestamp' => $timestamp, + ]); + + $this->mapper->insert($oldStatus); + + $this->mapper->clearStatusesOlderThan(5000, 8000); + + $updatedStatus = $this->mapper->findAll()[0]; + + if ($expectsClean) { + $this->assertEquals('offline', $updatedStatus->getStatus()); + $this->assertFalse($updatedStatus->getIsUserDefined()); + $this->assertEquals(8000, $updatedStatus->getStatusTimestamp()); + } else { + $this->assertEquals($status, $updatedStatus->getStatus()); + $this->assertEquals($isUserDefined, $updatedStatus->getIsUserDefined()); + $this->assertEquals($timestamp, $updatedStatus->getStatusTimestamp()); + } + } + + public static function clearStatusesOlderThanDataProvider(): array { + return [ + ['offline', false, 6000, false], + ['online', true, 6000, false], + ['online', true, 4000, true], + ['online', false, 6000, false], + ['online', false, 4000, true], + ['away', true, 6000, false], + ['away', true, 4000, false], + ['away', false, 6000, false], + ['away', false, 4000, true], + ['dnd', true, 6000, false], + ['dnd', true, 4000, false], + ['invisible', true, 6000, false], + ['invisible', true, 4000, false], + ]; + } + + public function testClearOlderThanClearAt(): void { + $this->insertSampleStatuses(); + + $this->mapper->clearOlderThanClearAt(55000); + + $allStatuses = $this->mapper->findAll(); + $this->assertCount(2, $allStatuses); + + $this->expectException(DoesNotExistException::class); + $this->mapper->findByUserId('user1'); + } + + 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->setStatusMessageTimestamp(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(6000); + $userStatus3->setStatusMessageTimestamp(6000); + $userStatus3->setIsUserDefined(false); + $userStatus3->setCustomIcon('🏝'); + $userStatus3->setCustomMessage('On vacation'); + $userStatus3->setClearAt(60000); + + $this->mapper->insert($userStatus1); + $this->mapper->insert($userStatus2); + $this->mapper->insert($userStatus3); + } + + public static function dataCreateBackupStatus(): array { + return [ + [false, false, false], + [true, false, true], + [false, true, false], + [true, true, false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCreateBackupStatus')] + public function testCreateBackupStatus(bool $hasStatus, bool $hasBackup, bool $backupCreated): void { + if ($hasStatus) { + $userStatus1 = new UserStatus(); + $userStatus1->setUserId('user1'); + $userStatus1->setStatus('online'); + $userStatus1->setStatusTimestamp(5000); + $userStatus1->setIsUserDefined(true); + $userStatus1->setIsBackup(false); + $userStatus1->setCustomIcon('🚀'); + $userStatus1->setCustomMessage('Current'); + $userStatus1->setClearAt(50000); + $this->mapper->insert($userStatus1); + } + + if ($hasBackup) { + $userStatus1 = new UserStatus(); + $userStatus1->setUserId('_user1'); + $userStatus1->setStatus('online'); + $userStatus1->setStatusTimestamp(5000); + $userStatus1->setIsUserDefined(true); + $userStatus1->setIsBackup(true); + $userStatus1->setCustomIcon('🚀'); + $userStatus1->setCustomMessage('Backup'); + $userStatus1->setClearAt(50000); + $this->mapper->insert($userStatus1); + } + + if ($hasStatus && $hasBackup) { + $this->expectException(Exception::class); + } + + self::assertSame($backupCreated, $this->mapper->createBackupStatus('user1')); + + if ($backupCreated) { + $user1Status = $this->mapper->findByUserId('user1', true); + $this->assertEquals('_user1', $user1Status->getUserId()); + $this->assertEquals(true, $user1Status->getIsBackup()); + $this->assertEquals('Current', $user1Status->getCustomMessage()); + } elseif ($hasBackup) { + $user1Status = $this->mapper->findByUserId('user1', true); + $this->assertEquals('_user1', $user1Status->getUserId()); + $this->assertEquals(true, $user1Status->getIsBackup()); + $this->assertEquals('Backup', $user1Status->getCustomMessage()); + } + } + + public function testRestoreBackupStatuses(): void { + $userStatus1 = new UserStatus(); + $userStatus1->setUserId('_user1'); + $userStatus1->setStatus('online'); + $userStatus1->setStatusTimestamp(5000); + $userStatus1->setIsUserDefined(true); + $userStatus1->setIsBackup(true); + $userStatus1->setCustomIcon('🚀'); + $userStatus1->setCustomMessage('Releasing'); + $userStatus1->setClearAt(50000); + $userStatus1 = $this->mapper->insert($userStatus1); + + $userStatus2 = new UserStatus(); + $userStatus2->setUserId('_user2'); + $userStatus2->setStatus('away'); + $userStatus2->setStatusTimestamp(5000); + $userStatus2->setIsUserDefined(true); + $userStatus2->setIsBackup(true); + $userStatus2->setCustomIcon('💩'); + $userStatus2->setCustomMessage('Do not disturb'); + $userStatus2->setClearAt(50000); + $userStatus2 = $this->mapper->insert($userStatus2); + + $userStatus3 = new UserStatus(); + $userStatus3->setUserId('_user3'); + $userStatus3->setStatus('away'); + $userStatus3->setStatusTimestamp(5000); + $userStatus3->setIsUserDefined(true); + $userStatus3->setIsBackup(true); + $userStatus3->setCustomIcon('🏝️'); + $userStatus3->setCustomMessage('Vacationing'); + $userStatus3->setClearAt(50000); + $this->mapper->insert($userStatus3); + + $this->mapper->restoreBackupStatuses([$userStatus1->getId(), $userStatus2->getId()]); + + $user1Status = $this->mapper->findByUserId('user1', false); + $this->assertEquals('user1', $user1Status->getUserId()); + $this->assertEquals(false, $user1Status->getIsBackup()); + $this->assertEquals('Releasing', $user1Status->getCustomMessage()); + + $user2Status = $this->mapper->findByUserId('user2', false); + $this->assertEquals('user2', $user2Status->getUserId()); + $this->assertEquals(false, $user2Status->getIsBackup()); + $this->assertEquals('Do not disturb', $user2Status->getCustomMessage()); + + $user3Status = $this->mapper->findByUserId('user3', true); + $this->assertEquals('_user3', $user3Status->getUserId()); + $this->assertEquals(true, $user3Status->getIsBackup()); + $this->assertEquals('Vacationing', $user3Status->getCustomMessage()); + } +} diff --git a/apps/user_status/tests/Unit/Listener/UserDeletedListenerTest.php b/apps/user_status/tests/Unit/Listener/UserDeletedListenerTest.php new file mode 100644 index 00000000000..fbcea23338d --- /dev/null +++ b/apps/user_status/tests/Unit/Listener/UserDeletedListenerTest.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +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 PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class UserDeletedListenerTest extends TestCase { + private StatusService&MockObject $service; + private UserDeletedListener $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->service = $this->createMock(StatusService::class); + $this->listener = new UserDeletedListener($this->service); + } + + public function testHandleWithCorrectEvent(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('john.doe'); + + $this->service->expects($this->once()) + ->method('removeUserStatus') + ->with('john.doe'); + + $event = new UserDeletedEvent($user); + $this->listener->handle($event); + } + + public function testHandleWithWrongEvent(): void { + $this->service->expects($this->never()) + ->method('removeUserStatus'); + + $event = new GenericEvent(); + $this->listener->handle($event); + } +} diff --git a/apps/user_status/tests/Unit/Listener/UserLiveStatusListenerTest.php b/apps/user_status/tests/Unit/Listener/UserLiveStatusListenerTest.php new file mode 100644 index 00000000000..c03eed0089e --- /dev/null +++ b/apps/user_status/tests/Unit/Listener/UserLiveStatusListenerTest.php @@ -0,0 +1,149 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Listener; + +use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Db\UserStatusMapper; +use OCA\UserStatus\Listener\UserLiveStatusListener; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\GenericEvent; +use OCP\IUser; +use OCP\User\Events\UserLiveStatusEvent; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class UserLiveStatusListenerTest extends TestCase { + private UserStatusMapper&MockObject $mapper; + private StatusService&MockObject $statusService; + private ITimeFactory&MockObject $timeFactory; + private CalendarStatusService&MockObject $calendarStatusService; + + private LoggerInterface&MockObject $logger; + private UserLiveStatusListener $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->mapper = $this->createMock(UserStatusMapper::class); + $this->statusService = $this->createMock(StatusService::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->calendarStatusService = $this->createMock(CalendarStatusService::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->listener = new UserLiveStatusListener( + $this->mapper, + $this->statusService, + $this->timeFactory, + $this->calendarStatusService, + $this->logger, + ); + } + + #[\PHPUnit\Framework\Attributes\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->statusService->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willReturn($userStatus); + } else { + $this->statusService->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->atMost(1)) + ->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 static 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], + ['john.doe', 'away', 5000, true, 'online', 5000, true, false], + ['john.doe', 'online', 5000, true, 'away', 5000, true, false], + ]; + } + + public function testHandleWithWrongEvent(): void { + $this->mapper->expects($this->never()) + ->method('insertOrUpdate'); + + $event = new GenericEvent(); + $this->listener->handle($event); + } +} diff --git a/apps/user_status/tests/Unit/Service/PredefinedStatusServiceTest.php b/apps/user_status/tests/Unit/Service/PredefinedStatusServiceTest.php new file mode 100644 index 00000000000..78e4a18d9f1 --- /dev/null +++ b/apps/user_status/tests/Unit/Service/PredefinedStatusServiceTest.php @@ -0,0 +1,184 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Service; + +use OCA\UserStatus\Service\PredefinedStatusService; +use OCP\IL10N; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class PredefinedStatusServiceTest extends TestCase { + protected IL10N&MockObject $l10n; + protected PredefinedStatusService $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(8)) + ->method('t') + ->willReturnCallback(function ($text, $parameters = []) { + return vsprintf($text, $parameters); + }); + + $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' => 'be-right-back', + 'icon' => '⏳', + 'message' => 'Be right back', + 'clearAt' => [ + 'type' => 'period', + 'time' => 900, + ], + ], + [ + '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, + ], + [ + 'id' => 'call', + 'icon' => '💬', + 'message' => 'In a call', + 'clearAt' => null, + 'visible' => false, + ], + [ + 'id' => 'out-of-office', + 'icon' => '🛑', + 'message' => 'Out of office', + 'clearAt' => null, + 'visible' => false, + ], + ], $actual); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('getIconForIdDataProvider')] + public function testGetIconForId(string $id, ?string $expectedIcon): void { + $actual = $this->service->getIconForId($id); + $this->assertEquals($expectedIcon, $actual); + } + + public static function getIconForIdDataProvider(): array { + return [ + ['meeting', '📅'], + ['commuting', '🚌'], + ['sick-leave', '🤒'], + ['vacationing', '🌴'], + ['remote-work', '🏡'], + ['be-right-back', '⏳'], + ['call', '💬'], + ['unknown-id', null], + ]; + } + + #[\PHPUnit\Framework\Attributes\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); + } + + public static function getTranslatedStatusForIdDataProvider(): array { + return [ + ['meeting', 'In a meeting'], + ['commuting', 'Commuting'], + ['sick-leave', 'Out sick'], + ['vacationing', 'Vacationing'], + ['remote-work', 'Working remotely'], + ['be-right-back', 'Be right back'], + ['call', 'In a call'], + ['unknown-id', null], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('isValidIdDataProvider')] + public function testIsValidId(string $id, bool $expected): void { + $actual = $this->service->isValidId($id); + $this->assertEquals($expected, $actual); + } + + public static function isValidIdDataProvider(): array { + return [ + ['meeting', true], + ['commuting', true], + ['sick-leave', true], + ['vacationing', true], + ['remote-work', true], + ['be-right-back', true], + ['call', true], + ['unknown-id', false], + ]; + } + + public function testGetDefaultStatusById(): void { + $this->l10n->expects($this->exactly(8)) + ->method('t') + ->willReturnCallback(function ($text, $parameters = []) { + return vsprintf($text, $parameters); + }); + + $this->assertEquals([ + 'id' => 'call', + 'icon' => '💬', + 'message' => 'In a call', + 'clearAt' => null, + 'visible' => false, + ], $this->service->getDefaultStatusById('call')); + } + + public function testGetDefaultStatusByUnknownId(): void { + $this->assertNull($this->service->getDefaultStatusById('unknown')); + } +} diff --git a/apps/user_status/tests/Unit/Service/StatusServiceTest.php b/apps/user_status/tests/Unit/Service/StatusServiceTest.php new file mode 100644 index 00000000000..7dfa5b0d064 --- /dev/null +++ b/apps/user_status/tests/Unit/Service/StatusServiceTest.php @@ -0,0 +1,828 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Service; + +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use OC\DB\Exceptions\DbalException; +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\PredefinedStatusService; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\Exception; +use OCP\IConfig; +use OCP\IEmojiHelper; +use OCP\IUserManager; +use OCP\UserStatus\IUserStatus; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class StatusServiceTest extends TestCase { + private UserStatusMapper&MockObject $mapper; + private ITimeFactory&MockObject $timeFactory; + private PredefinedStatusService&MockObject $predefinedStatusService; + private IEmojiHelper&MockObject $emojiHelper; + private IConfig&MockObject $config; + private IUserManager&MockObject $userManager; + private LoggerInterface&MockObject $logger; + + private StatusService $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->emojiHelper = $this->createMock(IEmojiHelper::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->config = $this->createMock(IConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'] + ]); + + $this->service = new StatusService($this->mapper, + $this->timeFactory, + $this->predefinedStatusService, + $this->emojiHelper, + $this->config, + $this->userManager, + $this->logger, + ); + } + + 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 testFindAllRecentStatusChanges(): void { + $status1 = $this->createMock(UserStatus::class); + $status2 = $this->createMock(UserStatus::class); + + $this->mapper->expects($this->once()) + ->method('findAllRecent') + ->with(20, 50) + ->willReturn([$status1, $status2]); + + $this->assertEquals([ + $status1, + $status2, + ], $this->service->findAllRecentStatusChanges(20, 50)); + } + + public function testFindAllRecentStatusChangesNoEnumeration(): void { + $status1 = $this->createMock(UserStatus::class); + $status2 = $this->createMock(UserStatus::class); + + $this->mapper->method('findAllRecent') + ->with(20, 50) + ->willReturn([$status1, $status2]); + + // Rebuild $this->service with user enumeration turned off + $this->config = $this->createMock(IConfig::class); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'no'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'] + ]); + + $this->service = new StatusService($this->mapper, + $this->timeFactory, + $this->predefinedStatusService, + $this->emojiHelper, + $this->config, + $this->userManager, + $this->logger, + ); + + $this->assertEquals([], $this->service->findAllRecentStatusChanges(20, 50)); + + // Rebuild $this->service with user enumeration limited to common groups + $this->config = $this->createMock(IConfig::class); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'] + ]); + + $this->service = new StatusService($this->mapper, + $this->timeFactory, + $this->predefinedStatusService, + $this->emojiHelper, + $this->config, + $this->userManager, + $this->logger, + ); + + $this->assertEquals([], $this->service->findAllRecentStatusChanges(20, 50)); + } + + 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->setStatus('online'); + $status->setStatusTimestamp(1000); + $status->setIsUserDefined(true); + + $this->timeFactory->method('getTime') + ->willReturn(2600); + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + + $this->assertEquals($status, $this->service->findByUserId('john.doe')); + $this->assertEquals('offline', $status->getStatus()); + $this->assertEquals(2600, $status->getStatusTimestamp()); + $this->assertFalse($status->getIsUserDefined()); + } + + public function testFindAllClearMessage(): void { + $status = new UserStatus(); + $status->setClearAt(50); + $status->setMessageId('commuting'); + $status->setStatusTimestamp(60); + + $this->timeFactory->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()); + } + + #[\PHPUnit\Framework\Attributes\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 static 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'], + ]; + } + + #[\PHPUnit\Framework\Attributes\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 static 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'], + ]; + } + + #[\PHPUnit\Framework\Attributes\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->emojiHelper->method('isValidSingleEmoji') + ->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 static 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); + } + + public function testCleanStatusAutomaticOnline(): void { + $status = new UserStatus(); + $status->setStatus(IUserStatus::ONLINE); + $status->setStatusTimestamp(1337); + $status->setIsUserDefined(false); + + $this->mapper->expects(self::once()) + ->method('update') + ->with($status); + + parent::invokePrivate($this->service, 'cleanStatus', [$status]); + } + + public function testCleanStatusCustomOffline(): void { + $status = new UserStatus(); + $status->setStatus(IUserStatus::OFFLINE); + $status->setStatusTimestamp(1337); + $status->setIsUserDefined(true); + + $this->mapper->expects(self::once()) + ->method('update') + ->with($status); + + parent::invokePrivate($this->service, 'cleanStatus', [$status]); + } + + public function testCleanStatusCleanedAlready(): void { + $status = new UserStatus(); + $status->setStatus(IUserStatus::OFFLINE); + $status->setStatusTimestamp(1337); + $status->setIsUserDefined(false); + + // Don't update the status again and again when no value changed + $this->mapper->expects(self::never()) + ->method('update') + ->with($status); + + parent::invokePrivate($this->service, 'cleanStatus', [$status]); + } + + public function testBackupWorkingHasBackupAlready(): void { + $p = $this->createMock(UniqueConstraintViolationException::class); + $e = DbalException::wrap($p); + $this->mapper->expects($this->once()) + ->method('createBackupStatus') + ->with('john') + ->willThrowException($e); + + $this->assertFalse($this->service->backupCurrentStatus('john')); + } + + public function testBackupThrowsOther(): void { + $e = new Exception('', Exception::REASON_CONNECTION_LOST); + $this->mapper->expects($this->once()) + ->method('createBackupStatus') + ->with('john') + ->willThrowException($e); + + $this->expectException(Exception::class); + $this->service->backupCurrentStatus('john'); + } + + public function testBackup(): void { + $this->mapper->expects($this->once()) + ->method('createBackupStatus') + ->with('john') + ->willReturn(true); + + $this->assertTrue($this->service->backupCurrentStatus('john')); + } + + public function testRevertMultipleUserStatus(): void { + $john = new UserStatus(); + $john->setId(1); + $john->setStatus(IUserStatus::AWAY); + $john->setStatusTimestamp(1337); + $john->setIsUserDefined(false); + $john->setMessageId('call'); + $john->setUserId('john'); + $john->setIsBackup(false); + + $johnBackup = new UserStatus(); + $johnBackup->setId(2); + $johnBackup->setStatus(IUserStatus::ONLINE); + $johnBackup->setStatusTimestamp(1337); + $johnBackup->setIsUserDefined(true); + $johnBackup->setMessageId('hello'); + $johnBackup->setUserId('_john'); + $johnBackup->setIsBackup(true); + + $noBackup = new UserStatus(); + $noBackup->setId(3); + $noBackup->setStatus(IUserStatus::AWAY); + $noBackup->setStatusTimestamp(1337); + $noBackup->setIsUserDefined(false); + $noBackup->setMessageId('call'); + $noBackup->setUserId('nobackup'); + $noBackup->setIsBackup(false); + + $backupOnly = new UserStatus(); + $backupOnly->setId(4); + $backupOnly->setStatus(IUserStatus::ONLINE); + $backupOnly->setStatusTimestamp(1337); + $backupOnly->setIsUserDefined(true); + $backupOnly->setMessageId('hello'); + $backupOnly->setUserId('_backuponly'); + $backupOnly->setIsBackup(true); + + $noBackupDND = new UserStatus(); + $noBackupDND->setId(5); + $noBackupDND->setStatus(IUserStatus::DND); + $noBackupDND->setStatusTimestamp(1337); + $noBackupDND->setIsUserDefined(false); + $noBackupDND->setMessageId('call'); + $noBackupDND->setUserId('nobackupanddnd'); + $noBackupDND->setIsBackup(false); + + $this->mapper->expects($this->once()) + ->method('findByUserIds') + ->with(['john', 'nobackup', 'backuponly', 'nobackupanddnd', '_john', '_nobackup', '_backuponly', '_nobackupanddnd']) + ->willReturn([ + $john, + $johnBackup, + $noBackup, + $backupOnly, + $noBackupDND, + ]); + + $this->mapper->expects($this->once()) + ->method('deleteByIds') + ->with([1, 3, 5]); + + $this->mapper->expects($this->once()) + ->method('restoreBackupStatuses') + ->with([2]); + + $this->service->revertMultipleUserStatus(['john', 'nobackup', 'backuponly', 'nobackupanddnd'], 'call'); + } + + public static function dataSetUserStatus(): array { + return [ + [IUserStatus::MESSAGE_CALENDAR_BUSY, '', false], + + // Call > Meeting + [IUserStatus::MESSAGE_CALENDAR_BUSY, IUserStatus::MESSAGE_CALL, false], + [IUserStatus::MESSAGE_CALL, IUserStatus::MESSAGE_CALENDAR_BUSY, true], + + // Availability > Call&Meeting + [IUserStatus::MESSAGE_CALENDAR_BUSY, IUserStatus::MESSAGE_AVAILABILITY, false], + [IUserStatus::MESSAGE_CALL, IUserStatus::MESSAGE_AVAILABILITY, false], + [IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::MESSAGE_CALENDAR_BUSY, true], + [IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::MESSAGE_CALL, true], + + // Out-of-office > Availability&Call&Meeting + [IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::MESSAGE_OUT_OF_OFFICE, false], + [IUserStatus::MESSAGE_CALENDAR_BUSY, IUserStatus::MESSAGE_OUT_OF_OFFICE, false], + [IUserStatus::MESSAGE_CALL, IUserStatus::MESSAGE_OUT_OF_OFFICE, false], + [IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::MESSAGE_AVAILABILITY, true], + [IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::MESSAGE_CALENDAR_BUSY, true], + [IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::MESSAGE_CALL, true], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSetUserStatus')] + public function testSetUserStatus(string $messageId, string $oldMessageId, bool $expectedUpdateShortcut): void { + $previous = new UserStatus(); + $previous->setId(1); + $previous->setStatus(IUserStatus::AWAY); + $previous->setStatusTimestamp(1337); + $previous->setIsUserDefined(false); + $previous->setMessageId($oldMessageId); + $previous->setUserId('john'); + $previous->setIsBackup(false); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john') + ->willReturn($previous); + + $e = DbalException::wrap($this->createMock(UniqueConstraintViolationException::class)); + $this->mapper->expects($expectedUpdateShortcut ? $this->never() : $this->once()) + ->method('createBackupStatus') + ->willThrowException($e); + + $this->mapper->expects($this->any()) + ->method('update') + ->willReturnArgument(0); + + $this->predefinedStatusService->expects($this->once()) + ->method('isValidId') + ->with($messageId) + ->willReturn(true); + + $this->service->setUserStatus('john', IUserStatus::DND, $messageId, true); + } +} diff --git a/apps/user_status/tests/bootstrap.php b/apps/user_status/tests/bootstrap.php new file mode 100644 index 00000000000..c98daca1dfc --- /dev/null +++ b/apps/user_status/tests/bootstrap.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +use OCP\App\IAppManager; +use OCP\Server; + +if (!defined('PHPUNIT_RUN')) { + define('PHPUNIT_RUN', 1); +} + +require_once __DIR__ . '/../../../lib/base.php'; +require_once __DIR__ . '/../../../tests/autoload.php'; + +Server::get(IAppManager::class)->loadApp('user_status'); |