Skip to content
Open
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
324 changes: 324 additions & 0 deletions .impeccable/design.json

Large diffs are not rendered by default.

426 changes: 426 additions & 0 deletions DESIGN.md

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions PRODUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Product

## Register

product

## Users

Enterprise knowledge workers who keep Rocket.Chat open all day, often in the background. Many work inside regulated or self-hosted environments (government, healthcare, finance, on-prem IT). A meaningful subset are power users juggling multiple Rocket.Chat workspaces (work + community + customer tenants) and depend on the desktop client for fast cross-server switching, OS-level notifications, and native integrations the browser cannot offer.

Primary context: long-session use on a work machine, alongside other native apps. Users alt-tab in and out; they do not study the chrome, they reach for it.

## Product Purpose

Rocket.Chat Desktop is the native shell around one or more Rocket.Chat server webviews. Its job is to make managing multiple workspaces fast: server list switching, notification badges, deep links, certificate handling, screen sharing, downloads, tray presence, auto-update. The webview is the chat product; the desktop client is the connective tissue that makes running several at once feel like one tool instead of several browser tabs.

Success looks like: users forget the shell exists during normal use, and reach for it instinctively the moment they need to switch servers, recover from a stuck webview, or configure something the browser cannot do.

## Brand Personality

Reliable, professional, focused. Voice is plain and unornamented: short labels, no marketing flourishes, no playful microcopy. The interface should feel like a tool an IT admin would trust on a regulated network, while remaining warm enough for end users who live in it eight hours a day. Confident, not loud. Capable, not clever.

## Anti-references

- **Discord / gamer chat aesthetics.** Neon accents, drenched dark purple, playful empty states, animated reactions in chrome. Wrong register for enterprise and public sector.
- **Old Skype / legacy Teams chrome.** Heavy gradients, layered drop shadows, busy toolbars, decorative iconography in titlebars. Dated, visually noisy, undermines trust.
- **Generic SaaS marketing patterns inside the app.** Gradient hero numbers, identical icon-and-title card grids, "modern dashboard" aesthetics. The desktop shell is not a marketing surface.
- **Pure brand minimalism (Linear-empty for its own sake).** Multi-server power users need information density in the sidebar. Sparse-for-sparseness-sake hides the value of the shell.

## Design Principles

1. **Shell disappears, servers shine.** Desktop chrome must not compete with the webview. Sidebar, titlebar, and dialogs exist to switch, notify, and configure, then recede. If a chrome element draws the eye during normal chat use, it is wrong.

2. **Density without clutter.** Multi-workspace power users need many servers and signals visible at once. Earn every pixel: tight rhythm, considered hierarchy, no decorative padding. Density and clarity are compatible; sparse is not automatically better.

3. **Native, not web-pretending.** Respect OS conventions per platform: macOS traffic lights and vibrancy, Windows titlebar buttons and Mica, Linux menubar fallbacks. The desktop edge over the browser IS the native feel. Cross-platform consistency matters less than per-platform correctness.

4. **Trust through restraint.** Enterprise, government, and healthcare users need confidence. No surprises, no flourishes, no theatrical motion. Predictable beats clever. Errors are quiet and actionable, not dramatic.

5. **Fuselage first, custom last.** Visual coherence with the Rocket.Chat web app is a feature for users who move between web and desktop. Use `@rocket.chat/fuselage` primitives by default. Introduce custom components only where the desktop shell has no web equivalent (server sidebar, native dialogs, platform-specific affordances).

## Accessibility & Inclusion

Target: **WCAG 2.2 AA**, with attention to extras commonly required by Rocket.Chat's public sector and healthcare deployments.

- Full keyboard navigation across the shell (sidebar, menus, dialogs). No mouse-only affordances.
- Screen reader labels on every interactive shell element, including server sidebar entries and notification badges.
- Honor `prefers-reduced-motion` for all shell motion (sidebar transitions, dialog enters, badge animations).
- Honor system-level high contrast and forced colors on Windows.
- Contrast meets 2.2 AA against the active theme; verify against both light and dark.
- Do not rely on color alone to convey state (unread, error, connecting). Always pair with shape, icon, or text.
- Color-vision-deficient safe accent choices when introducing new status colors.
4 changes: 2 additions & 2 deletions electron-builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,12 @@
"executableName": "rocketchat-desktop",
"category": "GNOME;GTK;Network;InstantMessaging",
"desktop": {
"MimeType": "x-scheme-handler/rocketchat;x-scheme-handler/callto;x-scheme-handler/tel;",
"entry": {
"Name": "Rocket.Chat",
"Comment": "Official Rocket.Chat Desktop Client",
"GenericName": "Rocket.Chat",
"Categories": "GNOME;GTK;Network;InstantMessaging"
"Categories": "GNOME;GTK;Network;InstantMessaging",
"MimeType": "x-scheme-handler/rocketchat;x-scheme-handler/callto;x-scheme-handler/tel;"
}
},
"artifactName": "rocketchat-${version}-${os}-${arch}.${ext}"
Expand Down
21 changes: 20 additions & 1 deletion src/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@
},
"downloads": {
"title": "Downloads",
"back": "Back",
"empty": {
"noDownloads": "No downloads yet.",
"noDownloadsDescription": "Files downloaded from your Rocket.Chat workspaces appear here.",
"noResults": "No downloads match these filters.",
"noResultsAction": "Clear filters"
},
"notifications": {
"downloadFinished": "Download Finished",
"downloadInterrupted": "Download Interrupted",
Expand Down Expand Up @@ -218,14 +225,20 @@
"remove": "Remove from list",
"resume": "Resume",
"retry": "Retry",
"showInFolder": "Show in Folder"
"showInFolder": "Show in folder"
},
"showingResults": "Showing results {{first}} - {{last}} of {{count}}"
},
"certificatesManager": {
"title": "Certificates manager",
"trustedCertificates": "Trusted certificates",
"trustedHint": "Servers whose certificates you accepted when connecting. Remove an entry to be prompted again on the next connection.",
"notTrustedCertificates": "Not trusted certificates",
"notTrustedHint": "Servers whose certificates were flagged and not stored. Remove an entry to clear the rejection.",
"empty": {
"trusted": "No trusted certificates yet.",
"notTrusted": "No certificates have been flagged."
},
"item": {
"domain": "Domain",
"actions": "Actions",
Expand All @@ -234,10 +247,16 @@
},
"settings": {
"title": "Settings",
"back": "Back",
"general": "General",
"certificates": "Certificates",
"developer": "Developer",
"sections": {
"notifications": "Notifications",
"performance": "Performance",
"callsAndVideo": "Calls & video",
"windowAndAppearance": "Window & appearance",
"integrations": "Integrations",
"logging": "Logging"
},
"options": {
Expand Down
12 changes: 4 additions & 8 deletions src/ui/components/CertificatesManager/ActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { Box } from '@rocket.chat/fuselage';
import type { AllHTMLAttributes } from 'react';
import { Button } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';

type ActionButtonProps = AllHTMLAttributes<HTMLAnchorElement>;
type ActionButtonProps = ComponentProps<typeof Button>;

const ActionButton = (props: ActionButtonProps) => (
<>
<Box marginInline={4} withRichContent>
<a href='#' {...props} />
</Box>
</>
<Button small danger {...props} />
);

export default ActionButton;
9 changes: 5 additions & 4 deletions src/ui/components/CertificatesManager/CertificateItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Icon, TableCell, TableRow } from '@rocket.chat/fuselage';
import { Box, TableCell, TableRow } from '@rocket.chat/fuselage';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';

Expand All @@ -17,10 +17,11 @@ const CertificateItem = ({ url }: CertificateItemProps) => {
}, [url]);

return (
<TableRow key={url}>
<TableRow key={url} action>
<TableCell>
<Icon name='key' size='x16' />
{url}
<Box withTruncatedText title={url}>
{url}
</Box>
</TableCell>
<TableCell align='end'>
<ActionButton onClick={handleRemove}>
Expand Down
60 changes: 60 additions & 0 deletions src/ui/components/CertificatesManager/CertificateSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
Box,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
} from '@rocket.chat/fuselage';
import { useTranslation } from 'react-i18next';

import CertificateItem from './CertificateItem';

export type CertificateSectionProps = {
title: string;
hint: string;
emptyText: string;
urls: string[];
isFirst?: boolean;
};

export const CertificateSection = ({
title,
hint,
emptyText,
urls,
isFirst,
}: CertificateSectionProps) => {
const { t } = useTranslation();
return (
<Box mbs={isFirst ? 0 : 32}>
<Box fontScale='h4' color='font-default' mbe={8}>
{title}
</Box>
<Box fontScale='c1' color='font-hint' mbe={16}>
{hint}
</Box>
{urls.length === 0 ? (
<Box fontScale='p2' color='font-annotation' pb={16}>
{emptyText}
</Box>
) : (
<Table fixed>
<TableHead>
<TableRow>
<TableCell>{t('certificatesManager.item.domain')}</TableCell>
<TableCell align='end' width='x120'>
{t('certificatesManager.item.actions')}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{urls.map((url) => (
<CertificateItem key={url} url={url} />
))}
</TableBody>
</Table>
)}
</Box>
);
};
64 changes: 17 additions & 47 deletions src/ui/components/CertificatesManager/CertificatesManager.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import {
Label,
Box,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
} from '@rocket.chat/fuselage';
import { Box } from '@rocket.chat/fuselage';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';

import type { RootState } from '../../../store/rootReducer';
import CertificateItem from './CertificateItem';
import { CertificateSection } from './CertificateSection';

export const CertificatesManager = () => {
const trustedCertificates = useSelector(
Expand All @@ -23,44 +15,22 @@ export const CertificatesManager = () => {
);

const { t } = useTranslation();

return (
<Box is='form' padding={24} flexGrow={1} flexShrink={1}>
<Box flexGrow={1} flexShrink={1} paddingBlock={8}>
<Label>{t('certificatesManager.trustedCertificates')}</Label>
<Table sticky striped fixed>
<TableHead>
<TableRow>
<TableCell>{t('certificatesManager.item.domain')}</TableCell>
<TableCell align='end'>
{t('certificatesManager.item.actions')}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.keys(trustedCertificates).map((url) => (
<CertificateItem key={url} url={url} />
))}
</TableBody>
</Table>
</Box>
<Box marginBlockStart={50} flexGrow={1} flexShrink={1} paddingBlock={8}>
<Label>{t('certificatesManager.notTrustedCertificates')}</Label>
<Table sticky striped fixed>
<TableHead>
<TableRow>
<TableCell>{t('certificatesManager.item.domain')}</TableCell>
<TableCell align='end'>
{t('certificatesManager.item.actions')}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.keys(notTrustedCertificates).map((url) => (
<CertificateItem key={url} url={url} />
))}
</TableBody>
</Table>
</Box>
<Box flexGrow={1} flexShrink={1}>
<CertificateSection
isFirst
title={t('certificatesManager.trustedCertificates')}
hint={t('certificatesManager.trustedHint')}
emptyText={t('certificatesManager.empty.trusted')}
urls={Object.keys(trustedCertificates)}
/>
<CertificateSection
title={t('certificatesManager.notTrustedCertificates')}
hint={t('certificatesManager.notTrustedHint')}
emptyText={t('certificatesManager.empty.notTrusted')}
urls={Object.keys(notTrustedCertificates)}
/>
</Box>
);
};
12 changes: 4 additions & 8 deletions src/ui/components/DownloadsManagerView/ActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { Box } from '@rocket.chat/fuselage';
import type { AllHTMLAttributes } from 'react';
import { Button } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';

type ActionButtonProps = AllHTMLAttributes<HTMLAnchorElement>;
type ActionButtonProps = ComponentProps<typeof Button>;

const ActionButton = (props: ActionButtonProps) => (
<>
<Box marginInline={4} withRichContent>
<a href='#' {...props} />
</Box>
</>
<Button small secondary {...props} />
);

export default ActionButton;
Loading
Loading