כתיבת אפליקציות (תוספים)

אפליקציות מוסיפות יכולות ל-Antimatter מבפנים: חלק שרת בשפת Go שמתחבר לשרת, וחלק אפליקציית Web ב-React שמוסיף תפריטים, פאנלים, סוגי הודעות ועמודים. עמוד זה מציג כיצד אפליקציה מונחת, תוך שימוש באפליקציית סקרים (Polls) המצורפת כדוגמה, ומה משתנה בממשק Fusion של Antimatter.

כיצד אפליקציה עובדת

  • חלק השרת הוא תוכנית בשפת Go. השרת מריץ אותו כתהליך נפרד ומתקשר איתו באמצעות RPC: הוא קורא להוקים של האפליקציה (הודעה עומדת להתפרסם, פקודה הורצה…), והאפליקציה קוראת לplugin API של השרת (יצירת פוסט, קריאת משתמש, אחסון נתונים…). היא יכולה גם לשרת נתיבי HTTP תחת /plugins/<app id>/.
  • חלק אפליקציית ה-Web הוא חבילת JavaScript שנטענת בדפדפן יחד עם אפליקציית ה-Web. היא רשומה בregistry: רכיבי React וקריאות חזרה (callbacks) – כפתור בתפריט, פאנל, דרך לצייר סוג הודעה.
  • כל חלק מהחלקים הוא אופציונלי. אפליקציה מותקנת כחבילת .tar.gz, ומנהלי מערכת מאפשרים, מעכבים ומגדירים אותה.
חלק השרת רץ עם גישה מלאה לנתוני השרת. יש להתייחס לאפליקציה כאל קוד שמועלה לשרת.

Antimatter מריץ אפליקציות שכתובות עבור Mattermost, ולהפך, כל עוד האפליקציה משתמשת רק בהוקים שיש לשני הצדדים. ראו הוקים שיש רק ל-Antimatter.

מבנה החבילה

אין תבנית התחלה נפרדת: יש להתחיל מאפליקציה מצורפת קטנה כמו antimatter-plugin-polls או antimatter-plugin-todo, ולשנות את המזהה (ID) והקוד שלה. אפליקציית Polls מונחת כך:

antimatter-plugin-polls/
├── plugin.json            the manifest
├── Makefile               build, test, bundle, deploy
├── build/                 the manifest and pluginctl helper tools (Go), setup.mk
├── assets/icon.svg        files shipped with the bundle
├── go.mod
├── server/
│   ├── main.go            plugin.ClientMain(&Plugin{})
│   ├── plugin.go          the Plugin type, OnActivate, ServeHTTP
│   ├── manifest.go        generated from plugin.json
│   ├── api.go, command.go, hooks.go, store.go …
│   └── dist/              compiled binaries, one per platform
└── webapp/
    ├── package.json, webpack.config.js, tsconfig.json
    ├── i18n/              en.json and one file per language
    ├── src/index.tsx      registers the app with the web app
    ├── src/manifest.ts    generated from plugin.json
    ├── src/translations.ts
    └── dist/main.js       the bundle

אופציונלי: תיקיית public/, שמוגשת כפי שהיא בנתיב /plugins/<app id>/public/.

plugin.json

{
  "id": "com.example.weather",
  "name": "Weather",
  "description": "Forecasts in the chat.",
  "homepage_url": "https://github.com/example/weather",
  "support_url": "https://github.com/example/weather/issues",
  "icon_path": "assets/icon.svg",
  "version": "1.0.0",
  "min_server_version": "12.0.0",
  "server": {
    "executables": {
      "linux-amd64": "server/dist/plugin-linux-amd64",
      "linux-arm64": "server/dist/plugin-linux-arm64",
      "darwin-amd64": "server/dist/plugin-darwin-amd64",
      "darwin-arm64": "server/dist/plugin-darwin-arm64",
      "windows-amd64": "server/dist/plugin-windows-amd64.exe"
    }
  },
  "webapp": {"bundle_path": "webapp/dist/main.js"},
  "settings_schema": {"settings": []}
}
  • id: ייחודי וקבוע; שם דומיין הפוך הוא נפוץ. הוא קובע את שם הנתיבים, ההגדרות המאוחסנות ואירועי WebSocket.
  • version: גרסה סמנטית. האפליקציות המצורפות משמיטות את השדה הזה מ-plugin.json; כלי הבנייה שלהן לוקח אותו מהתגית v* האחרונה ב-git.
  • min_server_version: Antimatter מדווח 12.0.0.
  • settings_schema: עמוד ההגדרות של האפליקציה בקונסולת המערכת (ראו הגדרות).

make apply מועיל את המניפסט לserver/manifest.go ולwebapp/src/manifest.ts; make manifest-check מאמת אותו.

חלק השרת

חלק השרת יוצא את אותו מודול Go כמו אפליקציות Mattermost, github.com/mattermost/mattermost/server/public, מהפרוקסי של מודולי Go. הmain שלו פשוט מריץ את האפליקציה:

package main

import "github.com/mattermost/mattermost/server/public/plugin"

func main() {
	plugin.ClientMain(&Plugin{})
}

הטיפוס Plugin משלב את plugin.MattermostPlugin, שמעניק לו p.API, ומממש את ה- hooks הנדרשים:

type Plugin struct {
	plugin.MattermostPlugin
	router *mux.Router
}

func (p *Plugin) OnActivate() error {
	p.router = p.newRouter()
	return p.API.RegisterCommand(&model.Command{
		Trigger:          "weather",
		AutoComplete:     true,
		AutoCompleteDesc: "Show the forecast",
		AutoCompleteHint: "[city]",
	})
}

func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
	city := strings.TrimSpace(strings.TrimPrefix(args.Command, "/weather"))
	return &model.CommandResponse{
		ResponseType: model.CommandResponseTypeEphemeral,
		Text:         "Forecast for " + city + ": sunny",
	}, nil
}

func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
	p.router.ServeHTTP(w, r)
}

hooks נפוצים

hookמתי הוא רץ
OnActivate, OnDeactivateכשהאפליקציה מופעלת או נכבית, וכשהשרת נדלק ונכבה. יש להרשיל פקודות ובוטים בOnActivate.
OnConfigurationChangeכשההגדרות משתנות; יש לטעון אותן עם p.API.LoadPluginConfiguration(&cfg).
ServeHTTPבקשות ל/plugins/<app id>/…. השרת מציב את כותרת Mattermost-User-Id כשהבקשה מגיעה ממשתמש מחובר; יש לסרב לבקשות בלעדיה אלא אם הן מיועדות להיות ציבוריות.
ExecuteCommandפקודה שהאפליקציה הרשילה הורצה.
MessageWillBePosted, MessageWillBeUpdatedלפני שהודעה נשמרת: להחזיר פוסט שונה, או סיבה לדחות אותו.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeletedאחרי העובדה, לצד אפקטים.
ReactionHasBeenAdded, ReactionHasBeenRemovedתגובות.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreatedשינויים בהשתייכות ובערוצים. צוות הוא קהילה.
FileWillBeUploadedלפני שקובץ נשמר: לשנות אותו או לדחות אותו.
OnPluginClusterEventהודעה מאותה אפליקציה משרת אחר בקלאסטר.

הרשימה המלאה, עם החתימות המדויקות, נמצאת בserver/public/plugin/hooks.go; ה-API נמצא בserver/public/plugin/api.go. חבילת pluginapi (server/public/pluginapi) עוטפת את ה-API בעזרים נוחים יותר.

קריאות API שימושיות

  • CreatePost, UpdatePost, SendEphemeralPost: לפרסם כבוט של האפליקציה או כמשתמש.
  • EnsureBotUser(&model.Bot{Username, DisplayName}): ליצור את הבוט של האפליקציה פעם אחת ולקבל את המזהה שלו.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): לשלוח אירוע לאפליקציית הווב. הוא מגיע כcustom_<app id>_poll_updated.
  • OpenInteractiveDialog: לפתוח דיאלוג.
  • LogInfo, LogError ושות': לכתוב ליומן השרת, עם תגית המזהה של האפליקציה.
  • GetUser, GetChannel, HasPermissionToChannel: לבדוק מי יכול לעשות מה לפני שפועלים בשם משתמש.

האפליקציה רצה על כל שרת בקלאסטר. יש לשמור את המצב במאגר מפתח־ערך או במסד הנתונים, ולא רק בזיכרון.

שירות ה-AI

שרת Antimatter פונה לדגמי AI עבור אפליקציות, באמצעות חיבורי הדגמים שהוגדרו על ידי מנהל (ראו AI במדריך למנהלים). אפליקציה לעולם אינה מחזיקה מפתח של ספק: היא מציינת את האדם שבו היא פועלת בשמו ואת המיקום, והשרת בודק שהאדם רשאי להשתמש בחיבור שם, מיישם את הגבלותיו ותקציביו, מונה את הטוקנים מול האדם והאפליקציה, ומבצע רישום ביקורת (audit) של השיחה ללא תוכנה.

שיטהמה היא עושה
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)השלמת שיחה (chat completion): הודעות, כלים, פלט מבני (JSONSchema).
AIChatStream(…)אותו דבר, בזרימה (streaming): קרא את Events של הזרימה עד שהערוץ נסגר, או Close אותו כדי לעצור את השיחה.
AIEmbed(…, *ai.EmbedRequest)Embeddings של טקסטים.
AITranscribe(…, *ai.TranscribeRequest)המרת דיבור לטקסט.
AIDecide(…, *ai.DecideRequest)שאלות טיפוסיות (typed) לדגם קבלת החלטות (כן או לא, בחירה, ציון) לגבי טקסט.
AIConnectionsForUser(userID, channelID, teamID)החיבורים שבהם אדם רשאי להשתמש, כולל הדגמים והיכולות שלהם, מעולם לא המפתחות.

ConnectionID של כל בקשה בוחר את החיבור, ו-Model בוחר אחד מהדגמים שלו (ריק עבור הדגם המוגדר כברירת מחדל בחיבור). Operation הוא תווית קצרה לשימוש ולרישומי הביקורת, מעולם לא תוכן. גרסת השרת המינימלית היא 11.11.

קח את actingUserID רק ממקור שהשרת אימת: כותרת Mattermost-User-Id של בקשה ל-ServeHTTP שלך, או המשתמש מההקשר של הוק (כותב ההודעה, המשתמש שריץ פקודה). מעולם אל תקח אותו מהודעה, מטענת webhook, מ-URL או מבקשה של אפליקציה אחרת: אפליקציה כזו תבזבז גישה של אדם אחר.

שיטות אלו ו-ai package (server/public/ai) קיימות רק בעותק של Antimatter של המודול, ולא בגרסה שפורסמה על ידי Mattermost. כדי להשתמש בהן, בצע build מול server/public של Antimatter עם שורת replace ב-go.mod שלך, למשל replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. אפליקציה כזו תרוץ רק על Antimatter.

שמירת נתונים

לכל אפליקציה יש מאגר מפתח־ערך משלה במסד הנתונים של השרת:

  • KVSet(key, value), KVGet(key), KVDelete(key): הערכים הם בתים, בדרך כלל JSON. המפתחות הם עד 150 תווים.
  • KVSetWithExpiry(key, value, seconds): הערך נעלם לאחר הזמן שצוין.
  • KVCompareAndSet(key, old, new) ו־KVSetWithOptions עם Atomic: עדכונים בטוחים כאשר מספר בקשות או שרתים כותבים את אותו מפתח. Polls שומרת כל קול כך ומנסה שוב במקרה של סתירה.
  • KVList(page, perPage): המפתחות, עמוד אחרי עמוד.

חלק האפליקציה הוובית

הבאנדל מרשם מחלקה באמצעות window.registerPlugin. ה־initialize שלה מקבל את הרשומה (registry) ומאגר Redux:

import manifest from './manifest';

class Plugin {
    initialize(registry, store) {
        registry.registerTranslations((locale) => translationsFor(locale));
        registry.registerPostTypeComponent('custom_poll', PollPost);
        registry.registerWebSocketEventHandler(`custom_${manifest.id}_poll_updated`, (msg) => {
            store.dispatch(pollUpdated(JSON.parse(msg.data.poll)));
        });
        registry.registerReconnectHandler(() => store.dispatch(reloadPolls()));
    }
}

window.registerPlugin(manifest.id, new Plugin());
  • ספריות משותפות. האפליקציה הוובית מספקת React, ReactDOM, Redux, React Redux ו־React Intl: יש לרשום אותן כ־externals ב־webpack (React, ReactDOM, Redux, ReactRedux, ReactIntl) במקום להכליל עותקים משלכם. ראו את webpack.config.js של Polls.
  • קריאה לחלק השרת שלכם. יש לבצע Fetch ל־${window.basename}/plugins/<app id>/… עם האפשרויות מ־Client4.getOptions() (מ־mattermost-redux/client): הן נושאות את הכותרות (headers) שהשרת זקוק לקבלת בקשה מהדפדפן.
  • המצב שלכם. registerReducer מוסיף reducer, הנשמר תחת state['plugins-<app id>'].
  • טיפוסים. שיטות הרשומה הנוספות של Antimatter אינן נכללות בטיפוסים שפורסמו עבור Mattermost: Polls מרשמה את הרשומה שהיא משתמשת בה ב־webapp/src/types/host.ts.

שיטות הרשומה ופרמטרים שלהן מודרכות בקוד, ב־webapp/fusion/src/plugins/registry.ts (Fusion) וב־webapp/channels/src/plugins/registry.ts (קלאסי).

תמיכה בממשק Fusion

ל־Antimatter יש שני ממשקי ווב: Fusion, הממשק ברירת המחדל, והממשק הקלאסי. שניהם טוענים את אותם באנדלים של האפליקציות ומשתמשים באותה רשומה, אך Fusion מצייר רבות מהדברים בעצמו ומציב רכיבי תוספים בדרך משלו. יש לבדוק את האפליקציה בשני הממשקים.

זיהוי הממשק

לפני שכל אפליקציה נטענת, שני הממשקים מגדירים את window.antimatterWebUI ל־'fusion' או 'classic', ואת אותה ערך על האלמנט השורש של הדף כ־data-am-web-ui. ב־Mattermost, שניהם אינם מוגדרים.

export function isFusionUI(): boolean {
    return window.antimatterWebUI === 'fusion' || document.documentElement.dataset.amWebUi === 'fusion';
}

האפליקציות המצורפות משתמשות בכך כדי לצייר בצורה שונה. סקרים מציירים את כרטיסי הסקר ואת תיבת הדיאלוג ליצירת סקר בסגנון של Fusion, ופותחים דיאלוג עצמאי עבור /poll בתוך Fusion. שיחות וערוצי קול מושמטים את רכיבי סרגל הצד וההודעות הזמניות (toasts) ש-Fusion מחליף בממשק השיחה שלו. אפליקציית הסוכנים מציבה את פעולות ה-AI שלה בתפריט ההודעה, כקבוצה מתחת לפעולות נוספות, במקום בסרגל הכלים המופיע ב-hover. כמדי ה-CSS של Fusion עצמה (המתחילות ב-am-) ושלבת הדיאלוג שלה (אלמנט am-layer) הן מה שהאפליקציות האלו משתמשות בו כדי להתאים את הממשק; הן אינן API מתועד ויציב.

המקומות שבהם Fusion מציגה רכיבי תוספים

שיטת הרשמההמקום שבו זה מופיע ב-Fusion
registerAppBarComponent, registerChannelHeaderButtonActionסרגל האפליקציות, עמודת האייקונים בקצה הימני, בין האווטאר שלך לכפתור אודות Antimatter. כפתורי כותרת הערוץ נמצאים שם, ולא בכותרת הערוץ עצמה. הסרגל נסתר כאשר מנהל מערכת מפעיל את כיבוי סרגל האפליקציות (ExperimentalSettings.DisableAppBar, כבוי כברירת מחדל).
registerRightHandSidebarComponentהפאנל בצד ימין. משתמשים יכולים לשנות את גודלו על ידי גרירת השפה שלו, וליפתח אותו בחלון חדש. אם האפליקציה היא גם מוצר, הפאנל מציע פתיחה כעמוד מלא. ראו הפאנל בחלון חדש.
registerChannelHeaderIconאייקונים בכותרת הערוץ, אחרי כפתורי השיחה; בטלפונים, בתפריט עוד של הכותרת.
registerCallButtonActionכפתור השיחה בכותרת הערוץ; מספר ספקים משתפים תפריט דרכים נוספות להתקשרות.
registerPostDropdownMenuActionתפריט פעולות הודעה (⋯) של ההודעה, בתפריט הנפתח פעולות נוספות; גם בתפריט פעולות שרשור של פאנל השרשור, מתחת לפעולות נוספות. Fusion מצייר את סמל האפליקציה לפני כל פעולה, בגוון אפור: את הסמל שלה עצמה עבור האפליקציות המצורפות, אחרת את הסמל שהאפליקציה מציגה בסרגל האפליקציות, ואחרת סמל ניטרלי. אלמנט ללא טקסט בהתחלת טקסט הפעולה (סמל שהאפליקציה מציירת בעצמה) מוסתר.
registerPostDropdownSubMenuActionקבוצה עם כותרת באותו תפריט נפתח פעולות נוספות, אחרי הפעולות הבודדות. לאף אפליקציה אין פריט עצמאי בתפריט ההודעה.
registerPostActionComponentסרגל הפעולות המופיע בריחוף על ההודעה, לפני כפתור ה-⋯.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponentההודעה עצמה: גוף ההודעה, מתחת לטקסט, אחרי הזמן, ובמקום תצוגות מקדימות של קישורים.
registerChannelHeaderMenuActionתפריט אפשרויות ערוץ של הערוץ (⋯ על ערוץ בסרגל הצד, או לחיצה ימנית), כפריטים עם אייקון של תקע.
registerMainMenuActionתפריט הקהילה (שם הקהילה בראש רשימת הערוצים), תחת פעולות נוספות, מקובץ לפי אפליקציה.
registerSidebarBrowseOrAddChannelMenuActionתפריט הקהילה, אחרי יצירת ערוץ ו-יצירת קטגוריה.
registerFileUploadMethodתפריט ה-+ של עורך ההודעה (הוספה להודעה), אחרי העלאת קובץ.
registerPostEditorActionComponentסרגל העיצוב של עורך ההודעה, כאשר העיצוב מוצג.
registerAIActionMenuItemComponentתפריט ה-AI של עורך ההודעה.
registerFileDropdownMenuAction, registerFilePreviewComponentתפריט האפשרויות של קובץ, ותצוגה מקדימה של הקובץ.
registerUserSettingsפרופיל והגדרות, לשונית אחת לכל אפליקציה תחת הכותרת תוספים.
registerChannelSettingsTabחלון הגדרות הערוץ, תחת הכותרת אפליקציות.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponentכרטיס הפרופיל המוצג כאשר לוחצים על שם או על אווטאר.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponentחלקו העליון של רשימת הערוצים של קהילה, ליד שם הערוץ, סמל הערוץ, וחלקו התחתון של סרגל הצד של הקהילה.
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholderהתחלת ההיסטוריה של ערוץ, החלק העליון של רשימת ההודעות, שורת לא נקרא, מעל עורך ההודעה, ומקום השתמשת של עורך ההודעה.
registerChannelTypeOptionסוגי הערוצים בתיבת הדיאלוג יצירת ערוץ.
registerSearchComponentsתיבת החיפוש בכותרת הערוץ: תחומי האפליקציה מופיעים כאשר היא ריקה.
registerUserGuideDropdownMenuActionאודות Antimatter (בחלק התחתון של סרגל האפליקציה), תחת עזרה.
registerProductעמודים מלאים בכתובת המוצר, המפורטים תחת מוצרים באודות Antimatter. עמודי מוצר שומרים על מסגרת העמוד הקלאסית, שבה תפריט המוצר מופיע רק כאשר אפליקציה רשמה מוצר או הוסיפה פריט אליו. אפליקציות Kanban ו-Playbooks המצורפות אינן רשמות מוצרים anymore: העמודים שלהם נפתחים בפאנלים הצדדיים שלהם.
registerRootComponent, registerGlobalComponent, הוקים כמו registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslationsעובדים כפי שעושים בממשק הקלאסי.
registerAdminConsolePlugin, registerAdminConsoleCustomSettingקונסולת מערכת, שהיא הקלאסית בשני הממשקים.

מה ש-Fusion לא מציג או לא מריץ

  • registerPostDropdownMenuComponent (רכיב בתפריט ההודעה), registerPostCardTypeComponent (כרטיס של פוסט בפאנל הימני) ו-registerProductSwitcherMenuItem אינם מוצגים בתצוגות הראשיות של Fusion. השתמשו בregisterPostDropdownMenuAction במקום הראשון.
  • registerMessageWillBeUpdatedHook אינו מורץ כאשר הודעה נערכת ב-Fusion. בדקו עריכות על השרת באמצעות ההוק MessageWillBeUpdated במקום.
  • registerActionAfterChannelCreation מורץ רק מתיבת יצירת הערוץ הקלאסית, ש-Fusion פותח מאפשרויות נוספות… בתיבת היצירת ערוץ שלו.
  • רכיבי סרגל האפליקציה מוצגים ללא קשר לsupportedProductIds שלהם.
  • נתיבים מregisterNeedsTeamRoute ו-registerCustomRoute שומרים על מסגרת העמוד הקלאסית, כמו מוצרים.

הוקים שיש רק ב-Antimatter

שיטות רישום אלו אינן קיימות ב-Mattermost. ודאו ששיטה קיימת לפני שאתם קוראים אליה, כדי שהאפליקציה תעבוד שם גם:

if (typeof registry.registerComposerMenuItem === 'function') {
    registry.registerComposerMenuItem({
        text: 'Create a poll',
        icon: <i className='icon icon-chart-bar'/>,
        fusionIcon: 'poll',
        action: ({channelId, rootId}) => openPollCreation(channelId, rootId),
    });
} else {
    registry.registerChannelHeaderButtonAction(icon, (channel) => openPollCreation(channel.id), 'Create a poll', 'Create a poll');
}
שיטהמה שהיא מוסיפה
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender})פריס בתפריט + של תיבת ההודעה, בשני הממשקים. action מקבל {channelId, rootId}.
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker})לשונית של בורר האמוג'י של תיבת ההודעה, כגון GIF או מדבקות. ב-Fusion, לשוניות עם order נמוך מ-10 מופיעות לפני לשונית האמוג'י. הרכיב מקבל channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion ו, ב-Fusion, pickerKit (ראו להלן).
registerChannelViewPanel({matcher, component})פאנל בין כותרת הערוץ להודעות, עבור הערוצים שmatcher(state, channel) בוחר. הוא יכול להסתיר את ההודעות ואת עורך ההודעה (setMessagesVisible), כפי שעושים ערוצי קול.
registerSidebarChannelFooterComponent(component)שורה מתחת לערוץ בסרגל הצד, כגון האנשים בשיחה שלו. היא מקבלת את channel.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})Fusion בלבד: פריט בדפי אינטגרציות, ליד האמוג'י המותאמים אישית. אפליקציית GIFs מוסיפה שם את ה-GIFs והמדבקות המותאמים אישית.

הוספה וניהול תצוגות בבורר האמוג'י

ב-Fusion, לשונית בוחר האמוג'י מקבלת תכונה בשם pickerKit: רכיבים שהשובר האמוג'י משמש לצפייה בהוספה ובניהול, כך שהתצוגות של השוברים נראות באופן אחיד ונשארות בתוך הבוחר. אפליקציות ה-GIF וה-Memes משתמשות בו כדי להוסיף ולנהל GIF, מדבקות וקבצי ממים. התכונה מכילה את version (1), את ה-hooks usePickerView ו-useOnEscape, ואת הרכיבים PickerViewButtons (כפתורי + והניהול ליד שדה החיפוש), PickerViewFrame (תצוגה עם חץ חזרה), PickerNotice, PickerField, PickerSearch, PickerDropZone ו-PickerManageList. לחיצה על Escape חוזרת מהתצוגה לרשת של השובר. ודאו שהתכונה קיימת: מארחים אחרים אינם מעבירים אותה, ואז השובר שומר על תיבות הדיאלוג שלו. הסוגים נמצאים ב-webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps) והרכיבים ב-webapp/fusion/src/fusion/popovers/picker_views.tsx.

הפאנל בחלון חדש

Fusion מעבירה לרכיב של registerRightHandSidebarComponent שתי תכונות נוספות, כך שפתיחה בחלון חדש פותחת את הפאנל במקום שבו הוא נמצא, ולא בדף הבית של האפליקציה:

  • setPanelState(state): יש לקרוא לפונקציה זו כאשר הפאנל משנה מקום, עם מחרוזת קצרה שהאפליקציה מגדירה, כמו 'note/<id>' או 'board/<id>/card/<id>', או null כאשר הוא בדף הבית. המחרוזת מופיעה בכתובת החלון, ולכן אין להכניס אליה סודות. מחרוזת ארוכה מ-512 תווים, או כזו המכילה תווים של בקרה, תימחק.
  • initialPanelState: בחלון החדש, המצב שהפאנל היה בו, או undefined. יש להתייחס למחרוזת זו כאל קלט לא אמין: יש להביא את הפריט שוב כמשתמש המחובר, ולהציג את דף הבית של האפליקציה או תצוגת "לא נמצא" אם אין אפשרות לפתוח אותו.

המצב נשכח כאשר הפאנל נסגר או שפאנל של אפליקציה אחרת תופס את מקומו. כדי לפתוח את חלון הפאנל בעצמכם, popoutRhsPlugin (גם ב-window.WebappUtils.popouts) מקבל את המצב כטיעון חמישי אופציונלי. פתקים, לוח לבן, משימות, Kanban ו-Playbooks משתמשים בכך.

fusionIcon שם סמל במערכת הסמלים של Fusion (למשל poll, sticker, calendar או image; ראו webapp/fusion/src/fusion/components/icon). Fusion מציגה אותו במקום icon, כך שהפריט תואם את האחרים.

הגדרות

settings_schema בplugin.json מתאר את דף האפליקציה בקונסולת מערכת › תוספים:

"settings_schema": {
  "header": "Forecasts come from the weather service below.",
  "settings": [
    {"key": "ServiceURL", "display_name": "Service URL", "type": "text", "default": "https://api.example.com"},
    {"key": "Units", "display_name": "Units", "type": "dropdown", "default": "metric",
     "options": [{"display_name": "Celsius", "value": "metric"}, {"display_name": "Fahrenheit", "value": "imperial"}]},
    {"key": "EnableAlerts", "display_name": "Severe weather alerts", "type": "bool", "default": false,
     "help_text": "Post alerts in the channels that follow a city."}
  ]
}

סוגי הגדרות: bool, text, longtext, number, dropdown, radio, generated (סוד אקראי), username, ו-custom (מוציג על ידי רכיב שחלק האפליקציה הוובי רשם עם registerAdminConsoleCustomSetting). ניתן לקבץ הגדרות בsections.

חלק השרת קורא אותם ל-struct ששדותיו תואמים למפתחות:

type configuration struct {
	ServiceURL   string
	Units        string
	EnableAlerts bool
}

func (p *Plugin) OnConfigurationChange() error {
	var cfg configuration
	if err := p.API.LoadPluginConfiguration(&cfg); err != nil {
		return err
	}
	p.setConfiguration(&cfg)
	return nil
}

מנהלים יכולים גם להגדיר אותם בקונפיגורציה, תחת PluginSettings.Plugins.<app id>, עם מפתחות באותיות קטנות (ראו מדריך המנהלים).

תרגומים

Antimatter מתורגם ל-37 שפות, וכן האפליקציות המצורפות. חלק האפליקציה הוובי עושה זאת כך:

  1. כתבו כל מחרוזת עם React Intl, עם מזהה וטקסט באנגלית: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> או defineMessages. הוסיפו קידומת של שם האפליקציה למזהים.
  2. חלצו את המחרוזות באנגלית לwebapp/i18n/en.json (האפליקציות המצורפות משתמשות בformatjs extract).
  3. הוסיפו קובץ אחד לכל שפה לצידו, עם אותם מזהים: fr.json, de.json, pt-BR.json, zh-CN.json…
  4. מעבירים אותם לאפליקציית הווב, ששואלת את שפת המשתמש:
    import fr from '../i18n/fr.json';
    import ptBR from '../i18n/pt-BR.json';
    
    const catalogs = {fr, 'pt-BR': ptBR};
    
    export function translationsFor(locale) {
        return catalogs[locale] || catalogs[locale.split('-')[0]] || {};
    }
    
    registry.registerTranslations(translationsFor);

מחרוזת חסרה חוזרת לערך ברירת המחדל שלה באנגלית, defaultMessage. עבור מחרוזות ששולח חלק השרת (תשובות פקודות, הודעות בוטים), חבילת pluginapi/i18n טוענת קבצי תרגום שמשוחררים בחבילה (למשל assets/i18n) ומספקת מקומליזר בשפת כל משתמש.

בנייה, התקנה וניפוי תקלות

נדרשים Go ו־Node.js (הגרסה המצוינת ב־.nvmrc של האפליקציה). תוך תיקיית האפליקציה:

פקודהמה היא עושה
make distבונה את חלק השרת לכל הפלטפורמות ואת חלק אפליקציית הווב, וכותב את החבילה ל־dist/<app id>-<version>.tar.gz.
make testמריץ את מבחני Go ו־JavaScript.
make check-styleמבצע בדיקת קוד (golangci-lint, ESLint) ובדיקת סוגי TypeScript.
make deployבונה, ואז מתקין ומפעיל את החבילה על השרת.
make watch, make deploy-from-watchהראשונה בונה מחדש את חלק אפליקציית הווב בכל שמירה; השנייה אורזת את מה שבנייה ומתקינה אותו על השרת.
make enable, make disable, make resetמפעיל, מכבה או מאתח מחדש את האפליקציה על השרת.
make logs, make logs-watchמציג את שורות האפליקציה ביומן השרת.
make attachמחבר את מחליף Delve לחלק השרת הפועל (יש לבנות תחילה עם AM_DEBUG=1).

make deploy ופקודות השרת האחרות משתמשות בסקטת מצב מקומי אם מוצאים אחת (/var/tmp/antimatter_local.socket, או AM_LOCALSOCKETPATH): יש להריץ אותן על מכונת השרת עם מצב מקומי פעיל. אחרת, הן נכנסות לשרת שנקבע על ידי MM_SERVICESETTINGS_SITEURL באמצעות AM_ADMIN_TOKEN, או AM_ADMIN_USERNAME ו־AM_ADMIN_PASSWORD. כלי הבנייה עדיין אינם קוראים שם AM_ כתכתובת האתר.

התקנת חבילה ידנית

  • מנהל מערכת מאפשר העלאות עם AM_PLUGINSETTINGS_ENABLEUPLOADS=true (כבוי כברירת מחדל; אין מתג בקונסולת מערכת).
  • לאחר מכן קונסולת מערכת › תוספים › ניהול פלאגינים › העלאת פלאגין, או amctl plugin add weather.tar.gz ולאחריו amctl plugin enable com.example.weather.
  • אם על השרת מופעלת האפשרות דרישה לחתימת תוסף, הוא מקבל רק חבילות שחתומות במפתח שהוא מאמין בו; ראו התקנת אפליקציות נוספות.
לחזרות מהירות על שרת פיתוח, יש להגדיר AM_SERVICESETTINGS_ENABLEDEVELOPER=true על השרת: אז שגיאות נושאות את הפרטים שלהן. קבצי Makefile של האפליקציות המצורפות בונים את חלק השרת לפלטפורמה שלך בלבד כאשר MM_SERVICESETTINGS_ENABLEDEVELOPER מוגדר ב־shell שלך.