Apps (Plugins) schreiben
Apps fügen Antimatter von innen neue Funktionen hinzu: ein Server-Teil in Go, der in den Server eingebunden wird, und ein Web-App-Teil in React, der Menüs, Panels, Nachrichtentypen und Seiten hinzufügt. Diese Seite zeigt anhand der mitgelieferten Polls-App, wie eine App aufgebaut ist, und welche Änderungen in der Fusion-Oberfläche von Antimatter auftreten.
Wie eine App funktioniert
- Der Server-Teil ist ein Go-Programm. Der Server startet es als eigenen Prozess und kommuniziert über RPC: Er ruft die Hooks der App auf (eine Nachricht soll gepostet werden, ein Befehl wurde ausgeführt …), und die App ruft die Plugin-API des Servers auf (einen Beitrag erstellen, einen Benutzer lesen, Daten speichern …). Sie kann auch HTTP-Routen unter
/plugins/<app id>/bereitstellen. - Der Web-App-Teil ist ein JavaScript-Bundle, das zusammen mit der Web-App im Browser geladen wird. Er registriert React-Komponenten und Callbacks im Registry: ein Button in einem Menü, ein Panel, eine Methode zum Zeichnen eines Nachrichtentyps.
- Beide Teile sind optional. Eine App wird als
.tar.gz-Bundle installiert, und Systemadministratoren aktivieren, deaktivieren und konfigurieren sie.
Antimatter führt Apps aus, die für Mattermost geschrieben wurden, und umgekehrt, solange eine App nur Hooks verwendet, die beide haben. Siehe Nur von Antimatter vorhandene Hooks.
Das Bundle-Layout
Es gibt keine separate Starter-Vorlage: Beginnen Sie mit einer kleinen mitgelieferten App wie antimatter-plugin-polls oder antimatter-plugin-todo und ändern Sie deren ID und Code. Polls ist wie folgt aufgebaut:
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
Optional: ein public/-Ordner, der unverändert unter /plugins/<app id>/public/ bereitgestellt wird.
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: eindeutig und dauerhaft; üblich ist ein umgekehrter Domain-Name. Er benennt die Routen, die gespeicherten Einstellungen und die WebSocket-Events.version: semantische Version. Die mitgelieferten Apps lassen sie inplugin.jsonaus; ihr Build-Tool übernimmt sie aus dem neuestenv*-Git-Tag.min_server_version: Antimatter meldet12.0.0.settings_schema: die Einstellungsseite der App in der Systemkonsole (siehe Einstellungen).
make apply kopiert das Manifest in server/manifest.go und webapp/src/manifest.ts; make manifest-check validiert es.
Der Serverteil
Der Serverteil importiert dasselbe Go-Modul wie Mattermost-Apps, github.com/mattermost/mattermost/server/public, aus dem Go-Modul-Proxy. Sein main startet nur die App:
package main
import "github.com/mattermost/mattermost/server/public/plugin"
func main() {
plugin.ClientMain(&Plugin{})
}
Der Typ Plugin kapselt plugin.MattermostPlugin, was ihm p.API zur Verfügung stellt, und implementiert die benötigten 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)
}
Gängige Hooks
| Hook | Wann er ausgeführt wird |
|---|---|
OnActivate, OnDeactivate | Wenn die App aktiviert oder deaktiviert wird sowie beim Start und Stopp des Servers. Befehle und Bots werden in OnActivate registriert. |
OnConfigurationChange | Wenn sich die Einstellungen ändern; laden Sie sie mit p.API.LoadPluginConfiguration(&cfg). |
ServeHTTP | Anfragen an /plugins/<app id>/…. Der Server setzt den Header Mattermost-User-Id, wenn die Anfrage von einem angemeldeten Benutzer stammt; lehnen Sie Anfragen ohne diesen Header ab, es sei denn, sie sollen öffentlich sein. |
ExecuteCommand | Ein von der App registrierter Befehl wurde ausgeführt. |
MessageWillBePosted, MessageWillBeUpdated | Bevor eine Nachricht gespeichert wird: eine geänderte Nachricht zurückgeben oder einen Grund für die Ablehnung angeben. |
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeleted | Nachträglich, für Nebeneffekte. |
ReactionHasBeenAdded, ReactionHasBeenRemoved | Reaktionen. |
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreated | Mitgliedschafts- und Kanaländerungen. Ein Team ist eine Community. |
FileWillBeUploaded | Bevor eine Datei gespeichert wird: sie ändern oder ablehnen. |
OnPluginClusterEvent | Eine Nachricht von derselben App auf einem anderen Server eines Clusters. |
Die vollständige Liste mit den exakten Signaturen finden Sie in server/public/plugin/hooks.go; die API befindet sich in server/public/plugin/api.go. Das Paket pluginapi (server/public/pluginapi) kapselt die API in nutzerfreundlicheren Hilfsfunktionen.
Nützliche API-Aufrufe
CreatePost,UpdatePost,SendEphemeralPost: als Bot der App oder als Benutzer posten.EnsureBotUser(&model.Bot{Username, DisplayName}): den Bot der App einmalig erstellen und dessen ID abrufen.PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): ein Ereignis an die Web-App senden. Es trifft alscustom_<app id>_poll_updatedein.OpenInteractiveDialog: einen Dialog öffnen.LogInfo,LogErrorund weitere: in das Serverprotokoll schreiben, markiert mit der ID der App.GetUser,GetChannel,HasPermissionToChannel: prüfen, wer was darf, bevor für einen Benutzer gehandelt wird.
Eine App läuft auf jedem Server eines Clusters. Speichern Sie den Zustand im Key-Value-Speicher oder in der Datenbank, nicht nur im Arbeitsspeicher.
Der KI-Dienst
Der Antimatter-Server ruft KI-Modelle für Apps über die von Administratoren eingerichteten Modellverbindungen auf (siehe KI im Admin-Handbuch). Eine App speichert niemals den Schlüssel eines Anbieters: Sie gibt an, für welche Person und an welchem Ort sie handelt, und der Server prüft, ob diese Person die Verbindung dort nutzen darf, wendet ihre Limits und Budgets an, zählt die Tokens für die Person und die App und protokolliert den Aufruf ohne dessen Inhalt.
| Methode | Funktion |
|---|---|
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest) | Eine Chat-Vervollständigung: Nachrichten, Tools, strukturierte Ausgabe (JSONSchema). |
AIChatStream(…) | Dasselbe im Stream: Lesen Sie die Events des Streams, bis der Kanal geschlossen wird, oder rufen Sie Close auf, um den Aufruf zu beenden. |
AIEmbed(…, *ai.EmbedRequest) | Einbettungen von Texten. |
AITranscribe(…, *ai.TranscribeRequest) | Sprache-zu-Text. |
AIDecide(…, *ai.DecideRequest) | Typisierte Fragen an ein Entscheidungsmodell (Ja oder Nein, eine Auswahl, eine Bewertung) zu einem Text. |
AIConnectionsForUser(userID, channelID, teamID) | Die Verbindungen, die eine Person dort nutzen darf, mit ihren Modellen und Fähigkeiten, niemals mit ihren Schlüsseln. |
Die ConnectionID jeder Anfrage wählt die Verbindung aus, und Model eines ihrer Modelle (leer für das Standardmodell der Verbindung). Operation ist eine kurze Bezeichnung für die Nutzungs- und Auditdatensätze, niemals der Inhalt. Die Mindestserverversion ist 11.11.
actingUserID nur aus einer Quelle, die der Server überprüft hat: dem Header Mattermost-User-Id einer Anfrage an Ihr ServeHTTP oder dem Benutzer des Kontexts eines Hooks (dem Autor eines Beitrags, dem Benutzer, der einen Befehl ausgeführt hat). Übernehmen Sie ihn niemals aus einer Nachricht, einem Webhook-Nutzlast, einer URL oder einer Anfrage einer anderen App: Ihre App würde den Zugriff einer anderen Person verwenden.Diese Methoden und das Paket ai (server/public/ai) existieren nur in der Kopie des Moduls von Antimatter, nicht in der von Mattermost veröffentlichten Version. Um sie zu nutzen, erstellen Sie Ihre Anwendung gegen server/public von Antimatter mit einer replace-Zeile in Ihrer go.mod, zum Beispiel replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. Eine solche App läuft nur auf Antimatter.
Agenten mit Tools ausstatten
Eine App stellt Agenten ihre eigenen Tools über RegisterAITools([]*model.AIToolDefinition) zur Verfügung: Dazu gehören der Name, der Titel, die Beschreibung, das Eingabe-JSON-Schema und die Art (read, write oder external_write) jedes Tools. Die Art bestimmt, ob die Richtlinie eines Agenten das Tool eigenständig ausführen darf (siehe Tools im Admin-Handbuch). Die Berechtigungen werden je nach Bedarf des Tools entweder anhand eines Kanal- oder Community-Arguments des Aufrufs oder auf dem Server geprüft. Wenn ein Agent das Tool nutzt, ruft der Server den Hook ExecuteAITool(c, *model.AIToolExecution) *model.AIToolResult auf und übergibt die verifizierte Person, die die Anfrage gestellt hat, im Feld UserId: Handeln Sie ausschließlich mit deren Berechtigungen, niemals mit denen des Agenten. Tools werden als <app id>.<name> benannt; Mindestserverversion ist 11.11; diese Methoden und der Hook existieren nur in Antimatters server/public, genau wie der oben genannte KI-Dienst. Apps mit der ID antimatter oder einer ID, die mit mcp. beginnt, können keine Tools registrieren.
Datenspeicherung
Jede App verfügt über einen eigenen Key-Value-Speicher in der Datenbank des Servers:
KVSet(key, value),KVGet(key),KVDelete(key): Die Werte sind Bytes, in der Regel JSON. Schlüssel sind höchstens 150 Zeichen lang.KVSetWithExpiry(key, value, seconds): Der Wert wird nach dieser Zeit gelöscht.KVCompareAndSet(key, old, new)undKVSetWithOptionsmitAtomic: Sichere Aktualisierungen, wenn mehrere Anfragen oder Server denselben Schlüssel schreiben. Polls speichert jede Stimme auf diese Weise und wiederholt bei Konflikten.KVList(page, perPage): Die Schlüssel, Seite für Seite.
Der Web-App-Anteil
Das Bundle registriert eine Klasse mit window.registerPlugin. Ihr initialize erhält das Registry und den Redux-Store:
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());
- Gemeinsame Bibliotheken. Die Web-App stellt React, ReactDOM, Redux, React Redux und React Intl bereit: Listen Sie diese als webpack
externals(React,ReactDOM,Redux,ReactRedux,ReactIntl) auf, anstatt eigene Kopien zu bündeln. Siehe Polls'webpack.config.js. - Aufruf Ihres Server-Anteils. Rufen Sie
${window.basename}/plugins/<app id>/…mit den Optionen ausClient4.getOptions()(ausmattermost-redux/client) ab: Sie enthalten die Header, die der Server benötigt, um eine Anfrage vom Browser zu akzeptieren. - Ihr eigener Zustand.
registerReducerfügt einen Reducer hinzu, der unterstate['plugins-<app id>']gespeichert wird. - Typen. Die zusätzlichen Registry-Methoden von Antimatter sind nicht in den veröffentlichten Typen von Mattermost enthalten: Polls deklariert die von ihm verwendete Registry in
webapp/src/types/host.ts.
Die Methoden der Registry und ihre Parameter sind im Code dokumentiert, in webapp/fusion/src/plugins/registry.ts (Fusion) und webapp/channels/src/plugins/registry.ts (klassisch).
Unterstützung der Fusion-Oberfläche
Antimatter hat zwei Web-Oberflächen: Fusion, die Standardoberfläche, und die klassische. Beide laden dieselben App-Bundles und haben dieselbe Registry, aber Fusion zeichnet viele Elemente selbst und platziert Plugin-Komponenten auf eigene Weise. Testen Sie Ihre App in beiden.
Die Oberfläche erkennen
Bevor eine App geladen wird, setzen beide Oberflächen window.antimatterWebUI auf 'fusion' oder 'classic', und denselben Wert auf dem Wurzelelement der Seite als data-am-web-ui. Bei Mattermost ist keiner der beiden gesetzt.
export function isFusionUI(): boolean {
return window.antimatterWebUI === 'fusion' || document.documentElement.dataset.amWebUi === 'fusion';
}
Die mitgelieferten Apps nutzen dies für eine unterschiedliche Darstellung. Polls zeichnet seine Umfragekarten im Stil von Fusion, und in Fusion wird die Umfrage im Composer geschrieben (ein Composer-Modus) anstelle eines Dialogs. Calls und Sprachkanäle lassen die Seitenleisten- und Toast-Komponenten weg, die Fusion durch seine eigene Anrufoberfläche ersetzt. Die KI-Chat-App platziert ihre KI-Aktionen im Nachrichtenmenü als Gruppe unter Weitere Aktionen anstelle der Hover-Werkzeugleiste. Die eigenen CSS-Klassen von Fusion (mit dem Präfix am-) und seine Dialogebene (das am-layer-Element) sind das, was diese Apps nutzen, um die Oberfläche anzupassen; sie sind keine dokumentierte, stabile API.
Wo Fusion Plugin-Komponenten anzeigt
| Registry-Methode | Wo sie in Fusion erscheint |
|---|---|
registerAppBarComponent, registerChannelHeaderButtonAction | Die App-Leiste, die Spalte mit den Symbolen am rechten Rand, zwischen Ihrem Avatar und der Schaltfläche Über Antimatter. Auch die Schaltflächen im Kanalheader erscheinen dort, nicht im Kanalheader selbst. Versteckt, wenn ein Systemadministrator Apps-Leiste deaktivieren (ExperimentalSettings.DisableAppBar, standardmäßig deaktiviert) aktiviert. |
registerRightHandSidebarComponent | Das rechte Panel. Sein Header ist der von Fusion und für jede App gleich: das Symbol und der Name der App (der registrierte Titel, wenn nur Text angezeigt wird, andernfalls die Beschriftung der App-Leiste oder der Name aus dem Manifest), dann Panelgröße wiederherstellen (nach einer Größenänderung), In neuem Fenster öffnen und Schließen. Ein Titel, der etwas anderes anzeigt (ein Steuerelement, einen Link, ein Bild), wird verworfen. Zeichnen Sie daher die eigenen Steuerelemente Ihrer App im Panel selbst, beispielsweise in einer schmalen Zeile am oberen Rand. Personen können die Größe des Panels ändern, indem sie dessen Rand ziehen. Siehe Das Panel in einem neuen Fenster. |
registerChannelHeaderIcon | Symbole im Kanalheader, nach den Anruftasten; auf Smartphones im Menü Mehr des Headers. |
registerCallButtonAction | Die Anruftaste im Kanalheader; mehrere Anbieter teilen sich ein Menü mit Weiteren Anrufoptionen. |
registerPostDropdownMenuAction | Das Menü Nachrichten-Aktionen (⋯) einer Nachricht im Untermenü Weitere Aktionen; auch im Menü Thread-Aktionen des Thread-Panels unter Weitere Aktionen. Fusion zeigt vor jeder Aktion das Symbol der App in Grau an: das eigene Symbol für mitgelieferte Apps, andernfalls das Symbol, das die App in der App-Leiste anzeigt, andernfalls ein neutrales Symbol. Ein Element ohne Text am Anfang des Aktionstextes (ein von der App selbst gezeichnetes Symbol) wird ausgeblendet. |
registerPostDropdownSubMenuAction | Eine Gruppe von Einträgen im selben Untermenü Weitere Aktionen, nach den einzelnen Aktionen, durch eine Linie getrennt, ohne Überschrift. Keine App erhält einen eigenen Eintrag im Nachrichtenmenü. |
registerPostActionComponent | Die Hover-Werkzeugleiste der Nachricht, vor der ⋯-Schaltfläche. |
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponent | Die Nachricht selbst: ihr Inhalt, unter ihrem Text, nach ihrer Zeitangabe und anstelle von Linkvorschauen. |
registerChannelHeaderMenuAction | Das Menü Kanaloptionen des Kanals (⋯ auf einem Kanal in der Seitenleiste oder Rechtsklick), als Einträge mit einem Stecker-Symbol. |
registerMainMenuAction | Das Community-Menü (der Name der Community oben in der Kanalliste), unter Weitere Aktionen, gruppiert nach App. |
registerSidebarBrowseOrAddChannelMenuAction | Das Community-Menü, nach Kanal erstellen und Kategorie erstellen. |
registerFileUploadMethod | Das Menü + des Eingabefelds (Zur Nachricht hinzufügen), nach Datei hochladen. |
registerPostEditorActionComponent | Die Formatierungsleiste des Eingabefelds, wenn Formatierung angezeigt wird. |
registerAIActionMenuItemComponent | Das Menü KI des Eingabefelds. |
registerFileDropdownMenuAction, registerFilePreviewComponent | Das Optionsmenü einer Datei und die Dateivorschau. |
registerUserSettings | Profil & Einstellungen, ein Tab pro App unter der Überschrift Plugins. |
registerChannelSettingsTab | Der Dialog für Kanaleinstellungen, unter der Überschrift Apps. |
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponent | Die Profilkarte, die angezeigt wird, wenn Sie auf einen Namen oder einen Avatar klicken. |
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponent | Der obere Bereich der Kanalliste einer Community, neben dem Namen und dem Symbol eines Kanals sowie der untere Bereich des Community-Bereichs links. |
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholder | Der Beginn des Verlaufes eines Kanals, der obere Bereich der Nachrichtenliste, die Ungelesen-Marke, über dem Eingabefeld und der Platzhaltertext des Eingabefelds. |
registerChannelTypeOption | Die Kanaltypen im Dialog Kanal erstellen. |
registerSearchComponents | Das Suchfeld im Kanalheader: Wenn es leer ist, werden die App-Bereiche angezeigt. |
registerUserGuideDropdownMenuAction | Über Antimatter (unten im App-Bereich), unter Hilfe. |
registerProduct | Vollständige Seiten unter der URL des Produkts im klassischen Seitenrahmen, ohne Produktmenü: Fusion hat keines und listet nirgends Produkte auf, daher verlinken Sie die Seite selbst. Keine mitgelieferte App registriert ein Produkt: Kanban, Playbooks und der KI-Chat öffnen sich in ihren Seitenleisten. |
registerRootComponent, registerGlobalComponent, Hooks wie registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslations | Funktionieren wie in der klassischen Oberfläche. |
registerAdminConsolePlugin, registerAdminConsoleCustomSetting | Die Systemkonsole, die in beiden Oberflächen die klassische ist. |
Was Fusion nicht anzeigt oder ausführt
registerPostDropdownMenuComponent(eine Komponente im Nachrichtenmenü) undregisterPostCardTypeComponent(die Karte eines Beitrags im rechten Panel) werden in den Hauptansichten von Fusion nicht angezeigt.registerProductSwitcherMenuItemhat in Fusion keine Wirkung, da es kein Produktmenü gibt (es wird eine Warnung protokolliert). Verwenden Sie stattdessenregisterPostDropdownMenuActionfür die erste Funktion.registerMessageWillBeUpdatedHookwird nicht ausgeführt, wenn eine Nachricht in Fusion bearbeitet wird. Prüfen Sie Änderungen auf dem Server stattdessen mit dem HookMessageWillBeUpdated.registerActionAfterChannelCreationwird nur aus dem klassischen Dialog zur Kanalerstellung ausgeführt, den Fusion über Weitere Optionen… in seinem Dialog Kanal erstellen öffnet.- Komponenten in der App-Leiste werden unabhängig von ihren
supportedProductIdsangezeigt. - Routen aus
registerNeedsTeamRouteundregisterCustomRoutebehalten das klassische Seitenlayout bei, wie bei Produkten.
Hooks, die nur Antimatter hat
Diese Registry-Methoden existieren nicht in Mattermost. Prüfen Sie, ob eine Methode existiert, bevor Sie sie aufrufen, damit die App auch dort funktioniert:
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');
}
| Methode | Was sie hinzufügt |
|---|---|
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender}) | Ein Eintrag im +-Menü des Nachrichtenfelds, in beiden Oberflächen. action erhält {channelId, rootId}. |
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker}) | Ein Reiter im Emoji-Picker des Nachrichtenfelds, z. B. für GIFs oder Sticker. In Fusion erscheinen Reiter mit einem order-Wert unter 10 vor dem Emoji-Reiter. Die Komponente erhält channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion und in Fusion pickerKit (siehe unten). |
registerChannelViewPanel({matcher, component}) | Ein Panel zwischen dem Kanal-Kopf und den Nachrichten, für die Kanäle, die matcher(state, channel) auswählt. Es kann die Nachrichten und den Editor ausblenden (setMessagesVisible), wie es Sprachkanäle tun. |
registerSidebarChannelFooterComponent(component) | Eine Zeile unter einem Kanal in der Seitenleiste, beispielsweise für die Personen in seinem Anruf. Sie erhält den channel. |
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender}) | Nur in Fusion: ein Eintrag auf den Seiten Integrationen, neben benutzerdefinierten Emojis. Die GIF-App fügt dort ihre benutzerdefinierten GIFs und Sticker hinzu. |
Ansichten im Emoji-Picker hinzufügen und verwalten
In Fusion erhält ein Tab der Emoji-Auswahl eine pickerKit-Prop: die Komponenten, die der Emoji-Tab für seine eigenen Ansichten zum Hinzufügen und Verwalten nutzt, damit die Ansichten eines Tabs einheitlich aussehen und innerhalb der Auswahl bleiben. Die Apps „GIFs“ und „Memes“ nutzen es, um GIFs, Sticker und Meme-Vorlagen hinzuzufügen und zu verwalten. Es enthält version (1), die Hooks usePickerView und useOnEscape sowie die Komponenten PickerViewButtons (die +- und Verwalten-Schaltflächen neben dem Suchfeld), PickerViewFrame (eine Ansicht mit ihrem Zurück-Pfeil), PickerNotice, PickerField, PickerSearch, PickerDropZone und PickerManageList. Mit Escape kehrt man von einer Ansicht zum Raster des Tabs zurück. Prüfen Sie, ob die Prop vorhanden ist: Andere Hosts lassen sie weg, und der Tab behält dann seine eigenen Dialoge bei. Die Typen befinden sich in webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps) und die Komponenten in webapp/fusion/src/fusion/popovers/picker_views.tsx.
Composer-Modi
In Fusion fügt registerComposerMode({id, label, fusionIcon, placeholder, sendLabel, component, shouldRender}) eine weitere Möglichkeit zum Verfassen einer Nachricht im Composer hinzu, wie es „Umfragen“ für eine Umfrage tut. Der Modus wird im +-Menü des Composers unter seinem label angeboten. Nach der Auswahl behält der Composer sein Textfeld (mit dem placeholder des Modus: der Frage der Umfrage), zeigt darunter Ihre component an und eine Leiste, die den Modus benennt, mit einer Abbrechen-Schaltfläche; Senden übernimmt das sendLabel und führt Ihren Submit-Handler aus, anstatt den Text zu posten.
- Die Komponente erhält
channelId,rootId(den Thread, in einem Antwortfeld),message(den Text des Composers),setSubmitundonCancel(zurück zu einer normalen Nachricht, wobei der Text erhalten bleibt). - Übergeben Sie
setSubmiteine Funktion, die mit dem Text des Composers aufgerufen wird und sich, sobald gesendet, zu nichts (oder einem leeren String) auflöst: Der Composer leert sich dann und kehrt zu einer normalen Nachricht zurück. Lösen Sie sich zu einem String auf, um ihn an Ort und Stelle als Fehler anzuzeigen. placeholderundsendLabelsind Strings oder Funktionen von react-intlsintl.shouldRender(state, {channelId, rootId})entscheidet, ob ein Composer den Modus anbietet.- Um den Modus von anderer Stelle aus zu öffnen, z. B. über einen Slash-Befehl ohne Argumente:
window.dispatchEvent(new CustomEvent('am-composer-mode', {detail: {id, channelId, rootId}})).
Nur Fusion unterstützt Composer-Modi: Prüfen Sie, ob die Funktion im Registry vorhanden ist, und behalten Sie andernfalls Ihren eigenen Dialog bei. Die Methode gibt die ID des Modus zurück, und der Modus wird entfernt, wenn die App entfernt wird.
Das Panel in einem neuen Fenster
Fusion stellt der Komponente von registerRightHandSidebarComponent zwei zusätzliche Props zur Verfügung, damit In neuem Fenster öffnen das Panel dort öffnet, wo es sich befindet, und nicht auf der Startseite der App:
setPanelState(state): Rufen Sie diese Funktion auf, wenn sich die Position des Panels ändert, und übergeben Sie einen kurzen String, den Ihre App definiert, z. B.'note/<id>'oder'board/<id>/card/<id>', odernull, wenn sich das Panel auf der Startseite befindet. Der String wird in die Adresse des Fensters aufgenommen, daher sollten Sie niemals vertrauliche Daten darin speichern. Ein State, der länger als 512 Zeichen ist oder Steuerzeichen enthält, wird verworfen.initialPanelState: Im neuen Fenster enthält dies den State, den das Panel zuvor hatte, oderundefined. Behandeln Sie dies als nicht vertrauenswürdige Eingabe: Rufen Sie das Element erneut als angemeldete Person ab und zeigen Sie die Startseite der App oder eine „Nicht gefunden“-Ansicht an, wenn die Person das Element nicht öffnen kann.
Der State wird vergessen, wenn das Panel geschlossen wird oder das Panel einer anderen App dessen Platz einnimmt. Um ein Panel-Fenster selbst zu öffnen, akzeptiert popoutRhsPlugin (ebenfalls in window.WebappUtils.popouts) den State als optionales fünftes Argument. Notes, Whiteboard, Todo, Kanban und Playbooks nutzen dies.
fusionIcon benennt ein Icon aus dem Icon-Set von Fusion (zum Beispiel poll, sticker, calendar oder image; siehe webapp/fusion/src/fusion/components/icon). Fusion zeigt es anstelle von icon, damit das Element zu den anderen passt.
Einstellungen
settings_schema in plugin.json beschreibt die Seite der App in Systemkonsole › Plugins:
"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."}
]
}
Einstellungstypen: bool, text, longtext, number, dropdown, radio, generated (ein zufälliges Geheimnis), username und custom (gerendert von einer Komponente, die der Web-App-Teil mit registerAdminConsoleCustomSetting registriert). Einstellungen können in sections gruppiert werden.
Der Server-Teil liest sie in eine Struktur ein, deren Felder mit den Schlüsseln übereinstimmen:
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
}
Administratoren können sie auch in der Konfiguration festlegen, unter PluginSettings.Plugins.<app id>, mit Schlüsseln in Kleinbuchstaben (siehe den Administratorleitfaden).
Übersetzungen
Antimatter ist in 37 Sprachen übersetzt, ebenso wie die mitgelieferten Apps. Der Web-App-Teil macht das wie folgt:
- Schreiben Sie jede Zeichenkette mit React Intl, mit einer ID und ihrem englischen Text:
<FormattedMessage id='polls.create' defaultMessage='Create a poll'/>oderdefineMessages. Präfixen Sie die IDs mit dem Namen Ihrer App. - Extrahieren Sie die englischen Zeichenketten in
webapp/i18n/en.json(die mitgelieferten Apps verwendenformatjs extract). - Fügen Sie neben dieser Datei eine Datei pro Sprache hinzu, mit denselben IDs:
fr.json,de.json,pt-BR.json,zh-CN.json… - Geben Sie sie an die Web-App weiter, die die Sprache des Nutzers abfragt:
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);
Fehlt eine Zeichenfolge, wird auf die englische defaultMessage zurückgegriffen. Für Zeichenfolgen, die der Server-Teil sendet (Befehlsantworten, Bot-Nachrichten), lädt das Paket pluginapi/i18n die im Bundle mitgelieferten Übersetzungsdateien (z. B. assets/i18n) und stellt einen Localizer in der jeweiligen Sprache des Nutzers bereit.
Bauen, installieren und debuggen
Sie benötigen Go und Node.js (die Version aus der .nvmrc der App). Im Ordner der App:
| Befehl | Funktion |
|---|---|
make dist | Baut den Server-Teil für alle Plattformen sowie den Web-App-Teil und schreibt das Bundle nach dist/<app id>-<version>.tar.gz. |
make test | Führt die Go- und JavaScript-Tests aus. |
make check-style | Prüft den Code (golangci-lint, ESLint) und die TypeScript-Typen. |
make deploy | Baut, installiert und aktiviert anschließend das Bundle auf dem Server. |
make watch, make deploy-from-watch | Der erste Befehl baut den Web-App-Teil jedes Mal neu, wenn Sie speichern; der zweite bündelt das Ergebnis und installiert es auf dem Server. |
make enable, make disable, make reset | Aktiviert, deaktiviert oder startet die App auf dem Server neu. |
make logs, make logs-watch | Zeigt die Zeilen der App im Server-Protokoll an. |
make attach | Hängt den Delve-Debugger an den laufenden Server-Teil an (zuvor mit AM_DEBUG=1 bauen). |
make deploy und die anderen Server-Befehle verwenden einen lokalen Modus-Socket, wenn sie einen finden (/var/tmp/antimatter_local.socket oder AM_LOCALSOCKETPATH): Führen Sie sie auf dem Server mit aktiviertem lokalem Modus aus. Andernfalls melden sie sich mit AM_ADMIN_TOKEN oder AM_ADMIN_USERNAME und AM_ADMIN_PASSWORD beim Server an, der durch MM_SERVICESETTINGS_SITEURL angegeben ist. Die Build-Tools lesen den Namen für die Site-URL noch nicht aus einer AM_-Umgebungsvariablen.
Ein Bundle manuell installieren
- Ein System-Administrator ermöglicht Uploads mit
AM_PLUGINSETTINGS_ENABLEUPLOADS=true(standardmäßig deaktiviert; es gibt keinen Schalter in der Systemkonsole). - Dann Systemkonsole › Plugins › Plugin-Verwaltung › Lade Plugin hoch, oder
amctl plugin add weather.tar.gzgefolgt vonamctl plugin enable com.example.weather. - Wenn auf dem Server Plugin-Signatur erfordern aktiviert ist, akzeptiert er nur Pakete, die mit einem vertrauenswürdigen Schlüssel signiert sind; siehe Andere Apps installieren.
AM_SERVICESETTINGS_ENABLEDEVELOPER=true: Fehler enthalten dann ihre Details. Die Makefiles der mitgelieferten Apps bauen den Server-Teil nur für Ihre Plattform, wenn MM_SERVICESETTINGS_ENABLEDEVELOPER in Ihrer Shell gesetzt ist.