Skip to content

Commit d52b8ba

Browse files
committed
Improve Slack App Home pinned spaces UX
1 parent 2dc2040 commit d52b8ba

3 files changed

Lines changed: 299 additions & 38 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ In Slack app settings:
103103

104104
Set `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, `SLACK_SIGNING_SECRET`, and `MUMBL_SLACK_TOKEN_ENCRYPTION_KEY` in the deployment environment. Install through `/api/slack/install`.
105105

106-
`/mumbl room platform team` creates a Mumbl room from Slack and returns a one-time creator handoff link for opening the room in a browser. `/mumbl start platform team` remains an alias. Slack-created rooms are auto-pinned for the creator when Mumbl can match their Slack email to a Mumbl login. `/mumbl pin platform-team` explicitly adds a Mumbl room to that Slack user's publish list without tracking room membership. App Home can draft, review, edit, and publish private field notes to pinned Mumbl spaces. Optional team-read Slack posting is creator-enabled per room. If a creator switches it on, Mumbl starts an optional Slack permission upgrade that asks for `chat:write`, `groups:write`, and `groups:read` so it can create one private channel, post published team reads there, and auto-pin that Mumbl room when a connected user joins the Mumbl-created Slack channel. It still does not request Slack history scopes.
106+
`/mumbl room platform team` creates a Mumbl room from Slack and returns a one-time creator handoff link for opening the room in a browser. `/mumbl start platform team` remains an alias. Slack-created rooms are auto-pinned for the creator when Mumbl can match their Slack email to a Mumbl login. `/mumbl pin platform-team` explicitly adds a Mumbl room to that Slack user's publish list without tracking room membership. App Home can draft, review, edit, publish private field notes to pinned Mumbl spaces, and manage personal pinned spaces. Optional team-read Slack posting is creator-enabled per room. If a creator switches it on, Mumbl starts an optional Slack permission upgrade that asks for `chat:write`, `groups:write`, and `groups:read` so it can create one private channel, post published team reads there, and auto-pin that Mumbl room when a connected user joins the Mumbl-created Slack channel. It still does not request Slack history scopes.
107107

108108
Slack reminders are intentionally not part of the beta because frequent scheduling does not fit the current free-tier posture.
109109

app/api/slack/interactions/route.js

Lines changed: 137 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
slackFieldNoteDraftPickerView,
2020
slackConnectPayload,
2121
slackConnectUrl,
22+
slackDumpModalView,
2223
slackDumpConnectModalView,
2324
slackDumpSavedModalView,
2425
slackFieldNoteEditModalView,
@@ -30,13 +31,17 @@ import {
3031
slackFieldNoteReviewPickerView,
3132
slackSavedDumpPayload,
3233
slackLoadingModalView,
34+
slackPinnedSpacesView,
3335
slackPublishOptionsView,
3436
slackPublishPreviewView,
3537
slackRoomModalView,
38+
slackUnpinPinnedSpaceConfirmView,
3639
openSlackDumpModal,
3740
openSlackRoomModal,
41+
publishSlackAppHome,
3842
updateSlackView,
3943
updateSlackFieldNoteDraft,
44+
unpinSlackPinnedSpace,
4045
} from "../../../../src/server/slack";
4146
import { cleanString } from "../../../../src/server/validation";
4247

@@ -67,10 +72,16 @@ export async function POST(request) {
6772
}
6873
}
6974
if (actionId === "new_private_dump") {
70-
await openSlackDumpModal({
71-
teamId: slackTeamId(payload),
72-
triggerId: cleanString(payload.trigger_id, 200),
73-
});
75+
const teamId = slackTeamId(payload);
76+
const viewId = cleanString(payload.view?.id, 120);
77+
if (viewId) {
78+
await updateSlackView({ teamId, viewId, view: slackDumpModalView() });
79+
} else {
80+
await openSlackDumpModal({
81+
teamId,
82+
triggerId: cleanString(payload.trigger_id, 200),
83+
});
84+
}
7485
}
7586
if (actionId === "draft_team_read") {
7687
const result = await openSlackLoadingModal({
@@ -95,26 +106,102 @@ export async function POST(request) {
95106
});
96107
}
97108
if (actionId === "review_field_note_drafts") {
98-
const result = await openSlackLoadingModal({
99-
teamId: slackTeamId(payload),
100-
triggerId: cleanString(payload.trigger_id, 200),
101-
title: "review drafts",
102-
message: "loading your private drafts...",
103-
});
104-
after(async () => {
105-
try {
106-
await updateSlackView({
107-
teamId: slackTeamId(payload),
108-
viewId: cleanString(result.view?.id, 120),
109-
view: await slackFieldNoteReviewPickerView({
110-
teamId: slackTeamId(payload),
111-
slackUserId: cleanString(payload.user?.id, 80),
112-
}),
113-
});
114-
} catch (error) {
115-
console.error("Slack draft review update failed", { message: error.message, slackError: error.slack?.error });
116-
}
117-
});
109+
const teamId = slackTeamId(payload);
110+
const slackUserId = cleanString(payload.user?.id, 80);
111+
const viewId = cleanString(payload.view?.id, 120);
112+
if (viewId) {
113+
await updateSlackView({
114+
teamId,
115+
viewId,
116+
view: slackLoadingModalView({ title: "review drafts", message: "loading your private drafts..." }),
117+
});
118+
after(async () => {
119+
try {
120+
await updateSlackView({
121+
teamId,
122+
viewId,
123+
view: await slackFieldNoteReviewPickerView({ teamId, slackUserId }),
124+
});
125+
} catch (error) {
126+
console.error("Slack draft review update failed", { message: error.message, slackError: error.slack?.error });
127+
}
128+
});
129+
} else {
130+
const result = await openSlackLoadingModal({
131+
teamId,
132+
triggerId: cleanString(payload.trigger_id, 200),
133+
title: "review drafts",
134+
message: "loading your private drafts...",
135+
});
136+
after(async () => {
137+
try {
138+
await updateSlackView({
139+
teamId,
140+
viewId: cleanString(result.view?.id, 120),
141+
view: await slackFieldNoteReviewPickerView({ teamId, slackUserId }),
142+
});
143+
} catch (error) {
144+
console.error("Slack draft review update failed", { message: error.message, slackError: error.slack?.error });
145+
}
146+
});
147+
}
148+
}
149+
if (actionId === "manage_pinned_spaces") {
150+
const teamId = slackTeamId(payload);
151+
const slackUserId = cleanString(payload.user?.id, 80);
152+
const viewId = cleanString(payload.view?.id, 120);
153+
if (viewId) {
154+
await updateSlackView({
155+
teamId,
156+
viewId,
157+
view: slackLoadingModalView({ title: "pinned spaces", message: "loading your pinned teamspaces..." }),
158+
});
159+
after(async () => {
160+
try {
161+
await updateSlackView({
162+
teamId,
163+
viewId,
164+
view: await slackPinnedSpacesView({ teamId, slackUserId }),
165+
});
166+
} catch (error) {
167+
console.error("Slack pinned spaces update failed", { message: error.message, slackError: error.slack?.error });
168+
}
169+
});
170+
} else {
171+
const result = await openSlackLoadingModal({
172+
teamId,
173+
triggerId: cleanString(payload.trigger_id, 200),
174+
title: "pinned spaces",
175+
message: "loading your pinned teamspaces...",
176+
});
177+
after(async () => {
178+
try {
179+
await updateSlackView({
180+
teamId,
181+
viewId: cleanString(result.view?.id, 120),
182+
view: await slackPinnedSpacesView({ teamId, slackUserId }),
183+
});
184+
} catch (error) {
185+
console.error("Slack pinned spaces update failed", { message: error.message, slackError: error.slack?.error });
186+
}
187+
});
188+
}
189+
}
190+
if (actionId === "unpin_pinned_space_start") {
191+
const teamId = slackTeamId(payload);
192+
const slackUserId = cleanString(payload.user?.id, 80);
193+
const viewId = cleanString(payload.view?.id, 120);
194+
if (viewId) {
195+
await updateSlackView({
196+
teamId,
197+
viewId,
198+
view: await slackUnpinPinnedSpaceConfirmView({
199+
teamId,
200+
slackUserId,
201+
pinId: cleanString(payload.actions?.[0]?.value, 64),
202+
}),
203+
});
204+
}
118205
}
119206
if (actionId === "publish_field_note_start") {
120207
const teamId = slackTeamId(payload);
@@ -322,12 +409,38 @@ export async function POST(request) {
322409
isAnonymous: metadata.isAnonymous !== false,
323410
displayName: metadata.displayName,
324411
});
412+
after(async () => {
413+
try {
414+
await publishSlackAppHome({ teamId, slackUserId });
415+
} catch (error) {
416+
console.error("Slack App Home refresh after publish failed", { message: error.message, slackError: error.slack?.error });
417+
}
418+
});
325419
return ok({ response_action: "update", view: slackFieldNotePublishedModalView(result) });
326420
} catch (error) {
327421
return ok({ response_action: "update", view: slackFieldNoteDraftErrorModalView(error.message || "couldn't publish that draft.") });
328422
}
329423
}
330424

425+
if (payload.type === "view_submission" && payload.view?.callback_id === "unpin_pinned_space_confirm") {
426+
const teamId = slackTeamId(payload);
427+
const slackUserId = cleanString(payload.user?.id, 80);
428+
const metadata = parseViewMetadata(payload.view?.private_metadata);
429+
try {
430+
await unpinSlackPinnedSpace({ teamId, slackUserId, pinId: metadata.pinId });
431+
after(async () => {
432+
try {
433+
await publishSlackAppHome({ teamId, slackUserId });
434+
} catch (error) {
435+
console.error("Slack App Home refresh after unpin failed", { message: error.message, slackError: error.slack?.error });
436+
}
437+
});
438+
return ok({ response_action: "update", view: await slackPinnedSpacesView({ teamId, slackUserId }) });
439+
} catch (error) {
440+
return ok({ response_action: "update", view: slackFieldNoteDraftErrorModalView(error.message || "couldn't unpin that space.") });
441+
}
442+
}
443+
331444
if (payload.type === "view_submission" && payload.view?.callback_id === "create_mumbl_room") {
332445
const teamId = slackTeamId(payload);
333446
const slackUserId = cleanString(payload.user?.id, 80);

0 commit comments

Comments
 (0)