PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.10.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.10.01
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
← All changes | app/Hooks/Handlers/PortalHandler.php +103 -19 2.7.0 → 2.10.01 View file →
@@ -22,8 +22,17 @@
22 22 use FluentCommunity\Modules\Course\Model\CourseLesson;
23 23
24 24 class PortalHandler
25 25 {
26 + /**
27 + * Route + model resolved while building the head meta, handed to the view so
28 + * `fluent_community/portal_content_pre_render` listeners can render a
29 + * server-side first paint without querying for the same record again.
30 + *
31 + * @var array{route: string, model: mixed}|array
32 + */
33 + protected $preRenderContext = [];
34 +
26 35 protected $currentPath = '';
27 36
28 37 protected $slug = null;
29 38
@@ -63,9 +72,11 @@
63 72 }
64 73 }, 1);
65 74
66 75 add_action('fluent_community/portal_html', function () {
67 - App::make('view')->render('portal.portal');
76 + App::make('view')->render('portal.portal', [
77 + 'preRenderContext' => $this->preRenderContext,
78 + ]);
68 79 });
69 80
70 81 add_action('fluent_community/portal_header', function ($context) {
71 82 $this->getPortalHeader(true, $context);
@@ -88,8 +99,12 @@
88 99 wp_enqueue_script('portal_general', Vite::getStaticSrcUrl('portal_general.js'), [], FLUENT_COMMUNITY_PLUGIN_VERSION, [
89 100 'in_footer' => true
90 101 ]);
91 102 wp_localize_script('portal_general', 'fcom_portal_general', $this->getGlobalScriptVars());
103 +
104 + if (Utility::isCustomizationEnabled('enable_sidebar_toggle')) {
105 + add_action('fluent_community/before_portal_dom', [$this, 'renderSidebarCollapseFouc'], 1);
106 + }
92 107 });
93 108
94 109 add_action('admin_bar_menu', function ($wp_admin_bar) {
95 110 if (!\FluentCommunity\App\Services\Helper::canAccessPortal()) {
@@ -114,10 +129,10 @@
114 129 if (Helper::isSiteAdmin()) {
115 130 ?>
116 131 <div style="display: flex;justify-content: space-between;" class="fcom_admin_menu">
117 132 <?php if (!defined('FLUENT_COMMUNITY_PRO')): ?>
118 - <a title="Upgrade to Pro" target="_blank" rel="noopener" class="fcom_inline_icon_link_item"
119 - href="<?php echo esc_url(Utility::getProductUrl(true)) ?>">
133 + <a target="_blank" rel="noopener" class="fcom_inline_icon_link_item"
134 + href="<?php echo esc_url(Utility::getProUpgradeUrl('sidebar_cta')) ?>">
120 135 <span class="el-icon">
121 136 <svg width="126" height="125" viewBox="0 0 126 125" fill="none"><rect x="0.22139"
122 137 width="125"
123 138 height="125"
@@ -148,9 +163,9 @@
148 163 </span>
149 164 <span><?php esc_html_e('Upgrade', 'fluent-community'); ?></span>
150 165 </a>
151 166 <?php else: ?>
152 - <a title="Go to /wp-admin" class="fcom_inline_icon_link_item fcom_wp_admin_link"
167 + <a aria-label="<?php echo esc_attr__('Go to WordPress admin', 'fluent-community'); ?>" class="fcom_inline_icon_link_item fcom_wp_admin_link"
153 168 href="<?php echo esc_url(admin_url()); ?>">
154 169 <span class="el-icon">
155 170 <svg viewBox="0 0 122.52 122.523"><g fill="currentColor">
156 171 <path
@@ -166,9 +181,11 @@
166 181 </svg>
167 182 </span>
168 183 </a>
169 184 <?php endif; ?>
170 - <a title="Portal Settings" class="fcom_inline_icon_link_item"
185 + <?php // The label text is hidden while the sidebar is collapsed, so the link carries its own name. ?>
186 + <a class="fcom_inline_icon_link_item"
187 + aria-label="<?php echo esc_attr__('Settings', 'fluent-community'); ?>"
171 188 href="<?php echo esc_url(Helper::baseUrl('admin/settings')); ?>">
172 189 <span class="el-icon">
173 190 <svg viewBox="0 0 1024 1024" data-v-d2e47025="">
174 191 <path fill="currentColor"
@@ -180,9 +197,9 @@
180 197 </div>
181 198 <?php
182 199 } else if (Utility::isCustomizationEnabled('show_powered_by')) {
183 200 ?>
184 - <a target="_blank" rel="noopener" style="font-size: 80%; cursor: pointer;"
201 + <a target="_blank" rel="noopener" style="font-size: 13px; cursor: pointer;"
185 202 class="fcom_inline_icon_link_item" href="<?php echo esc_url(Utility::getProductUrl(true)) ?>">
186 203 <?php echo esc_html__('Powered by FluentCommunity', 'fluent-community'); ?>
187 204 </a>
188 205 <?php
@@ -194,9 +211,9 @@
194 211 if (Helper::isSiteAdmin()) {
195 212 add_action('fluent_community/before_header_menu_items', function () {
196 213 ?>
197 214 <li class="top_menu_item fcom_icon_link">
198 - <a href="<?php echo esc_url(Helper::baseUrl('admin/settings')); ?>">
215 + <a aria-label="<?php echo esc_attr__('Settings', 'fluent-community'); ?>" href="<?php echo esc_url(Helper::baseUrl('admin/settings')); ?>">
199 216 <span class="el-icon">
200 217 <svg viewBox="0 0 1024 1024" data-v-d2e47025="">
201 218 <path fill="currentColor"
202 219 d="M600.704 64a32 32 0 0 1 30.464 22.208l35.2 109.376c14.784 7.232 28.928 15.36 42.432 24.512l112.384-24.192a32 32 0 0 1 34.432 15.36L944.32 364.8a32 32 0 0 1-4.032 37.504l-77.12 85.12a357.12 357.12 0 0 1 0 49.024l77.12 85.248a32 32 0 0 1 4.032 37.504l-88.704 153.6a32 32 0 0 1-34.432 15.296L708.8 803.904c-13.44 9.088-27.648 17.28-42.368 24.512l-35.264 109.376A32 32 0 0 1 600.704 960H423.296a32 32 0 0 1-30.464-22.208L357.696 828.48a351.616 351.616 0 0 1-42.56-24.64l-112.32 24.256a32 32 0 0 1-34.432-15.36L79.68 659.2a32 32 0 0 1 4.032-37.504l77.12-85.248a357.12 357.12 0 0 1 0-48.896l-77.12-85.248A32 32 0 0 1 79.68 364.8l88.704-153.6a32 32 0 0 1 34.432-15.296l112.32 24.256c13.568-9.152 27.776-17.408 42.56-24.64l35.2-109.312A32 32 0 0 1 423.232 64H600.64zm-23.424 64H446.72l-36.352 113.088-24.512 11.968a294.113 294.113 0 0 0-34.816 20.096l-22.656 15.36-116.224-25.088-65.28 113.152 79.68 88.192-1.92 27.136a293.12 293.12 0 0 0 0 40.192l1.92 27.136-79.808 88.192 65.344 113.152 116.224-25.024 22.656 15.296a294.113 294.113 0 0 0 34.816 20.096l24.512 11.968L446.72 896h130.688l36.48-113.152 24.448-11.904a288.282 288.282 0 0 0 34.752-20.096l22.592-15.296 116.288 25.024 65.28-113.152-79.744-88.192 1.92-27.136a293.12 293.12 0 0 0 0-40.256l-1.92-27.136 79.808-88.128-65.344-113.152-116.288 24.96-22.592-15.232a287.616 287.616 0 0 0-34.752-20.096l-24.448-11.904L577.344 128zM512 320a192 192 0 1 1 0 384 192 192 0 0 1 0-384m0 64a128 128 0 1 0 0 256 128 128 0 0 0 0-256"></path>
@@ -245,9 +262,9 @@
245 262 <ul class="fcom_user_context_menu_items">
246 263 <?php do_action('fluent_community/before_header_right_menu_items', $auth); ?>
247 264 <?php if ($has_color_scheme): ?>
248 265 <li class="top_menu_item fcom_color_mode">
249 - <button class="fcom_color_mode_core fcom_mode_switch fcom_menu_button">
266 + <button type="button" aria-label="<?php echo esc_attr__('Toggle dark mode', 'fluent-community'); ?>" class="fcom_color_mode_core fcom_mode_switch fcom_menu_button">
250 267 <span class="fcom_color_mode_action el-icon">
251 268 <svg class="show_on_light" width="20" height="20" viewBox="0 0 20 20" fill="none"
252 269 xmlns="http://www.w3.org/2000/svg">
253 270 <path
@@ -284,8 +301,9 @@
284 301 ->where('user_id', $auth->user_id)
285 302 ->count();
286 303 ?>
287 304 <a href="<?php echo esc_url(Helper::baseUrl('/notifications')); ?>" type="button"
305 + aria-label="<?php echo esc_attr__('Notifications', 'fluent-community'); ?>"
288 306 class="fcom_menu_button fcom_theme_button">
289 307 <i class="el-icon">
290 308 <svg width="20" height="20" viewBox="0 0 20 20" fill="none"
291 309 xmlns="http://www.w3.org/2000/svg">
@@ -312,10 +330,10 @@
312 330 <li class="top_menu_item fcom_menu_item_user">
313 331 <div class="fcom_user_menu_item">
314 332 <div class="fcom_profile_extend">
315 333 <div class="fcom_profile_menu">
316 - <div class="user_avatar">
317 - <img alt="User Photo" src="<?php echo esc_url($auth->avatar); ?>"/>
334 + <div class="user_avatar" role="button" tabindex="0" aria-expanded="false" aria-controls="fcom_profile_sub_menu" aria-label="<?php echo esc_attr__('Account menu', 'fluent-community'); ?>">
335 + <img alt="<?php echo esc_attr($auth->display_name); ?>" src="<?php echo esc_url($auth->avatar); ?>"/>
318 336 <span class="avatar_icon">
319 337 <svg xmlns="http://www.w3.org/2000/svg" height="8" width="8"
320 338 viewBox="0 0 1024 1024">
321 339 <path fill="currentColor"
@@ -325,9 +343,9 @@
325 343 </div>
326 344 <span class="user_name">
327 345 <?php echo esc_html($auth->display_name); ?>
328 346 </span>
329 - <ul class="fcom_profile_sub_menu">
347 + <ul id="fcom_profile_sub_menu" class="fcom_profile_sub_menu">
330 348 <li class="fcom_profile_block">
331 349 <a href="<?php echo esc_url(Helper::baseUrl('/u/' . $auth->username)); ?>">
332 350 <div class="fcom_profile_avatar">
333 351 <img src="<?php echo esc_url($auth->avatar); ?>"
@@ -513,9 +531,8 @@
513 531 'has_topics' => !!Utility::getTopics(),
514 532 'show_post_modal' => Utility::isCustomizationEnabled('show_post_modal'),
515 533 'has_analytics' => Utility::hasAnalyticsEnabled(),
516 534 'can_deactivate_account' => Utility::getPrivacySetting('can_deactive_account') === 'yes',
517 - 'enable_sidebar_toggle' => Utility::isCustomizationEnabled('enable_sidebar_toggle'),
518 535 ],
519 536 'route_classes' => array_filter([
520 537 'fcom_sticky_header' => Utility::isCustomizationEnabled('fixed_page_header'),
521 538 'fcom_sticky_sidebar' => Utility::isCustomizationEnabled('fixed_sidebar'),
@@ -542,9 +559,9 @@
542 559 'comment_order_by_options' => Helper::getCommentOrderOptions('comment'),
543 560 'user_post_order_by_options' => Helper::getPostOrderOptions('user'),
544 561 'routing_system' => Helper::getPortalRouteType(),
545 562 'portal_url' => Helper::baseUrl('/'),
546 - 'upgrade_url' => 'https://fluentcommunity.co/discount-deal/?utm_source=wp&utm_medium=upgrade&utm_campaign=upgrade',
563 + 'upgrade_url' => Utility::getProUpgradeUrl('upgrade_page'),
547 564 'dateTime18n' => apply_filters('fluent_community/date_time_i18n', [
548 565 /* translators: weekday. Please keep the serial and format */
549 566 'weekdays' => __('Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday', 'fluent-community'),
550 567 /* translators: Months Please keep the serial and format*/
@@ -749,8 +766,22 @@
749 766
750 767 return $portalVars;
751 768 }
752 769
770 + public function renderSidebarCollapseFouc()
771 + {
772 + ?>
773 + <script>
774 + (function () {
775 + var m = document.cookie.match(/(?:^|; )fcom_sidebar_collapsed=([^;]+)/);
776 + if (m && m[1] === 'true') {
777 + document.documentElement.classList.add('fcom_sidebar_collapsed_desktop');
778 + }
779 + })();
780 + </script>
781 + <?php
782 + }
783 +
753 784 public function getGlobalScriptVars($scope = 'wp')
754 785 {
755 786 return apply_filters('fluent_community/general_portal_vars', [
756 787 'scope' => $scope,
@@ -757,10 +788,13 @@
757 788 'theme' => get_option('template'),
758 789 'default_color' => 'light',
759 790 'color_switch_cookie_name' => '',
760 791 'has_color_scheme' => Helper::hasColorScheme(),
792 + 'default_theme_mode' => Helper::getDefaultThemeMode(),
761 793 'collapse_sidebar_groups' => Utility::isCustomizationEnabled('collapse_sidebar_groups'),
762 794 'hide_header_on_scroll' => Utility::isCustomizationEnabled('hide_header_on_scroll'),
795 + 'enable_sidebar_toggle' => Utility::isCustomizationEnabled('enable_sidebar_toggle'),
796 + 'sidebar_toggle_label' => __('Toggle sidebar', 'fluent-community'),
763 797 ]);
764 798 }
765 799
766 800 protected function getRestInfo()
@@ -861,8 +895,16 @@
861 895
862 896 $data = $this->getAppData();
863 897 $data = $this->maybePushDynamicMetaData($data);
864 898
899 + /**
900 + * Lets a listener adjust the resolved head meta (canonical_url, title,
901 + * description, json_ld) with knowledge the handler does not have - for
902 + * example a pre-renderer that paints ?page=N of a space listing and needs
903 + * the canonical to point at that page rather than at page one.
904 + */
905 + $data = apply_filters('fluent_community/portal_page_meta', $data, $this->currentPath, $this->preRenderContext);
906 +
865 907 $data['isHeadless'] = Helper::isHeadless();
866 908
867 909 if (!$data['isHeadless']) {
868 910 $this->loadClassicPortalAssets($data);
@@ -903,10 +945,9 @@
903 945 }, 10, 2);
904 946 foreach ($jsFiles as $fileName => $jsFile) {
905 947 wp_enqueue_script($fileName, $jsFile['url'], $jsFile['deps'], FLUENT_COMMUNITY_PLUGIN_VERSION, [
906 948 'in_footer' => true,
907 - 'strategy' => 'defer',
908 - 'type' => 'module'
949 + 'strategy' => 'defer'
909 950 ]);
910 951 }
911 952
912 953 foreach ($data['js_vars'] as $varKey => $values) {
@@ -1029,9 +1070,9 @@
1029 1070 $uriParts = explode('/', $this->currentPath);
1030 1071 if (count($uriParts) >= 2) {
1031 1072 $userName = $uriParts[1];
1032 1073 $xProfile = XProfile::where('username', $userName)->first();
1033 - if ($xProfile) {
1074 + if ($xProfile && $xProfile->status === 'active' && Utility::canViewUserProfile($xProfile->user_id)) {
1034 1075 $data['title'] = 'User ' . esc_html($xProfile->display_name) . ' - ' . $data['title'];
1035 1076 $data['og_title'] = esc_html($xProfile->display_name) . ' - ' . $data['og_title'];
1036 1077 if ($xProfile->short_description) {
1037 1078 $data['description'] = Helper::getHumanExcerpt($xProfile->short_description, 100);
@@ -1050,8 +1091,17 @@
1050 1091 $paceSlug = $uriParts[1];
1051 1092 $space = BaseSpace::query()->onlyMain()->where('slug', $paceSlug)->first();
1052 1093 if ($space && $space->privacy != 'secret') {
1053 1094
1095 + // matches the URL the sitemap submits, so tab variants consolidate onto it
1096 + $data['canonical_url'] = $space->getPermalink();
1097 +
1098 + $this->preRenderContext = [
1099 + 'route' => $dynamicRoute,
1100 + 'model' => $space,
1101 + 'path' => $this->currentPath,
1102 + ];
1103 +
1054 1104 if ($dynamicRoute == 'course_view') {
1055 1105 /* translators: %s is replaced by the title of the space */
1056 1106 $data['title'] = sprintf(__('Enroll %s', 'fluent-community'), esc_html($space->title) . ' - ' . $data['title']);
1057 1107 /* translators: %s is replaced by the title of the space */
@@ -1083,9 +1133,14 @@
1083 1133 if ($dynamicRoute == 'feed_view') {
1084 1134 $uriParts = explode('/', $this->currentPath);
1085 1135 if (count($uriParts) >= 2) {
1086 1136 $postSlug = end($uriParts);
1087 - $feed = Feed::query()->withoutGlobalScopes()->where('slug', $postSlug)
1137 + // Scopes are dropped to reach image and article posts, so the type
1138 + // has to be narrowed by hand - see Feed::$feedViewTypes.
1139 + $feed = Feed::query()->withoutGlobalScopes()
1140 + ->whereIn('type', Feed::$feedViewTypes)
1141 + ->where('status', 'published')
1142 + ->where('slug', $postSlug)
1088 1143 ->with([
1089 1144 'xprofile' => function ($q) {
1090 1145 $q->select(ProfileHelper::getXProfilePublicFields());
1091 1146 },
@@ -1128,8 +1183,14 @@
1128 1183 $data['featured_image'] = $space->cover_photo;
1129 1184 }
1130 1185 }
1131 1186
1187 + $this->preRenderContext = [
1188 + 'route' => 'feed_view',
1189 + 'model' => $feed,
1190 + 'path' => $this->currentPath,
1191 + ];
1192 +
1132 1193 $data['json_ld'] = apply_filters('fluent_community/feed_view_json_ld', [], $feed, $data);
1133 1194 }
1134 1195 return $data;
1135 1196 }
@@ -1137,14 +1198,32 @@
1137 1198 if ($dynamicRoute == 'lesson_view') {
1138 1199 $uriParts = explode('/', $this->currentPath);
1139 1200
1140 1201 $lessonSlug = Arr::get($uriParts, 3);
1141 - if (!$lessonSlug) {
1202 + $courseSlug = Arr::get($uriParts, 1);
1203 + if (!$lessonSlug || !$courseSlug) {
1142 1204 return $data;
1143 1205 }
1144 1206
1145 - $lesson = CourseLesson::query()->where('slug', $lessonSlug)->first();
1207 + // Resolve the course first: lesson slugs are only unique within one, so a
1208 + // slug-first lookup would answer with whichever course was created first
1209 + // and then fail the course comparison, costing the real lesson its metadata.
1210 + $course = BaseSpace::query()->onlyMain()
1211 + ->where('slug', $courseSlug)
1212 + ->where('status', 'published')
1213 + ->where('privacy', 'public')
1214 + ->first();
1146 1215
1216 + if (!$course) {
1217 + return $data;
1218 + }
1219 +
1220 + $lesson = CourseLesson::query()
1221 + ->where('space_id', $course->id)
1222 + ->where('slug', $lessonSlug)
1223 + ->where('status', 'published')
1224 + ->first();
1225 +
1147 1226 if (!$lesson) {
1148 1227 return $data;
1149 1228 }
1150 1229
@@ -1150,9 +1229,12 @@
1150 1229
1151 1230 $data['title'] = esc_html($lesson->title) . ' - ' . $data['title'];
1152 1231 $data['og_title'] = esc_html($lesson->title);
1153 1232 $data['description'] = esc_html(Helper::getHumanExcerpt($lesson->message, 120));
1233 + $data['canonical_url'] = $lesson->getPermalink();
1154 1234
1235 + $data['json_ld'] = apply_filters('fluent_community/lesson_view_json_ld', [], $lesson, $course, $data);
1236 +
1155 1237 return $data;
1156 1238 }
1157 1239
1158 1240 return $data;
@@ -1257,8 +1339,10 @@
1257 1339
1258 1340 public function getPortalSidebar($echo = false, $context = 'headless')
1259 1341 {
1260 1342 $data = Utility::getPortalSidebarData('sidebar');
1343 +
1344 + $data['context'] = $context;
1261 1345
1262 1346 if ($echo) {
1263 1347 App::make('view')->render('portal.main_sidebar', $data);
1264 1348 return;