Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/sweet-paws-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/models": patch
---

Fixes an issue where overlapping calendar events could cause the user status to stay busy indefinitely
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const UserAvatarWithStatus = () => {

const { status = !user ? 'online' : 'offline', username, avatarETag } = user || anon;

const effectiveStatus = presenceDisabled ? 'disabled' : status;

return (
<Box
position='relative'
Expand All @@ -44,7 +46,7 @@ const UserAvatarWithStatus = () => {
mie='neg-x2'
mbe='neg-x2'
>
<UserStatus small status={presenceDisabled ? 'disabled' : status} />
<UserStatus role='status' aria-label={effectiveStatus} small status={effectiveStatus} />
</Box>
</Box>
);
Expand Down
9 changes: 7 additions & 2 deletions apps/meteor/server/services/calendar/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,13 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
return;
}

if (user.status) {
await CalendarEvent.updateEvent(event._id, { previousStatus: user.status });
const overlappingEvents = await CalendarEvent.findOverlappingEvents(event._id, event.uid, event.startTime, event.endTime)
.sort({ startTime: -1 })
.toArray();
const previousStatus = overlappingEvents.at(0)?.previousStatus ?? user.status;

if (previousStatus) {
await CalendarEvent.updateEvent(event._id, { previousStatus });
}

await applyStatusChange({
Expand Down

This file was deleted.

13 changes: 0 additions & 13 deletions apps/meteor/server/services/calendar/statusEvents/index.ts

This file was deleted.

69 changes: 69 additions & 0 deletions apps/meteor/tests/e2e/calendar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { CalendarEventImportProps } from '@rocket.chat/rest-typings';

import { Users } from './fixtures/userStates';
import { test, expect, type BaseTest } from './utils/test';

test.use({ storageState: Users.admin.state });

// TODO: Make this test run on CI - it's slow and requires an open browser, run manually for now.
test.describe.skip('Calendar', () => {
test.beforeAll(async ({ api }) => {
expect((await api.post('/settings/Calendar_BusyStatus_Enabled', { value: true })).status()).toBe(200);
});

test.afterAll(async ({ api }) => {
expect((await api.post('/settings/Calendar_BusyStatus_Enabled', { value: false })).status()).toBe(200);
});

test.describe('Status changes', () => {
test.beforeEach(async ({ page, api }) => {
expect((await api.post('/users.setStatus', { status: 'away', message: '' })).status()).toBe(200);
await page.goto('/home');
});

test('Should change user status back to away when there are overlapping imported events', async ({ page, api }) => {
await expect(page.getByRole('button', { name: 'User menu' }).getByRole('status')).toHaveAccessibleName('away');

const event1 = await importCalendarEvent(api, { start: 1, end: 2 });
await importCalendarEvent(api, { start: 1.5, end: 2.5 });
const event3 = await importCalendarEvent(api, { start: 2.5, end: 3 });

await test.step('Should change status to busy', async () => {
await expect(page.getByRole('button', { name: 'User menu' }).getByRole('status')).toHaveAccessibleName('busy', {
timeout: event1.endTime.getTime() - Date.now(),
});
});

await test.step('Should change status to away again', async () => {
await expect(page.getByRole('button', { name: 'User menu' }).getByRole('status')).toHaveAccessibleName('away', {
timeout: event3.endTime.getTime() - Date.now() + 1000,
});
});
});
});
});

async function importCalendarEvent(api: BaseTest['api'], { now = Date.now(), start = 2, end = 4 } = {}) {
const startTime = new Date(now + 1000 * 60 * start);
const endTime = new Date(now + 1000 * 60 * end);

const apiResponse = await api.post('/calendar-events.import', {
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
subject: 'Test appointment',
description: 'Test appointment description',
meetingUrl: 'https://rocket.chat/',
busy: true,
externalId: `test-${start}-${end}-${now}`,
} satisfies CalendarEventImportProps);

expect(apiResponse.status()).toBe(200);

const { id } = await apiResponse.json();

if (typeof id !== 'string') {
throw new Error(`Expected id to be a string, but got ${typeof id}`);
}

return { startTime, endTime, now, id };
}
1 change: 1 addition & 0 deletions packages/models/src/models/CalendarEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export class CalendarEventRaw extends BaseRaw<ICalendarEvent> implements ICalend
return this.find({
_id: { $ne: eventId }, // Exclude current event
uid,
busy: { $ne: false },
$or: [
// Event starts during our event
{ startTime: { $gte: startTime, $lt: endTime } },
Expand Down
Loading