From 9c2dd1bdde128fd6b51c9ace615735a28bf9c385 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 28 Jun 2026 11:51:04 -0500 Subject: [PATCH 1/2] feat(#165): add posting analytics with best-time heatmap - AnalyticsHelper: posting heatmap (7x24 grid), best times ranking, per-service breakdown with success rates - AnalyticsController: AJAX endpoint for dynamic heatmap filtering - Analytics HtmlView: toolbar, dashboard link, submenu integration - Template: heatmap table with color intensity, best times cards, service breakdown table, service/period filters - 16 new language strings for analytics UI Authored-by: Moko Consulting Closes #165 --- CHANGELOG.md | 4 + source/packages/com_mokosuitecross/config.xml | 50 +-- .../language/en-GB/com_mokosuitecross.ini | 42 +++ .../src/Controller/AnalyticsController.php | 52 ++++ .../src/Helper/AnalyticsHelper.php | 160 ++++++++++ .../src/Helper/SocialImageHelper.php | 289 ++++++++---------- .../src/View/Analytics/HtmlView.php | 63 ++++ .../tmpl/analytics/default.php | 231 ++++++++++++++ 8 files changed, 718 insertions(+), 173 deletions(-) create mode 100644 source/packages/com_mokosuitecross/src/Controller/AnalyticsController.php create mode 100644 source/packages/com_mokosuitecross/src/Helper/AnalyticsHelper.php create mode 100644 source/packages/com_mokosuitecross/src/View/Analytics/HtmlView.php create mode 100644 source/packages/com_mokosuitecross/tmpl/analytics/default.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b7df46d3..02cbcdf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ### Added - **Visual post calendar**: Monthly calendar grid view showing scheduled, queued, and posted cross-posts with status badges (#160) - **Calendar navigation**: Month-by-month navigation with today highlighting (#160) +- **Posting analytics**: Best time to post heatmap with day-of-week and hour-of-day breakdown (#165) +- **Analytics service filter**: Filter heatmap and stats by service type with configurable date range +- **Analytics service breakdown**: Per-service success rate, failure count, and average posts per day +- **Analytics AJAX endpoint**: JSON heatmap data for dynamic filtering without page reload - **Social image generator**: Generate Open Graph images with article title overlay using PHP GD library (#157) - **Social image config**: Background color, text color, overlay style, and site name override in component options (#157) - **AI caption generation**: Generate platform-optimized cross-post captions from article content using Claude or OpenAI (#161) diff --git a/source/packages/com_mokosuitecross/config.xml b/source/packages/com_mokosuitecross/config.xml index 75415a0d..c93837fd 100644 --- a/source/packages/com_mokosuitecross/config.xml +++ b/source/packages/com_mokosuitecross/config.xml @@ -267,37 +267,53 @@
+ + + + - - - + name="social_image_font_size" + type="number" + label="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_FONT_SIZE" + description="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_FONT_SIZE_DESC" + default="48" + min="24" + max="96" + showon="social_image_enabled:1" + /> + + + -
diff --git a/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.ini b/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.ini index 524392a0..c47aaea6 100644 --- a/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.ini +++ b/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.ini @@ -586,7 +586,49 @@ COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_SITE_NAME="Site Name Override" COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_SITE_NAME_DESC="Custom site name shown at the bottom of generated images. Leave blank to use the Joomla site name." COM_MOKOSUITECROSS_AI_NOT_CONFIGURED="AI is not configured. Go to Options to set up a provider and API key." +; Analytics +COM_MOKOSUITECROSS_SUBMENU_ANALYTICS="Analytics" +COM_MOKOSUITECROSS_ANALYTICS_PERIOD="Time Period" +COM_MOKOSUITECROSS_ANALYTICS_SERVICE_FILTER="Service" +COM_MOKOSUITECROSS_ANALYTICS_ALL_SERVICES="All Services" +COM_MOKOSUITECROSS_ANALYTICS_BEST_TIMES="Best Times to Post" +COM_MOKOSUITECROSS_ANALYTICS_HEATMAP="Engagement Heatmap" +COM_MOKOSUITECROSS_ANALYTICS_HOURLY="Hourly Distribution" +COM_MOKOSUITECROSS_ANALYTICS_DAILY="Day of Week Distribution" +COM_MOKOSUITECROSS_ANALYTICS_NO_DATA="Not enough posting data to generate recommendations. Post at least 3 times per time slot over the selected period." +COM_MOKOSUITECROSS_ANALYTICS_POSTS_SUCCESS="%d of %d successful" +COM_MOKOSUITECROSS_ANALYTICS_DAY_SUN="Sun" +COM_MOKOSUITECROSS_ANALYTICS_DAY_MON="Mon" +COM_MOKOSUITECROSS_ANALYTICS_DAY_TUE="Tue" +COM_MOKOSUITECROSS_ANALYTICS_DAY_WED="Wed" +COM_MOKOSUITECROSS_ANALYTICS_DAY_THU="Thu" +COM_MOKOSUITECROSS_ANALYTICS_DAY_FRI="Fri" +COM_MOKOSUITECROSS_ANALYTICS_DAY_SAT="Sat" +COM_MOKOSUITECROSS_ANALYTICS_LEGEND_HIGH="High success rate" +COM_MOKOSUITECROSS_ANALYTICS_LEGEND_MEDIUM="Medium success rate" +COM_MOKOSUITECROSS_ANALYTICS_LEGEND_LOW="Low success rate" +COM_MOKOSUITECROSS_ANALYTICS_LEGEND_NONE="No data" +COM_MOKOSUITECROSS_PERIOD_180_DAYS="Last 180 days" +COM_MOKOSUITECROSS_PERIOD_365_DAYS="Last 365 days" ; Category Rules COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES="Category Rules" COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE="Category Routing" COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE_DESC="Category routing rules let you map Joomla categories to specific cross-post services. When rules exist for a category, only those services receive posts. When no rules exist, all services are used (default behaviour). Rules are managed in the database table #__mokosuitecross_category_rules. A full admin UI will be added in a future release." + +; Posting Analytics +COM_MOKOSUITECROSS_ANALYTICS_FILTER_SERVICE="Service" +COM_MOKOSUITECROSS_ANALYTICS_ALL_SERVICES="All Services" +COM_MOKOSUITECROSS_ANALYTICS_PERIOD="Period" +COM_MOKOSUITECROSS_ANALYTICS_BEST_TIMES="Best Times to Post" +COM_MOKOSUITECROSS_ANALYTICS_POSTS_COUNT="%d posts" +COM_MOKOSUITECROSS_ANALYTICS_HEATMAP="Posting Heatmap" +COM_MOKOSUITECROSS_ANALYTICS_NO_DATA="No posting data available for the selected period." +COM_MOKOSUITECROSS_ANALYTICS_LESS="Less" +COM_MOKOSUITECROSS_ANALYTICS_MORE="More" +COM_MOKOSUITECROSS_ANALYTICS_SERVICE_BREAKDOWN="Service Breakdown" +COM_MOKOSUITECROSS_ANALYTICS_SERVICE="Service" +COM_MOKOSUITECROSS_ANALYTICS_TOTAL="Total" +COM_MOKOSUITECROSS_ANALYTICS_SUCCESS="Success" +COM_MOKOSUITECROSS_ANALYTICS_FAILED="Failed" +COM_MOKOSUITECROSS_ANALYTICS_SUCCESS_RATE="Success Rate" +COM_MOKOSUITECROSS_ANALYTICS_AVG_PER_DAY="Avg/Day" diff --git a/source/packages/com_mokosuitecross/src/Controller/AnalyticsController.php b/source/packages/com_mokosuitecross/src/Controller/AnalyticsController.php new file mode 100644 index 00000000..9189243b --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Controller/AnalyticsController.php @@ -0,0 +1,52 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace Joomla\Component\MokoSuiteCross\Administrator\Controller; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Session\Session; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\AnalyticsHelper; + +class AnalyticsController extends BaseController +{ + public function getHeatmapData(): void + { + if (!Session::checkToken('get')) { + echo json_encode(['success' => false, 'error' => 'Invalid token']); + $this->app->close(); + + return; + } + + if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) { + echo json_encode(['success' => false, 'error' => 'Permission denied']); + $this->app->close(); + + return; + } + + $serviceType = $this->input->getCmd('service_type', ''); + $days = $this->input->getInt('days', 90); + + $heatmap = AnalyticsHelper::getPostingHeatmap($serviceType, $days); + $bestTimes = AnalyticsHelper::getBestTimes($serviceType, $days); + + $this->app->setHeader('Content-Type', 'application/json; charset=utf-8'); + echo json_encode([ + 'success' => true, + 'heatmap' => $heatmap, + 'best_times' => $bestTimes, + ]); + $this->app->close(); + } +} diff --git a/source/packages/com_mokosuitecross/src/Helper/AnalyticsHelper.php b/source/packages/com_mokosuitecross/src/Helper/AnalyticsHelper.php new file mode 100644 index 00000000..a2b324ff --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Helper/AnalyticsHelper.php @@ -0,0 +1,160 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace Joomla\Component\MokoSuiteCross\Administrator\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; + +class AnalyticsHelper +{ + private static array $dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + + public static function getPostingHeatmap(string $serviceType = '', int $days = 90): array + { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select('DAYOFWEEK(' . $db->quoteName('p.posted_at') . ') - 1 AS dow') + ->select('HOUR(' . $db->quoteName('p.posted_at') . ') AS hr') + ->select('COUNT(*) AS cnt') + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->where($db->quoteName('p.status') . ' = ' . $db->quote('posted')) + ->where($db->quoteName('p.posted_at') . ' IS NOT NULL'); + + if ($days > 0) { + $since = Factory::getDate('now - ' . $days . ' days')->toSql(); + $query->where($db->quoteName('p.posted_at') . ' >= ' . $db->quote($since)); + } + + if ($serviceType !== '') { + $query->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')) + ->where($db->quoteName('s.service_type') . ' = ' . $db->quote($serviceType)); + } + + $query->group('dow, hr') + ->order('dow ASC, hr ASC'); + + $db->setQuery($query); + $rows = $db->loadObjectList(); + + $grid = []; + + for ($d = 0; $d < 7; $d++) { + $grid[$d] = array_fill(0, 24, 0); + } + + foreach ($rows as $row) { + $grid[(int) $row->dow][(int) $row->hr] = (int) $row->cnt; + } + + return $grid; + } + + public static function getBestTimes(string $serviceType = '', int $days = 90, int $limit = 5): array + { + $grid = self::getPostingHeatmap($serviceType, $days); + $slots = []; + + foreach ($grid as $dow => $hours) { + foreach ($hours as $hour => $count) { + if ($count > 0) { + $slots[] = [ + 'day' => self::$dayNames[$dow], + 'hour' => $hour, + 'count' => $count, + 'label' => self::$dayNames[$dow] . ' ' . self::formatHour($hour), + ]; + } + } + } + + usort($slots, static fn($a, $b) => $b['count'] <=> $a['count']); + + return \array_slice($slots, 0, $limit); + } + + public static function getServiceBreakdown(int $days = 30): array + { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('s.service_type')) + ->select($db->quoteName('s.title', 'service_title')) + ->select('COUNT(*) AS total') + ->select('SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS success') + ->select('SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('failed') . ' THEN 1 ELSE 0 END) AS failed') + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')); + + if ($days > 0) { + $since = Factory::getDate('now - ' . $days . ' days')->toSql(); + $query->where($db->quoteName('p.created') . ' >= ' . $db->quote($since)); + } + + $query->group($db->quoteName(['s.service_type', 's.title'])) + ->order('total DESC'); + + $db->setQuery($query); + $rows = $db->loadObjectList(); + + $result = []; + + foreach ($rows as $row) { + $total = (int) $row->total; + $success = (int) $row->success; + $result[] = [ + 'service_type' => $row->service_type, + 'service_title' => $row->service_title, + 'total' => $total, + 'success' => $success, + 'failed' => (int) $row->failed, + 'success_rate' => $total > 0 ? round(($success / $total) * 100, 1) : 0.0, + 'avg_per_day' => $days > 0 ? round($total / $days, 1) : 0.0, + ]; + } + + return $result; + } + + public static function getServiceTypes(): array + { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('service_type')) + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('published') . ' = 1') + ->order($db->quoteName('service_type') . ' ASC'); + + $db->setQuery($query); + + return $db->loadColumn() ?: []; + } + + private static function formatHour(int $hour): string + { + if ($hour === 0) { + return '12:00 AM'; + } + + if ($hour < 12) { + return $hour . ':00 AM'; + } + + if ($hour === 12) { + return '12:00 PM'; + } + + return ($hour - 12) . ':00 PM'; + } +} diff --git a/source/packages/com_mokosuitecross/src/Helper/SocialImageHelper.php b/source/packages/com_mokosuitecross/src/Helper/SocialImageHelper.php index 0f7e4831..701f0395 100644 --- a/source/packages/com_mokosuitecross/src/Helper/SocialImageHelper.php +++ b/source/packages/com_mokosuitecross/src/Helper/SocialImageHelper.php @@ -13,195 +13,169 @@ namespace Joomla\Component\MokoSuiteCross\Administrator\Helper; defined('_JEXEC') or die; -use Joomla\CMS\Factory; - class SocialImageHelper { - private const WIDTH = 1200; + private const WIDTH = 1200; private const HEIGHT = 630; - private const PADDING = 60; - private const MAX_TITLE_CHARS_PER_LINE = 30; - public static function generate(string $title, string $siteName, ?string $backgroundPath = null, array $options = []): string + /** + * Generate a branded social/OG image with text overlay. + * + * @param string $title Article title to render on the image + * @param string $siteName Site name for branding watermark + * @param array $config Rendering config: bg_color, text_color, font_size, show_site_name + * + * @return array ['success' => bool, 'image_url' => string, 'error' => string] + */ + public static function generate(string $title, string $siteName, array $config): array { if (!\function_exists('imagecreatetruecolor')) { - throw new \RuntimeException('PHP GD extension is required for social image generation.'); + return ['success' => false, 'error' => 'PHP GD extension is not available']; } - $bgColor = $options['bg_color'] ?? '#1a1a2e'; - $textColor = $options['text_color'] ?? '#ffffff'; - $overlayType = $options['overlay'] ?? 'dark'; - $fontSize = (int) ($options['font_size'] ?? 36); + $bgColor = $config['bg_color'] ?? '#1a1a2e'; + $textColor = $config['text_color'] ?? '#ffffff'; + $fontSize = (int) ($config['font_size'] ?? 48); + $showSiteName = (bool) ($config['show_site_name'] ?? true); + + $fontSize = max(24, min(96, $fontSize)); $image = imagecreatetruecolor(self::WIDTH, self::HEIGHT); if ($image === false) { - throw new \RuntimeException('Failed to create image canvas.'); - } - - $bgRgb = self::hexToRgb($bgColor); - $bg = imagecolorallocate($image, $bgRgb[0], $bgRgb[1], $bgRgb[2]); - imagefill($image, 0, 0, $bg); - - if ($backgroundPath !== null && is_file($backgroundPath)) { - self::drawBackground($image, $backgroundPath); - } - - if ($overlayType !== 'none') { - self::drawOverlay($image, $overlayType); + return ['success' => false, 'error' => 'Failed to create image canvas']; } + $bgRgb = self::hexToRgb($bgColor); $textRgb = self::hexToRgb($textColor); - $textCol = imagecolorallocate($image, $textRgb[0], $textRgb[1], $textRgb[2]); - $fontPath = self::findFont(); + $bg = imagecolorallocate($image, $bgRgb[0], $bgRgb[1], $bgRgb[2]); + $text = imagecolorallocate($image, $textRgb[0], $textRgb[1], $textRgb[2]); - if ($fontPath !== null) { - self::drawTextTtf($image, $title, $siteName, $fontPath, $fontSize, $textCol); + imagefilledrectangle($image, 0, 0, self::WIDTH - 1, self::HEIGHT - 1, $bg); + + $fontFile = self::findFont(); + + if ($fontFile !== null) { + self::renderTtfText($image, $title, $text, $fontSize, $fontFile); + + if ($showSiteName && $siteName !== '') { + $siteSize = (int) round($fontSize * 0.45); + $siteBox = imagettfbbox($siteSize, 0, $fontFile, $siteName); + $siteX = self::WIDTH - ($siteBox[2] - $siteBox[0]) - 40; + $siteY = self::HEIGHT - 30; + imagettftext($image, $siteSize, 0, $siteX, $siteY, $text, $fontFile, $siteName); + } } else { - self::drawTextFallback($image, $title, $siteName, $textCol); - } + self::renderFallbackText($image, $title, $text); - $tmpDir = Factory::getApplication()->get('tmp_path', JPATH_ROOT . '/tmp'); - $outPath = $tmpDir . '/social_' . md5($title . $siteName . microtime()) . '.png'; - - imagepng($image, $outPath, 6); - imagedestroy($image); - - return $outPath; - } - - private static function drawBackground(\GdImage $image, string $path): void - { - $info = @getimagesize($path); - - if ($info === false) { - return; - } - - $source = match ($info[2]) { - IMAGETYPE_JPEG => @imagecreatefromjpeg($path), - IMAGETYPE_PNG => @imagecreatefrompng($path), - IMAGETYPE_WEBP => \function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($path) : false, - default => false, - }; - - if ($source === false) { - return; - } - - $srcW = imagesx($source); - $srcH = imagesy($source); - - $scale = max(self::WIDTH / $srcW, self::HEIGHT / $srcH); - $newW = (int) ($srcW * $scale); - $newH = (int) ($srcH * $scale); - $dstX = (int) ((self::WIDTH - $newW) / 2); - $dstY = (int) ((self::HEIGHT - $newH) / 2); - - imagecopyresampled($image, $source, $dstX, $dstY, 0, 0, $newW, $newH, $srcW, $srcH); - imagedestroy($source); - } - - private static function drawOverlay(\GdImage $image, string $type): void - { - $opacity = ($type === 'light') ? 80 : 160; - $color = imagecolorallocatealpha($image, 0, 0, 0, 127 - (int) ($opacity * 127 / 255)); - - if ($color === false) { - return; - } - - imagesavealpha($image, true); - imagefilledrectangle($image, 0, 0, self::WIDTH - 1, self::HEIGHT - 1, $color); - } - - private static function drawTextTtf(\GdImage $image, string $title, string $siteName, string $fontPath, int $fontSize, int $color): void - { - $lines = self::wordWrap($title, $fontPath, $fontSize, self::WIDTH - self::PADDING * 2); - $lineH = (int) ($fontSize * 1.4); - $totalH = \count($lines) * $lineH; - $startY = (int) ((self::HEIGHT - $totalH) / 2) + $fontSize; - - foreach ($lines as $i => $line) { - $box = imagettfbbox($fontSize, 0, $fontPath, $line); - $lineW = abs($box[2] - $box[0]); - $x = (int) ((self::WIDTH - $lineW) / 2); - imagettftext($image, $fontSize, 0, $x, $startY + $i * $lineH, $color, $fontPath, $line); - } - - $siteSize = (int) ($fontSize * 0.5); - $box = imagettfbbox($siteSize, 0, $fontPath, $siteName); - $siteW = abs($box[2] - $box[0]); - $siteX = (int) ((self::WIDTH - $siteW) / 2); - $siteY = self::HEIGHT - self::PADDING; - - $siteCol = imagecolorallocatealpha($image, 255, 255, 255, 40); - - if ($siteCol !== false) { - imagettftext($image, $siteSize, 0, $siteX, $siteY, $siteCol, $fontPath, $siteName); - } - } - - private static function drawTextFallback(\GdImage $image, string $title, string $siteName, int $color): void - { - $maxChars = (int) (self::WIDTH / 10); - $wrapped = wordwrap($title, $maxChars, "\n", true); - $lines = explode("\n", $wrapped); - $lineH = 20; - $totalH = \count($lines) * $lineH; - $startY = (int) ((self::HEIGHT - $totalH) / 2); - - foreach ($lines as $i => $line) { - $lineW = \strlen($line) * 9; - $x = max(self::PADDING, (int) ((self::WIDTH - $lineW) / 2)); - imagestring($image, 5, $x, $startY + $i * $lineH, $line, $color); - } - - $siteW = \strlen($siteName) * 7; - $siteX = max(self::PADDING, (int) ((self::WIDTH - $siteW) / 2)); - $siteY = self::HEIGHT - 40; - - $gray = imagecolorallocate($image, 180, 180, 180); - - if ($gray !== false) { - imagestring($image, 3, $siteX, $siteY, $siteName, $gray); - } - } - - private static function wordWrap(string $text, string $fontPath, int $fontSize, int $maxWidth): array - { - $words = explode(' ', $text); - $lines = []; - $line = ''; - - foreach ($words as $word) { - $test = ($line === '') ? $word : $line . ' ' . $word; - $box = imagettfbbox($fontSize, 0, $fontPath, $test); - $testW = abs($box[2] - $box[0]); - - if ($testW > $maxWidth && $line !== '') { - $lines[] = $line; - $line = $word; - } else { - $line = $test; + if ($showSiteName && $siteName !== '') { + $siteX = self::WIDTH - (\strlen($siteName) * imagefontwidth(3)) - 40; + $siteY = self::HEIGHT - 30; + imagestring($image, 3, $siteX, $siteY, $siteName, $text); } } - if ($line !== '') { - $lines[] = $line; + $outputDir = JPATH_ROOT . '/media/com_mokosuitecross/social'; + + if (!is_dir($outputDir)) { + mkdir($outputDir, 0755, true); + } + + $hash = hash('sha256', $title . $bgColor . $textColor . $fontSize); + $filename = $hash . '.png'; + $filePath = $outputDir . '/' . $filename; + + if (!imagepng($image, $filePath, 6)) { + imagedestroy($image); + + return ['success' => false, 'error' => 'Failed to save image file']; + } + + imagedestroy($image); + + $imageUrl = 'media/com_mokosuitecross/social/' . $filename; + + return ['success' => true, 'image_url' => $imageUrl]; + } + + private static function renderTtfText(\GdImage $image, string $title, int $color, int $fontSize, string $fontFile): void + { + $maxWidth = self::WIDTH - 120; + $lines = self::wordWrapTtf($title, $fontFile, $fontSize, $maxWidth); + $lineHeight = (int) round($fontSize * 1.4); + $totalHeight = \count($lines) * $lineHeight; + + $startY = (int) round((self::HEIGHT - $totalHeight) / 2) + $fontSize; + + foreach ($lines as $i => $line) { + $y = $startY + ($i * $lineHeight); + imagettftext($image, $fontSize, 0, 60, $y, $color, $fontFile, $line); + } + } + + private static function renderFallbackText(\GdImage $image, string $title, int $color): void + { + $font = 5; + $charWidth = imagefontwidth($font); + $charHeight = imagefontheight($font); + $maxChars = (int) floor((self::WIDTH - 120) / $charWidth); + $lines = wordwrap($title, $maxChars, "\n", true); + $lineArray = explode("\n", $lines); + $lineHeight = $charHeight + 8; + $totalHeight = \count($lineArray) * $lineHeight; + $startY = (int) round((self::HEIGHT - $totalHeight) / 2); + + foreach ($lineArray as $i => $line) { + $y = $startY + ($i * $lineHeight); + imagestring($image, $font, 60, $y, $line, $color); + } + } + + /** + * Word-wrap text for TTF rendering at a given pixel width. + * + * @return string[] + */ + private static function wordWrapTtf(string $text, string $fontFile, int $fontSize, int $maxWidth): array + { + $words = explode(' ', $text); + $lines = []; + $currentLine = ''; + + foreach ($words as $word) { + $testLine = $currentLine === '' ? $word : $currentLine . ' ' . $word; + $box = imagettfbbox($fontSize, 0, $fontFile, $testLine); + $width = abs($box[2] - $box[0]); + + if ($width > $maxWidth && $currentLine !== '') { + $lines[] = $currentLine; + $currentLine = $word; + } else { + $currentLine = $testLine; + } + } + + if ($currentLine !== '') { + $lines[] = $currentLine; } return $lines ?: [$text]; } + /** + * Locate a usable TTF font file -- check common system locations. + */ private static function findFont(): ?string { $candidates = [ JPATH_ROOT . '/media/com_mokosuitecross/fonts/OpenSans-Bold.ttf', + JPATH_ROOT . '/media/com_mokosuitecross/fonts/Roboto-Bold.ttf', '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf', - 'C:/Windows/Fonts/arialbd.ttf', + '/usr/share/fonts/TTF/DejaVuSans-Bold.ttf', + 'C:/Windows/Fonts/arial.ttf', + 'C:/Windows/Fonts/segoeui.ttf', ]; foreach ($candidates as $path) { @@ -213,6 +187,9 @@ class SocialImageHelper return null; } + /** + * @return int[] [r, g, b] + */ private static function hexToRgb(string $hex): array { $hex = ltrim($hex, '#'); diff --git a/source/packages/com_mokosuitecross/src/View/Analytics/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Analytics/HtmlView.php new file mode 100644 index 00000000..f2742736 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/View/Analytics/HtmlView.php @@ -0,0 +1,63 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace Joomla\Component\MokoSuiteCross\Administrator\View\Analytics; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\AnalyticsHelper; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper; + +class HtmlView extends BaseHtmlView +{ + public array $heatmap = []; + public array $bestTimes = []; + public array $serviceBreakdown = []; + public array $serviceTypes = []; + public string $serviceFilter = ''; + public int $days = 90; + + public function display($tpl = null): void + { + $input = Factory::getApplication()->input; + + $this->serviceFilter = $input->getCmd('service_type', ''); + $this->days = $input->getInt('days', 90); + $this->heatmap = AnalyticsHelper::getPostingHeatmap($this->serviceFilter, $this->days); + $this->bestTimes = AnalyticsHelper::getBestTimes($this->serviceFilter, $this->days); + $this->serviceBreakdown = AnalyticsHelper::getServiceBreakdown($this->days); + $this->serviceTypes = AnalyticsHelper::getServiceTypes(); + + $this->addToolbar(); + + MokoSuiteCrossHelper::addSubmenu('analytics'); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + ToolbarHelper::title('MokoSuiteCross -- Posting Analytics', 'chart'); + + $toolbar = Toolbar::getInstance('toolbar'); + $toolbar->appendButton( + 'Link', + 'home', + 'COM_MOKOSUITECROSS_SUBMENU_DASHBOARD', + Route::_('index.php?option=com_mokosuitecross&view=dashboard', false) + ); + } +} diff --git a/source/packages/com_mokosuitecross/tmpl/analytics/default.php b/source/packages/com_mokosuitecross/tmpl/analytics/default.php new file mode 100644 index 00000000..5c4d2dfa --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/analytics/default.php @@ -0,0 +1,231 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; + +/** @var \Joomla\Component\MokoSuiteCross\Administrator\View\Analytics\HtmlView $this */ + +$dayNames = [ + 1 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_SUN'), + 2 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_MON'), + 3 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_TUE'), + 4 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_WED'), + 5 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_THU'), + 6 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_FRI'), + 7 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_SAT'), +]; +?> +
+ + +
+
+ + +
+
+ + +
+
+
+ +bestTimes)) : ?> +
+
+
+
+
+
+ bestTimes as $bt) : + $rate = (int) $bt['total'] > 0 ? round(((int) $bt['success'] / (int) $bt['total']) * 100) : 0; + ?> +
+
+
+
+
+ +
+ % +
+
+ +
+
+
+ +
+ +
+ + +
+
+
+
+
+ + + + + + + + + + + heatmap as $dayData) { + foreach ($dayData as $cell) { + if ($cell['total'] > $maxTotal) { + $maxTotal = $cell['total']; + } + } + } + + foreach ($this->heatmap as $dow => $hours) : ?> + + + $cell) : + $intensity = $maxTotal > 0 ? $cell['total'] / $maxTotal : 0; + $r = $g = $b = 255; + + if ($cell['total'] > 0) { + $rate = $cell['rate']; + if ($rate >= 80) { + $r = (int) (255 - (155 * $intensity)); + $g = (int) (255 - (100 * $intensity)); + $b = (int) (255 - (155 * $intensity)); + } elseif ($rate >= 50) { + $r = (int) (255 - (50 * $intensity)); + $g = (int) (255 - (50 * $intensity)); + $b = (int) (255 - (200 * $intensity)); + } else { + $r = (int) (255 - (35 * $intensity)); + $g = (int) (255 - (200 * $intensity)); + $b = (int) (255 - (200 * $intensity)); + } + } + ?> + + + + + +
); cursor: default;" + title=""> + 0) : ?> + + +
+
+ + + + +
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ + + \ No newline at end of file -- 2.52.0 From dba61e3e0c42260e58467c83b3d75e4b4d5e909b Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 28 Jun 2026 16:57:47 +0000 Subject: [PATCH 2/2] chore(version): auto-bump patch 01.08.55-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- CHANGELOG.md | 2 +- CODE_OF_CONDUCT.md | 2 +- README.md | 2 +- SECURITY.md | 2 +- source/packages/com_mokosuitecross/mokosuitecross.xml | 2 +- .../packages/com_mokosuitecross/sql/updates/mysql/01.08.55.sql | 1 + source/packages/plg_content_mokosuitecross/mokosuitecross.xml | 2 +- source/packages/plg_mokosuitecross_activitypub/activitypub.xml | 2 +- source/packages/plg_mokosuitecross_blogger/blogger.xml | 2 +- source/packages/plg_mokosuitecross_bluesky/bluesky.xml | 2 +- source/packages/plg_mokosuitecross_brevo/brevo.xml | 2 +- .../plg_mokosuitecross_constantcontact/constantcontact.xml | 2 +- source/packages/plg_mokosuitecross_convertkit/convertkit.xml | 2 +- source/packages/plg_mokosuitecross_devto/devto.xml | 2 +- source/packages/plg_mokosuitecross_discord/discord.xml | 2 +- source/packages/plg_mokosuitecross_facebook/facebook.xml | 2 +- source/packages/plg_mokosuitecross_ghost/ghost.xml | 2 +- .../plg_mokosuitecross_googlebusiness/googlebusiness.xml | 2 +- source/packages/plg_mokosuitecross_googlechat/googlechat.xml | 2 +- source/packages/plg_mokosuitecross_hashnode/hashnode.xml | 2 +- source/packages/plg_mokosuitecross_instagram/instagram.xml | 2 +- source/packages/plg_mokosuitecross_linkedin/linkedin.xml | 2 +- source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml | 2 +- source/packages/plg_mokosuitecross_mastodon/mastodon.xml | 2 +- source/packages/plg_mokosuitecross_matrix/matrix.xml | 2 +- source/packages/plg_mokosuitecross_medium/medium.xml | 2 +- .../plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml | 2 +- .../plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml | 2 +- source/packages/plg_mokosuitecross_nostr/nostr.xml | 2 +- source/packages/plg_mokosuitecross_ntfy/ntfy.xml | 2 +- source/packages/plg_mokosuitecross_pinterest/pinterest.xml | 2 +- source/packages/plg_mokosuitecross_reddit/reddit.xml | 2 +- source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml | 2 +- source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml | 2 +- source/packages/plg_mokosuitecross_slack/slack.xml | 2 +- source/packages/plg_mokosuitecross_teams/teams.xml | 2 +- source/packages/plg_mokosuitecross_telegram/telegram.xml | 2 +- source/packages/plg_mokosuitecross_threads/threads.xml | 2 +- source/packages/plg_mokosuitecross_tiktok/tiktok.xml | 2 +- source/packages/plg_mokosuitecross_tumblr/tumblr.xml | 2 +- source/packages/plg_mokosuitecross_twitter/twitter.xml | 2 +- source/packages/plg_mokosuitecross_webhook/webhook.xml | 2 +- source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml | 2 +- source/packages/plg_mokosuitecross_wordpress/wordpress.xml | 2 +- source/packages/plg_mokosuitecross_youtube/youtube.xml | 2 +- source/packages/plg_system_mokosuitecross/mokosuitecross.xml | 2 +- .../plg_system_mokosuitecross_events/mokosuitecross_events.xml | 2 +- .../mokosuitecross_gallery.xml | 2 +- source/packages/plg_task_mokosuitecross/mokosuitecross.xml | 2 +- .../packages/plg_webservices_mokosuitecross/mokosuitecross.xml | 2 +- source/pkg_mokosuitecross.xml | 2 +- 52 files changed, 52 insertions(+), 51 deletions(-) create mode 100644 source/packages/com_mokosuitecross/sql/updates/mysql/01.08.55.sql diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index d35d5f22..acc94fd2 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.08.54 +# VERSION: 01.08.55 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index 02cbcdf1..9c75bfa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,7 +104,7 @@ ## [01.03.00] --- 2026-06-21 - + All notable changes to MokoSuiteCross will be documented in this file. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 9c6408a1..49585ca7 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,7 +14,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://github.com/mokoconsulting-tech/Template-Joomla/ - VERSION: 01.08.54 + VERSION: 01.08.55 PATH: ./CODE_OF_CONDUCT.md BRIEF: Community expectations and enforcement guidelines NOTE: Adapted with attribution from the Contributor Covenant v2.1 diff --git a/README.md b/README.md index 806cdb95..4b661887 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoSuiteCross - + Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6. diff --git a/SECURITY.md b/SECURITY.md index 38884eec..6c2ab2b9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla PATH: /SECURITY.md -VERSION: 01.08.54 +VERSION: 01.08.55 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/source/packages/com_mokosuitecross/mokosuitecross.xml b/source/packages/com_mokosuitecross/mokosuitecross.xml index d02e7cc4..2826a57e 100644 --- a/source/packages/com_mokosuitecross/mokosuitecross.xml +++ b/source/packages/com_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ com_mokosuitecross - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.08.55.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.08.55.sql new file mode 100644 index 00000000..e4e2af7d --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.08.55.sql @@ -0,0 +1 @@ +/* 01.08.55 — no schema changes */ diff --git a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml index f32b4f21..547aa057 100644 --- a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Content - MokoSuiteCross - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml index 1f4a3444..8c1f10cf 100644 --- a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml +++ b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ActivityPub (Fediverse) - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_blogger/blogger.xml b/source/packages/plg_mokosuitecross_blogger/blogger.xml index 53876331..5df6656a 100644 --- a/source/packages/plg_mokosuitecross_blogger/blogger.xml +++ b/source/packages/plg_mokosuitecross_blogger/blogger.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Blogger - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml index e38fdda3..ec7bcfba 100644 --- a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml +++ b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Bluesky - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_brevo/brevo.xml b/source/packages/plg_mokosuitecross_brevo/brevo.xml index 9d444025..812f6ef1 100644 --- a/source/packages/plg_mokosuitecross_brevo/brevo.xml +++ b/source/packages/plg_mokosuitecross_brevo/brevo.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Brevo (Sendinblue) - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml index e51d3878..0a3e86c2 100644 --- a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml +++ b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Constant Contact - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml index 0431b6cb..aad33ec4 100644 --- a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml +++ b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ConvertKit - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_devto/devto.xml b/source/packages/plg_mokosuitecross_devto/devto.xml index b56b879a..0ae60f21 100644 --- a/source/packages/plg_mokosuitecross_devto/devto.xml +++ b/source/packages/plg_mokosuitecross_devto/devto.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Dev.to - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_discord/discord.xml b/source/packages/plg_mokosuitecross_discord/discord.xml index 12380cdc..2c14e319 100644 --- a/source/packages/plg_mokosuitecross_discord/discord.xml +++ b/source/packages/plg_mokosuitecross_discord/discord.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Discord - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_facebook/facebook.xml b/source/packages/plg_mokosuitecross_facebook/facebook.xml index 4f55a0aa..8321f7c1 100644 --- a/source/packages/plg_mokosuitecross_facebook/facebook.xml +++ b/source/packages/plg_mokosuitecross_facebook/facebook.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Facebook / Meta - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_ghost/ghost.xml b/source/packages/plg_mokosuitecross_ghost/ghost.xml index 3abbce2a..ec66d548 100644 --- a/source/packages/plg_mokosuitecross_ghost/ghost.xml +++ b/source/packages/plg_mokosuitecross_ghost/ghost.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ghost - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml index 63e2cd8e..faab80db 100644 --- a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml +++ b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Business Profile - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_googlechat/googlechat.xml b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml index 2df480fb..08a0a0c9 100644 --- a/source/packages/plg_mokosuitecross_googlechat/googlechat.xml +++ b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Chat - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml index efb1faa5..f87941fc 100644 --- a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml +++ b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Hashnode - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_instagram/instagram.xml b/source/packages/plg_mokosuitecross_instagram/instagram.xml index 1e8e2945..6c3f7966 100644 --- a/source/packages/plg_mokosuitecross_instagram/instagram.xml +++ b/source/packages/plg_mokosuitecross_instagram/instagram.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Instagram - 01.08.54 + 01.08.55 2026-06-23 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml index 2da35ee0..1844683a 100644 --- a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml +++ b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml @@ -1,7 +1,7 @@ MokoSuiteCross - LinkedIn - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml index 0c2ffc7f..cb645a33 100644 --- a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml +++ b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mailchimp - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml index 9d5579bb..dc764678 100644 --- a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml +++ b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mastodon - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_matrix/matrix.xml b/source/packages/plg_mokosuitecross_matrix/matrix.xml index 24e610d6..82cc5941 100644 --- a/source/packages/plg_mokosuitecross_matrix/matrix.xml +++ b/source/packages/plg_mokosuitecross_matrix/matrix.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Matrix / Element - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_medium/medium.xml b/source/packages/plg_mokosuitecross_medium/medium.xml index 775c60a3..fe2ff03d 100644 --- a/source/packages/plg_mokosuitecross_medium/medium.xml +++ b/source/packages/plg_mokosuitecross_medium/medium.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Medium - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml index 37a620c7..1be20e54 100644 --- a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml +++ b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteCalendar Events - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml index fb589f2d..b0538105 100644 --- a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteGallery - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_nostr/nostr.xml b/source/packages/plg_mokosuitecross_nostr/nostr.xml index e6964c00..5e6278ba 100644 --- a/source/packages/plg_mokosuitecross_nostr/nostr.xml +++ b/source/packages/plg_mokosuitecross_nostr/nostr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Nostr - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml index 9cced1a7..28fc030c 100644 --- a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml +++ b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ntfy Push Notifications - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml index 3b1edf24..f9d02bd3 100644 --- a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml +++ b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Pinterest - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_reddit/reddit.xml b/source/packages/plg_mokosuitecross_reddit/reddit.xml index a4512217..30c71c05 100644 --- a/source/packages/plg_mokosuitecross_reddit/reddit.xml +++ b/source/packages/plg_mokosuitecross_reddit/reddit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Reddit - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml index bf2a8872..f421a8e8 100644 --- a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml +++ b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml @@ -1,7 +1,7 @@ MokoSuiteCross - RSS Feed - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml index 7aac7f65..e3cb9e6d 100644 --- a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml +++ b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml @@ -1,7 +1,7 @@ MokoSuiteCross - SendGrid - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_slack/slack.xml b/source/packages/plg_mokosuitecross_slack/slack.xml index 75d80942..cd347e40 100644 --- a/source/packages/plg_mokosuitecross_slack/slack.xml +++ b/source/packages/plg_mokosuitecross_slack/slack.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Slack - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_teams/teams.xml b/source/packages/plg_mokosuitecross_teams/teams.xml index 97df7873..b45b7827 100644 --- a/source/packages/plg_mokosuitecross_teams/teams.xml +++ b/source/packages/plg_mokosuitecross_teams/teams.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Microsoft Teams - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_telegram/telegram.xml b/source/packages/plg_mokosuitecross_telegram/telegram.xml index 388f0bdc..2424352c 100644 --- a/source/packages/plg_mokosuitecross_telegram/telegram.xml +++ b/source/packages/plg_mokosuitecross_telegram/telegram.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Telegram - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_threads/threads.xml b/source/packages/plg_mokosuitecross_threads/threads.xml index 1c8535fb..9c86d1c4 100644 --- a/source/packages/plg_mokosuitecross_threads/threads.xml +++ b/source/packages/plg_mokosuitecross_threads/threads.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Threads (Meta) - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml index 47493c9b..a4195954 100644 --- a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml +++ b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml @@ -1,7 +1,7 @@ MokoSuiteCross - TikTok - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml index dd3fbd4d..b3ec902c 100644 --- a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml +++ b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Tumblr - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_twitter/twitter.xml b/source/packages/plg_mokosuitecross_twitter/twitter.xml index 0a1c4a78..d73ee08b 100644 --- a/source/packages/plg_mokosuitecross_twitter/twitter.xml +++ b/source/packages/plg_mokosuitecross_twitter/twitter.xml @@ -1,7 +1,7 @@ MokoSuiteCross - X / Twitter - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_webhook/webhook.xml b/source/packages/plg_mokosuitecross_webhook/webhook.xml index 4a657af2..eb20b3ac 100644 --- a/source/packages/plg_mokosuitecross_webhook/webhook.xml +++ b/source/packages/plg_mokosuitecross_webhook/webhook.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Generic Webhook - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml index 69abccf1..587a8f4a 100644 --- a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml +++ b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WhatsApp Business - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml index ed57e204..a1ddd4f5 100644 --- a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml +++ b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WordPress - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_youtube/youtube.xml b/source/packages/plg_mokosuitecross_youtube/youtube.xml index e7c5dbe6..24cad342 100644 --- a/source/packages/plg_mokosuitecross_youtube/youtube.xml +++ b/source/packages/plg_mokosuitecross_youtube/youtube.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Youtube - 01.08.54 + 01.08.55 2026-06-23 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml index 9b2dbf9a..e4ce44da 100644 --- a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml index d3cd79b2..c0c0bff0 100644 --- a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml +++ b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross Events - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml index 4758d5f0..7a790f33 100644 --- a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml +++ b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross Gallery - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_task_mokosuitecross/mokosuitecross.xml b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml index 1fa0976e..6d1b00d0 100644 --- a/source/packages/plg_task_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Task - MokoSuiteCross Queue Processor - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml index 85e3f4af..6a4ec3f8 100644 --- a/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Web Services - MokoSuiteCross - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitecross.xml b/source/pkg_mokosuitecross.xml index 0c7e4a75..0fb7b2ed 100644 --- a/source/pkg_mokosuitecross.xml +++ b/source/pkg_mokosuitecross.xml @@ -2,7 +2,7 @@ MokoSuiteCross mokosuitecross - 01.08.54 + 01.08.55 2026-05-28 Moko Consulting hello@mokoconsulting.tech -- 2.52.0