การเขียนแอป (ปลั๊กอิน)

แอปเพิ่มฟีเจอร์ให้กับ Antimatter จากภายใน: ส่วนเซิร์ฟเวอร์ใน Go ที่เชื่อมต่อเข้ากับเซิร์ฟเวอร์ และส่วน web app ใน React ที่เพิ่มเมนู, แผง, ประเภทข้อความ และหน้าต่างๆ หน้านี้แสดงว่าแอปมีโครงสร้างอย่างไร โดยใช้แอป Polls ที่มากับระบบเป็นตัวอย่าง และสิ่งที่จะเปลี่ยนแปลงในอินเทอร์เฟซ Fusion ของ Antimatter

การทำงานของแอป

  • ส่วนเซิร์ฟเวอร์ เป็นโปรแกรม Go เซิร์ฟเวอร์จะรันมันเป็นกระบวนการแยกต่างหากและสื่อสารกันผ่าน RPC: เซิร์ฟเวอร์เรียก hooks ของแอป (เช่น กำลังจะโพสต์ข้อความ, รันคำสั่งแล้ว…), และแอปเรียก plugin API ของเซิร์ฟเวอร์ (สร้างโพสต์, อ่านผู้ใช้, บันทึกละดับข้อมูล…) นอกจากนี้ยังสามารถให้บริการ HTTP routes ภายใต้ /plugins/<app id>/ ได้ด้วย
  • ส่วน web app เป็น JavaScript bundle ที่โหลดเข้าเบราว์เซอร์พร้อมกับ web app มันจะลงทะเบียน React components และ callbacks กับ registry: ปุ่มในเมนู, แผง, หรือวิธีวาดประเภทข้อความ
  • แต่ละส่วนเป็นตัวเลือก (optional) แอปจะถูกติดตั้งเป็น bundle รูปแบบ .tar.gz และผู้ดูแลระบบจะเป็นผู้เปิดใช้งาน, ปิดใช้งาน และตั้งค่า
ส่วนเซิร์ฟเวอร์ทำงานด้วยการเข้าถึงข้อมูลของเซิร์ฟเวอร์อย่างเต็มที่ ควรจัดการกับแอปเสมือนเป็นโค้ดที่คุณ deploy ไปยังเซิร์ฟเวอร์

Antimatter รันแอปที่เขียนสำหรับ Mattermost และในทางกลับกันได้ ตราบใดที่แอปใช้เฉพาะ hooks ที่ทั้งสองระบบมี ดู Hooks ที่มีเฉพาะใน Antimatter

โครงสร้างของ bundle

ไม่มี starter template แยกต่างหาก: เริ่มต้นจากแอปขนาดเล็กที่มากับระบบ เช่น 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

ตัวเลือก (Optional): โฟลเดอร์ 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: ต้องไม่ซ้ำกันและคงที่; โดยทั่วไปจะใช้ชื่อโดเมนแบบกลับ (reverse domain name) มันจะเป็นชื่อของ routes, การตั้งค่าที่บันทึกไว้ และ WebSocket events
  • version: semantic version แอปที่มากับระบบจะไม่ระบุใน plugin.json; เครื่องมือ build ของพวกมันจะดึงค่าจาก git tag v* ล่าสุด
  • min_server_version: Antimatter รายงานค่าเป็น 12.0.0
  • settings_schema: หน้่าการตั้่งค่าของแอปในคอนโซลระบบ (ดู การตั้่งค่า).

make apply จะคัดลอก manifest ไปยัง server/manifest.go และ webapp/src/manifest.ts; make manifest-check ใช้เพื่อยืนยันความถูกต้องของ manifest

ส่วนเซิร์ฟเวอร์

ส่วนเซิร์ฟเวอร์จะนำโมดูล Go เดียวกันกับแอป Mattermost คือ github.com/mattermost/mattermost/server/public มาจาก Go module proxy โดย main ของมันจะทำหน้าที่เริ่มแอปเท่านั้น:

package main

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

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

ประเภท Plugin ฝัง plugin.MattermostPlugin ไว้ ซึ่งทำให้มี p.API พร้อมใช้งาน และอิมพลีเมนต์ฮุคท่ีตองการ:

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)
}

ฮุคท่ีใช้บ่่อย

ฮุคเมื่อกำลังรัน
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}): สร้างบอทของแอปเพียงครั้งเดียวและรับ ID ของมัน
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): ส่งเหตุการณ์ไปยังแอปบนเว็บ ซึ่งจะได้รับเป็น custom_<app id>_poll_updated
  • OpenInteractiveDialog: เปิดไดอะล็อก.
  • LogInfo, LogError และฟังก์ชันอื่น ๆ: เขียนลงล็อกของเซิร์ฟเวอร์ พร้อมติดแท็กด้วย ID ของแอป
  • GetUser, GetChannel, HasPermissionToChannel: ตรวจสอบสิทธิ์ว่าผู้ใช้สามารถทำอะไรได้บ้างก่อนดำเนินการแทนผู้ใช้

แอปจะทำงานบนเซิร์ฟเวอร์ทุกตัวในคลัสเตอร์ เก็บสถานะไว้ใน key-value store หรือฐานข้อมูลเท่านั้น ไม่ใช่เก็บไว้ในหน่วยความจำอย่างเดียว

บริการ AI

เซิร์ฟเวอร์ของ Antimatter จะเรียกใช้โมเดล AI สำหรับแอป ผ่านการเชื่อมต่อโมเดลที่ผู้ดูแลระบบตั้งค่าไว้ (ดู AI ในคู่มือผู้ดูแลระบบ) แอปจะไม่เก็บคีย์ของผู้ให้บริการไว้เอง: แอปจะระบุชื่อผู้ใช้ที่ดำเนินการแทนและตำแหน่งที่ใช้งาน จากนั้นเซิร์ฟเวอร์จะตรวจสอบว่าผู้ใช้คนนั้นมีสิทธิ์使用该การเชื่อมต่อในตำแหน่งนั้น, ใช้ขีดจำกัดและงบประมาณ, นับจำนวนโทเคนโดยอ้างอิงกับผู้ใช้และแอป, และตรวจสอบบันทึกการใช้งานโดยไม่ตรวจสอบเนื้อหา

เมธอดสิ่งที่ทำ
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)การเติมเต็มบทสนทนา (chat completion): ข้อความ, เครื่องมือ, ผลลัพธ์ที่มีโครงสร้าง (JSONSchema)
AIChatStream(…)ทำนองเดียวกัน แต่เป็นแบบสตรีม: อ่าน Events ของสตรีมจนกว่าช่องจะปิด หรือใช้ Close เพื่อหยุดการเรียกใช้
AIEmbed(…, *ai.EmbedRequest)Embeddings ของข้อความ
AITranscribe(…, *ai.TranscribeRequest)การแปลงเสียงเป็นข้อความ
AIDecide(…, *ai.DecideRequest)คำถามที่มีรูปแบบ (typed questions) ไปยังโมเดลการตัดสินใจ (ใช่หรือไม่, การเลือก, คะแนน) เกี่ยวกับข้อความ
AIConnectionsForUser(userID, channelID, teamID)การเชื่อมต่อที่ผู้ใช้สามารถใช้ได้ที่นั่น พร้อมโมเดลและขีดความสามารถ โดยไม่รวมคีย์

ConnectionID ของแต่ละคำขอจะใช้สำหรับเลือกการเชื่อมต่อ และ Model จะเลือกหนึ่งในโมเดลของมัน (เว้นว่างเพื่อใช้ค่าเริ่มต้นของการเชื่อมต่อ) Operation เป็นป้ายกำกับสั้นๆ สำหรับบันทึกการใช้งานและการตรวจสอบ (audit) โดยไม่ใช่เนื้อหา เวอร์ชันขั้นต่ำของเซิร์ฟเวอร์คือ 11.11

รับ actingUserID เฉพาะจากแหล่งข้อมูลที่เซิร์ฟเวอร์ตรวจสอบแล้วเท่านั้น: หัวข้อ Mattermost-User-Id ของคำขอไปยัง ServeHTTP ของคุณ หรือผู้ใช้ในบริบทของ hook (ผู้เขียนโพสต์, ผู้ที่รันคำสั่ง) ห้ามรับจากข้อความ, payload ของ webhook, URL หรือคำขอจากแอปอื่น: แอปของคุณจะสิ้นเปลืองสิทธิ์การเข้าถึงของผู้อื่น

เมธอดเหล่านี้และแพ็กเกจ ai (server/public/ai) มีอยู่ในสำเนาของ Antimatter เท่านั้น ไม่ใช่ในเวอร์ชันที่เผยแพร่โดย Mattermost เพื่อใช้งานสิ่งเหล่านี้ ให้ build โดยอ้างอิง server/public ของ Antimatter โดยใช้บรรทัด replace ใน go.mod ของคุณ เช่น replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public แอปเช่นนี้จะทำงานบน Antimatter เท่านั้น

การเก็บข้อมูล

แต่ละแอปมี key-value store เป็นของตัวเองในฐานข้อมูลของเซิร์ฟเวอร์:

  • KVSet(key, value), KVGet(key), KVDelete(key): ค่าเป็น bytes โดยปกติจะเป็น JSON คีย์ยาวได้ไม่เกิน 150 ตัวอักษร
  • KVSetWithExpiry(key, value, seconds): ค่าจะหายไปหลังจากเวลาผ่านไปเท่านั้น
  • KVCompareAndSet(key, old, new) และ KVSetWithOptions ที่ใช้ Atomic: การอัปเดตที่ปลอดภัยเมื่อมีคำขอหรือเซิร์ฟเวอร์หลายตัวเขียนคีย์เดียวกัน Polls ใช้วิธีนี้บันทึกคะแนนโหวตทุกครั้ง และลองใหม่เมื่อเกิดการชนกัน
  • KVList(page, perPage): คีย์ทีละหน้า

ส่วนแอปเว็บ

bundle จะลงทะเบียนคลาสกับ window.registerPlugin โดย initialize จะได้รับ registry และ 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());
  • ไลบรารีที่ใช้ร่วมกัน. แอปเว็บจัดเตรียม React, ReactDOM, Redux, React Redux และ React Intl ให้: ระบุรายการเหล่านี้เป็น webpack externals (React, ReactDOM, Redux, ReactRedux, ReactIntl) แทนที่จะรวมสำเนาของตัวเองใน bundle ดู webpack.config.js ของ Polls
  • การเรียกส่วนเซิร์ฟเวอร์ของคุณ. ใช้ Fetch กับ ${window.basename}/plugins/<app id>/… พร้อมตัวเลือกจาก Client4.getOptions() (จาก mattermost-redux/client): ตัวเลือกเหล่านี้จะนำ header ที่เซิร์ฟเวอร์ต้องการเพื่อรับคำขอจากเบราว์เซอร์ไปด้วย
  • สถานะของคุณเอง. registerReducer เพิ่ม reducer ซึ่งจะถูกเก็บไว้ภายใต้ state['plugins-<app id>']
  • ประเภท. วิธีการของ registry เพิ่มเติมของ Antimatter ไม่มีอยู่ใน types ที่ Mattermost เผยแพร่: Polls ประกาศ registry ที่ใช้ไว้ใน webapp/src/types/host.ts

วิธีการของ registry และพารามิเตอร์ของมันได้รับการบันทึกไว้ในโค้ด ที่ webapp/fusion/src/plugins/registry.ts (Fusion) และ webapp/channels/src/plugins/registry.ts (classic)

การรองรับอินเทอร์เฟซ Fusion

Antimatter มีอินเทอร์เฟซเว็บสองแบบ: Fusion ซึ่งเป็นค่าเริ่มต้น และแบบ classic ทั้งสองแบบโหลด bundle ของแอปเดียวกันและมี registry เดียวกัน แต่ 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 การโทรและช่องเสียงจะละเว้นแถบด้านข้างและองค์ประกอบ toast ที่ Fusion แทนที่ด้วยอินเทอร์เฟซการโทรของตัวเอง แอปเอเจนต์จะวางการกระทำของ AI ไว้ในเมนูข้อความ เป็นกลุ่มภายใต้ การดำเนินการเพิ่มเติม แทนที่จะเป็นแถบเครื่องมือเมื่อชี้เมาส์ คลาส CSS ของ Fusion เอง (ที่มีคำนำหน้า am-) และเลเยอร์กล่องโต้ตอบ (องค์ประกอบ am-layer) คือสิ่งที่แอปเหล่านี้ใช้เพื่อให้อินเทอร์เฟซเข้ากัน; พวกมันไม่ใช่ API ที่ได้รับการบันทึกไว้และมีความเสถียร

ตำแหน่งที่ Fusion แสดงองค์ประกอบของปลั๊กอิน

วิธีการลงทะเบียนตำแหน่งที่ปรากฏใน Fusion
registerAppBarComponent, registerChannelHeaderButtonActionรางแอป (app rail) คอลัมน์ของไอคอนที่ขอบด้านขวา ระหว่างโปรไฟล์ของคุณกับปุ่ม เกี่ยวกับ 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กล่องค้นหาในหัวของช่อง: ขอบเขตของแอปจะปรากฏเมื่อบoxยังไม่มีข้อมู
registerUserGuideDropdownMenuActionเกี่ยวกับ Antimatter (ส่วนล่างของแถบด้านข้างของแอป) ใต้ ความช่่วยเหลื่อ
registerProductหน้าเต็มที่ URL ของผลิตภัณฑ์ ซึ่งแสดงรายการภายใต้ ผลิตภัณฑ์ ใน เกี่ยวกับ Antimatter หน้าผลิตภัณฑ์ยังคงใช้กรอบหน้าแบบคลาสสิก ซึ่งเมนูผลิตภัณฑ์จะแสดงเฉพาะเมื่อแอปลงทะเบียนผลิตภัณฑ์หรือเพิ่มรายการเข้าไป แอป Kanban และ Playbooks ที่รวมมาไม่ได้อีกต่อไปลงทะเบียนผลิตภัณฑ์: หน้าของพวกมันเปิดในแผงด้านข้าง
registerRootComponent, registerGlobalComponent, hooks เชน registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslationsทำงานเหมือนในอินเทอร์เฟซแบบคลาสสิก
registerAdminConsolePlugin, registerAdminConsoleCustomSettingคอนโซลระบบ ท่ีเป็นแบบคลาสสิกในอินเทอร์เฟซทั้งสอง

สิ่่งท่ี Fusion ไม่แสดงหรือไม่รัน

  • registerPostDropdownMenuComponent (คอมโพเนนต์ในเมนูของข้อความ), registerPostCardTypeComponent (การด์ของข้อความในแผงด้านขวา) และ registerProductSwitcherMenuItem ไม่แสดงในมุมมองหลัของ Fusion ใช้ registerPostDropdownMenuAction แทนตัวแรก
  • registerMessageWillBeUpdatedHook ไม่รันเมื่อข้อความถูกแก้ไขใน Fusion ตรวจสอบการแก้ไขบนเซิร์ฟเวอร์ด้วย hook MessageWillBeUpdated แทน
  • registerActionAfterChannelCreation รันเฉพาะจากไดอะล็อกสร้างช่องแบบคลาสสิก ท่ี Fusion เปิดจาก ตัวเลือกเพิ่มเติม… ในไดอะล็อก สร้างช่อง ของมัน
  • คอมโพเนนต์ของแถบแอปจะแสดงไม่ว่า supportedProductIds จะเป็นอะไร
  • เส้นทางจาก registerNeedsTeamRoute และ registerCustomRoute คงกรอบหน้าแบบคลาสสิกไว้ เชนผลิตภัณฑ์

Hooks ท่ี Antimatter มีเพีง

เมธอดใน registry เหล่านีไ่ม่มีใน 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 จะมาก่อนแท็บ Emoji องค์ประกอบนี้จะรับ 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: รายการในหน้า อินทิเกรชัน อยู่ถัดจากอิโมจิแบบกำหนดเอง แอป GIF จะเพิ่ม GIF และสติกเกอร์แบบกำหนดเองของมันไว้ที่นั่น

เพิ่มและจัดการมุมมองในตัวเลือกอิโมจิ

ใน Fusion แท็บตัวเลือกอิโมจิจะรับ prop ชื่อ pickerKit: ซึ่งประกอบด้วยคอมโพเนนต์ที่แท็บอิโมจิใช้สำหรับมุมมองการเพิ่มและจัดการ เพื่อให้มุมมองของแท็บมีลักษณะเหมือนกันและยังคงอยู่ภายในตัวเลือก GIFs และ Memes apps ใช้สิ่งนี้เพื่อเพิ่มและจัดการ GIF, สติกเกอร์ และเทมเพลตมีม ซึ่งประกอบด้วย version (1), hooks usePickerView และ useOnEscape, และคอมโพเนนต์ PickerViewButtons (ปุ่ม + และปุ่มจัดการข้างกล่องค้นหา), PickerViewFrame (มุมมองพร้อมลูกศรย้อนกลับ), PickerNotice, PickerField, PickerSearch, PickerDropZone และ PickerManageList การกดปุ่ม Escape จะย้อนกลับจากมุมมองไปยังกริดของแท็บ ตรวจสอบว่า prop นี้มีอยู่: host อื่น ๆ อาจไม่รวมไว้ และแท็บจะยังคงใช้ไดอะล็อกของตัวเองในกรณีนั้น ประเภท (types) อยู่ใน webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps) และคอมโพเนนต์อยู่ใน webapp/fusion/src/fusion/popovers/picker_views.tsx.

แผงในหน้าต่างใหม่

Fusion ให้คอมโพเนนต์ของ registerRightHandSidebarComponent รับ props เพิ่มอีก 2 ตัว เพื่อให้ เปิดในหน้าต่างใหม่ เปิดแผงในตำแหน่งที่มันอยู่แทนที่จะอยู่ที่หน้าหลักของแอป:

  • setPanelState(state): เรียกใช้เมื่อแผงเปลี่ยนตำแหน่ง โดยส่งสตริงสั้น ๆ ที่แอปของคุณกำหนด เช่น 'note/<id>' หรือ 'board/<id>/card/<id>' หรือ null เมื่ออยู่ที่หน้าหลัก ข้อมูลนี้จะถูกใส่ในที่อยู่ของหน้าต่าง ดังนั้นอย่าใส่ข้อมูลลับไว้ในนั้น สถานะ (state) ที่ยาวเกิน 512 ตัวอักษร หรือมี control characters จะถูกตัดทิ้ง
  • initialPanelState: ในหน้าต่างใหม่ จะเป็นสถานะที่แผงเคยมี หรือ undefined ให้ถือว่าเป็นข้อมูลที่ไม่สามารถเชื่อถือได้: ดึงข้อมูลรายการใหม่อีกครั้งในฐานะผู้ใช้ที่เข้าสู่ระบบ และแสดงหน้าหลักของแอปหรือมุมมองไม่พบรายการ (not-found view) หากผู้ใช้ไม่สามารถเปิดรายการนั้นได้

สถานะ (state) จะถูกลืมเมื่อแผงปิดลง หรือแผงของแอปอื่นเข้ามาแทนที่ หากต้องการเปิดหน้าต่างของแผงด้วยตนเอง popoutRhsPlugin (ซึ่งอยู่ใน window.WebappUtils.popouts ด้วย) จะรับ state เป็นอาร์กิวเมนต์ที่ 5 (ไม่บังคับ) Notes, Whiteboard, Todo, 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 พร้อม ID และข้อความภาษาอังกฤษ: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> หรือ defineMessages เพิ่มคำนำหน้า ID ด้วยชื่อแอปของคุณ
  2. ดึงข้อความภาษาอังกฤษออกมาไว้ใน webapp/i18n/en.json (แอปที่รวมมาจะใช้ formatjs extract)
  3. เพิ่มไฟล์หนึ่งไฟล์ต่อภาษาไว้ข้างเคียง โดยใช้ ID เดียวกัน: 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) และให้ localizer ในภาษาของผู้ใช้แต่ละคน

สร้าง ติดตั้ง และดีบัก

คุณต้องมี 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 debugger เข้ากับส่วนเซิร์ฟเวอร์ที่กำลังทำงาน (สร้างด้วย AM_DEBUG=1 ก่อน)

make deploy และคำสั่งเซิร์ฟเวอร์อื่น ๆ ใช้ local mode socket เมื่อพบ (/var/tmp/antimatter_local.socket หรือ AM_LOCALSOCKETPATH): รันบนเครื่องของเซิร์ฟเวอร์โดยเปิด local mode ไว้ มิฉะนั้นจะเข้าสู่ระบบไปยังเซิร์ฟเวอร์ที่ระบุโดย MM_SERVICESETTINGS_SITEURL ด้วย AM_ADMIN_TOKEN หรือ AM_ADMIN_USERNAME และ AM_ADMIN_PASSWORD เครื่องมือสร้างยังไม่อ่านชื่อ AM_ สำหรับ URL ของไซต์

ติดตั้งบันเดิลด้วยตนเอง

  • ผู้ดูแลระบบอนุญาตการอัปโหลดด้วย 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 ของคุณ