Compare commits

..

38 commits

Author SHA1 Message Date
6eb9b55af0 Merge pull request 'Issue 14439: Check for the exipration days / use "Atmosphere"' (#1653) from heluecht/friendica-addons:issue-14439 into develop
Reviewed-on: friendica/friendica-addons#1653
Reviewed-by: Hypolite Petovan <hypolite@mrpetovan.com>
2026-04-26 23:06:40 +02:00
f00c96b734 Issue 14439: Check for the exipration days / use "Atmosphere" 2026-04-26 23:06:40 +02:00
1d9da6d09e Merge pull request '[CI] Bump PHP versions' (#1649) from nupplaPhil/friendica-addons:ci/bump_versions into develop
Reviewed-on: friendica/friendica-addons#1649
Reviewed-by: Hypolite Petovan <hypolite@mrpetovan.com>
2026-04-20 20:26:16 +02:00
cee8369194
[CI] Bump PHP versions
- new PHP versions
- Include missing extensions "gmp" and "imagick"
2026-04-18 21:02:21 +02:00
7bc5512554 Merge pull request 'fix phpstan errors' (#1648) from Art4/friendica-addons:fix-phpstan-errors into develop
Reviewed-on: friendica/friendica-addons#1648
2026-04-17 20:42:27 +02:00
Art4
b4999da405 refactor: remove dead code, because get_magic_quotes_gpc() always returns false 2026-04-15 20:49:32 +02:00
Art4
16eb8722d3 fix: fix phpstan errors 2026-04-15 20:48:36 +02:00
ecbdbe3ea9 Merge pull request 'atproto: Usage of new atproto functions to handle with records' (#1647) from heluecht/friendica-addons:atp into develop
Reviewed-on: friendica/friendica-addons#1647
Reviewed-by: Hypolite Petovan <hypolite@mrpetovan.com>
2026-04-12 06:51:56 +02:00
7473dad087 atproto: Usage of new atproto functions to handle with records 2026-04-12 04:47:41 +00:00
8310e7fd18 Merge pull request 'atproto: Transmit the correct parcel when fetching missing posts' (#1646) from heluecht/friendica-addons:parcel into develop
Reviewed-on: friendica/friendica-addons#1646
2026-04-11 13:37:01 +02:00
d2e71b0307 atproto: Transmit the correct parcel when fetching missing posts 2026-04-08 10:37:31 +00:00
8328d3b583 Merge pull request 'Invidious Addon Optimization' (#1643) from loma-one/friendica-addons:loma-one-patch-2 into develop
Reviewed-on: friendica/friendica-addons#1643
Reviewed-by: Hypolite Petovan <hypolite@mrpetovan.com>
2026-04-08 04:13:50 +02:00
f9f767c021 Invidious Addon Optimization
Performance: Consolidated multiple preg_replace calls into a single, efficient regex pass.
Support: Added handling for YouTube Shorts, Music, and Embed links.
Features: Enhanced link detection to preserve video timestamps (?t=...).
Robustness: Improved URL sanitization to prevent broken links (double slashes) and added rel="noopener" for security.
UI: Cleaned up the footer note by displaying a clickable hostname instead of the full URL.
2026-04-08 04:13:50 +02:00
1268d8fc98 Merge pull request 'Mailstream: delete cookiejar after fetching image' (#1645) from mexon/friendica-addons:mat/mailstream-delete-cookiejar into develop
Reviewed-on: friendica/friendica-addons#1645
Reviewed-by: Hypolite Petovan <hypolite@mrpetovan.com>
2026-04-06 23:11:06 +02:00
Matthew Exon
0820765c4e Mailstream: delete cookiejar after fetching image 2026-04-06 23:11:06 +02:00
a888947b67 Merge pull request 'AT-Protocol: Configurable web frontend' (#1644) from heluecht/friendica-addons:atproto2 into develop
Reviewed-on: friendica/friendica-addons#1644
2026-04-04 08:21:35 +02:00
7ffd587819 AT-Protocol: Configurable web frontend 2026-03-29 13:31:47 +00:00
52f7242247 Merge pull request '"bluesky" is now "AT Protocol"' (#1642) from heluecht/friendica-addons:bluesky into develop
Reviewed-on: friendica/friendica-addons#1642
2026-03-29 11:40:23 +02:00
ea16229e27 Merge branch 'develop' into bluesky 2026-03-29 11:40:08 +02:00
3d199ba7d7 Setting for preferred web front end added 2026-03-18 13:21:21 +00:00
5a099deded Renamed function 2026-03-17 21:30:07 +00:00
d7a380bad5 "bluesky" is now "AT Protocol" 2026-03-15 23:17:04 +00:00
f552fff6b8 Merge pull request 'Add QuickPhoto Addon: Elegant BBCode simplification for images' (#1639) from loma-one/friendica-addons:loma-one-patch-1 into develop
Reviewed-on: friendica/friendica-addons#1639
Reviewed-by: Hypolite Petovan <hypolite@mrpetovan.com>
2026-03-07 17:20:56 +01:00
2e9f640724 quickphoto/quickphoto.php aktualisiert
DI::l10n()->t inserted
2026-03-07 17:09:57 +01:00
59ece8856c Translation file 2026-03-07 15:13:03 +01:00
aa47cc6e04 quickphoto/quickphoto.js aktualisiert
Make sure the translation is applied.
2026-03-07 15:13:03 +01:00
4a133456ce quickphoto/quickphoto.php aktualisiert
Provides a translation
2026-03-07 15:13:03 +01:00
22026ed533 quickphoto/quickphoto.js aktualisiert
"Description" instead of "Bildbeschreibung"
2026-03-07 15:13:03 +01:00
29e8c2875c quickphoto/README.md aktualisiert
Inserted | in `[img]|filename description[/img]`
2026-03-07 15:13:03 +01:00
0ae86dafa0 quickphoto/README.md hinzugefügt 2026-03-07 15:13:03 +01:00
ec3b0c5941 Add QuickPhoto Addon: Elegant BBCode simplification for images
This PR introduces the QuickPhoto addon, designed to improve the user experience within the Friendica editor. It addresses the issue of long, cluttered BBCodes (monster-links) that appear when images are inserted via drag-and-drop or the image uploader.
Key Features

- Automatic Simplification: Replaces complex [url=...][img=...]...[/img][/url] structures with a human-readable shorthand: [img]filename|description[/img].
- Seamless Reconstruction: Automatically restores the full, valid Friendica BBCode before previewing or submitting a post, ensuring 100% compatibility with the server.
- Enhanced Readability: Keeps the editor clean and focus-oriented while writing long posts with multiple images.

Technical Highlights & Optimizations

- Resource Efficient: The JavaScript is strictly scoped. It only becomes active if a textarea is present and visible in the DOM.
- Zero Latency Typing: Implements requestIdleCallback and throttling (500ms) to ensure that the simplification process never interferes with the user's typing flow or causes UI lag.
- Background Throttling: Logic is suspended when the browser tab is inactive (document.hidden) to save CPU and battery life.
- LocalStorage Cache: Uses a local cache for image data (auto-cleaned after 12 hours) to ensure reliability during a single editing session.

Testing performed

- Tested with the standard "Jot" editor and the newer Compose/Comment templates.
- Verified that image previews render correctly (reconstruction triggers on preview click).
- Verified that the final post contains the correct full BBCode on the server side.
- Confirmed that Drag & Drop inserts are handled immediately.
2026-03-07 15:13:03 +01:00
3509144228 Add QuickPhoto Addon: Elegant BBCode simplification for images
This PR introduces the QuickPhoto addon, designed to improve the user experience within the Friendica editor. It addresses the issue of long, cluttered BBCodes (monster-links) that appear when images are inserted via drag-and-drop or the image uploader.
Key Features

- Automatic Simplification: Replaces complex [url=...][img=...]...[/img][/url] structures with a human-readable shorthand: [img]filename|description[/img].
- Seamless Reconstruction: Automatically restores the full, valid Friendica BBCode before previewing or submitting a post, ensuring 100% compatibility with the server.
- Enhanced Readability: Keeps the editor clean and focus-oriented while writing long posts with multiple images.

Technical Highlights & Optimizations

- Resource Efficient: The JavaScript is strictly scoped. It only becomes active if a textarea is present and visible in the DOM.
- Zero Latency Typing: Implements requestIdleCallback and throttling (500ms) to ensure that the simplification process never interferes with the user's typing flow or causes UI lag.
- Background Throttling: Logic is suspended when the browser tab is inactive (document.hidden) to save CPU and battery life.
- LocalStorage Cache: Uses a local cache for image data (auto-cleaned after 12 hours) to ensure reliability during a single editing session.

Testing performed

- Tested with the standard "Jot" editor and the newer Compose/Comment templates.
- Verified that image previews render correctly (reconstruction triggers on preview click).
- Verified that the final post contains the correct full BBCode on the server side.
- Confirmed that Drag & Drop inserts are handled immediately.
2026-03-07 15:13:03 +01:00
ade3c6b22e Merge pull request 'IRC: Use friendica nickname as suggested IRC nickname, when signed in' (#1637) from marcusxs/friendica-addons:default_irc_nickname_to_friendica_nickname into develop
Reviewed-on: friendica/friendica-addons#1637
2026-03-07 15:07:09 +01:00
Marcus F.
31c130436c IRC: Use friendica nickname as suggested IRC nickname, when signed in 2026-03-07 13:31:50 +01:00
8a4715271c Merge pull request 'Mailstream: respect blocked/ignored/collapsed contact settings' (#1640) from mexon/friendica-addons:mat/mailstream-block into develop
Reviewed-on: friendica/friendica-addons#1640
Reviewed-by: Hypolite Petovan <hypolite@mrpetovan.com>
2026-03-07 04:12:30 +01:00
Matthew Exon
ed70b2ca27 Mailstream: respect blocked/ignored/collapsed contact settings 2026-03-02 18:13:28 +01:00
2c75b00b16 Merge pull request 'Smileybutton Add-on Upgrade (github PR 1380)' (#1638) from tobias/friendica-addons:rp-smileybutton into develop
Reviewed-on: friendica/friendica-addons#1638
Reviewed-by: Hypolite Petovan <hypolite@mrpetovan.com>
2026-02-13 16:56:19 +01:00
Random Penguin
6d3dd9b8b2 Update smileybutton.php
- Enabling for Quattro. Enabled for Mobile because it may have emoji mobile devices do not have. Switched structure from <table> to <div> to make layout more flexible.
- Switched to DIV layout
- Also fixes button and position appearance.
- Also fixes button alignment and appearance issues
- Adding support for Quattro theme
2026-02-10 21:22:03 -05:00
24 changed files with 781 additions and 242 deletions

View file

@ -5,6 +5,11 @@ labels:
skip_clone: true
matrix:
include:
- PHP_MAJOR_VERSION: 8.5
PHP_VERSION: 8.5.5
steps:
clone_friendica_base:
image: alpine/git
@ -48,7 +53,7 @@ steps:
branch: [ develop, '*-rc' ]
event: push
composer_install:
image: friendicaci/php8.2:php8.2.28
image: friendicaci/php${PHP_MAJOR_VERSION}:php${PHP_VERSION}
commands:
- export COMPOSER_HOME=.composer
- composer validate

View file

@ -5,6 +5,11 @@ labels:
skip_clone: true
matrix:
include:
- PHP_MAJOR_VERSION: 8.5
PHP_VERSION: 8.5.5
steps:
clone_friendica_base:
image: alpine/git
@ -48,7 +53,7 @@ steps:
branch: [ develop, '*-rc' ]
event: push
composer_install:
image: friendicaci/php8.2:php8.2.28
image: friendicaci/php${PHP_MAJOR_VERSION}:php${PHP_VERSION}
commands:
- export COMPOSER_HOME=.composer
- composer validate

View file

@ -5,13 +5,15 @@ matrix:
- PHP_MAJOR_VERSION: 8.0
PHP_VERSION: 8.0.30
- PHP_MAJOR_VERSION: 8.1
PHP_VERSION: 8.1.31
PHP_VERSION: 8.1.34
- PHP_MAJOR_VERSION: 8.2
PHP_VERSION: 8.2.28
PHP_VERSION: 8.2.30
- PHP_MAJOR_VERSION: 8.3
PHP_VERSION: 8.3.17
PHP_VERSION: 8.3.30
- PHP_MAJOR_VERSION: 8.4
PHP_VERSION: 8.4.5
PHP_VERSION: 8.4.20
- PHP_MAJOR_VERSION: 8.5
PHP_VERSION: 8.5.5
# This forces PHP Unit executions at the "opensocial" labeled location (because of much more power...)
labels:

View file

@ -5,6 +5,11 @@ labels:
skip_clone: true
matrix:
include:
- PHP_MAJOR_VERSION: 8.5
PHP_VERSION: 8.5.5
steps:
clone_friendica_base:
image: alpine/git
@ -45,7 +50,7 @@ steps:
repo: friendica/friendica-addons
event: tag
composer_install:
image: friendicaci/php8.2:php8.2.28
image: friendicaci/php${PHP_MAJOR_VERSION}:php${PHP_VERSION}
commands:
- export COMPOSER_HOME=.composer
- composer validate

View file

@ -1,7 +1,7 @@
<?php
/**
* Name: Bluesky Connector
* Description: Post to Bluesky, import timelines and feeds
* Name: AT Protocol Atmosphere Connector (Bluesky, Eurosky, Blacksky, ...)
* Description: Post to the Atmosphere via the AT Protocol, import timelines and feeds
* Version: 1.1
* Author: Michael Vogel <https://pirati.ca/profile/heluecht>
*
@ -46,6 +46,7 @@ use Friendica\Protocol\Activity;
use Friendica\Protocol\ATProtocol;
use Friendica\Protocol\Relay;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\ParseUrl;
use Friendica\Util\Strings;
const BLUESKY_DEFAULT_POLL_INTERVAL = 10; // given in minutes
@ -81,6 +82,8 @@ function bluesky_check_item_notification(array &$notification_data)
return;
}
DI::atProtocol()->setApiForUser($notification_data['uid']);
$did = DI::atProtocol()->getUserDid($notification_data['uid']);
if (empty($did)) {
return;
@ -96,24 +99,16 @@ function bluesky_item_by_link(array &$hookData)
return;
}
if (substr($hookData['uri'], 0, 5) != 'at://') {
if (!preg_match('#^' . ATProtocol::WEB . '/profile/(.+)/post/(.+)#', $hookData['uri'], $matches)) {
return;
}
DI::atProtocol()->setApiForUser($hookData['uid']);
$did = DI::atProtocol()->getDid($matches[1]);
if (empty($did)) {
return;
}
DI::logger()->debug('Found bluesky post', ['uri' => $hookData['uri'], 'did' => $did, 'cid' => $matches[2]]);
$uri = 'at://' . $did . '/app.bsky.feed.post/' . $matches[2];
if (!str_starts_with($hookData['uri'], 'at://')) {
$data = ParseUrl::getSiteinfoCached($hookData['uri']);
$uri = $data['atprotocol']['uri'] ?? '';
} else {
$uri = $hookData['uri'];
}
$uri = DI::atpProcessor()->fetchMissingPost($uri, $hookData['uid'], Item::PR_FETCHED, 0, 0);
$uri = DI::atpProcessor()->fetchMissingPost($uri, $hookData['uid'], Item::PR_FETCHED, 0, 0, '', false, Conversation::PARCEL_CONNECTOR);
DI::logger()->debug('Got post', ['uri' => $uri]);
if (!empty($uri)) {
$item = Post::selectFirst(['id'], ['uri' => $uri, 'uid' => $hookData['uid']]);
@ -125,20 +120,22 @@ function bluesky_item_by_link(array &$hookData)
function bluesky_support_follow(array &$data)
{
if ($data['protocol'] == Protocol::BLUESKY) {
if ($data['protocol'] == Protocol::ATPROTO) {
$data['result'] = true;
}
}
function bluesky_follow(array &$hook_data)
{
DI::atProtocol()->setApiForUser($hook_data['uid']);
$token = DI::atProtocol()->getUserToken($hook_data['uid']);
if (empty($token)) {
return;
}
DI::logger()->debug('Check if contact is bluesky', ['data' => $hook_data]);
$contact = DBA::selectFirst('contact', [], ['network' => Protocol::BLUESKY, 'nurl' => Strings::normaliseLink($hook_data['url']), 'uid' => [0, $hook_data['uid']]]);
DI::logger()->debug('Check if contact is AT Protocol', ['data' => $hook_data]);
$contact = DBA::selectFirst('contact', [], ['network' => Protocol::ATPROTO, 'nurl' => Strings::normaliseLink($hook_data['url']), 'uid' => [0, $hook_data['uid']]]);
if (empty($contact)) {
return;
}
@ -149,13 +146,7 @@ function bluesky_follow(array &$hook_data)
'$type' => 'app.bsky.graph.follow'
];
$post = [
'collection' => 'app.bsky.graph.follow',
'repo' => DI::atProtocol()->getUserDid($hook_data['uid']),
'record' => $record
];
$activity = DI::atProtocol()->XRPCPost($hook_data['uid'], 'com.atproto.repo.createRecord', $post);
$activity = DI::atProtocol()->createRecord($hook_data['uid'], 'app.bsky.graph.follow', $record);
if (!empty($activity->uri)) {
$hook_data['contact'] = $contact;
DI::logger()->debug('Successfully start following', ['url' => $contact['url'], 'uri' => $activity->uri]);
@ -164,12 +155,14 @@ function bluesky_follow(array &$hook_data)
function bluesky_unfollow(array &$hook_data)
{
DI::atProtocol()->setApiForUser($hook_data['uid']);
$token = DI::atProtocol()->getUserToken($hook_data['uid']);
if (empty($token)) {
return;
}
if ($hook_data['contact']['network'] != Protocol::BLUESKY) {
if ($hook_data['contact']['network'] != Protocol::ATPROTO) {
return;
}
@ -178,19 +171,21 @@ function bluesky_unfollow(array &$hook_data)
return;
}
bluesky_delete_post($data->viewer->following, $hook_data['uid']);
DI::atProtocol()->deleteRecord($hook_data['uid'], $data->viewer->following);
$hook_data['result'] = true;
}
function bluesky_block(array &$hook_data)
{
DI::atProtocol()->setApiForUser($hook_data['uid']);
$token = DI::atProtocol()->getUserToken($hook_data['uid']);
if (empty($token)) {
return;
}
if ($hook_data['contact']['network'] != Protocol::BLUESKY) {
if ($hook_data['contact']['network'] != Protocol::ATPROTO) {
return;
}
@ -200,13 +195,7 @@ function bluesky_block(array &$hook_data)
'$type' => 'app.bsky.graph.block'
];
$post = [
'collection' => 'app.bsky.graph.block',
'repo' => DI::atProtocol()->getUserDid($hook_data['uid']),
'record' => $record
];
$activity = DI::atProtocol()->XRPCPost($hook_data['uid'], 'com.atproto.repo.createRecord', $post);
$activity = DI::atProtocol()->createRecord($hook_data['uid'], 'app.bsky.graph.block', $record);
if (!empty($activity->uri)) {
$ucid = Contact::getUserContactId($hook_data['contact']['id'], $hook_data['uid']);
if ($ucid) {
@ -218,12 +207,14 @@ function bluesky_block(array &$hook_data)
function bluesky_unblock(array &$hook_data)
{
DI::atProtocol()->setApiForUser($hook_data['uid']);
$token = DI::atProtocol()->getUserToken($hook_data['uid']);
if (empty($token)) {
return;
}
if ($hook_data['contact']['network'] != Protocol::BLUESKY) {
if ($hook_data['contact']['network'] != Protocol::ATPROTO) {
return;
}
@ -232,7 +223,7 @@ function bluesky_unblock(array &$hook_data)
return;
}
bluesky_delete_post($data->viewer->blocking, $hook_data['uid']);
DI::atProtocol()->deleteRecord($hook_data['uid'], $data->viewer->blocking);
$hook_data['result'] = true;
}
@ -243,7 +234,7 @@ function bluesky_addon_admin(string &$o)
$o = Renderer::replaceMacros($t, [
'$submit' => DI::l10n()->t('Save Settings'),
'$friendica_handles' => ['friendica_handles', DI::l10n()->t('Allow your users to use your hostname for their Bluesky handles'), DI::config()->get('bluesky', 'friendica_handles'), DI::l10n()->t('Before enabling this option, you have to setup a wildcard domain configuration and you have to enable wildcard requests in your webserver configuration. On Apache this is done by adding "ServerAlias *.%s" to your HTTP configuration. You don\'t need to change the HTTPS configuration.', DI::baseUrl()->getHost())],
'$friendica_handles' => ['friendica_handles', DI::l10n()->t('Allow your users to use your hostname for their Atmosphere handles'), DI::config()->get('bluesky', 'friendica_handles'), DI::l10n()->t('Before enabling this option, you have to setup a wildcard domain configuration and you have to enable wildcard requests in your webserver configuration. On Apache this is done by adding "ServerAlias *.%s" to your HTTP configuration. You don\'t need to change the HTTPS configuration.', DI::baseUrl()->getHost())],
]);
}
@ -258,10 +249,13 @@ function bluesky_settings(array &$data)
return;
}
DI::atProtocol()->setApiForUser(DI::userSession()->getLocalUserId());
$enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'post') ?? false;
$def_enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'post_by_default') ?? false;
$pds = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'pds');
$handle = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle');
$web = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'web');
$did = DI::atProtocol()->getUserDid(DI::userSession()->getLocalUserId());
$token = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'access_token');
$import = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'import') ?? false;
@ -272,7 +266,7 @@ function bluesky_settings(array &$data)
if (DI::config()->get('bluesky', 'friendica_handles')) {
$self = User::getById(DI::userSession()->getLocalUserId(), ['nickname']);
$host_handle = $self['nickname'] . '.' . DI::baseUrl()->getHost();
$friendica_handle = ['bluesky_friendica_handle', DI::l10n()->t('Allow to use %s as your Bluesky handle.', $host_handle), $custom_handle, DI::l10n()->t('When enabled, you can use %s as your Bluesky handle. After you enabled this option, please go to https://bsky.app/settings and select to change your handle. Select that you have got your own domain. Then enter %s and select "No DNS Panel". Then select "Verify Text File".', $host_handle, $host_handle)];
$friendica_handle = ['bluesky_friendica_handle', DI::l10n()->t('Allow to use %s as your Atmosphere handle.', $host_handle), $custom_handle, DI::l10n()->t('When enabled, you can use %s as your Atmosphere handle. After you enabled this option, please go to https://bsky.app/settings and select to change your handle. Select that you have got your own domain. Then enter %s and select "No DNS Panel". Then select "Verify Text File".', $host_handle, $host_handle)];
if ($custom_handle) {
$handle = $host_handle;
}
@ -280,25 +274,31 @@ function bluesky_settings(array &$data)
$friendica_handle = [];
}
$web_frontend = ['' => 'System Default'];
foreach (DI::config()->get('atprotocol', 'frontends') as $key => $frontend) {
$web_frontend[$key] = $frontend[0];
}
$t = Renderer::getMarkupTemplate('connector_settings.tpl', 'addon/bluesky/');
$html = Renderer::replaceMacros($t, [
'$enable' => ['bluesky', DI::l10n()->t('Enable Bluesky Post Addon'), $enabled],
'$bydefault' => ['bluesky_bydefault', DI::l10n()->t('Post to Bluesky by default'), $def_enabled],
'$enable' => ['bluesky', DI::l10n()->t('Enable Atmosphere Addon'), $enabled],
'$bydefault' => ['bluesky_bydefault', DI::l10n()->t('Post to the Atmosphere by default'), $def_enabled],
'$import' => ['bluesky_import', DI::l10n()->t('Import the remote timeline'), $import],
'$import_feeds' => ['bluesky_import_feeds', DI::l10n()->t('Import the pinned feeds'), $import_feeds, DI::l10n()->t('When activated, Posts will be imported from all the feeds that you pinned in Bluesky.')],
'$import_feeds' => ['bluesky_import_feeds', DI::l10n()->t('Import the pinned feeds'), $import_feeds, DI::l10n()->t('When activated, Posts will be imported from all the feeds that you pinned in your Atmosphere client.')],
'$complete_threads' => ['bluesky_complete_threads', DI::l10n()->t('Complete the threads'), $complete_threads, DI::l10n()->t('When activated, the system fetches additional replies for the posts in the timeline. This leads to more complete threads.')],
'$custom_handle' => $friendica_handle,
'$pds' => ['bluesky_pds', DI::l10n()->t('Personal Data Server'), $pds, DI::l10n()->t('The personal data server (PDS) is the system that hosts your profile.'), '', 'readonly'],
'$handle' => ['bluesky_handle', DI::l10n()->t('Bluesky handle'), $handle, '', '', $custom_handle ? 'readonly' : ''],
'$did' => ['bluesky_did', DI::l10n()->t('Bluesky DID'), $did, DI::l10n()->t('This is the unique identifier. It will be fetched automatically, when the handle is entered.'), '', 'readonly'],
'$password' => ['bluesky_password', DI::l10n()->t('Bluesky app password'), '', DI::l10n()->t("Please don't add your real password here, but instead create a specific app password in the Bluesky settings.")],
'$handle' => ['bluesky_handle', DI::l10n()->t('Atmosphere handle'), $handle, '', '', $custom_handle ? 'readonly' : ''],
'$did' => ['bluesky_did', DI::l10n()->t('Atmosphere DID'), $did, DI::l10n()->t('This is the unique identifier. It will be fetched automatically, when the handle is entered.'), '', 'readonly'],
'$password' => ['bluesky_password', DI::l10n()->t('Atmosphere app password'), '', DI::l10n()->t("Please don't add your real password here, but instead create a specific app password in the settings of your Atmosphere client.")],
'$web' => ['bluesky_web', DI::l10n()->t('Web front end'), $web, DI::l10n()->t('Choose your preferred external web front end for displaying posts and profiles.'), $web_frontend, ''],
'$status' => bluesky_get_status($handle, $did, $pds, $token),
]);
$data = [
'connector' => 'bluesky',
'title' => DI::l10n()->t('Bluesky Import/Export'),
'image' => 'images/bluesky.jpg',
'title' => DI::l10n()->t('Atmosphere (Bluesky, Eurosky, Blacksky, ...) Import/Export'),
'image' => 'images/500px-AT_Protocol_logo.png',
'enabled' => $enabled,
'html' => $html,
];
@ -328,7 +328,7 @@ function bluesky_get_status(string $handle = null, string $did = null, string $p
switch ($status) {
case ATProtocol::STATUS_TOKEN_OK:
return DI::l10n()->t("You are authenticated to Bluesky. For security reasons the password isn't stored.");
return DI::l10n()->t("You are authenticated to the Atmosphere PDS. For security reasons the password isn't stored.");
case ATProtocol::STATUS_SUCCESS:
return DI::l10n()->t('The communication with the personal data server service (PDS) is established.');
case ATProtocol::STATUS_API_FAIL;
@ -350,6 +350,8 @@ function bluesky_settings_post(array &$b)
return;
}
DI::atProtocol()->setApiForUser(DI::userSession()->getLocalUserId());
$old_pds = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'pds');
$old_handle = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle');
$old_did = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did');
@ -363,6 +365,11 @@ function bluesky_settings_post(array &$b)
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'import_feeds', intval($_POST['bluesky_import_feeds']));
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'complete_threads', intval($_POST['bluesky_complete_threads']));
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'friendica_handle', intval($_POST['bluesky_friendica_handle'] ?? false));
if ($_POST['bluesky_web'] <> '') {
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'web', $_POST['bluesky_web']);
} else {
DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'bluesky', 'web');
}
if (!empty($handle)) {
$did = DI::atProtocol()->getUserDid(DI::userSession()->getLocalUserId(), empty($old_did) || $old_handle != $handle);
@ -401,7 +408,7 @@ function bluesky_jot_nets(array &$jotnets_fields)
'type' => 'checkbox',
'field' => [
'bluesky_enable',
DI::l10n()->t('Post to Bluesky'),
DI::l10n()->t('Post via the Atmosphere'),
DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'post_by_default')
]
];
@ -435,6 +442,8 @@ function bluesky_cron()
$pconfigs = DBA::selectToArray('pconfig', [], ["`cat` = ? AND `k` IN (?, ?) AND `v`", 'bluesky', 'import', 'import_feeds']);
foreach ($pconfigs as $pconfig) {
DI::atProtocol()->setApiForUser($pconfig['uid']);
if (empty(DI::atProtocol()->getUserDid($pconfig['uid']))) {
DI::logger()->debug('User has got no valid DID', ['uid' => $pconfig['uid']]);
continue;
@ -478,7 +487,7 @@ function bluesky_cron()
$last_clean = DI::keyValue()->get('bluesky_last_clean');
if (empty($last_clean) || ($last_clean + 86400 < time())) {
DI::logger()->notice('Start contact cleanup');
$contacts = DBA::select('account-user-view', ['id', 'pid'], ["`network` = ? AND `uid` != ? AND `rel` = ?", Protocol::BLUESKY, 0, Contact::NOTHING]);
$contacts = DBA::select('account-user-view', ['id', 'pid'], ["`network` = ? AND `uid` != ? AND `rel` = ?", Protocol::ATPROTO, 0, Contact::NOTHING]);
while ($contact = DBA::fetch($contacts)) {
Worker::add(Worker::PRIORITY_LOW, 'MergeContact', $contact['pid'], $contact['id'], 0);
}
@ -505,9 +514,9 @@ function bluesky_hook_fork(array &$b)
}
if (DI::pConfig()->get($post['uid'], 'bluesky', 'import')) {
// Don't post if it isn't a reply to a bluesky post
if (($post['gravity'] != Item::GRAVITY_PARENT) && !Post::exists(['id' => $post['parent'], 'network' => Protocol::BLUESKY])) {
DI::logger()->notice('No bluesky parent found', ['item' => $post['id']]);
// Don't post if it isn't a reply to an Atmosphere post
if (($post['gravity'] != Item::GRAVITY_PARENT) && !Post::exists(['id' => $post['parent'], 'network' => Protocol::ATPROTO])) {
DI::logger()->notice('No Atmosphere parent found', ['item' => $post['id']]);
$b['execute'] = false;
return;
}
@ -549,6 +558,8 @@ function bluesky_post_local(array &$b)
function bluesky_send(array &$b)
{
DI::atProtocol()->setApiForUser($b['uid']);
if (($b['created'] !== $b['edited']) && !$b['deleted']) {
return;
}
@ -563,10 +574,10 @@ function bluesky_send(array &$b)
if ($b['deleted']) {
$uri = DI::atpProcessor()->getUriClass($b['uri']);
if (empty($uri)) {
DI::logger()->debug('Not a bluesky post', ['uri' => $b['uri']]);
DI::logger()->debug('Not an Atmosphere post', ['uri' => $b['uri']]);
return;
}
bluesky_delete_post($b['uri'], $b['uid']);
DI::atProtocol()->deleteRecord($b['uid'], $uri->uri);
return;
}
@ -574,7 +585,7 @@ function bluesky_send(array &$b)
$parent = DI::atpProcessor()->getUriClass($b['thr-parent']);
if (empty($root) || empty($parent)) {
DI::logger()->debug('No bluesky post', ['parent' => $b['parent'], 'thr-parent' => $b['thr-parent']]);
DI::logger()->debug('No Atmosphere post', ['parent' => $b['parent'], 'thr-parent' => $b['thr-parent']]);
return;
}
@ -593,48 +604,31 @@ function bluesky_send(array &$b)
bluesky_create_post($b);
}
function bluesky_create_activity(array $item, stdClass $parent = null)
function bluesky_create_activity(array $item, ?stdClass $parent = null)
{
$uid = $item['uid'];
DI::atProtocol()->setApiForUser($uid);
$token = DI::atProtocol()->getUserToken($uid);
if (empty($token)) {
return;
}
$did = DI::atProtocol()->getUserDid($uid);
if (empty($did)) {
if ($item['verb'] == Activity::LIKE) {
$collection = 'app.bsky.feed.like';
} elseif ($item['verb'] == Activity::ANNOUNCE) {
$collection = 'app.bsky.feed.repost';
} else {
return;
}
$post = [];
$record = [
'subject' => $parent,
'createdAt' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
'$type' => $collection,
];
if ($item['verb'] == Activity::LIKE) {
$record = [
'subject' => $parent,
'createdAt' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
'$type' => 'app.bsky.feed.like'
];
$post = [
'collection' => 'app.bsky.feed.like',
'repo' => $did,
'record' => $record
];
} elseif ($item['verb'] == Activity::ANNOUNCE) {
$record = [
'subject' => $parent,
'createdAt' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
'$type' => 'app.bsky.feed.repost'
];
$post = [
'collection' => 'app.bsky.feed.repost',
'repo' => $did,
'record' => $record
];
}
$activity = DI::atProtocol()->XRPCPost($uid, 'com.atproto.repo.createRecord', $post);
$activity = DI::atProtocol()->createRecord($uid, $collection, $record);
if (empty($activity->uri)) {
return;
}
@ -647,6 +641,8 @@ function bluesky_create_activity(array $item, stdClass $parent = null)
function bluesky_create_post(array $item, stdClass $root = null, stdClass $parent = null)
{
$uid = $item['uid'];
DI::atProtocol()->setApiForUser($uid);
$token = DI::atProtocol()->getUserToken($uid);
if (empty($token)) {
return;
@ -681,7 +677,7 @@ function bluesky_create_post(array $item, stdClass $root = null, stdClass $paren
$urls = bluesky_get_urls($item['body']);
$item['body'] = $urls['body'];
$msg = Plaintext::getPost($item, 300, false, BBCode::BLUESKY);
$msg = Plaintext::getPost($item, 300, false, BBCode::ATPROTOCOL);
foreach ($msg['parts'] as $key => $part) {
$facets = bluesky_get_facets($part, $urls['urls']);
@ -714,13 +710,7 @@ function bluesky_create_post(array $item, stdClass $root = null, stdClass $paren
}
}
$post = [
'collection' => 'app.bsky.feed.post',
'repo' => DI::atProtocol()->getUserDid($uid),
'record' => $record
];
$parent = DI::atProtocol()->XRPCPost($uid, 'com.atproto.repo.createRecord', $post);
$parent = DI::atProtocol()->createRecord($uid, 'app.bsky.feed.post', $record);
if (empty($parent->uri)) {
if ($part == 0) {
Worker::defer();
@ -946,24 +936,15 @@ function bluesky_upload_blob(int $uid, array $photo): ?stdClass
return null;
}
Item::incrementOutbound(Protocol::BLUESKY);
Item::incrementOutbound(Protocol::ATPROTO);
DI::logger()->debug('Uploaded blob', ['return' => $data, 'uid' => $uid, 'retrial' => $retrial, 'height' => $new_height, 'width' => $new_width, 'size' => $new_size, 'orig-height' => $height, 'orig-width' => $width, 'orig-size' => $size]);
return $data->blob;
}
function bluesky_delete_post(string $uri, int $uid)
{
$parts = DI::atpProcessor()->getUriParts($uri);
if (empty($parts)) {
DI::logger()->debug('No uri delected', ['uri' => $uri]);
return;
}
DI::atProtocol()->XRPCPost($uid, 'com.atproto.repo.deleteRecord', $parts);
DI::logger()->debug('Deleted', ['parts' => $parts]);
}
function bluesky_fetch_timeline(int $uid)
{
DI::atProtocol()->setApiForUser($uid);
$data = DI::atProtocol()->XRPCGet('app.bsky.feed.getTimeline', [], $uid);
if (empty($data)) {
return;
@ -1002,7 +983,7 @@ function bluesky_complete_post(stdClass $post, int $uid, int $post_reason, int $
}
if ($complete) {
$uri = DI::atpProcessor()->fetchMissingPost(DI::atpProcessor()->getUri($post), $uid, $post_reason, $causer, 0, '', true, $protocol);
$uri = DI::atpProcessor()->fetchMissingPost(DI::atpProcessor()->getUri($post), $uid, $post_reason, $causer, 0, '', false, $protocol);
$uri_id = DI::atpProcessor()->fetchUriId($uri, $uid);
} else {
$uri_id = DI::atpProcessor()->processPost($post, $uid, $post_reason, $causer, 0, $protocol);
@ -1020,7 +1001,7 @@ function bluesky_process_reason(stdClass $reason, string $uri, int $uid)
$contact = DI::atpActor()->getContactByDID($reason->by->did, $uid, 0);
$item = [
'network' => Protocol::BLUESKY,
'network' => Protocol::ATPROTO,
'protocol' => Conversation::PARCEL_CONNECTOR,
'uid' => $uid,
'wall' => false,
@ -1057,12 +1038,18 @@ function bluesky_process_reason(stdClass $reason, string $uri, int $uid)
function bluesky_fetch_notifications(int $uid)
{
DI::atProtocol()->setApiForUser($uid);
$data = DI::atProtocol()->XRPCGet('app.bsky.notification.listNotifications', [], $uid);
if (empty($data->notifications)) {
return;
}
foreach ($data->notifications as $notification) {
if (DI::contentItem()->isTooOld($notification->indexedAt)) {
continue;
}
$uri = DI::atpProcessor()->getUri($notification);
if (Post::exists(['uri' => $uri, 'uid' => $uid]) || Post::exists(['extid' => $uri, 'uid' => $uid])) {
DI::logger()->debug('Notification already processed', ['uid' => $uid, 'reason' => $notification->reason, 'uri' => $uri, 'indexedAt' => $notification->indexedAt]);
@ -1075,7 +1062,7 @@ function bluesky_fetch_notifications(int $uid)
$item['gravity'] = Item::GRAVITY_ACTIVITY;
$item['body'] = $item['verb'] = Activity::LIKE;
$item['thr-parent'] = DI::atpProcessor()->getUri($notification->record->subject);
$item['thr-parent'] = DI::atpProcessor()->fetchMissingPost($item['thr-parent'], $uid, Item::PR_FETCHED, $item['contact-id'], 0);
$item['thr-parent'] = DI::atpProcessor()->fetchMissingPost($item['thr-parent'], $uid, Item::PR_FETCHED, $item['contact-id'], 0, '', false, Conversation::PARCEL_CONNECTOR);
if (!empty($item['thr-parent'])) {
$data = Item::insert($item);
DI::logger()->debug('Got like', ['uid' => $uid, 'result' => $data, 'uri' => $uri]);
@ -1089,7 +1076,7 @@ function bluesky_fetch_notifications(int $uid)
$item['gravity'] = Item::GRAVITY_ACTIVITY;
$item['body'] = $item['verb'] = Activity::ANNOUNCE;
$item['thr-parent'] = DI::atpProcessor()->getUri($notification->record->subject);
$item['thr-parent'] = DI::atpProcessor()->fetchMissingPost($item['thr-parent'], $uid, Item::PR_FETCHED, $item['contact-id'], 0);
$item['thr-parent'] = DI::atpProcessor()->fetchMissingPost($item['thr-parent'], $uid, Item::PR_FETCHED, $item['contact-id'], 0, '', false, Conversation::PARCEL_CONNECTOR);
if (!empty($item['thr-parent'])) {
$data = Item::insert($item);
DI::logger()->debug('Got repost', ['uid' => $uid, 'result' => $data, 'uri' => $uri]);
@ -1105,19 +1092,19 @@ function bluesky_fetch_notifications(int $uid)
case 'mention':
$contact = DI::atpActor()->getContactByDID($notification->author->did, $uid, 0);
$result = DI::atpProcessor()->fetchMissingPost($uri, $uid, Item::PR_TO, $contact['id'], 0);
$result = DI::atpProcessor()->fetchMissingPost($uri, $uid, Item::PR_TO, $contact['id'], 0, '', false, Conversation::PARCEL_CONNECTOR);
DI::logger()->debug('Got mention', ['uid' => $uid, 'nick' => $contact['nick'], 'result' => $result, 'uri' => $uri]);
break;
case 'reply':
$contact = DI::atpActor()->getContactByDID($notification->author->did, $uid, 0);
$result = DI::atpProcessor()->fetchMissingPost($uri, $uid, Item::PR_COMMENT, $contact['id'], 0);
$result = DI::atpProcessor()->fetchMissingPost($uri, $uid, Item::PR_COMMENT, $contact['id'], 0, '', false, Conversation::PARCEL_CONNECTOR);
DI::logger()->debug('Got reply', ['uid' => $uid, 'nick' => $contact['nick'], 'result' => $result, 'uri' => $uri]);
break;
case 'quote':
$contact = DI::atpActor()->getContactByDID($notification->author->did, $uid, 0);
$result = DI::atpProcessor()->fetchMissingPost($uri, $uid, Item::PR_PUSHED, $contact['id'], 0);
$result = DI::atpProcessor()->fetchMissingPost($uri, $uid, Item::PR_PUSHED, $contact['id'], 0, '', false, Conversation::PARCEL_CONNECTOR);
DI::logger()->debug('Got quote', ['uid' => $uid, 'nick' => $contact['nick'], 'result' => $result, 'uri' => $uri]);
break;
@ -1130,6 +1117,8 @@ function bluesky_fetch_notifications(int $uid)
function bluesky_fetch_feed(int $uid, string $feed)
{
DI::atProtocol()->setApiForUser($uid);
$data = DI::atProtocol()->XRPCGet('app.bsky.feed.getFeed', ['feed' => $feed], $uid);
if (empty($data)) {
return;

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-29 18:16+0000\n"
"POT-Creation-Date: 2026-04-25 10:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,117 +17,126 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: bluesky.php:335
#: bluesky.php:236
msgid "Save Settings"
msgstr ""
#: bluesky.php:336
msgid "Allow your users to use your hostname for their Bluesky handles"
#: bluesky.php:237
msgid "Allow your users to use your hostname for their Atmosphere handles"
msgstr ""
#: bluesky.php:336
#: bluesky.php:237
#, php-format
msgid "Before enabling this option, you have to setup a wildcard domain configuration and you have to enable wildcard requests in your webserver configuration. On Apache this is done by adding \"ServerAlias *.%s\" to your HTTP configuration. You don't need to change the HTTPS configuration."
msgstr ""
#: bluesky.php:365
#: bluesky.php:269
#, php-format
msgid "Allow to use %s as your Bluesky handle."
msgid "Allow to use %s as your Atmosphere handle."
msgstr ""
#: bluesky.php:365
#: bluesky.php:269
#, php-format
msgid "When enabled, you can use %s as your Bluesky handle. After you enabled this option, please go to https://bsky.app/settings and select to change your handle. Select that you have got your own domain. Then enter %s and select \"No DNS Panel\". Then select \"Verify Text File\"."
msgid "When enabled, you can use %s as your Atmosphere handle. After you enabled this option, please go to https://bsky.app/settings and select to change your handle. Select that you have got your own domain. Then enter %s and select \"No DNS Panel\". Then select \"Verify Text File\"."
msgstr ""
#: bluesky.php:375
msgid "Enable Bluesky Post Addon"
#: bluesky.php:284
msgid "Enable Atmosphere Addon"
msgstr ""
#: bluesky.php:376
msgid "Post to Bluesky by default"
#: bluesky.php:285
msgid "Post to the Atmosphere by default"
msgstr ""
#: bluesky.php:377
#: bluesky.php:286
msgid "Import the remote timeline"
msgstr ""
#: bluesky.php:378
#: bluesky.php:287
msgid "Import the pinned feeds"
msgstr ""
#: bluesky.php:378
msgid "When activated, Posts will be imported from all the feeds that you pinned in Bluesky."
#: bluesky.php:287
msgid "When activated, Posts will be imported from all the feeds that you pinned in your Atmosphere client."
msgstr ""
#: bluesky.php:379
#: bluesky.php:288
msgid "Complete the threads"
msgstr ""
#: bluesky.php:379
#: bluesky.php:288
msgid "When activated, the system fetches additional replies for the posts in the timeline. This leads to more complete threads."
msgstr ""
#: bluesky.php:381
#: bluesky.php:290
msgid "Personal Data Server"
msgstr ""
#: bluesky.php:381
#: bluesky.php:290
msgid "The personal data server (PDS) is the system that hosts your profile."
msgstr ""
#: bluesky.php:382
msgid "Bluesky handle"
#: bluesky.php:291
msgid "Atmosphere handle"
msgstr ""
#: bluesky.php:383
msgid "Bluesky DID"
#: bluesky.php:292
msgid "Atmosphere DID"
msgstr ""
#: bluesky.php:383
#: bluesky.php:292
msgid "This is the unique identifier. It will be fetched automatically, when the handle is entered."
msgstr ""
#: bluesky.php:384
msgid "Bluesky app password"
#: bluesky.php:293
msgid "Atmosphere app password"
msgstr ""
#: bluesky.php:384
msgid "Please don't add your real password here, but instead create a specific app password in the Bluesky settings."
#: bluesky.php:293
msgid "Please don't add your real password here, but instead create a specific app password in the settings of your Atmosphere client."
msgstr ""
#: bluesky.php:390
msgid "Bluesky Import/Export"
#: bluesky.php:294
msgid "Web front end"
msgstr ""
#: bluesky.php:400
#: bluesky.php:294
msgid "Choose your preferred external web front end for displaying posts and profiles."
msgstr ""
#: bluesky.php:300
msgid "Atmosphere (Bluesky, Eurosky, Blacksky, ...) Import/Export"
msgstr ""
#: bluesky.php:310
msgid "You are not authenticated. Please enter your handle and the app password."
msgstr ""
#: bluesky.php:420
msgid "You are authenticated to Bluesky. For security reasons the password isn't stored."
#: bluesky.php:331
msgid "You are authenticated to the Atmosphere PDS. For security reasons the password isn't stored."
msgstr ""
#: bluesky.php:422
#: bluesky.php:333
msgid "The communication with the personal data server service (PDS) is established."
msgstr ""
#: bluesky.php:424
msgid "Communication issues with the personal data server service (PDS)."
#: bluesky.php:335
#, php-format
msgid "Communication issues with the personal data server service (PDS): %s"
msgstr ""
#: bluesky.php:426
#: bluesky.php:337
msgid "The DID for the provided handle could not be detected. Please check if you entered the correct handle."
msgstr ""
#: bluesky.php:428
#: bluesky.php:339
msgid "The personal data server service (PDS) could not be detected."
msgstr ""
#: bluesky.php:430
#: bluesky.php:341
msgid "The authentication with the provided handle and password failed. Please check if you entered the correct password."
msgstr ""
#: bluesky.php:492
msgid "Post to Bluesky"
#: bluesky.php:411
msgid "Post via the Atmosphere"
msgstr ""

View file

@ -10,4 +10,5 @@
{{include file="field_input.tpl" field=$pds}}
{{include file="field_input.tpl" field=$handle}}
{{include file="field_input.tpl" field=$did}}
{{include file="field_input.tpl" field=$password}}
{{include file="field_input.tpl" field=$password}}
{{include file="field_select.tpl" field=$web}}

View file

@ -2,11 +2,10 @@
/*
* Name: invidious
* Description: Replaces links to youtube.com to an invidious instance in all displays of postings on a node.
* Version: 0.4
* Version: 0.7
* Author: Matthias Ebers <https://loma.ml/profile/feb>
* Author: Michael Vogel <https://pirati.ca/profile/heluecht>
* Status: Unsupported
* Note: Please use the URL Replace addon instead
* Status:
*/
use Friendica\Core\Hook;
@ -26,7 +25,11 @@ function invidious_install()
*/
function invidious_addon_admin_post()
{
DI::config()->set('invidious', 'server', trim($_POST['invidiousserver'], " \n\r\t\v\x00/"));
// Sanitize and validate the input as a valid URL
$url = filter_var(trim($_POST['invidiousserver'], " \n\r\t\v\x00/"), FILTER_VALIDATE_URL);
if ($url !== false) {
DI::config()->set('invidious', 'server', $url);
}
}
/* Hook into the admin settings to let the admin choose an
@ -54,8 +57,8 @@ function invidious_settings(array &$data)
$t = Renderer::getMarkupTemplate('settings.tpl', 'addon/invidious/');
$html = Renderer::replaceMacros($t, [
'$enabled' => ['invidious-enabled', DI::l10n()->t('Replace Youtube links with links to an Invidious server'), $enabled, DI::l10n()->t('If enabled, Youtube links are replaced with the links to the specified Invidious server.')],
'$server' => ['invidious-server', DI::l10n()->t('Invidious server'), $server, DI::l10n()->t('See %s for a list of available Invidious servers.', '<a href="https://api.invidious.io/">https://api.invidious.io/</a>')],
'$enabled' => ['enabled', DI::l10n()->t('Replace Youtube links with links to an Invidious server'), $enabled, DI::l10n()->t('If enabled, Youtube links are replaced with the links to the specified Invidious server.')],
'$server' => ['server', DI::l10n()->t('Invidious server'), $server, DI::l10n()->t('See %s for a list of available Invidious servers.', '<a href="https://api.invidious.io/">https://api.invidious.io/</a>')],
]);
$data = [
@ -71,11 +74,13 @@ function invidious_settings_post(array &$b)
return;
}
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'invidious', 'enabled', (bool)$_POST['invidious-enabled']);
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'invidious', 'enabled', (bool)$_POST['enabled']);
$server = trim($_POST['invidious-server'], " \n\r\t\v\x00/");
if ($server != DI::config()->get('invidious', 'server', INVIDIOUS_DEFAULT) && !empty($server)) {
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'invidious', 'server', $server);
$server = trim($_POST['server'], " \n\r\t\v\x00/");
// Sanitize and validate the server URL before saving
$validatedServer = filter_var($server, FILTER_VALIDATE_URL);
if ($validatedServer !== false && $validatedServer != DI::config()->get('invidious', 'server', INVIDIOUS_DEFAULT)) {
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'invidious', 'server', $validatedServer);
} else {
DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'invidious', 'server');
}
@ -86,21 +91,37 @@ function invidious_settings_post(array &$b)
*/
function invidious_render(array &$b)
{
if (!DI::userSession()->getLocalUserId() || !DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'invidious', 'enabled')) {
return;
}
if (!DI::userSession()->getLocalUserId() || !DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'invidious', 'enabled')) {
return;
}
$original = $b['html'];
$server = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'invidious', 'server', DI::config()->get('invidious', 'server', INVIDIOUS_DEFAULT));
$original = $b['html'];
$b['html'] = preg_replace("~https?://(?:www\.)?youtube\.com/watch\?v=(.*?)~ism", $server . '/watch?v=$1', $b['html']);
$b['html'] = preg_replace("~https?://(?:www\.)?youtube\.com/embed/(.*?)~ism", $server . '/embed/$1', $b['html']);
$b['html'] = preg_replace("~https?://(?:www\.)?youtube\.com/shorts/(.*?)~ism", $server . '/shorts/$1', $b['html']);
$b['html'] = preg_replace ("/https?:\/\/music.youtube.com\/(.*?)/ism", $server . '/watch?v=$1', $b['html']);
$b['html'] = preg_replace ("/https?:\/\/m.youtube.com\/(.*?)/ism", $server . '/watch?v=$1', $b['html']);
$b['html'] = preg_replace("/https?:\/\/youtu.be\/(.*?)/ism", $server . '/watch?v=$1', $b['html']);
$server = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'invidious', 'server', DI::config()->get('invidious', 'server', INVIDIOUS_DEFAULT));
$server = rtrim($server, '/');
if ($original != $b['html']) {
$b['html'] .= '<hr><p><small>' . DI::l10n()->t('(Invidious addon enabled: YouTube links via %s)', $server) . '</small></p>';
}
$pattern = "~https?://(?:(?:www\.|m\.|music\.)?youtube\.com/(?:watch\?v=|embed/|shorts/)|youtu\.be/)([a-zA-Z0-9_-]{11})([^ \n\r\t\v\x00\"<>]*[?&][tT]=[^ \n\r\t\v\x00\"<>]*)?~ism";
$b['html'] = preg_replace_callback($pattern, function($matches) use ($server) {
$videoId = $matches[1];
$params = $matches[2] ?? '';
if (!empty($params)) {
$params = str_replace('&amp;', '&', $params);
if (strpos($params, '?') === false && strpos($params, '&') === 0) {
$params = '?' . ltrim($params, '&');
}
}
return $server . '/watch?v=' . $videoId . $params;
}, $b['html']);
if ($original != $b['html']) {
$displayHost = parse_url($server, PHP_URL_HOST);
$serverLink = '<a href="' . $server . '" target="_blank" rel="noopener noreferrer">' . $displayHost . '</a>';
$b['html'] .= '<hr><p><small class="invidious-note">' .
DI::l10n()->t('(Invidious addon enabled: YouTube links via %s)', $serverLink) .
'</small></p>';
}
}

View file

@ -74,6 +74,7 @@ function irc_content()
{
$baseurl = DI::baseUrl() . '/addon/irc';
$o = '';
$usernick = '';
/* set the list of popular channels */
if (DI::userSession()->getLocalUserId()) {
@ -81,6 +82,7 @@ function irc_content()
if (!$sitechats) {
$sitechats = DI::config()->get('irc', 'sitechats');
}
$usernick = "nick=" . DI::userSession()->getLocalUserNickname() . "&";
} else {
$sitechats = DI::config()->get('irc','sitechats');
}
@ -117,7 +119,7 @@ function irc_content()
$o .= <<< EOT
<h2>IRC chat</h2>
<p><a href="https://tldp.org/HOWTO/IRC/beginners.html" target="_blank" rel="noopener noreferrer">A beginner's guide to using IRC. [en]</a></p>
<iframe src="//web.libera.chat?channels=$channels" style="width:100%; max-width:900px; height: 600px;"></iframe>
<iframe src="//web.libera.chat?{$usernick}channels=$channels" style="width:100%; max-width:900px; height: 600px;"></iframe>
EOT;
return $o;

View file

@ -192,7 +192,7 @@ class js_upload_qqFileUploader
*/
private $file;
function __construct(array $allowedExtensions = [], $sizeLimit)
function __construct(array $allowedExtensions, $sizeLimit)
{
$allowedExtensions = array_map('strtolower', $allowedExtensions);

View file

@ -166,7 +166,35 @@ function mailstream_send_hook(array $data)
return;
}
if (!mailstream_send($data['message_id'], $item, $user)) {
$author = DBA::selectFirst('contact', ['nick', 'blocked', 'uri-id'], ['id' => $data['author-id'], 'self' => false]);
if (!DBA::isResult($author)) {
DI::logger()->error('could not find author', ['guid' => $item['guid'], 'author-id' => $data['author-id']]);
return;
}
if ($author['blocked']) {
DI::logger()->info('author is blocked', ['guid' => $item['guid'], 'author-id' => $data['author-id']]);
return;
}
$collapsed = false;
$user_contact = DBA::selectFirst('user-contact', ['cid', 'blocked', 'ignored', 'collapsed'], ['uid' => $item['uid'], 'uri-id' => $item['author-uri-id']]);
if (!DBA::isResult($user_contact)) {
$user_contact = DBA::selectFirst('user-contact', ['cid', 'blocked', 'ignored', 'collapsed'], ['uid' => $item['uid'], 'cid' => $item['author-id']]);
}
if (DBA::isResult($user_contact)) {
if ($user_contact['blocked']) {
DI::logger()->info('author is blocked', ['guid' => $item['guid'], 'cid' => $user_contact['cid']]);
return;
}
if ($user_contact['ignored']) {
DI::logger()->info('author is ignored', ['guid' => $item['guid'], 'cid' => $user_contact['cid']]);
return;
}
if ($user_contact['collapsed']) {
$collapsed = true;
}
}
if (!mailstream_send($data['message_id'], $item, $user, $collapsed)) {
DI::logger()->debug('send failed, will retry', $data);
if (!Worker::defer()) {
DI::logger()->error('failed and could not defer', $data);
@ -220,6 +248,7 @@ function mailstream_post_hook(array &$item)
$send_hook_data = [
'uid' => $item['uid'],
'contact-id' => $item['contact-id'],
'author-id' => $item['author-id'],
'uri' => $item['uri'],
'message_id' => $message_id,
'tries' => 0,
@ -260,6 +289,7 @@ function mailstream_do_images(array &$item, array &$attachments)
$cookiejar = tempnam(System::getTempPath(), 'cookiejar-mailstream-');
try {
$curlResult = DI::httpClient()->get($url, HttpClientAccept::DEFAULT, [HttpClientOptions::COOKIEJAR => $cookiejar]);
unlink($cookiejar);
if (!$curlResult->isSuccess()) {
DI::logger()->debug('mailstream: fetch image url failed', [
'url' => $url,
@ -406,10 +436,11 @@ function mailstream_subject(array $item): string
* @param string $message_id ID of the message (RFC 1036)
* @param array $item content of the item
* @param array $user results from the user table
* @param bool $collapsed true if the content should be hidden
*
* @return bool True if this message has been completed. False if it should be retried.
*/
function mailstream_send(string $message_id, array $item, array $user): bool
function mailstream_send(string $message_id, array $item, array $user, bool $collapsed): bool
{
if (!is_array($item)) {
DI::logger()->error('item is empty', ['message_id' => $message_id]);
@ -427,10 +458,16 @@ function mailstream_send(string $message_id, array $item, array $user): bool
require_once(dirname(__file__) . '/phpmailer/class.phpmailer.php');
$item['body'] = Post\Media::addAttachmentsToBody($item['uri-id'], $item['body']);
if ($collapsed) {
$item['body'] = DI::l10n()->t('Content from %s is collapsed', $item['author-name']);
} else {
$item['body'] = Post\Media::addAttachmentsToBody($item['uri-id'], $item['body']);
}
$attachments = [];
mailstream_do_images($item, $attachments);
if (!$collapsed) {
mailstream_do_images($item, $attachments);
}
$frommail = DI::config()->get('mailstream', 'frommail');
if ($frommail == '') {
$frommail = 'friendica@localhost.local';

45
quickphoto/README.md Normal file
View file

@ -0,0 +1,45 @@
# QuickPhoto Addon for Friendica
QuickPhoto is a Friendica addon that simplifies working with images in the editor. It automatically replaces long, cumbersome BBCode structures with a compact shorthand notation, without affecting functionality or compatibility.
---
## Features
- **Automatic Simplification:** Converts "monster BBCodes" like `[url=...][img=...]...[/img][/url]` instantly into the handy format `[img]|filename description[/img]`.
- **Intelligent Reconstruction:** Before submitting or previewing, the shorthand code is quickly converted back into the original, valid Friendica BBCode.
- **Real-Time Processing:** Responds immediately to drag & drop, copy & paste, and inserting images via editor buttons.
- **Focus Safety:** Cursor management ensures the focus remains stable during automatic conversion while typing.
- **Maximum Compatibility:** Supports both the standard Jot editor and the Compose module, as well as reply fields.
- **Local Cache:** Image data is securely stored in the browser's localStorage and automatically cleared after 12 hours.
---
## How It Works
The addon operates in a hybrid manner:
- **Frontend:** A JavaScript watcher scans textareas and simplifies complex image links for better readability while writing.
- **Interface:** It integrates deeply with Friendica's jQuery functions to ensure that preview and save functions always receive the correct original data.
- **Events:** By intercepting submit and preview clicks, it guarantees that shorthand codes are never sent to the server in a format it cannot interpret.
---
## Installation
1. Create a folder named `quickphoto` in the `addon/` directory of your Friendica installation.
2. Place the file `quickphoto.php` in this folder.
3. Place the file `quickphoto.js` in the same folder.
4. Enable the addon in the Friendica administration area under **Addons**.
---
MIT License
Copyright (c) 2024-2026 Friendica Project & Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,22 @@
# ADDON quickphoto
# Copyright (C)
# This file is distributed under the same license as the Friendica quickphoto addon package.
#
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-07 10:18+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: quickphoto.php:18
msgid "Image description"
msgstr ""

126
quickphoto/quickphoto.js Normal file
View file

@ -0,0 +1,126 @@
(function() {
const monsterPattern = /\[url=(.*?)\]\[img=(.*?)\](.*?)\[\/img\]\[\/url\]/gi;
let throttleTimer;
const i18nDesc = (window.qp_i18n && window.qp_i18n.imageDesc) ? window.qp_i18n.imageDesc : "Image description";
const cleanupOldEntries = () => {
const now = Date.now();
const twelveHours = 12 * 60 * 60 * 1000;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('qp_')) {
try {
const data = JSON.parse(localStorage.getItem(key));
if (data && data.timestamp && (now - data.timestamp > twelveHours)) {
localStorage.removeItem(key);
}
} catch (e) { localStorage.removeItem(key); }
}
}
};
const simplify = (text) => {
if (!text || !text.includes('[url=')) return text;
return text.replace(monsterPattern, (match, urlPart, imgPart, existingDesc) => {
const fileName = imgPart.split('/').pop();
const storageKey = `qp_${fileName}`;
localStorage.setItem(storageKey, JSON.stringify({
url: urlPart,
img: imgPart,
timestamp: Date.now()
}));
let userDesc = existingDesc.trim() || i18nDesc;
return `[img]${fileName}|${userDesc}[/img]`;
});
};
const reconstruct = (text) => {
if (!text || !text.includes('[img]')) return text;
return text.replace(/\[img\](.*?)\|(.*?)\[\/img\]/g, (match, fileName, desc) => {
const data = localStorage.getItem(`qp_${fileName}`);
if (data) {
const parsed = JSON.parse(data);
const finalDesc = (desc === i18nDesc) ? "" : desc;
return `[url=${parsed.url}][img=${parsed.img}]${finalDesc}[/img][/url]`;
}
return match;
});
};
const applySimplify = (textarea) => {
if (!textarea || !textarea.value || !textarea.value.includes('[/img]')) return;
(window.requestIdleCallback || function(cb) { return setTimeout(cb, 1); })(() => {
const current = textarea.value;
const simple = simplify(current);
if (current !== simple) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
textarea.value = simple;
textarea.setSelectionRange(start, end);
}
});
};
if (typeof jQuery !== 'undefined') {
const originalVal = jQuery.fn.val;
jQuery.fn.val = function(value) {
if (arguments.length === 0 && this.is('textarea')) {
return reconstruct(originalVal.call(this));
}
if (arguments.length > 0 && this.is('textarea')) {
return originalVal.call(this, simplify(value));
}
return originalVal.apply(this, arguments);
};
}
document.addEventListener('drop', (e) => {
if (e.target.tagName === 'TEXTAREA') {
setTimeout(() => applySimplify(e.target), 150);
}
}, true);
document.addEventListener('input', (e) => {
if (e.target.tagName === 'TEXTAREA') {
clearTimeout(throttleTimer);
throttleTimer = setTimeout(() => applySimplify(e.target), 500);
}
});
document.addEventListener('click', (e) => {
const btn = e.target.closest(
'#wall-submit-preview, #profile-jot-submit, #wall-submit-submit, #jot-submit, ' +
'[id^="comment-edit-submit-"], [id^="comment-edit-preview-link-"]'
);
if (btn) {
const textareas = document.querySelectorAll('textarea');
if (textareas.length > 0) {
textareas.forEach(textarea => {
textarea.value = reconstruct(textarea.value);
if (btn.id.includes('preview')) {
setTimeout(() => applySimplify(textarea), 1000);
}
});
}
}
}, true);
setInterval(() => {
if (document.hidden) return;
const textareas = document.querySelectorAll('textarea');
if (textareas.length === 0) return;
textareas.forEach(textarea => {
if (textarea.offsetParent !== null) {
applySimplify(textarea);
}
});
}, 2500);
cleanupOldEntries();
})();

28
quickphoto/quickphoto.php Normal file
View file

@ -0,0 +1,28 @@
<?php
/**
* Name: QuickPhoto
* Description: Replaces the BBCode for inserted images and provides a placeholder for image descriptions.
* Version: 1.2
* Author: Matthias Ebers <https://loma.ml/profile/feb>
*/
use Friendica\Core\Hook;
use Friendica\DI;
function quickphoto_install() {
Hook::register('page_header', 'addon/quickphoto/quickphoto.php', 'quickphoto_header');
Hook::register('post_post', 'addon/quickphoto/quickphoto.php', 'quickphoto_post_hook');
}
function quickphoto_header(&$header) {
$desc_label = DI::l10n()->t('Image description');
$js_label = addslashes($desc_label);
$header .= "\n" . '<script type="text/javascript">var qp_i18n = { imageDesc: "' . $js_label . '" };</script>';
$header .= "\n" . '<script type="text/javascript" src="/addon/quickphoto/quickphoto.js?v=5.1"></script>' . "\n";
}
function quickphoto_post_hook(&$item) {
// Placeholder
}

View file

@ -81,7 +81,7 @@ function get_body_length($body)
/** @var DOMNodeList $xr */
$xr = $xpath->query('//*[@style]');
foreach ($xr as $node) {
if (preg_match('/.*display: *none *;.*/',$node->getAttribute('style'))) {
if ($node instanceof DOMElement && preg_match('/.*display: *none *;.*/',$node->getAttribute('style'))) {
// Hidden, remove it from its parent
$node->parentNode->removeChild($node);
}

View file

@ -2,7 +2,7 @@
/**
* Name: Smileybutton
* Description: Adds a smileybutton to the Inputbox
* Version: 1.0
* Version: 1.1
* Author: Johannes Schwab <https://friendica.jschwab.org/profile/ddorian>
* Maintainer: Hypolite Petovan <https://friendica.mrpetovan.com/profile/hypolite>
*/
@ -18,14 +18,9 @@ function smileybutton_install()
function smileybutton_jot_tool(string &$body)
{
// Disable if theme is quattro
if (DI::appHelper()->getCurrentTheme() == 'quattro') {
return;
}
// Disable for mobile because they have a smiley key of their own
// this plugin may have smilies mobile devices do not have, disable for mobile by uncommenting return below
if (DI::mode()->isMobile() || DI::mode()->isMobile()) {
return;
// return;
}
$texts = [
@ -85,12 +80,12 @@ function smileybutton_jot_tool(string &$body)
Hook::callAll('smilie', $params);
//Generate html for smiley list
$s = '<table class="smiley-preview"><tr>';
$s = '<div class="smiley-preview">';
for ($x = 0; $x < count($params['texts']); $x++) {
$icon = $params['icons'][$x];
$s .= '<td onclick="smileybutton_addsmiley(\'' . $params['texts'][$x] . '\')">' . $icon . '</td>';
$s .= '<span onclick="smileybutton_addsmiley(\'' . $params['texts'][$x] . '\')">' . $icon . '</span>';
if (($x + 1) % (floor(sqrt(count($params['texts']))) + 1) == 0) {
$s .= '</tr><tr>';
$s .= '</div>';
}
}
$s .= '</tr></table>';
@ -112,7 +107,7 @@ function smileybutton_jot_tool(string &$body)
$image_url = DI::baseUrl() . '/' . $image;
//Add the hmtl and script to the page
$body = <<< EOT
$body .= <<< EOT
<div id="profile-smiley-wrapper">
<button type="button" class="btn btn-link smiley_button" onclick="toggle_smileybutton()"><img src="$image_url" alt="smiley"></button>
<div id="smileybutton">

View file

@ -1,3 +1,14 @@
/* fix positioning if more than one jot tool */
.jotplugins > div,
#profile-jot-plugin-wrapper > div {
float: left;
}
.jotplugins::after,
#profile-jot-plugin-wrapper::after {
content: '';
display: block;
clear: both;
}
#profile-smiley-wrapper {
display: block;
}
@ -11,15 +22,36 @@
width: 18px;
}
table.smiley-preview img.smiley {
div.smiley-preview img.smiley {
max-height: 25px;
max-width: 25px;
}
table.smiley-preview {
border: 1px solid #AAAAAA;
}
table.smiley-preview td {
cursor: pointer;
vertical-align: baseline;
}
div.smiley-preview {
border: 1px solid #AAAAAA;
max-height: 200px;
overflow: auto;
}
div.smiley-preview > span {
cursor: pointer;
font-size: 24px;
padding: 5px;
text-align: center;
width: 45px;
height: 45px;
line-height: 45px;
float: left;
display: block;
}
div.smiley-preview > span:hover,
div.smiley-preview > span:focus {
background-color: rgba(0,0,0,.1);
}
div.smiley-preview::after {
content: '';
display: block;
clear: both;
}

View file

@ -1,3 +1,15 @@
/* fix positioning if more than one jot tool */
.jotplugins > div,
#profile-jot-plugin-wrapper > div {
float: left;
}
.jotplugins::after,
#profile-jot-plugin-wrapper::after {
content: '';
display: block;
clear: both;
}
#profile-smiley-wrapper {
display: block;
}
@ -17,21 +29,41 @@
background: none;
}
table.smiley-preview img.smiley {
div.smiley-preview img.smiley {
max-height: 25px;
max-width: 25px;
cursor: pointer;
vertical-align: baseline;
}
table.smiley-preview {
div.smiley-preview {
border: 1px solid #AAAAAA;
-moz-border-radius: 3px;
border-radius: 3px;
position: relative;
left: 285px;
left: auto;
top: -36px;
max-height: 200px;
overflow: auto;
}
table.smiley-preview td {
div.smiley-preview > span {
cursor: pointer;
font-size: 24px;
padding: 5px;
text-align: center;
width: 45px;
height: 45px;
line-height: 45px;
float: left;
display: block;
}
div.smiley-preview > span:hover,
div.smiley-preview > span:focus {
background-color: rgba(0,0,0,.1);
}
div.smiley-preview::after {
content: '';
display: block;
clear: both;
}

View file

@ -1,29 +1,80 @@
/* fix positioning if more than one jot tool */
.jotplugins > div,
#profile-jot-plugin-wrapper > div {
float: left;
}
.jotplugins::after,
#profile-jot-plugin-wrapper::after {
content: '';
display: block;
clear: both;
}
#profile-smiley-wrapper {
display: block;
}
#smileybutton {
display: none;
position: fixed;
background-color: #FFF;
width: auto;
border-radius: 8px;
padding: 10px;
-webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
box-shadow: 0 6px 12px rgba(0,0,0,.175);
}
.jotplugins #smileybutton {
position: absolute;
}
/* image does not work with Frio schemes use icon font */
.smiley_button {
-webkit-box-shadow: none !important;
box-shadow: none !important;
}
.smiley_button > img {
height: 14px;
width: 14px;
display: none;
}
.smiley_button::before {
content: '\f055';
font-family: ForkAwesome;
font-size: inherit;
color: inherit;
}
table.smiley-preview img.smiley {
div.smiley-preview img.smiley {
max-height: 25px;
max-width: 25px;
}
table.smiley-preview {
border: 1px solid #AAAAAA;
}
table.smiley-preview td {
cursor: pointer;
vertical-align: baseline;
}
div.smiley-preview {
border: none;
max-height: 200px;
overflow: auto;
}
div.smiley-preview > span {
cursor: pointer;
font-size: 24px;
padding: 5px;
text-align: center;
width: 45px;
height: 45px;
line-height: 45px;
float: left;
display: block;
}
div.smiley-preview > span:hover,
div.smiley-preview > span:focus {
background-color: rgba(0,0,0,.1);
}
div.smiley-preview::after {
content: '';
display: block;
clear: both;
}
#profile-smiley-wrapper > .btn-link {
position: relative;
display: block;

View file

@ -0,0 +1,64 @@
/* fix positioning if more than one jot tool */
.jotplugins > div,
#profile-jot-plugin-wrapper > div {
float: left;
}
.jotplugins::after,
#profile-jot-plugin-wrapper::after {
content: '';
display: block;
clear: both;
}
#profile-smiley-wrapper {
display: block;
margin-bottom: -100px;
}
#smileybutton {
display: none;
position: absolute;
max-width: 770px;
z-index: 99;
background-color: white;
}
.smiley_button {
height: 42px;
}
.smiley_button > img {
height: 18px;
width: 18px;
}
div.smiley-preview img.smiley {
max-height: 25px;
max-width: 25px;
cursor: pointer;
vertical-align: baseline;
}
div.smiley-preview {
border: 1px solid #AAAAAA;
max-height: 200px;
overflow: auto;
}
div.smiley-preview > span {
cursor: pointer;
font-size: 24px;
padding: 5px;
text-align: center;
width: 45px;
height: 45px;
line-height: 45px;
float: left;
display: block;
}
div.smiley-preview > span:hover,
div.smiley-preview > span:focus {
background-color: rgba(0,0,0,.1);
}
div.smiley-preview::after {
content: '';
display: block;
clear: both;
}

View file

@ -1,34 +1,80 @@
/* fix positioning if more than one jot tool */
#profile-jot-plugin-wrapper {
width: 100%;
margin-top: 10px;
}
.jotplugins > div,
#profile-jot-plugin-wrapper > div {
float: left;
}
.jotplugins::after,
#profile-jot-plugin-wrapper::after {
content: '';
display: block;
clear: both;
}
#profile-smiley-wrapper {
display: block;
}
#smileybutton {
display: none;
position: absolute;
background-color: #FFF;
width: auto;
border-radius: 8px;
padding: 10px;
z-index: 99;
-webkit-box-shadow: 0 0 5px rgba(0,0,0,.7);
box-shadow: 0 0 5px rgba(0,0,0,.7);
}
.jotplugins #smileybutton {
position: absolute;
}
.smiley_button > img {
height: 22px;
width: 22px;
position: relative;
left: -330px;
left: 0px;
margin: 4px;
-moz-border-radius: 0px;
border-radius: 0px;
}
table.smiley-preview img.smiley {
div.smiley-preview img.smiley {
max-height: 25px;
max-width: 25px;
cursor: pointer;
vertical-align: baseline;
}
table.smiley-preview {
border: 1px solid #AAAAAA;
div.smiley-preview {
border: none;
-moz-border-radius: 5px;
border-radius: 5px;
margin: 5px;
max-height: 200px;
overflow: auto;
}
table.smiley-preview td {
div.smiley-preview > span {
cursor: pointer;
font-size: 24px;
padding: 5px;
text-align: center;
width: 45px;
height: 45px;
line-height: 45px;
float: left;
display: block;
}
div.smiley-preview > span:hover,
div.smiley-preview > span:focus {
background-color: rgba(0,0,0,.1);
}
div.smiley-preview::after {
content: '';
display: block;
clear: both;
}

View file

@ -1,3 +1,15 @@
/* fix positioning if more than one jot tool */
.jotplugins > div,
#profile-jot-plugin-wrapper > div {
float: left;
}
.jotplugins::after,
#profile-jot-plugin-wrapper::after {
content: '';
display: block;
clear: both;
}
#profile-smiley-wrapper {
float: left;
margin-left: 15px;
@ -33,17 +45,37 @@
margin-right: 18px;
}
table.smiley-preview {
div.smiley-preview {
background-color: #FFF;
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.7);
max-height: 200px;
overflow: auto;
}
table.smiley-preview img.smiley {
div.smiley-preview img.smiley {
max-height: 25px;
max-width: 25px;
cursor: pointer;
vertical-align: baseline;
}
table.smiley-preview td {
div.smiley-preview > span {
cursor: pointer;
font-size: 24px;
padding: 5px;
text-align: center;
width: 45px;
height: 45px;
line-height: 45px;
float: left;
display: block;
}
div.smiley-preview > span:hover,
div.smiley-preview > span:focus {
background-color: rgba(0,0,0,.1);
}
div.smiley-preview::after {
content: '';
display: block;
clear: both;
}

View file

@ -223,16 +223,6 @@ class CodebirdSN
$apiparams = $params[0];
} else {
parse_str($params[0], $apiparams);
// remove auto-added slashes if on magic quotes steroids
if (get_magic_quotes_gpc()) {
foreach($apiparams as $key => $value) {
if (is_array($value)) {
$apiparams[$key] = array_map('stripslashes', $value);
} else {
$apiparams[$key] = stripslashes($value);
}
}
}
}
}