نحن نتراجع عن نظام عرض “الودجات” القديم، ونستبدله بمكونات Glimmer الحديثة.
لقد قمنا مؤخرًا بتحديث تدفق المنشورات باستخدام مكونات Glimmer. ستأخذك هذه الدليل خطوة بخطوة في عملية نقل الإضافات والمظاهر (themes) الخاصة بك من النظام القديم القائم على الودجات إلى تطبيق Glimmer الجديد.
لا تقلق — عملية النقل هذه أكثر بساطة مما قد تبدو عليه للوهلة الأولى. لقد صممنا النظام الجديد ليكون أكثر بديهية وقوة من نظام الودجات القديم، وستساعدك هذه الدليل في إتمام العملية.
الجدول الزمني
هذه تقديرات قابلة للتغيير
الربع الثاني 2025:
اكتمل التنفيذ الأساسي
البدء في ترقية مكونات الإضافات والمظاهر الرسمية
تم التفعيل على Meta
نشر نصائح الترقية (هذه الدليل)
الربع الثالث 2025:
إكمال ترقية مكونات الإضافات والمظاهر الرسمية
تعيين glimmer_post_stream_modeالافتراضي إلىautoوتمكين رسائل الإهمال في وحدة التحكم (console)
تفعيل رسائل الإهمال مع شريط تحذير للمسؤول (مخطط له في يوليو 2025)- يجب تحديث الإضافات والمظاهر من جهات خارجية
الربع الرابع 2025:
تفعيل تدفق المنشورات الجديد افتراضيًا
إزالة إعداد ميزة العلم (feature flag) والكود القديم
ماذا يعني هذا بالنسبة لي؟
إذا كانت أي من الإضافات أو المظاهر الخاصة بك تستخدم أي واجهات برمجة تطبيقات (APIs) لـ ‘widget’ لتخصيص تدفق المنشورات، فستحتاج إلى تحديثها لتعمل مع الإصدار الجديد.
كيف يمكنني تجربة تدفق المنشورات الجديد؟
لتجربة تدفق المنشورات الجديد، ما عليك سوى تغيير إعداد glimmer_post_stream_mode إلى auto في إعدادات موقعك. سيؤدي هذا إلى تمكين تدفق المنشورات الجديد إذا لم يكن لديك أي إضافات أو مظاهر غير متوافقة.
عندما يتم تعيين glimmer_post_stream_mode على auto، يقوم Discourse تلقائيًا باكتشاف الإضافات والمظاهر غير المتوافقة، وإذا وجد أيًا منها، فسترى رسائل تحذير مفيدة في وحدة تحكم المتصفح تحدد بالضبط أي الإضافات أو المظاهر تحتاج إلى تحديث، جنبًا إلى جنب مع تتبعات المكدس (stack traces) لمساعدتك في العثور على الكود ذي الصلة.
ستساعدك هذه الرسائل في تحديد الأجزاء بالضبط من إضافاتك أو مظاهرك التي تحتاج إلى تحديث لتكون متوافقة مع تدفق منشورات Glimmer الجديد.
إذا واجهت أي مشاكل عند استخدام تدفق المنشورات الجديد، فلا تقلق، يمكنك حاليًا تعيين الإعداد إلى disabled للعودة إلى النظام القديم. إذا كان لديك امتدادات غير متوافقة مثبتة، ولكنك لا تزال ترغب في تجربتها، فبصفتك مسؤولًا، يمكنك تعيين الخيار إلى enabled لفرض تدفق المنشورات الجديد. فقط استخدم هذا بحذر — قد لا يعمل موقعك بشكل صحيح اعتمادًا على التخصيصات التي تمتلكها.
لدي إضافات أو مظاهر مخصصة مثبتة. هل أحتاج إلى تحديثها؟
ستحتاج إلى تحديث إضافاتك أو مظاهرك إذا قامت بأي من التخصيصات التالية:
-
استخدام
decorateWidget،changeWidgetSetting،reopenWidgetأوattachWidgetActionعلى هذه الودجات:actions-summaryavatar-flairembedded-postexpand-hiddenexpand-post-buttonfilter-jump-to-postfilter-show-allpost-articlepost-avatar-user-infopost-avatarpost-bodypost-contentspost-datepost-edits-indicatorpost-email-indicatorpost-gappost-group-requestpost-linkspost-locked-indicatorpost-meta-datapost-noticepost-placeholderpost-streampostposter-nameposter-name-titleposts-filtered-noticereply-to-tabselect-posttopic-post-visited-line
-
استخدام إحدى طرق واجهة برمجة التطبيقات (API methods) التالية:
addPostTransformCallbackincludePostAttributes
إذا كان لديك امتدادات تستخدم أيًا من التخصيصات المذكورة أعلاه، فسترى تحذيرًا في وحدة التحكم يحدد أي إضافة أو مكون يحتاج إلى ترقية عند زيارة صفحة موضوع.
معرف الإهمال هو:
discourse.post-stream-widget-overrides
إذا كنت تستخدم أكثر من مظهر واحد في مثيلتك (instance)، فتأكد من التحقق منها جميعًا، حيث ستظهر التحذيرات فقط للإضافات النشطة والمظاهر والمكونات المظهرية المستخدمة حاليًا.
ما هي البدائل؟
يمنحك تدفق منشورات Glimmer الجديد عدة طرق لتخصيص كيفية ظهور المنشورات:
- مخارج الإضافات (Plugin Outlets): لإضافة المحتوى في نقاط محددة في تدفق المنشورات.
- المحولات (Transformers): لتخصيص العناصر، أو تعديل هياكل البيانات، أو تغيير سلوك المكونات.
استبدال includePostAttributes بـ addTrackedPostProperties
إذا كانت إضافتك تستخدم includePostAttributes لإضافة خصائص إلى نموذج المنشور، فستحتاج إلى تحديثها لاستخدام addTrackedPostProperties بدلاً من ذلك.
قبل:
api.includePostAttributes('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');
بعد:
api.addTrackedPostProperties('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');
تقوم دالة addTrackedPostProperties بوضع علامة على الخصائص كمتتبعات لتحديثات المنشور. هذا مهم إذا كانت إضافتك تضيف خصائص إلى منشور وتستخدمها أثناء العرض. يضمن ذلك تحديث واجهة المستخدم تلقائيًا عند تغيير هذه الخصائص.
أنماط النقل الشائعة
استخدام مخارج الإضافات (Plugin Outlets)
تُعد مخارج الإضافات الأداة الرئيسية لإضافة المحتوى في نقاط محددة في تدفق المنشورات. فكر فيها على أنها مواقع مخصصة يمكنك من خلالها حقن محتوى مخصص.
هي المفتاح للانتقال بعيدًا عن زخارف الودجات.
يغلف تدفق منشورات Glimmer المحتوى الذي يتم تخصيصه غالبًا داخل مخارج. استخدم دوال واجهة برمجة التطبيقات renderBeforeWrapperOutlet و renderAfterWrapperOutlet لإدراج المحتوى قبلها أو بعدها.
1. استبدال زخارف الودجات بمخارج الإضافات
التخصيص الأكثر شيوعًا هو إضافة محتوى إلى المنشورات. في نظام الودجات، كنت ستستخدم decorateWidget. مع Glimmer، ستستخدم مخارج الإضافات بدلاً من ذلك.
قبل:
// جزء من مهيئ في إضافة
import { withPluginApi } from "discourse/lib/plugin-api";
// ... استيرادات أخرى
function customizeWidgetPost(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
const post = helper.getModel();
if (post.post_number === 1 && post.topic.accepted_answer) {
return helper.attach("solved-accepted-answer", { post });
}
});
}
export default {
name: "extend-for-solved-button",
initialize() {
withPluginApi((api) => {
// ... تخصيصات أخرى
customizeWidgetPost(api);
});
}
};
بعد:
// جزء من مهيئ .gjs في إضافة
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
// ... استيرادات أخرى
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
class extends Component {
static shouldRender(args) {
return args.post?.post_number === 1 && args.post?.topic?.accepted_answer;
}
<template>
<SolvedAcceptedAnswer
@post={{@post}}
/>
</template>
}
);
}
export default {
name: "extend-for-solved-button",
initialize() {
withPluginApi((api) => {
// ... تخصيصات أخرى
customizePost(api);
});
}
};
2. إضافة محتوى بعد اسم الناشر
إذا كانت إضافتك تضيف محتوى بعد اسم الناشر، فستستخدم واجهة برمجة التطبيقات renderAfterWrapperOutlet مع مخرج post-meta-data-poster-name.
قبل:
// جزء من مهيئ في إضافة
import { withPluginApi } from "discourse/lib/plugin-api";
// ... استيرادات أخرى
function customizeWidgetPost(api) {
api.decorateWidget(`poster-name:after`, (dec) => {
if (!isGPTBot(dec.attrs.user)) {
return;
}
return dec.widget.attach("persona-flair", {
personaName: dec.model?.topic?.ai_persona_name,
});
});
}
export default {
name: "ai-bot-replies",
initialize() {
withPluginApi((api) => {
// ... تخصيصات أخرى
customizeWidgetPost(api);
});
}
};
بعد:
// جزء من مهيئ .gjs في إضافة
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... استيرادات أخرى
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-meta-data-poster-name",
class extends Component {
static shouldRender(args) {
return isGPTBot(args.post?.user);
}
<template>
<span class="persona-flair">{{@post.topic.ai_persona_name}}</span>
</template>
}
);
}
export default {
name: "ai-bot-replies",
initialize() {
withPluginApi((api) => {
// ... تخصيصات أخرى
customizePost(api);
});
}
};
3. إضافة محتوى قبل محتوى المنشور
// جزء من مهيئ .gjs في مظهر
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... استيرادات أخرى
function customizePost(api) {
api.renderBeforeWrapperOutlet(
"post-article",
class extends Component {
static shouldRender(args) {
return args.post?.topic?.pinned;
}
<template>
<div class="pinned-post-notice">
هذا موضوع مثبت
</div>
</template>
}
);
}
export default {
name: "pinned-topic-notice",
initialize() {
withPluginApi((api) => {
// ... تخصيصات أخرى
customizePost(api);
});
}
};
4. إضافة محتوى بعد محتوى المنشور
// جزء من مهيئ .gjs في مظهر
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... استيرادات أخرى
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-article",
class extends Component {
static shouldRender(args) {
return args.post?.wiki;
}
// في مكون حقيقي، ستستخدم قالبًا مثل هذا:
<template>
<div class="wiki-post-notice">
هذا المنشور هو ويكي
</div>
</template>
}
);
}
export default {
name: "wiki-post-notice",
initialize() {
withPluginApi((api) => {
customizePost(api);
// ... تخصيصات أخرى
});
}
};
استخدام المحولات (Transformers)
تُعد المحولات طريقة قوية لتخصيص مكونات Discourse. تتيح لك تعديل البيانات أو سلوك المكون دون الحاجة إلى تجاوز المكونات بأكملها.
إليك بعض محولات القيم الأكثر صلة بتخصيص تدفق المنشورات:
| اسم المحول | الوصف | السياق |
|---|---|---|
post-class |
تخصيص فئات CSS المطبقة على عنصر المنشور الرئيسي. | { post } |
post-meta-data-infos |
تخصيص قائمة مكونات元 البيانات المعروضة للمنشور. يتيح هذا إضافة أو إزالة أو إعادة ترتيب عناصر مثل تاريخ المنشور، مؤشر التعديل، إلخ. | { post, metaDataInfoKeys } |
post-meta-data-poster-name-suppress-similar-name |
تحديد ما إذا كان يجب كتم عرض الاسم الكامل للمستخدم عندما يكون مشابهًا لاسم المستخدم الخاص به. أعد true للكتم. |
{ post, name } |
post-notice-component |
تخصيص أو استبدال المكون المستخدم لعرض إشعار المنشور. | { post, type } |
post-show-topic-map |
التحكم في ظهور مكون خريطة الموضوع في المنشور الأول. | { post, isPM, isRegular, showWithoutReplies } |
post-small-action-class |
إضافة فئات CSS مخصصة لمنشور إجراء صغير. | { post, actionCode } |
post-small-action-custom-component |
استبدال منشور إجراء صغير قياسي بمكون Glimmer مخصص. | { post, actionCode } |
post-small-action-icon |
تخصيص الأيقونة المستخدمة لمنشور إجراء صغير. | { post, actionCode } |
poster-name-class |
إضافة فئات CSS مخصصة لحاوية اسم الناشر. | { user } |
1. إضافة فئة مخصصة إلى المنشورات
// جزء من مهيئ في إضافة
import { withPluginApi } from "discourse/lib/plugin-api";
function customizePostClasses(api) {
api.registerValueTransformer(
"post-class",
({ value, context }) => {
const { post } = context;
// إضافة فئة مخصصة للمنشورات من مستخدم معين
if (post.user_id === 1) {
return [...value, "special-user-post"];
}
return value;
}
);
}
export default {
name: "custom-post-classes",
initialize() {
withPluginApi((api) => {
// ... تخصيصات أخرى
customizePostClasses(api);
});
}
};
2. إضافة元 بيانات مخصصة للمنشور
يسمح محول post-meta-data-infos بإضافة مكونات مخصصة إلى قسم元 بيانات المنشور.
// جزء من مهيئ في إضافة
import { withPluginApi } from "discourse/lib/plugin-api";
function customizePostMetadata(api) {
// تعريف مكون ليتم استخدامه في قسم元 البيانات
// يجب إنشاء المكون خارج دالة استدعاء المحول،
// وإلا فقد يسبب مشاكل في الذاكرة.
const CustomMetadataComponent = <template>...</template>;
api.registerValueTransformer(
"post-meta-data-infos",
({ value: metadata, context: { post, metaDataInfoKeys } }) => {
// إضافة المكون فقط لمنشورات معينة
if (post.some_custom_property) {
metadata.add(
"custom-metadata-key",
CustomMetadataComponent,
{
// وضعه قبل التاريخ
before: metaDataInfoKeys.DATE,
// وبعد علامة التبويب الرد
after: metaDataInfoKeys.REPLY_TO_TAB,
}
);
}
}
);
}
export default {
name: "custom-post-metadata",
initialize() {
withPluginApi((api) => {
// ... تخصيصات أخرى
customizePostMetadata(api);
});
}
};
إليك مثال من العالم الحقيقي من إضافة discourse-activity-pub:
// جزء من مهيئ في إضافة discourse-activity-pub
import { withPluginApi } from "discourse/lib/plugin-api";
import ActivityPubPostStatus from "../components/activity-pub-post-status";
import {
activityPubPostStatus,
showStatusToUser,
} from "../lib/activity-pub-utilities";
function customizePost(api, container) {
const currentUser = api.getCurrentUser();
const PostMetadataActivityPubStatus = <template>
<div class="post-info activity-pub">
<ActivityPubPostStatus @post={{@post}} />
</div>
</template>;
api.registerValueTransformer(
"post-meta-data-infos",
({ value: metadata, context: { post, metaDataInfoKeys } }) => {
const site = container.lookup("service:site");
const siteSettings = container.lookup("service:site-settings");
if (
site.activity_pub_enabled &&
post.activity_pub_enabled &&
post.post_number !== 1 &&
showStatusToUser(currentUser, siteSettings)
) {
const status = activityPubPostStatus(post);
if (status) {
metadata.add(
"activity-pub-indicator",
PostMetadataActivityPubStatus,
{
before: metaDataInfoKeys.DATE,
after: metaDataInfoKeys.REPLY_TO_TAB,
}
);
}
}
}
);
}
export default {
name: "activity-pub",
initialize(container) {
withPluginApi((api) => {
customizePost(api, container);
// ... تخصيصات أخرى
});
}
};
5. إدراج محتوى قبل أو بعد المحتوى المطبوخ (cooked) للمنشور
إذا كانت إضافتك تضيف محتوى بعد النص في المنشور، فستستخدم واجهة برمجة التطبيقات renderAfterWrapperOutlet مع مخرج post-content-cooked-html.
قبل:
// جزء من مهيئ في إضافة
import { withPluginApi } from "discourse/lib/plugin-api";
function customizeCooked(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
const post = helper.getModel();
if (post.wiki) {
const banner = document.createElement("div");
banner.classList.add("wiki-footer");
banner.textContent = "هذا المنشور هو ويكي";
element.prepend(banner);
}
});
}
export default {
name: "wiki-footer",
initialize() {
withPluginApi((api) => {
// ... تخصيصات أخرى
customizeCooked(api);
});
}
};
بعد:
// جزء من مهيئ في إضافة (.gjs)
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// تعريف مكون ليتم استخدامه في مخرج الإضافة
class WikiBanner extends Component {
static shouldRender(args) {
return args.post.wiki;
}
<template>
<div class="wiki-footer">هذا المنشور هو ويكي</div>
</template>
}
function customizePost(api) {
// استخدم renderBeforeWrapperOutlet لإضافة محتوى قبل محتوى المنشور
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
WikiBanner
);
}
export default {
name: "wiki-footer",
initialize() {
withPluginApi((api) => {
customizePost(api);
// ... تخصيصات أخرى
});
}
};
دعم النظامين القديم والجديد أثناء فترة الانتقال
بصفتك مؤلف إضافة أو مظهر، قد ترغب في دعم النظامين أثناء فترة الانتقال لضمان عمل امتداداتك مع تدفقي المنشورات القديم والجديد.
إليك نمطًا تستخدمه العديد من الإضافات الرسمية:
// solved-button.js
import Component from "@glimmer/component";
import { withSilencedDeprecations } from "discourse/lib/deprecated";
import { withPluginApi } from "discourse/lib/plugin-api";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
function customizePost(api) {
// تخصيصات تدفق منشورات glimmer
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
class extends Component {
static shouldRender(args) {
return args.post?.post_number === 1 && args.post?.topic?.accepted_answer;
}
<template>
<SolvedAcceptedAnswer
@post={{@post}}
@decoratorState={{@decoratorState}}
/>
</template>
}
);
// ...
// تغليف كود الودجات القديم مع إسكات تحذيرات الإهمال
withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
customizeWidgetPost(api)
);
}
// كود الودجات القديم
function customizeWidgetPost(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
let post = helper.getModel();
if (helper.attrs.post_number === 1 && post?.topic?.accepted_answer) {
// استخدم RenderGlimmer لعرض مكون Glimmer في نظام الودجات
return new RenderGlimmer(
helper.widget,
"div",
<template><SolvedAcceptedAnswer @post={{@data.post}} /></template>
null,
{ post }
);
}
});
}
export default {
name: "extend-for-solved-button",
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
if (siteSettings.solved_enabled) {
withPluginApi((api) => {
customizePost(api);
// ... تخصيصات أخرى
});
}
},
};
أمثلة من العالم الحقيقي
إليك روابط لسحب طلبات (pull requests) هجرة فعلية من إضافاتنا الرسمية. تظهر هذه كيفية تحديث كل نوع من أنواع التخصيص:
- discourse-ai
- discourse-assign
- discourse-cakeday
- discourse-post-voting
- discourse-reactions
- discourse-shared-edits
- discourse-solved
- discourse-topic-voting
- discourse-user-notes
- discourse-activity-pub
استكشاف الأخطاء وإصلاحها
موقعي يبدو معطلاً بعد تفعيل تدفق المنشورات الجديد
- أعد تعيين
glimmer_post_stream_modeإلىdisabled - تحقق من وحدة التحكم (console) بحثًا عن رسائل خطأ محددة
- قم بتحديث الإضافات/المظاهر المشكلة قبل المحاولة مرة أخرى
لا أرى أي تحذيرات، لكن تخصيصاتي لا تعمل
- تحقق من أن تخصيصاتك تستهدف الودجات المذكورة أعلاه
- تأكد من أنك تختبر على صفحة الموضوع مع موضوعات تكون فيها تخصيصاتك مرئية
هل تحتاج إلى مساعدة؟
إذا وجدت خطأً أو لم يكن من الممكن تحقيق تخصيصك باستخدام واجهات برمجة التطبيقات الجديدة التي قدمناها، فيرجى إخبارنا بذلك أدناه.