aboutsummaryrefslogtreecommitdiffstats
path: root/apps/user_status
diff options
context:
space:
mode:
Diffstat (limited to 'apps/user_status')
-rw-r--r--apps/user_status/.l10nignore4
-rw-r--r--apps/user_status/appinfo/info.xml35
-rw-r--r--apps/user_status/composer/autoload.php25
-rw-r--r--apps/user_status/composer/composer.json13
-rw-r--r--apps/user_status/composer/composer.lock18
-rw-r--r--apps/user_status/composer/composer/ClassLoader.php579
-rw-r--r--apps/user_status/composer/composer/InstalledVersions.php359
-rw-r--r--apps/user_status/composer/composer/LICENSE21
-rw-r--r--apps/user_status/composer/composer/autoload_classmap.php41
-rw-r--r--apps/user_status/composer/composer/autoload_namespaces.php9
-rw-r--r--apps/user_status/composer/composer/autoload_psr4.php10
-rw-r--r--apps/user_status/composer/composer/autoload_real.php37
-rw-r--r--apps/user_status/composer/composer/autoload_static.php67
-rw-r--r--apps/user_status/composer/composer/installed.json5
-rw-r--r--apps/user_status/composer/composer/installed.php23
-rw-r--r--apps/user_status/css/user-status-menu.css4
-rw-r--r--apps/user_status/css/user-status-menu.css.map1
-rw-r--r--apps/user_status/css/user-status-menu.css.map.license2
-rw-r--r--apps/user_status/css/user-status-menu.scss12
-rw-r--r--apps/user_status/img/app-dark.svg1
-rw-r--r--apps/user_status/img/app.svg1
-rw-r--r--apps/user_status/l10n/.gitkeep0
-rw-r--r--apps/user_status/l10n/af.js35
-rw-r--r--apps/user_status/l10n/af.json33
-rw-r--r--apps/user_status/l10n/ar.js50
-rw-r--r--apps/user_status/l10n/ar.json48
-rw-r--r--apps/user_status/l10n/ast.js48
-rw-r--r--apps/user_status/l10n/ast.json46
-rw-r--r--apps/user_status/l10n/bg.js48
-rw-r--r--apps/user_status/l10n/bg.json46
-rw-r--r--apps/user_status/l10n/ca.js50
-rw-r--r--apps/user_status/l10n/ca.json48
-rw-r--r--apps/user_status/l10n/cs.js50
-rw-r--r--apps/user_status/l10n/cs.json48
-rw-r--r--apps/user_status/l10n/da.js50
-rw-r--r--apps/user_status/l10n/da.json48
-rw-r--r--apps/user_status/l10n/de.js51
-rw-r--r--apps/user_status/l10n/de.json49
-rw-r--r--apps/user_status/l10n/de_DE.js51
-rw-r--r--apps/user_status/l10n/de_DE.json49
-rw-r--r--apps/user_status/l10n/el.js39
-rw-r--r--apps/user_status/l10n/el.json37
-rw-r--r--apps/user_status/l10n/en_GB.js51
-rw-r--r--apps/user_status/l10n/en_GB.json49
-rw-r--r--apps/user_status/l10n/es.js48
-rw-r--r--apps/user_status/l10n/es.json46
-rw-r--r--apps/user_status/l10n/es_EC.js48
-rw-r--r--apps/user_status/l10n/es_EC.json46
-rw-r--r--apps/user_status/l10n/es_MX.js48
-rw-r--r--apps/user_status/l10n/es_MX.json46
-rw-r--r--apps/user_status/l10n/et_EE.js51
-rw-r--r--apps/user_status/l10n/et_EE.json49
-rw-r--r--apps/user_status/l10n/eu.js50
-rw-r--r--apps/user_status/l10n/eu.json48
-rw-r--r--apps/user_status/l10n/fa.js48
-rw-r--r--apps/user_status/l10n/fa.json46
-rw-r--r--apps/user_status/l10n/fi.js50
-rw-r--r--apps/user_status/l10n/fi.json48
-rw-r--r--apps/user_status/l10n/fr.js50
-rw-r--r--apps/user_status/l10n/fr.json48
-rw-r--r--apps/user_status/l10n/ga.js51
-rw-r--r--apps/user_status/l10n/ga.json49
-rw-r--r--apps/user_status/l10n/gl.js50
-rw-r--r--apps/user_status/l10n/gl.json48
-rw-r--r--apps/user_status/l10n/he.js38
-rw-r--r--apps/user_status/l10n/he.json36
-rw-r--r--apps/user_status/l10n/hr.js39
-rw-r--r--apps/user_status/l10n/hr.json37
-rw-r--r--apps/user_status/l10n/hu.js50
-rw-r--r--apps/user_status/l10n/hu.json48
-rw-r--r--apps/user_status/l10n/is.js50
-rw-r--r--apps/user_status/l10n/is.json48
-rw-r--r--apps/user_status/l10n/it.js50
-rw-r--r--apps/user_status/l10n/it.json48
-rw-r--r--apps/user_status/l10n/ja.js51
-rw-r--r--apps/user_status/l10n/ja.json49
-rw-r--r--apps/user_status/l10n/ko.js39
-rw-r--r--apps/user_status/l10n/ko.json37
-rw-r--r--apps/user_status/l10n/lt_LT.js41
-rw-r--r--apps/user_status/l10n/lt_LT.json39
-rw-r--r--apps/user_status/l10n/mk.js39
-rw-r--r--apps/user_status/l10n/mk.json37
-rw-r--r--apps/user_status/l10n/nb.js50
-rw-r--r--apps/user_status/l10n/nb.json48
-rw-r--r--apps/user_status/l10n/nl.js51
-rw-r--r--apps/user_status/l10n/nl.json49
-rw-r--r--apps/user_status/l10n/oc.js39
-rw-r--r--apps/user_status/l10n/oc.json37
-rw-r--r--apps/user_status/l10n/pl.js51
-rw-r--r--apps/user_status/l10n/pl.json49
-rw-r--r--apps/user_status/l10n/pt_BR.js51
-rw-r--r--apps/user_status/l10n/pt_BR.json49
-rw-r--r--apps/user_status/l10n/pt_PT.js39
-rw-r--r--apps/user_status/l10n/pt_PT.json37
-rw-r--r--apps/user_status/l10n/ro.js48
-rw-r--r--apps/user_status/l10n/ro.json46
-rw-r--r--apps/user_status/l10n/ru.js51
-rw-r--r--apps/user_status/l10n/ru.json49
-rw-r--r--apps/user_status/l10n/sc.js38
-rw-r--r--apps/user_status/l10n/sc.json36
-rw-r--r--apps/user_status/l10n/sk.js50
-rw-r--r--apps/user_status/l10n/sk.json48
-rw-r--r--apps/user_status/l10n/sl.js48
-rw-r--r--apps/user_status/l10n/sl.json46
-rw-r--r--apps/user_status/l10n/sr.js50
-rw-r--r--apps/user_status/l10n/sr.json48
-rw-r--r--apps/user_status/l10n/sr@latin.js47
-rw-r--r--apps/user_status/l10n/sr@latin.json45
-rw-r--r--apps/user_status/l10n/sv.js50
-rw-r--r--apps/user_status/l10n/sv.json48
-rw-r--r--apps/user_status/l10n/sw.js51
-rw-r--r--apps/user_status/l10n/sw.json49
-rw-r--r--apps/user_status/l10n/th.js36
-rw-r--r--apps/user_status/l10n/th.json34
-rw-r--r--apps/user_status/l10n/tr.js50
-rw-r--r--apps/user_status/l10n/tr.json48
-rw-r--r--apps/user_status/l10n/ug.js50
-rw-r--r--apps/user_status/l10n/ug.json48
-rw-r--r--apps/user_status/l10n/uk.js51
-rw-r--r--apps/user_status/l10n/uk.json49
-rw-r--r--apps/user_status/l10n/uz.js50
-rw-r--r--apps/user_status/l10n/uz.json48
-rw-r--r--apps/user_status/l10n/vi.js47
-rw-r--r--apps/user_status/l10n/vi.json45
-rw-r--r--apps/user_status/l10n/zh_CN.js51
-rw-r--r--apps/user_status/l10n/zh_CN.json49
-rw-r--r--apps/user_status/l10n/zh_HK.js51
-rw-r--r--apps/user_status/l10n/zh_HK.json49
-rw-r--r--apps/user_status/l10n/zh_TW.js51
-rw-r--r--apps/user_status/l10n/zh_TW.json49
-rw-r--r--apps/user_status/lib/AppInfo/Application.php85
-rw-r--r--apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php47
-rw-r--r--apps/user_status/lib/Capabilities.php38
-rw-r--r--apps/user_status/lib/Connector/UserStatus.php86
-rw-r--r--apps/user_status/lib/Connector/UserStatusProvider.php52
-rw-r--r--apps/user_status/lib/ContactsMenu/StatusProvider.php53
-rw-r--r--apps/user_status/lib/Controller/HeartbeatController.php94
-rw-r--r--apps/user_status/lib/Controller/PredefinedStatusController.php57
-rw-r--r--apps/user_status/lib/Controller/StatusesController.php104
-rw-r--r--apps/user_status/lib/Controller/UserStatusController.php209
-rw-r--r--apps/user_status/lib/Dashboard/UserStatusWidget.php177
-rw-r--r--apps/user_status/lib/Db/UserStatus.php86
-rw-r--r--apps/user_status/lib/Db/UserStatusMapper.php197
-rw-r--r--apps/user_status/lib/Exception/InvalidClearAtException.php12
-rw-r--r--apps/user_status/lib/Exception/InvalidMessageIdException.php12
-rw-r--r--apps/user_status/lib/Exception/InvalidStatusIconException.php12
-rw-r--r--apps/user_status/lib/Exception/InvalidStatusTypeException.php12
-rw-r--r--apps/user_status/lib/Exception/StatusMessageTooLongException.php12
-rw-r--r--apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php75
-rw-r--r--apps/user_status/lib/Listener/OutOfOfficeStatusListener.php57
-rw-r--r--apps/user_status/lib/Listener/UserDeletedListener.php47
-rw-r--r--apps/user_status/lib/Listener/UserLiveStatusListener.php115
-rw-r--r--apps/user_status/lib/Migration/Version0001Date20200602134824.php80
-rw-r--r--apps/user_status/lib/Migration/Version0002Date20200902144824.php40
-rw-r--r--apps/user_status/lib/Migration/Version1000Date20201111130204.php44
-rw-r--r--apps/user_status/lib/Migration/Version1003Date20210809144824.php43
-rw-r--r--apps/user_status/lib/Migration/Version1008Date20230921144701.php54
-rw-r--r--apps/user_status/lib/ResponseDefinitions.php44
-rw-r--r--apps/user_status/lib/Service/JSDataService.php62
-rw-r--r--apps/user_status/lib/Service/PredefinedStatusService.php223
-rw-r--r--apps/user_status/lib/Service/StatusService.php599
-rw-r--r--apps/user_status/openapi.json1195
-rw-r--r--apps/user_status/openapi.json.license2
-rw-r--r--apps/user_status/src/UserStatus.vue184
-rw-r--r--apps/user_status/src/components/ClearAtSelect.vue85
-rw-r--r--apps/user_status/src/components/CustomMessageInput.vue106
-rw-r--r--apps/user_status/src/components/OnlineStatusSelect.vue110
-rw-r--r--apps/user_status/src/components/PredefinedStatus.vue128
-rw-r--r--apps/user_status/src/components/PredefinedStatusesList.vue84
-rw-r--r--apps/user_status/src/components/PreviousStatus.vue106
-rw-r--r--apps/user_status/src/components/SetStatusModal.vue391
-rw-r--r--apps/user_status/src/filters/clearAtFilter.js52
-rw-r--r--apps/user_status/src/menu.js52
-rw-r--r--apps/user_status/src/mixins/OnlineStatusMixin.js71
-rw-r--r--apps/user_status/src/services/clearAtOptionsService.js52
-rw-r--r--apps/user_status/src/services/clearAtService.js47
-rw-r--r--apps/user_status/src/services/dateService.js12
-rw-r--r--apps/user_status/src/services/heartbeatService.js25
-rw-r--r--apps/user_status/src/services/predefinedStatusService.js23
-rw-r--r--apps/user_status/src/services/statusOptionsService.js36
-rw-r--r--apps/user_status/src/services/statusService.js110
-rw-r--r--apps/user_status/src/store/index.js21
-rw-r--r--apps/user_status/src/store/predefinedStatuses.js53
-rw-r--r--apps/user_status/src/store/userBackupStatus.js102
-rw-r--r--apps/user_status/src/store/userStatus.js295
-rw-r--r--apps/user_status/tests/Integration/Service/StatusServiceIntegrationTest.php196
-rw-r--r--apps/user_status/tests/Unit/BackgroundJob/ClearOldStatusesBackgroundJobTest.php44
-rw-r--r--apps/user_status/tests/Unit/CapabilitiesTest.php49
-rw-r--r--apps/user_status/tests/Unit/Connector/UserStatusProviderTest.php73
-rw-r--r--apps/user_status/tests/Unit/Connector/UserStatusTest.php53
-rw-r--r--apps/user_status/tests/Unit/Controller/PredefinedStatusControllerTest.php53
-rw-r--r--apps/user_status/tests/Unit/Controller/StatusesControllerTest.php94
-rw-r--r--apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php313
-rw-r--r--apps/user_status/tests/Unit/Dashboard/UserStatusWidgetTest.php69
-rw-r--r--apps/user_status/tests/Unit/Db/UserStatusMapperTest.php332
-rw-r--r--apps/user_status/tests/Unit/Listener/UserDeletedListenerTest.php51
-rw-r--r--apps/user_status/tests/Unit/Listener/UserLiveStatusListenerTest.php149
-rw-r--r--apps/user_status/tests/Unit/Service/PredefinedStatusServiceTest.php184
-rw-r--r--apps/user_status/tests/Unit/Service/StatusServiceTest.php828
-rw-r--r--apps/user_status/tests/bootstrap.php20
200 files changed, 14967 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..d76dd2511f5
--- /dev/null
+++ b/apps/user_status/l10n/es.js
@@ -0,0 +1,48 @@
+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",
+ "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",
+ "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..d743ee8a033
--- /dev/null
+++ b/apps/user_status/l10n/es.json
@@ -0,0 +1,46 @@
+{ "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",
+ "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",
+ "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..18c3c88e824
--- /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änast",
+ "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..903466eef24
--- /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änast",
+ "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');