From fb8cc06ce25531d1e2af4afb41cab943112d18e2 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Sat, 29 Mar 2025 15:45:59 -0400 Subject: [PATCH 01/22] Remove remaining references of unused urlpath config key --- doc/Config.md | 14 ++------------ doc/Install.md | 1 - doc/Settings.md | 2 +- doc/de/Settings.md | 2 +- src/App/BaseURL.php | 3 +-- static/defaults.config.php | 11 +++++++++++ .../datasets/config/transformer/C.node.config.php | 1 - .../Console/AutomaticInstallationConsoleTest.php | 7 +------ 8 files changed, 17 insertions(+), 24 deletions(-) diff --git a/doc/Config.md b/doc/Config.md index 3e0459f9c8..afd4e65277 100644 --- a/doc/Config.md +++ b/doc/Config.md @@ -43,7 +43,7 @@ Some examples of common known configuration files: Addons can define their own default configuration values in `addon/[addon]/config/[addon].config.php` which is loaded when the addon is activated. If needed, an alternative `config` path can be used by using the `FRIENDICA_CONFIG_DIR` environment variable (full path required!). -This is useful in case of hardening the system by separating configuration from program binaries. +This is useful in case of hardening the system by separating configuration from program binaries. ### Static Configuration location @@ -160,16 +160,6 @@ $a->config['register_policy'] = REGISTER_CLOSED;
-$a->path = "value";
-
-
-'system' => [
-	'urlpath' => 'value',
-],
-
- - -
 $default_timezone = "value";
 
@@ -313,7 +303,7 @@ Enabling the admin panel for an account, and thus making the account holder admi
     'config' => [
         'admin_email' => 'someone@example.com',
     ]
-    
+
 
 Where you have to match the email address used for the account with the one you enter to the `config/local.config.php` file.
 If more than one account should be able to access the admin panel, separate the email addresses with a comma.
diff --git a/doc/Install.md b/doc/Install.md
index f613b2ffc3..3c162d519b 100644
--- a/doc/Install.md
+++ b/doc/Install.md
@@ -218,7 +218,6 @@ All options will be saved in the `config/local.config.php` and are overruling th
 -	`-U|--dbuser ` The username of the mysql/mariadb database login (env `MYSQL_USER` or `MYSQL_USERNAME`)
 -	`-P|--dbpass ` The password of the mysql/mariadb database login (env `MYSQL_PASSWORD`)
 -	`-d|--dbdata ` The name of the mysql/mariadb database (env `MYSQL_DATABASE`)
--	`-u|--urlpath ` The URL path of Friendica - f.e. '/friendica' (env `FRIENDICA_URL_PATH`)
 -	`-b|--phppath ` The path of the PHP binary (env `FRIENDICA_PHP_PATH`)
 -	`-A|--admin ` The admin email address of Friendica (env `FRIENDICA_ADMIN_MAIL`)
 -	`-T|--tz ` The timezone of Friendica (env `FRIENDICA_TZ`)
diff --git a/doc/Settings.md b/doc/Settings.md
index 00c8be2c2e..7ca0e22286 100644
--- a/doc/Settings.md
+++ b/doc/Settings.md
@@ -419,7 +419,7 @@ We strongly discourage you from doing so, as this will break federation to other
 Say you have a subdirectory for tests and put Friendica into a further subdirectory, the config would be:
 
 	'system' => [
-		'urlpath' => 'tests/friendica',
+		'url' => 'https://example.com/tests/friendica',
 	],
 
 ## Other exceptions
diff --git a/doc/de/Settings.md b/doc/de/Settings.md
index 34b349e885..cee965ccaf 100644
--- a/doc/de/Settings.md
+++ b/doc/de/Settings.md
@@ -410,7 +410,7 @@ Wir raten allerdings dringen davon ab, da es die Interoperabilität mit anderen
 Mal angenommen, du hast ein Unterverzeichnis tests und willst Friendica in ein weiteres Unterverzeichnis installieren, dann lautet die Konfiguration hierfĂĽr:
 
 	'system' => [
-		'urlpath' => 'tests/friendica',
+		'url' => 'https://example.com/tests/friendica',
 	],
 
 ## Weitere Ausnahmen
diff --git a/src/App/BaseURL.php b/src/App/BaseURL.php
index 83416866f0..0ebf72dbf8 100644
--- a/src/App/BaseURL.php
+++ b/src/App/BaseURL.php
@@ -17,8 +17,7 @@ use Psr\Http\Message\UriInterface;
 use Psr\Log\LoggerInterface;
 
 /**
- * A class which checks and contains the basic
- * environment for the BaseURL (url, urlpath, ssl_policy, hostname, scheme)
+ * A class which checks and contains the basic environment for the BaseURL (url)
  */
 class BaseURL extends Uri implements UriInterface
 {
diff --git a/static/defaults.config.php b/static/defaults.config.php
index d821ff1813..4a150ec7ab 100644
--- a/static/defaults.config.php
+++ b/static/defaults.config.php
@@ -126,6 +126,11 @@ return [
 		// Display "Emoji Only" posts in big.
 		'big_emojis' => true,
 
+		// basepath (String)
+		// Absolute file path to your Friendica install
+		// Examples: /var/www, /home/user/friendica...
+		'basepath' => '',
+
 		// bulk_delivery (Boolean)
 		// Delivers AP messages in a bulk (experimental)
 		'bulk_delivery' => false,
@@ -576,6 +581,12 @@ return [
 		// Transmit pending events upon accepted contact request for groups
 		'transmit_pending_events' => false,
 
+		// url (String)
+		// The absolute URL used to access your Friendica node. It should include the scheme, the domain name, and the
+		// sub-folder if any. Used by command-line processes to send correct links to your Friendica server.
+		// Example: https://example.com/friendica
+		'url' => '',
+
 		// username_min_length (Integer)
 		// The minimum character length a username can be.
 		// This length is checked once the username has been trimmed and multiple spaces have been collapsed into one.
diff --git a/tests/datasets/config/transformer/C.node.config.php b/tests/datasets/config/transformer/C.node.config.php
index 44969d1eb5..0b2964acd2 100644
--- a/tests/datasets/config/transformer/C.node.config.php
+++ b/tests/datasets/config/transformer/C.node.config.php
@@ -17,7 +17,6 @@ return [
 		'temppath' => '/tmp/friendica.local',
 		'theme' => 'frio',
 		'url' => 'https://friendica.local',
-		'urlpath' => '',
 		'build' => 1508,
 		'maintenance' => false,
 		'dbupdate' => 1,
diff --git a/tests/src/Console/AutomaticInstallationConsoleTest.php b/tests/src/Console/AutomaticInstallationConsoleTest.php
index 7fe56bb152..f3466c0203 100644
--- a/tests/src/Console/AutomaticInstallationConsoleTest.php
+++ b/tests/src/Console/AutomaticInstallationConsoleTest.php
@@ -129,7 +129,6 @@ class AutomaticInstallationConsoleTest extends ConsoleTestCase
 					],
 					'system' => [
 						'basepath'         => '',
-						'urlpath'          => '',
 						'url'              => 'http://friendica.local',
 						'ssl_policy'       => 0,
 						'default_timezone' => '',
@@ -152,7 +151,6 @@ class AutomaticInstallationConsoleTest extends ConsoleTestCase
 						'admin_email' => 'admin@philipp.info',
 					],
 					'system' => [
-						'urlpath'          => 'test/it',
 						'url'              => 'http://friendica.local/test/it',
 						'basepath'         => '',
 						'ssl_policy'       => '2',
@@ -176,7 +174,6 @@ class AutomaticInstallationConsoleTest extends ConsoleTestCase
 						'admin_email' => 'admin@philipp.info',
 					],
 					'system' => [
-						'urlpath'          => 'test/it',
 						'url'              => 'https://friendica.local/test/it',
 						'basepath'         => '',
 						'ssl_policy'       => '1',
@@ -352,7 +349,6 @@ FIN;
 		self::assertConfigEntry('system', 'default_timezone', $assertion, ($default) ? Installer::DEFAULT_TZ : null);
 		self::assertConfigEntry('system', 'language', $assertion, ($default) ? Installer::DEFAULT_LANG : null);
 		self::assertConfigEntry('system', 'url', $assertion);
-		self::assertConfigEntry('system', 'urlpath', $assertion);
 		self::assertConfigEntry('system', 'ssl_policy', $assertion, ($default) ? App\BaseURL::DEFAULT_SSL_SCHEME : null);
 		self::assertConfigEntry('system', 'basepath', ($realBasepath) ? $this->root->url() : $assertion);
 	}
@@ -446,7 +442,6 @@ return [
 	],
 	'system' => [
 		'basepath' => '{$conf('system', 'basepath')}',
-		'urlpath' => '{$conf('system', 'urlpath')}',
 		'url' => '{$conf('system', 'url')}',
 		'ssl_policy' => '{$conf('system', 'ssl_policy')}',
 		'default_timezone' => '{$conf('system', 'default_timezone')}',
@@ -604,7 +599,7 @@ CONF;
 		self::assertStuckDB($txt);
 		self::assertTrue($this->root->hasChild('config' . DIRECTORY_SEPARATOR . 'local.config.php'));
 
-		self::assertConfig(['config' => ['hostname' => 'friendica.local'], 'system' => ['url' => 'http://friendica.local', 'ssl_policy' => 0, 'urlpath' => '']], false, true, false, true);
+		self::assertConfig(['config' => ['hostname' => 'friendica.local'], 'system' => ['url' => 'http://friendica.local', 'ssl_policy' => 0]], false, true, false, true);
 	}
 
 	public function testGetHelp()

From cd73f6a27b690e7fce61c598cf11a97ec0671503 Mon Sep 17 00:00:00 2001
From: Michael 
Date: Thu, 17 Apr 2025 20:13:03 +0000
Subject: [PATCH 02/22] Bluesky: Update actors regularly

---
 src/Protocol/ATProtocol/Processor.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Protocol/ATProtocol/Processor.php b/src/Protocol/ATProtocol/Processor.php
index d7a26f78cf..0d7505f6a8 100755
--- a/src/Protocol/ATProtocol/Processor.php
+++ b/src/Protocol/ATProtocol/Processor.php
@@ -327,7 +327,7 @@ class Processor
 
 	private function getHeaderFromJetstream(stdClass $data, int $uid, int $protocol = Conversation::PARCEL_JETSTREAM): array
 	{
-		$contact = $this->actor->getContactByDID($data->did, $uid, 0);
+		$contact = $this->actor->getContactByDID($data->did, $uid, 0, true);
 		if (empty($contact)) {
 			$this->logger->info('Contact not found for user', ['did' => $data->did, 'uid' => $uid]);
 			return [];
@@ -392,7 +392,7 @@ class Processor
 		if (empty($post->author) || empty($post->cid) || empty($parts->rkey)) {
 			return [];
 		}
-		$contact = $this->actor->getContactByDID($post->author->did, $uid, 0);
+		$contact = $this->actor->getContactByDID($post->author->did, $uid, 0, true);
 		if (empty($contact)) {
 			$this->logger->info('Contact not found for user', ['did' => $post->author->did, 'uid' => $uid]);
 			return [];

From 85e5dfc8cfb924d9f795a311f2e00f7ee0930062 Mon Sep 17 00:00:00 2001
From: Michael 
Date: Fri, 18 Apr 2025 03:45:04 +0000
Subject: [PATCH 03/22] Issue 14890: Translate page titles

---
 src/App/Page.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/App/Page.php b/src/App/Page.php
index a9fc9674a0..623f464086 100644
--- a/src/App/Page.php
+++ b/src/App/Page.php
@@ -198,7 +198,7 @@ class Page implements ArrayAccess
 	) {
 		// Default title: current module called
 		if (empty($this->page['title']) && $args->getModuleName()) {
-			$this->page['title'] = ucfirst($args->getModuleName());
+			$this->page['title'] = $l10n->t(ucfirst($args->getModuleName()));
 		}
 
 		// Prepend the sitename to the page title

From 01296e98f948c4cdda36f4bc92b9ede3189987c1 Mon Sep 17 00:00:00 2001
From: Philipp 
Date: Mon, 21 Apr 2025 19:37:37 +0200
Subject: [PATCH 04/22] Add getStats() method for MemoryCaches

---
 .../Cache/Capability/ICanCacheInMemory.php    |  7 ++++++
 src/Core/Cache/Type/APCuCache.php             | 15 ++++++++++++
 src/Core/Cache/Type/ArrayCache.php            |  6 +++++
 src/Core/Cache/Type/MemcacheCache.php         | 17 ++++++++++++++
 src/Core/Cache/Type/MemcachedCache.php        | 23 +++++++++++++++++++
 .../Cache/Type/ProfilerCacheDecorator.php     | 10 ++++++++
 src/Core/Cache/Type/RedisCache.php            | 17 ++++++++++++++
 src/Core/Lock/Type/CacheLock.php              | 10 ++++++++
 tests/src/Core/Cache/APCuCacheTest.php        | 14 +++++++++++
 tests/src/Core/Cache/MemcacheCacheTest.php    | 17 ++++++++++++++
 tests/src/Core/Cache/MemcachedCacheTest.php   | 17 ++++++++++++++
 tests/src/Core/Cache/RedisCacheTest.php       | 17 ++++++++++++++
 12 files changed, 170 insertions(+)

diff --git a/src/Core/Cache/Capability/ICanCacheInMemory.php b/src/Core/Cache/Capability/ICanCacheInMemory.php
index 82492d368a..550273f5f0 100644
--- a/src/Core/Cache/Capability/ICanCacheInMemory.php
+++ b/src/Core/Cache/Capability/ICanCacheInMemory.php
@@ -53,4 +53,11 @@ interface ICanCacheInMemory extends ICanCache
 	 * @throws CachePersistenceException In case the underlying cache driver has errors during persistence
 	 */
 	public function compareDelete(string $key, $value): bool;
+
+	/**
+	 * Returns some basic statistics of the used Cache instance
+	 *
+	 * @return array Returns an associative array of statistics
+	 */
+	public function getStats(): array;
 }
diff --git a/src/Core/Cache/Type/APCuCache.php b/src/Core/Cache/Type/APCuCache.php
index f1dde2462c..3c72e76c98 100644
--- a/src/Core/Cache/Type/APCuCache.php
+++ b/src/Core/Cache/Type/APCuCache.php
@@ -147,4 +147,19 @@ class APCuCache extends AbstractCache implements ICanCacheInMemory
 
 		return true;
 	}
+
+	/** {@inheritDoc} */
+	public function getStats(): array
+	{
+		$apcu = apcu_cache_info();
+		$sma  = apcu_sma_info();
+
+		return [
+			'entries'     => $apcu['num_entries'] ?? null,
+			'used_memory'    => $apcu['mem_size'] ?? null,
+			'hits'        => $apcu['num_hits'] ?? null,
+			'misses'      => $apcu['num_misses'] ?? null,
+			'avail_mem'   => $sma['avail_mem'] ?? null,
+		];
+	}
 }
diff --git a/src/Core/Cache/Type/ArrayCache.php b/src/Core/Cache/Type/ArrayCache.php
index 7fd44deb0a..c6a20d627b 100644
--- a/src/Core/Cache/Type/ArrayCache.php
+++ b/src/Core/Cache/Type/ArrayCache.php
@@ -96,4 +96,10 @@ class ArrayCache extends AbstractCache implements ICanCacheInMemory
 			return false;
 		}
 	}
+
+	/** {@inheritDoc} */
+	public function getStats(): array
+	{
+		return [];
+	}
 }
diff --git a/src/Core/Cache/Type/MemcacheCache.php b/src/Core/Cache/Type/MemcacheCache.php
index 14bd5e310b..b2142ffd36 100644
--- a/src/Core/Cache/Type/MemcacheCache.php
+++ b/src/Core/Cache/Type/MemcacheCache.php
@@ -156,4 +156,21 @@ class MemcacheCache extends AbstractCache implements ICanCacheInMemory
 		$cacheKey = $this->getCacheKey($key);
 		return $this->memcache->add($cacheKey, serialize($value), MEMCACHE_COMPRESSED, $ttl);
 	}
+
+	/** {@inheritDoc} */
+	public function getStats(): array
+	{
+		$stats = $this->memcache->getStats();
+
+		return [
+			'version'           => $stats['version'] ?? null,
+			'entries'           => $stats['curr_items'] ?? null,
+			'used_memory'       => $stats['bytes'] ?? null,
+			'uptime'            => $stats['uptime'] ?? null,
+			'connected_clients' => $stats['curr_connections'] ?? null,
+			'hits'              => $stats['get_hits'] ?? null,
+			'misses'            => $stats['get_misses'] ?? null,
+			'evictions'         => $stats['evictions'] ?? null,
+		];
+	}
 }
diff --git a/src/Core/Cache/Type/MemcachedCache.php b/src/Core/Cache/Type/MemcachedCache.php
index 2e970e6078..53f959fd2b 100644
--- a/src/Core/Cache/Type/MemcachedCache.php
+++ b/src/Core/Cache/Type/MemcachedCache.php
@@ -172,4 +172,27 @@ class MemcachedCache extends AbstractCache implements ICanCacheInMemory
 		$cacheKey = $this->getCacheKey($key);
 		return $this->memcached->add($cacheKey, $value, $ttl);
 	}
+
+	/** {@inheritDoc} */
+	public function getStats(): array
+	{
+		$stats = $this->memcached->getStats();
+
+		// get statistics of the first instance
+		foreach ($stats as $value) {
+			$stats = $value;
+			break;
+		}
+
+		return [
+			'version'           => $stats['version'] ?? null,
+			'entries'     =>       $stats['curr_items'] ?? null,
+			'used_memory'       => $stats['bytes'] ?? null,
+			'uptime'            => $stats['uptime'] ?? null,
+			'connected_clients' => $stats['curr_connections'] ?? null,
+			'hits'              => $stats['get_hits'] ?? null,
+			'misses'            => $stats['get_misses'] ?? null,
+			'evictions'         => $stats['evictions'] ?? null,
+		];
+	}
 }
diff --git a/src/Core/Cache/Type/ProfilerCacheDecorator.php b/src/Core/Cache/Type/ProfilerCacheDecorator.php
index 8071b79c5e..113aa76688 100644
--- a/src/Core/Cache/Type/ProfilerCacheDecorator.php
+++ b/src/Core/Cache/Type/ProfilerCacheDecorator.php
@@ -166,4 +166,14 @@ class ProfilerCacheDecorator implements ICanCache, ICanCacheInMemory
 	{
 		return $this->cache->getName() . ' (with profiler)';
 	}
+
+	/** {@inheritDoc} */
+	public function getStats(): array
+	{
+		if ($this->cache instanceof ICanCacheInMemory) {
+			return $this->cache->getStats();
+		} else {
+			return [];
+		}
+	}
 }
diff --git a/src/Core/Cache/Type/RedisCache.php b/src/Core/Cache/Type/RedisCache.php
index cf78d362bb..d005624641 100644
--- a/src/Core/Cache/Type/RedisCache.php
+++ b/src/Core/Cache/Type/RedisCache.php
@@ -207,4 +207,21 @@ class RedisCache extends AbstractCache implements ICanCacheInMemory
 		$this->redis->unwatch();
 		return false;
 	}
+
+	/** {@inheritDoc} */
+	public function getStats(): array
+	{
+		$info = $this->redis->info();
+
+		return [
+			'version'           => $info['redis_version'] ?? null,
+			'entries'           => $this->redis->dbSize() ?? null,
+			'used_memory'       => $info['used_memory'] ?? null,
+			'connected_clients' => $info['connected_clients'] ?? null,
+			'uptime'            => $info['uptime_in_seconds'] ?? null,
+			'hits'              => $info['keyspace_hits'] ?? null,
+			'misses'            => $info['keyspace_misses'] ?? null,
+			'evictions'         => $info['evicted_keys'] ?? null,
+		];
+	}
 }
diff --git a/src/Core/Lock/Type/CacheLock.php b/src/Core/Lock/Type/CacheLock.php
index c3794d06a7..9fc9fad8f8 100644
--- a/src/Core/Lock/Type/CacheLock.php
+++ b/src/Core/Lock/Type/CacheLock.php
@@ -156,6 +156,16 @@ class CacheLock extends AbstractLock
 		return $success;
 	}
 
+	/**
+	 * Returns stats about the cache provider
+	 *
+	 * @return array
+	 */
+	public function getCacheStats(): array
+	{
+		return $this->cache->getStats();
+	}
+
 	/**
 	 * @param string $key The original key
 	 *
diff --git a/tests/src/Core/Cache/APCuCacheTest.php b/tests/src/Core/Cache/APCuCacheTest.php
index 117c211b04..47e660b26a 100644
--- a/tests/src/Core/Cache/APCuCacheTest.php
+++ b/tests/src/Core/Cache/APCuCacheTest.php
@@ -35,4 +35,18 @@ class APCuCacheTest extends MemoryCacheTestCase
 		$this->cache->clear(false);
 		parent::tearDown();
 	}
+
+	/**
+	 * @small
+	 */
+	public function testStats()
+	{
+		$stats = $this->instance->getStats();
+
+		self::assertNotNull($stats['entries']);
+		self::assertNotNull($stats['used_memory']);
+		self::assertNotNull($stats['hits']);
+		self::assertNotNull($stats['misses']);
+		self::assertNotNull($stats['avail_mem']);
+	}
 }
diff --git a/tests/src/Core/Cache/MemcacheCacheTest.php b/tests/src/Core/Cache/MemcacheCacheTest.php
index abd073f483..c622f22216 100644
--- a/tests/src/Core/Cache/MemcacheCacheTest.php
+++ b/tests/src/Core/Cache/MemcacheCacheTest.php
@@ -59,4 +59,21 @@ class MemcacheCacheTest extends MemoryCacheTestCase
 	{
 		static::markTestIncomplete('Race condition because of too fast getAllKeys() which uses a workaround');
 	}
+
+	/**
+	 * @small
+	 */
+	public function testStats()
+	{
+		$stats = $this->instance->getStats();
+
+		self::assertNotNull($stats['version']);
+		self::assertIsNumeric($stats['hits']);
+		self::assertIsNumeric($stats['misses']);
+		self::assertIsNumeric($stats['evictions']);
+		self::assertIsNumeric($stats['entries']);
+		self::assertIsNumeric($stats['used_memory']);
+		self::assertGreaterThan(0, $stats['connected_clients']);
+		self::assertGreaterThan(0, $stats['uptime']);
+	}
 }
diff --git a/tests/src/Core/Cache/MemcachedCacheTest.php b/tests/src/Core/Cache/MemcachedCacheTest.php
index f3b6107b5b..a1c3653f1b 100644
--- a/tests/src/Core/Cache/MemcachedCacheTest.php
+++ b/tests/src/Core/Cache/MemcachedCacheTest.php
@@ -58,4 +58,21 @@ class MemcachedCacheTest extends MemoryCacheTestCase
 	{
 		static::markTestIncomplete('Race condition because of too fast getAllKeys() which uses a workaround');
 	}
+
+	/**
+	 * @small
+	 */
+	public function testStats()
+	{
+		$stats = $this->instance->getStats();
+
+		self::assertNotNull($stats['version']);
+		self::assertIsNumeric($stats['hits']);
+		self::assertIsNumeric($stats['misses']);
+		self::assertIsNumeric($stats['evictions']);
+		self::assertIsNumeric($stats['entries']);
+		self::assertIsNumeric($stats['used_memory']);
+		self::assertGreaterThan(0, $stats['connected_clients']);
+		self::assertGreaterThan(0, $stats['uptime']);
+	}
 }
diff --git a/tests/src/Core/Cache/RedisCacheTest.php b/tests/src/Core/Cache/RedisCacheTest.php
index 6169171f40..d16bf5a64c 100644
--- a/tests/src/Core/Cache/RedisCacheTest.php
+++ b/tests/src/Core/Cache/RedisCacheTest.php
@@ -57,4 +57,21 @@ class RedisCacheTest extends MemoryCacheTestCase
 		$this->cache->clear(false);
 		parent::tearDown();
 	}
+
+	/**
+	 * @small
+	 */
+	public function testStats()
+	{
+		$stats = $this->instance->getStats();
+
+		self::assertNotNull($stats['version']);
+		self::assertIsNumeric($stats['hits']);
+		self::assertIsNumeric($stats['misses']);
+		self::assertIsNumeric($stats['evictions']);
+		self::assertIsNumeric($stats['entries']);
+		self::assertIsNumeric($stats['used_memory']);
+		self::assertGreaterThan(0, $stats['connected_clients']);
+		self::assertGreaterThan(0, $stats['uptime']);
+	}
 }

From a20828f6187902b2d5915b1a8690168916271b01 Mon Sep 17 00:00:00 2001
From: Philipp 
Date: Mon, 21 Apr 2025 20:12:54 +0200
Subject: [PATCH 05/22] Add Caching stats

---
 src/Module/StatsCaching.php | 98 +++++++++++++++++++++++++++++++++++++
 static/routes.config.php    |  3 +-
 2 files changed, 100 insertions(+), 1 deletion(-)
 create mode 100644 src/Module/StatsCaching.php

diff --git a/src/Module/StatsCaching.php b/src/Module/StatsCaching.php
new file mode 100644
index 0000000000..72ba61c67a
--- /dev/null
+++ b/src/Module/StatsCaching.php
@@ -0,0 +1,98 @@
+config = $config;
+		$this->cache  = $cache;
+		$this->lock   = $lock;
+	}
+
+	private function isAllowed(array $request): bool
+	{
+		return empty(!$request['key']) && $request['key'] == $this->config->get('system', 'stats_key');
+	}
+
+	protected function content(array $request = []): string
+	{
+		if (!$this->isAllowed($request)) {
+			throw new HTTPException\NotFoundException($this->l10n->t('Page not found.'));
+		}
+		return '';
+	}
+
+	protected function rawContent(array $request = [])
+	{
+		if (!$this->isAllowed($request)) {
+			return;
+		}
+
+		$data = [];
+
+		// OPcache
+		if (function_exists('opcache_get_status')) {
+			$status          = opcache_get_status(false);
+			$data['opcache'] = [
+				'enabled'            => $status['opcache_enabled'] ?? false,
+				'hit_rate'           => $status['opcache_statistics']['opcache_hit_rate'] ?? null,
+				'used_memory'        => $status['memory_usage']['used_memory'] ?? null,
+				'free_memory'        => $status['memory_usage']['free_memory'] ?? null,
+				'num_cached_scripts' => $status['opcache_statistics']['num_cached_scripts'] ?? null,
+			];
+		} else {
+			$data['opcache'] = [
+				'enabled' => false,
+			];
+		}
+
+		if ($this->cache instanceof ICanCacheInMemory) {
+			$data['cache'] = [
+				'type' => $this->cache->getName(),
+				'stats' => $this->cache->getStats(),
+			];
+		} else {
+			$data['cache'] = [
+				'type' => $this->cache->getName(),
+			];
+		}
+
+		if ($this->lock instanceof CacheLock) {
+			$data['lock'] = [
+				'type' => $this->lock->getName(),
+				'stats' => $this->lock->getCacheStats(),
+			];
+		} else {
+			$data['lock'] = [
+				'type' => $this->lock->getName(),
+			];
+		}
+
+		$this->jsonExit($data);
+	}
+}
diff --git a/static/routes.config.php b/static/routes.config.php
index 88e642c27a..4c7b6949ec 100644
--- a/static/routes.config.php
+++ b/static/routes.config.php
@@ -642,7 +642,8 @@ return [
 		],
 	],
 
-	'/stats' => [Module\Stats::class, [R::GET]],
+	'/stats'         => [Module\Stats::class, [R::GET]],
+	'/stats/caching' => [Module\StatsCaching::class, [R::GET]],
 
 	'/network' => [
 		'[/{content}]'                => [Module\Conversation\Network::class, [R::GET]],

From 50c720688badc7185c6a71fd185bab0db8e7179d Mon Sep 17 00:00:00 2001
From: Philipp 
Date: Mon, 21 Apr 2025 20:49:41 +0200
Subject: [PATCH 06/22] Make PHPCS happy

---
 src/Core/Cache/Type/APCuCache.php      | 11 +++++------
 src/Core/Cache/Type/ArrayCache.php     |  3 +--
 src/Core/Cache/Type/MemcacheCache.php  | 17 ++++++++---------
 src/Core/Cache/Type/MemcachedCache.php | 17 ++++++++---------
 src/Core/Lock/Type/CacheLock.php       |  1 -
 src/Module/StatsCaching.php            | 12 ++++++------
 6 files changed, 28 insertions(+), 33 deletions(-)

diff --git a/src/Core/Cache/Type/APCuCache.php b/src/Core/Cache/Type/APCuCache.php
index 3c72e76c98..b269b5aa9d 100644
--- a/src/Core/Cache/Type/APCuCache.php
+++ b/src/Core/Cache/Type/APCuCache.php
@@ -16,10 +16,9 @@ use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
  */
 class APCuCache extends AbstractCache implements ICanCacheInMemory
 {
-	const NAME = 'apcu';
-
 	use CompareSetTrait;
 	use CompareDeleteTrait;
+	const NAME = 'apcu';
 
 	/**
 	 * @throws InvalidCacheDriverException
@@ -156,10 +155,10 @@ class APCuCache extends AbstractCache implements ICanCacheInMemory
 
 		return [
 			'entries'     => $apcu['num_entries'] ?? null,
-			'used_memory'    => $apcu['mem_size'] ?? null,
-			'hits'        => $apcu['num_hits'] ?? null,
-			'misses'      => $apcu['num_misses'] ?? null,
-			'avail_mem'   => $sma['avail_mem'] ?? null,
+			'used_memory' => $apcu['mem_size']    ?? null,
+			'hits'        => $apcu['num_hits']    ?? null,
+			'misses'      => $apcu['num_misses']  ?? null,
+			'avail_mem'   => $sma['avail_mem']    ?? null,
 		];
 	}
 }
diff --git a/src/Core/Cache/Type/ArrayCache.php b/src/Core/Cache/Type/ArrayCache.php
index c6a20d627b..148210b4e8 100644
--- a/src/Core/Cache/Type/ArrayCache.php
+++ b/src/Core/Cache/Type/ArrayCache.php
@@ -15,9 +15,8 @@ use Friendica\Core\Cache\Enum;
  */
 class ArrayCache extends AbstractCache implements ICanCacheInMemory
 {
-	const NAME = 'array';
-
 	use CompareDeleteTrait;
+	const NAME = 'array';
 
 	/** @var array Array with the cached data */
 	protected $cachedData = [];
diff --git a/src/Core/Cache/Type/MemcacheCache.php b/src/Core/Cache/Type/MemcacheCache.php
index b2142ffd36..b3a6588841 100644
--- a/src/Core/Cache/Type/MemcacheCache.php
+++ b/src/Core/Cache/Type/MemcacheCache.php
@@ -19,11 +19,10 @@ use Memcache;
  */
 class MemcacheCache extends AbstractCache implements ICanCacheInMemory
 {
-	const NAME = 'memcache';
-
 	use CompareSetTrait;
 	use CompareDeleteTrait;
 	use MemcacheCommandTrait;
+	const NAME = 'memcache';
 
 	/**
 	 * @var Memcache
@@ -163,14 +162,14 @@ class MemcacheCache extends AbstractCache implements ICanCacheInMemory
 		$stats = $this->memcache->getStats();
 
 		return [
-			'version'           => $stats['version'] ?? null,
-			'entries'           => $stats['curr_items'] ?? null,
-			'used_memory'       => $stats['bytes'] ?? null,
-			'uptime'            => $stats['uptime'] ?? null,
+			'version'           => $stats['version']          ?? null,
+			'entries'           => $stats['curr_items']       ?? null,
+			'used_memory'       => $stats['bytes']            ?? null,
+			'uptime'            => $stats['uptime']           ?? null,
 			'connected_clients' => $stats['curr_connections'] ?? null,
-			'hits'              => $stats['get_hits'] ?? null,
-			'misses'            => $stats['get_misses'] ?? null,
-			'evictions'         => $stats['evictions'] ?? null,
+			'hits'              => $stats['get_hits']         ?? null,
+			'misses'            => $stats['get_misses']       ?? null,
+			'evictions'         => $stats['evictions']        ?? null,
 		];
 	}
 }
diff --git a/src/Core/Cache/Type/MemcachedCache.php b/src/Core/Cache/Type/MemcachedCache.php
index 53f959fd2b..03ad7d8322 100644
--- a/src/Core/Cache/Type/MemcachedCache.php
+++ b/src/Core/Cache/Type/MemcachedCache.php
@@ -20,11 +20,10 @@ use Psr\Log\LoggerInterface;
  */
 class MemcachedCache extends AbstractCache implements ICanCacheInMemory
 {
-	const NAME = 'memcached';
-
 	use CompareSetTrait;
 	use CompareDeleteTrait;
 	use MemcacheCommandTrait;
+	const NAME = 'memcached';
 
 	/**
 	 * @var \Memcached
@@ -185,14 +184,14 @@ class MemcachedCache extends AbstractCache implements ICanCacheInMemory
 		}
 
 		return [
-			'version'           => $stats['version'] ?? null,
-			'entries'     =>       $stats['curr_items'] ?? null,
-			'used_memory'       => $stats['bytes'] ?? null,
-			'uptime'            => $stats['uptime'] ?? null,
+			'version'           => $stats['version']          ?? null,
+			'entries'           => $stats['curr_items']       ?? null,
+			'used_memory'       => $stats['bytes']            ?? null,
+			'uptime'            => $stats['uptime']           ?? null,
 			'connected_clients' => $stats['curr_connections'] ?? null,
-			'hits'              => $stats['get_hits'] ?? null,
-			'misses'            => $stats['get_misses'] ?? null,
-			'evictions'         => $stats['evictions'] ?? null,
+			'hits'              => $stats['get_hits']         ?? null,
+			'misses'            => $stats['get_misses']       ?? null,
+			'evictions'         => $stats['evictions']        ?? null,
 		];
 	}
 }
diff --git a/src/Core/Lock/Type/CacheLock.php b/src/Core/Lock/Type/CacheLock.php
index 9fc9fad8f8..c7fa75e021 100644
--- a/src/Core/Lock/Type/CacheLock.php
+++ b/src/Core/Lock/Type/CacheLock.php
@@ -7,7 +7,6 @@
 
 namespace Friendica\Core\Lock\Type;
 
-use Friendica\Core\Cache\Capability\ICanCache;
 use Friendica\Core\Cache\Capability\ICanCacheInMemory;
 use Friendica\Core\Cache\Enum\Duration;
 use Friendica\Core\Cache\Exception\CachePersistenceException;
diff --git a/src/Module/StatsCaching.php b/src/Module/StatsCaching.php
index 72ba61c67a..b8d0cd1829 100644
--- a/src/Module/StatsCaching.php
+++ b/src/Module/StatsCaching.php
@@ -59,10 +59,10 @@ class StatsCaching extends BaseModule
 		if (function_exists('opcache_get_status')) {
 			$status          = opcache_get_status(false);
 			$data['opcache'] = [
-				'enabled'            => $status['opcache_enabled'] ?? false,
-				'hit_rate'           => $status['opcache_statistics']['opcache_hit_rate'] ?? null,
-				'used_memory'        => $status['memory_usage']['used_memory'] ?? null,
-				'free_memory'        => $status['memory_usage']['free_memory'] ?? null,
+				'enabled'            => $status['opcache_enabled']                          ?? false,
+				'hit_rate'           => $status['opcache_statistics']['opcache_hit_rate']   ?? null,
+				'used_memory'        => $status['memory_usage']['used_memory']              ?? null,
+				'free_memory'        => $status['memory_usage']['free_memory']              ?? null,
 				'num_cached_scripts' => $status['opcache_statistics']['num_cached_scripts'] ?? null,
 			];
 		} else {
@@ -73,7 +73,7 @@ class StatsCaching extends BaseModule
 
 		if ($this->cache instanceof ICanCacheInMemory) {
 			$data['cache'] = [
-				'type' => $this->cache->getName(),
+				'type'  => $this->cache->getName(),
 				'stats' => $this->cache->getStats(),
 			];
 		} else {
@@ -84,7 +84,7 @@ class StatsCaching extends BaseModule
 
 		if ($this->lock instanceof CacheLock) {
 			$data['lock'] = [
-				'type' => $this->lock->getName(),
+				'type'  => $this->lock->getName(),
 				'stats' => $this->lock->getCacheStats(),
 			];
 		} else {

From 596957658568769d70260f1d749aae5bcfce0828 Mon Sep 17 00:00:00 2001
From: Philipp 
Date: Mon, 21 Apr 2025 20:50:26 +0200
Subject: [PATCH 07/22] Add missing license

---
 src/Module/StatsCaching.php | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/Module/StatsCaching.php b/src/Module/StatsCaching.php
index b8d0cd1829..6247eca9c1 100644
--- a/src/Module/StatsCaching.php
+++ b/src/Module/StatsCaching.php
@@ -1,5 +1,10 @@
 
Date: Mon, 21 Apr 2025 20:53:28 +0200
Subject: [PATCH 08/22] Make PHPCS happy - again

---
 src/Core/Cache/Type/RedisCache.php | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/Core/Cache/Type/RedisCache.php b/src/Core/Cache/Type/RedisCache.php
index d005624641..fc04a5433a 100644
--- a/src/Core/Cache/Type/RedisCache.php
+++ b/src/Core/Cache/Type/RedisCache.php
@@ -214,14 +214,14 @@ class RedisCache extends AbstractCache implements ICanCacheInMemory
 		$info = $this->redis->info();
 
 		return [
-			'version'           => $info['redis_version'] ?? null,
-			'entries'           => $this->redis->dbSize() ?? null,
-			'used_memory'       => $info['used_memory'] ?? null,
+			'version'           => $info['redis_version']     ?? null,
+			'entries'           => $this->redis->dbSize()     ?? null,
+			'used_memory'       => $info['used_memory']       ?? null,
 			'connected_clients' => $info['connected_clients'] ?? null,
 			'uptime'            => $info['uptime_in_seconds'] ?? null,
-			'hits'              => $info['keyspace_hits'] ?? null,
-			'misses'            => $info['keyspace_misses'] ?? null,
-			'evictions'         => $info['evicted_keys'] ?? null,
+			'hits'              => $info['keyspace_hits']     ?? null,
+			'misses'            => $info['keyspace_misses']   ?? null,
+			'evictions'         => $info['evicted_keys']      ?? null,
 		];
 	}
 }

From b222aa0c48020de168291cd406e92c5f89ced396 Mon Sep 17 00:00:00 2001
From: Philipp 
Date: Sun, 27 Apr 2025 01:36:30 +0200
Subject: [PATCH 09/22] Add a bunch of tests for StatsCaching

---
 composer.json                                 |   1 +
 composer.lock                                 |  67 +++++-
 src/Module/StatsCaching.php                   |   9 +-
 tests/CacheLockTestCase.php                   |  26 +++
 tests/LockTestCase.php                        |  17 +-
 tests/src/Core/Cache/ArrayCacheTest.php       |   8 +
 .../Core/Cache/ProfilerCacheDecoratorTest.php |  56 +++++
 tests/src/Core/Lock/APCuCacheLockTest.php     |  21 +-
 tests/src/Core/Lock/ArrayCacheLockTest.php    |  25 ++-
 .../src/Core/Lock/DatabaseLockDriverTest.php  |   3 +-
 tests/src/Core/Lock/MemcacheCacheLockTest.php |  28 ++-
 .../src/Core/Lock/MemcachedCacheLockTest.php  |  27 ++-
 tests/src/Core/Lock/RedisCacheLockTest.php    |  24 +-
 tests/src/Core/Lock/SemaphoreLockTest.php     |   3 +-
 tests/src/Module/StatsCachingTest.php         | 205 ++++++++++++++++++
 15 files changed, 477 insertions(+), 43 deletions(-)
 create mode 100644 tests/CacheLockTestCase.php
 create mode 100644 tests/src/Core/Cache/ProfilerCacheDecoratorTest.php
 create mode 100644 tests/src/Module/StatsCachingTest.php

diff --git a/composer.json b/composer.json
index a2c9eea3c9..655f987df9 100644
--- a/composer.json
+++ b/composer.json
@@ -153,6 +153,7 @@
 		"dms/phpunit-arraysubset-asserts": "^0.3.1",
 		"mikey179/vfsstream": "^1.6",
 		"mockery/mockery": "^1.3",
+		"php-mock/php-mock-mockery": "^1.5",
 		"php-mock/php-mock-phpunit": "^2.10",
 		"phpmd/phpmd": "^2.15",
 		"phpstan/phpstan": "^2.0",
diff --git a/composer.lock b/composer.lock
index e12cc6533c..bae30155dd 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "b77bf714197f04022a5feb001bf07852",
+    "content-hash": "32af97f73ec49df2a6cfe98f11bc1d60",
     "packages": [
         {
             "name": "asika/simple-console",
@@ -5441,6 +5441,71 @@
             ],
             "time": "2024-02-10T21:37:25+00:00"
         },
+        {
+            "name": "php-mock/php-mock-mockery",
+            "version": "1.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-mock/php-mock-mockery.git",
+                "reference": "291994acdc26daf1e3c659cfbe58b01eeb180b7f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-mock/php-mock-mockery/zipball/291994acdc26daf1e3c659cfbe58b01eeb180b7f",
+                "reference": "291994acdc26daf1e3c659cfbe58b01eeb180b7f",
+                "shasum": ""
+            },
+            "require": {
+                "mockery/mockery": "^1",
+                "php": ">=5.6",
+                "php-mock/php-mock-integration": "^2.2.1 || ^3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4|^5|^8"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "phpmock\\mockery\\": "classes/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "WTFPL"
+            ],
+            "authors": [
+                {
+                    "name": "Markus Malkusch",
+                    "email": "markus@malkusch.de",
+                    "homepage": "http://markus.malkusch.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Mock built-in PHP functions (e.g. time()) with Mockery. This package relies on PHP's namespace fallback policy. No further extension is needed.",
+            "homepage": "https://github.com/php-mock/php-mock-mockery",
+            "keywords": [
+                "BDD",
+                "TDD",
+                "function",
+                "mock",
+                "mockery",
+                "stub",
+                "test",
+                "test double",
+                "testing"
+            ],
+            "support": {
+                "issues": "https://github.com/php-mock/php-mock-mockery/issues",
+                "source": "https://github.com/php-mock/php-mock-mockery/tree/1.5.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/michalbundyra",
+                    "type": "github"
+                }
+            ],
+            "time": "2025-03-08T19:46:20+00:00"
+        },
         {
             "name": "php-mock/php-mock-phpunit",
             "version": "2.10.0",
diff --git a/src/Module/StatsCaching.php b/src/Module/StatsCaching.php
index 6247eca9c1..668d26e021 100644
--- a/src/Module/StatsCaching.php
+++ b/src/Module/StatsCaching.php
@@ -15,6 +15,7 @@ use Friendica\Core\Config\Capability\IManageConfigValues;
 use Friendica\Core\L10n;
 use Friendica\Core\Lock\Capability\ICanLock;
 use Friendica\Core\Lock\Type\CacheLock;
+use Friendica\Network\HTTPException\NotFoundException;
 use Friendica\Util\Profiler;
 use Psr\Log\LoggerInterface;
 use Friendica\Network\HTTPException;
@@ -41,9 +42,12 @@ class StatsCaching extends BaseModule
 
 	private function isAllowed(array $request): bool
 	{
-		return empty(!$request['key']) && $request['key'] == $this->config->get('system', 'stats_key');
+		return !empty($request['key']) && $request['key'] == $this->config->get('system', 'stats_key');
 	}
 
+	/**
+	 * @throws NotFoundException In case the rquest isn't allowed
+	 */
 	protected function content(array $request = []): string
 	{
 		if (!$this->isAllowed($request)) {
@@ -98,6 +102,7 @@ class StatsCaching extends BaseModule
 			];
 		}
 
-		$this->jsonExit($data);
+		$this->response->setType('json', 'application/json; charset=utf-8');
+		$this->response->addContent(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
 	}
 }
diff --git a/tests/CacheLockTestCase.php b/tests/CacheLockTestCase.php
new file mode 100644
index 0000000000..1599391ece
--- /dev/null
+++ b/tests/CacheLockTestCase.php
@@ -0,0 +1,26 @@
+getCache()->getStats()), array_keys($this->instance->getCacheStats()));
+	}
+}
diff --git a/tests/LockTestCase.php b/tests/LockTestCase.php
index 9ce86497b7..1b80e575ef 100644
--- a/tests/LockTestCase.php
+++ b/tests/LockTestCase.php
@@ -7,22 +7,21 @@
 
 namespace Friendica\Test;
 
+use Friendica\Core\Cache\Capability\ICanCache;
+use Friendica\Core\Cache\Capability\ICanCacheInMemory;
 use Friendica\Core\Lock\Capability\ICanLock;
-use Friendica\Test\MockedTestCase;
+use Friendica\Core\Lock\Type\CacheLock;
 
 abstract class LockTestCase extends MockedTestCase
 {
 	/**
-	 * @var int Start time of the mock (used for time operations)
+	 * Start time of the mock (used for time operations)
 	 */
-	protected $startTime = 1417011228;
+	protected int $startTime = 1417011228;
+	protected ICanLock $instance;
 
-	/**
-	 * @var ICanLock
-	 */
-	protected $instance;
+	abstract protected function getInstance(): ICanLock;
 
-	abstract protected function getInstance();
 
 	protected function setUp(): void
 	{
@@ -205,4 +204,6 @@ abstract class LockTestCase extends MockedTestCase
 		self::assertFalse($this->instance->isLocked('wrongLock'));
 		self::assertFalse($this->instance->release('wrongLock'));
 	}
+
+
 }
diff --git a/tests/src/Core/Cache/ArrayCacheTest.php b/tests/src/Core/Cache/ArrayCacheTest.php
index 967cb07bce..50226b0907 100644
--- a/tests/src/Core/Cache/ArrayCacheTest.php
+++ b/tests/src/Core/Cache/ArrayCacheTest.php
@@ -33,4 +33,12 @@ class ArrayCacheTest extends MemoryCacheTestCase
 		self::markTestSkipped("Array Cache doesn't support TTL");
 		return true;
 	}
+
+	/**
+	 * @small
+	 */
+	public function testGetStats()
+	{
+		self::assertEmpty($this->cache->getStats());
+	}
 }
diff --git a/tests/src/Core/Cache/ProfilerCacheDecoratorTest.php b/tests/src/Core/Cache/ProfilerCacheDecoratorTest.php
new file mode 100644
index 0000000000..3f44bcd0bf
--- /dev/null
+++ b/tests/src/Core/Cache/ProfilerCacheDecoratorTest.php
@@ -0,0 +1,56 @@
+shouldReceive('get')->with('system', 'profiler')->once()->andReturn(false);
+		$config->shouldReceive('get')->with('rendertime', 'callstack')->once()->andReturn(false);
+
+		$this->cache = new ProfilerCacheDecorator(new ArrayCache('localhost'), new Profiler($config));
+		return $this->cache;
+	}
+
+	protected function tearDown(): void
+	{
+		$this->cache->clear(false);
+		parent::tearDown();
+	}
+
+	/**
+	 * @doesNotPerformAssertions
+	 */
+	public function testTTL()
+	{
+		// Array Cache doesn't support TTL
+		self::markTestSkipped("Array Cache doesn't support TTL");
+		return true;
+	}
+
+	/**
+	 * @small
+	 */
+	public function testGetStats()
+	{
+		self::assertEmpty($this->cache->getStats());
+	}
+
+	public function testGetName()
+	{
+		self::assertStringEndsWith(' (with profiler)', $this->instance->getName());
+	}
+}
diff --git a/tests/src/Core/Lock/APCuCacheLockTest.php b/tests/src/Core/Lock/APCuCacheLockTest.php
index 3ee0d09661..42a1fc72d5 100644
--- a/tests/src/Core/Lock/APCuCacheLockTest.php
+++ b/tests/src/Core/Lock/APCuCacheLockTest.php
@@ -7,26 +7,39 @@
 
 namespace Friendica\Test\src\Core\Lock;
 
+use Friendica\Core\Cache\Capability\ICanCacheInMemory;
 use Friendica\Core\Cache\Type\APCuCache;
+use Friendica\Core\Lock\Capability\ICanLock;
 use Friendica\Core\Lock\Type\CacheLock;
-use Friendica\Test\LockTestCase;
+use Friendica\Test\CacheLockTestCase;
 
 /**
  * @group APCU
  */
-class APCuCacheLockTest extends LockTestCase
+class APCuCacheLockTest extends CacheLockTestCase
 {
+	private APCuCache $cache;
+	private ICanLock $lock;
+
 	protected function setUp(): void
 	{
 		if (!APCuCache::isAvailable()) {
 			static::markTestSkipped('APCu is not available');
 		}
 
+		$this->cache = new APCuCache('localhost');
+		$this->lock = new CacheLock($this->cache);
+
 		parent::setUp();
 	}
 
-	protected function getInstance()
+	protected function getInstance(): CacheLock
 	{
-		return new CacheLock(new APCuCache('localhost'));
+		return $this->lock;
+	}
+
+	protected function getCache(): ICanCacheInMemory
+	{
+		return $this->cache;
 	}
 }
diff --git a/tests/src/Core/Lock/ArrayCacheLockTest.php b/tests/src/Core/Lock/ArrayCacheLockTest.php
index 19ac7925c6..2aac8e8293 100644
--- a/tests/src/Core/Lock/ArrayCacheLockTest.php
+++ b/tests/src/Core/Lock/ArrayCacheLockTest.php
@@ -7,15 +7,32 @@
 
 namespace Friendica\Test\src\Core\Lock;
 
+use Friendica\Core\Cache\Capability\ICanCacheInMemory;
 use Friendica\Core\Cache\Type\ArrayCache;
 use Friendica\Core\Lock\Type\CacheLock;
-use Friendica\Test\LockTestCase;
+use Friendica\Test\CacheLockTestCase;
 
-class ArrayCacheLockTest extends LockTestCase
+class ArrayCacheLockTest extends CacheLockTestCase
 {
-	protected function getInstance()
+	private CacheLock $lock;
+	private ArrayCache $cache;
+
+	protected function setUp(): void
 	{
-		return new CacheLock(new ArrayCache('localhost'));
+		$this->cache = new ArrayCache('localhost');
+		$this->lock = new CacheLock($this->cache);
+
+		parent::setUp();
+	}
+
+	protected function getInstance(): CacheLock
+	{
+		return $this->lock;
+	}
+
+	protected function getCache(): ICanCacheInMemory
+	{
+		return $this->cache;
 	}
 
 	/**
diff --git a/tests/src/Core/Lock/DatabaseLockDriverTest.php b/tests/src/Core/Lock/DatabaseLockDriverTest.php
index ebc2b0090f..fbfe61762e 100644
--- a/tests/src/Core/Lock/DatabaseLockDriverTest.php
+++ b/tests/src/Core/Lock/DatabaseLockDriverTest.php
@@ -7,6 +7,7 @@
 
 namespace Friendica\Test\src\Core\Lock;
 
+use Friendica\Core\Lock\Capability\ICanLock;
 use Friendica\Core\Lock\Type\DatabaseLock;
 use Friendica\Test\LockTestCase;
 use Friendica\Test\Util\CreateDatabaseTrait;
@@ -26,7 +27,7 @@ class DatabaseLockDriverTest extends LockTestCase
 		parent::setUp();
 	}
 
-	protected function getInstance()
+	protected function getInstance(): ICanLock
 	{
 		return new DatabaseLock($this->getDbInstance(), $this->pid);
 	}
diff --git a/tests/src/Core/Lock/MemcacheCacheLockTest.php b/tests/src/Core/Lock/MemcacheCacheLockTest.php
index 2bb0595cff..c1dec663f1 100644
--- a/tests/src/Core/Lock/MemcacheCacheLockTest.php
+++ b/tests/src/Core/Lock/MemcacheCacheLockTest.php
@@ -8,19 +8,23 @@
 namespace Friendica\Test\src\Core\Lock;
 
 use Exception;
+use Friendica\Core\Cache\Capability\ICanCacheInMemory;
 use Friendica\Core\Cache\Type\MemcacheCache;
 use Friendica\Core\Config\Capability\IManageConfigValues;
 use Friendica\Core\Lock\Type\CacheLock;
-use Friendica\Test\LockTestCase;
+use Friendica\Test\CacheLockTestCase;
 use Mockery;
 
 /**
  * @requires extension Memcache
  * @group MEMCACHE
  */
-class MemcacheCacheLockTest extends LockTestCase
+class MemcacheCacheLockTest extends CacheLockTestCase
 {
-	protected function getInstance()
+	private CacheLock $lock;
+	private MemcacheCache $cache;
+
+	protected function setUp(): void
 	{
 		$configMock = Mockery::mock(IManageConfigValues::class);
 
@@ -36,16 +40,24 @@ class MemcacheCacheLockTest extends LockTestCase
 			->with('system', 'memcache_port')
 			->andReturn($port);
 
-		$lock = null;
-
 		try {
-			$cache = new MemcacheCache($host, $configMock);
-			$lock = new CacheLock($cache);
+			$this->cache = new MemcacheCache($host, $configMock);
+			$this->lock = new CacheLock($this->cache);
 		} catch (Exception $e) {
 			static::markTestSkipped('Memcache is not available');
 		}
 
-		return $lock;
+		parent::setUp();
+	}
+
+	protected function getInstance(): CacheLock
+	{
+		return $this->lock;
+	}
+
+	protected function getCache(): ICanCacheInMemory
+	{
+		return $this->cache;
 	}
 
 	/**
diff --git a/tests/src/Core/Lock/MemcachedCacheLockTest.php b/tests/src/Core/Lock/MemcachedCacheLockTest.php
index fb38ec3312..773e664108 100644
--- a/tests/src/Core/Lock/MemcachedCacheLockTest.php
+++ b/tests/src/Core/Lock/MemcachedCacheLockTest.php
@@ -8,9 +8,11 @@
 namespace Friendica\Test\src\Core\Lock;
 
 use Exception;
+use Friendica\Core\Cache\Capability\ICanCacheInMemory;
 use Friendica\Core\Cache\Type\MemcachedCache;
 use Friendica\Core\Config\Capability\IManageConfigValues;
 use Friendica\Core\Lock\Type\CacheLock;
+use Friendica\Test\CacheLockTestCase;
 use Friendica\Test\LockTestCase;
 use Mockery;
 use Psr\Log\NullLogger;
@@ -19,9 +21,12 @@ use Psr\Log\NullLogger;
  * @requires extension memcached
  * @group MEMCACHED
  */
-class MemcachedCacheLockTest extends LockTestCase
+class MemcachedCacheLockTest extends CacheLockTestCase
 {
-	protected function getInstance()
+	private MemcachedCache $cache;
+	private CacheLock $lock;
+
+	protected function setUp(): void
 	{
 		$configMock = Mockery::mock(IManageConfigValues::class);
 
@@ -35,16 +40,24 @@ class MemcachedCacheLockTest extends LockTestCase
 
 		$logger = new NullLogger();
 
-		$lock = null;
-
 		try {
-			$cache = new MemcachedCache($host, $configMock, $logger);
-			$lock = new CacheLock($cache);
+			$this->cache = new MemcachedCache($host, $configMock, $logger);
+			$this->lock = new CacheLock($this->cache);
 		} catch (Exception $e) {
 			static::markTestSkipped('Memcached is not available');
 		}
 
-		return $lock;
+		parent::setUp();
+	}
+
+	protected function getInstance(): CacheLock
+	{
+		return $this->lock;
+	}
+
+	protected function getCache(): ICanCacheInMemory
+	{
+		return $this->cache;
 	}
 
 	/**
diff --git a/tests/src/Core/Lock/RedisCacheLockTest.php b/tests/src/Core/Lock/RedisCacheLockTest.php
index d0237682c3..3cb4fba436 100644
--- a/tests/src/Core/Lock/RedisCacheLockTest.php
+++ b/tests/src/Core/Lock/RedisCacheLockTest.php
@@ -8,9 +8,11 @@
 namespace Friendica\Test\src\Core\Lock;
 
 use Exception;
+use Friendica\Core\Cache\Capability\ICanCacheInMemory;
 use Friendica\Core\Cache\Type\RedisCache;
 use Friendica\Core\Config\Capability\IManageConfigValues;
 use Friendica\Core\Lock\Type\CacheLock;
+use Friendica\Test\CacheLockTestCase;
 use Friendica\Test\LockTestCase;
 use Mockery;
 
@@ -18,9 +20,9 @@ use Mockery;
  * @requires extension redis
  * @group REDIS
  */
-class RedisCacheLockTest extends LockTestCase
+class RedisCacheLockTest extends CacheLockTestCase
 {
-	protected function getInstance()
+	protected function setUp(): void
 	{
 		$configMock = Mockery::mock(IManageConfigValues::class);
 
@@ -45,15 +47,23 @@ class RedisCacheLockTest extends LockTestCase
 			->with('system', 'redis_password')
 			->andReturn(null);
 
-		$lock = null;
-
 		try {
-			$cache = new RedisCache($host, $configMock);
-			$lock = new CacheLock($cache);
+			$this->cache = new RedisCache($host, $configMock);
+			$this->lock = new CacheLock($this->cache);
 		} catch (Exception $e) {
 			static::markTestSkipped('Redis is not available. Error: ' . $e->getMessage());
 		}
 
-		return $lock;
+		parent::setUp();
+	}
+
+	protected function getInstance(): CAcheLock
+	{
+		return $this->lock;
+	}
+
+	protected function getCache(): ICanCacheInMemory
+	{
+		return $this->cache;
 	}
 }
diff --git a/tests/src/Core/Lock/SemaphoreLockTest.php b/tests/src/Core/Lock/SemaphoreLockTest.php
index 06b4e02f46..1ddb2dade7 100644
--- a/tests/src/Core/Lock/SemaphoreLockTest.php
+++ b/tests/src/Core/Lock/SemaphoreLockTest.php
@@ -12,6 +12,7 @@ use Friendica\App;
 use Friendica\Core\Config\Capability\IManageConfigValues;
 use Friendica\Core\Config\Model\ReadOnlyFileConfig;
 use Friendica\Core\Config\ValueObject\Cache;
+use Friendica\Core\Lock\Capability\ICanLock;
 use Friendica\Core\Lock\Type\SemaphoreLock;
 use Friendica\Core\System;
 use Friendica\DI;
@@ -40,7 +41,7 @@ class SemaphoreLockTest extends LockTestCase
 		parent::setUp();
 	}
 
-	protected function getInstance()
+	protected function getInstance(): ICanLock
 	{
 		return new SemaphoreLock();
 	}
diff --git a/tests/src/Module/StatsCachingTest.php b/tests/src/Module/StatsCachingTest.php
new file mode 100644
index 0000000000..08d4b8fdb6
--- /dev/null
+++ b/tests/src/Module/StatsCachingTest.php
@@ -0,0 +1,205 @@
+httpExceptionMock = \Mockery::mock(HTTPException::class);
+		$this->config = \Mockery::mock(IManageConfigValues::class);
+		$this->cache = new ArrayCache('localhost');
+		$this->lock = new CacheLock($this->cache);
+	}
+
+	public function testStatsCachingNotAllowed()
+	{
+		$this->httpExceptionMock->shouldReceive('content')->andReturn('failed')->once();
+
+		$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
+			->run($this->httpExceptionMock);
+
+		self::assertEquals('404', $response->getStatusCode());
+		self::assertEquals('Page not found', $response->getReasonPhrase());
+		self::assertEquals('failed', $response->getBody());
+	}
+
+	public function testStatsCachingWitMinimumCache()
+	{
+		$request = [
+			'key' => '12345',
+		];
+		$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
+		PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false);
+
+		$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
+			->run($this->httpExceptionMock, $request);
+
+		self::assertJson($response->getBody());
+		self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders());
+
+		$json = json_decode($response->getBody(), true);
+
+		self::assertEquals([
+			'type' => 'array',
+			'stats' => [],
+		], $json['cache']);
+		self::assertEquals([
+			'type' => 'array',
+			'stats' => [],
+		], $json['lock']);
+	}
+
+	public function testStatsCachingWithDatabase()
+	{
+		$request = [
+			'key' => '12345',
+		];
+		$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
+
+		$this->cache = new DatabaseCache('localhost', DI::dba());
+		$this->lock = new DatabaseLock(DI::dba());
+		PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false);
+
+		$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
+			->run($this->httpExceptionMock, $request);
+
+		self::assertJson($response->getBody());
+		self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders());
+
+		$json = json_decode($response->getBody(), true);
+
+		self::assertEquals(['enabled' => false], $json['opcache']);
+		self::assertEquals(['type' => 'database'], $json['cache']);
+		self::assertEquals(['type' => 'database'], $json['lock']);
+	}
+
+	public function testStatsCachingWithCache()
+	{
+		$request = [
+			'key' => '12345',
+		];
+		$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
+
+		$this->cache = new DatabaseCache('localhost', DI::dba());
+		$this->lock = new DatabaseLock(DI::dba());
+		PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false);
+
+		$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
+			->run($this->httpExceptionMock, $request);
+
+		self::assertJson($response->getBody());
+		self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders());
+
+		$json = json_decode($response->getBody(), true);
+
+		self::assertEquals(['enabled' => false], $json['opcache']);
+		self::assertEquals(['type' => 'database'], $json['cache']);
+		self::assertEquals(['type' => 'database'], $json['lock']);
+	}
+
+	public function testStatsCachingWithOpcacheAndNull()
+	{
+		$request = [
+			'key' => '12345',
+		];
+		$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
+
+		$this->cache = new DatabaseCache('localhost', DI::dba());
+		$this->lock = new DatabaseLock(DI::dba());
+		PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(true);
+		PHPMockery::mock("Friendica\\Module", "opcache_get_status")->with(false)->once()->andReturn(false);
+
+		$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
+			->run($this->httpExceptionMock, $request);
+
+		self::assertJson($response->getBody());
+		self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders());
+
+		$json = json_decode($response->getBody(), true);
+
+		print_r($json);
+
+		self::assertEquals([
+			'enabled' => false,
+			'hit_rate' => null,
+			'used_memory' => null,
+			'free_memory' => null,
+			'num_cached_scripts' => null,
+		], $json['opcache']);
+		self::assertEquals(['type' => 'database'], $json['cache']);
+		self::assertEquals(['type' => 'database'], $json['lock']);
+	}
+
+	public function testStatsCachingWithOpcacheAndValues()
+	{
+		$request = [
+			'key' => '12345',
+		];
+		$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
+
+		$this->cache = new DatabaseCache('localhost', DI::dba());
+		$this->lock = new DatabaseLock(DI::dba());
+		PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(true);
+		PHPMockery::mock("Friendica\\Module", "opcache_get_status")->with(false)->once()->andReturn([
+			'opcache_enabled' => true,
+			'opcache_statistics' => [
+				'opcache_hit_rate' => 1,
+				'num_cached_scripts' => 2,
+			],
+			'memory_usage' => [
+				'used_memory' => 3,
+				'free_memory' => 4,
+			]
+		]);
+
+		$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
+			->run($this->httpExceptionMock, $request);
+
+		self::assertJson($response->getBody());
+		self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders());
+
+		$json = json_decode($response->getBody(), true);
+
+		self::assertEquals([
+			'enabled' => true,
+			'hit_rate' => 1,
+			'used_memory' => 3,
+			'free_memory' => 4,
+			'num_cached_scripts' => 2,
+		], $json['opcache']);
+		self::assertEquals(['type' => 'database'], $json['cache']);
+		self::assertEquals(['type' => 'database'], $json['lock']);
+	}
+}

From c92d23909151b3e34b9fa2ef712204fdb7378a73 Mon Sep 17 00:00:00 2001
From: Philipp 
Date: Sun, 27 Apr 2025 01:58:03 +0200
Subject: [PATCH 10/22] fix test

---
 tests/src/Module/StatsCachingTest.php | 2 --
 1 file changed, 2 deletions(-)

diff --git a/tests/src/Module/StatsCachingTest.php b/tests/src/Module/StatsCachingTest.php
index 08d4b8fdb6..78554929e2 100644
--- a/tests/src/Module/StatsCachingTest.php
+++ b/tests/src/Module/StatsCachingTest.php
@@ -149,8 +149,6 @@ class StatsCachingTest extends FixtureTestCase
 
 		$json = json_decode($response->getBody(), true);
 
-		print_r($json);
-
 		self::assertEquals([
 			'enabled' => false,
 			'hit_rate' => null,

From 03cfa2d066a2201334462cd446f72c3b8170dda3 Mon Sep 17 00:00:00 2001
From: Philipp 
Date: Sun, 27 Apr 2025 01:58:20 +0200
Subject: [PATCH 11/22] Add doc

---
 doc/stats.md | 35 +++++++++++++++++++++++++++++++++++
 doc/tools.md | 12 ------------
 2 files changed, 35 insertions(+), 12 deletions(-)
 create mode 100644 doc/stats.md

diff --git a/doc/stats.md b/doc/stats.md
new file mode 100644
index 0000000000..1b6a2dfd2a
--- /dev/null
+++ b/doc/stats.md
@@ -0,0 +1,35 @@
+Monitoring
+===========
+
+* [Home](help)
+
+## Endpoints
+
+Currently, there are two endpoints for statistics available
+
+-	`/stats` Returns some basic statistics of the current node
+-	`/stats/caching` Returns statistics of cache or lock instances, which are used for the currend node
+
+### `/stats`
+
+The statistics contain data about the worker performance, the last cron call, number of reports, inbound and outbound packets, posts and comments.
+
+### `/stats/caching`
+
+The statistics contain data about the opcache, the used caching (like memory usage, hits/misses, entries, ...) and the used lock (including the cache data)
+
+## Configuration
+
+Please define 'stats_key' in your local.config.php in the 'system' section to be able to access the statistics page at /stats?key=your-defined-stats_key
+
+## 3rd Party monitoring tools
+
+### Zabbix
+
+To monitor the health status of your Friendica installation, you can use for example a tool like Zabbix.
+
+### Prometheus
+
+To use [prometheus](https://prometheus.io) for gathering metrics, use the [Friendica exporter](https://git.friendi.ca/friendica/friendica-exporter).
+
+You can find the installation instructions here: https://git.friendi.ca/friendica/friendica-exporter#installation
diff --git a/doc/tools.md b/doc/tools.md
index 2a273e3650..fac1f4b392 100644
--- a/doc/tools.md
+++ b/doc/tools.md
@@ -78,15 +78,3 @@ The following will compress */var/log/friendica* (assuming this is the location
 		daily
 		rotate 2
 	}
-
-### Zabbix
-
-To monitor the health status of your Friendica installation, you can use for example a tool like Zabbix. Please define 'stats_key' in your local.config.php in the 'system' section to be able to access the statistics page at /stats?key=your-defined-stats_key
-
-The statistics contain data about the worker performance, the last cron call, number of reports, inbound and outbound packets, posts and comments.
-
-### Prometheus
-
-To use [prometheus](https://prometheus.io) for gathering metrics, use the [Friendica exporter](https://git.friendi.ca/friendica/friendica-exporter).
-
-You can find the installation instructions here: https://git.friendi.ca/friendica/friendica-exporter#installation

From 7b39b3b9c07c92e1713c29c0cbeac3f4ca9954ba Mon Sep 17 00:00:00 2001
From: Philipp 
Date: Sun, 27 Apr 2025 02:08:01 +0200
Subject: [PATCH 12/22] Mak PHPCS happy

---
 tests/LockTestCase.php                        |  3 --
 tests/src/Core/Lock/APCuCacheLockTest.php     |  2 +-
 tests/src/Core/Lock/ArrayCacheLockTest.php    |  2 +-
 tests/src/Core/Lock/MemcacheCacheLockTest.php |  2 +-
 .../src/Core/Lock/MemcachedCacheLockTest.php  |  3 +-
 tests/src/Core/Lock/RedisCacheLockTest.php    |  3 +-
 tests/src/Core/Lock/SemaphoreLockTest.php     |  2 +-
 tests/src/Module/StatsCachingTest.php         | 38 +++++++++----------
 8 files changed, 25 insertions(+), 30 deletions(-)

diff --git a/tests/LockTestCase.php b/tests/LockTestCase.php
index 1b80e575ef..92abf0ca7b 100644
--- a/tests/LockTestCase.php
+++ b/tests/LockTestCase.php
@@ -7,10 +7,7 @@
 
 namespace Friendica\Test;
 
-use Friendica\Core\Cache\Capability\ICanCache;
-use Friendica\Core\Cache\Capability\ICanCacheInMemory;
 use Friendica\Core\Lock\Capability\ICanLock;
-use Friendica\Core\Lock\Type\CacheLock;
 
 abstract class LockTestCase extends MockedTestCase
 {
diff --git a/tests/src/Core/Lock/APCuCacheLockTest.php b/tests/src/Core/Lock/APCuCacheLockTest.php
index 42a1fc72d5..3b6c7904b4 100644
--- a/tests/src/Core/Lock/APCuCacheLockTest.php
+++ b/tests/src/Core/Lock/APCuCacheLockTest.php
@@ -28,7 +28,7 @@ class APCuCacheLockTest extends CacheLockTestCase
 		}
 
 		$this->cache = new APCuCache('localhost');
-		$this->lock = new CacheLock($this->cache);
+		$this->lock  = new CacheLock($this->cache);
 
 		parent::setUp();
 	}
diff --git a/tests/src/Core/Lock/ArrayCacheLockTest.php b/tests/src/Core/Lock/ArrayCacheLockTest.php
index 2aac8e8293..07cd88dd1c 100644
--- a/tests/src/Core/Lock/ArrayCacheLockTest.php
+++ b/tests/src/Core/Lock/ArrayCacheLockTest.php
@@ -20,7 +20,7 @@ class ArrayCacheLockTest extends CacheLockTestCase
 	protected function setUp(): void
 	{
 		$this->cache = new ArrayCache('localhost');
-		$this->lock = new CacheLock($this->cache);
+		$this->lock  = new CacheLock($this->cache);
 
 		parent::setUp();
 	}
diff --git a/tests/src/Core/Lock/MemcacheCacheLockTest.php b/tests/src/Core/Lock/MemcacheCacheLockTest.php
index c1dec663f1..8915e6d37c 100644
--- a/tests/src/Core/Lock/MemcacheCacheLockTest.php
+++ b/tests/src/Core/Lock/MemcacheCacheLockTest.php
@@ -42,7 +42,7 @@ class MemcacheCacheLockTest extends CacheLockTestCase
 
 		try {
 			$this->cache = new MemcacheCache($host, $configMock);
-			$this->lock = new CacheLock($this->cache);
+			$this->lock  = new CacheLock($this->cache);
 		} catch (Exception $e) {
 			static::markTestSkipped('Memcache is not available');
 		}
diff --git a/tests/src/Core/Lock/MemcachedCacheLockTest.php b/tests/src/Core/Lock/MemcachedCacheLockTest.php
index 773e664108..522f60c64a 100644
--- a/tests/src/Core/Lock/MemcachedCacheLockTest.php
+++ b/tests/src/Core/Lock/MemcachedCacheLockTest.php
@@ -13,7 +13,6 @@ use Friendica\Core\Cache\Type\MemcachedCache;
 use Friendica\Core\Config\Capability\IManageConfigValues;
 use Friendica\Core\Lock\Type\CacheLock;
 use Friendica\Test\CacheLockTestCase;
-use Friendica\Test\LockTestCase;
 use Mockery;
 use Psr\Log\NullLogger;
 
@@ -42,7 +41,7 @@ class MemcachedCacheLockTest extends CacheLockTestCase
 
 		try {
 			$this->cache = new MemcachedCache($host, $configMock, $logger);
-			$this->lock = new CacheLock($this->cache);
+			$this->lock  = new CacheLock($this->cache);
 		} catch (Exception $e) {
 			static::markTestSkipped('Memcached is not available');
 		}
diff --git a/tests/src/Core/Lock/RedisCacheLockTest.php b/tests/src/Core/Lock/RedisCacheLockTest.php
index 3cb4fba436..1136b80c4b 100644
--- a/tests/src/Core/Lock/RedisCacheLockTest.php
+++ b/tests/src/Core/Lock/RedisCacheLockTest.php
@@ -13,7 +13,6 @@ use Friendica\Core\Cache\Type\RedisCache;
 use Friendica\Core\Config\Capability\IManageConfigValues;
 use Friendica\Core\Lock\Type\CacheLock;
 use Friendica\Test\CacheLockTestCase;
-use Friendica\Test\LockTestCase;
 use Mockery;
 
 /**
@@ -49,7 +48,7 @@ class RedisCacheLockTest extends CacheLockTestCase
 
 		try {
 			$this->cache = new RedisCache($host, $configMock);
-			$this->lock = new CacheLock($this->cache);
+			$this->lock  = new CacheLock($this->cache);
 		} catch (Exception $e) {
 			static::markTestSkipped('Redis is not available. Error: ' . $e->getMessage());
 		}
diff --git a/tests/src/Core/Lock/SemaphoreLockTest.php b/tests/src/Core/Lock/SemaphoreLockTest.php
index 1ddb2dade7..30152ac427 100644
--- a/tests/src/Core/Lock/SemaphoreLockTest.php
+++ b/tests/src/Core/Lock/SemaphoreLockTest.php
@@ -32,7 +32,7 @@ class SemaphoreLockTest extends LockTestCase
 		$dice->shouldReceive('create')->with(App::class)->andReturn($app);
 
 		$configCache = new Cache(['system' => ['temppath' => '/tmp']]);
-		$configMock = new ReadOnlyFileConfig($configCache);
+		$configMock  = new ReadOnlyFileConfig($configCache);
 		$dice->shouldReceive('create')->with(IManageConfigValues::class)->andReturn($configMock);
 
 		// @todo Because "get_temppath()" is using static methods, we have to initialize the BaseObject
diff --git a/tests/src/Module/StatsCachingTest.php b/tests/src/Module/StatsCachingTest.php
index 78554929e2..58226769a9 100644
--- a/tests/src/Module/StatsCachingTest.php
+++ b/tests/src/Module/StatsCachingTest.php
@@ -38,9 +38,9 @@ class StatsCachingTest extends FixtureTestCase
 		parent::setUp();
 
 		$this->httpExceptionMock = \Mockery::mock(HTTPException::class);
-		$this->config = \Mockery::mock(IManageConfigValues::class);
-		$this->cache = new ArrayCache('localhost');
-		$this->lock = new CacheLock($this->cache);
+		$this->config            = \Mockery::mock(IManageConfigValues::class);
+		$this->cache             = new ArrayCache('localhost');
+		$this->lock              = new CacheLock($this->cache);
 	}
 
 	public function testStatsCachingNotAllowed()
@@ -72,11 +72,11 @@ class StatsCachingTest extends FixtureTestCase
 		$json = json_decode($response->getBody(), true);
 
 		self::assertEquals([
-			'type' => 'array',
+			'type'  => 'array',
 			'stats' => [],
 		], $json['cache']);
 		self::assertEquals([
-			'type' => 'array',
+			'type'  => 'array',
 			'stats' => [],
 		], $json['lock']);
 	}
@@ -89,7 +89,7 @@ class StatsCachingTest extends FixtureTestCase
 		$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
 
 		$this->cache = new DatabaseCache('localhost', DI::dba());
-		$this->lock = new DatabaseLock(DI::dba());
+		$this->lock  = new DatabaseLock(DI::dba());
 		PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false);
 
 		$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
@@ -113,7 +113,7 @@ class StatsCachingTest extends FixtureTestCase
 		$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
 
 		$this->cache = new DatabaseCache('localhost', DI::dba());
-		$this->lock = new DatabaseLock(DI::dba());
+		$this->lock  = new DatabaseLock(DI::dba());
 		PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false);
 
 		$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
@@ -137,7 +137,7 @@ class StatsCachingTest extends FixtureTestCase
 		$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
 
 		$this->cache = new DatabaseCache('localhost', DI::dba());
-		$this->lock = new DatabaseLock(DI::dba());
+		$this->lock  = new DatabaseLock(DI::dba());
 		PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(true);
 		PHPMockery::mock("Friendica\\Module", "opcache_get_status")->with(false)->once()->andReturn(false);
 
@@ -150,10 +150,10 @@ class StatsCachingTest extends FixtureTestCase
 		$json = json_decode($response->getBody(), true);
 
 		self::assertEquals([
-			'enabled' => false,
-			'hit_rate' => null,
-			'used_memory' => null,
-			'free_memory' => null,
+			'enabled'            => false,
+			'hit_rate'           => null,
+			'used_memory'        => null,
+			'free_memory'        => null,
 			'num_cached_scripts' => null,
 		], $json['opcache']);
 		self::assertEquals(['type' => 'database'], $json['cache']);
@@ -168,12 +168,12 @@ class StatsCachingTest extends FixtureTestCase
 		$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
 
 		$this->cache = new DatabaseCache('localhost', DI::dba());
-		$this->lock = new DatabaseLock(DI::dba());
+		$this->lock  = new DatabaseLock(DI::dba());
 		PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(true);
 		PHPMockery::mock("Friendica\\Module", "opcache_get_status")->with(false)->once()->andReturn([
-			'opcache_enabled' => true,
+			'opcache_enabled'    => true,
 			'opcache_statistics' => [
-				'opcache_hit_rate' => 1,
+				'opcache_hit_rate'   => 1,
 				'num_cached_scripts' => 2,
 			],
 			'memory_usage' => [
@@ -191,10 +191,10 @@ class StatsCachingTest extends FixtureTestCase
 		$json = json_decode($response->getBody(), true);
 
 		self::assertEquals([
-			'enabled' => true,
-			'hit_rate' => 1,
-			'used_memory' => 3,
-			'free_memory' => 4,
+			'enabled'            => true,
+			'hit_rate'           => 1,
+			'used_memory'        => 3,
+			'free_memory'        => 4,
 			'num_cached_scripts' => 2,
 		], $json['opcache']);
 		self::assertEquals(['type' => 'database'], $json['cache']);

From c6c6640b81cf50399bdc6743f229573ee966ba05 Mon Sep 17 00:00:00 2001
From: Philipp 
Date: Sun, 27 Apr 2025 22:08:01 +0200
Subject: [PATCH 13/22] Fixup woodpecker

---
 .woodpecker/.code_standards_check.yml | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/.woodpecker/.code_standards_check.yml b/.woodpecker/.code_standards_check.yml
index 1217ea3f0b..d147e8fc69 100644
--- a/.woodpecker/.code_standards_check.yml
+++ b/.woodpecker/.code_standards_check.yml
@@ -43,14 +43,10 @@ steps:
       - apt-get update -q
       - DEBIAN_FRONTEND=noninteractive apt-get install -q -y git
       - if [ ! -z "$${CI_COMMIT_PULL_REQUEST}" ]; then
-          git fetch --no-tags origin ${CI_COMMIT_TARGET_BRANCH};
-          CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB $(git merge-base FETCH_HEAD origin/${CI_COMMIT_TARGET_BRANCH})..${CI_COMMIT_SHA})";
+          git fetch --no-tags --unshallow origin ${CI_COMMIT_TARGET_BRANCH}:refs/remotes/origin/${CI_COMMIT_TARGET_BRANCH};
+          CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB $(git merge-base ${CI_COMMIT_SHA} origin/${CI_COMMIT_TARGET_BRANCH})..${CI_COMMIT_SHA})";
         else
           CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB ${CI_COMMIT_SHA})";
         fi
-      - if ! echo "$${CHANGED_FILES}" | grep -qE "^(\\.php-cs-fixer(\\.dist)?\\.php|composer\\.lock)$"; then
-          EXTRA_ARGS=$(printf -- '--path-mode=intersection\n--\n%s' "$${CHANGED_FILES}");
-        else
-          EXTRA_ARGS='';
-        fi
+      - EXTRA_ARGS="--path-mode=intersection -- $${CHANGED_FILES}";
       - ./bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer check --config=.php-cs-fixer.dist.php -v --diff --using-cache=no $${EXTRA_ARGS}

From 2e50e9387290c83d95e8f73a370286d6d207a8c9 Mon Sep 17 00:00:00 2001
From: Art4 
Date: Mon, 28 Apr 2025 13:32:18 +0000
Subject: [PATCH 14/22] Replace Addon class with AddonHelper in stats modules

---
 src/Module/Statistics.php | 46 ++++++++++++++++++++++++++-------------
 src/Module/Stats.php      | 32 ++++++++++++++++++++-------
 2 files changed, 55 insertions(+), 23 deletions(-)

diff --git a/src/Module/Statistics.php b/src/Module/Statistics.php
index 6a5996dac9..4a35d55d12 100644
--- a/src/Module/Statistics.php
+++ b/src/Module/Statistics.php
@@ -8,8 +8,10 @@
 namespace Friendica\Module;
 
 use Friendica\App;
+use Friendica\App\Arguments;
+use Friendica\App\BaseURL;
 use Friendica\BaseModule;
-use Friendica\Core\Addon;
+use Friendica\Core\Addon\AddonHelper;
 use Friendica\Core\Config\Capability\IManageConfigValues;
 use Friendica\Core\KeyValueStorage\Capability\IManageKeyValuePairs;
 use Friendica\Core\L10n;
@@ -23,13 +25,27 @@ class Statistics extends BaseModule
 	protected $config;
 	/** @var IManageKeyValuePairs */
 	protected $keyValue;
+	private AddonHelper $addonHelper;
 
-	public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, IManageConfigValues $config, IManageKeyValuePairs $keyValue, Response $response, array $server, array $parameters = [])
-	{
+	public function __construct(
+		L10n $l10n,
+		BaseURL $baseUrl,
+		Arguments $args,
+		LoggerInterface $logger,
+		Profiler $profiler,
+		IManageConfigValues $config,
+		IManageKeyValuePairs $keyValue,
+		AddonHelper $addonHelper,
+		Response $response,
+		array $server,
+		array $parameters = []
+	) {
 		parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
 
-		$this->config   = $config;
-		$this->keyValue = $keyValue;
+		$this->config      = $config;
+		$this->keyValue    = $keyValue;
+		$this->addonHelper = $addonHelper;
+
 		if (!$this->config->get("system", "nodeinfo")) {
 			throw new NotFoundException();
 		}
@@ -43,16 +59,16 @@ class Statistics extends BaseModule
 
 		/// @todo mark the "service" addons and load them dynamically here
 		$services = [
-			'appnet'      => Addon::isEnabled('appnet'),
-			'bluesky'     => Addon::isEnabled('bluesky'),
-			'dreamwidth'  => Addon::isEnabled('dreamwidth'),
-			'gnusocial'   => Addon::isEnabled('gnusocial'),
-			'libertree'   => Addon::isEnabled('libertree'),
-			'livejournal' => Addon::isEnabled('livejournal'),
-			'pumpio'      => Addon::isEnabled('pumpio'),
-			'twitter'     => Addon::isEnabled('twitter'),
-			'tumblr'      => Addon::isEnabled('tumblr'),
-			'wordpress'   => Addon::isEnabled('wordpress'),
+			'appnet'      => $this->addonHelper->isAddonEnabled('appnet'),
+			'bluesky'     => $this->addonHelper->isAddonEnabled('bluesky'),
+			'dreamwidth'  => $this->addonHelper->isAddonEnabled('dreamwidth'),
+			'gnusocial'   => $this->addonHelper->isAddonEnabled('gnusocial'),
+			'libertree'   => $this->addonHelper->isAddonEnabled('libertree'),
+			'livejournal' => $this->addonHelper->isAddonEnabled('livejournal'),
+			'pumpio'      => $this->addonHelper->isAddonEnabled('pumpio'),
+			'twitter'     => $this->addonHelper->isAddonEnabled('twitter'),
+			'tumblr'      => $this->addonHelper->isAddonEnabled('tumblr'),
+			'wordpress'   => $this->addonHelper->isAddonEnabled('wordpress'),
 		];
 
 		$statistics = array_merge([
diff --git a/src/Module/Stats.php b/src/Module/Stats.php
index 4bc9d95711..e59696a889 100644
--- a/src/Module/Stats.php
+++ b/src/Module/Stats.php
@@ -8,8 +8,10 @@
 namespace Friendica\Module;
 
 use Friendica\App;
+use Friendica\App\Arguments;
+use Friendica\App\BaseURL;
 use Friendica\BaseModule;
-use Friendica\Core\Addon;
+use Friendica\Core\Addon\AddonHelper;
 use Friendica\Core\Config\Capability\IManageConfigValues;
 use Friendica\Core\KeyValueStorage\Capability\IManageKeyValuePairs;
 use Friendica\Core\L10n;
@@ -40,14 +42,28 @@ class Stats extends BaseModule
 	protected $logger;
 	/** @var IManageKeyValuePairs */
 	protected $keyValue;
+	private AddonHelper $addonHelper;
 
-	public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, IManageConfigValues $config, IManageKeyValuePairs $keyValue, Database $dba, Response $response, array $server, array $parameters = [])
-	{
+	public function __construct(
+		L10n $l10n,
+		BaseURL $baseUrl,
+		Arguments $args,
+		LoggerInterface $logger,
+		Profiler $profiler,
+		IManageConfigValues $config,
+		IManageKeyValuePairs $keyValue,
+		Database $dba,
+		AddonHelper $addonHelper,
+		Response $response,
+		array $server,
+		array $parameters = []
+	) {
 		parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
 
-		$this->config   = $config;
-		$this->keyValue = $keyValue;
-		$this->dba      = $dba;
+		$this->config      = $config;
+		$this->keyValue    = $keyValue;
+		$this->dba         = $dba;
+		$this->addonHelper = $addonHelper;
 	}
 
 	protected function content(array $request = []): string
@@ -164,11 +180,11 @@ class Stats extends BaseModule
 			],
 		];
 
-		if (Addon::isEnabled('bluesky')) {
+		if ($this->addonHelper->isAddonEnabled('bluesky')) {
 			$statistics['packets']['inbound'][Protocol::BLUESKY] = intval($this->keyValue->get('stats_packets_inbound_' . Protocol::BLUESKY) ?? 0);
 			$statistics['packets']['outbound'][Protocol::BLUESKY] = intval($this->keyValue->get('stats_packets_outbound_' . Protocol::BLUESKY) ?? 0);
 		}
-		if (Addon::isEnabled('tumblr')) {
+		if ($this->addonHelper->isAddonEnabled('tumblr')) {
 			$statistics['packets']['inbound'][Protocol::TUMBLR] = intval($this->keyValue->get('stats_packets_inbound_' . Protocol::TUMBLR) ?? 0);
 			$statistics['packets']['outbound'][Protocol::TUMBLR] = intval($this->keyValue->get('stats_packets_outbound_' . Protocol::TUMBLR) ?? 0);
 		}

From 2ccb62bbd85462a1e3e4d8256d2964b23f4ddc08 Mon Sep 17 00:00:00 2001
From: Art4 
Date: Mon, 28 Apr 2025 13:57:21 +0000
Subject: [PATCH 15/22] Replace Addon class with AddonHelper in Nodeinfo Model

---
 src/Model/Nodeinfo.php | 38 ++++++++++++++++++++------------------
 1 file changed, 20 insertions(+), 18 deletions(-)

diff --git a/src/Model/Nodeinfo.php b/src/Model/Nodeinfo.php
index c4ee580e02..cdee561112 100644
--- a/src/Model/Nodeinfo.php
+++ b/src/Model/Nodeinfo.php
@@ -7,7 +7,6 @@
 
 namespace Friendica\Model;
 
-use Friendica\Core\Addon;
 use Friendica\Core\Config\Capability\IManageConfigValues;
 use Friendica\Database\DBA;
 use Friendica\DI;
@@ -26,13 +25,14 @@ class Nodeinfo
 	 */
 	public static function update()
 	{
-		$config = DI::config();
-		$logger = DI::logger();
+		$config      = DI::config();
+		$logger      = DI::logger();
+		$addonHelper = DI::addonHelper();
 
 		// If the addon 'statistics_json' is enabled then disable it and activate nodeinfo.
-		if (Addon::isEnabled('statistics_json')) {
+		if ($addonHelper->isAddonEnabled('statistics_json')) {
 			$config->set('system', 'nodeinfo', true);
-			Addon::uninstall('statistics_json');
+			$addonHelper->uninstallAddon('statistics_json');
 		}
 
 		if (empty($config->get('system', 'nodeinfo'))) {
@@ -66,14 +66,14 @@ class Nodeinfo
 	/**
 	 * Return the supported services
 	 *
-	 * @return Object with supported services
+	 * @return stdClass with supported services
 	 */
-	public static function getUsage(bool $version2 = false)
+	public static function getUsage(bool $version2 = false): stdClass
 	{
 		$config = DI::config();
 
 		$usage = new stdClass();
-		$usage->users = new \stdClass;
+		$usage->users = new stdClass;
 
 		if (!empty($config->get('system', 'nodeinfo'))) {
 			$usage->users->total = intval(DI::keyValue()->get('nodeinfo_total_users'));
@@ -97,45 +97,47 @@ class Nodeinfo
 	 */
 	public static function getServices(): array
 	{
+		$addonHelper = DI::addonHelper();
+
 		$services = [
 			'inbound'  => [],
 			'outbound' => [],
 		];
 
-		if (Addon::isEnabled('bluesky')) {
+		if ($addonHelper->isAddonEnabled('bluesky')) {
 			$services['inbound'][] = 'bluesky';
 			$services['outbound'][] = 'bluesky';
 		}
-		if (Addon::isEnabled('dwpost')) {
+		if ($addonHelper->isAddonEnabled('dwpost')) {
 			$services['outbound'][] = 'dreamwidth';
 		}
-		if (Addon::isEnabled('statusnet')) {
+		if ($addonHelper->isAddonEnabled('statusnet')) {
 			$services['inbound'][] = 'gnusocial';
 			$services['outbound'][] = 'gnusocial';
 		}
-		if (Addon::isEnabled('ijpost')) {
+		if ($addonHelper->isAddonEnabled('ijpost')) {
 			$services['outbound'][] = 'insanejournal';
 		}
-		if (Addon::isEnabled('libertree')) {
+		if ($addonHelper->isAddonEnabled('libertree')) {
 			$services['outbound'][] = 'libertree';
 		}
-		if (Addon::isEnabled('ljpost')) {
+		if ($addonHelper->isAddonEnabled('ljpost')) {
 			$services['outbound'][] = 'livejournal';
 		}
-		if (Addon::isEnabled('pumpio')) {
+		if ($addonHelper->isAddonEnabled('pumpio')) {
 			$services['inbound'][] = 'pumpio';
 			$services['outbound'][] = 'pumpio';
 		}
 
 		$services['outbound'][] = 'smtp';
 
-		if (Addon::isEnabled('tumblr')) {
+		if ($addonHelper->isAddonEnabled('tumblr')) {
 			$services['outbound'][] = 'tumblr';
 		}
-		if (Addon::isEnabled('twitter')) {
+		if ($addonHelper->isAddonEnabled('twitter')) {
 			$services['outbound'][] = 'twitter';
 		}
-		if (Addon::isEnabled('wppost')) {
+		if ($addonHelper->isAddonEnabled('wppost')) {
 			$services['outbound'][] = 'wordpress';
 		}
 

From f5eee2c3343abc65619cf2903d48b9b539f10bc2 Mon Sep 17 00:00:00 2001
From: Art4 
Date: Mon, 28 Apr 2025 14:07:20 +0000
Subject: [PATCH 16/22] Replace Addon class with AddonHelper in Post Object

---
 src/Object/Post.php | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/Object/Post.php b/src/Object/Post.php
index d4b381a115..afc3d04fa7 100644
--- a/src/Object/Post.php
+++ b/src/Object/Post.php
@@ -9,7 +9,6 @@ namespace Friendica\Object;
 
 use Friendica\Content\ContactSelector;
 use Friendica\Content\Feature;
-use Friendica\Core\Addon;
 use Friendica\Core\Protocol;
 use Friendica\Core\Renderer;
 use Friendica\DI;
@@ -1118,12 +1117,14 @@ class Post
 		$conv        = $this->getThread();
 
 		if ($conv->isWritable() && $this->isWritable()) {
+			$addonHelper = DI::addonHelper();
+
 			/*
 			 * Hmmm, code depending on the presence of a particular addon?
 			 * This should be better if done by a hook
 			 */
 			$qcomment = null;
-			if (Addon::isEnabled('qcomment')) {
+			if ($addonHelper->isAddonEnabled('qcomment')) {
 				$words    = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'qcomment', 'words');
 				$qcomment = $words ? explode("\n", $words) : [];
 			}

From 362b223c5eda15ee48854a21d003c6c25504e120 Mon Sep 17 00:00:00 2001
From: Art4 
Date: Mon, 28 Apr 2025 14:10:32 +0000
Subject: [PATCH 17/22] Replace Addon class with AddonHelper in vier theme

---
 view/theme/vier/theme.php | 31 ++++++++++++++++---------------
 1 file changed, 16 insertions(+), 15 deletions(-)

diff --git a/view/theme/vier/theme.php b/view/theme/vier/theme.php
index b985449166..dd178c87ce 100644
--- a/view/theme/vier/theme.php
+++ b/view/theme/vier/theme.php
@@ -14,9 +14,8 @@
  * Description: "Vier" is a very compact and modern theme. It uses the font awesome font library: http://fortawesome.github.com/Font-Awesome/
  */
 
-use Friendica\App;
+use Friendica\App\Mode;
 use Friendica\Content\GroupManager;
-use Friendica\Core\Addon;
 use Friendica\Core\Renderer;
 use Friendica\Core\Search;
 use Friendica\Database\DBA;
@@ -35,7 +34,7 @@ function vier_init()
 	$args = DI::args();
 
 	if (
-		DI::mode()->has(App\Mode::MAINTENANCEDISABLED)
+		DI::mode()->has(Mode::MAINTENANCEDISABLED)
 		&& (
 			$args->get(0) === 'profile' && $args->get(1) === (DI::userSession()->getLocalUserNickname() ?? '')
 			|| $args->get(0) === 'network' && DI::userSession()->getLocalUserId()
@@ -244,55 +243,57 @@ function vier_community_info()
 
 	// connectable services
 	if ($show_services) {
+		$addonHelper = DI::addonHelper();
+
 		/// @TODO This whole thing is hard-coded, better rewrite to Intercepting Filter Pattern (future-todo)
 		$r = [];
 
-		if (Addon::isEnabled("buffer")) {
+		if ($addonHelper->isAddonEnabled("buffer")) {
 			$r[] = ["photo" => "images/buffer.png", "name" => "Buffer"];
 		}
 
-		if (Addon::isEnabled("blogger")) {
+		if ($addonHelper->isAddonEnabled("blogger")) {
 			$r[] = ["photo" => "images/blogger.png", "name" => "Blogger"];
 		}
 
-		if (Addon::isEnabled("dwpost")) {
+		if ($addonHelper->isAddonEnabled("dwpost")) {
 			$r[] = ["photo" => "images/dreamwidth.png", "name" => "Dreamwidth"];
 		}
 
-		if (Addon::isEnabled("ifttt")) {
+		if ($addonHelper->isAddonEnabled("ifttt")) {
 			$r[] = ["photo" => "addon/ifttt/ifttt.png", "name" => "IFTTT"];
 		}
 
-		if (Addon::isEnabled("statusnet")) {
+		if ($addonHelper->isAddonEnabled("statusnet")) {
 			$r[] = ["photo" => "images/gnusocial.png", "name" => "GNU Social"];
 		}
 
 		/// @TODO old-lost code (and below)?
-		//if (Addon::isEnabled("ijpost")) {
+		//if ($addonHelper->isAddonEnabled("ijpost")) {
 		//	$r[] = array("photo" => "images/", "name" => "");
 		//}
 
-		if (Addon::isEnabled("libertree")) {
+		if ($addonHelper->isAddonEnabled("libertree")) {
 			$r[] = ["photo" => "images/libertree.png", "name" => "Libertree"];
 		}
 
-		//if (Addon::isEnabled("ljpost")) {
+		//if ($addonHelper->isAddonEnabled("ljpost")) {
 		//	$r[] = array("photo" => "images/", "name" => "");
 		//}
 
-		if (Addon::isEnabled("pumpio")) {
+		if ($addonHelper->isAddonEnabled("pumpio")) {
 			$r[] = ["photo" => "images/pumpio.png", "name" => "pump.io"];
 		}
 
-		if (Addon::isEnabled("tumblr")) {
+		if ($addonHelper->isAddonEnabled("tumblr")) {
 			$r[] = ["photo" => "images/tumblr.png", "name" => "Tumblr"];
 		}
 
-		if (Addon::isEnabled("twitter")) {
+		if ($addonHelper->isAddonEnabled("twitter")) {
 			$r[] = ["photo" => "images/twitter.png", "name" => "Twitter"];
 		}
 
-		if (Addon::isEnabled("wppost")) {
+		if ($addonHelper->isAddonEnabled("wppost")) {
 			$r[] = ["photo" => "images/wordpress.png", "name" => "Wordpress"];
 		}
 

From baaec75bfc613af4517433b2dc6c384683148045 Mon Sep 17 00:00:00 2001
From: Art4 
Date: Mon, 28 Apr 2025 14:12:27 +0000
Subject: [PATCH 18/22] fix code style

---
 src/Model/Nodeinfo.php    | 23 +++++++-------
 src/Module/Statistics.php |  3 +-
 src/Module/Stats.php      | 22 ++++++-------
 view/theme/vier/theme.php | 66 +++++++++++++++++++--------------------
 4 files changed, 56 insertions(+), 58 deletions(-)

diff --git a/src/Model/Nodeinfo.php b/src/Model/Nodeinfo.php
index cdee561112..0d3a88ad8a 100644
--- a/src/Model/Nodeinfo.php
+++ b/src/Model/Nodeinfo.php
@@ -10,7 +10,6 @@ namespace Friendica\Model;
 use Friendica\Core\Config\Capability\IManageConfigValues;
 use Friendica\Database\DBA;
 use Friendica\DI;
-use Friendica\Model\Item;
 use stdClass;
 
 /**
@@ -50,12 +49,12 @@ class Nodeinfo
 
 		$logger->info('user statistics - done', $userStats);
 
-		$posts = DBA::count('post-thread', ["`uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE NOT `deleted` AND `origin`)"]);
+		$posts    = DBA::count('post-thread', ["`uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE NOT `deleted` AND `origin`)"]);
 		$comments = DBA::count('post', ["NOT `deleted` AND `gravity` = ? AND `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `origin`)", Item::GRAVITY_COMMENT]);
 		DI::keyValue()->set('nodeinfo_local_posts', $posts);
 		DI::keyValue()->set('nodeinfo_local_comments', $comments);
 
-		$posts = DBA::count('post', ['deleted' => false, 'gravity' => Item::GRAVITY_COMMENT]);
+		$posts    = DBA::count('post', ['deleted' => false, 'gravity' => Item::GRAVITY_COMMENT]);
 		$comments = DBA::count('post', ['deleted' => false, 'gravity' => Item::GRAVITY_COMMENT]);
 		DI::keyValue()->set('nodeinfo_total_posts', $posts);
 		DI::keyValue()->set('nodeinfo_total_comments', $comments);
@@ -72,15 +71,15 @@ class Nodeinfo
 	{
 		$config = DI::config();
 
-		$usage = new stdClass();
-		$usage->users = new stdClass;
+		$usage        = new stdClass();
+		$usage->users = new stdClass();
 
 		if (!empty($config->get('system', 'nodeinfo'))) {
-			$usage->users->total = intval(DI::keyValue()->get('nodeinfo_total_users'));
+			$usage->users->total          = intval(DI::keyValue()->get('nodeinfo_total_users'));
 			$usage->users->activeHalfyear = intval(DI::keyValue()->get('nodeinfo_active_users_halfyear'));
-			$usage->users->activeMonth = intval(DI::keyValue()->get('nodeinfo_active_users_monthly'));
-			$usage->localPosts = intval(DI::keyValue()->get('nodeinfo_local_posts'));
-			$usage->localComments = intval(DI::keyValue()->get('nodeinfo_local_comments'));
+			$usage->users->activeMonth    = intval(DI::keyValue()->get('nodeinfo_active_users_monthly'));
+			$usage->localPosts            = intval(DI::keyValue()->get('nodeinfo_local_posts'));
+			$usage->localComments         = intval(DI::keyValue()->get('nodeinfo_local_comments'));
 
 			if ($version2) {
 				$usage->users->activeWeek = intval(DI::keyValue()->get('nodeinfo_active_users_weekly'));
@@ -105,14 +104,14 @@ class Nodeinfo
 		];
 
 		if ($addonHelper->isAddonEnabled('bluesky')) {
-			$services['inbound'][] = 'bluesky';
+			$services['inbound'][]  = 'bluesky';
 			$services['outbound'][] = 'bluesky';
 		}
 		if ($addonHelper->isAddonEnabled('dwpost')) {
 			$services['outbound'][] = 'dreamwidth';
 		}
 		if ($addonHelper->isAddonEnabled('statusnet')) {
-			$services['inbound'][] = 'gnusocial';
+			$services['inbound'][]  = 'gnusocial';
 			$services['outbound'][] = 'gnusocial';
 		}
 		if ($addonHelper->isAddonEnabled('ijpost')) {
@@ -125,7 +124,7 @@ class Nodeinfo
 			$services['outbound'][] = 'livejournal';
 		}
 		if ($addonHelper->isAddonEnabled('pumpio')) {
-			$services['inbound'][] = 'pumpio';
+			$services['inbound'][]  = 'pumpio';
 			$services['outbound'][] = 'pumpio';
 		}
 
diff --git a/src/Module/Statistics.php b/src/Module/Statistics.php
index 4a35d55d12..6540730e01 100644
--- a/src/Module/Statistics.php
+++ b/src/Module/Statistics.php
@@ -53,8 +53,7 @@ class Statistics extends BaseModule
 
 	protected function rawContent(array $request = [])
 	{
-		$registration_open =
-			Register::getPolicy() !== Register::CLOSED
+		$registration_open = Register::getPolicy() !== Register::CLOSED
 			&& !$this->config->get('config', 'invitation_only');
 
 		/// @todo mark the "service" addons and load them dynamically here
diff --git a/src/Module/Stats.php b/src/Module/Stats.php
index e59696a889..c8122177eb 100644
--- a/src/Module/Stats.php
+++ b/src/Module/Stats.php
@@ -101,14 +101,14 @@ class Stats extends BaseModule
 					'datetime'  => DateTimeFormat::utc($this->keyValue->get('last_worker_execution'), DateTimeFormat::JSON),
 					'timestamp' => strtotime($this->keyValue->get('last_worker_execution')),
 				],
-				'jpm'           => [
+				'jpm' => [
 					1 => $this->dba->count('workerqueue', ["`done` AND `executed` > ?", DateTimeFormat::utc('now - 1 minute')]),
 					3 => round($this->dba->count('workerqueue', ["`done` AND `executed` > ?", DateTimeFormat::utc('now - 3 minute')]) / 3),
 					5 => round($this->dba->count('workerqueue', ["`done` AND `executed` > ?", DateTimeFormat::utc('now - 5 minute')]) / 5),
 				],
-				'active'        => [],
-				'deferred'      => [],
-				'total'         => [],
+				'active'   => [],
+				'deferred' => [],
+				'total'    => [],
 			],
 			'jetstream' => [
 				'drift'     => intval($this->keyValue->get('jetstream_drift')),
@@ -161,14 +161,14 @@ class Stats extends BaseModule
 				'closed' => $this->dba->count('report', ['status' => Report::STATUS_CLOSED]),
 			],
 			'update' => [
-				'available' 		=> Update::isAvailable(),
+				'available'         => Update::isAvailable(),
 				'available_version' => Update::getAvailableVersion(),
 				'status'            => Update::getStatus(),
-				'db_status'			=> DBStructure::getUpdateStatus(),
+				'db_status'         => DBStructure::getUpdateStatus(),
 			],
 			'server' => [
-				'version'  => App::VERSION,
-				'php'      => [
+				'version' => App::VERSION,
+				'php'     => [
 					'version'             => phpversion(),
 					'upload_max_filesize' => ini_get('upload_max_filesize'),
 					'post_max_size'       => ini_get('post_max_size'),
@@ -181,11 +181,11 @@ class Stats extends BaseModule
 		];
 
 		if ($this->addonHelper->isAddonEnabled('bluesky')) {
-			$statistics['packets']['inbound'][Protocol::BLUESKY] = intval($this->keyValue->get('stats_packets_inbound_' . Protocol::BLUESKY) ?? 0);
+			$statistics['packets']['inbound'][Protocol::BLUESKY]  = intval($this->keyValue->get('stats_packets_inbound_' . Protocol::BLUESKY) ?? 0);
 			$statistics['packets']['outbound'][Protocol::BLUESKY] = intval($this->keyValue->get('stats_packets_outbound_' . Protocol::BLUESKY) ?? 0);
 		}
 		if ($this->addonHelper->isAddonEnabled('tumblr')) {
-			$statistics['packets']['inbound'][Protocol::TUMBLR] = intval($this->keyValue->get('stats_packets_inbound_' . Protocol::TUMBLR) ?? 0);
+			$statistics['packets']['inbound'][Protocol::TUMBLR]  = intval($this->keyValue->get('stats_packets_inbound_' . Protocol::TUMBLR) ?? 0);
 			$statistics['packets']['outbound'][Protocol::TUMBLR] = intval($this->keyValue->get('stats_packets_outbound_' . Protocol::TUMBLR) ?? 0);
 		}
 
@@ -218,7 +218,7 @@ class Stats extends BaseModule
 
 		$jobs = $this->dba->p("SELECT COUNT(*) AS `entries`, `priority` FROM `workerqueue` WHERE NOT `done` AND `retrial` = ? GROUP BY `priority`", 0);
 		while ($entry = $this->dba->fetch($jobs)) {
-			$running  = $this->dba->count('workerqueue-view', ['priority' => $entry['priority']]);
+			$running = $this->dba->count('workerqueue-view', ['priority' => $entry['priority']]);
 			$statistics['worker']['active']['total'] += $running;
 			$statistics['worker']['active'][$entry['priority']] = $running;
 			$statistics['worker']['total']['total'] += $entry['entries'];
diff --git a/view/theme/vier/theme.php b/view/theme/vier/theme.php
index dd178c87ce..c70f55ecc1 100644
--- a/view/theme/vier/theme.php
+++ b/view/theme/vier/theme.php
@@ -92,7 +92,7 @@ EOT;
 	// Hide the left menu bar
 	/// @TODO maybe move this static array out where it should belong?
 	if (empty(DI::page()['aside']) && in_array($args->get(0), ["community", "calendar", "help", "delegation", "notifications",
-			"probe", "webfinger", "login", "invite", "credits"])) {
+		"probe", "webfinger", "login", "invite", "credits"])) {
 		DI::page()['htmlhead'] .= "";
 	}
 }
@@ -116,12 +116,12 @@ function get_vier_config($key, $default = false, $admin = false)
 
 function vier_community_info()
 {
-	$show_pages      = get_vier_config("show_pages", 1);
-	$show_profiles   = get_vier_config("show_profiles", 1);
-	$show_helpers    = get_vier_config("show_helpers", 1);
-	$show_services   = get_vier_config("show_services", 1);
-	$show_friends    = get_vier_config("show_friends", 1);
-	$show_lastusers  = get_vier_config("show_lastusers", 1);
+	$show_pages     = get_vier_config("show_pages", 1);
+	$show_profiles  = get_vier_config("show_profiles", 1);
+	$show_helpers   = get_vier_config("show_helpers", 1);
+	$show_services  = get_vier_config("show_services", 1);
+	$show_friends   = get_vier_config("show_friends", 1);
+	$show_lastusers = get_vier_config("show_lastusers", 1);
 
 	// get_baseurl
 	$aside['$url'] = $url = (string)DI::baseUrl();
@@ -137,10 +137,10 @@ function vier_community_info()
 
 			foreach ($contacts as $contact) {
 				$entry = Renderer::replaceMacros($tpl, [
-					'$id' => $contact['id'],
+					'$id'           => $contact['id'],
 					'$profile_link' => 'contact/follow?url=' . urlencode($contact['url']),
-					'$photo' => Contact::getMicro($contact),
-					'$alt_text' => $contact['name'],
+					'$photo'        => Contact::getMicro($contact),
+					'$alt_text'     => $contact['name'],
 				]);
 				$aside['$community_profiles_items'][] = $entry;
 			}
@@ -164,11 +164,11 @@ function vier_community_info()
 
 			foreach ($profiles as $profile) {
 				$profile_link = 'profile/' . ((strlen($profile['nickname'])) ? $profile['nickname'] : $profile['uid']);
-				$entry = Renderer::replaceMacros($tpl, [
-					'$id' => $profile['id'],
+				$entry        = Renderer::replaceMacros($tpl, [
+					'$id'           => $profile['id'],
 					'$profile_link' => $profile_link,
-					'$photo' => DI::baseUrl()->remove($profile['thumb']),
-					'$alt_text' => $profile['name']]);
+					'$photo'        => DI::baseUrl()->remove($profile['thumb']),
+					'$alt_text'     => $profile['name']]);
 				$aside['$lastusers_items'][] = $entry;
 			}
 		}
@@ -176,18 +176,18 @@ function vier_community_info()
 
 	//right_aside FIND FRIENDS
 	if ($show_friends && DI::userSession()->getLocalUserId()) {
-		$nv = [];
-		$nv['findpeople'] = DI::l10n()->t('Find People');
-		$nv['desc'] = DI::l10n()->t('Enter name or interest');
-		$nv['label'] = DI::l10n()->t('Connect/Follow');
-		$nv['hint'] = DI::l10n()->t('Examples: Robert Morgenstein, Fishing');
-		$nv['findthem'] = DI::l10n()->t('Find');
-		$nv['suggest'] = DI::l10n()->t('Friend Suggestions');
-		$nv['similar'] = DI::l10n()->t('Similar Interests');
-		$nv['random'] = DI::l10n()->t('Random Profile');
-		$nv['inv'] = DI::l10n()->t('Invite Friends');
-		$nv['directory'] = DI::l10n()->t('Global Directory');
-		$nv['global_dir'] = Search::getGlobalDirectory();
+		$nv                    = [];
+		$nv['findpeople']      = DI::l10n()->t('Find People');
+		$nv['desc']            = DI::l10n()->t('Enter name or interest');
+		$nv['label']           = DI::l10n()->t('Connect/Follow');
+		$nv['hint']            = DI::l10n()->t('Examples: Robert Morgenstein, Fishing');
+		$nv['findthem']        = DI::l10n()->t('Find');
+		$nv['suggest']         = DI::l10n()->t('Friend Suggestions');
+		$nv['similar']         = DI::l10n()->t('Similar Interests');
+		$nv['random']          = DI::l10n()->t('Random Profile');
+		$nv['inv']             = DI::l10n()->t('Invite Friends');
+		$nv['directory']       = DI::l10n()->t('Global Directory');
+		$nv['global_dir']      = Search::getGlobalDirectory();
 		$nv['local_directory'] = DI::l10n()->t('Local Directory');
 
 		$aside['$nv'] = $nv;
@@ -223,14 +223,14 @@ function vier_community_info()
 		$tpl = Renderer::getMarkupTemplate('ch_helpers.tpl');
 
 		if ($r) {
-			$helpers = [];
+			$helpers          = [];
 			$helpers['title'] = ["", DI::l10n()->t('Help'), "", ""];
 
 			$aside['$helpers_items'] = [];
 
 			foreach ($r as $rr) {
 				$entry = Renderer::replaceMacros($tpl, [
-					'$url' => $rr['url'],
+					'$url'   => $rr['url'],
 					'$title' => $rr['name'],
 				]);
 				$aside['$helpers_items'][] = $entry;
@@ -304,14 +304,14 @@ function vier_community_info()
 		$tpl = Renderer::getMarkupTemplate('ch_connectors.tpl');
 
 		if (DBA::isResult($r)) {
-			$con_services = [];
-			$con_services['title'] = ["", DI::l10n()->t('Connect Services'), "", ""];
+			$con_services           = [];
+			$con_services['title']  = ["", DI::l10n()->t('Connect Services'), "", ""];
 			$aside['$con_services'] = $con_services;
 
 			foreach ($r as $rr) {
 				$entry = Renderer::replaceMacros($tpl, [
-					'$url' => $url,
-					'$photo' => $rr['photo'],
+					'$url'      => $url,
+					'$photo'    => $rr['photo'],
 					'$alt_text' => $rr['name'],
 				]);
 				$aside['$connector_items'][] = $entry;
@@ -321,7 +321,7 @@ function vier_community_info()
 	//end connectable services
 
 	//print right_aside
-	$tpl = Renderer::getMarkupTemplate('communityhome.tpl');
+	$tpl                      = Renderer::getMarkupTemplate('communityhome.tpl');
 	DI::page()['right_aside'] = Renderer::replaceMacros($tpl, $aside);
 }
 

From 42b411788ae1a8915585f0979e5c0473aca20a3b Mon Sep 17 00:00:00 2001
From: Marcus Funch 
Date: Wed, 30 Apr 2025 20:28:29 +0200
Subject: [PATCH 19/22] Add missing translations to trending tags

---
 src/Content/Widget/TrendingTags.php     |  10 +-
 view/lang/C/messages.po                 | 778 ++++++++++++------------
 view/templates/widget/trending_tags.tpl |   8 +-
 3 files changed, 403 insertions(+), 393 deletions(-)

diff --git a/src/Content/Widget/TrendingTags.php b/src/Content/Widget/TrendingTags.php
index a0cb25c66f..a7590eb20b 100644
--- a/src/Content/Widget/TrendingTags.php
+++ b/src/Content/Widget/TrendingTags.php
@@ -35,10 +35,12 @@ class TrendingTags
 		}
 
 		$tpl = Renderer::getMarkupTemplate('widget/trending_tags.tpl');
-		$o = Renderer::replaceMacros($tpl, [
-			'$title' => DI::l10n()->tt('Trending Tags (last %d hour)', 'Trending Tags (last %d hours)', $period),
-			'$more'  => DI::l10n()->t('More Trending Tags'),
-			'$tags'  => $tags,
+		$o   = Renderer::replaceMacros($tpl, [
+			'$title'    => DI::l10n()->tt('Trending Tags (last %d hour)', 'Trending Tags (last %d hours)', $period),
+			'$more'     => DI::l10n()->t('More Trending Tags'),
+			'$showmore' => DI::l10n()->t('Show More'),
+			'$showless' => DI::l10n()->t('Show Less'),
+			'$tags'     => $tags,
 		]);
 
 		return $o;
diff --git a/view/lang/C/messages.po b/view/lang/C/messages.po
index 2cfc48efbc..83b1734263 100644
--- a/view/lang/C/messages.po
+++ b/view/lang/C/messages.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: 2025.02-dev\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-02-11 07:41+0000\n"
+"POT-Creation-Date: 2025-04-28 19:34+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME \n"
 "Language-Team: LANGUAGE \n"
@@ -18,33 +18,33 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 
 
-#: mod/item.php:87 mod/item.php:90 mod/item.php:157 mod/item.php:160
+#: mod/item.php:91 mod/item.php:94 mod/item.php:161 mod/item.php:164
 msgid "Unable to locate original post."
 msgstr ""
 
-#: mod/item.php:125
+#: mod/item.php:129
 msgid "Post updated."
 msgstr ""
 
-#: mod/item.php:190 mod/item.php:194
+#: mod/item.php:194 mod/item.php:198
 msgid "Item wasn't stored."
 msgstr ""
 
-#: mod/item.php:204
+#: mod/item.php:208
 msgid "Item couldn't be fetched."
 msgstr ""
 
-#: mod/item.php:248 mod/item.php:252
+#: mod/item.php:252 mod/item.php:256
 msgid "Empty post discarded."
 msgstr ""
 
-#: mod/item.php:423 src/Module/Admin/Themes/Details.php:31
+#: mod/item.php:431 src/Module/Admin/Themes/Details.php:31
 #: src/Module/Admin/Themes/Index.php:51 src/Module/Debug/ItemBody.php:34
 #: src/Module/Debug/ItemBody.php:42 src/Module/Item/Feed.php:66
 msgid "Item not found."
 msgstr ""
 
-#: mod/item.php:447 mod/message.php:54 mod/message.php:100 mod/notes.php:34
+#: mod/item.php:455 mod/message.php:54 mod/message.php:100 mod/notes.php:34
 #: mod/photos.php:131 mod/photos.php:623 src/Model/Event.php:506
 #: src/Module/Attach.php:40 src/Module/BaseApi.php:90
 #: src/Module/BaseNotifications.php:83 src/Module/BaseSettings.php:38
@@ -59,7 +59,7 @@ msgstr ""
 #: src/Module/Invite.php:28 src/Module/Invite.php:116
 #: src/Module/Notifications/Notification.php:62
 #: src/Module/Notifications/Notification.php:93
-#: src/Module/OStatus/Subscribe.php:54 src/Module/Post/Edit.php:67
+#: src/Module/OStatus/Subscribe.php:54 src/Module/Post/Edit.php:72
 #: src/Module/Profile/Common.php:63 src/Module/Profile/Contacts.php:66
 #: src/Module/Profile/Photos.php:81 src/Module/Profile/Schedule.php:25
 #: src/Module/Profile/Schedule.php:42 src/Module/Register.php:70
@@ -77,7 +77,7 @@ msgstr ""
 #: src/Module/Settings/UserExport.php:100
 #: src/Module/Settings/UserExport.php:199
 #: src/Module/Settings/UserExport.php:219
-#: src/Module/Settings/UserExport.php:284 src/Module/User/Delegation.php:142
+#: src/Module/Settings/UserExport.php:284 src/Module/User/Delegation.php:146
 #: src/Module/User/Import.php:71 src/Module/User/Import.php:78
 msgid "Permission denied."
 msgstr ""
@@ -273,25 +273,25 @@ msgstr ""
 msgid "Your message:"
 msgstr ""
 
-#: mod/message.php:186 mod/message.php:341 src/Content/Conversation.php:358
-#: src/Module/Post/Edit.php:122
+#: mod/message.php:186 mod/message.php:341 src/Content/Conversation.php:359
+#: src/Module/Post/Edit.php:127
 msgid "Upload photo"
 msgstr ""
 
-#: mod/message.php:187 mod/message.php:342 src/Module/Post/Edit.php:126
+#: mod/message.php:187 mod/message.php:342 src/Module/Post/Edit.php:131
 msgid "Insert web link"
 msgstr ""
 
-#: mod/message.php:188 mod/message.php:344 mod/photos.php:1252
-#: src/Content/Conversation.php:389 src/Content/Conversation.php:1565
-#: src/Module/Item/Compose.php:202 src/Module/Post/Edit.php:136
-#: src/Object/Post.php:608
+#: mod/message.php:188 mod/message.php:344 mod/photos.php:1256
+#: src/Content/Conversation.php:390 src/Content/Conversation.php:1569
+#: src/Module/Item/Compose.php:205 src/Module/Post/Edit.php:141
+#: src/Object/Post.php:614
 msgid "Please wait"
 msgstr ""
 
-#: mod/message.php:189 mod/message.php:343 mod/photos.php:654
-#: mod/photos.php:774 mod/photos.php:1051 mod/photos.php:1093
-#: mod/photos.php:1149 mod/photos.php:1229
+#: mod/message.php:189 mod/message.php:343 mod/photos.php:658
+#: mod/photos.php:778 mod/photos.php:1055 mod/photos.php:1097
+#: mod/photos.php:1153 mod/photos.php:1233
 #: src/Module/Calendar/Event/Form.php:236 src/Module/Contact/Advanced.php:118
 #: src/Module/Contact/Profile.php:376
 #: src/Module/Debug/ActivityPubConversion.php:128
@@ -299,15 +299,15 @@ msgstr ""
 #: src/Module/Debug/Probe.php:40 src/Module/Debug/WebFinger.php:37
 #: src/Module/FriendSuggest.php:132 src/Module/Install.php:219
 #: src/Module/Install.php:259 src/Module/Install.php:296
-#: src/Module/Invite.php:162 src/Module/Item/Compose.php:185
+#: src/Module/Invite.php:162 src/Module/Item/Compose.php:188
 #: src/Module/Moderation/Item/Source.php:74
 #: src/Module/Moderation/Report/Create.php:154
 #: src/Module/Moderation/Report/Create.php:169
 #: src/Module/Moderation/Report/Create.php:197
 #: src/Module/Moderation/Report/Create.php:249
 #: src/Module/Profile/Profile.php:265 src/Module/Settings/Profile/Index.php:248
-#: src/Module/Settings/Server/Action.php:65 src/Module/User/Delegation.php:177
-#: src/Object/Post.php:1149 view/theme/duepuntozero/config.php:73
+#: src/Module/Settings/Server/Action.php:65 src/Module/User/Delegation.php:181
+#: src/Object/Post.php:1158 view/theme/duepuntozero/config.php:73
 #: view/theme/frio/config.php:155 view/theme/quattro/config.php:75
 #: view/theme/vier/config.php:123
 msgid "Submit"
@@ -371,9 +371,9 @@ msgstr ""
 msgid "Personal notes are visible only by yourself."
 msgstr ""
 
-#: mod/notes.php:46 src/Content/Text/HTML.php:847
+#: mod/notes.php:46 src/Content/Text/HTML.php:855
 #: src/Module/Admin/Storage.php:128 src/Module/Filer/SaveTag.php:60
-#: src/Module/Post/Edit.php:120 src/Module/Settings/Channels.php:215
+#: src/Module/Post/Edit.php:125 src/Module/Settings/Channels.php:215
 msgid "Save"
 msgstr ""
 
@@ -399,7 +399,7 @@ msgstr ""
 msgid "Recent Photos"
 msgstr ""
 
-#: mod/photos.php:90 mod/photos.php:822 src/Module/Profile/Photos.php:375
+#: mod/photos.php:90 mod/photos.php:826 src/Module/Profile/Photos.php:375
 #: src/Module/Profile/Photos.php:395
 msgid "Upload New Photos"
 msgstr ""
@@ -448,176 +448,176 @@ msgstr ""
 msgid "No photos selected"
 msgstr ""
 
-#: mod/photos.php:670
+#: mod/photos.php:674
 #, php-format
 msgid "The maximum accepted image size is %s"
 msgstr ""
 
-#: mod/photos.php:677
+#: mod/photos.php:681
 msgid "Upload Photos"
 msgstr ""
 
-#: mod/photos.php:681 mod/photos.php:770
+#: mod/photos.php:685 mod/photos.php:774
 msgid "New album name: "
 msgstr ""
 
-#: mod/photos.php:682
+#: mod/photos.php:686
 msgid "or select existing album:"
 msgstr ""
 
-#: mod/photos.php:683
+#: mod/photos.php:687
 msgid "Do not show a status post for this upload"
 msgstr ""
 
-#: mod/photos.php:686 mod/photos.php:1047 src/Content/Conversation.php:391
-#: src/Module/Calendar/Event/Form.php:239 src/Module/Post/Edit.php:174
+#: mod/photos.php:690 mod/photos.php:1051 src/Content/Conversation.php:392
+#: src/Module/Calendar/Event/Form.php:239 src/Module/Post/Edit.php:179
 msgid "Permissions"
 msgstr ""
 
-#: mod/photos.php:751
+#: mod/photos.php:755
 msgid "Do you really want to delete this photo album and all its photos?"
 msgstr ""
 
-#: mod/photos.php:752 mod/photos.php:775
+#: mod/photos.php:756 mod/photos.php:779
 msgid "Delete Album"
 msgstr ""
 
-#: mod/photos.php:753 mod/photos.php:853 src/Content/Conversation.php:406
+#: mod/photos.php:757 mod/photos.php:857 src/Content/Conversation.php:407
 #: src/Module/Contact/Follow.php:158 src/Module/Contact/Revoke.php:92
 #: src/Module/Contact/Unfollow.php:112
 #: src/Module/Media/Attachment/Browser.php:64
-#: src/Module/Media/Photo/Browser.php:76 src/Module/Post/Edit.php:158
+#: src/Module/Media/Photo/Browser.php:76 src/Module/Post/Edit.php:163
 #: src/Module/Post/Tag/Remove.php:96 src/Module/Profile/RemoteFollow.php:120
 #: src/Module/Security/TwoFactor/SignOut.php:111
 msgid "Cancel"
 msgstr ""
 
-#: mod/photos.php:779
+#: mod/photos.php:783
 msgid "Edit Album"
 msgstr ""
 
-#: mod/photos.php:780
+#: mod/photos.php:784
 msgid "Drop Album"
 msgstr ""
 
-#: mod/photos.php:784
+#: mod/photos.php:788
 msgid "Show Newest First"
 msgstr ""
 
-#: mod/photos.php:786
+#: mod/photos.php:790
 msgid "Show Oldest First"
 msgstr ""
 
-#: mod/photos.php:807 src/Module/Profile/Photos.php:343
+#: mod/photos.php:811 src/Module/Profile/Photos.php:343
 msgid "View Photo"
 msgstr ""
 
-#: mod/photos.php:839
+#: mod/photos.php:843
 msgid "Permission denied. Access to this item may be restricted."
 msgstr ""
 
-#: mod/photos.php:841
+#: mod/photos.php:845
 msgid "Photo not available"
 msgstr ""
 
-#: mod/photos.php:851
+#: mod/photos.php:855
 msgid "Do you really want to delete this photo?"
 msgstr ""
 
-#: mod/photos.php:852 mod/photos.php:1052
+#: mod/photos.php:856 mod/photos.php:1056
 msgid "Delete Photo"
 msgstr ""
 
-#: mod/photos.php:950
+#: mod/photos.php:954
 msgid "View photo"
 msgstr ""
 
-#: mod/photos.php:952
+#: mod/photos.php:956
 msgid "Edit photo"
 msgstr ""
 
-#: mod/photos.php:953
+#: mod/photos.php:957
 msgid "Delete photo"
 msgstr ""
 
-#: mod/photos.php:954
+#: mod/photos.php:958
 msgid "Use as profile photo"
 msgstr ""
 
-#: mod/photos.php:961
+#: mod/photos.php:965
 msgid "Private Photo"
 msgstr ""
 
-#: mod/photos.php:967
+#: mod/photos.php:971
 msgid "View Full Size"
 msgstr ""
 
-#: mod/photos.php:1020
+#: mod/photos.php:1024
 msgid "Tags: "
 msgstr ""
 
-#: mod/photos.php:1023
+#: mod/photos.php:1027
 msgid "[Select tags to remove]"
 msgstr ""
 
-#: mod/photos.php:1038
+#: mod/photos.php:1042
 msgid "New album name"
 msgstr ""
 
-#: mod/photos.php:1039
+#: mod/photos.php:1043
 msgid "Caption"
 msgstr ""
 
-#: mod/photos.php:1040
+#: mod/photos.php:1044
 msgid "Add a Tag"
 msgstr ""
 
-#: mod/photos.php:1040
+#: mod/photos.php:1044
 msgid "Example: @bob, @Barbara_Jensen, @jim@example.com, #California, #camping"
 msgstr ""
 
-#: mod/photos.php:1041
+#: mod/photos.php:1045
 msgid "Do not rotate"
 msgstr ""
 
-#: mod/photos.php:1042
+#: mod/photos.php:1046
 msgid "Rotate CW (right)"
 msgstr ""
 
-#: mod/photos.php:1043
+#: mod/photos.php:1047
 msgid "Rotate CCW (left)"
 msgstr ""
 
-#: mod/photos.php:1090 mod/photos.php:1146 mod/photos.php:1226
-#: src/Module/Contact.php:599 src/Module/Item/Compose.php:184
-#: src/Object/Post.php:1146
+#: mod/photos.php:1094 mod/photos.php:1150 mod/photos.php:1230
+#: src/Module/Contact.php:599 src/Module/Item/Compose.php:187
+#: src/Object/Post.php:1155
 msgid "This is you"
 msgstr ""
 
-#: mod/photos.php:1092 mod/photos.php:1148 mod/photos.php:1228
-#: src/Module/Moderation/Reports.php:105 src/Object/Post.php:602
-#: src/Object/Post.php:1148
+#: mod/photos.php:1096 mod/photos.php:1152 mod/photos.php:1232
+#: src/Module/Moderation/Reports.php:105 src/Object/Post.php:608
+#: src/Object/Post.php:1157
 msgid "Comment"
 msgstr ""
 
-#: mod/photos.php:1094 mod/photos.php:1150 mod/photos.php:1230
-#: src/Content/Conversation.php:403 src/Module/Calendar/Event/Form.php:234
-#: src/Module/Item/Compose.php:197 src/Module/Post/Edit.php:156
-#: src/Object/Post.php:1162
+#: mod/photos.php:1098 mod/photos.php:1154 mod/photos.php:1234
+#: src/Content/Conversation.php:404 src/Module/Calendar/Event/Form.php:234
+#: src/Module/Item/Compose.php:200 src/Module/Post/Edit.php:161
+#: src/Object/Post.php:1171
 msgid "Preview"
 msgstr ""
 
-#: mod/photos.php:1095 src/Content/Conversation.php:357
-#: src/Module/Post/Edit.php:121 src/Object/Post.php:1150
+#: mod/photos.php:1099 src/Content/Conversation.php:358
+#: src/Module/Post/Edit.php:126 src/Object/Post.php:1159
 msgid "Loading..."
 msgstr ""
 
-#: mod/photos.php:1187 src/Content/Conversation.php:1487
+#: mod/photos.php:1191 src/Content/Conversation.php:1491
 #: src/Object/Post.php:260
 msgid "Select"
 msgstr ""
 
-#: mod/photos.php:1188 src/Content/Conversation.php:1488
+#: mod/photos.php:1192 src/Content/Conversation.php:1492
 #: src/Module/Moderation/Users/Active.php:92
 #: src/Module/Moderation/Users/Blocked.php:92
 #: src/Module/Moderation/Users/Index.php:100
@@ -626,23 +626,23 @@ msgstr ""
 msgid "Delete"
 msgstr ""
 
-#: mod/photos.php:1249 src/Object/Post.php:426
+#: mod/photos.php:1253 src/Object/Post.php:432
 msgid "Like"
 msgstr ""
 
-#: mod/photos.php:1250 src/Object/Post.php:426
+#: mod/photos.php:1254 src/Object/Post.php:432
 msgid "I like this (toggle)"
 msgstr ""
 
-#: mod/photos.php:1251 src/Object/Post.php:427
+#: mod/photos.php:1255 src/Object/Post.php:433
 msgid "Dislike"
 msgstr ""
 
-#: mod/photos.php:1253 src/Object/Post.php:427
+#: mod/photos.php:1257 src/Object/Post.php:433
 msgid "I don't like this (toggle)"
 msgstr ""
 
-#: mod/photos.php:1275
+#: mod/photos.php:1279
 msgid "Map"
 msgstr ""
 
@@ -752,17 +752,17 @@ msgstr ""
 msgid "Close"
 msgstr ""
 
-#: src/App/Router.php:287
+#: src/App/Router.php:294
 #, php-format
 msgid "Method not allowed for this module. Allowed method(s): %s"
 msgstr ""
 
-#: src/App/Router.php:289 src/Module/HTTPException/PageNotFound.php:35
+#: src/App/Router.php:296 src/Module/HTTPException/PageNotFound.php:35
 #: src/Module/Stats.php:56
 msgid "Page not found."
 msgstr ""
 
-#: src/App/Router.php:301
+#: src/App/Router.php:308
 msgid "You must be logged in to use addons. "
 msgstr ""
 
@@ -779,7 +779,7 @@ msgid "All contacts"
 msgstr ""
 
 #: src/BaseModule.php:440 src/Content/Conversation/Factory/Channel.php:32
-#: src/Content/Widget.php:257 src/Core/ACL.php:182 src/Module/Contact.php:394
+#: src/Content/Widget.php:257 src/Core/ACL.php:185 src/Module/Contact.php:394
 #: src/Module/Privacy/PermissionTooltip.php:150
 #: src/Module/Privacy/PermissionTooltip.php:172
 #: src/Module/Settings/Channels.php:146
@@ -1049,19 +1049,19 @@ msgstr ""
 msgid "Monthly"
 msgstr ""
 
-#: src/Content/ContactSelector.php:117
+#: src/Content/ContactSelector.php:119
 msgid "DFRN"
 msgstr ""
 
-#: src/Content/ContactSelector.php:118
+#: src/Content/ContactSelector.php:120
 msgid "OStatus"
 msgstr ""
 
-#: src/Content/ContactSelector.php:119
+#: src/Content/ContactSelector.php:121
 msgid "RSS/Atom"
 msgstr ""
 
-#: src/Content/ContactSelector.php:120
+#: src/Content/ContactSelector.php:122
 #: src/Module/Moderation/Users/Active.php:82
 #: src/Module/Moderation/Users/Blocked.php:82
 #: src/Module/Moderation/Users/Create.php:58
@@ -1072,67 +1072,67 @@ msgstr ""
 msgid "Email"
 msgstr ""
 
-#: src/Content/ContactSelector.php:121 src/Module/Debug/Babel.php:273
+#: src/Content/ContactSelector.php:123 src/Module/Debug/Babel.php:273
 msgid "Diaspora"
 msgstr ""
 
-#: src/Content/ContactSelector.php:122
+#: src/Content/ContactSelector.php:124
 msgid "Zot!"
 msgstr ""
 
-#: src/Content/ContactSelector.php:123
+#: src/Content/ContactSelector.php:125
 msgid "LinkedIn"
 msgstr ""
 
-#: src/Content/ContactSelector.php:124
+#: src/Content/ContactSelector.php:126
 msgid "XMPP/IM"
 msgstr ""
 
-#: src/Content/ContactSelector.php:125
+#: src/Content/ContactSelector.php:127
 msgid "MySpace"
 msgstr ""
 
-#: src/Content/ContactSelector.php:126
+#: src/Content/ContactSelector.php:128
 msgid "Google+"
 msgstr ""
 
-#: src/Content/ContactSelector.php:127
+#: src/Content/ContactSelector.php:129
 msgid "pump.io"
 msgstr ""
 
-#: src/Content/ContactSelector.php:128
+#: src/Content/ContactSelector.php:130
 msgid "Twitter"
 msgstr ""
 
-#: src/Content/ContactSelector.php:129
+#: src/Content/ContactSelector.php:131
 msgid "Discourse"
 msgstr ""
 
-#: src/Content/ContactSelector.php:130
+#: src/Content/ContactSelector.php:132
 msgid "Diaspora Connector"
 msgstr ""
 
-#: src/Content/ContactSelector.php:131
+#: src/Content/ContactSelector.php:133
 msgid "GNU Social Connector"
 msgstr ""
 
-#: src/Content/ContactSelector.php:132
+#: src/Content/ContactSelector.php:134
 msgid "ActivityPub"
 msgstr ""
 
-#: src/Content/ContactSelector.php:133
+#: src/Content/ContactSelector.php:135
 msgid "pnut"
 msgstr ""
 
-#: src/Content/ContactSelector.php:134
+#: src/Content/ContactSelector.php:136
 msgid "Tumblr"
 msgstr ""
 
-#: src/Content/ContactSelector.php:135
+#: src/Content/ContactSelector.php:137
 msgid "Bluesky"
 msgstr ""
 
-#: src/Content/ContactSelector.php:161
+#: src/Content/ContactSelector.php:165
 #, php-format
 msgid "%s (via %s)"
 msgstr ""
@@ -1234,8 +1234,8 @@ msgstr[1] ""
 msgid "Visible to everybody"
 msgstr ""
 
-#: src/Content/Conversation.php:327 src/Module/Item/Compose.php:196
-#: src/Object/Post.php:1161
+#: src/Content/Conversation.php:327 src/Module/Item/Compose.php:199
+#: src/Object/Post.php:1170
 msgid "Please enter a image/video/audio/webpage URL:"
 msgstr ""
 
@@ -1255,143 +1255,143 @@ msgstr ""
 msgid "Delete item(s)?"
 msgstr ""
 
-#: src/Content/Conversation.php:343 src/Module/Item/Compose.php:171
+#: src/Content/Conversation.php:344 src/Module/Item/Compose.php:174
 msgid "Created at"
 msgstr ""
 
-#: src/Content/Conversation.php:353
+#: src/Content/Conversation.php:354
 msgid "New Post"
 msgstr ""
 
-#: src/Content/Conversation.php:356
+#: src/Content/Conversation.php:357
 msgid "Share"
 msgstr ""
 
-#: src/Content/Conversation.php:359 src/Module/Post/Edit.php:123
+#: src/Content/Conversation.php:360 src/Module/Post/Edit.php:128
 msgid "upload photo"
 msgstr ""
 
-#: src/Content/Conversation.php:360 src/Module/Post/Edit.php:124
+#: src/Content/Conversation.php:361 src/Module/Post/Edit.php:129
 msgid "Attach file"
 msgstr ""
 
-#: src/Content/Conversation.php:361 src/Module/Post/Edit.php:125
+#: src/Content/Conversation.php:362 src/Module/Post/Edit.php:130
 msgid "attach file"
 msgstr ""
 
-#: src/Content/Conversation.php:362 src/Module/Item/Compose.php:186
-#: src/Module/Post/Edit.php:162 src/Object/Post.php:1151
+#: src/Content/Conversation.php:363 src/Module/Item/Compose.php:189
+#: src/Module/Post/Edit.php:167 src/Object/Post.php:1160
 msgid "Bold"
 msgstr ""
 
-#: src/Content/Conversation.php:363 src/Module/Item/Compose.php:187
-#: src/Module/Post/Edit.php:163 src/Object/Post.php:1152
+#: src/Content/Conversation.php:364 src/Module/Item/Compose.php:190
+#: src/Module/Post/Edit.php:168 src/Object/Post.php:1161
 msgid "Italic"
 msgstr ""
 
-#: src/Content/Conversation.php:364 src/Module/Item/Compose.php:188
-#: src/Module/Post/Edit.php:164 src/Object/Post.php:1153
+#: src/Content/Conversation.php:365 src/Module/Item/Compose.php:191
+#: src/Module/Post/Edit.php:169 src/Object/Post.php:1162
 msgid "Underline"
 msgstr ""
 
-#: src/Content/Conversation.php:365 src/Module/Item/Compose.php:189
-#: src/Module/Post/Edit.php:165 src/Object/Post.php:1155
+#: src/Content/Conversation.php:366 src/Module/Item/Compose.php:192
+#: src/Module/Post/Edit.php:170 src/Object/Post.php:1164
 msgid "Quote"
 msgstr ""
 
-#: src/Content/Conversation.php:366 src/Module/Item/Compose.php:190
-#: src/Module/Post/Edit.php:166 src/Object/Post.php:1156
+#: src/Content/Conversation.php:367 src/Module/Item/Compose.php:193
+#: src/Module/Post/Edit.php:171 src/Object/Post.php:1165
 msgid "Add emojis"
 msgstr ""
 
-#: src/Content/Conversation.php:367 src/Module/Item/Compose.php:191
-#: src/Object/Post.php:1154
+#: src/Content/Conversation.php:368 src/Module/Item/Compose.php:194
+#: src/Object/Post.php:1163
 msgid "Content Warning"
 msgstr ""
 
-#: src/Content/Conversation.php:368 src/Module/Item/Compose.php:192
-#: src/Module/Post/Edit.php:167 src/Object/Post.php:1157
+#: src/Content/Conversation.php:369 src/Module/Item/Compose.php:195
+#: src/Module/Post/Edit.php:172 src/Object/Post.php:1166
 msgid "Code"
 msgstr ""
 
-#: src/Content/Conversation.php:369 src/Module/Item/Compose.php:193
-#: src/Object/Post.php:1158
+#: src/Content/Conversation.php:370 src/Module/Item/Compose.php:196
+#: src/Object/Post.php:1167
 msgid "Image"
 msgstr ""
 
-#: src/Content/Conversation.php:370 src/Module/Item/Compose.php:194
-#: src/Module/Post/Edit.php:168 src/Object/Post.php:1159
+#: src/Content/Conversation.php:371 src/Module/Item/Compose.php:197
+#: src/Module/Post/Edit.php:173 src/Object/Post.php:1168
 msgid "Link"
 msgstr ""
 
-#: src/Content/Conversation.php:371 src/Module/Item/Compose.php:195
-#: src/Module/Post/Edit.php:169 src/Object/Post.php:1160
+#: src/Content/Conversation.php:372 src/Module/Item/Compose.php:198
+#: src/Module/Post/Edit.php:174 src/Object/Post.php:1169
 msgid "Link or Media"
 msgstr ""
 
-#: src/Content/Conversation.php:372
+#: src/Content/Conversation.php:373
 msgid "Video"
 msgstr ""
 
-#: src/Content/Conversation.php:373 src/Module/Item/Compose.php:198
-#: src/Module/Post/Edit.php:132
+#: src/Content/Conversation.php:374 src/Module/Item/Compose.php:201
+#: src/Module/Post/Edit.php:137
 msgid "Set your location"
 msgstr ""
 
-#: src/Content/Conversation.php:374 src/Module/Post/Edit.php:133
+#: src/Content/Conversation.php:375 src/Module/Post/Edit.php:138
 msgid "set location"
 msgstr ""
 
-#: src/Content/Conversation.php:375 src/Module/Post/Edit.php:134
+#: src/Content/Conversation.php:376 src/Module/Post/Edit.php:139
 msgid "Clear browser location"
 msgstr ""
 
-#: src/Content/Conversation.php:376 src/Module/Post/Edit.php:135
+#: src/Content/Conversation.php:377 src/Module/Post/Edit.php:140
 msgid "clear location"
 msgstr ""
 
-#: src/Content/Conversation.php:378 src/Module/Item/Compose.php:203
-#: src/Module/Post/Edit.php:148
+#: src/Content/Conversation.php:379 src/Module/Item/Compose.php:206
+#: src/Module/Post/Edit.php:153
 msgid "Set title"
 msgstr ""
 
-#: src/Content/Conversation.php:380 src/Module/Item/Compose.php:204
-#: src/Module/Post/Edit.php:150
+#: src/Content/Conversation.php:381 src/Module/Item/Compose.php:207
+#: src/Module/Post/Edit.php:155
 msgid "Categories (comma-separated list)"
 msgstr ""
 
-#: src/Content/Conversation.php:385 src/Module/Item/Compose.php:220
+#: src/Content/Conversation.php:386 src/Module/Item/Compose.php:227
 msgid "Scheduled at"
 msgstr ""
 
-#: src/Content/Conversation.php:390 src/Module/Post/Edit.php:137
+#: src/Content/Conversation.php:391 src/Module/Post/Edit.php:142
 msgid "Permission settings"
 msgstr ""
 
-#: src/Content/Conversation.php:399 src/Module/Post/Edit.php:146
+#: src/Content/Conversation.php:400 src/Module/Post/Edit.php:151
 msgid "Public post"
 msgstr ""
 
-#: src/Content/Conversation.php:413 src/Content/Widget/VCard.php:120
+#: src/Content/Conversation.php:414 src/Content/Widget/VCard.php:120
 #: src/Model/Profile.php:458 src/Module/Admin/Logs/View.php:80
-#: src/Module/Post/Edit.php:172
+#: src/Module/Post/Edit.php:177
 msgid "Message"
 msgstr ""
 
-#: src/Content/Conversation.php:414 src/Module/Post/Edit.php:173
+#: src/Content/Conversation.php:415 src/Module/Post/Edit.php:178
 #: src/Module/Settings/TwoFactor/Trusted.php:129
 msgid "Browser"
 msgstr ""
 
-#: src/Content/Conversation.php:416 src/Module/Post/Edit.php:176
+#: src/Content/Conversation.php:417 src/Module/Post/Edit.php:181
 msgid "Open Compose page"
 msgstr ""
 
-#: src/Content/Conversation.php:583
+#: src/Content/Conversation.php:587
 msgid "remove"
 msgstr ""
 
-#: src/Content/Conversation.php:587
+#: src/Content/Conversation.php:591
 msgid "Delete Selected Items"
 msgstr ""
 
@@ -1481,30 +1481,30 @@ msgstr ""
 msgid "Pushed to us"
 msgstr ""
 
-#: src/Content/Conversation.php:1507 src/Object/Post.php:247
+#: src/Content/Conversation.php:1511 src/Object/Post.php:247
 msgid "Pinned item"
 msgstr ""
 
-#: src/Content/Conversation.php:1524 src/Object/Post.php:543
-#: src/Object/Post.php:544
+#: src/Content/Conversation.php:1528 src/Object/Post.php:549
+#: src/Object/Post.php:550
 #, php-format
 msgid "View %s's profile @ %s"
 msgstr ""
 
-#: src/Content/Conversation.php:1538 src/Object/Post.php:531
+#: src/Content/Conversation.php:1542 src/Object/Post.php:537
 msgid "Categories:"
 msgstr ""
 
-#: src/Content/Conversation.php:1539 src/Object/Post.php:532
+#: src/Content/Conversation.php:1543 src/Object/Post.php:538
 msgid "Filed under:"
 msgstr ""
 
-#: src/Content/Conversation.php:1547 src/Object/Post.php:559
+#: src/Content/Conversation.php:1551 src/Object/Post.php:565
 #, php-format
 msgid "%s from %s"
 msgstr ""
 
-#: src/Content/Conversation.php:1563
+#: src/Content/Conversation.php:1567
 msgid "View in context"
 msgstr ""
 
@@ -1637,7 +1637,7 @@ msgstr ""
 msgid "Posts that mention or involve you"
 msgstr ""
 
-#: src/Content/Conversation/Factory/Network.php:28 src/Object/Post.php:397
+#: src/Content/Conversation/Factory/Network.php:28 src/Object/Post.php:403
 msgid "Starred"
 msgstr ""
 
@@ -1712,7 +1712,7 @@ msgid "Display posts that have been created by accounts of the selected circle."
 msgstr ""
 
 #: src/Content/Feature.php:127 src/Content/GroupManager.php:128
-#: src/Content/Nav.php:274 src/Content/Text/HTML.php:868
+#: src/Content/Nav.php:274 src/Content/Text/HTML.php:876
 #: src/Content/Widget.php:558 src/Model/User.php:1393
 msgid "Groups"
 msgstr ""
@@ -1837,57 +1837,57 @@ msgstr ""
 msgid "Create new group"
 msgstr ""
 
-#: src/Content/Item.php:321 src/Model/Item.php:2980
+#: src/Content/Item.php:324 src/Model/Item.php:2984
 msgid "event"
 msgstr ""
 
-#: src/Content/Item.php:324 src/Content/Item.php:334
+#: src/Content/Item.php:327 src/Content/Item.php:337
 msgid "status"
 msgstr ""
 
-#: src/Content/Item.php:330 src/Model/Item.php:2982
-#: src/Module/Post/Tag/Add.php:109
+#: src/Content/Item.php:333 src/Model/Item.php:2986
+#: src/Module/Post/Tag/Add.php:112
 msgid "photo"
 msgstr ""
 
-#: src/Content/Item.php:344 src/Module/Post/Tag/Add.php:127
+#: src/Content/Item.php:347 src/Module/Post/Tag/Add.php:130
 #, php-format
 msgid "%1$s tagged %2$s's %3$s with %4$s"
 msgstr ""
 
-#: src/Content/Item.php:418 view/theme/frio/theme.php:251
+#: src/Content/Item.php:421 view/theme/frio/theme.php:251
 msgid "Follow Thread"
 msgstr ""
 
-#: src/Content/Item.php:419 src/Model/Contact.php:1253
+#: src/Content/Item.php:422 src/Model/Contact.php:1293
 msgid "View Status"
 msgstr ""
 
-#: src/Content/Item.php:420 src/Content/Item.php:443 src/Model/Contact.php:1188
-#: src/Model/Contact.php:1244 src/Model/Contact.php:1254
+#: src/Content/Item.php:423 src/Content/Item.php:446 src/Model/Contact.php:1228
+#: src/Model/Contact.php:1284 src/Model/Contact.php:1294
 #: src/Module/Directory.php:143 src/Module/Settings/Profile/Index.php:250
 msgid "View Profile"
 msgstr ""
 
-#: src/Content/Item.php:421 src/Model/Contact.php:1255
+#: src/Content/Item.php:424 src/Model/Contact.php:1295
 msgid "View Photos"
 msgstr ""
 
-#: src/Content/Item.php:422 src/Model/Contact.php:1222
+#: src/Content/Item.php:425 src/Model/Contact.php:1262
 #: src/Model/Profile.php:443
 msgid "Network Posts"
 msgstr ""
 
-#: src/Content/Item.php:423 src/Model/Contact.php:1246
-#: src/Model/Contact.php:1257
+#: src/Content/Item.php:426 src/Model/Contact.php:1286
+#: src/Model/Contact.php:1297
 msgid "View Contact"
 msgstr ""
 
-#: src/Content/Item.php:424 src/Model/Contact.php:1258
+#: src/Content/Item.php:427 src/Model/Contact.php:1298
 msgid "Send PM"
 msgstr ""
 
-#: src/Content/Item.php:425 src/Module/Contact.php:448
+#: src/Content/Item.php:428 src/Module/Contact.php:448
 #: src/Module/Contact/Profile.php:524
 #: src/Module/Moderation/Blocklist/Contact.php:104
 #: src/Module/Moderation/Users/Active.php:93
@@ -1895,7 +1895,7 @@ msgstr ""
 msgid "Block"
 msgstr ""
 
-#: src/Content/Item.php:426 src/Module/Contact.php:449
+#: src/Content/Item.php:429 src/Module/Contact.php:449
 #: src/Module/Contact/Profile.php:532
 #: src/Module/Notifications/Introductions.php:126
 #: src/Module/Notifications/Introductions.php:199
@@ -1903,32 +1903,32 @@ msgstr ""
 msgid "Ignore"
 msgstr ""
 
-#: src/Content/Item.php:427 src/Module/Contact.php:450
+#: src/Content/Item.php:430 src/Module/Contact.php:450
 #: src/Module/Contact/Profile.php:540
 msgid "Collapse"
 msgstr ""
 
-#: src/Content/Item.php:428 src/Object/Post.php:288
+#: src/Content/Item.php:431 src/Object/Post.php:288
 #, php-format
 msgid "Ignore %s server"
 msgstr ""
 
-#: src/Content/Item.php:432 src/Module/Settings/Channels.php:188
-#: src/Module/Settings/Channels.php:209 src/Object/Post.php:503
+#: src/Content/Item.php:435 src/Module/Settings/Channels.php:188
+#: src/Module/Settings/Channels.php:209 src/Object/Post.php:509
 msgid "Languages"
 msgstr ""
 
-#: src/Content/Item.php:435 src/Object/Post.php:586
+#: src/Content/Item.php:438 src/Object/Post.php:592
 msgid "Search Text"
 msgstr ""
 
-#: src/Content/Item.php:440 src/Content/Widget.php:65
-#: src/Model/Contact.php:1247 src/Model/Contact.php:1259
+#: src/Content/Item.php:443 src/Content/Widget.php:65
+#: src/Model/Contact.php:1287 src/Model/Contact.php:1299
 #: src/Module/Contact/Follow.php:152 view/theme/vier/theme.php:183
 msgid "Connect/Follow"
 msgstr ""
 
-#: src/Content/Item.php:869
+#: src/Content/Item.php:874
 msgid "Unable to fetch user."
 msgstr ""
 
@@ -1948,7 +1948,7 @@ msgstr ""
 msgid "Clear notifications"
 msgstr ""
 
-#: src/Content/Nav.php:119 src/Content/Text/HTML.php:855
+#: src/Content/Nav.php:119 src/Content/Text/HTML.php:863
 msgid "@name, !group, #tags, content"
 msgstr ""
 
@@ -2061,7 +2061,7 @@ msgstr ""
 msgid "Addon applications, utilities, games"
 msgstr ""
 
-#: src/Content/Nav.php:265 src/Content/Text/HTML.php:853
+#: src/Content/Nav.php:265 src/Content/Text/HTML.php:861
 #: src/Module/Admin/Logs/View.php:74 src/Module/Search/Index.php:99
 msgid "Search"
 msgstr ""
@@ -2070,17 +2070,17 @@ msgstr ""
 msgid "Search site content"
 msgstr ""
 
-#: src/Content/Nav.php:268 src/Content/Text/HTML.php:862
+#: src/Content/Nav.php:268 src/Content/Text/HTML.php:870
 msgid "Full Text"
 msgstr ""
 
-#: src/Content/Nav.php:269 src/Content/Text/HTML.php:863
+#: src/Content/Nav.php:269 src/Content/Text/HTML.php:871
 #: src/Content/Widget/TagCloud.php:54
 msgid "Tags"
 msgstr ""
 
 #: src/Content/Nav.php:270 src/Content/Nav.php:325
-#: src/Content/Text/HTML.php:864 src/Module/BaseProfile.php:112
+#: src/Content/Text/HTML.php:872 src/Module/BaseProfile.php:112
 #: src/Module/BaseProfile.php:115 src/Module/Contact.php:406
 #: src/Module/Contact.php:516 view/theme/frio/theme.php:232
 msgid "Contacts"
@@ -2247,40 +2247,40 @@ msgstr ""
 msgid "%2$s %3$s"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:926 src/Model/Item.php:3787
-#: src/Model/Item.php:3793 src/Model/Item.php:3794
+#: src/Content/Text/BBCode.php:926 src/Model/Item.php:3791
+#: src/Model/Item.php:3797 src/Model/Item.php:3798
 msgid "Link to source"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:1740 src/Content/Text/HTML.php:892
+#: src/Content/Text/BBCode.php:1748 src/Content/Text/HTML.php:900
 msgid "Click to open/close"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:1795
+#: src/Content/Text/BBCode.php:1803
 msgid "$1 wrote:"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:1869 src/Content/Text/BBCode.php:1870
+#: src/Content/Text/BBCode.php:1877 src/Content/Text/BBCode.php:1878
 msgid "Encrypted content"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:2203
+#: src/Content/Text/BBCode.php:2211
 msgid "Invalid source protocol"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:2222
+#: src/Content/Text/BBCode.php:2230
 msgid "Invalid link protocol"
 msgstr ""
 
-#: src/Content/Text/HTML.php:770
+#: src/Content/Text/HTML.php:778
 msgid "Loading more entries..."
 msgstr ""
 
-#: src/Content/Text/HTML.php:771
+#: src/Content/Text/HTML.php:779
 msgid "The end"
 msgstr ""
 
-#: src/Content/Text/HTML.php:847 src/Content/Widget/VCard.php:116
+#: src/Content/Text/HTML.php:855 src/Content/Widget/VCard.php:116
 #: src/Model/Profile.php:452 src/Module/Contact/Profile.php:484
 msgid "Follow"
 msgstr ""
@@ -2399,7 +2399,7 @@ msgstr ""
 msgid "Organisations"
 msgstr ""
 
-#: src/Content/Widget.php:557 src/Model/Contact.php:1757
+#: src/Content/Widget.php:557 src/Model/Contact.php:1797
 msgid "News"
 msgstr ""
 
@@ -2423,18 +2423,18 @@ msgstr ""
 msgid "Export calendar as csv"
 msgstr ""
 
-#: src/Content/Widget/ContactBlock.php:64
+#: src/Content/Widget/ContactBlock.php:63
 msgid "No contacts"
 msgstr ""
 
-#: src/Content/Widget/ContactBlock.php:95
+#: src/Content/Widget/ContactBlock.php:94
 #, php-format
 msgid "%d Contact"
 msgid_plural "%d Contacts"
 msgstr[0] ""
 msgstr[1] ""
 
-#: src/Content/Widget/ContactBlock.php:112
+#: src/Content/Widget/ContactBlock.php:111
 msgid "View Contacts"
 msgstr ""
 
@@ -2453,12 +2453,20 @@ msgstr[1] ""
 msgid "More Trending Tags"
 msgstr ""
 
-#: src/Content/Widget/VCard.php:94 src/Model/Contact.php:1216
+#: src/Content/Widget/TrendingTags.php:41
+msgid "Show More"
+msgstr ""
+
+#: src/Content/Widget/TrendingTags.php:42
+msgid "Show Less"
+msgstr ""
+
+#: src/Content/Widget/VCard.php:94 src/Model/Contact.php:1256
 #: src/Model/Profile.php:437
 msgid "Post to group"
 msgstr ""
 
-#: src/Content/Widget/VCard.php:99 src/Model/Contact.php:1220
+#: src/Content/Widget/VCard.php:99 src/Model/Contact.php:1260
 #: src/Model/Profile.php:441 src/Module/Moderation/Item/Source.php:80
 msgid "Mention"
 msgstr ""
@@ -2486,68 +2494,68 @@ msgstr ""
 msgid "Network:"
 msgstr ""
 
-#: src/Content/Widget/VCard.php:118 src/Model/Contact.php:1248
-#: src/Model/Contact.php:1260 src/Model/Profile.php:454
+#: src/Content/Widget/VCard.php:118 src/Model/Contact.php:1288
+#: src/Model/Contact.php:1300 src/Model/Profile.php:454
 #: src/Module/Contact/Profile.php:476
 msgid "Unfollow"
 msgstr ""
 
-#: src/Content/Widget/VCard.php:124 src/Model/Contact.php:1218
+#: src/Content/Widget/VCard.php:124 src/Model/Contact.php:1258
 #: src/Model/Profile.php:439
 msgid "View group"
 msgstr ""
 
-#: src/Core/ACL.php:153 src/Module/Profile/Profile.php:260
+#: src/Core/ACL.php:154 src/Module/Profile/Profile.php:260
 msgid "Yourself"
 msgstr ""
 
-#: src/Core/ACL.php:189 src/Module/Privacy/PermissionTooltip.php:156
+#: src/Core/ACL.php:192 src/Module/Privacy/PermissionTooltip.php:156
 #: src/Module/Privacy/PermissionTooltip.php:178
 msgid "Mutuals"
 msgstr ""
 
-#: src/Core/ACL.php:281
+#: src/Core/ACL.php:284
 msgid "Post to Email"
 msgstr ""
 
-#: src/Core/ACL.php:308 src/Module/Privacy/PermissionTooltip.php:103
+#: src/Core/ACL.php:316 src/Module/Privacy/PermissionTooltip.php:103
 #: src/Module/Privacy/PermissionTooltip.php:217
 msgid "Public"
 msgstr ""
 
-#: src/Core/ACL.php:309
+#: src/Core/ACL.php:317
 msgid "This content will be shown to all your followers and can be seen in the community pages and by anyone with its link."
 msgstr ""
 
-#: src/Core/ACL.php:310 src/Module/Privacy/PermissionTooltip.php:105
+#: src/Core/ACL.php:318 src/Module/Privacy/PermissionTooltip.php:105
 msgid "Limited/Private"
 msgstr ""
 
-#: src/Core/ACL.php:311
+#: src/Core/ACL.php:319
 msgid "This content will be shown only to the people in the first box, to the exception of the people mentioned in the second box. It won't appear anywhere public."
 msgstr ""
 
-#: src/Core/ACL.php:311
+#: src/Core/ACL.php:319
 msgid "Start typing the name of a contact or a circle to show a filtered list. You can also mention the special circles \"Followers\" and \"Mutuals\"."
 msgstr ""
 
-#: src/Core/ACL.php:312
+#: src/Core/ACL.php:320
 msgid "Show to:"
 msgstr ""
 
-#: src/Core/ACL.php:313
+#: src/Core/ACL.php:321
 msgid "Except to:"
 msgstr ""
 
-#: src/Core/ACL.php:314 src/Module/Post/Edit.php:145
+#: src/Core/ACL.php:322 src/Module/Post/Edit.php:150
 msgid "CC: email addresses"
 msgstr ""
 
-#: src/Core/ACL.php:315 src/Module/Post/Edit.php:151
+#: src/Core/ACL.php:323 src/Module/Post/Edit.php:156
 msgid "Example: bob@example.com, mary@example.com"
 msgstr ""
 
-#: src/Core/ACL.php:316
+#: src/Core/ACL.php:324
 msgid "Connectors"
 msgstr ""
 
@@ -2836,167 +2844,167 @@ msgstr ""
 msgid "Could not connect to database."
 msgstr ""
 
-#: src/Core/L10n.php:426 src/Model/Item.php:2023
+#: src/Core/L10n.php:453 src/Model/Item.php:2030
 msgid "Undetermined"
 msgstr ""
 
-#: src/Core/L10n.php:433
+#: src/Core/L10n.php:460
 #, php-format
 msgid "%s (%s)"
 msgstr ""
 
-#: src/Core/L10n.php:481 src/Model/Event.php:416
+#: src/Core/L10n.php:509 src/Model/Event.php:416
 #: src/Module/Settings/Display.php:282
 msgid "Monday"
 msgstr ""
 
-#: src/Core/L10n.php:481 src/Model/Event.php:417
+#: src/Core/L10n.php:509 src/Model/Event.php:417
 #: src/Module/Settings/Display.php:283
 msgid "Tuesday"
 msgstr ""
 
-#: src/Core/L10n.php:481 src/Model/Event.php:418
+#: src/Core/L10n.php:509 src/Model/Event.php:418
 #: src/Module/Settings/Display.php:284
 msgid "Wednesday"
 msgstr ""
 
-#: src/Core/L10n.php:481 src/Model/Event.php:419
+#: src/Core/L10n.php:509 src/Model/Event.php:419
 #: src/Module/Settings/Display.php:285
 msgid "Thursday"
 msgstr ""
 
-#: src/Core/L10n.php:481 src/Model/Event.php:420
+#: src/Core/L10n.php:509 src/Model/Event.php:420
 #: src/Module/Settings/Display.php:286
 msgid "Friday"
 msgstr ""
 
-#: src/Core/L10n.php:481 src/Model/Event.php:421
+#: src/Core/L10n.php:509 src/Model/Event.php:421
 #: src/Module/Settings/Display.php:287
 msgid "Saturday"
 msgstr ""
 
-#: src/Core/L10n.php:481 src/Model/Event.php:415
+#: src/Core/L10n.php:509 src/Model/Event.php:415
 #: src/Module/Settings/Display.php:281
 msgid "Sunday"
 msgstr ""
 
-#: src/Core/L10n.php:485 src/Model/Event.php:436
+#: src/Core/L10n.php:515 src/Model/Event.php:436
 msgid "January"
 msgstr ""
 
-#: src/Core/L10n.php:485 src/Model/Event.php:437
+#: src/Core/L10n.php:515 src/Model/Event.php:437
 msgid "February"
 msgstr ""
 
-#: src/Core/L10n.php:485 src/Model/Event.php:438
+#: src/Core/L10n.php:515 src/Model/Event.php:438
 msgid "March"
 msgstr ""
 
-#: src/Core/L10n.php:485 src/Model/Event.php:439
+#: src/Core/L10n.php:515 src/Model/Event.php:439
 msgid "April"
 msgstr ""
 
-#: src/Core/L10n.php:485 src/Core/L10n.php:504 src/Model/Event.php:427
+#: src/Core/L10n.php:515 src/Core/L10n.php:538 src/Model/Event.php:427
 msgid "May"
 msgstr ""
 
-#: src/Core/L10n.php:485 src/Model/Event.php:440
+#: src/Core/L10n.php:515 src/Model/Event.php:440
 msgid "June"
 msgstr ""
 
-#: src/Core/L10n.php:485 src/Model/Event.php:441
+#: src/Core/L10n.php:515 src/Model/Event.php:441
 msgid "July"
 msgstr ""
 
-#: src/Core/L10n.php:485 src/Model/Event.php:442
+#: src/Core/L10n.php:515 src/Model/Event.php:442
 msgid "August"
 msgstr ""
 
-#: src/Core/L10n.php:485 src/Model/Event.php:443
+#: src/Core/L10n.php:515 src/Model/Event.php:443
 msgid "September"
 msgstr ""
 
-#: src/Core/L10n.php:485 src/Model/Event.php:444
+#: src/Core/L10n.php:515 src/Model/Event.php:444
 msgid "October"
 msgstr ""
 
-#: src/Core/L10n.php:485 src/Model/Event.php:445
+#: src/Core/L10n.php:515 src/Model/Event.php:445
 msgid "November"
 msgstr ""
 
-#: src/Core/L10n.php:485 src/Model/Event.php:446
+#: src/Core/L10n.php:515 src/Model/Event.php:446
 msgid "December"
 msgstr ""
 
-#: src/Core/L10n.php:500 src/Model/Event.php:408
+#: src/Core/L10n.php:532 src/Model/Event.php:408
 msgid "Mon"
 msgstr ""
 
-#: src/Core/L10n.php:500 src/Model/Event.php:409
+#: src/Core/L10n.php:532 src/Model/Event.php:409
 msgid "Tue"
 msgstr ""
 
-#: src/Core/L10n.php:500 src/Model/Event.php:410
+#: src/Core/L10n.php:532 src/Model/Event.php:410
 msgid "Wed"
 msgstr ""
 
-#: src/Core/L10n.php:500 src/Model/Event.php:411
+#: src/Core/L10n.php:532 src/Model/Event.php:411
 msgid "Thu"
 msgstr ""
 
-#: src/Core/L10n.php:500 src/Model/Event.php:412
+#: src/Core/L10n.php:532 src/Model/Event.php:412
 msgid "Fri"
 msgstr ""
 
-#: src/Core/L10n.php:500 src/Model/Event.php:413
+#: src/Core/L10n.php:532 src/Model/Event.php:413
 msgid "Sat"
 msgstr ""
 
-#: src/Core/L10n.php:500 src/Model/Event.php:407
+#: src/Core/L10n.php:532 src/Model/Event.php:407
 msgid "Sun"
 msgstr ""
 
-#: src/Core/L10n.php:504 src/Model/Event.php:423
+#: src/Core/L10n.php:538 src/Model/Event.php:423
 msgid "Jan"
 msgstr ""
 
-#: src/Core/L10n.php:504 src/Model/Event.php:424
+#: src/Core/L10n.php:538 src/Model/Event.php:424
 msgid "Feb"
 msgstr ""
 
-#: src/Core/L10n.php:504 src/Model/Event.php:425
+#: src/Core/L10n.php:538 src/Model/Event.php:425
 msgid "Mar"
 msgstr ""
 
-#: src/Core/L10n.php:504 src/Model/Event.php:426
+#: src/Core/L10n.php:538 src/Model/Event.php:426
 msgid "Apr"
 msgstr ""
 
-#: src/Core/L10n.php:504 src/Model/Event.php:428
+#: src/Core/L10n.php:538 src/Model/Event.php:428
 msgid "Jun"
 msgstr ""
 
-#: src/Core/L10n.php:504 src/Model/Event.php:429
+#: src/Core/L10n.php:538 src/Model/Event.php:429
 msgid "Jul"
 msgstr ""
 
-#: src/Core/L10n.php:504 src/Model/Event.php:430
+#: src/Core/L10n.php:538 src/Model/Event.php:430
 msgid "Aug"
 msgstr ""
 
-#: src/Core/L10n.php:504
+#: src/Core/L10n.php:538
 msgid "Sep"
 msgstr ""
 
-#: src/Core/L10n.php:504 src/Model/Event.php:432
+#: src/Core/L10n.php:538 src/Model/Event.php:432
 msgid "Oct"
 msgstr ""
 
-#: src/Core/L10n.php:504 src/Model/Event.php:433
+#: src/Core/L10n.php:538 src/Model/Event.php:433
 msgid "Nov"
 msgstr ""
 
-#: src/Core/L10n.php:504 src/Model/Event.php:434
+#: src/Core/L10n.php:538 src/Model/Event.php:434
 msgid "Dec"
 msgstr ""
 
@@ -3196,84 +3204,84 @@ msgstr ""
 msgid "Edit circles"
 msgstr ""
 
-#: src/Model/Contact.php:1267 src/Module/Moderation/Users/Pending.php:88
+#: src/Model/Contact.php:1307 src/Module/Moderation/Users/Pending.php:88
 #: src/Module/Notifications/Introductions.php:124
 #: src/Module/Notifications/Introductions.php:197
 msgid "Approve"
 msgstr ""
 
-#: src/Model/Contact.php:1601 src/Model/Contact.php:1673
+#: src/Model/Contact.php:1641 src/Model/Contact.php:1713
 #: src/Module/Contact/Profile.php:360
 #, php-format
 msgid "%s has blocked you"
 msgstr ""
 
-#: src/Model/Contact.php:1753
+#: src/Model/Contact.php:1793
 msgid "Organisation"
 msgstr ""
 
-#: src/Model/Contact.php:1761
+#: src/Model/Contact.php:1801
 msgid "Group"
 msgstr ""
 
-#: src/Model/Contact.php:1765 src/Module/Moderation/BaseUsers.php:122
+#: src/Model/Contact.php:1805 src/Module/Moderation/BaseUsers.php:122
 msgid "Relay"
 msgstr ""
 
-#: src/Model/Contact.php:3091
+#: src/Model/Contact.php:3131
 msgid "Disallowed profile URL."
 msgstr ""
 
-#: src/Model/Contact.php:3096 src/Module/Friendica.php:90
+#: src/Model/Contact.php:3136 src/Module/Friendica.php:90
 msgid "Blocked domain"
 msgstr ""
 
-#: src/Model/Contact.php:3101
+#: src/Model/Contact.php:3141
 msgid "Connect URL missing."
 msgstr ""
 
-#: src/Model/Contact.php:3110
+#: src/Model/Contact.php:3150
 msgid "The contact could not be added. Please check the relevant network credentials in your Settings -> Social Networks page."
 msgstr ""
 
-#: src/Model/Contact.php:3128
+#: src/Model/Contact.php:3168
 #, php-format
 msgid "Expected network %s does not match actual network %s"
 msgstr ""
 
-#: src/Model/Contact.php:3145
+#: src/Model/Contact.php:3185
 msgid "This seems to be a relay account. They can't be followed by users."
 msgstr ""
 
-#: src/Model/Contact.php:3152
+#: src/Model/Contact.php:3192
 msgid "The profile address specified does not provide adequate information."
 msgstr ""
 
-#: src/Model/Contact.php:3154
+#: src/Model/Contact.php:3194
 msgid "No compatible communication protocols or feeds were discovered."
 msgstr ""
 
-#: src/Model/Contact.php:3157
+#: src/Model/Contact.php:3197
 msgid "An author or name was not found."
 msgstr ""
 
-#: src/Model/Contact.php:3160
+#: src/Model/Contact.php:3200
 msgid "No browser URL could be matched to this address."
 msgstr ""
 
-#: src/Model/Contact.php:3163
+#: src/Model/Contact.php:3203
 msgid "Unable to match @-style Identity Address with a known protocol or email contact."
 msgstr ""
 
-#: src/Model/Contact.php:3164
+#: src/Model/Contact.php:3204
 msgid "Use mailto: in front of address to force email check."
 msgstr ""
 
-#: src/Model/Contact.php:3170
+#: src/Model/Contact.php:3210
 msgid "Limited profile. This person will be unable to receive direct/personal notifications from you."
 msgstr ""
 
-#: src/Model/Contact.php:3229
+#: src/Model/Contact.php:3269
 msgid "Unable to retrieve contact information."
 msgstr ""
 
@@ -3378,92 +3386,92 @@ msgstr ""
 msgid "Happy Birthday %s"
 msgstr ""
 
-#: src/Model/Item.php:2030
+#: src/Model/Item.php:2037
 #, php-format
 msgid "%s (%s - %s): %s"
 msgstr ""
 
-#: src/Model/Item.php:2032
+#: src/Model/Item.php:2039
 #, php-format
 msgid "%s (%s): %s"
 msgstr ""
 
-#: src/Model/Item.php:2035
+#: src/Model/Item.php:2042
 #, php-format
 msgid ""
 "Detected languages in this post:\n"
 "%s"
 msgstr ""
 
-#: src/Model/Item.php:2984
+#: src/Model/Item.php:2988
 msgid "activity"
 msgstr ""
 
-#: src/Model/Item.php:2986
+#: src/Model/Item.php:2990
 msgid "comment"
 msgstr ""
 
-#: src/Model/Item.php:2989 src/Module/Post/Tag/Add.php:109
+#: src/Model/Item.php:2993 src/Module/Post/Tag/Add.php:112
 msgid "post"
 msgstr ""
 
-#: src/Model/Item.php:3162
-#, php-format
-msgid "%s is blocked"
-msgstr ""
-
-#: src/Model/Item.php:3164
-#, php-format
-msgid "%s is ignored"
-msgstr ""
-
 #: src/Model/Item.php:3166
 #, php-format
-msgid "Content from %s is collapsed"
+msgid "%s is blocked"
+msgstr ""
+
+#: src/Model/Item.php:3168
+#, php-format
+msgid "%s is ignored"
 msgstr ""
 
 #: src/Model/Item.php:3170
+#, php-format
+msgid "Content from %s is collapsed"
+msgstr ""
+
+#: src/Model/Item.php:3174
 msgid "Sensitive content"
 msgstr ""
 
-#: src/Model/Item.php:3687
+#: src/Model/Item.php:3691
 msgid "bytes"
 msgstr ""
 
-#: src/Model/Item.php:3718
+#: src/Model/Item.php:3722
 #, php-format
 msgid "%2$s (%3$d%%, %1$d vote)"
 msgid_plural "%2$s (%3$d%%, %1$d votes)"
 msgstr[0] ""
 msgstr[1] ""
 
-#: src/Model/Item.php:3720
+#: src/Model/Item.php:3724
 #, php-format
 msgid "%2$s (%1$d vote)"
 msgid_plural "%2$s (%1$d votes)"
 msgstr[0] ""
 msgstr[1] ""
 
-#: src/Model/Item.php:3725
+#: src/Model/Item.php:3729
 #, php-format
 msgid "%d voter. Poll end: %s"
 msgid_plural "%d voters. Poll end: %s"
 msgstr[0] ""
 msgstr[1] ""
 
-#: src/Model/Item.php:3727
+#: src/Model/Item.php:3731
 #, php-format
 msgid "%d voter."
 msgid_plural "%d voters."
 msgstr[0] ""
 msgstr[1] ""
 
-#: src/Model/Item.php:3729
+#: src/Model/Item.php:3733
 #, php-format
 msgid "Poll end: %s"
 msgstr ""
 
-#: src/Model/Item.php:3770 src/Model/Item.php:3771
+#: src/Model/Item.php:3774 src/Model/Item.php:3775
 msgid "View on separate page"
 msgstr ""
 
@@ -5745,7 +5753,7 @@ msgstr ""
 #: src/Module/Contact.php:537 src/Module/Conversation/Channel.php:98
 #: src/Module/Conversation/Community.php:91
 #: src/Module/Conversation/Network.php:295
-#: src/Module/Moderation/BaseUsers.php:102 src/Object/Post.php:606
+#: src/Module/Moderation/BaseUsers.php:102 src/Object/Post.php:612
 msgid "More"
 msgstr ""
 
@@ -6063,7 +6071,7 @@ msgid "Only show blocked contacts"
 msgstr ""
 
 #: src/Module/Contact.php:348 src/Module/Contact.php:420
-#: src/Module/Settings/Server/Index.php:93 src/Object/Post.php:385
+#: src/Module/Settings/Server/Index.php:93 src/Object/Post.php:391
 msgid "Ignored"
 msgstr ""
 
@@ -7110,7 +7118,7 @@ msgstr ""
 msgid "Help:"
 msgstr ""
 
-#: src/Module/Home.php:52
+#: src/Module/Home.php:53
 #, php-format
 msgid "Welcome to %s"
 msgstr ""
@@ -7330,39 +7338,39 @@ msgstr ""
 msgid "For more information about the Friendica project and why we feel it is important, please visit http://friendi.ca"
 msgstr ""
 
-#: src/Module/Item/Compose.php:83
+#: src/Module/Item/Compose.php:86
 msgid "Please enter a post body."
 msgstr ""
 
-#: src/Module/Item/Compose.php:94
+#: src/Module/Item/Compose.php:97
 msgid "This feature is only available with the frio theme."
 msgstr ""
 
-#: src/Module/Item/Compose.php:118
+#: src/Module/Item/Compose.php:121
 msgid "Compose new personal note"
 msgstr ""
 
-#: src/Module/Item/Compose.php:127
+#: src/Module/Item/Compose.php:130
 msgid "Compose new post"
 msgstr ""
 
-#: src/Module/Item/Compose.php:183
+#: src/Module/Item/Compose.php:186
 msgid "Visibility"
 msgstr ""
 
-#: src/Module/Item/Compose.php:199
+#: src/Module/Item/Compose.php:202
 msgid "Clear the location"
 msgstr ""
 
-#: src/Module/Item/Compose.php:200
+#: src/Module/Item/Compose.php:203
 msgid "Location services are unavailable on your device"
 msgstr ""
 
-#: src/Module/Item/Compose.php:201
+#: src/Module/Item/Compose.php:204
 msgid "Location services are disabled. Please check the website's permissions on your device"
 msgstr ""
 
-#: src/Module/Item/Compose.php:207
+#: src/Module/Item/Compose.php:214
 msgid "You can make this page always open when you use the New Post button in the Theme Customization settings."
 msgstr ""
 
@@ -8458,46 +8466,46 @@ msgstr ""
 msgid "The Photo is not available."
 msgstr ""
 
-#: src/Module/Photo.php:133
+#: src/Module/Photo.php:134
 #, php-format
 msgid "The Photo with id %s is not available."
 msgstr ""
 
-#: src/Module/Photo.php:178
+#: src/Module/Photo.php:181
 #, php-format
 msgid "Invalid external resource with url %s."
 msgstr ""
 
-#: src/Module/Photo.php:180
+#: src/Module/Photo.php:183
 #, php-format
 msgid "Invalid photo with id %s."
 msgstr ""
 
-#: src/Module/Post/Edit.php:73 src/Module/Post/Edit.php:87
+#: src/Module/Post/Edit.php:78 src/Module/Post/Edit.php:92
 msgid "Post not found."
 msgstr ""
 
-#: src/Module/Post/Edit.php:93
+#: src/Module/Post/Edit.php:98
 msgid "Edit post"
 msgstr ""
 
-#: src/Module/Post/Edit.php:127
+#: src/Module/Post/Edit.php:132
 msgid "web link"
 msgstr ""
 
-#: src/Module/Post/Edit.php:128
+#: src/Module/Post/Edit.php:133
 msgid "Insert video link"
 msgstr ""
 
-#: src/Module/Post/Edit.php:129
+#: src/Module/Post/Edit.php:134
 msgid "video link"
 msgstr ""
 
-#: src/Module/Post/Edit.php:130
+#: src/Module/Post/Edit.php:135
 msgid "Insert audio link"
 msgstr ""
 
-#: src/Module/Post/Edit.php:131
+#: src/Module/Post/Edit.php:136
 msgid "audio link"
 msgstr ""
 
@@ -8571,19 +8579,19 @@ msgid "No contacts."
 msgstr ""
 
 #: src/Module/Profile/Conversations.php:96 src/Module/Profile/Profile.php:342
-#: src/Protocol/Feed.php:1114
+#: src/Protocol/Feed.php:1124
 #, php-format
 msgid "%s's posts"
 msgstr ""
 
 #: src/Module/Profile/Conversations.php:97 src/Module/Profile/Profile.php:343
-#: src/Protocol/Feed.php:1117
+#: src/Protocol/Feed.php:1127
 #, php-format
 msgid "%s's comments"
 msgstr ""
 
 #: src/Module/Profile/Conversations.php:98 src/Module/Profile/Profile.php:344
-#: src/Protocol/Feed.php:1110
+#: src/Protocol/Feed.php:1120
 #, php-format
 msgid "%s's timeline"
 msgstr ""
@@ -10806,24 +10814,24 @@ msgstr ""
 msgid "The requested item doesn't exist or has been deleted."
 msgstr ""
 
-#: src/Module/User/Delegation.php:134
+#: src/Module/User/Delegation.php:138
 #, php-format
 msgid "You are now logged in as %s"
 msgstr ""
 
-#: src/Module/User/Delegation.php:173
+#: src/Module/User/Delegation.php:177
 msgid "Switch between your accounts"
 msgstr ""
 
-#: src/Module/User/Delegation.php:174
+#: src/Module/User/Delegation.php:178
 msgid "Manage your accounts"
 msgstr ""
 
-#: src/Module/User/Delegation.php:175
+#: src/Module/User/Delegation.php:179
 msgid "Toggle between different identities or community/group pages which share your account details or which you have been granted \"manage\" permissions"
 msgstr ""
 
-#: src/Module/User/Delegation.php:176
+#: src/Module/User/Delegation.php:180
 msgid "Select an identity to manage: "
 msgstr ""
 
@@ -11525,227 +11533,227 @@ msgstr ""
 msgid "Save to folder"
 msgstr ""
 
-#: src/Object/Post.php:333
+#: src/Object/Post.php:339
 msgid "I will attend"
 msgstr ""
 
-#: src/Object/Post.php:333
+#: src/Object/Post.php:339
 msgid "I will not attend"
 msgstr ""
 
-#: src/Object/Post.php:333
+#: src/Object/Post.php:339
 msgid "I might attend"
 msgstr ""
 
-#: src/Object/Post.php:380
+#: src/Object/Post.php:386
 msgid "Ignore thread"
 msgstr ""
 
-#: src/Object/Post.php:381
+#: src/Object/Post.php:387
 msgid "Unignore thread"
 msgstr ""
 
-#: src/Object/Post.php:382
+#: src/Object/Post.php:388
 msgid "Toggle ignore status"
 msgstr ""
 
-#: src/Object/Post.php:392
+#: src/Object/Post.php:398
 msgid "Add star"
 msgstr ""
 
-#: src/Object/Post.php:393
+#: src/Object/Post.php:399
 msgid "Remove star"
 msgstr ""
 
-#: src/Object/Post.php:394
+#: src/Object/Post.php:400
 msgid "Toggle star status"
 msgstr ""
 
-#: src/Object/Post.php:405
+#: src/Object/Post.php:411
 msgid "Pin"
 msgstr ""
 
-#: src/Object/Post.php:406
+#: src/Object/Post.php:412
 msgid "Unpin"
 msgstr ""
 
-#: src/Object/Post.php:407
+#: src/Object/Post.php:413
 msgid "Toggle pin status"
 msgstr ""
 
-#: src/Object/Post.php:410
+#: src/Object/Post.php:416
 msgid "Pinned"
 msgstr ""
 
-#: src/Object/Post.php:415
+#: src/Object/Post.php:421
 msgid "Add tag"
 msgstr ""
 
-#: src/Object/Post.php:430
+#: src/Object/Post.php:436
 msgid "Quote share this"
 msgstr ""
 
-#: src/Object/Post.php:430
+#: src/Object/Post.php:436
 msgid "Quote Share"
 msgstr ""
 
-#: src/Object/Post.php:433
+#: src/Object/Post.php:439
 msgid "Reshare this"
 msgstr ""
 
-#: src/Object/Post.php:433
+#: src/Object/Post.php:439
 msgid "Reshare"
 msgstr ""
 
-#: src/Object/Post.php:434
+#: src/Object/Post.php:440
 msgid "Cancel your Reshare"
 msgstr ""
 
-#: src/Object/Post.php:434
+#: src/Object/Post.php:440
 msgid "Unshare"
 msgstr ""
 
-#: src/Object/Post.php:478
+#: src/Object/Post.php:484
 #, php-format
 msgid "%s (Received %s)"
 msgstr ""
 
-#: src/Object/Post.php:484
+#: src/Object/Post.php:490
 msgid "Comment this item on your system"
 msgstr ""
 
-#: src/Object/Post.php:484
+#: src/Object/Post.php:490
 msgid "Remote comment"
 msgstr ""
 
-#: src/Object/Post.php:508
+#: src/Object/Post.php:514
 msgid "Share via ..."
 msgstr ""
 
-#: src/Object/Post.php:508
+#: src/Object/Post.php:514
 msgid "Share via external services"
 msgstr ""
 
-#: src/Object/Post.php:515
+#: src/Object/Post.php:521
 msgid "Unknown parent"
 msgstr ""
 
-#: src/Object/Post.php:519
+#: src/Object/Post.php:525
 #, php-format
 msgid "in reply to %s"
 msgstr ""
 
-#: src/Object/Post.php:521
+#: src/Object/Post.php:527
 msgid "Parent is probably private or not federated."
 msgstr ""
 
-#: src/Object/Post.php:545
+#: src/Object/Post.php:551
 msgid "to"
 msgstr ""
 
-#: src/Object/Post.php:546
+#: src/Object/Post.php:552
 msgid "via"
 msgstr ""
 
-#: src/Object/Post.php:547
+#: src/Object/Post.php:553
 msgid "Wall-to-Wall"
 msgstr ""
 
-#: src/Object/Post.php:548
+#: src/Object/Post.php:554
 msgid "via Wall-To-Wall:"
 msgstr ""
 
-#: src/Object/Post.php:603
+#: src/Object/Post.php:609
 #, php-format
 msgid "Reply to %s"
 msgstr ""
 
-#: src/Object/Post.php:625
+#: src/Object/Post.php:631
 msgid "Notifier task is pending"
 msgstr ""
 
-#: src/Object/Post.php:626
+#: src/Object/Post.php:632
 msgid "Delivery to remote servers is pending"
 msgstr ""
 
-#: src/Object/Post.php:627
+#: src/Object/Post.php:633
 msgid "Delivery to remote servers is underway"
 msgstr ""
 
-#: src/Object/Post.php:628
+#: src/Object/Post.php:634
 msgid "Delivery to remote servers is mostly done"
 msgstr ""
 
-#: src/Object/Post.php:629
+#: src/Object/Post.php:635
 msgid "Delivery to remote servers is done"
 msgstr ""
 
-#: src/Object/Post.php:651
+#: src/Object/Post.php:660
 #, php-format
 msgid "%d comment"
 msgid_plural "%d comments"
 msgstr[0] ""
 msgstr[1] ""
 
-#: src/Object/Post.php:652
+#: src/Object/Post.php:661
 msgid "Show more"
 msgstr ""
 
-#: src/Object/Post.php:653
+#: src/Object/Post.php:662
 msgid "Show fewer"
 msgstr ""
 
-#: src/Object/Post.php:690
+#: src/Object/Post.php:699
 #, php-format
 msgid "Reshared by: %s"
 msgstr ""
 
-#: src/Object/Post.php:695
+#: src/Object/Post.php:704
 #, php-format
 msgid "Viewed by: %s"
 msgstr ""
 
-#: src/Object/Post.php:700
+#: src/Object/Post.php:709
 #, php-format
 msgid "Read by: %s"
 msgstr ""
 
-#: src/Object/Post.php:705
+#: src/Object/Post.php:714
 #, php-format
 msgid "Liked by: %s"
 msgstr ""
 
-#: src/Object/Post.php:710
+#: src/Object/Post.php:719
 #, php-format
 msgid "Disliked by: %s"
 msgstr ""
 
-#: src/Object/Post.php:715
+#: src/Object/Post.php:724
 #, php-format
 msgid "Attended by: %s"
 msgstr ""
 
-#: src/Object/Post.php:720
+#: src/Object/Post.php:729
 #, php-format
 msgid "Maybe attended by: %s"
 msgstr ""
 
-#: src/Object/Post.php:725
+#: src/Object/Post.php:734
 #, php-format
 msgid "Not attended by: %s"
 msgstr ""
 
-#: src/Object/Post.php:730
+#: src/Object/Post.php:739
 #, php-format
 msgid "Commented by: %s"
 msgstr ""
 
-#: src/Object/Post.php:735
+#: src/Object/Post.php:744
 #, php-format
 msgid "Reacted with %s by: %s"
 msgstr ""
 
-#: src/Object/Post.php:758
+#: src/Object/Post.php:767
 #, php-format
 msgid "Quote shared by: %s"
 msgstr ""
diff --git a/view/templates/widget/trending_tags.tpl b/view/templates/widget/trending_tags.tpl
index 9058da5480..186b6f4018 100644
--- a/view/templates/widget/trending_tags.tpl
+++ b/view/templates/widget/trending_tags.tpl
@@ -27,7 +27,7 @@