chore: publish pearcord-ui-flow from pearcord workspace

This commit is contained in:
Pearcord
2026-05-21 23:43:48 -04:00
commit c5cff3c990
5 changed files with 688 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
package-lock.json
+5
View File
@@ -0,0 +1,5 @@
# pearcord-ui-flow
Single source of truth for **desktop UI → sidecar IPC** (`dispatchUiMessage`) plus Bare smoke scenarios.
See [UI_FLOW.md](../../docs/UI_FLOW.md).
+566
View File
@@ -0,0 +1,566 @@
'use strict'
const b4a = require('b4a')
const { USER_STATUS } = require('pearcord-platform')
function fromBase64 (str) {
if (typeof Buffer !== 'undefined') return Buffer.from(str || '', 'base64')
return b4a.from(str || '', 'base64')
}
function normalizeEmojiImage (image) {
if (!image?.dataBase64) return image
return {
data: fromBase64(image.dataBase64),
filename: image.filename,
mimeType: image.mimeType
}
}
/**
* Dispatch one UI → sidecar IPC message (same contract as apps/pearcord/index.js).
* @param {import('pearcord-platform').PearcordPlatform} platform
* @param {object} msg parsed JSON line from pear-pipe
* @param {{ send?: (obj: object) => void, pushState?: () => void }} hooks
* @returns {Promise<boolean>} true when type was handled
*/
async function dispatchUiMessage (platform, msg, hooks = {}) {
const send = hooks.send || (() => {})
const pushState = hooks.pushState || (() => {})
const t = msg?.type
if (!t) return false
if (t === 'register') {
await platform.registerUser({
username: msg.username,
displayName: msg.displayName || msg.username
})
pushState()
return true
}
if (t === 'create-guild') {
await platform.createGuild({ name: msg.name || 'My Server' })
pushState()
return true
}
if (t === 'join-invite') {
await platform.joinInvite(msg.code)
pushState()
return true
}
if (t === 'join-public-discovery') {
await platform.joinPublicDiscovery(msg.inviteCode)
pushState()
return true
}
if (t === 'set-discovery-listing') {
await platform.setGuildDiscoveryListing(msg.guildId, msg.listed !== false)
pushState()
return true
}
if (t === 'update-discovery-filter') {
platform.setDiscoveryFilter({
query: msg.query,
sort: msg.sort,
minMembers: msg.minMembers,
tag: msg.tag
})
pushState()
return true
}
if (t === 'set-discovery-tags') {
await platform.setGuildDiscoveryTags(msg.guildId, msg.tags)
pushState()
return true
}
if (t === 'set-automod-config') {
await platform.setAutomodConfig(msg.config || {})
pushState()
return true
}
if (t === 'set-announcement-crosspost') {
await platform.setAnnouncementCrosspostTargets(
msg.channelId,
msg.targetChannelIds || []
)
pushState()
return true
}
if (t === 'set-announcement-hub-publish') {
await platform.setAnnouncementHubPublisher(msg.channelId, !!msg.enabled)
pushState()
return true
}
if (t === 'subscribe-remote-announcement-hub') {
await platform.subscribeRemoteAnnouncementHub({
sourceGuildId: msg.sourceGuildId,
sourceChannelId: msg.sourceChannelId,
targetChannelId: msg.targetChannelId
})
pushState()
return true
}
if (t === 'follow-announcement') {
await platform.followAnnouncementChannel(msg.channelId)
pushState()
return true
}
if (t === 'unfollow-announcement') {
await platform.unfollowAnnouncementChannel(msg.channelId)
pushState()
return true
}
if (t === 'add-guild-boost') {
await platform.addGuildBoost(msg.amount ?? 1)
pushState()
return true
}
if (t === 'contribute-guild-boost') {
await platform.contributeGuildBoost(msg.amount ?? 1)
pushState()
return true
}
if (t === 'set-guild-boost-banner') {
await platform.setGuildBoostBannerColor(msg.bannerColor)
pushState()
return true
}
if (t === 'voice-capture-chunk') {
platform.ingestVoiceCapture(msg.data)
return true
}
if (t === 'display-capture-frame') {
platform.ingestDisplayCapture({
data: msg.data,
mimeType: msg.mimeType,
width: msg.width,
height: msg.height
})
return true
}
if (t === 'create-invite') {
const inv = await platform.createInvite()
send({ type: 'invite', invite: inv })
pushState()
return true
}
if (t === 'select-guild') {
await platform.loadGuild(msg.guildId)
pushState()
return true
}
if (t === 'select-channel') {
await platform.selectChannel(msg.channelId)
pushState()
return true
}
if (t === 'join-voice') {
await platform.joinVoiceChannel(msg.channelId)
pushState()
return true
}
if (t === 'leave-voice') {
await platform.leaveVoiceChannel()
pushState()
return true
}
if (t === 'start-screen-share') {
await platform.startScreenShare(msg.label)
pushState()
return true
}
if (t === 'stop-screen-share') {
await platform.stopScreenShare()
pushState()
return true
}
if (t === 'set-voice-mute') {
await platform.setVoiceMute({ muted: msg.muted, deafened: msg.deafened })
pushState()
return true
}
if (t === 'register-slash-commands') {
await platform.registerSlashCommands(msg.commands || [])
pushState()
return true
}
if (t === 'create-bot-install-token') {
const out = await platform.createBotInstallToken({
name: msg.name,
intents: msg.intents,
permissions: msg.permissions,
botPublicKey: msg.botPublicKey,
ttlMs: msg.ttlMs
})
send({ type: 'bot-install-token', token: out.token, bot: out.bot, expiresAt: out.expiresAt })
pushState()
return true
}
if (t === 'install-bot-token') {
await platform.installBotFromToken(msg.token)
pushState()
return true
}
if (t === 'send-message') {
await platform.sendMessage(msg.content, {
replyToId: msg.replyToId,
attachmentIds: msg.attachmentIds,
stickerNames: msg.stickerNames
})
pushState()
return true
}
if (t === 'stage-attachment') {
const att = await platform.stageAttachment({
data: fromBase64(msg.dataBase64),
filename: msg.filename,
mimeType: msg.mimeType
})
send({ type: 'attachment-staged', attachment: att })
pushState()
return true
}
if (t === 'update-guild-settings') {
await platform.updateGuildSettings({ name: msg.name, iconHash: msg.iconHash })
pushState()
return true
}
if (t === 'set-presence') {
const custom = msg.customStatus !== undefined ? msg.customStatus : undefined
platform.setPresence(msg.status || USER_STATUS.ONLINE, custom)
pushState()
return true
}
if (t === 'edit-message') {
await platform.editMessage(msg.messageId, msg.content)
pushState()
return true
}
if (t === 'delete-message') {
await platform.deleteMessage(msg.messageId)
pushState()
return true
}
if (t === 'create-thread') {
await platform.createThread({ messageId: msg.messageId, name: msg.name })
pushState()
return true
}
if (t === 'create-forum-post') {
await platform.createForumPost({
title: msg.title,
content: msg.content,
tags: msg.tags
})
pushState()
return true
}
if (t === 'set-forum-channel-palette') {
await platform.setForumChannelPalette(msg.channelId, msg.tags)
pushState()
return true
}
if (t === 'update-forum-tag-filter') {
platform.setForumTagFilter(msg.tag)
pushState()
return true
}
if (t === 'archive-thread') {
await platform.archiveThread(msg.threadId, msg.archived !== false)
pushState()
return true
}
if (t === 'create-category') {
await platform.createCategory({ name: msg.name })
pushState()
return true
}
if (t === 'create-channel') {
await platform.createChannel({
name: msg.name,
type: msg.channelType,
parentId: msg.parentId
})
pushState()
return true
}
if (t === 'toggle-reaction') {
await platform.toggleReaction(msg.messageId, msg.emoji)
pushState()
return true
}
if (t === 'select-home') {
await platform.openHome()
pushState()
return true
}
if (t === 'send-friend-request') {
await platform.sendFriendRequest({
peerUserId: msg.peerUserId,
peerDisplayName: msg.peerDisplayName
})
pushState()
return true
}
if (t === 'accept-friend-request') {
await platform.acceptFriendRequest(msg.peerUserId)
pushState()
return true
}
if (t === 'decline-friend-request') {
await platform.declineFriendRequest(msg.peerUserId)
pushState()
return true
}
if (t === 'remove-contact') {
await platform.removeContact(msg.peerUserId)
pushState()
return true
}
if (t === 'block-contact') {
await platform.blockContact(msg.peerUserId)
pushState()
return true
}
if (t === 'unblock-contact') {
await platform.unblockContact(msg.peerUserId)
pushState()
return true
}
if (t === 'request-attachment-preview') {
const preview = await platform.readAttachmentPreview(msg.attachmentId)
send({ type: 'attachment-preview', preview })
return true
}
if (t === 'upsert-guild-emoji') {
await platform.upsertGuildEmoji({
name: msg.name,
glyph: msg.glyph,
description: msg.description,
attachmentId: msg.attachmentId,
packName: msg.packName,
image: normalizeEmojiImage(msg.image)
})
pushState()
return true
}
if (t === 'upsert-emoji-pack') {
await platform.upsertEmojiPack({
name: msg.name,
displayName: msg.displayName,
description: msg.description,
sortOrder: msg.sortOrder,
coverEmojiName: msg.coverEmojiName,
emojiNames: msg.emojiNames
})
pushState()
return true
}
if (t === 'remove-emoji-pack') {
await platform.removeEmojiPack(msg.name)
pushState()
return true
}
if (t === 'upsert-guild-sticker') {
await platform.upsertGuildSticker({
name: msg.name,
description: msg.description,
attachmentId: msg.attachmentId,
packName: msg.packName,
image: normalizeEmojiImage(msg.image)
})
pushState()
return true
}
if (t === 'upsert-sticker-pack') {
await platform.upsertStickerPack({
name: msg.name,
displayName: msg.displayName,
description: msg.description,
sortOrder: msg.sortOrder,
coverStickerName: msg.coverStickerName,
stickerNames: msg.stickerNames
})
pushState()
return true
}
if (t === 'remove-sticker-pack') {
await platform.removeStickerPack(msg.name)
pushState()
return true
}
if (t === 'request-emoji-image') {
const preview = await platform.readEmojiImage(msg.name || msg.attachmentId)
send({ type: 'emoji-image', preview, emojiName: msg.name || null })
return true
}
if (t === 'stage-guild-emoji-image') {
const att = await platform.stageGuildEmojiImage({
data: fromBase64(msg.dataBase64),
filename: msg.filename,
mimeType: msg.mimeType
})
send({ type: 'emoji-image-staged', attachment: att })
pushState()
return true
}
if (t === 'update-search') {
platform.setSearch({ query: msg.query, scope: msg.scope })
pushState()
return true
}
if (t === 'open-dm') {
await platform.openDM({
peerUserId: msg.peerUserId,
peerDisplayName: msg.peerDisplayName || msg.peerUserId?.slice(0, 8)
})
pushState()
return true
}
if (t === 'create-group-dm') {
await platform.createGroupDM({
peerUserIds: msg.peerUserIds || [],
name: msg.name
})
pushState()
return true
}
if (t === 'open-group-dm') {
await platform.openGroupDM({ channelId: msg.channelId })
pushState()
return true
}
if (t === 'typing-start') {
platform.signalTyping()
return true
}
if (t === 'update-profile') {
await platform.updateProfile({ displayName: msg.displayName })
pushState()
return true
}
if (t === 'update-profile-cosmetics') {
await platform.updateProfileCosmetics({
bannerColor: msg.bannerColor,
avatarRing: msg.avatarRing,
accentColor: msg.accentColor,
profileBio: msg.profileBio
})
pushState()
return true
}
if (t === 'set-profile-banner-image') {
await platform.setProfileBannerImage({
data: fromBase64(msg.dataBase64),
filename: msg.filename,
mimeType: msg.mimeType
})
pushState()
return true
}
if (t === 'request-profile-banner-image') {
const preview = await platform.readProfileBannerPreview(msg.attachmentId)
send({ type: 'profile-banner-image', preview })
return true
}
if (t === 'request-peer-profile-banner-image') {
const preview = await platform.readPeerProfileBannerPreview(msg.userId)
send({ type: 'profile-banner-image', preview })
return true
}
if (t === 'pin-message') {
await platform.pinMessage(msg.messageId)
pushState()
return true
}
if (t === 'unpin-message') {
await platform.unpinMessage(msg.messageId)
pushState()
return true
}
if (t === 'ban-member') {
await platform.banMember({ userId: msg.userId, reason: msg.reason })
pushState()
return true
}
if (t === 'kick-member') {
await platform.kickMember({ userId: msg.userId, reason: msg.reason })
pushState()
return true
}
if (t === 'timeout-member') {
await platform.timeoutMember({
userId: msg.userId,
durationMinutes: msg.durationMinutes,
reason: msg.reason
})
pushState()
return true
}
if (t === 'set-slowmode') {
await platform.setChannelSlowmode(msg.channelId, msg.slowmodeSeconds)
pushState()
return true
}
if (t === 'set-member-role') {
await platform.setMemberRole(msg.userId, msg.role)
pushState()
return true
}
if (t === 'update-notification-prefs') {
await platform.updateNotificationPrefs(msg.prefs || {})
pushState()
return true
}
if (t === 'update-user-prefs') {
await platform.updateUserPrefs(msg.prefs || {})
pushState()
return true
}
if (t === 'mark-all-notifications-read') {
await platform.markAllNotificationsRead()
pushState()
return true
}
if (t === 'create-device-pair-code') {
const invite = await platform.createDevicePairCode({
deviceLabel: msg.deviceLabel,
ttlMs: msg.ttlMs
})
send({ type: 'device-pair-code', invite })
pushState()
return true
}
if (t === 'link-device-pair-code') {
await platform.linkDevicePairCode(msg.code, { deviceLabel: msg.deviceLabel })
pushState()
return true
}
if (t === 'set-step-up-pin') {
await platform.setStepUpPin(msg.pin)
pushState()
return true
}
if (t === 'verify-step-up-pin') {
await platform.verifyStepUpPin(msg.pin)
pushState()
return true
}
if (t === 'clear-step-up-pin') {
await platform.clearStepUpPin()
pushState()
return true
}
if (t === 'refresh') {
pushState()
return true
}
return false
}
module.exports = {
dispatchUiMessage,
fromBase64,
normalizeEmojiImage
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "pearcord-ui-flow",
"version": "0.1.0",
"main": "index.js",
"type": "commonjs",
"description": "Pearcord desktop UI IPC dispatch + automated flow scenarios for Bare smokes",
"dependencies": {
"b4a": "^1.6.7",
"pearcord-platform": "git+https://git.ssh.surf/pearcord/pearcord-platform.git#main"
}
}
+104
View File
@@ -0,0 +1,104 @@
'use strict'
const { dispatchUiMessage } = require('./index')
async function ipc (platform, msg) {
const ok = await dispatchUiMessage(platform, msg, {})
if (!ok) throw new Error(`unhandled IPC type: ${msg?.type}`)
}
/**
* Full desktop UI journey: onboarding, settings, navigation, invite join, restore.
*/
async function runFullUiFlowScenario (platform) {
await ipc(platform, {
type: 'register',
username: 'ui_flow_user',
displayName: 'Pearcord UI'
})
let view = await platform.view()
if (!view.onboarded) throw new Error('onboarded after register')
if (!view.needsGuild) throw new Error('needsGuild after register')
await ipc(platform, { type: 'create-guild', name: 'Pearcord HQ' })
view = await platform.view()
if (view.guild?.name !== 'Pearcord HQ') throw new Error('guild name')
if (view.needsGuild) throw new Error('needsGuild cleared')
if (view.mode !== 'guild') throw new Error('guild mode')
if (!view.activeChannelId) throw new Error('active channel')
const guildId = view.guild.id
const generalId = view.activeChannelId
await ipc(platform, { type: 'update-profile', displayName: 'UI Flow Tester' })
await ipc(platform, {
type: 'update-user-prefs',
prefs: { theme: 'amoled', density: 'compact', showTimestamps: true }
})
view = await platform.view()
if (view.user?.displayName !== 'UI Flow Tester') throw new Error('profile display name')
if (view.userPrefs?.theme !== 'amoled') throw new Error('user prefs theme')
await ipc(platform, { type: 'update-guild-settings', name: 'Pearcord HQ Renamed' })
await ipc(platform, {
type: 'set-discovery-tags',
guildId,
tags: ['gaming', 'p2p']
})
await ipc(platform, {
type: 'set-automod-config',
config: { enabled: true, maxMentions: 4, blockInvites: true }
})
await ipc(platform, { type: 'add-guild-boost', amount: 1 })
await ipc(platform, { type: 'set-presence', status: 'idle', customStatus: 'smoke testing' })
view = await platform.view()
if (view.guild?.name !== 'Pearcord HQ Renamed') throw new Error('guild renamed')
if (!view.automodConfig?.enabled) throw new Error('automod enabled')
if ((view.guildBoost?.boostCount || 0) < 1) throw new Error('guild boost')
if (view.presence?.self?.customStatus !== 'smoke testing') throw new Error('custom status')
await ipc(platform, { type: 'send-message', content: 'UI flow smoke message' })
await ipc(platform, { type: 'select-home' })
view = await platform.view()
if (view.mode !== 'home') throw new Error('home mode after select-home')
const peerId = 'a'.repeat(32)
await ipc(platform, {
type: 'open-dm',
peerUserId: peerId,
peerDisplayName: 'Peer'
})
view = await platform.view()
if (view.mode !== 'dm') throw new Error('dm mode')
await ipc(platform, { type: 'send-message', content: 'DM from UI flow' })
await ipc(platform, { type: 'select-guild', guildId })
view = await platform.view()
if (view.mode !== 'guild') throw new Error('back to guild')
await ipc(platform, { type: 'select-channel', channelId: generalId })
if (view.activeChannelId !== generalId) {
view = await platform.view()
if (view.activeChannelId !== generalId) throw new Error('channel reselected')
}
await ipc(platform, {
type: 'update-notification-prefs',
prefs: { desktopEnabled: true, notifyOnAllMessages: true }
})
view = await platform.view()
if (!view.notificationPrefs?.desktopEnabled) throw new Error('notification prefs')
if (!view.notificationPrefs?.notifyOnAllMessages) throw new Error('notification prefs patch')
const invite = await platform.createInvite()
return {
guildId,
generalId,
inviteCode: invite.shareCode || invite.code,
view
}
}
module.exports = {
runFullUiFlowScenario,
ipc
}