Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(livechat): hide or change logo #31820

Merged
merged 11 commits into from Mar 25, 2024
11 changes: 11 additions & 0 deletions .changeset/clean-cars-teach.md
@@ -0,0 +1,11 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/core-typings": minor
"@rocket.chat/livechat": minor
---

**Added the ability for premium workspaces to hide Rocket.Chat's watermark as well as change the Livechat widget's logo**

The new settings (named below) can be found in the Omnichannel workspace settings within the livechat section.
- Hide "powered by Rocket.Chat"
- Livechat widget logo (svg, png, jpg)
51 changes: 37 additions & 14 deletions apps/meteor/app/assets/server/assets.ts
@@ -1,7 +1,7 @@
import crypto from 'crypto';
import type { ServerResponse, IncomingMessage } from 'http';

import type { IRocketChatAssets, IRocketChatAsset } from '@rocket.chat/core-typings';
import type { IRocketChatAssets, IRocketChatAsset, ISetting } from '@rocket.chat/core-typings';
import { Settings } from '@rocket.chat/models';
import type { ServerMethods } from '@rocket.chat/ui-contexts';
import type { NextHandleFunction } from 'connect';
Expand All @@ -20,7 +20,10 @@ import { getURL } from '../../utils/server/getURL';
const RocketChatAssetsInstance = new RocketChatFile.GridFS({
name: 'assets',
});
const assets: IRocketChatAssets = {

type IRocketChatAssetsConfig = Record<keyof IRocketChatAssets, IRocketChatAsset & { settingOptions?: Partial<ISetting> }>;

const assets: IRocketChatAssetsConfig = {
logo: {
label: 'logo (svg, png, jpg)',
defaultUrl: 'images/logo/logo.svg',
Expand Down Expand Up @@ -189,9 +192,27 @@ const assets: IRocketChatAssets = {
extensions: ['svg'],
},
},
livechat_widget_logo: {
label: 'widget logo (svg, png, jpg)',
constraints: {
type: 'image',
extensions: ['svg', 'png', 'jpg', 'jpeg'],
},
settingOptions: {
section: 'Livechat',
group: 'Omnichannel',
invalidValue: {
defaultUrl: undefined,
},
enableQuery: { _id: 'Livechat_enabled', value: true },
enterprise: true,
modules: ['livechat-enterprise'],
sorter: 999 + 1,
},
},
};

function getAssetByKey(key: string): IRocketChatAsset {
function getAssetByKey(key: string) {
return assets[key as keyof IRocketChatAssets];
}

Expand Down Expand Up @@ -325,7 +346,7 @@ class RocketChatAssetsClass {

export const RocketChatAssets = new RocketChatAssetsClass();

async function addAssetToSetting(asset: string, value: IRocketChatAsset): Promise<void> {
export async function addAssetToSetting(asset: string, value: IRocketChatAsset, options?: Partial<ISetting>): Promise<void> {
const key = `Assets_${asset}`;

await settingsRegistry.add(
Expand All @@ -334,28 +355,30 @@ async function addAssetToSetting(asset: string, value: IRocketChatAsset): Promis
defaultUrl: value.defaultUrl,
},
{
type: 'asset',
group: 'Assets',
fileConstraints: value.constraints,
i18nLabel: value.label,
asset,
public: true,
wizard: value.wizard,
...{
type: 'asset',
group: 'Assets',
fileConstraints: value.constraints,
i18nLabel: value.label,
asset,
public: true,
},
...options,
},
);

const currentValue = settings.get<IRocketChatAsset>(key);

if (typeof currentValue === 'object' && currentValue.defaultUrl !== getAssetByKey(asset).defaultUrl) {
if (currentValue && typeof currentValue === 'object' && currentValue.defaultUrl !== getAssetByKey(asset).defaultUrl) {
currentValue.defaultUrl = getAssetByKey(asset).defaultUrl;
await Settings.updateValueById(key, currentValue);
}
}

void (async () => {
for await (const key of Object.keys(assets)) {
const value = getAssetByKey(key);
await addAssetToSetting(key, value);
const { wizard, settingOptions, ...value } = getAssetByKey(key);
await addAssetToSetting(key, value, { ...settingOptions, wizard });
}
})();

Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/app/livechat/server/api/lib/livechat.ts
Expand Up @@ -171,6 +171,8 @@ export async function settings({ businessUnit = '' }: { businessUnit?: string }
initSettings.Livechat_enable_message_character_limit &&
(initSettings.Livechat_message_character_limit || initSettings.Message_MaxAllowedSize),
hiddenSystemMessages: initSettings.Livechat_hide_system_messages,
livechatLogo: initSettings.Assets_livechat_widget_logo,
hideWatermark: initSettings.Livechat_hide_watermark || false,
},
theme: {
title: initSettings.Livechat_title,
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Expand Up @@ -1129,6 +1129,8 @@ class LivechatClass {
'Livechat_hide_system_messages',
'Livechat_widget_position',
'Livechat_background',
'Assets_livechat_widget_logo',
'Livechat_hide_watermark',
] as const;

type SettingTypes = (typeof validSettings)[number] | 'Livechat_Show_Connecting';
Expand Down
12 changes: 12 additions & 0 deletions apps/meteor/ee/app/livechat-enterprise/server/settings.ts
Expand Up @@ -223,6 +223,18 @@ export const createSettings = async (): Promise<void> => {
enableQuery: omnichannelEnabledQuery,
});

await settingsRegistry.add('Livechat_hide_watermark', false, {
type: 'boolean',
group: 'Omnichannel',
section: 'Livechat',
invalidValue: false,
enableQuery: omnichannelEnabledQuery,
i18nDescription: 'Livechat_hide_watermark_description',
enterprise: true,
sorter: 999,
modules: ['livechat-enterprise'],
});

await settingsRegistry.add('Omnichannel_contact_manager_routing', true, {
type: 'boolean',
group: 'Omnichannel',
Expand Down
1 change: 1 addition & 0 deletions packages/core-typings/src/IRocketChatAssets.ts
Expand Up @@ -53,4 +53,5 @@ export interface IRocketChatAssets {
tile_310_square: IRocketChatAsset;
tile_310_wide: IRocketChatAsset;
safari_pinned: IRocketChatAsset;
livechat_widget_logo: IRocketChatAsset;
}
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Expand Up @@ -3223,6 +3223,9 @@
"Livechat_Calls": "Livechat Calls",
"Livechat_visitor_email_and_transcript_email_do_not_match": "Visitor's email and transcript's email do not match",
"Livechat_visitor_transcript_request": "{{guest}} requested the chat transcript",
"Assets_livechat_widget_logo": "Livechat widget logo (svg, png, jpg)",
"Livechat_hide_watermark": "Hide \"powered by Rocket.Chat\"",
"Livechat_hide_watermark_description": "Remove the Rocket.Chat logo from the widget",
"LiveStream & Broadcasting": "LiveStream & Broadcasting",
"LiveStream & Broadcasting_Description": "This integration between Rocket.Chat and YouTube Live allows channel owners to broadcast their camera feed live to livestream inside a channel.",
"Livestream": "Livestream",
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/pt-BR.i18n.json
Expand Up @@ -2725,6 +2725,7 @@
"Livechat_Calls": "Chamadas do livechat",
"Livechat_visitor_email_and_transcript_email_do_not_match": "O e-mail do visitante e o e-mail da transcrição não correspondem",
"Livechat_visitor_transcript_request": "{{guest}} solicitou a transcrição da conversa",
"Assets_livechat_widget_logo": "Logotipo do widget do Livechat (svg, png, jpg)",
"LiveStream & Broadcasting": "LiveStream e transmissão",
"Livestream": "Livestream",
"Livestream_close": "Fechar Livestream",
Expand Down
29 changes: 29 additions & 0 deletions packages/livechat/src/components/Screen/ChatButton.tsx
@@ -0,0 +1,29 @@
import ChatIcon from '../../icons/chat.svg';
import CloseIcon from '../../icons/close.svg';
import { Button } from '../Button';

type ChatButtonProps = {
text: string;
minimized: boolean;
badge: number;
onClick: () => void;
triggered?: boolean;
className?: string;
logoUrl?: string;
};

export const ChatButton = ({ text, minimized, badge, onClick, triggered = false, className, logoUrl }: ChatButtonProps) => {
const openIcon = logoUrl ? <img src={logoUrl} width={30} height={30} alt='Livechat' /> : <ChatIcon />;

return (
<Button
icon={minimized || triggered ? openIcon : <CloseIcon />}
badge={badge}
onClick={onClick}
className={className}
data-qa-id='chat-button'
>
{text}
</Button>
);
};
5 changes: 5 additions & 0 deletions packages/livechat/src/components/Screen/ScreenProvider.tsx
Expand Up @@ -9,6 +9,8 @@ import Triggers from '../../lib/triggers';
import { StoreContext } from '../../store';

export type ScreenContextValue = {
hideWatermark: boolean;
livechatLogo: { url: string } | undefined;
notificationsEnabled: boolean;
minimized: boolean;
expanded: boolean;
Expand Down Expand Up @@ -72,6 +74,7 @@ export const ScreenProvider: FunctionalComponent = ({ children }) => {
} = useContext(StoreContext);
const { department, name, email } = iframe.guest || {};
const { color, position: configPosition, background } = config.theme || {};
const { livechatLogo, hideWatermark = false } = config.settings || {};

const {
color: customColor,
Expand Down Expand Up @@ -165,6 +168,8 @@ export const ScreenProvider: FunctionalComponent = ({ children }) => {
minimized: !poppedOut && (minimized || undocked),
expanded: !minimized && expanded,
windowed: !minimized && poppedOut,
livechatLogo,
hideWatermark,
sound,
alerts,
modal,
Expand Down
40 changes: 17 additions & 23 deletions packages/livechat/src/components/Screen/index.js
@@ -1,12 +1,12 @@
import { useContext, useEffect } from 'preact/hooks';

import { createClassName } from '../../helpers/createClassName';
import ChatIcon from '../../icons/chat.svg';
import CloseIcon from '../../icons/close.svg';
import { Button } from '../Button';
import { Footer, FooterContent, PoweredBy } from '../Footer';
import { PopoverContainer } from '../Popover';
import { Sound } from '../Sound';
import { ChatButton } from './ChatButton';
import ScreenHeader from './Header';
import { ScreenContext } from './ScreenProvider';
import styles from './styles.scss';
Expand All @@ -15,29 +15,20 @@ export const ScreenContent = ({ children, nopadding, triggered = false, full = f
<main className={createClassName(styles, 'screen__main', { nopadding, triggered, full })}>{children}</main>
);

export const ScreenFooter = ({ children, options, limit }) => (
<Footer>
{children && <FooterContent>{children}</FooterContent>}
<FooterContent>
{options}
{limit}
<PoweredBy />
</FooterContent>
</Footer>
);
export const ScreenFooter = ({ children, options, limit }) => {
const { hideWatermark } = useContext(ScreenContext);

const ChatButton = ({ text, minimized, badge, onClick, triggered = false, agent }) => (
<Button
icon={minimized || triggered ? <ChatIcon /> : <CloseIcon />}
badge={badge}
onClick={onClick}
className={createClassName(styles, 'screen__chat-button')}
data-qa-id='chat-button'
img={triggered && agent && agent.avatar.src}
>
{text}
</Button>
);
return (
<Footer>
{children && <FooterContent>{children}</FooterContent>}
<FooterContent>
{options}
{limit}
{!hideWatermark && <PoweredBy />}
</FooterContent>
</Footer>
);
};

const CssVar = ({ theme }) => {
useEffect(() => {
Expand Down Expand Up @@ -81,6 +72,7 @@ const CssVar = ({ theme }) => {
export const Screen = ({ title, color, agent, children, className, unread, triggered = false, queueInfo, onSoundStop }) => {
const {
theme = {},
livechatLogo,
notificationsEnabled,
minimized = false,
expanded = false,
Expand Down Expand Up @@ -150,6 +142,8 @@ export const Screen = ({ title, color, agent, children, className, unread, trigg
text={title}
badge={unread}
minimized={minimized}
logoUrl={livechatLogo?.url}
className={createClassName(styles, 'screen__chat-button')}
onClick={minimized ? onRestore : onMinimize}
/>

Expand Down
2 changes: 2 additions & 0 deletions packages/livechat/src/store/index.tsx
Expand Up @@ -55,6 +55,8 @@ export type StoreState = {
limitTextLength?: any;
displayOfflineForm?: boolean;
hiddenSystemMessages?: LivechatHiddenSytemMessageType[];
hideWatermark?: boolean;
livechatLogo?: { url: string };
};
online?: boolean;
departments: Department[];
Expand Down