Decouple conversation creation from rendering

- This allows to separately obtain a list of threads for rendering without having to deal with an already-formed HTML output
This commit is contained in:
Hypolite Petovan 2023-02-04 19:14:21 -05:00
parent 31fbe70ec7
commit 706444bdb2
14 changed files with 294 additions and 269 deletions

View file

@ -274,7 +274,7 @@ function item_process(array $post, array $request, bool $preview, string $return
$post['body'] = BBCode::removeSharedData(Item::setHashtags($post['body']));
$post['writable'] = true;
$o = DI::conversation()->create([$post], Conversation::MODE_SEARCH, false, true);
$o = DI::conversation()->render([$post], Conversation::MODE_SEARCH, false, true);
System::jsonExit(['preview' => $o]);
}

View file

@ -85,7 +85,7 @@ function notes_content(App $a, bool $update = false)
$count = count($notes);
$o .= DI::conversation()->create($notes, Conversation::MODE_NOTES, $update);
$o .= DI::conversation()->render($notes, Conversation::MODE_NOTES, $update);
}
$o .= $pager->renderMinimal($count);

View file

@ -438,17 +438,17 @@ class Conversation
* The $mode parameter decides between the various renderings and also
* figures out how to determine page owner and other contextual items
* that are based on unique features of the calling module.
* @param array $items
* @param string $mode
* @param $update @TODO Which type?
* @param bool $preview
* @param string $order
* @param array $items An array of Posts
* @param string $mode One of self::MODE_*
* @param bool $update Asynchronous update rendering
* @param bool $preview Post preview (no actual database record)
* @param string $order Either "received" or "commented"
* @param int $uid
* @return string
* @throws ImagickException
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function create(array $items, string $mode, $update, bool $preview = false, string $order = 'commented', int $uid = 0): string
public function render(array $items, string $mode, bool $update = false, bool $preview = false, string $order = 'commented', int $uid = 0): string
{
$this->profiler->startRecording('rendering');
@ -459,10 +459,6 @@ class Conversation
$live_update_div = '';
$blocklist = $this->getBlocklist();
$previewing = (($preview) ? ' preview ' : '');
if ($mode === self::MODE_NETWORK) {
$items = $this->addChildren($items, false, $order, $uid, $mode);
if (!$update) {
@ -556,246 +552,14 @@ class Conversation
$items = $cb['items'];
$conv_responses = [
'like' => [],
'dislike' => [],
'attendyes' => [],
'attendno' => [],
'attendmaybe' => [],
'announce' => [],
];
if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'hide_dislike')) {
unset($conv_responses['dislike']);
}
// array with html for each thread (parent+comments)
$threads = [];
$threadsid = -1;
$page_template = Renderer::getMarkupTemplate("conversation.tpl");
$formSecurityToken = BaseModule::getFormSecurityToken('contact_action');
if (!empty($items)) {
if (in_array($mode, [self::MODE_COMMUNITY, self::MODE_CONTACTS, self::MODE_PROFILE])) {
$writable = true;
} else {
$writable = $items[0]['writable'] || ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED);
}
$threads = $this->getThreadList($items, $mode, $preview, $page_dropping, $formSecurityToken);
if (!$this->session->getLocalUserId()) {
$writable = false;
}
if (in_array($mode, [self::MODE_FILED, self::MODE_SEARCH, self::MODE_CONTACT_POSTS])) {
/*
* "New Item View" on network page or search page results
* - just loop through the items and format them minimally for display
*/
$tpl = 'search_item.tpl';
$uriids = [];
foreach ($items as $item) {
if (in_array($item['uri-id'], $uriids)) {
continue;
}
$uriids[] = $item['uri-id'];
if (!$this->item->isVisibleActivity($item)) {
continue;
}
if (in_array($item['author-id'], $blocklist)) {
continue;
}
$threadsid++;
// prevent private email from leaking.
if ($item['network'] === Protocol::MAIL && $this->session->getLocalUserId() != $item['uid']) {
continue;
}
$profile_name = $item['author-name'];
if (!empty($item['author-link']) && empty($item['author-name'])) {
$profile_name = $item['author-link'];
}
$tags = Tag::populateFromItem($item);
$author = [
'uid' => 0,
'id' => $item['author-id'],
'network' => $item['author-network'],
'url' => $item['author-link'],
'alias' => $item['author-alias'],
];
$profile_link = Contact::magicLinkByContact($author);
$sparkle = '';
if (strpos($profile_link, 'contact/redir/') === 0) {
$sparkle = ' sparkle';
}
$locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
Hook::callAll('render_location', $locate);
$location_html = $locate['html'] ?: Strings::escapeHtml($locate['location'] ?: $locate['coord'] ?: '');
$this->item->localize($item);
if ($mode === self::MODE_FILED) {
$dropping = true;
} else {
$dropping = false;
}
$drop = [
'dropping' => $dropping,
'pagedrop' => $page_dropping,
'select' => $this->l10n->t('Select'),
'delete' => $this->l10n->t('Delete'),
];
$likebuttons = [
'like' => null,
'dislike' => null,
'share' => null,
'announce' => null,
];
if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'hide_dislike')) {
unset($likebuttons['dislike']);
}
$body_html = ItemModel::prepareBody($item, true, $preview);
[$categories, $folders] = $this->item->determineCategoriesTerms($item, $this->session->getLocalUserId());
if (!empty($item['title'])) {
$title = $item['title'];
} elseif (!empty($item['content-warning']) && $this->pConfig->get($this->session->getLocalUserId(), 'system', 'disable_cw', false)) {
$title = ucfirst($item['content-warning']);
} else {
$title = '';
}
if (!empty($item['featured'])) {
$pinned = $this->l10n->t('Pinned item');
} else {
$pinned = '';
}
$tmp_item = [
'template' => $tpl,
'id' => ($preview ? 'P0' : $item['id']),
'guid' => ($preview ? 'Q0' : $item['guid']),
'commented' => $item['commented'],
'received' => $item['received'],
'created_date' => $item['created'],
'uriid' => $item['uri-id'],
'network' => $item['network'],
'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network'], $item['author-gsid']),
'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link'], $item['author-gsid']),
'linktitle' => $this->l10n->t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
'profile_url' => $profile_link,
'item_photo_menu_html' => $this->item->photoMenu($item, $formSecurityToken),
'name' => $profile_name,
'sparkle' => $sparkle,
'lock' => false,
'thumb' => $this->baseURL->remove($this->item->getAuthorAvatar($item)),
'title' => $title,
'body_html' => $body_html,
'tags' => $tags['tags'],
'hashtags' => $tags['hashtags'],
'mentions' => $tags['mentions'],
'implicit_mentions' => $tags['implicit_mentions'],
'txt_cats' => $this->l10n->t('Categories:'),
'txt_folders' => $this->l10n->t('Filed under:'),
'has_cats' => ((count($categories)) ? 'true' : ''),
'has_folders' => ((count($folders)) ? 'true' : ''),
'categories' => $categories,
'folders' => $folders,
'text' => strip_tags($body_html),
'localtime' => DateTimeFormat::local($item['created'], 'r'),
'utc' => DateTimeFormat::utc($item['created'], 'c'),
'ago' => (($item['app']) ? $this->l10n->t('%s from %s', Temporal::getRelativeDate($item['created']), $item['app']) : Temporal::getRelativeDate($item['created'])),
'location_html' => $location_html,
'indent' => '',
'owner_name' => '',
'owner_url' => '',
'owner_photo' => $this->baseURL->remove($this->item->getOwnerAvatar($item)),
'plink' => ItemModel::getPlink($item),
'edpost' => false,
'pinned' => $pinned,
'isstarred' => 'unstarred',
'star' => false,
'drop' => $drop,
'vote' => $likebuttons,
'like_html' => '',
'dislike_html ' => '',
'comment_html' => '',
'conv' => ($preview ? '' : ['href' => 'display/' . $item['guid'], 'title' => $this->l10n->t('View in context')]),
'previewing' => $previewing,
'wait' => $this->l10n->t('Please wait'),
'thread_level' => 1,
];
$arr = ['item' => $item, 'output' => $tmp_item];
Hook::callAll('display_item', $arr);
$threads[$threadsid]['id'] = $item['id'];
$threads[$threadsid]['network'] = $item['network'];
$threads[$threadsid]['items'] = [$arr['output']];
}
} else {
// Normal View
$page_template = Renderer::getMarkupTemplate("threaded_conversation.tpl");
$conv = new Thread($mode, $preview, $writable);
/*
* get all the topmost parents
* this shouldn't be needed, as we should have only them in our array
* But for now, this array respects the old style, just in case
*/
foreach ($items as $item) {
if (in_array($item['author-id'], $blocklist)) {
continue;
}
// Can we put this after the visibility check?
$this->builtinActivityPuller($item, $conv_responses);
// Only add what is visible
if ($item['network'] === Protocol::MAIL && $this->session->getLocalUserId() != $item['uid']) {
continue;
}
if (!$this->item->isVisibleActivity($item)) {
continue;
}
/// @todo Check if this call is needed or not
$arr = ['item' => $item];
Hook::callAll('display_item', $arr);
$item['pagedrop'] = $page_dropping;
if ($item['gravity'] == ItemModel::GRAVITY_PARENT) {
$item_object = new PostObject($item);
$conv->addParent($item_object);
}
}
$threads = $conv->getTemplateData($conv_responses, $formSecurityToken);
if (!$threads) {
$this->logger->info('[ERROR] conversation : Failed to get template data.');
$threads = [];
}
}
if (in_array($mode, [self::MODE_FILED, self::MODE_SEARCH, self::MODE_CONTACT_POSTS])) {
$page_template = Renderer::getMarkupTemplate('conversation.tpl');
} else {
$page_template = Renderer::getMarkupTemplate('threaded_conversation.tpl');
}
$o = Renderer::replaceMacros($page_template, [
@ -813,6 +577,91 @@ class Conversation
return $o;
}
/**
* @param array $items
* @param string $mode One of self::MODE_*
* @param bool $preview
* @param bool $pagedrop Whether to enable the user to select the thread for deletion
* @param string $formSecurityToken A 'contact_action' form security token
* @return array
* @throws InternalServerErrorException
* @throws \ImagickException
*/
public function getThreadList(array $items, string $mode, bool $preview, bool $pagedrop, string $formSecurityToken): array
{
if (in_array($mode, [self::MODE_FILED, self::MODE_SEARCH, self::MODE_CONTACT_POSTS])) {
$threads = $this->getContextLessThreadList($items, $mode, $preview, $pagedrop, $formSecurityToken);
} else {
$conv_responses = [
'like' => [],
'dislike' => [],
'attendyes' => [],
'attendno' => [],
'attendmaybe' => [],
'announce' => [],
];
if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'hide_dislike')) {
unset($conv_responses['dislike']);
}
if (in_array($mode, [self::MODE_COMMUNITY, self::MODE_CONTACTS, self::MODE_PROFILE])) {
$writable = true;
} else {
$writable = $items[0]['writable'] || ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED);
}
if (!$this->session->getLocalUserId()) {
$writable = false;
}
// Normal View
$conv = new Thread($mode, $preview, $writable);
/*
* get all the topmost parents
* this shouldn't be needed, as we should have only them in our array
* But for now, this array respects the old style, just in case
*/
foreach ($items as $item) {
if (in_array($item['author-id'], $this->getBlocklist())) {
continue;
}
// Can we put this after the visibility check?
$this->builtinActivityPuller($item, $conv_responses);
// Only add what is visible
if ($item['network'] === Protocol::MAIL && $this->session->getLocalUserId() != $item['uid']) {
continue;
}
if (!$this->item->isVisibleActivity($item)) {
continue;
}
/// @todo Check if this call is needed or not
$arr = ['item' => $item];
Hook::callAll('display_item', $arr);
$item['pagedrop'] = $pagedrop;
if ($item['gravity'] == ItemModel::GRAVITY_PARENT) {
$item_object = new PostObject($item);
$conv->addParent($item_object);
}
}
$threads = $conv->getTemplateData($conv_responses, $formSecurityToken);
if (!$threads) {
$this->logger->info('[ERROR] conversation : Failed to get template data.');
$threads = [];
}
}
return $threads;
}
private function getBlocklist(): array
{
if (!$this->session->getLocalUserId()) {
@ -1494,4 +1343,180 @@ class Conversation
{
return strcmp($b['created'], $a['created']);
}
/**
* "New Item View" on network page or search page results
* - just loop through the items and format them minimally for display
*
* @param array $items
* @param string $mode One of self::MODE_*
* @param bool $preview Whether the display is a preview
* @param bool $pagedrop Whether the user can select the threads for deletion
* @param string $formSecurityToken A 'contact_action' form security token
* @return array
* @throws InternalServerErrorException
* @throws \ImagickException
*/
public function getContextLessThreadList(array $items, string $mode, bool $preview, bool $pagedrop, string $formSecurityToken): array
{
$threads = [];
$uriids = [];
foreach ($items as $item) {
if (in_array($item['uri-id'], $uriids)) {
continue;
}
$uriids[] = $item['uri-id'];
if (!$this->item->isVisibleActivity($item)) {
continue;
}
if (in_array($item['author-id'], $this->getBlocklist())) {
continue;
}
$threadsid++;
// prevent private email from leaking.
if ($item['network'] === Protocol::MAIL && $this->session->getLocalUserId() != $item['uid']) {
continue;
}
$profile_name = $item['author-name'];
if (!empty($item['author-link']) && empty($item['author-name'])) {
$profile_name = $item['author-link'];
}
$tags = Tag::populateFromItem($item);
$author = [
'uid' => 0,
'id' => $item['author-id'],
'network' => $item['author-network'],
'url' => $item['author-link'],
'alias' => $item['author-alias'],
];
$profile_link = Contact::magicLinkByContact($author);
$sparkle = '';
if (strpos($profile_link, 'contact/redir/') === 0) {
$sparkle = ' sparkle';
}
$locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
Hook::callAll('render_location', $locate);
$location_html = $locate['html'] ?: Strings::escapeHtml($locate['location'] ?: $locate['coord'] ?: '');
$this->item->localize($item);
if ($mode === self::MODE_FILED) {
$dropping = true;
} else {
$dropping = false;
}
$drop = [
'dropping' => $dropping,
'pagedrop' => $pagedrop,
'select' => $this->l10n->t('Select'),
'delete' => $this->l10n->t('Delete'),
];
$likebuttons = [
'like' => null,
'dislike' => null,
'share' => null,
'announce' => null,
];
if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'hide_dislike')) {
unset($likebuttons['dislike']);
}
$body_html = ItemModel::prepareBody($item, true, $preview);
[$categories, $folders] = $this->item->determineCategoriesTerms($item, $this->session->getLocalUserId());
if (!empty($item['title'])) {
$title = $item['title'];
} elseif (!empty($item['content-warning']) && $this->pConfig->get($this->session->getLocalUserId(), 'system', 'disable_cw', false)) {
$title = ucfirst($item['content-warning']);
} else {
$title = '';
}
if (!empty($item['featured'])) {
$pinned = $this->l10n->t('Pinned item');
} else {
$pinned = '';
}
$tmp_item = [
'template' => 'search_item.tpl',
'id' => ($preview ? 'P0' : $item['id']),
'guid' => ($preview ? 'Q0' : $item['guid']),
'commented' => $item['commented'],
'received' => $item['received'],
'created_date' => $item['created'],
'uriid' => $item['uri-id'],
'network' => $item['network'],
'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network'], $item['author-gsid']),
'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link'], $item['author-gsid']),
'linktitle' => $this->l10n->t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
'profile_url' => $profile_link,
'item_photo_menu_html' => $this->item->photoMenu($item, $formSecurityToken),
'name' => $profile_name,
'sparkle' => $sparkle,
'lock' => false,
'thumb' => $this->baseURL->remove($this->item->getAuthorAvatar($item)),
'title' => $title,
'body_html' => $body_html,
'tags' => $tags['tags'],
'hashtags' => $tags['hashtags'],
'mentions' => $tags['mentions'],
'implicit_mentions' => $tags['implicit_mentions'],
'txt_cats' => $this->l10n->t('Categories:'),
'txt_folders' => $this->l10n->t('Filed under:'),
'has_cats' => ((count($categories)) ? 'true' : ''),
'has_folders' => ((count($folders)) ? 'true' : ''),
'categories' => $categories,
'folders' => $folders,
'text' => strip_tags($body_html),
'localtime' => DateTimeFormat::local($item['created'], 'r'),
'utc' => DateTimeFormat::utc($item['created'], 'c'),
'ago' => (($item['app']) ? $this->l10n->t('%s from %s', Temporal::getRelativeDate($item['created']), $item['app']) : Temporal::getRelativeDate($item['created'])),
'location_html' => $location_html,
'indent' => '',
'owner_name' => '',
'owner_url' => '',
'owner_photo' => $this->baseURL->remove($this->item->getOwnerAvatar($item)),
'plink' => ItemModel::getPlink($item),
'edpost' => false,
'pinned' => $pinned,
'isstarred' => 'unstarred',
'star' => false,
'drop' => $drop,
'vote' => $likebuttons,
'like_html' => '',
'dislike_html ' => '',
'comment_html' => '',
'conv' => $preview ? '' : ['href' => 'display/' . $item['guid'], 'title' => $this->l10n->t('View in context')],
'previewing' => $preview ? ' preview ' : '',
'wait' => $this->l10n->t('Please wait'),
'thread_level' => 1,
];
$arr = ['item' => $item, 'output' => $tmp_item];
Hook::callAll('display_item', $arr);
$threads[] = [
'id' => $item['id'],
'network' => $item['network'],
'items' => [$arr['output']],
];
}
return $threads;
}
}

View file

@ -1626,7 +1626,7 @@ class Contact
}
}
$o .= DI::conversation()->create($items, ConversationContent::MODE_CONTACTS, $update, false, 'pinned_commented', DI::userSession()->getLocalUserId());
$o .= DI::conversation()->render($items, ConversationContent::MODE_CONTACTS, $update, false, 'pinned_commented', DI::userSession()->getLocalUserId());
} else {
$fields = array_merge(Item::DISPLAY_FIELDLIST, ['featured']);
$items = Post::toArray(Post::selectForUser(DI::userSession()->getLocalUserId(), $fields, $condition, $params));
@ -1643,7 +1643,7 @@ class Contact
}
}
$o .= DI::conversation()->create($items, ConversationContent::MODE_CONTACT_POSTS, $update);
$o .= DI::conversation()->render($items, ConversationContent::MODE_CONTACT_POSTS, $update);
}
if (!$update) {

View file

@ -157,7 +157,7 @@ class Community extends BaseModule
return $o;
}
$o .= DI::conversation()->create($items, Conversation::MODE_COMMUNITY, false, false, 'commented', DI::userSession()->getLocalUserId());
$o .= DI::conversation()->render($items, Conversation::MODE_COMMUNITY, false, false, 'commented', DI::userSession()->getLocalUserId());
$pager = new BoundariesPager(
DI::l10n(),

View file

@ -201,7 +201,7 @@ class Network extends BaseModule
$ordering = '`commented`';
}
$o .= DI::conversation()->create($items, Conversation::MODE_NETWORK, false, false, $ordering, DI::userSession()->getLocalUserId());
$o .= DI::conversation()->render($items, Conversation::MODE_NETWORK, false, false, $ordering, DI::userSession()->getLocalUserId());
if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'infinite_scroll')) {
$o .= HTML::scrollLoader();

View file

@ -275,7 +275,7 @@ class Display extends BaseModule
$output .= $this->conversation->statusEditor([], 0, true);
}
$output .= $this->conversation->create([$item], Conversation::MODE_DISPLAY, $updateUid, false, 'commented', $itemUid);
$output .= $this->conversation->render([$item], Conversation::MODE_DISPLAY, $updateUid, false, 'commented', $itemUid);
return $output;
}

View file

@ -240,7 +240,7 @@ class Conversations extends BaseProfile
$items = array_merge($items, $pinned);
}
$o .= $this->conversation->create($items, Conversation::MODE_PROFILE, false, false, 'pinned_received', $profile['uid']);
$o .= $this->conversation->render($items, Conversation::MODE_PROFILE, false, false, 'pinned_received', $profile['uid']);
$o .= $pager->renderMinimal(count($items));

View file

@ -99,7 +99,7 @@ class Filed extends BaseSearch
$items = Post::toArray(Post::selectForUser(DI::userSession()->getLocalUserId(), Item::DISPLAY_FIELDLIST, $item_condition, $item_params));
$o .= DI::conversation()->create($items, Conversation::MODE_FILED, false, false, '', DI::userSession()->getLocalUserId());
$o .= DI::conversation()->render($items, Conversation::MODE_FILED, false, false, '', DI::userSession()->getLocalUserId());
if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'infinite_scroll')) {
$o .= HTML::scrollLoader();

View file

@ -213,7 +213,7 @@ class Index extends BaseSearch
Logger::info('Start Conversation.', ['q' => $search]);
$o .= DI::conversation()->create($items, Conversation::MODE_SEARCH, false, false, 'commented', DI::userSession()->getLocalUserId());
$o .= DI::conversation()->render($items, Conversation::MODE_SEARCH, false, false, 'commented', DI::userSession()->getLocalUserId());
if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'infinite_scroll')) {
$o .= HTML::scrollLoader();

View file

@ -40,7 +40,7 @@ class Community extends CommunityModule
$o = '';
if (!empty($request['force'])) {
$o = DI::conversation()->create(self::getItems(), Conversation::MODE_COMMUNITY, true, false, 'commented', DI::userSession()->getLocalUserId());
$o = DI::conversation()->render(self::getItems(), Conversation::MODE_COMMUNITY, true, false, 'commented', DI::userSession()->getLocalUserId());
}
System::htmlUpdateExit($o);

View file

@ -79,7 +79,7 @@ class Network extends NetworkModule
$ordering = '`commented`';
}
$o = DI::conversation()->create($items, Conversation::MODE_NETWORK, $profile_uid, false, $ordering, DI::userSession()->getLocalUserId());
$o = DI::conversation()->render($items, Conversation::MODE_NETWORK, $profile_uid, false, $ordering, DI::userSession()->getLocalUserId());
System::htmlUpdateExit($o);
}

View file

@ -116,7 +116,7 @@ class Profile extends BaseModule
}
}
$o .= DI::conversation()->create($items, Conversation::MODE_PROFILE, $a->getProfileOwner(), false, 'received', $a->getProfileOwner());
$o .= DI::conversation()->render($items, Conversation::MODE_PROFILE, $a->getProfileOwner(), false, 'received', $a->getProfileOwner());
System::htmlUpdateExit($o);
}

View file

@ -190,13 +190,13 @@ class Thread
* We should find a way to avoid using those arguments (at least most of them)
*
* @param array $conv_responses data
* @param string $formSecurityToken A security Token to avoid CSF attacks
* @param string $formSecurityToken A 'contact_action' form security token
*
* @return mixed The data requested on success
* false on failure
* @throws \Exception
*/
public function getTemplateData($conv_responses, string $formSecurityToken)
public function getTemplateData(array $conv_responses, string $formSecurityToken)
{
$result = [];