@@ -322,7 +322,21 @@ export async function createSlackStartedSpace({ teamId, slackUserId, name }) {
322322 }
323323
324324 const handoff = await createCreatorHandoff ( { spaceId : insertedSpace . id , creatorToken, accessToken } ) ;
325- const teamReadsSetup = await createTeamReadsSetup ( { spaceId : insertedSpace . id } ) ;
325+
326+ // The room already exists at this point. If building the optional team-reads
327+ // install link fails (e.g. missing Slack OAuth env), don't fail the whole
328+ // creation — the user already has a working room. Just omit the button.
329+ let teamReadsUrl = null ;
330+ try {
331+ const teamReadsSetup = await createTeamReadsSetup ( { spaceId : insertedSpace . id } ) ;
332+ teamReadsUrl = slackTeamReadsInstallUrl ( teamReadsSetup ) ;
333+ } catch ( error ) {
334+ console . error ( "Slack team reads setup link failed after room creation" , {
335+ spaceId : insertedSpace . id ,
336+ message : error . message ,
337+ } ) ;
338+ }
339+
326340 return {
327341 space : insertedSpace ,
328342 creatorToken,
@@ -331,7 +345,7 @@ export async function createSlackStartedSpace({ teamId, slackUserId, name }) {
331345 pinned : Boolean ( connection ) ,
332346 openUrl : slackSpaceHandoffUrl ( handoff ) ,
333347 roomUrl : slackRoomReadsUrl ( insertedSpace , accessToken ) ,
334- teamReadsUrl : slackTeamReadsInstallUrl ( teamReadsSetup ) ,
348+ teamReadsUrl,
335349 } ;
336350}
337351
@@ -805,6 +819,17 @@ async function findSlackConnectionForAutoPin({ teamId, slackUserId }) {
805819 }
806820}
807821
822+ // Resolve a connection for joining/pinning a room. If the Slack user already
823+ // has (or can be matched to) a mumbl account, use that real connection so the
824+ // pin is attributed to it. Otherwise synthesize a Slack-identity-only stand-in:
825+ // pins are keyed on (slack_team_id, slack_user_id), so this is enough to pin and
826+ // invite in one step. `mumbl_user_id` stays null and backfills on connect.
827+ async function resolveSlackConnectionForJoin ( { teamId, slackUserId } ) {
828+ const connection = await findSlackConnectionForAutoPin ( { teamId, slackUserId } ) ;
829+ if ( connection ) return connection ;
830+ return { slack_team_id : teamId , slack_user_id : slackUserId , mumbl_user_id : null } ;
831+ }
832+
808833export async function connectSlackUser ( { teamId, slackUserId, mumblUserId } ) {
809834 const supabase = getSupabaseAdmin ( ) ;
810835 const sessionTokenHash = hashToken ( `slack:${ teamId } :${ slackUserId } ` ) ;
@@ -824,9 +849,24 @@ export async function connectSlackUser({ teamId, slackUserId, mumblUserId }) {
824849 . single ( ) ;
825850 if ( error ) throw error ;
826851 await reconcileSlackStartedSpacesForConnection ( data ) ;
852+ await backfillPinnedSpacesUserId ( data ) ;
827853 return data ;
828854}
829855
856+ // When a Slack user connects their mumbl account, attribute any rooms they
857+ // joined earlier (pinned with a null mumbl_user_id) to that account.
858+ async function backfillPinnedSpacesUserId ( connection ) {
859+ if ( ! connection ?. mumbl_user_id || ! connection . slack_team_id || ! connection . slack_user_id ) return ;
860+ const supabase = getSupabaseAdmin ( ) ;
861+ const { error } = await supabase
862+ . from ( "slack_pinned_spaces" )
863+ . update ( { mumbl_user_id : connection . mumbl_user_id } )
864+ . eq ( "slack_team_id" , connection . slack_team_id )
865+ . eq ( "slack_user_id" , connection . slack_user_id )
866+ . is ( "mumbl_user_id" , null ) ;
867+ if ( error ) throw error ;
868+ }
869+
830870export async function saveSlackDump ( { connection, content, sourceMeta = { } } ) {
831871 const supabase = getSupabaseAdmin ( ) ;
832872 const cleanedContent = cleanString ( content , 4000 ) ;
@@ -1001,7 +1041,7 @@ export async function updateSlackFieldNoteDraft({ teamId, slackUserId, fieldNote
10011041}
10021042
10031043export async function pinSlackSpaceBySlug ( { teamId, slackUserId, slug } ) {
1004- const connection = await findOrCreateSlackConnectionByEmail ( { teamId, slackUserId } ) ;
1044+ const connection = await resolveSlackConnectionForJoin ( { teamId, slackUserId } ) ;
10051045 const space = await findSpaceForSlackPin ( slug ) ;
10061046 const pin = await pinSlackSpace ( { connection, spaceId : space . id } ) ;
10071047 const channelJoin = await inviteSlackUserToSpaceChannel ( { teamId, slackUserId, spaceId : space . id } ) ;
@@ -1592,7 +1632,7 @@ export function slackRoomCreatedPayload({ space, openUrl, roomUrl, teamReadsUrl,
15921632 blocks : [
15931633 section ( `*${ escapeSlackText ( space . name ) } is ready.*\n${ status } ` ) ,
15941634 actions ( [
1595- { text : "create team reads channel" , url : teamReadsUrl , style : "primary" } ,
1635+ ... ( teamReadsUrl ? [ { text : "create team reads channel" , url : teamReadsUrl , style : "primary" } ] : [ ] ) ,
15961636 { text : creatorLinked ? "open team reads" : "claim room" , url : openUrl } ,
15971637 { text : "share with team" , actionId : "share_room_invite" , value : JSON . stringify ( { roomUrl, spaceName : space . name } ) } ,
15981638 ] ) ,
@@ -1617,7 +1657,7 @@ export function slackRoomCreatedModalView({ space, openUrl, roomUrl, teamReadsUr
16171657 : `*${ escapeSlackText ( space . name ) } is ready.*\nConnect once to claim creator access in Mumbl.` ,
16181658 ) ,
16191659 actions ( [
1620- { text : "create team reads channel" , url : teamReadsUrl , style : "primary" } ,
1660+ ... ( teamReadsUrl ? [ { text : "create team reads channel" , url : teamReadsUrl , style : "primary" } ] : [ ] ) ,
16211661 { text : creatorLinked ? "open team reads" : "claim room" , url : openUrl } ,
16221662 { text : "share with team" , actionId : "share_room_invite" , value : JSON . stringify ( { roomUrl, spaceName : space . name } ) } ,
16231663 ] ) ,
@@ -1633,7 +1673,7 @@ export function slackRoomCreatedModalView({ space, openUrl, roomUrl, teamReadsUr
16331673}
16341674
16351675export function slackShareRoomInviteModalView ( { roomUrl, spaceName } ) {
1636- const inviteMessage = `hey team — just set up a mumbl room for ${ spaceName || "us" } .\nwrite private work thoughts, publish as team reads when ready.\n\nto join, run this in Slack:\n${ slackJoinCommand ( roomUrl ) } ` ;
1676+ const inviteMessage = `📓 hey team — I set up a mumbl room for ${ spaceName || "us" } .\n\nmumbl is where we keep the half-formed work thoughts privately, then shape the good ones into team reads when they're ready. ✍️ \n\n👉 to join, copy this and run it in Slack:\n${ slackJoinCommand ( roomUrl ) } \n\nyou'll land in our reads channel right away and the room pins to your mumbl App Home. ✨ ` ;
16371677 return {
16381678 type : "modal" ,
16391679 callback_id : "share_room_invite" ,
@@ -1963,7 +2003,7 @@ async function slackApi(method, token, body) {
19632003}
19642004
19652005function channelNameForSpace ( space ) {
1966- return `mumbl- ${ slugify ( space ?. slug || space ?. name || "team-reads" ) } ` . slice ( 0 , 80 ) ;
2006+ return `${ slugify ( space ?. slug || space ?. name || "team-reads" ) } ` . slice ( 0 , 80 ) ;
19672007}
19682008
19692009function teamReadMessage ( { space, post, channel, roomAccessToken = "" } ) {
@@ -1987,12 +2027,13 @@ async function slackAppHomeBlocks({ teamId, slackUserId }) {
19872027 const { appUrl } = getServerEnv ( ) ;
19882028 const connection = await findSlackConnection ( { teamId, slackUserId } ) ;
19892029 const { patternGraphEnabled } = getServerEnv ( ) ;
1990- const [ pinnedSpaces , pendingPattern ] = connection
1991- ? await Promise . all ( [
1992- listSlackPinnedSpaces ( connection ) ,
1993- patternGraphEnabled ? findPendingPattern ( connection . mumbl_user_id ) : null ,
1994- ] )
1995- : [ [ ] , null ] ;
2030+ // Pins are keyed on Slack identity, so list them even before the user has a
2031+ // linked mumbl account — a teammate who just ran `/mumbl join` should see the
2032+ // room here without connecting first.
2033+ const [ pinnedSpaces , pendingPattern ] = await Promise . all ( [
2034+ listSlackPinnedSpaces ( connection || { slack_team_id : teamId , slack_user_id : slackUserId } ) ,
2035+ connection && patternGraphEnabled ? findPendingPattern ( connection . mumbl_user_id ) : null ,
2036+ ] ) ;
19962037 const topPinnedSpaces = pinnedSpaces . slice ( 0 , 5 ) ;
19972038 const pinnedList = topPinnedSpaces
19982039 . map ( ( pin ) => {
@@ -2313,6 +2354,25 @@ async function slackPinnedSpacesModal({ pinnedSpaces, notice = "" }) {
23132354 { text : "open team reads" , url : openReadsUrl } ,
23142355 { text : "publish a draft" , actionId : "review_field_note_drafts" } ,
23152356 ] ;
2357+
2358+ // Offer a one-step "create reads channel" link for spaces that don't have
2359+ // one yet. Building the install link is optional — skip it if it fails.
2360+ if ( ! channel ?. slack_channel_name && space . id ) {
2361+ try {
2362+ const teamReadsSetup = await createTeamReadsSetup ( { spaceId : space . id } ) ;
2363+ rowActions . push ( {
2364+ text : "create reads channel" ,
2365+ url : slackTeamReadsInstallUrl ( teamReadsSetup ) ,
2366+ style : "primary" ,
2367+ } ) ;
2368+ } catch ( error ) {
2369+ console . error ( "Slack team reads setup link failed in manage teamspaces" , {
2370+ spaceId : space . id ,
2371+ message : error . message ,
2372+ } ) ;
2373+ }
2374+ }
2375+
23162376 rowActions . push ( { text : "unpin" , actionId : "unpin_pinned_space_start" , value : pin . id , style : "danger" } ) ;
23172377
23182378 blocks . push (
0 commit comments