EditInstanceDetails.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import React, { useState, useContext, useEffect, FC } from 'react';
  2. import { Button, Modal, Typography } from 'antd';
  3. import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
  4. import CodeMirror from '@uiw/react-codemirror';
  5. import { bbedit } from '@uiw/codemirror-theme-bbedit';
  6. import { languages } from '@codemirror/language-data';
  7. import {
  8. TextFieldWithSubmit,
  9. TEXTFIELD_TYPE_TEXTAREA,
  10. TEXTFIELD_TYPE_URL,
  11. } from '../../TextFieldWithSubmit';
  12. import { ServerStatusContext } from '../../../../utils/server-status-context';
  13. import {
  14. postConfigUpdateToAPI,
  15. TEXTFIELD_PROPS_INSTANCE_URL,
  16. TEXTFIELD_PROPS_SERVER_NAME,
  17. TEXTFIELD_PROPS_SERVER_SUMMARY,
  18. TEXTFIELD_PROPS_SERVER_OFFLINE_MESSAGE,
  19. API_YP_SWITCH,
  20. FIELD_PROPS_YP,
  21. FIELD_PROPS_NSFW,
  22. FIELD_PROPS_HIDE_VIEWER_COUNT,
  23. API_SERVER_OFFLINE_MESSAGE,
  24. FIELD_PROPS_DISABLE_SEARCH_INDEXING,
  25. } from '../../../../utils/config-constants';
  26. import { UpdateArgs } from '../../../../types/config-section';
  27. import { ToggleSwitch } from '../../ToggleSwitch';
  28. import { EditLogo } from '../../EditLogo';
  29. import FormStatusIndicator from '../../FormStatusIndicator';
  30. import { createInputStatus, STATUS_SUCCESS } from '../../../../utils/input-statuses';
  31. const { Title } = Typography;
  32. export type DirectoryInfoModalProps = {
  33. cancelPressed: () => void;
  34. okPressed: () => void;
  35. };
  36. const DirectoryInfoModal: FC<DirectoryInfoModalProps> = ({ cancelPressed, okPressed }) => (
  37. <Modal
  38. width="70%"
  39. title="Owncast Directory"
  40. visible
  41. onCancel={cancelPressed}
  42. footer={
  43. <div>
  44. <Button onClick={cancelPressed}>Do not share my server.</Button>
  45. <Button type="primary" onClick={okPressed}>
  46. Understood. Share my server publicly.
  47. </Button>
  48. </div>
  49. }
  50. >
  51. <Typography.Title level={3}>What is the Owncast Directory?</Typography.Title>
  52. <Typography.Paragraph>
  53. Owncast operates a public directory at{' '}
  54. <a href="https://directory.owncast.online">directory.owncast.online</a> to share your video
  55. streams with more people, while also using these as examples for others. Live streams and
  56. servers listed on the directory may optionally be shared on other platforms and applications.
  57. </Typography.Paragraph>
  58. <Typography.Title level={3}>Disclaimers and Responsibility</Typography.Title>
  59. <Typography.Paragraph>
  60. <ul>
  61. <li>
  62. By enabling this feature you are granting explicit rights to Owncast to share your stream
  63. to the public via the directory, as well as other sites, applications and any platform
  64. where the Owncast project may be promoting Owncast-powered streams including social media.
  65. </li>
  66. <li>
  67. There is no obligation to list any specific server or topic. Servers can and will be
  68. removed at any time for any reason.
  69. </li>
  70. <li>
  71. Any server that is streaming Not Safe For Work (NSFW) content and does not have the NSFW
  72. toggle enabled on their server will be removed.
  73. </li>
  74. <li>
  75. Any server streaming harmful, hurtful, misleading or hateful content in any way will not
  76. be listed.
  77. </li>
  78. <li>
  79. You may reach out to the Owncast team to report any objectionable content or content that
  80. you believe should not be be publicly listed.
  81. </li>
  82. <li>
  83. You have the right to free software and to build whatever you want with it. But there is
  84. no obligation for others to share it.
  85. </li>
  86. </ul>
  87. </Typography.Paragraph>
  88. </Modal>
  89. );
  90. // eslint-disable-next-line react/function-component-definition
  91. export default function EditInstanceDetails() {
  92. const [formDataValues, setFormDataValues] = useState(null);
  93. const serverStatusData = useContext(ServerStatusContext);
  94. const { serverConfig } = serverStatusData || {};
  95. const { instanceDetails, yp, hideViewerCount, disableSearchIndexing } = serverConfig;
  96. const { instanceUrl } = yp;
  97. const [offlineMessageSaveStatus, setOfflineMessageSaveStatus] = useState(null);
  98. const [isDirectoryInfoModalOpen, setIsDirectoryInfoModalOpen] = useState(false);
  99. useEffect(() => {
  100. setFormDataValues({
  101. ...instanceDetails,
  102. ...yp,
  103. hideViewerCount,
  104. disableSearchIndexing,
  105. });
  106. }, [instanceDetails, yp]);
  107. if (!formDataValues) {
  108. return null;
  109. }
  110. const handleDirectorySwitchChange = (value: boolean) => {
  111. if (!value) {
  112. // Disabled. No-op.
  113. } else {
  114. setIsDirectoryInfoModalOpen(true);
  115. }
  116. setFormDataValues({
  117. ...formDataValues,
  118. yp: {
  119. enabled: value,
  120. },
  121. });
  122. };
  123. // if instanceUrl is empty, we should also turn OFF the `enabled` field of directory.
  124. const handleSubmitInstanceUrl = () => {
  125. if (formDataValues.instanceUrl === '') {
  126. if (yp.enabled === true) {
  127. postConfigUpdateToAPI({
  128. apiPath: API_YP_SWITCH,
  129. data: { value: false },
  130. });
  131. }
  132. }
  133. };
  134. const handleSaveOfflineMessage = () => {
  135. postConfigUpdateToAPI({
  136. apiPath: API_SERVER_OFFLINE_MESSAGE,
  137. data: { value: formDataValues.offlineMessage },
  138. });
  139. setOfflineMessageSaveStatus(createInputStatus(STATUS_SUCCESS));
  140. setTimeout(() => {
  141. setOfflineMessageSaveStatus(null);
  142. }, 2000);
  143. };
  144. const handleFieldChange = ({ fieldName, value }: UpdateArgs) => {
  145. setFormDataValues({
  146. ...formDataValues,
  147. [fieldName]: value,
  148. });
  149. };
  150. function handleHideViewerCountChange(enabled: boolean) {
  151. handleFieldChange({ fieldName: 'hideViewerCount', value: enabled });
  152. }
  153. function handleDisableSearchEngineIndexingChange(enabled: boolean) {
  154. handleFieldChange({ fieldName: 'disableSearchIndexing', value: enabled });
  155. }
  156. function directoryInfoModalCancelPressed() {
  157. setIsDirectoryInfoModalOpen(false);
  158. handleDirectorySwitchChange(false);
  159. handleFieldChange({ fieldName: 'enabled', value: false });
  160. }
  161. function directoryInfoModalOkPressed() {
  162. setIsDirectoryInfoModalOpen(false);
  163. handleFieldChange({ fieldName: 'enabled', value: true });
  164. setFormDataValues({
  165. ...formDataValues,
  166. yp: {
  167. enabled: true,
  168. },
  169. });
  170. }
  171. const hasInstanceUrl = instanceUrl !== '';
  172. return (
  173. <div className="edit-general-settings">
  174. <Title level={3} className="section-title">
  175. Configure Instance Details
  176. </Title>
  177. <br />
  178. <TextFieldWithSubmit
  179. fieldName="name"
  180. {...TEXTFIELD_PROPS_SERVER_NAME}
  181. value={formDataValues.name}
  182. initialValue={instanceDetails.name}
  183. onChange={handleFieldChange}
  184. />
  185. <TextFieldWithSubmit
  186. fieldName="instanceUrl"
  187. {...TEXTFIELD_PROPS_INSTANCE_URL}
  188. value={formDataValues.instanceUrl}
  189. initialValue={yp.instanceUrl}
  190. type={TEXTFIELD_TYPE_URL}
  191. onChange={handleFieldChange}
  192. onSubmit={handleSubmitInstanceUrl}
  193. />
  194. <TextFieldWithSubmit
  195. fieldName="summary"
  196. {...TEXTFIELD_PROPS_SERVER_SUMMARY}
  197. type={TEXTFIELD_TYPE_TEXTAREA}
  198. value={formDataValues.summary}
  199. initialValue={instanceDetails.summary}
  200. onChange={handleFieldChange}
  201. />
  202. <div style={{ marginBottom: '50px', marginRight: '150px' }}>
  203. <div
  204. style={{
  205. display: 'flex',
  206. width: '80vh',
  207. justifyContent: 'space-between',
  208. alignItems: 'end',
  209. }}
  210. >
  211. <p style={{ margin: '20px', marginRight: '10px', fontWeight: '400' }}>Offline Message:</p>
  212. <CodeMirror
  213. value={formDataValues.offlineMessage}
  214. {...TEXTFIELD_PROPS_SERVER_OFFLINE_MESSAGE}
  215. theme={bbedit}
  216. height="150px"
  217. width="450px"
  218. onChange={value => {
  219. handleFieldChange({ fieldName: 'offlineMessage', value });
  220. }}
  221. extensions={[markdown({ base: markdownLanguage, codeLanguages: languages })]}
  222. />
  223. </div>
  224. <div className="field-tip">
  225. The offline message is displayed to your page visitors when you&apos;re not streaming.
  226. Markdown is supported.
  227. </div>
  228. <Button
  229. type="primary"
  230. onClick={handleSaveOfflineMessage}
  231. style={{ margin: '10px', float: 'right' }}
  232. >
  233. Save Message
  234. </Button>
  235. <FormStatusIndicator status={offlineMessageSaveStatus} />
  236. </div>
  237. {/* Logo section */}
  238. <EditLogo />
  239. <ToggleSwitch
  240. fieldName="hideViewerCount"
  241. useSubmit
  242. {...FIELD_PROPS_HIDE_VIEWER_COUNT}
  243. checked={formDataValues.hideViewerCount}
  244. onChange={handleHideViewerCountChange}
  245. />
  246. <ToggleSwitch
  247. fieldName="disableSearchIndexing"
  248. useSubmit
  249. {...FIELD_PROPS_DISABLE_SEARCH_INDEXING}
  250. checked={formDataValues.disableSearchIndexing}
  251. onChange={handleDisableSearchEngineIndexingChange}
  252. />
  253. <br />
  254. <p className="description">
  255. Increase your audience by appearing in the{' '}
  256. <a href="https://directory.owncast.online" target="_blank" rel="noreferrer">
  257. <strong>Owncast Directory</strong>
  258. </a>
  259. . This is an external service run by the Owncast project.{' '}
  260. <a
  261. href="https://owncast.online/docs/directory/?source=admin"
  262. target="_blank"
  263. rel="noopener noreferrer"
  264. >
  265. Learn more
  266. </a>
  267. .
  268. </p>
  269. {!yp.instanceUrl && (
  270. <p className="description">
  271. You must set your <strong>Server URL</strong> above to enable the directory.
  272. </p>
  273. )}
  274. <div className="config-yp-container">
  275. <ToggleSwitch
  276. fieldName="enabled"
  277. useSubmit
  278. {...FIELD_PROPS_YP}
  279. checked={formDataValues.enabled}
  280. disabled={!hasInstanceUrl}
  281. onChange={handleDirectorySwitchChange}
  282. />
  283. <ToggleSwitch
  284. fieldName="nsfw"
  285. useSubmit
  286. {...FIELD_PROPS_NSFW}
  287. checked={formDataValues.nsfw}
  288. disabled={!hasInstanceUrl}
  289. />
  290. </div>
  291. {isDirectoryInfoModalOpen && (
  292. <DirectoryInfoModal
  293. cancelPressed={directoryInfoModalCancelPressed}
  294. okPressed={directoryInfoModalOkPressed}
  295. />
  296. )}
  297. </div>
  298. );
  299. }