From a9b3b28d001941921aa70d37fdc87c758a163a44 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 05 Jun 2026 16:47:03 +0000
Subject: [PATCH] =Some hefty changes to FeedBlock. Transitioning to loading first page in php to save on extra requests. Got a bit to do yet, but I have to work on Northeh for a bit here.

---
 inc/blocks/FeedBlock.php |  398 ++++++++++++++++++++++++++++++++++++++++++++------------
 1 files changed, 313 insertions(+), 85 deletions(-)

diff --git a/inc/blocks/FeedBlock.php b/inc/blocks/FeedBlock.php
index 3693d93..abe8395 100644
--- a/inc/blocks/FeedBlock.php
+++ b/inc/blocks/FeedBlock.php
@@ -2,6 +2,7 @@
 namespace JVBase\blocks;
 
 use JVBase\managers\Cache;
+use JVBase\meta\Meta;
 use JVBase\registrar\Registrar;
 use JVBase\base\Site;
 use JVBase\forms\TaxonomySelector;
@@ -25,6 +26,9 @@
 	protected ?int $contextID = null;
 
 	protected bool $isGallery = false;
+	protected array $args;
+	protected bool $isContentTax = false;
+	protected bool $hasMore = false;
 
 	public function __construct()
 	{
@@ -33,6 +37,7 @@
 		if (JVB_TESTING) {
 			$this->cache->flush();
 		}
+		$this->cache->flush();
 
 		add_action('init', [$this, 'registerBlock']);
 	}
@@ -78,15 +83,26 @@
 	protected function determineContent(array $attrs):void
 	{
 		if (array_key_exists('inheritQuery', $attrs) && $attrs['inheritQuery'] === true) {
-			if (is_post_type_archive()) {
+			$args = [
+				'posts_per_page'	=> 36,
+				'fields'			=> 'ids',
+			];
+			if (is_post_type_archive() &&!is_tax()) {
 				$obj = get_queried_object();
+				$registrar = Registrar::getInstance($obj->name);
+				if ($registrar && $registrar->hasFeature('is_timeline')) {
+					$args['post_parent'] = 0;
+				}
 				$this->content = [jvbNoBase($obj->name)];
+				$args['post_type'] = $obj->name;
+				$this->args = $args;
 				return;
 			} elseif (!empty(Registrar::getProfileTypes()) && is_singular(Registrar::getProfileTypes())) {
 				global $post;
 				$author = $post->post_author;
 				$role = jvbUserRole($author);
 
+				$args['post_author'] = $author;
 				$this->context = jvbNoBase($role);
 				$this->contextID = $author;
 				$registrar = Registrar::getInstance($role);
@@ -94,25 +110,50 @@
 					return;
 				}
 				$this->content = $registrar->getCreatable();
+				$args['post_type'] = array_map('jvbCheckBase', $this->content);
+				foreach($args['post_type'] as $post_type) {
+					$reg = Registrar::getInstance($post_type);
+					if ($reg && $reg->hasFeature('is_timeline')) {
+						$args['post_parent'] = 0;
+					}
+				}
+				$this->args = $args;
 				return;
 			} elseif (is_tax()) {
 				$obj = get_queried_object();
 				$this->context = jvbNoBase($obj->taxonomy);
 				$this->contextID = $obj->term_id;
+				$args['tax_query'] = [];
+				$args['tax_query'][] = [
+					'taxonomy'	=> $obj->taxonomy,
+					'terms'		=> $obj->term_id,
+				];
 				$registrar = Registrar::getInstance($obj->taxonomy);
 				if (!$registrar) {
 					return;
 				}
 				if ($registrar->hasFeature('is_content')) {
-					//example: tattoo shop, etc TODO
+					$this->isContentTax = true;
+					$this->content = [$registrar->getBased()];
 					return;
 				}
 				$this->content = array_map(function ($item) { return jvbNoBase($item); }, $registrar->registrar->for);
+				$args['post_type'] = array_map('jvbCheckBase', $registrar->registrar->for);
+				foreach($args['post_type'] as $post_type) {
+					$reg = Registrar::getInstance($post_type);
+					if ($reg && $reg->hasFeature('is_timeline')) {
+						$args['post_parent'] = 0;
+					}
+				}
+				$this->args = $args;
 				return;
 			}
 		}
 		// not inheriting, getting from config
 		$this->content = $attrs['contentTypes']??[];
+
+		$args['post_type'] = array_map('jvbCheckBase', $attrs['contentTypes']);
+		$this->args = $args;
 	}
 		protected function getContent():string
 		{
@@ -212,7 +253,6 @@
 					'fields'	=> 'ids',
 				];
 				if (!is_null($this->context)) {
-
 					$context = Registrar::getInstance($this->context);
 					switch ($context->getType()) {
 						case 'term':
@@ -229,6 +269,7 @@
 				}
 				$check = new WP_Query($args);
 				$hasPosts = !empty($check->posts);
+				wp_reset_postdata();
 
 				$disabled = !$hasPosts;
 				$checked = $i === 0 && $hasPosts;
@@ -463,7 +504,7 @@
 			);
 		}
 
-	protected function renderGrid():string
+	protected function renderPlaceholders():string
 	{
 		$placeholders = '';
 		$total = count($this->content) - 1;
@@ -492,13 +533,58 @@
 		);
 	}
 
+
+	protected function renderGrid():string
+	{
+		if (empty($this->args)) {
+			return $this->renderPlaceholders();
+		}
+		$items = $this->getItems();
+
+		$out = '<div class="item-grid">';
+			$out .= implode('',array_map(function ($ID) {
+				$content = $this->isContentTax
+					? jvbNoBase($this->content[0])
+					: jvbNoBase(get_post_type($ID));
+
+				return $this->renderItem($content, $ID);
+			}, $items));
+		$out .= '</div>';
+		return $out;
+	}
+		protected function getItems():array
+		{
+			if ($this->isContentTax) {
+				$items = get_terms([
+					'taxonomy'	=> $this->content,
+					'fields'	=> 'ids',
+					'meta_key'	=> BASE.'date_modified',
+					'meta_type'	=> 'DATETIME',
+					'orderby'	=> 'meta_value',
+					'order'		=> 'desc',
+					'number'	=> $this->args['posts_per_page']
+				]);
+				$items = $items && !is_wp_error($items) ? $items : [];
+			} else {
+				$items = new WP_Query($this->args);
+				$this->hasMore = $items->found_posts > $this->args['posts_per_page'];
+				$items = $items->posts;
+				wp_reset_postdata();
+			}
+			return $items;
+		}
+
 	protected function renderLoader():string
 	{
-		return sprintf(
-			'<button type="button" class="load-more">%s<span>Show Me More</span>%s</button>
-			%s%s',
+		$button = sprintf(
+			'<button type="button" class="load-more"%s>%s<span>Show Me More</span>%s</button>',
+			$this->hasMore ? '' : ' hidden',
 			jvbIcon('arrow-elbow-left-down'),
 			jvbIcon('arrow-elbow-right-down'),
+		);
+		return sprintf(
+			'%s%s%s',
+			$button,
 			jvbLoadingScreen(),
 			$this->isGallery ? jvbRenderGallery(false) : '',
 		);
@@ -542,10 +628,10 @@
 
 	protected function renderActions():string
 	{
-		return sprintf(
+		return is_user_logged_in() ? sprintf(
 			'<button data-action="refresh" data-ignore>%s<span>Hard Refresh</span></span></button>',
 			jvbIcon('arrows-clockwise')
-		);
+		) : '';
 	}
 
 	public static function getFavouritesButton(string $content):string
@@ -554,10 +640,11 @@
 		if (!$registrar || !Site::has('favourites') || !$registrar->hasFeature('favouritable')) {
 			return '';
 		}
-		return '<button class="favourite" type="button" title="Add to favourites" data-action="favourite">
-			'.jvbIcon('heart')
-			.jvbIcon('heart', ['style'=>'fill']).'
-		</button>';
+		return sprintf(
+			'<button class="favourite" type="button" title="Add to favourites" data-action="favourite">%s%s</button>',
+			jvbIcon('heart'),
+			jvbIcon('heart', ['style'=>'fill'])
+		);
 	}
 	public static function getUpvotesButton(string $content):string
 	{
@@ -565,98 +652,239 @@
 		if (!Site::has('karma') || !$registrar || !$registrar->hasFeature('karma')){
 			return '';
 		}
-		return '<div class="karma row">
+		return sprintf(
+			'<div class="karma row">
 			<button type="button" class="vote" data-action="upvote">
-				'.jvbIcon('arrow-fat-up')
-				.jvbIcon('arrow-fat-up', ['style'=>'fill']).
-			'</button>
+				%s%s
+			</button>
 			<button type="button" class="vote" data-action="downvote">
-				'.jvbIcon('arrow-fat-down')
-				.jvbIcon('arrow-fat-down', ['style'=>'fill']).
-			'</button>
+				%s%s
+			</button>
 			<span class="score"></span>
-		</div>';
+		</div>',
+			jvbIcon('arrow-fat-up'),
+			jvbIcon('arrow-fat-up', ['style'=>'fill']),
+			jvbIcon('arrow-fat-down'),
+			jvbIcon('arrow-fat-down', ['style'=>'fill'])
+		);
 	}
 	protected function getDefaultTemplate(string $content): string
 	{
-		$template = apply_filters('jvbFeedItem', '', $content);
-		if (empty($template)) {
-			$config = Registrar::getInstance($content)->getConfig('feed');
-			$allFields = Registrar::getFieldsFor($content);
-			$images = $config['images']??['post_thumbnail'];
-			$fields = $config['fields']??['post_title','post_date','post_excerpt'];
-			$fields = array_filter($fields, function($field) use($images) {
-				return !in_array($field, $images);
-			});
-			$fields = array_filter($allFields, function($field) use($fields) {
-				return in_array($field, $fields);
-			}, ARRAY_FILTER_USE_KEY);
-
-
-			$template = sprintf(
-				'<div class="feed item col %s">%s%s',
-				$content,
-				self::getFavouritesButton($content),
-				self::getUpvotesButton($content)
-			);
-
-			//Add all defined images, but allow for filtering
-			$imageTemplate = '<a>';
-			foreach ($images as $image) {
-				$imageTemplate .= sprintf(
-					'<img data-field="%s" width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">',
-					$image
-				);
-			}
-			$imageTemplate .= '</a>';
-
-			$template .= sprintf(
-				'<div class="images">%s</div>',
-				apply_filters('jvbFeedImages', $imageTemplate, $content, $images)
-			);
-
-			//Output default fields, but allow for filtering
-			$template .= sprintf(
-				'<details>
-		<summary>%s</summary>',
-				apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content)
-			);
-
-			$fieldsTemplate = '';
-			foreach ($fields as $fieldName => $config) {
-				$fieldsTemplate .= apply_filters('jvbFeedItemField', $this->defaultFieldTemplate($config['type'], $fieldName), $content, $fieldName, $config['type']);
-			}
-			$template .= sprintf(
-				'<div class="item-info">%s</div>',
-				apply_filters('jvbFeedItemFields', $fieldsTemplate, $content, $fields)
-			);
-			$template .= '</details></div>';
-		}
-
-
 		return sprintf(
 			'<template class="feedItem%s">%s</template>',
 			ucfirst($content),
-			$template
+			$this->renderItem($content)
 		);
 	}
 
-	protected function defaultFieldTemplate(string $fieldType, string $fieldName):string
+	protected function renderItem(string $content, ?int $ID = null):string
+	{
+		/**
+		 * Allow plugins to replace the content within the feed item
+		 */
+		$function = BASE.'render_'.$content.'_feed_item';
+		if (function_exists($function)) {
+			$out = $function($ID);
+		} else {
+			$out = $this->buildFeedItem($content, $ID);
+		}
+		$registrar = Registrar::getInstance($content);
+		$data = [];
+		if ($registrar && $registrar->hasFeature('is_timeline')) {
+			$data[] = 'data-timeline';
+		}
+
+		$data = !empty($data) ? ' '.implode(' ', $data) : '';
+
+		return sprintf(
+			'<div class="feed item col %s"%s>%s%s%s</div>',
+			$content,
+			$data,
+			self::getFavouritesButton($content),
+			self::getUpvotesButton($content),
+			$out
+		);
+	}
+		protected function buildFeedItem(string $content, ?int $ID = null):string
+		{
+			$registrar = Registrar::getInstance($content);
+			$meta = is_null($ID) ? false :
+				match ($registrar->getType()) {
+					'post'	=> Meta::forPost($ID),
+					'term'	=> Meta::forTerm($ID),
+					'user'	=> Meta::forUser($ID),
+					default => false
+				};
+
+			[$images, $fields] = $registrar->getFeedFields();
+
+			/**
+			 * Get the main image for the feed item.
+			 * Output can be overridden with the $imagesFn
+			 */
+			$imagesFn = BASE.'render_'.$content.'_feed_item_images';
+			if (function_exists($imagesFn)) {
+				$img = $imagesFn($ID, $images);
+			} else {
+				$img = '';
+				foreach ($images as $config) {
+					$field = $config['name'];
+					$img .= $meta ? jvbFormatImage($meta->get($field), 'tiny', 'medium')
+						: $this->defaultFieldTemplate($config['type'], $field);
+				}
+			}
+			$img = sprintf(
+				'<div class="images"><a href="%s">%s</a></div>',
+				$ID ? get_the_permalink($ID) : '',
+				$img
+			);
+
+			/**
+			 * Start the Details with the fields
+			 * Plugins can modify the summary title with the 'jvbFeedItemSummary' filter
+			 */
+			$summary = sprintf(
+				'<details><summary>%s</summary>',
+				apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content)
+			);
+				/**
+				 * Work through the fields
+				 * Each field output can be overridden with $field or $fieldType
+				 */
+				foreach ($fields as $config) {
+					$f = $config['name'];
+					$functions = [
+						BASE.'render_'.$content.'_field_'.$f, 	//Overrides field for this content
+						BASE.'render_field_'.$f,				//Overrides field with this name
+						BASE.'render_field_type_'.$config['type'] //Overrides field of this type
+					];
+
+					$didIt = false;
+					foreach ($functions as $func) {
+						if (!$didIt && function_exists($func)) {
+							$didIt = true;
+							$summary .= $func($ID, is_null($ID));
+						}
+					}
+					if (!$didIt) {
+						$summary .= $this->defaultFieldTemplate($config['type'], $f, is_null($ID), $meta, $config);
+					}
+
+				}
+			$summary .= '</details>';
+
+			return $img.$summary;
+		}
+
+	protected function defaultFieldTemplate(string $fieldType, string $fieldName, bool $isTemplate = true, bool|Meta $meta = false, array $config = []):string
 	{
 		$data = ' data-field="'.$fieldName.'"';
+		$value = $meta ? $meta->get($fieldName) : '';
+		if (!$isTemplate && empty($value)) {
+			return '';
+		}
 		switch ($fieldName) {
 			case 'post_title':
-				return '<h3'.$data.'></h3>';
+				return sprintf(
+					'<h3%s>%s</h3>',
+			$data,
+					$meta && !empty($value) ? $value : '',
+				);
 			case 'post_date':
 			case 'post_modified':
-				return '<time'.$data.'></time>';
+				return sprintf(
+					'<time%s%s>%s</time>',
+					$data,
+					$meta && !empty($value) ? date('c', strtotime($value)) : '',
+					$meta && !empty($value) ? date('F j, Y', strtotime($value)) : '',
+				);
 		}
-		return match($fieldType) {
-			'date','datetime','time' => '<time'.$data.'></time>',
-			'upload'	=> '<img'.$data.' width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">',
-			'taxonomy'	=> '<ul'.$data.'><li><a><i></i></a></li></ul>',
-			default	=>  '<p'.$data.'></p>',
+
+		return match ($fieldType) {
+			'date' => sprintf(
+				'<time%s%s>%s</time>',
+				$data,
+				$meta && !empty($value) ? date('c', strtotime($value)) : '',
+				$meta && !empty($value) ? date('F j, Y', strtotime($value)) : '',
+			),
+			'datetime' => sprintf(
+				'<time%s%s>%s</time>',
+				$data,
+				$meta && !empty($value) ? date('c', strtotime($value)) : '',
+				$meta && !empty($value) ? date('F j, Y at h:iA', strtotime($value)) : '',
+			),
+			'time' => sprintf(
+				'<time%s%s>%s</time>',
+				$data,
+				$value,
+				$value
+			),
+			'upload' => empty($value) ? sprintf(
+				'<img%s width="300px" height="300px" loading="lazy" decoding="async">',
+				$data
+			) :
+				str_replace('<img', '<img' . $data, jvbFormatImage($value)),
+			'selector' => sprintf(
+				'<ul%s class="terms %s">%s</ul>',
+				$data,
+				$config['taxonomy']??$config['post_type']??$config['role']??'',
+				empty($value) ? sprintf('<li><a>%s</a></li>',
+					$this->iconFor($config)
+				) :
+					implode('', array_filter(array_map(function ($ID) use ($config) {
+						$type = $config['subtype'];
+						$taxonomy = isset($config['taxonomy']) ? jvbCheckBase($config['taxonomy']) : '';
+
+						$item = match ($type) {
+							'taxonomy' => get_term($ID, $taxonomy),
+							'user' => get_userdata($ID),
+							'post' => get_post($ID),
+							default => false,
+						};
+
+						if (!$item || is_wp_error($item)) {
+							return '';
+						}
+
+						$icon = $this->iconFor($config);
+
+						$name = match ($type) {
+							'taxonomy' => $item->name,
+							'user' => $item->display_name,
+							'post' => $item->post_title,
+							default => '',
+						};
+						$url = match ($type) {
+							'taxonomy' => get_term_link((int)$ID, $taxonomy),
+							'user' => get_the_permalink(get_user_meta($ID, BASE . 'userLink', true)),
+							'post' => get_the_permalink($ID),
+							default => ''
+						};
+						return sprintf(
+							'<li><a href="%s">%s%s</a></li>',
+							$url,
+							!empty($icon) ? jvbIcon($icon) : '',
+							$name
+						);
+					}, explode(',', $value))))
+			),
+			default => sprintf(
+				'<p%s>%s</p>',
+				$data,
+				$value
+			),
 		};
 	}
+	protected function iconFor(array $fieldConfig):string
+	{
+		$icon = match ($fieldConfig['type']??'') {
+			'taxonomy' => Registrar::getInstance($fieldConfig['taxonomy'])->getIcon(),
+			'user' => isset($fieldConfig['role']) ? Registrar::getInstance($fieldConfig['role'])->getIcon() : 'user',
+			'post' => Registrar::getInstance($fieldConfig['post_type'])->getIcon(),
+			default => ''
+		};
+
+		return empty($icon) ? $icon : jvbIcon($icon);
+	}
 
 }

--
Gitblit v1.10.0