Merge pull request #13635 from gudzpoz/emojis-please

Improve emoji federation (ActivityPub) and Mastodon API compliance
This commit is contained in:
Hypolite Petovan 2023-11-16 03:26:34 -08:00 committed by GitHub
commit 21227453e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 569 additions and 33 deletions

View file

@ -21,6 +21,7 @@
namespace Friendica\Content;
use Friendica\Content\Text\BBCode;
use Friendica\Core\Hook;
use Friendica\DI;
use Friendica\Util\Strings;
@ -67,7 +68,7 @@ class Smilies
*/
public static function getList(): array
{
$texts = [
$texts = [
'<3',
'</3',
'<\\3',
@ -152,6 +153,155 @@ class Smilies
return $params;
}
/**
* Normalizes smiley shortcodes into texts with no special symbols.
*
* @return array
* 'texts' => smilie shortcut
* 'icons' => icon url or an empty string
* 'norms' => normalized shortcut
*/
public static function getNormalizedList(): array
{
$smilies = self::getList();
$norms = [];
$icons = $smilies['icons'];
foreach ($smilies['texts'] as $i => $shortcode) {
// Extract urls
$icon = $icons[$i];
if (preg_match('/src="(.+?)"/', $icon, $match)) {
$icon = $match[1];
} else {
$icon = '';
}
$icons[$i] = $icon;
// Normalize name
$norm = preg_replace('/[\s\-:#~]/', '', $shortcode);
if (ctype_alnum($norm)) {
$norms[] = $norm;
} elseif (preg_match('#/smiley-(\w+)\.gif#', $icon, $match)) {
$norms[] = $match[1];
} else {
$norms[] = 'smiley' . $i;
}
}
$smilies['norms'] = $norms;
return $smilies;
}
/**
* Finds all used smilies (denoted by quoting colons like :heart:) in the provided text and normalizes their usages.
*
* @param string $text that might contain smiley usages
* @return array with smilie codes (colon included) as the keys, their image urls as values;
* the normalized string is put under the '' (empty string) key
*/
public static function extractUsedSmilies(string $text, string &$normalized = null): array
{
$emojis = [];
$normalized = BBCode::performWithEscapedTags($text, ['code'], function ($text) use (&$emojis) {
return BBCode::performWithEscapedTags($text, ['noparse', 'nobb', 'pre'], function ($text) use (&$emojis) {
if (strpos($text, '[nosmile]') !== false || self::noSmilies()) {
return $text;
}
$smilies = self::getNormalizedList();
$normalized = array_combine($smilies['texts'], $smilies['norms']);
return self::performForEachWordMatch(
array_combine($smilies['texts'], $smilies['icons']),
$text,
function (string $name, string $image) use($normalized, &$emojis) {
$name = $normalized[$name];
if (preg_match('/src="(.+?)"/', $image, $match)) {
$image = $match[1];
$emojis[$name] = $image;
}
return ':' . $name . ':';
},
);
});
});
return $emojis;
}
/**
* Similar to strtr but matches only whole words and replaces texts with $callback.
*
* @param array $words
* @param string $subject
* @param callable $callback ($offset, $value)
* @return string
*/
private static function performForEachWordMatch(array $words, string $subject, callable $callback): string
{
$ord1_bitset = 0;
$ord2_bitset = 0;
$prefixes = [];
foreach ($words as $word => $_) {
if (strlen($word) < 2 || !ctype_graph($word)) {
continue;
}
$ord1 = ord($word);
$ord2 = ord($word[1]);
$ord1_bitset |= 1 << ($ord1 & 31);
$ord2_bitset |= 1 << ($ord2 & 31);
if (!array_key_exists($word[0], $prefixes)) {
$prefixes[$word[0]] = [];
}
$prefixes[$word[0]][] = $word;
}
$result = '';
$processed = 0;
$s_start = 0; // Segment start
// No spaces are allowed in smilies, so they can serve as delimiters.
// Splitting by some delimiters may not necessary though?
while (true) {
if ($s_start >= strlen($subject)) {
$result .= substr($subject, $processed);
break;
}
if (preg_match('/\s+?(?=\S|$)/', $subject, $match, PREG_OFFSET_CAPTURE, $s_start)) {
[$whitespaces, $s_end] = $match[0];
} else {
$s_end = strlen($subject);
$whitespaces = '';
}
$s_length = $s_end - $s_start;
if ($s_length > 1) {
$segment = substr($subject, $s_start, $s_length);
// Find possible starting points for smilies.
// For built-in smilies, the two bitsets should make attempts quite efficient.
// However, presuming custom smilies follow the format of ":shortcode" or ":shortcode:",
// if the user adds more smilies (with addons), the second bitset may eventually become useless.
for ($i = 0; $i < $s_length - 1; $i++) {
$c = $segment[$i];
$d = $segment[$i + 1];
if (($ord1_bitset & (1 << (ord($c) & 31))) && ($ord2_bitset & (1 << (ord($d) & 31))) && array_key_exists($c, $prefixes)) {
foreach ($prefixes[$c] as $word) {
$wlength = strlen($word);
if ($wlength <= $s_length - $i && substr($segment, $i, $wlength) === $word) {
// Check for boundaries
if (($i === 0 || ctype_space($segment[$i - 1]) || ctype_punct($segment[$i - 1]))
&& ($i + $wlength >= $s_length || ctype_space($segment[$i + $wlength]) || ctype_punct($segment[$i + $wlength]))) {
$result .= substr($subject, $processed, $s_start - $processed + $i);
$result .= call_user_func($callback, $word, $words[$word]);
$i += $wlength;
$processed = $s_start + $i;
$i--;
break;
}
}
}
}
}
}
$s_start = $s_end + strlen($whitespaces);
}
return $result;
}
/**
* Copied from http://php.net/manual/en/function.str-replace.php#88569
@ -170,7 +320,13 @@ class Smilies
*/
private static function strOrigReplace(array $search, array $replace, string $subject): string
{
return strtr($subject, array_combine($search, $replace));
return self::performForEachWordMatch(
array_combine($search, $replace),
$subject,
function (string $_, string $value) {
return $value;
}
);
}
/**
@ -199,6 +355,12 @@ class Smilies
return $s;
}
private static function noSmilies(): bool {
return (intval(DI::config()->get('system', 'no_smilies')) ||
(DI::userSession()->getLocalUserId() &&
intval(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'no_smilies'))));
}
/**
* Replaces emoji shortcodes in a string from a structured array of searches and replaces.
*
@ -212,9 +374,7 @@ class Smilies
*/
public static function replaceFromArray(string $text, array $smilies, bool $no_images = false): string
{
if (intval(DI::config()->get('system', 'no_smilies'))
|| (DI::userSession()->getLocalUserId() && intval(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'no_smilies')))
) {
if (self::noSmilies()) {
return $text;
}
@ -233,7 +393,7 @@ class Smilies
$smilies = $cleaned;
}
$text = preg_replace_callback('/&lt;(3+)/', [self::class, 'heartReplaceCallback'], $text);
$text = preg_replace_callback('/\B&lt;3+?\b/', [self::class, 'heartReplaceCallback'], $text);
$text = self::strOrigReplace($smilies['texts'], $smilies['icons'], $text);
$text = preg_replace_callback('/<(code)>(.*?)<\/code>/ism', [self::class, 'decode'], $text);
@ -274,16 +434,7 @@ class Smilies
*/
private static function heartReplaceCallback(array $matches): string
{
if (strlen($matches[1]) == 1) {
return $matches[0];
}
$t = '';
for ($cnt = 0; $cnt < strlen($matches[1]); $cnt ++) {
$t .= '❤';
}
return str_replace($matches[0], $t, $matches[0]);
return str_repeat('❤', strlen($matches[0]) - 4);
}
/**

View file

@ -1234,7 +1234,7 @@ class BBCode
}
/**
* Expand Youtube and Vimeo links to
* Expand Youtube and Vimeo links to
*
* @param string $text
* @return string
@ -1387,7 +1387,7 @@ class BBCode
"\n[hr]", "[hr]\n", " [hr]", "[hr] ",
"\n[attachment ", " [attachment ", "\n[/attachment]", "[/attachment]\n", " [/attachment]", "[/attachment] ",
"[table]\n", "[table] ", " [table]", "\n[/table]", " [/table]", "[/table] ",
" \n", "\t\n", "[/li]\n", "\n[li]", "\n[*]",
" \n", "\t\n", "[/li]\n", "\n[li]", "\n[*]",
];
$replace = [
"[th]", "[th]", "[th]", "[/th]", "[/th]", "[/th]",
@ -1480,14 +1480,14 @@ class BBCode
if ($simple_html == self::INTERNAL) {
//Ensure to always start with <h4> if possible
$heading_count = 0;
for ($level = 6; $level > 0; $level--) {
for ($level = 6; $level > 0; $level--) {
if (preg_match("(\[h$level\].*?\[\/h$level\])ism", $text)) {
$heading_count++;
}
}
if ($heading_count > 0) {
$heading = min($heading_count + 3, 6);
for ($level = 6; $level > 0; $level--) {
for ($level = 6; $level > 0; $level--) {
if (preg_match("(\[h$level\].*?\[\/h$level\])ism", $text)) {
$text = preg_replace("(\[h$level\](.*?)\[\/h$level\])ism", "</p><h$heading>$1</h$heading><p>", $text);
$heading--;
@ -1548,7 +1548,11 @@ class BBCode
$text = preg_replace("(\[style=(.*?)\](.*?)\[\/style\])ism", '<span style="$1">$2</span>', $text);
// Mastodon Emoji (internal tag, do not document for users)
$text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '<span class="mastodon emoji"><img src="$1" alt="$2" title="$2"/></span>', $text);
if ($simple_html == self::MASTODON_API) {
$text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '$2', $text);
} else {
$text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '<span class="mastodon emoji"><img src="$1" alt="$2" title="$2"/></span>', $text);
}
// Check for CSS classes
// @deprecated since 2021.12, left for backward-compatibility reasons

View file

@ -32,19 +32,21 @@ class Emoji extends BaseFactory
}
/**
* Creates an emoji collection from shortcode => image mappings.
*
* @param array $smilies
*
* @return Emojis
*/
public function createCollectionFromSmilies(array $smilies): Emojis
public function createCollectionFromArray(array $smilies): Emojis
{
$prototype = null;
$emojis = [];
foreach ($smilies['texts'] as $key => $shortcode) {
if (preg_match('/src="(.+?)"/', $smilies['icons'][$key], $matches)) {
$url = $matches[1];
foreach ($smilies as $shortcode => $url) {
if ($shortcode !== '' && $url !== '') {
$shortcode = trim($shortcode, ':');
if ($prototype === null) {
$prototype = $this->create($shortcode, $url);
@ -52,9 +54,27 @@ class Emoji extends BaseFactory
} else {
$emojis[] = \Friendica\Object\Api\Mastodon\Emoji::createFromPrototype($prototype, $shortcode, $url);
}
};
}
}
return new Emojis($emojis);
}
/**
* @param array $smilies as is returned by Smilies::getList()
*
* @return Emojis
*/
public function createCollectionFromSmilies(array $smilies): Emojis
{
$emojis = [];
$icons = $smilies['icons'];
foreach ($smilies['texts'] as $i => $name) {
$url = $icons[$i];
if (preg_match('/src="(.+?)"/', $url, $matches)) {
$emojis[$name] = $matches[1];
}
}
return self::createCollectionFromArray($emojis);
}
}

View file

@ -24,6 +24,7 @@ namespace Friendica\Factory\Api\Mastodon;
use Friendica\BaseFactory;
use Friendica\Content\ContactSelector;
use Friendica\Content\Item as ContentItem;
use Friendica\Content\Smilies;
use Friendica\Content\Text\BBCode;
use Friendica\Core\Logger;
use Friendica\Database\Database;
@ -57,6 +58,8 @@ class Status extends BaseFactory
private $mstdnCardFactory;
/** @var Attachment */
private $mstdnAttachmentFactory;
/** @var Emoji */
private $mstdnEmojiFactory;
/** @var Error */
private $mstdnErrorFactory;
/** @var Poll */
@ -74,6 +77,7 @@ class Status extends BaseFactory
Tag $mstdnTagFactory,
Card $mstdnCardFactory,
Attachment $mstdnAttachmentFactory,
Emoji $mstdnEmojiFactory,
Error $mstdnErrorFactory,
Poll $mstdnPollFactory,
ContentItem $contentItem,
@ -86,6 +90,7 @@ class Status extends BaseFactory
$this->mstdnTagFactory = $mstdnTagFactory;
$this->mstdnCardFactory = $mstdnCardFactory;
$this->mstdnAttachmentFactory = $mstdnAttachmentFactory;
$this->mstdnEmojiFactory = $mstdnEmojiFactory;
$this->mstdnErrorFactory = $mstdnErrorFactory;
$this->mstdnPollFactory = $mstdnPollFactory;
$this->contentItem = $contentItem;
@ -283,6 +288,21 @@ class Status extends BaseFactory
}
}
$emojis = null;
if (DI::baseUrl()->isLocalUrl($item['uri'])) {
$used_smilies = Smilies::extractUsedSmilies($item['raw-body'] ?: $item['body'], $normalized);
if ($item['raw-body']) {
$item['raw-body'] = $normalized;
} elseif ($item['body']) {
$item['body'] = $normalized;
}
$emojis = $this->mstdnEmojiFactory->createCollectionFromArray($used_smilies)->getArrayCopy(true);
} else {
if (preg_match_all("(\[emoji=(.*?)](.*?)\[/emoji])ism", $item['body'] ?: $item['raw-body'], $matches)) {
$emojis = $this->mstdnEmojiFactory->createCollectionFromArray(array_combine($matches[2], $matches[1]))->getArrayCopy(true);
}
}
if ($is_reshare) {
try {
$reshare = $this->createFromUriId($uriId, $uid, $display_quote, false, false)->toArray();
@ -309,7 +329,7 @@ class Status extends BaseFactory
$visibility_data = $uid != $item['uid'] ? null : new FriendicaVisibility($this->aclFormatter->expand($item['allow_cid']), $this->aclFormatter->expand($item['deny_cid']), $this->aclFormatter->expand($item['allow_gid']), $this->aclFormatter->expand($item['deny_gid']));
$friendica = new FriendicaExtension($item['title'] ?? '', $item['changed'], $item['commented'], $item['received'], $counts->dislikes, $origin_dislike, $delivery_data, $visibility_data);
return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $in_reply, $reshare, $friendica, $quote, $poll);
return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $in_reply, $reshare, $friendica, $quote, $poll, $emojis);
}
/**

View file

@ -107,7 +107,7 @@ class Status extends BaseDataTransferObject
* @param array $item
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function __construct(array $item, Account $account, Counts $counts, UserAttributes $userAttributes, bool $sensitive, Application $application, array $mentions, array $tags, Card $card, array $attachments, array $in_reply, array $reblog, FriendicaExtension $friendica, array $quote = null, array $poll = null)
public function __construct(array $item, Account $account, Counts $counts, UserAttributes $userAttributes, bool $sensitive, Application $application, array $mentions, array $tags, Card $card, array $attachments, array $in_reply, array $reblog, FriendicaExtension $friendica, array $quote = null, array $poll = null, array $emojis = null)
{
$reblogged = !empty($reblog);
$this->id = (string)$item['uri-id'];
@ -152,7 +152,7 @@ class Status extends BaseDataTransferObject
$this->media_attachments = $reblogged ? [] : $attachments;
$this->mentions = $reblogged ? [] : $mentions;
$this->tags = $reblogged ? [] : $tags;
$this->emojis = $reblogged ? [] : [];
$this->emojis = $reblogged ? [] : ($emojis ?: []);
$this->card = $reblogged ? null : ($card->toArray() ?: null);
$this->poll = $reblogged ? null : $poll;
$this->friendica = $reblogged ? null : $friendica;

View file

@ -23,6 +23,7 @@ namespace Friendica\Protocol\ActivityPub;
use Friendica\App;
use Friendica\Content\Feature;
use Friendica\Content\Smilies;
use Friendica\Content\Text\BBCode;
use Friendica\Core\Cache\Enum\Duration;
use Friendica\Core\Logger;
@ -898,7 +899,7 @@ class Transmitter
$tags = Tag::getByURIId($uri_id, [Tag::TO, Tag::CC, Tag::BCC, Tag::AUDIENCE]);
if (empty($tags)) {
Logger::debug('No receivers found', ['uri-id' => $uri_id]);
$post = Post::selectFirst([Item::DELIVER_FIELDLIST], ['uri-id' => $uri_id, 'origin' => true]);
$post = Post::selectFirst(Item::DELIVER_FIELDLIST, ['uri-id' => $uri_id, 'origin' => true]);
if (!empty($post)) {
ActivityPub\Transmitter::storeReceiversForItem($post);
$tags = Tag::getByURIId($uri_id, [Tag::TO, Tag::CC, Tag::BCC, Tag::AUDIENCE]);
@ -1506,6 +1507,29 @@ class Transmitter
return $location;
}
/**
* Appends emoji tags to a tag array according to the tags used.
*
* @param array $tags Tag array
* @param string $text Text containing tags like :tag:
* @return string normalized text
*/
private static function addEmojiTags(array &$tags, string $text): string
{
$emojis = Smilies::extractUsedSmilies($text, $normalized);
foreach ($emojis as $name => $url) {
$tags[] = [
'type' => 'Emoji',
'name' => $name,
'icon' => [
'type' => 'Image',
'url' => $url,
],
];
}
return $normalized;
}
/**
* Returns a tag array for a given item array
*
@ -1785,10 +1809,11 @@ class Transmitter
$item = Post\Media::addHTMLAttachmentToItem($item);
$body = $item['body'];
$emojis = [];
if ($type == 'Note') {
$body = $item['raw-body'] ?? self::removePictures($body);
}
$body = self::addEmojiTags($emojis, $body);
/**
* @todo Improve the automated summary
@ -1870,7 +1895,7 @@ class Transmitter
}
$data['attachment'] = self::createAttachmentList($item);
$data['tag'] = self::createTagList($item, $data['quoteUrl'] ?? '');
$data['tag'] = array_merge(self::createTagList($item, $data['quoteUrl'] ?? ''), $emojis);
if (empty($data['location']) && (!empty($item['coord']) || !empty($item['location']))) {
$data['location'] = self::createLocation($item);

View file

@ -112,6 +112,11 @@ return [
'uri' => 'http://localhost/profile/mutualcontact',
'guid' => '46',
],
[
'id' => 100,
'uri' => 'https://friendica.local/posts/100',
'guid' => '100',
],
],
'contact' => [
[
@ -363,6 +368,12 @@ return [
'et sed beatae nihil ullam temporibus corporis ratione blanditiis',
'plink' => 'http://localhost/display/6',
],
[
'uri-id' => 100,
'title' => 'item_title',
'body' => ':like ~friendica no [code]:dislike[/code] :-p :-[ <3',
'plink' => 'https://friendica.local/post/100',
],
],
'post' => [
[
@ -744,6 +755,28 @@ return [
'deleted' => 0,
'wall' => 0,
],
// An emoji post
[
'id' => 14,
'uri-id' => 100,
'visible' => 1,
'contact-id' => 44,
'author-id' => 44,
'owner-id' => 42,
'causer-id' => 44,
'uid' => 0,
'vid' => 8,
'unseen' => 0,
'parent-uri-id' => 7,
'thr-parent-id' => 7,
'private' => Item::PUBLIC,
'global' => true,
'gravity' => Item::GRAVITY_PARENT,
'network' => Protocol::DFRN,
'origin' => 0,
'deleted' => 0,
'wall' => 0,
],
],
'post-thread' => [
[

View file

@ -143,4 +143,126 @@ class SmiliesTest extends FixtureTest
{
$this->assertEquals($expected, Smilies::isEmojiPost($body));
}
public function dataReplace(): array
{
$data = [
'simple-1' => [
'expected' => 'alt=":-p"',
'body' => ':-p',
],
'simple-1' => [
'expected' => 'alt=":-p"',
'body' => ' :-p ',
],
'word-boundary-1' => [
'expected' => ':-pppp',
'body' => ':-pppp',
],
'word-boundary-2' => [
'expected' => '~friendicaca',
'body' => '~friendicaca',
],
'symbol-boundary-1' => [
'expected' => 'alt=":-p"',
'body' => '(:-p)',
],
'hearts-1' => [
'expected' => '❤ (❤) ❤',
'body' => '&lt;3 (&lt;3) &lt;3',
],
'hearts-8' => [
'expected' => '(❤❤❤❤❤❤❤❤)',
'body' => '(&lt;33333333)',
],
'no-hearts-1' => [
'expected' => '(&lt;30)',
'body' => '(&lt;30)',
],
'no-hearts-2' => [
'expected' => '(3&lt;33)',
'body' => '(3&lt;33)',
],
];
foreach ([':-[', ':-D', 'o.O'] as $emoji) {
foreach (['A', '_', ':', '-'] as $prefix) {
foreach (['', ' ', 'A', ':', '-'] as $suffix) {
$no_smile = ($prefix !== '' && ctype_alnum($prefix)) || ($suffix !== '' && ctype_alnum($suffix));
$s = $prefix . $emoji . $suffix;
$data[] = [
'expected' => $no_smile ? $s : 'alt="' . $emoji . '"',
'body' => $s,
];
}
}
}
return $data;
}
/**
* @dataProvider dataReplace
*
* @param string $expected
* @param string $body
*/
public function testReplace(string $expected, string $body)
{
$result = Smilies::replace($body);
$this->assertStringContainsString($expected, $result);
}
public function dataExtractUsedSmilies(): array
{
return [
'symbols' => [
'expected' => ['p', 'heart', 'embarrassed', 'kiss'],
'body' => ':-p &lt;3 ":-[:-"',
'normalized' => ':p: :heart: ":embarrassed::kiss:',
],
'single-smiley' => [
'expected' => ['like'],
'body' => ':like',
'normalized' => ':like:',
],
'multiple-smilies' => [
'expected' => ['like', 'dislike'],
'body' => ':like :dislike',
'normalized' => ':like: :dislike:',
],
'nosmile' => [
'expected' => [],
'body' => '[nosmile] :like :like',
'normalized' => '[nosmile] :like :like'
],
'in-code' => [
'expected' => [],
'body' => '[code]:like :like :like[/code]',
'normalized' => '[code]:like :like :like[/code]'
],
'~friendica' => [
'expected' => ['friendica'],
'body' => '~friendica',
'normalized' => ':friendica:'
],
];
}
/**
* @dataProvider dataExtractUsedSmilies
*
* @param array $expected
* @param string $body
* @param stirng $normalized
*/
public function testExtractUsedSmilies(array $expected, string $body, string $normalized)
{
$extracted = Smilies::extractUsedSmilies($body, $converted);
$expected = array_fill_keys($expected, true);
$this->assertEquals($normalized, $converted);
foreach (array_keys($extracted) as $shortcode) {
$this->assertArrayHasKey($shortcode, $expected);
}
$this->assertEquals(count($expected), count($extracted));
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Test\src\Factory\Api\Mastodon;
use Friendica\Content\Smilies;
use Friendica\DI;
use Friendica\Test\FixtureTest;
class EmojiTest extends FixtureTest
{
protected function setUp(): void
{
parent::setUp();
DI::config()->set('system', 'no_smilies', false);
}
public function testBuiltInCollection()
{
$emoji = DI::mstdnEmoji();
$collection = $emoji->createCollectionFromSmilies(Smilies::getList())->getArrayCopy(true);
foreach ($collection as $item) {
$this->assertTrue(preg_match('(/images/.*)', $item['url']) === 1, $item['url']);
}
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Test\src\Factory\Api\Mastodon;
use Friendica\Model\Post;
use Friendica\DI;
use Friendica\Test\FixtureTest;
class StatusTest extends FixtureTest
{
protected $status;
protected function setUp(): void
{
parent::setUp();
DI::config()->set('system', 'no_smilies', false);
$this->status = DI::mstdnStatus();
}
public function testSimpleStatus()
{
$post = Post::selectFirst([], ['id' => 13]);
$this->assertNotNull($post);
$result = $this->status->createFromUriId($post['uri-id']);
$this->assertNotNull($result);
}
public function testSimpleEmojiStatus()
{
$post = Post::selectFirst([], ['id' => 14]);
$this->assertNotNull($post);
$result = $this->status->createFromUriId($post['uri-id'])->toArray();
$this->assertEquals(':like: :friendica: no <code>:dislike</code> :p: :embarrassed: ❤', $result['content']);
$emojis = array_fill_keys(['like', 'friendica', 'p', 'embarrassed'], true);
$this->assertEquals(count($emojis), count($result['emojis']));
foreach ($result['emojis'] as $emoji) {
$this->assertTrue(array_key_exists($emoji['shortcode'], $emojis));
$this->assertEquals(0, strpos($emoji['url'], 'http'));
}
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Test\src\Protocol\ActivityPub;
use Friendica\DI;
use Friendica\Model\Post;
use Friendica\Protocol\ActivityPub\Transmitter;
use Friendica\Test\FixtureTest;
class TransmitterTest extends FixtureTest
{
protected function setUp(): void
{
parent::setUp();
DI::config()->set('system', 'no_smilies', false);
}
public function testEmojiPost()
{
$post = Post::selectFirst([], ['id' => 14]);
$this->assertNotNull($post);
$note = Transmitter::createNote($post);
$this->assertNotNull($note);
$this->assertEquals(':like: :friendica: no <code>:dislike</code> :p: :embarrassed: ❤', $note['content']);
$emojis = array_fill_keys(['like', 'friendica', 'p', 'embarrassed'], true);
$this->assertEquals(count($emojis), count($note['tag']));
foreach ($note['tag'] as $emoji) {
$this->assertTrue(array_key_exists($emoji['name'], $emojis));
$this->assertEquals('Emoji', $emoji['type']);
}
}
}

View file

@ -1413,6 +1413,9 @@ section #jotOpen {
max-height: calc(100vh - 62px);
}
}
#jot-modal #jot-modal-body {
overflow: auto;
}
#jot-modal #jot-sections,
#jot-modal #jot-modal-body,
#jot-modal #profile-jot-form,
@ -1423,7 +1426,6 @@ section #jotOpen {
#jot-modal #item-Q0,
#jot-modal #profile-jot-acl-wrapper,
#jot-modal #acl-wrapper {
overflow: hidden;
display: flex;
flex: auto;
flex-direction: column;