-
- {statusIndicatorWithThumb}
+ {statusIndicatorWithThumb}
{headerAlertMessage}
@@ -235,6 +270,11 @@ export default function MainLayout(props) {
+
+ setPostModalDisplayed(false)}
+ />
);
}
diff --git a/web/components/moderator-user-button.tsx b/web/components/moderator-user-button.tsx
index 965f2830c..b92ffa0d5 100644
--- a/web/components/moderator-user-button.tsx
+++ b/web/components/moderator-user-button.tsx
@@ -1,5 +1,10 @@
import { Modal, Button } from 'antd';
-import { ExclamationCircleFilled, QuestionCircleFilled, StopTwoTone } from '@ant-design/icons';
+import {
+ ExclamationCircleFilled,
+ QuestionCircleFilled,
+ StopTwoTone,
+ SafetyCertificateTwoTone,
+} from '@ant-design/icons';
import { USER_SET_MODERATOR, fetchData } from '../utils/apis';
import { User } from '../types/chat';
@@ -70,7 +75,13 @@ export default function ModeratorUserButton({ user, onClick }: ModeratorUserButt
: null}
+ icon={
+ isModerator ? (
+
+ ) : (
+
+ )
+ }
className="block-user-button"
>
{actionString}
diff --git a/web/components/user-popover.tsx b/web/components/user-popover.tsx
index 85fc45bd3..8ea49f3d4 100644
--- a/web/components/user-popover.tsx
+++ b/web/components/user-popover.tsx
@@ -1,7 +1,7 @@
// This displays a clickable user name (or whatever children element you provide), and displays a simple tooltip of created time. OnClick a modal with more information about the user is displayed.
import { useState, ReactNode } from 'react';
-import { Divider, Modal, Tooltip, Typography, Row, Col } from 'antd';
+import { Divider, Modal, Tooltip, Typography, Row, Col, Space } from 'antd';
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import format from 'date-fns/format';
import { uniq } from 'lodash';
@@ -117,27 +117,29 @@ export default function UserPopover({ user, connectionInfo, children }: UserPopo
)}
- {disabledAt ? (
- <>
- This user was banned on {formatDisplayDate(disabledAt)}.
-
-
+
+ {disabledAt ? (
+ <>
+ This user was banned on {formatDisplayDate(disabledAt)}.
+
+
+
+ >
+ ) : (
- >
- ) : (
-
- )}
-
+ )}
+
+
>
diff --git a/web/next-env.d.ts b/web/next-env.d.ts
index 9bc3dd46b..4f11a03dc 100644
--- a/web/next-env.d.ts
+++ b/web/next-env.d.ts
@@ -1,5 +1,4 @@
///
-///
///
// NOTE: This file should not be edited
diff --git a/web/package-lock.json b/web/package-lock.json
index 15c0a55b4..e024707bb 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1387,9 +1387,13 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001243",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz",
- "integrity": "sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA=="
+ "version": "1.0.30001297",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001297.tgz",
+ "integrity": "sha512-6bbIbowYG8vFs/Lk4hU9jFt7NknGDleVAciK916tp6ft1j+D//ZwwL6LbF1wXMQ32DMSjeuUV8suhh6dlmFjcA==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ }
},
"node_modules/chalk": {
"version": "2.4.2",
@@ -7663,9 +7667,9 @@
"dev": true
},
"caniuse-lite": {
- "version": "1.0.30001243",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz",
- "integrity": "sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA=="
+ "version": "1.0.30001297",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001297.tgz",
+ "integrity": "sha512-6bbIbowYG8vFs/Lk4hU9jFt7NknGDleVAciK916tp6ft1j+D//ZwwL6LbF1wXMQ32DMSjeuUV8suhh6dlmFjcA=="
},
"chalk": {
"version": "2.4.2",
diff --git a/web/pages/config-federation.tsx b/web/pages/config-federation.tsx
new file mode 100644
index 000000000..a13c0b5d8
--- /dev/null
+++ b/web/pages/config-federation.tsx
@@ -0,0 +1,323 @@
+/* eslint-disable react/no-unescaped-entities */
+import { Typography, Modal, Button, Row, Col } from 'antd';
+import React, { useContext, useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import {
+ TEXTFIELD_TYPE_TEXT,
+ TEXTFIELD_TYPE_TEXTAREA,
+ TEXTFIELD_TYPE_URL,
+} from '../components/config/form-textfield';
+import TextFieldWithSubmit from '../components/config/form-textfield-with-submit';
+import ToggleSwitch from '../components/config/form-toggleswitch';
+import EditValueArray from '../components/config/edit-string-array';
+import { UpdateArgs } from '../types/config-section';
+import {
+ FIELD_PROPS_ENABLE_FEDERATION,
+ TEXTFIELD_PROPS_FEDERATION_LIVE_MESSAGE,
+ TEXTFIELD_PROPS_FEDERATION_DEFAULT_USER,
+ FIELD_PROPS_FEDERATION_IS_PRIVATE,
+ FIELD_PROPS_SHOW_FEDERATION_ENGAGEMENT,
+ TEXTFIELD_PROPS_FEDERATION_INSTANCE_URL,
+ FIELD_PROPS_FEDERATION_BLOCKED_DOMAINS,
+ postConfigUpdateToAPI,
+ RESET_TIMEOUT,
+ API_FEDERATION_BLOCKED_DOMAINS,
+ FIELD_PROPS_FEDERATION_NSFW,
+} from '../utils/config-constants';
+import { ServerStatusContext } from '../utils/server-status-context';
+import { createInputStatus, STATUS_ERROR, STATUS_SUCCESS } from '../utils/input-statuses';
+
+function FederationInfoModal({ cancelPressed, okPressed }) {
+ return (
+
+
+
+
+ }
+ >
+ How do Owncast's social features work?
+
+ Owncast's social features are accomplished by having your server join The{' '}
+
+ Fediverse
+
+ , a decentralized, open, collection of independent servers, like yours.
+
+ Please{' '}
+
+ read more
+ {' '}
+ about these features, the details behind them, and how they work.
+
+ What do you need to know?
+
+
+ These features are brand new. Given the variability of interfacing with the rest of the
+ world, bugs are possible. Please report anything that you think isn't working quite right.
+
+
You must always host your Owncast server with SSL using a https url.
+
+ You should not change your server name URL or social username once people begin following
+ you, as you will be seen as a completely different user on the Fediverse, and the old user
+ will disappear.
+
+
+ Turning on Private mode will allow you to manually approve each follower and limit
+ the visibility of your posts to followers only.
+
+
+ Learn more about The Fediverse
+
+ If these concepts are new you should discover more about what this functionality has to
+ offer. Visit{' '}
+
+ our documentation
+ {' '}
+ to be pointed at some resources that will help get you started on The Fediverse.
+
+
+ );
+}
+
+FederationInfoModal.propTypes = {
+ cancelPressed: PropTypes.func.isRequired,
+ okPressed: PropTypes.func.isRequired,
+};
+
+export default function ConfigFederation() {
+ const { Title } = Typography;
+ const [formDataValues, setFormDataValues] = useState(null);
+ const [isInfoModalOpen, setIsInfoModalOpen] = useState(false);
+ const serverStatusData = useContext(ServerStatusContext);
+ const { serverConfig, setFieldInConfigState } = serverStatusData || {};
+ const [blockedDomainSaveState, setBlockedDomainSaveState] = useState(null);
+
+ const { federation, yp, instanceDetails } = serverConfig;
+ const { enabled, isPrivate, username, goLiveMessage, showEngagement, blockedDomains } =
+ federation;
+ const { instanceUrl } = yp;
+ const { nsfw } = instanceDetails;
+
+ const handleFieldChange = ({ fieldName, value }: UpdateArgs) => {
+ setFormDataValues({
+ ...formDataValues,
+ [fieldName]: value,
+ });
+ };
+
+ const handleEnabledSwitchChange = (value: boolean) => {
+ if (!value) {
+ setFormDataValues({
+ ...formDataValues,
+ enabled: false,
+ });
+ } else {
+ setIsInfoModalOpen(true);
+ }
+ };
+
+ // if instanceUrl is empty, we should also turn OFF the `enabled` field of directory.
+ const handleSubmitInstanceUrl = () => {
+ const hasInstanceUrl = formDataValues.instanceUrl !== '';
+ const isInstanceUrlSecure = formDataValues.instanceUrl.startsWith('https://');
+
+ if (!hasInstanceUrl || !isInstanceUrlSecure) {
+ postConfigUpdateToAPI({
+ apiPath: FIELD_PROPS_ENABLE_FEDERATION.apiPath,
+ data: { value: false },
+ });
+ setFormDataValues({
+ ...formDataValues,
+ enabled: false,
+ });
+ }
+ };
+
+ function federationInfoModalCancelPressed() {
+ setIsInfoModalOpen(false);
+ setFormDataValues({
+ ...formDataValues,
+ enabled: false,
+ });
+ }
+
+ function federationInfoModalOkPressed() {
+ setIsInfoModalOpen(false);
+ setFormDataValues({
+ ...formDataValues,
+ enabled: true,
+ });
+ }
+
+ function resetBlockedDomainsSaveState() {
+ setBlockedDomainSaveState(null);
+ }
+
+ function saveBlockedDomains() {
+ try {
+ postConfigUpdateToAPI({
+ apiPath: API_FEDERATION_BLOCKED_DOMAINS,
+ data: { value: formDataValues.blockedDomains },
+ onSuccess: () => {
+ setFieldInConfigState({
+ fieldName: 'forbiddenUsernames',
+ value: formDataValues.forbiddenUsernames,
+ });
+ setBlockedDomainSaveState(STATUS_SUCCESS);
+ setTimeout(resetBlockedDomainsSaveState, RESET_TIMEOUT);
+ },
+ onError: (message: string) => {
+ setBlockedDomainSaveState(createInputStatus(STATUS_ERROR, message));
+ setTimeout(resetBlockedDomainsSaveState, RESET_TIMEOUT);
+ },
+ });
+ } catch (e) {
+ console.error(e);
+ setBlockedDomainSaveState(STATUS_ERROR);
+ }
+ }
+
+ function handleDeleteBlockedDomain(index: number) {
+ formDataValues.blockedDomains.splice(index, 1);
+ saveBlockedDomains();
+ }
+
+ function handleCreateBlockedDomain(domain: string) {
+ let newDomain;
+ try {
+ const u = new URL(domain);
+ newDomain = u.host;
+ } catch (_) {
+ newDomain = domain;
+ }
+
+ formDataValues.blockedDomains.push(newDomain);
+ handleFieldChange({
+ fieldName: 'blockedDomains',
+ value: formDataValues.blockedDomains,
+ });
+ saveBlockedDomains();
+ }
+
+ useEffect(() => {
+ setFormDataValues({
+ enabled,
+ isPrivate,
+ username,
+ goLiveMessage,
+ showEngagement,
+ blockedDomains,
+ nsfw,
+ instanceUrl: yp.instanceUrl,
+ });
+ }, [serverConfig, yp]);
+
+ if (!formDataValues) {
+ return null;
+ }
+
+ const hasInstanceUrl = instanceUrl !== '';
+ const isInstanceUrlSecure = instanceUrl.startsWith('https://');
+
+ return (
+
+ Configure Social Features
+
+ Owncast provides the ability for people to follow and engage with your instance. It's a
+ great way to promote alerting, sharing and engagement of your stream.
+
+
+ Once enabled you'll alert your followers when you go live as well as gain the ability to
+ compose custom posts to share any information you like.
+
+ Fediverse Actions
+
+ Below is a list of actions that were taken by others in response to your posts as well as
+ people who requested to follow you.
+
+ {makeTable(actions, columns)}
+
+ The following people are requesting to follow your Owncast server on the{' '}
+
+ Fediverse
+ {' '}
+ and be alerted to when you go live. Each must be approved.
+