Skip to content

Add Huddle Conversation Topics With Stand-Up Workdays, Team Conversations and Direct Messages - #871

Open
Dharp02 wants to merge 6 commits into
mainfrom
feat/866-conversation-topics
Open

Dharp02 wants to merge 6 commits into
mainfrom
feat/866-conversation-topics

Conversation

@Dharp02

@Dharp02 Dharp02 commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Implements #866. Huddle posts now belong to a conversation topic with a permanent ID instead of a calendar date. A workday runs from one Start huddle to the next, so a 2 AM post still counts for the workday it was part of. Teams can also hold named team conversations and direct messages. The composer shows where every post goes, and warns before sending one that won't show in the thread being read.

Built in five commits, one per milestone of the implementation plan, plus one that removes Day. Each commit stands on its own.

What changed

Commit Milestone
6b74015d M1 · Conversation records and backfill. huddleConversations and huddleConversationLabels, plus conversationId on every post. An idempotent startup backfill maps old posts onto one closed "legacy" workday per team and day. It never invents a stand-up time.
0c215f88 M2 · Workdays. huddle.startWorkday closes the open workday and opens one for the starter's local date. createHuddlePost resolves and checks the conversation for every new post: the composer, plans, wrap-ups, Pulse, timer updates and scheduled delivery.
0750cfe1 M3 · Team and direct conversations. huddle.conversations.list/create/rename/setMyLabel. Private posts carry audienceIds, and every read filters on them: REST feeds, the live publication and its change stream, ticket views and notifications.
15232228 M4 · Inbox. Group by → Conversation, a Start huddle button, + to create a team conversation or direct message, and ⋯ to rename for everyone or just for me. All @mieweb/ui.
b00efb58 M5 · Composer. "To: Team › Conversation" next to the message box. A warning before sending a post that won't show where you're reading. A workday that closes mid-draft keeps the draft and says why.
9efffcd7 Day removed. Group by offers All, Conversation, Session, Person and Ticket. Huddle opens in Conversation. Old Day links and landings go to the matching conversation.

Every new Meteor method is exposed over REST in main.js, so the mobile app can call it.

Race conditions handled

These follow the plan's R1–R23: a unique index, insert-then-read-the-winner on a duplicate key, or a conditional update. No locks are needed.

  • Starting a huddle: two people starting at once, or a post landing mid-start, still end with one open workday ({ open: true } partial unique index).
  • A workday closing mid-draft: the composer always names its destination explicitly, so the server refuses a closed workday instead of quietly moving the post.
  • Server-side posts (Pulse, timer updates, clock-out recovery) are redirected to the open workday, since they have no draft to give back.
  • Scheduled posts get their workday in the same update that claims the delivery.
  • Direct messages: two people opening one at the same moment get a single conversation (unique pair key).
  • Private posts never leak to people outside the conversation, including in a live feed opened before the conversation existed.

Testing

  • Backend unit tests: meteor-backend/tests/conversations-core.test.ts, 32 tests.
  • Backend integration tests: conversations.test.ts and conversations-access.test.ts, 32 tests, including the concurrent races. Run several times in a row.
  • Frontend unit tests: grouping, empty threads, destination and warning rules, and the closed-workday error message. All 924 pass.
  • New browser tests in tests/e2e/huddle/conversations.spec.ts: the warning when sending from a closed workday, Cancel keeping the draft, and a workday closing mid-draft. The existing inbox-filters, clock-lines and mounting specs were updated for Conversation replacing Day.
  • Full npm run test:all: 452 of 468 browser tests passed.
    • The four inbox-filters failures came from this PR's warning blocking sends from an older Session thread. Fixed: Day and Session don't warn, because they already open the thread a post lands in (fix(huddle): posts made while clocked in join the live session; Session shows shifts only #854). Those specs pass on rerun.
    • Still failing, not caused by this PR: tickets/video-upload.spec.ts › "a browser WebM is attached and served as an MP4". The server stores video/webm. This branch doesn't touch uploads, PulseVault or tickets.
    • Already failing on main: huddle-post-rest.test.ts › "stores each supported format…".
  • Checked by hand on the dev server, desktop and phone width: Start huddle, creating a direct message and posting in it, rename for me, the send warning (Cancel, and "Send and open it"), and a workday closing mid-draft.

Decisions for the reviewer

  • Day is gone; Conversation is the default (9efffcd7). Day and Conversation showed nearly the same threads. A saved Day choice falls back to Conversation, an old day:YYYY-MM-DD link opens that date's workday, and a post sent from Session while clocked out opens its conversation (huddle.createPost now returns conversationId). The 'day' grouping still exists inside superChatFeed.ts, where about 30 unit tests use it as a fixture; removing it there is a follow-up.
  • Workday dates use the starter's local date. There's no team timezone in the codebase. dateKey is checked against POST_DATE_RE, must be within a day of server time, and can't be earlier than the open workday.
  • Who can start a huddle: any team member. Rename for everyone: the creator or a team admin. No open workday when someone posts: one opens automatically.

Not in this PR

  • Comments, likes, edits and deletes on a private post still only check team membership, so someone with the post's id could act on it. This needs a small follow-up requirePostAccess check.
  • A teammate sees a workday someone else started once the first post in it arrives, not the moment it starts.
  • The new controls haven't been tested keyboard-only. The send warning's Cancel was tested with text, not attachments.
  • Prototype-only UI ideas from the issue discussion are deliberately left out.

🤖 Generated with Claude Code

Adds huddleConversations and huddleConversationLabels with their indexes,
and a stable conversationId on every Huddle post.

- conversations-core.js: pure rules (access, open/closed, names, direct-pair key)
- conversations.js: indexes, legacy workdays, and an idempotent startup backfill
  that maps existing posts onto one closed workday per team and day, with no
  invented stand-up time
- New posts (composer, Pulse, wrap-up, timer updates) join the legacy workday
  for their date; a scheduled post gets its conversation in the same update
  that delivers it
- The feed returns conversationId; nothing changes on screen yet
… M2)

- huddle.startWorkday({ teamId, dateKey }) closes the open workday and opens
  one for the starter's local date; starting again the same day is a no-op.
  dateKey must be a real date within a day of the server's and not before
  the open workday's, so a device with the wrong date can't close today's
- openWorkday is the one way a workday opens: an { open: true } partial
  unique index keeps one open per team, and a duplicate-key loser returns
  the winner, so concurrent starts and posts never make two (R1-R3)
- createHuddlePost resolves and checks every post's conversation: on the
  team, visible to the author, open. The composer is refused a closed
  workday (conversation-closed) so its draft stays; server-side posts
  (Pulse, timer updates, wrap-up recovery) go to the open workday instead
- Scheduled posts bound for a workday join the one open at delivery,
  written in the same update that claims the delivery
- A legacy day for the same date is reopened rather than duplicated
- huddle.startWorkday and createPost's conversationId exposed over REST
…#866 M3)

- huddle.conversations.list/create/rename/setMyLabel, each over REST too.
  A direct conversation is unique per pair (insert, then read the winner on
  a duplicate key); a team conversation has a shared name its creator or a
  team admin can change once; anyone can set a name only they see
- Cindy sees her direct conversation with Bob as "Bob" and Bob sees it as
  "Cindy": one record, names worked out per reader in two queries
- A post in a team or direct conversation carries its members as
  audienceIds, and every read filters on the post itself: getPosts,
  getMyPosts, getPostsByTicket, the huddlePosts.byTeam publication and its
  change stream. A conversation made after a page loaded can't leak (R21)
- Notifications for a private post (followers, @team/@here, @mentions,
  mention retries) reach its members only
…nversations (#866 M4)

- Group by gets Conversation: one thread per workday, team conversation and
  direct message, titled with the name the reader sees. A workday past
  midnight shows day dividers. Open topics with no posts yet (a workday just
  started, a direct message just made) get an empty thread to post from
- useConversationTopics loads the team's topics and refetches when a post
  names one it doesn't know (R22); conversationThreadId/topicIdOfThread map
  between thread and topic ids
- Start huddle, a + to create a team conversation or direct message, and a
  conversation menu to rename for everyone or for me, all @mieweb/ui
- A post sent from a conversation thread goes into that conversation; a
  scheduled one bound for a workday is placed at delivery
- Day stays the default grouping until #866's open question is decided
- Release note in 1.2.0
… won't show (#866 M5)

- "To: Dev Team › Thu, Nov 12" beside the message box names the team and
  conversation every post goes to; tapping it opens that conversation
- Every send names its conversation explicitly (R5): a workday that closed
  while the post was written is refused, the draft stays, and the message
  says the next huddle started; the topics reload so the new workday shows
- Before sending a post that won't show in the thread being read (a closed
  workday or another conversation, another person's or ticket's thread, or
  a search it won't match), an AlertDialog says why; Cancel keeps the draft,
  "Send and open it" posts and opens where it went. Day and Session need no
  warning: they already open the thread a post lands in (#854)
- e2e: tests/e2e/huddle/conversations.spec.ts (closed workday warning, Cancel,
  workday closing mid-draft)
- Release note
Copilot AI balanced review requested due to automatic review settings November 12, 2026 23:13
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

🚀 Preview Deployment Ready

Service URL
App https://mieweb-timehuddle-feat-866-conversation-topics.os.mieweb.org
API (Meteor) https://mieweb-timehuddle-feat-866-conversation-topics-api.os.mieweb.org

Preview auto-deletes when this PR is closed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved issues can expose private posts to former team members, publish private replies team-wide, and create future-dated workdays that block legitimate huddles.

9 open issues
What changed in this PR

Adds persistent conversation topics to Huddle, replacing date-only post associations with stand-up-based workdays, named team conversations, and direct messages while retaining existing inbox groupings.

Changes:

  • Adds conversation storage, legacy-post backfill, workday lifecycle, access filtering, and REST methods.
  • Adds conversation controls, personal labels, explicit composer destinations, and pre-send warnings.
  • Adds backend, frontend, and browser coverage plus release notes.
File Description
tests/​e2e/​huddle/​inbox-filters.spec.ts Adds the Conversation grouping expectation.
tests/​e2e/​huddle/​helpers.ts Supports selecting Conversation grouping.
tests/​e2e/​huddle/​conversations.spec.ts Tests send warnings and draft retention.
src/​pages/​Huddle.tsx Integrates topics, controls, destinations, and warnings.
src/​lib/​api.ts Adds conversation types and API methods.
src/​features/​huddle/​useConversationTopics.ts Loads and refreshes topic metadata.
src/​features/​huddle/​superChatFeed.ts Adds topic grouping and destination checks.
src/​features/​huddle/​superChatFeed.test.ts Tests grouping and warning rules.
src/​features/​huddle/​ConversationControls.tsx Adds workday, creation, and rename controls.
src/​features/​huddle/​composerErrors.ts Explains closed-workday send failures.
src/​features/​huddle/​composerErrors.test.ts Tests the closed-workday message.
release-notes/​1.2.0.md Documents the new conversation features.
meteor-backend/​tests/​conversations.test.ts Tests workdays, posting, races, and delivery.
meteor-backend/​tests/​conversations-core.test.ts Tests conversation rules and validation.
meteor-backend/​tests/​conversations-access.test.ts Tests private access, labels, and notifications.
meteor-backend/​server/​timer-updates.js Assigns timer updates to workdays.
meteor-backend/​server/​main.js Exposes conversation methods over REST.
meteor-backend/​server/​huddle.js Assigns post destinations and filters private audiences.
meteor-backend/​server/​huddle-delivery.js Resolves scheduled destinations and notification audiences.
meteor-backend/​server/​conversations.js Implements storage, lifecycle, labels, and backfill.
meteor-backend/​server/​conversations-core.js Defines access, naming, audience, and date rules.

Review effort: Balanced

export async function currentWorkday(teamId, { postDate, at = new Date() } = {}) {
const open = await findOpenWorkday(teamId);
if (open) return open;
const dateKey = typeof postDate === 'string' && DATE_KEY_RE.test(postDate) ? postDate : utcDateKey(at);
Comment on lines +268 to +271
const conversation = await resolvePostConversation(pending.userId, team, {
conversationId: pending.conversationId,
postDate: pending.postDate,
});
.find({
ticketIds: ticketId,
...PUBLISHED,
$and: [visibleTo(identity.userId), audienceFilter(identity.userId)],
Comment thread src/pages/Huddle.tsx
// rather than the post moving silently (#866 R5). A scheduled post bound
// for a workday names none: its workday is the one open when it is
// delivered.
const destination = postDestination(conversationId, topics);
...base,
label: null,
memberIds: [userId, others[0]],
directKey: directKey(userId, others[0]),
if (row.kind !== 'group') {
throw new Meteor.Error('bad-request', 'Only a team conversation has a shared name; set your own name instead');
}
if (row.createdBy !== userId && !(team.admins ?? []).includes(userId)) {
Comment on lines +288 to +289
if (kind === 'ticket' && !view.ticketIds.includes(key))
reasons.push(DESTINATION_TEXT.otherTicket);
Comment on lines +57 to +61
const unknown = posts.find(
(p) =>
p.conversationId && !known.has(p.conversationId) && !asked.current.has(p.conversationId),
);
if (!unknown?.conversationId) return;
Comment thread src/pages/Huddle.tsx Outdated
Comment on lines +1254 to +1257
if (
threadBy === 'conversation' &&
postTopicId &&
conversationThreadId(postTopicId) !== conversationId
Day and Conversation showed nearly the same threads, so Day goes: Group by
offers All, Conversation, Session, Person and Ticket, and Huddle opens in
Conversation. A saved Day choice falls back to it.

- An old ?conversation=day:YYYY-MM-DD link opens the workday for that date
- A link to an off-the-clock post opens the conversation that holds it, and
  a post sent from Session while clocked out opens its conversation:
  huddle.createPost now returns the conversationId it used
- A post without a conversation (only before the backfill reaches it) waits
  in "Earlier updates" under a key that doesn't move when the topic list
  loads; only a not-yet-delivered scheduled post shows in the open workday
- e2e: Day assertions moved to Conversation; the scroll test seeds one past
  workday per date (seedWorkdayPost)
- Release note

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved privacy flaws can publish private replies to the whole team and allow removed team members to retrieve private posts.

11 open issues
1 previously missed issue

In code that hasn't changed since last review

Medium severity Check search visibility using inbox grouping logic

src/​features/​huddle/​superChatFeed.ts:300

This check warns that a search hides the new post even when the inbox keeps it visible: searching a conversation's title retains the whole thread, including replies that do not repeat that title. All also searches ticket, date and media fields omitted here. Evaluate the prospective post or thread with the same grouping-specific search logic as the inbox, supplying its conversation and post metadata.

Review effort: Balanced

Comment thread src/pages/Huddle.tsx
// rather than the post moving silently (#866 R5). A scheduled post bound
// for a workday names none: its workday is the one open when it is
// delivered.
const destination = postDestination(conversationId, topics);
Comment thread src/pages/Huddle.tsx
Comment on lines +1255 to +1256
if (threadBy === 'conversation' && landedThread && landedThread !== conversationId) {
openConversation(landedThread);

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants