AppearanceConfig.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import React, { FC, useContext, useCallback, useEffect, useState } from 'react';
  2. import { Button, Col, Collapse, Row, Slider, Space } from 'antd';
  3. import Paragraph from 'antd/lib/typography/Paragraph';
  4. import Title from 'antd/lib/typography/Title';
  5. import { EditCustomStyles } from '../../EditCustomStyles';
  6. import s from './appearance.module.scss';
  7. import { postConfigUpdateToAPI, RESET_TIMEOUT } from '../../../../utils/config-constants';
  8. import {
  9. createInputStatus,
  10. StatusState,
  11. STATUS_ERROR,
  12. STATUS_SUCCESS,
  13. } from '../../../../utils/input-statuses';
  14. import { ServerStatusContext } from '../../../../utils/server-status-context';
  15. import { FormStatusIndicator } from '../../FormStatusIndicator';
  16. const { Panel } = Collapse;
  17. const ENDPOINT = '/appearance';
  18. interface AppearanceVariable {
  19. value: string;
  20. description: string;
  21. }
  22. type ColorCollectionProps = {
  23. variables: { name; description; value }[];
  24. updateColor: (variable: string, color: string, description: string) => void;
  25. };
  26. const chatColorVariables = [
  27. { name: 'theme-color-users-0', description: '' },
  28. { name: 'theme-color-users-1', description: '' },
  29. { name: 'theme-color-users-2', description: '' },
  30. { name: 'theme-color-users-3', description: '' },
  31. { name: 'theme-color-users-4', description: '' },
  32. { name: 'theme-color-users-5', description: '' },
  33. { name: 'theme-color-users-6', description: '' },
  34. { name: 'theme-color-users-7', description: '' },
  35. ];
  36. const componentColorVariables = [
  37. { name: 'theme-color-background-main', description: 'Background' },
  38. { name: 'theme-color-action', description: 'Action' },
  39. { name: 'theme-color-action-hover', description: 'Action Hover' },
  40. { name: 'theme-color-components-primary-button-border', description: 'Primary Button Border' },
  41. { name: 'theme-color-components-primary-button-text', description: 'Primary Button Text' },
  42. { name: 'theme-color-components-chat-background', description: 'Chat Background' },
  43. { name: 'theme-color-components-chat-text', description: 'Text: Chat' },
  44. { name: 'theme-color-components-text-on-dark', description: 'Text: Light' },
  45. { name: 'theme-color-components-text-on-light', description: 'Text: Dark' },
  46. { name: 'theme-color-background-header', description: 'Header/Footer' },
  47. { name: 'theme-color-components-content-background', description: 'Page Content' },
  48. {
  49. name: 'theme-color-components-video-status-bar-background',
  50. description: 'Video Status Bar Background',
  51. },
  52. {
  53. name: 'theme-color-components-video-status-bar-foreground',
  54. description: 'Video Status Bar Foreground',
  55. },
  56. ];
  57. const others = [{ name: 'theme-rounded-corners', description: 'Corner radius' }];
  58. // Create an object so these vars can be indexed by name.
  59. const allAvailableValues = [...componentColorVariables, ...chatColorVariables, ...others].reduce(
  60. (obj, val) => {
  61. // eslint-disable-next-line no-param-reassign
  62. obj[val.name] = { name: val.name, description: val.description };
  63. return obj;
  64. },
  65. {},
  66. );
  67. // eslint-disable-next-line react/function-component-definition
  68. const ColorPicker = React.memo(
  69. ({
  70. value,
  71. name,
  72. description,
  73. onChange,
  74. }: {
  75. value: string;
  76. name: string;
  77. description: string;
  78. onChange: (name: string, value: string, description: string) => void;
  79. }) => (
  80. <Col span={3} key={name}>
  81. <input
  82. type="color"
  83. id={name}
  84. name={description}
  85. title={description}
  86. value={value}
  87. className={s.colorPicker}
  88. onChange={e => onChange(name, e.target.value, description)}
  89. />
  90. <div style={{ padding: '2px' }}>{description}</div>
  91. </Col>
  92. ),
  93. );
  94. const ColorCollection: FC<ColorCollectionProps> = ({ variables, updateColor }) => {
  95. const cc = variables.map(colorVar => {
  96. const { name, description, value } = colorVar;
  97. return (
  98. <ColorPicker
  99. key={name}
  100. value={value}
  101. name={name}
  102. description={description}
  103. onChange={updateColor}
  104. />
  105. );
  106. });
  107. // eslint-disable-next-line react/jsx-no-useless-fragment
  108. return <>{cc}</>;
  109. };
  110. // eslint-disable-next-line react/function-component-definition
  111. export default function Appearance() {
  112. const serverStatusData = useContext(ServerStatusContext);
  113. const { serverConfig, setFieldInConfigState } = serverStatusData;
  114. const { instanceDetails } = serverConfig;
  115. const { appearanceVariables } = instanceDetails;
  116. const [defaultValues, setDefaultValues] = useState<Record<string, AppearanceVariable>>();
  117. const [customValues, setCustomValues] = useState<Record<string, AppearanceVariable>>();
  118. const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
  119. let resetTimer = null;
  120. const resetStates = () => {
  121. setSubmitStatus(null);
  122. resetTimer = null;
  123. clearTimeout(resetTimer);
  124. };
  125. const setDefaults = () => {
  126. const c = {};
  127. [...componentColorVariables, ...chatColorVariables, ...others].forEach(color => {
  128. const resolvedColor = getComputedStyle(document.documentElement).getPropertyValue(
  129. `--${color.name}`,
  130. );
  131. c[color.name] = { value: resolvedColor.trim(), description: color.description };
  132. });
  133. setDefaultValues(c);
  134. };
  135. useEffect(() => {
  136. setDefaults();
  137. }, []);
  138. useEffect(() => {
  139. if (Object.keys(appearanceVariables).length === 0) return;
  140. const c = {};
  141. Object.keys(appearanceVariables).forEach(key => {
  142. c[key] = {
  143. value: appearanceVariables[key],
  144. description: allAvailableValues[key]?.description || '',
  145. };
  146. });
  147. setCustomValues(c);
  148. }, [appearanceVariables]);
  149. const updateColor = useCallback((variable: string, color: string, description: string) => {
  150. setCustomValues(oldCustomValues => ({
  151. ...oldCustomValues,
  152. [variable]: { value: color, description },
  153. }));
  154. }, []);
  155. const reset = async () => {
  156. await postConfigUpdateToAPI({
  157. apiPath: ENDPOINT,
  158. data: { value: {} },
  159. onSuccess: () => {
  160. setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
  161. resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
  162. setCustomValues({});
  163. },
  164. onError: (message: string) => {
  165. setSubmitStatus(createInputStatus(STATUS_ERROR, message));
  166. resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
  167. },
  168. });
  169. };
  170. const save = async () => {
  171. const c = {};
  172. Object.keys(customValues).forEach(color => {
  173. c[color] = customValues[color].value;
  174. });
  175. await postConfigUpdateToAPI({
  176. apiPath: ENDPOINT,
  177. data: { value: c },
  178. onSuccess: () => {
  179. setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
  180. resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
  181. setFieldInConfigState({
  182. fieldName: 'appearanceVariables',
  183. value: c,
  184. path: 'instanceDetails',
  185. });
  186. },
  187. onError: (message: string) => {
  188. setSubmitStatus(createInputStatus(STATUS_ERROR, message));
  189. resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
  190. },
  191. });
  192. };
  193. const onBorderRadiusChange = (value: string) => {
  194. const variableName = 'theme-rounded-corners';
  195. updateColor(variableName, `${value.toString()}px`, '');
  196. };
  197. if (!defaultValues) {
  198. return <div>Loading...</div>;
  199. }
  200. const transformToColorMap = variables =>
  201. variables.map(colorVar => {
  202. const source = customValues?.[colorVar.name] ? customValues : defaultValues;
  203. const { name, description } = colorVar;
  204. const { value } = source[name];
  205. return { name, description, value };
  206. });
  207. return (
  208. <>
  209. <Space direction="vertical">
  210. <Title>Customize Appearance</Title>
  211. <Paragraph>The following colors are used across the user interface.</Paragraph>
  212. <div>
  213. <Collapse defaultActiveKey={['1']}>
  214. <Panel header={<strong>Section Colors</strong>} key="1">
  215. <p>
  216. Certain sections of the interface can be customized by selecting new colors for
  217. them.
  218. </p>
  219. <Row gutter={[16, 16]}>
  220. <ColorCollection
  221. variables={transformToColorMap(componentColorVariables)}
  222. updateColor={updateColor}
  223. />
  224. </Row>
  225. </Panel>
  226. <Panel header={<strong>Chat User Colors</strong>} key="2">
  227. <Row gutter={[16, 16]}>
  228. <ColorCollection
  229. variables={transformToColorMap(chatColorVariables)}
  230. updateColor={updateColor}
  231. />
  232. </Row>
  233. </Panel>
  234. <Panel header={<strong>Other Settings</strong>} key="4">
  235. How rounded should corners be?
  236. <Row gutter={[16, 16]}>
  237. <Col span={12}>
  238. <Slider
  239. min={0}
  240. max={20}
  241. tooltip={{ formatter: null }}
  242. onChange={v => {
  243. onBorderRadiusChange(v);
  244. }}
  245. value={Number(
  246. customValues?.['theme-rounded-corners']?.value?.replace('px', '') ??
  247. defaultValues?.['theme-rounded-corners']?.value?.replace('px', '') ??
  248. 0,
  249. )}
  250. />
  251. </Col>
  252. <Col span={4}>
  253. <div
  254. style={{
  255. width: '100px',
  256. height: '30px',
  257. borderRadius: `${
  258. customValues?.['theme-rounded-corners']?.value ??
  259. defaultValues?.['theme-rounded-corners']?.value
  260. }`,
  261. backgroundColor: 'var(--theme-color-palette-7)',
  262. }}
  263. />
  264. </Col>
  265. </Row>
  266. </Panel>
  267. </Collapse>
  268. </div>
  269. <Space direction="horizontal">
  270. <Button type="primary" onClick={save}>
  271. Save Colors
  272. </Button>
  273. <Button type="ghost" onClick={reset}>
  274. Reset to Defaults
  275. </Button>
  276. </Space>
  277. <FormStatusIndicator status={submitStatus} />
  278. </Space>
  279. <div className="form-module page-content-module">
  280. <EditCustomStyles />
  281. </div>
  282. </>
  283. );
  284. }