feat: Introduce Feature Preview page (#29698)

Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
pull/29749/head^2
Douglas Fabris 3 years ago committed by GitHub
parent 0113c62a5c
commit e846d873b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .changeset/fair-rivers-occur.md
  2. 2
      apps/meteor/app/reactions/client/init.js
  3. 2
      apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts
  4. 7
      apps/meteor/client/components/message/toolbox/Toolbox.tsx
  5. 21
      apps/meteor/client/hooks/useFeaturePreview.ts
  6. 44
      apps/meteor/client/hooks/useFeaturePreviewList.ts
  7. 25
      apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx
  8. 9
      apps/meteor/client/sidebar/header/index.tsx
  9. 22
      apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx
  10. 136
      apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx
  11. 9
      apps/meteor/client/views/account/routes.tsx
  12. 11
      apps/meteor/client/views/account/sidebarItems.tsx
  13. 8
      apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
  14. BIN
      apps/meteor/public/images/featurePreview/quick-reactions.png
  15. 4
      apps/meteor/server/settings/accounts.ts
  16. 12
      packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts
  17. 40
      yarn.lock

@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---
feat: Introduce Feature Preview page

@ -39,7 +39,7 @@ Meteor.startup(function () {
return true;
},
order: -2,
order: -3,
group: ['message', 'menu'],
});
});

@ -110,7 +110,7 @@ Meteor.startup(async function () {
return true;
},
order: -3,
order: -2,
group: ['message', 'menu'],
});

@ -10,6 +10,7 @@ import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/M
import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction';
import { sdk } from '../../../../app/utils/client/lib/SDKClient';
import { useEmojiPickerData } from '../../../contexts/EmojiPickerContext';
import { useFeaturePreview } from '../../../hooks/useFeaturePreview';
import EmojiElement from '../../../views/composer/EmojiPicker/EmojiElement';
import { useIsSelecting } from '../../../views/room/MessageList/contexts/SelectedMessagesContext';
import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoTranslate';
@ -47,9 +48,10 @@ type ToolboxProps = {
const Toolbox = ({ message, messageContext, room, subscription }: ToolboxProps): ReactElement | null => {
const t = useTranslation();
const user = useUser();
const settings = useSettings();
const quickReactionsEnabled = useFeaturePreview('quickReactions');
const context = getMessageContext(message, room, messageContext);
const mapSettings = useMemo(() => Object.fromEntries(settings.map((setting) => [setting._id, setting.value])), [settings]);
@ -91,7 +93,8 @@ const Toolbox = ({ message, messageContext, room, subscription }: ToolboxProps):
return (
<MessageToolbox>
{isReactionAllowed &&
{quickReactionsEnabled &&
isReactionAllowed &&
quickReactions.slice(0, 3).map(({ emoji, image }) => {
return <EmojiElement small key={emoji} title={emoji} emoji={emoji} image={image} onClick={() => handleSetReaction(emoji)} />;
})}

@ -0,0 +1,21 @@
import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts';
import type { FeaturesAvailable, FeaturePreviewProps } from './useFeaturePreviewList';
export const useFeaturePreview = (featureName: FeaturesAvailable) => {
const featurePreviewEnabled = useSetting('Accounts_AllowFeaturePreview');
const features = useUserPreference<FeaturePreviewProps[]>('featuresPreview');
const currentFeature = features?.find((feature) => feature.name === featureName);
if (!featurePreviewEnabled) {
return false;
}
if (!currentFeature) {
console.error(`Feature ${featureName} not found`);
return false;
}
return currentFeature.value;
};

@ -0,0 +1,44 @@
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts';
export type FeaturesAvailable = 'quickReactions';
export type FeaturePreviewProps = {
name: FeaturesAvailable;
i18n: TranslationKey;
description: TranslationKey;
group: 'Message' | 'Navigation';
imageUrl?: string;
value: boolean;
};
export const defaultFeaturesPreview: FeaturePreviewProps[] = [
{
name: 'quickReactions',
i18n: 'Quick_reactions',
description: 'Quick_reactions_description',
group: 'Message',
imageUrl: 'images/featurePreview/quick-reactions.png',
value: false,
},
];
export const useFeaturePreviewList = () => {
const featurePreviewEnabled = useSetting<boolean>('Accounts_AllowFeaturePreview');
const userFeaturesPreview = useUserPreference<FeaturePreviewProps[]>('featuresPreview');
if (!featurePreviewEnabled) {
return { unseenFeatures: 0, features: [] as FeaturePreviewProps[], featurePreviewEnabled };
}
const unseenFeatures = defaultFeaturesPreview.filter(
(feature) => !userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name),
).length;
const mergedFeatures = defaultFeaturesPreview.map((feature) => {
const userFeature = userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name);
return { ...feature, ...userFeature };
});
return { unseenFeatures, features: mergedFeatures, featurePreviewEnabled };
};

@ -1,21 +1,45 @@
import { Badge } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useLogout, useRoute, useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import type { GenericMenuItemProps } from '../../../components/GenericMenu/GenericMenuItem';
import { useFeaturePreviewList, defaultFeaturesPreview } from '../../../hooks/useFeaturePreviewList';
export const useAccountItems = (): GenericMenuItemProps[] => {
const t = useTranslation();
const accountRoute = useRoute('account-index');
const featurePreviewRoute = useRoute('feature-preview');
const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList();
const logout = useLogout();
const handleMyAccount = useMutableCallback(() => {
accountRoute.push({});
});
const handleFeaturePreview = useMutableCallback(() => {
featurePreviewRoute.push();
});
const handleLogout = useMutableCallback(() => {
logout();
});
const featurePreviewItem = {
id: 'feature-preview',
icon: 'flask' as const,
content: t('Feature_preview'),
onClick: handleFeaturePreview,
...(unseenFeatures > 0 && {
addon: () => (
<Badge variant='primary' aria-label={t('Unseen_features')}>
{unseenFeatures}
</Badge>
),
}),
};
return [
{
id: 'my-account',
@ -23,6 +47,7 @@ export const useAccountItems = (): GenericMenuItemProps[] => {
content: t('My_Account'),
onClick: handleMyAccount,
},
...(featurePreviewEnabled && defaultFeaturesPreview.length > 0 ? [featurePreviewItem] : []),
{
id: 'logout',
icon: 'sign-out',

@ -13,18 +13,13 @@ import Login from './actions/Login';
import Search from './actions/Search';
import Sort from './actions/Sort';
// TODO: Remove styles from here
const HeaderWithData = (): ReactElement => {
const user = useUser();
const t = useTranslation();
const user = useUser();
return (
<>
<Sidebar.TopBar.Section
{...{
style: { flexShrink: 0 },
}}
>
<Sidebar.TopBar.Section>
{user ? <UserMenu user={user} /> : <UserAvatarWithStatus />}
<Sidebar.TopBar.Actions>
<Home title={t('Home')} />

@ -0,0 +1,22 @@
import { Badge } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useFeaturePreviewList } from '../../../hooks/useFeaturePreviewList';
const AccountFeaturePreviewBadge = () => {
const t = useTranslation();
const { unseenFeatures } = useFeaturePreviewList();
if (!unseenFeatures) {
return null;
}
return (
<Badge variant='primary' aria-label={t('Unseen_features')}>
{unseenFeatures}
</Badge>
);
};
export default AccountFeaturePreviewBadge;

@ -0,0 +1,136 @@
import { css } from '@rocket.chat/css-in-js';
import {
ButtonGroup,
Button,
Box,
Field,
ToggleSwitch,
FieldGroup,
States,
StatesIcon,
StatesTitle,
Accordion,
} from '@rocket.chat/fuselage';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import type { ChangeEvent } from 'react';
import React, { useEffect, Fragment } from 'react';
import { useForm } from 'react-hook-form';
import Page from '../../../components/Page';
import type { FeaturePreviewProps } from '../../../hooks/useFeaturePreviewList';
import { useFeaturePreviewList } from '../../../hooks/useFeaturePreviewList';
const AccountFeaturePreviewPage = () => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const { features, unseenFeatures } = useFeaturePreviewList();
const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences');
useEffect(() => {
if (unseenFeatures) {
const featuresPreview = features.map((feature) => ({
name: feature.name,
value: feature.value,
}));
void setUserPreferences({ data: { featuresPreview } });
}
}, [setUserPreferences, features, unseenFeatures]);
const {
watch,
formState: { isDirty },
setValue,
handleSubmit,
reset,
} = useForm({
defaultValues: { featuresPreview: features },
});
const { featuresPreview } = watch();
const handleSave = async () => {
try {
await setUserPreferences({ data: { featuresPreview } });
dispatchToastMessage({ type: 'success', message: t('Preferences_saved') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
reset({ featuresPreview });
}
};
const handleFeatures = (e: ChangeEvent<HTMLInputElement>) => {
const updated = featuresPreview.map((item) => (item.name === e.target.name ? { ...item, value: e.target.checked } : item));
setValue('featuresPreview', updated, { shouldDirty: true });
};
const grouppedFeaturesPreview = Object.entries(
featuresPreview.reduce((result, currentValue) => {
(result[currentValue.group] = result[currentValue.group] || []).push(currentValue);
return result;
}, {} as Record<FeaturePreviewProps['group'], FeaturePreviewProps[]>),
);
return (
<Page>
<Page.Header title={t('Feature_preview')}>
<ButtonGroup>
<Button primary disabled={!isDirty} onClick={handleSubmit(handleSave)}>
{t('Save_changes')}
</Button>
</ButtonGroup>
</Page.Header>
<Page.ScrollableContentWithShadow>
<Box maxWidth='x600' w='full' alignSelf='center'>
{featuresPreview.length === 0 && (
<States>
<StatesIcon name='magnifier' />
<StatesTitle>{t('No_feature_to_preview')}</StatesTitle>
</States>
)}
{featuresPreview.length > 0 && (
<>
<Box
className={css`
white-space: break-spaces;
`}
pbe='x24'
fontScale='p1'
>
{t('Feature_preview_page_description')}
</Box>
<Accordion>
{grouppedFeaturesPreview?.map(([group, features], index) => (
<Accordion.Item defaultExpanded={index === 0} key={group} title={t(group as TranslationKey)}>
<FieldGroup>
{features.map((feature) => (
<Fragment key={feature.name}>
<Field>
<Box display='flex' flexDirection='row' justifyContent='spaceBetween' flexGrow={1}>
<Field.Label>{t(feature.i18n)}</Field.Label>
<Field.Row>
<Box mie='x12'>{t('Enabled')}</Box>
<ToggleSwitch checked={feature.value} name={feature.name} onChange={handleFeatures} />
</Field.Row>
</Box>
{feature.description && <Field.Hint mbs='x12'>{t(feature.description)}</Field.Hint>}
</Field>
{feature.imageUrl && <Box is='img' width='100%' height='auto' mbs='x16' src={feature.imageUrl} />}
</Fragment>
))}
</FieldGroup>
</Accordion.Item>
))}
</Accordion>
</>
)}
</Box>
</Page.ScrollableContentWithShadow>
</Page>
);
};
export default AccountFeaturePreviewPage;

@ -32,6 +32,10 @@ declare module '@rocket.chat/ui-contexts' {
pathname: '/account/omnichannel';
pattern: '/account/omnichannel';
};
'feature-preview': {
pathname: '/account/feature-preview';
pattern: '/account/feature-preview';
};
}
}
@ -70,3 +74,8 @@ registerAccountRoute('/omnichannel', {
name: 'omnichannel',
component: lazy(() => import('./omnichannel/OmnichannelPreferencesPage')),
});
registerAccountRoute('/feature-preview', {
name: 'feature-preview',
component: lazy(() => import('./featurePreview/AccountFeaturePreviewPage')),
});

@ -1,6 +1,10 @@
import React from 'react';
import { hasPermission, hasAtLeastOnePermission } from '../../../app/authorization/client';
import { settings } from '../../../app/settings/client';
import { defaultFeaturesPreview } from '../../hooks/useFeaturePreviewList';
import { createSidebarItems } from '../../lib/createSidebarItems';
import AccountFeaturePreviewBadge from './featurePreview/AccountFeaturePreviewBadge';
export const {
registerSidebarItem: registerAccountSidebarItem,
@ -43,4 +47,11 @@ export const {
icon: 'headset',
permissionGranted: (): boolean => hasAtLeastOnePermission(['send-omnichannel-chat-transcript', 'request-pdf-transcript']),
},
{
href: '/account/feature-preview',
i18nLabel: 'Feature_preview',
icon: 'flask',
badge: () => <AccountFeaturePreviewBadge />,
permissionGranted: () => settings.get('Accounts_AllowFeaturePreview') && defaultFeaturesPreview?.length > 0,
},
]);

@ -65,6 +65,7 @@
"Accounts_AllowInvisibleStatusOption": "Allow Invisible status option",
"Accounts_AllowEmailChange": "Allow Email Change",
"Accounts_AllowEmailNotifications": "Allow Email Notifications",
"Accounts_AllowFeaturePreview": "Allow Feature Preview",
"Accounts_AllowPasswordChange": "Allow Password Change",
"Accounts_AllowPasswordChangeForOAuthUsers": "Allow Password Change for OAuth Users",
"Accounts_AllowRealNameChange": "Allow Name Change",
@ -1572,6 +1573,7 @@
"Desktop_Notifications_Enabled": "Desktop Notifications are Enabled",
"Desktop_Notifications_Not_Enabled": "Desktop Notifications are Not Enabled",
"Unselected_by_default": "Unselected by default",
"Unseen_features": "Unseen features",
"Details": "Details",
"Device_Changes_Not_Available": "Device changes not available in this browser. For guaranteed availability, please use Rocket.Chat's official desktop app.",
"Device_Changes_Not_Available_Insecure_Context": "Device changes are only available on secure contexts (e.g. https://)",
@ -2141,6 +2143,8 @@
"Favorite": "Favorite",
"Favorite_Rooms": "Enable Favorite Rooms",
"Favorites": "Favorites",
"Feature_preview": "Feature preview",
"Feature_preview_page_description": "Welcome to the features preview page! Here, you can enable the latest cutting-edge features that are currently under development and not yet officially released.\n\nPlease note that these configurations are still in the testing phase and may not be stable or fully functional.",
"featured": "featured",
"Featured": "Featured",
"Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "This feature depends on the above selected call provider to be enabled from the administration settings (Admin -> Video Conference).",
@ -3563,6 +3567,7 @@
"Name_of_agent": "Name of agent",
"Name_optional": "Name (optional)",
"Name_Placeholder": "Please enter your name...",
"Navigation": "Navigation",
"Navigation_History": "Navigation History",
"Next": "Next",
"Never": "Never",
@ -4055,6 +4060,8 @@
"Queue_delay_timeout": "Queue processing delay timeout",
"Queue_Time": "Queue Time",
"Queue_management": "Queue Management",
"Quick_reactions": "Quick reactions",
"Quick_reactions_description": "The three most used reactions get an easy access while your mouse is over the message",
"quote": "quote",
"Quote": "Quote",
"Random": "Random",
@ -4268,6 +4275,7 @@
"Required_action": "Required action",
"Default_Referrer_Policy": "Default Referrer Policy",
"Default_Referrer_Policy_Description": "This controls the 'referrer' header that's sent when requesting embedded media from other servers. For more information, refer to [this link from MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy). Remember, a full page refresh is required for this to take effect",
"No_feature_to_preview": "No feature to preview",
"No_Referrer": "No Referrer",
"No_Referrer_When_Downgrade": "No referrer when downgrade",
"Notes": "Notes",

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

@ -205,6 +205,10 @@ export const createAccountSettings = () =>
type: 'boolean',
public: true,
});
await this.add('Accounts_AllowFeaturePreview', false, {
type: 'boolean',
public: true,
});
await this.add('Accounts_CustomFieldsToShowInUserInfo', '', {
type: 'string',
public: true,

@ -39,6 +39,7 @@ export type UsersSetPreferencesParamsPOST = {
sidebarGroupByType?: boolean;
muteFocusedConversations?: boolean;
dontAskAgainList?: Array<{ action: string; label: string }>;
featuresPreview?: { name: string; value: boolean }[];
themeAppearence?: 'auto' | 'light' | 'dark';
receiveLoginDetectionEmail?: boolean;
notifyCalendarEvents?: boolean;
@ -197,6 +198,17 @@ const UsersSetPreferencesParamsPostSchema = {
},
nullable: true,
},
featuresPreview: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'boolean' },
},
},
nullable: true,
},
themeAppearence: {
type: 'string',
nullable: true,

@ -9392,7 +9392,7 @@ __metadata:
languageName: unknown
linkType: soft
"@rocket.chat/css-in-js@npm:^0.31.12, @rocket.chat/css-in-js@npm:^0.31.23, @rocket.chat/css-in-js@npm:~0.31.23-dev.103, @rocket.chat/css-in-js@npm:~0.31.23-dev.151":
"@rocket.chat/css-in-js@npm:^0.31.12, @rocket.chat/css-in-js@npm:^0.31.23, @rocket.chat/css-in-js@npm:~0.31.23-dev.103, @rocket.chat/css-in-js@npm:~0.31.23-dev.154":
version: 0.31.23
resolution: "@rocket.chat/css-in-js@npm:0.31.23"
dependencies:
@ -9418,7 +9418,7 @@ __metadata:
languageName: node
linkType: hard
"@rocket.chat/css-supports@npm:^0.31.23, @rocket.chat/css-supports@npm:~0.31.23-dev.103, @rocket.chat/css-supports@npm:~0.31.23-dev.151":
"@rocket.chat/css-supports@npm:^0.31.23, @rocket.chat/css-supports@npm:~0.31.23-dev.103, @rocket.chat/css-supports@npm:~0.31.23-dev.154":
version: 0.31.23
resolution: "@rocket.chat/css-supports@npm:0.31.23"
dependencies:
@ -9646,10 +9646,10 @@ __metadata:
languageName: node
linkType: hard
"@rocket.chat/fuselage-tokens@npm:~0.32.0-dev.327":
version: 0.32.0-dev.327
resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.327"
checksum: 93597b59a76829d5074834295c0b13ecf1256433bdedab6ce5f92b50799893ca8f5eceabcdeaba0fbd24df7abd64f4c62fa28f008d8fa90a7ecd869bbd8b0d6a
"@rocket.chat/fuselage-tokens@npm:~0.32.0-dev.330":
version: 0.32.0-dev.330
resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.330"
checksum: 9733eeb0c8f1d8220e13d20c85e9ecf219b6d22e3c339352a080a19cbdc7aae7828ae591884b070c48047fa984383122d8a816e7102a4c38ba6a6ce73f364452
languageName: node
linkType: hard
@ -9709,14 +9709,14 @@ __metadata:
linkType: soft
"@rocket.chat/fuselage@npm:next":
version: 0.32.0-dev.377
resolution: "@rocket.chat/fuselage@npm:0.32.0-dev.377"
dependencies:
"@rocket.chat/css-in-js": ~0.31.23-dev.151
"@rocket.chat/css-supports": ~0.31.23-dev.151
"@rocket.chat/fuselage-tokens": ~0.32.0-dev.327
"@rocket.chat/memo": ~0.31.23-dev.151
"@rocket.chat/styled": ~0.31.23-dev.151
version: 0.32.0-dev.380
resolution: "@rocket.chat/fuselage@npm:0.32.0-dev.380"
dependencies:
"@rocket.chat/css-in-js": ~0.31.23-dev.154
"@rocket.chat/css-supports": ~0.31.23-dev.154
"@rocket.chat/fuselage-tokens": ~0.32.0-dev.330
"@rocket.chat/memo": ~0.31.23-dev.154
"@rocket.chat/styled": ~0.31.23-dev.154
invariant: ^2.2.4
react-aria: ~3.23.1
react-keyed-flatten-children: ^1.3.0
@ -9728,7 +9728,7 @@ __metadata:
react: ^17.0.2
react-dom: ^17.0.2
react-virtuoso: 1.2.4
checksum: e61ddd6ce6dd7ea4ba8d5b5c5f464a82d5600934c7ae05a38f20227308f39b55dbd5b91f9190d494b814efac6e60eb70810ab7d171ff1b482e54a2132573bdda
checksum: 07be707ad2fab881852d1b4ac49044915b24d5bf82352cc6e3e57526dfbcd13ec3b2d73b362bbe52d122aa5b45d90371838f1e463a443934d573022ac70a57e6
languageName: node
linkType: hard
@ -9816,9 +9816,9 @@ __metadata:
linkType: soft
"@rocket.chat/icons@npm:next":
version: 0.32.0-dev.349
resolution: "@rocket.chat/icons@npm:0.32.0-dev.349"
checksum: 45553695c95ef0aa908b133e7347aff9b3845fe03486e145ed2e717efa1b2fc5044f8d7bd390031cc3e401ecfb89723ef4c158a136f164bf73b4a9653775963d
version: 0.32.0-dev.362
resolution: "@rocket.chat/icons@npm:0.32.0-dev.362"
checksum: 1c2096913e154d0db68b8ccc933294486ff06e250983809ce165f1497df05deec8b5165b47dbefdd013013b4cdf4a3a20f2ac191155b629e5ec4a7bcab5d8870
languageName: node
linkType: hard
@ -9956,7 +9956,7 @@ __metadata:
languageName: node
linkType: hard
"@rocket.chat/memo@npm:^0.31.23, @rocket.chat/memo@npm:~0.31.23-dev.103, @rocket.chat/memo@npm:~0.31.23-dev.151":
"@rocket.chat/memo@npm:^0.31.23, @rocket.chat/memo@npm:~0.31.23-dev.103, @rocket.chat/memo@npm:~0.31.23-dev.154":
version: 0.31.23
resolution: "@rocket.chat/memo@npm:0.31.23"
checksum: 070debb940749a2e4463cf767dd65c6967cea664a5bd67c22a812d611f6c3c46d6fe4bb0bf329e43dcd927493413add37c45ae3b05ec08f0b24e9d7385caebdd
@ -10761,7 +10761,7 @@ __metadata:
languageName: node
linkType: hard
"@rocket.chat/styled@npm:~0.31.23-dev.103, @rocket.chat/styled@npm:~0.31.23-dev.151":
"@rocket.chat/styled@npm:~0.31.23-dev.103, @rocket.chat/styled@npm:~0.31.23-dev.154":
version: 0.31.23
resolution: "@rocket.chat/styled@npm:0.31.23"
dependencies:

Loading…
Cancel
Save