Skip to content

Commit 015eb84

Browse files
fix(consumers): replace Slack checkboxes with interactive toggle lists
Slack rejects a `checkboxes` element with more than 10 options as `invalid_blocks` (HTTP 404 on the response_url POST). The DAO list (11) and notification Settings (12 types) had both grown past that cap, so clicking "Manage DAOs" or "Settings" failed with "Sorry, there was an error loading...". Telegram was unaffected (no such limit). Replace both with per-item toggle buttons that save on click and re-render the message in place: - DAOs: a grid of toggle buttons (daoToggleList) — green + checkmark when tracked - Settings: a list of rows, each notification type with an On/Off button accessory A "Done" button collapses the list into a summary; for the DAO onboarding flow it still advances to the wallet step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a65535f commit 015eb84

4 files changed

Lines changed: 211 additions & 137 deletions

File tree

‎apps/consumers/src/services/bot/slack-bot.service.ts‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,15 @@ export class SlackBotService implements BotServiceInterface {
9696
}
9797
});
9898

99-
handlers.action('dao_checkboxes', async (ctx) => {
100-
await ctx.ack();
99+
handlers.action(/^dao_toggle_/, async (ctx) => {
100+
if (this.daoService) {
101+
const daoId = ctx.body.actions?.[0]?.value;
102+
if (daoId) {
103+
await this.daoService.toggle(ctx, daoId);
104+
} else {
105+
await ctx.ack();
106+
}
107+
}
101108
});
102109

103110

@@ -140,8 +147,15 @@ export class SlackBotService implements BotServiceInterface {
140147
}
141148
});
142149

143-
handlers.action('settings_checkboxes', async (ctx) => {
144-
await ctx.ack();
150+
handlers.action(/^settings_toggle_/, async (ctx) => {
151+
if (this.settingsService) {
152+
const typeId = ctx.body.actions?.[0]?.value;
153+
if (typeId) {
154+
await this.settingsService.toggle(ctx, typeId);
155+
} else {
156+
await ctx.ack();
157+
}
158+
}
145159
});
146160

147161
handlers.action('settings_confirm', async (ctx) => {

‎apps/consumers/src/services/dao/slack-dao.service.ts‎

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import { SlackCommandContext, SlackActionContext } from '../../interfaces/slack-
99

1010
export type SlackDAORequest = Pick<SlackActionContext, 'body' | 'session' | 'ack' | 'respond'>;
1111
import { slackMessages, replacePlaceholders } from '@notification-system/messages';
12-
import type { ViewStateSelectedOption } from '@slack/bolt';
1312
import {
14-
daoSelectionList,
13+
daoToggleList,
1514
errorMessage,
1615
daoEmptyState,
1716
daoListWithEdit
@@ -50,10 +49,10 @@ export class SlackDAOService extends BaseDAOService {
5049
const userPreferences = await this.getUserSubscriptions(fullUserId);
5150
const currentSelections = new Set(userPreferences);
5251

53-
const blocks = daoSelectionList(
52+
const blocks = daoToggleList(
5453
daos,
5554
currentSelections,
56-
'dao_checkboxes',
55+
'dao_toggle',
5756
'dao_confirm_subscribe',
5857
slackMessages.dao.subscribeInstructions
5958
);
@@ -121,64 +120,83 @@ export class SlackDAOService extends BaseDAOService {
121120
}
122121

123122
/**
124-
* Confirm DAO selection changes from checkboxes
123+
* Toggle a single DAO subscription on/off, then re-render the list in place.
124+
* Option 2 UI: each DAO is a button that saves immediately on click — there is
125+
* no batch "confirm" step for the selection itself.
125126
*/
126-
async confirm(context: SlackActionContext): Promise<void> {
127-
const channelId = context.body.channel?.id;
128-
const workspaceId = context.body.team?.id || context.body.user?.team_id;
127+
async toggle(context: SlackActionContext, daoId: string): Promise<void> {
128+
const channelId = context.body.channel?.id || context.body.channel_id;
129+
const workspaceId = context.body.team?.id || context.body.team_id || context.body.user?.team_id;
129130
const fullUserId = `${workspaceId}:${channelId}`;
130131

131132
try {
132133
await context.ack();
133134

134-
// Extract selected DAOs from checkbox state
135-
const state = context.body.state;
136-
if (typeof state === 'string') {
137-
throw new Error('Unexpected DialogAction state format');
135+
const normalized = daoId.toUpperCase();
136+
const daos = await this.fetchAvailableDAOs();
137+
const current = new Set(
138+
await this.subscriptionApi.getUserPreferences(fullUserId, this.getPlatformId(), daos.map(dao => dao.id))
139+
);
140+
141+
// Flip just the clicked DAO and persist that single change
142+
const willSubscribe = !current.has(normalized);
143+
await this.subscriptionApi.saveUserPreference(normalized, fullUserId, this.getPlatformId(), willSubscribe);
144+
if (willSubscribe) {
145+
current.add(normalized);
146+
} else {
147+
current.delete(normalized);
138148
}
139149

140-
const selectedOptions: ViewStateSelectedOption[] =
141-
state?.values?.dao_checkboxes_block?.dao_checkboxes?.selected_options || [];
142-
const selectedDAOs = new Set<string>(selectedOptions.map(opt => opt.value));
150+
if (context.respond) {
151+
await context.respond({
152+
blocks: daoToggleList(daos, current, 'dao_toggle', 'dao_confirm_subscribe', slackMessages.dao.subscribeInstructions),
153+
response_type: 'in_channel',
154+
replace_original: true
155+
});
156+
}
157+
} catch (error) {
158+
this.logger.error({ err: error, event: 'dao.toggle_failed' }, 'error toggling DAO subscription');
159+
if (context.respond) {
160+
await context.respond({
161+
text: slackMessages.dao.updateError,
162+
response_type: 'in_channel'
163+
});
164+
}
165+
}
166+
}
143167

144-
// Sync to the complete desired state (handles both adds and removes)
145-
await this.syncSubscriptionsToState(fullUserId, selectedDAOs);
168+
/**
169+
* "Done" button. Subscriptions were already saved per-click via toggle(), so
170+
* this just collapses the button list into a final summary. Onboarding advance
171+
* (the wallet step) is triggered by the action handler in slack-bot.service.
172+
*/
173+
async confirm(context: SlackActionContext): Promise<void> {
174+
const channelId = context.body.channel?.id || context.body.channel_id;
175+
const workspaceId = context.body.team?.id || context.body.team_id || context.body.user?.team_id;
176+
const fullUserId = `${workspaceId}:${channelId}`;
146177

147-
// Show confirmation message
148-
let successMessage: string;
149-
if (selectedDAOs.size === 0) {
150-
successMessage = slackMessages.dao.unsubscribeAllSuccess;
151-
} else {
152-
const daoList = this.formatDAOList(selectedDAOs);
153-
successMessage = replacePlaceholders(slackMessages.dao.subscribeSuccess, { daoList });
154-
}
178+
try {
179+
await context.ack();
180+
181+
const daos = await this.fetchAvailableDAOs();
182+
const current = await this.subscriptionApi.getUserPreferences(fullUserId, this.getPlatformId(), daos.map(dao => dao.id));
183+
184+
const summaryMessage = current.length === 0
185+
? slackMessages.dao.unsubscribeAllSuccess
186+
: replacePlaceholders(slackMessages.dao.subscribeSuccess, { daoList: this.formatDAOList(current) });
155187

156188
if (context.respond) {
157189
await context.respond({
158-
replace_original: false,
190+
replace_original: true,
159191
blocks: [
160-
{
161-
type: 'section',
162-
text: {
163-
type: 'mrkdwn',
164-
text: successMessage
165-
}
166-
},
167-
{
168-
type: 'context',
169-
elements: [
170-
{
171-
type: 'mrkdwn',
172-
text: slackMessages.dao.updateInstructions
173-
}
174-
]
175-
}
192+
{ type: 'section', text: { type: 'mrkdwn', text: summaryMessage } },
193+
{ type: 'context', elements: [{ type: 'mrkdwn', text: slackMessages.dao.updateInstructions }] }
176194
],
177195
response_type: 'in_channel'
178196
});
179197
}
180198
} catch (error) {
181-
this.logger.error({ err: error, event: 'dao.update_failed' }, 'error updating subscriptions');
199+
this.logger.error({ err: error, event: 'dao.confirm_failed' }, 'error finalizing DAO selection');
182200
if (context.respond) {
183201
await context.respond({
184202
replace_original: false,

0 commit comments

Comments
 (0)