Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/15178

 Conflicts:
	src/settings/Settings.ts
	src/settings/UIFeature.ts
pull/21833/head
Michael Telatynski 2020-09-17 13:31:00 +01:00
commit 5bf6697e48
17 changed files with 167 additions and 100 deletions

View File

@ -170,15 +170,19 @@ class Analytics {
return !this.baseUrl; return !this.baseUrl;
} }
canEnable() {
const config = SdkConfig.get();
return navigator.doNotTrack !== "1" && config && config.piwik && config.piwik.url && config.piwik.siteId;
}
/** /**
* Enable Analytics if initialized but disabled * Enable Analytics if initialized but disabled
* otherwise try and initalize, no-op if piwik config missing * otherwise try and initalize, no-op if piwik config missing
*/ */
async enable() { async enable() {
if (!this.disabled) return; if (!this.disabled) return;
if (!this.canEnable()) return;
const config = SdkConfig.get(); const config = SdkConfig.get();
if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return;
this.baseUrl = new URL("piwik.php", config.piwik.url); this.baseUrl = new URL("piwik.php", config.piwik.url);
// set constants // set constants

View File

@ -44,6 +44,8 @@ import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
import { Action } from "./dispatcher/actions"; import { Action } from "./dispatcher/actions";
import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership"; import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership";
import SdkConfig from "./SdkConfig"; import SdkConfig from "./SdkConfig";
import SettingsStore from "./settings/SettingsStore";
import {UIFeature} from "./settings/UIFeature";
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
interface HTMLInputEvent extends Event { interface HTMLInputEvent extends Event {
@ -797,6 +799,7 @@ export const Commands = [
command: 'addwidget', command: 'addwidget',
args: '<url | embed code | Jitsi url>', args: '<url | embed code | Jitsi url>',
description: _td('Adds a custom widget by URL to the room'), description: _td('Adds a custom widget by URL to the room'),
isEnabled: () => SettingsStore.getValue(UIFeature.Widgets),
runFn: function(roomId, widgetUrl) { runFn: function(roomId, widgetUrl) {
if (!widgetUrl) { if (!widgetUrl) {
return reject(_t("Please supply a widget URL or embed code")); return reject(_t("Please supply a widget URL or embed code"));

View File

@ -79,6 +79,7 @@ import { SettingLevel } from "../../settings/SettingLevel";
import { leaveRoomBehaviour } from "../../utils/membership"; import { leaveRoomBehaviour } from "../../utils/membership";
import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog"; import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog";
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore"; import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
import {UIFeature} from "../../settings/UIFeature";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -1233,8 +1234,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
StorageManager.tryPersistStorage(); StorageManager.tryPersistStorage();
if (SettingsStore.getValue("showCookieBar") && this.props.config.piwik && navigator.doNotTrack !== "1") { if (SettingsStore.getValue("showCookieBar") && Analytics.canEnable()) {
showAnalyticsToast(this.props.config.piwik && this.props.config.piwik.policyUrl); showAnalyticsToast(this.props.config.piwik?.policyUrl);
} }
} }
@ -1372,15 +1373,19 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
ready: true, ready: true,
}); });
}); });
cli.on('Call.incoming', function(call) {
// we dispatch this synchronously to make sure that the event if (SettingsStore.getValue(UIFeature.Voip)) {
// handlers on the call are set up immediately (so that if cli.on('Call.incoming', function(call) {
// we get an immediate hangup, we don't get a stuck call) // we dispatch this synchronously to make sure that the event
dis.dispatch({ // handlers on the call are set up immediately (so that if
action: 'incoming_call', // we get an immediate hangup, we don't get a stuck call)
call: call, dis.dispatch({
}, true); action: 'incoming_call',
}); call: call,
}, true);
});
}
cli.on('Session.logged_out', function(errObj) { cli.on('Session.logged_out', function(errObj) {
if (Lifecycle.isLoggingOut()) return; if (Lifecycle.isLoggingOut()) return;

View File

@ -70,10 +70,10 @@ export default class RoomDirectory extends React.Component {
this.scrollPanel = null; this.scrollPanel = null;
this.protocols = null; this.protocols = null;
this.setState({protocolsLoading: true}); this.state.protocolsLoading = true;
if (!MatrixClientPeg.get()) { if (!MatrixClientPeg.get()) {
// We may not have a client yet when invoked from welcome page // We may not have a client yet when invoked from welcome page
this.setState({protocolsLoading: false}); this.state.protocolsLoading = false;
return; return;
} }
@ -102,14 +102,16 @@ export default class RoomDirectory extends React.Component {
}); });
} else { } else {
// We don't use the protocols in the communities v2 prototype experience // We don't use the protocols in the communities v2 prototype experience
this.setState({protocolsLoading: false}); this.state.protocolsLoading = false;
// Grab the profile info async // Grab the profile info async
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => { FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
this.setState({communityName: profile.name}); this.setState({communityName: profile.name});
}); });
} }
}
componentDidMount() {
this.refreshRoomList(); this.refreshRoomList();
} }

View File

@ -50,6 +50,7 @@ import dis from "../../dispatcher/dispatcher";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import ErrorDialog from "../views/dialogs/ErrorDialog"; import ErrorDialog from "../views/dialogs/ErrorDialog";
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog"; import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
import {UIFeature} from "../../settings/UIFeature";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -285,6 +286,15 @@ export default class UserMenu extends React.Component<IProps, IState> {
); );
} }
let feedbackButton;
if (SettingsStore.getValue(UIFeature.Feedback)) {
feedbackButton = <IconizedContextMenuOption
iconClassName="mx_UserMenu_iconMessage"
label={_t("Feedback")}
onClick={this.onProvideFeedback}
/>;
}
let primaryHeader = ( let primaryHeader = (
<div className="mx_UserMenu_contextMenu_name"> <div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName"> <span className="mx_UserMenu_contextMenu_displayName">
@ -319,11 +329,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
label={_t("Archived rooms")} label={_t("Archived rooms")}
onClick={this.onShowArchived} onClick={this.onShowArchived}
/> */} /> */}
<IconizedContextMenuOption { feedbackButton }
iconClassName="mx_UserMenu_iconMessage"
label={_t("Feedback")}
onClick={this.onProvideFeedback}
/>
</IconizedContextMenuOptionList> </IconizedContextMenuOptionList>
<IconizedContextMenuOptionList red> <IconizedContextMenuOptionList red>
<IconizedContextMenuOption <IconizedContextMenuOption
@ -384,11 +390,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
aria-label={_t("User settings")} aria-label={_t("User settings")}
onClick={(e) => this.onSettingsOpen(e, null)} onClick={(e) => this.onSettingsOpen(e, null)}
/> />
<IconizedContextMenuOption { feedbackButton }
iconClassName="mx_UserMenu_iconMessage"
label={_t("Feedback")}
onClick={this.onProvideFeedback}
/>
</IconizedContextMenuOptionList> </IconizedContextMenuOptionList>
<IconizedContextMenuOptionList red> <IconizedContextMenuOptionList red>
<IconizedContextMenuOption <IconizedContextMenuOption

View File

@ -107,12 +107,16 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_preferencesIcon", "mx_UserSettingsDialog_preferencesIcon",
<PreferencesUserSettingsTab />, <PreferencesUserSettingsTab />,
)); ));
tabs.push(new Tab(
USER_VOICE_TAB, if (SettingsStore.getValue(UIFeature.Voip)) {
_td("Voice & Video"), tabs.push(new Tab(
"mx_UserSettingsDialog_voiceIcon", USER_VOICE_TAB,
<VoiceUserSettingsTab />, _td("Voice & Video"),
)); "mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />,
));
}
tabs.push(new Tab( tabs.push(new Tab(
USER_SECURITY_TAB, USER_SECURITY_TAB,
_td("Security & Privacy"), _td("Security & Privacy"),

View File

@ -20,6 +20,7 @@ import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg'; import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
/** /**
* This error boundary component can be used to wrap large content areas and * This error boundary component can be used to wrap large content areas and
@ -73,9 +74,10 @@ export default class ErrorBoundary extends React.PureComponent {
if (this.state.error) { if (this.state.error) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new"; const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
return <div className="mx_ErrorBoundary">
<div className="mx_ErrorBoundary_body"> let bugReportSection;
<h1>{_t("Something went wrong!")}</h1> if (SdkConfig.get().bug_report_endpoint_url) {
bugReportSection = <React.Fragment>
<p>{_t( <p>{_t(
"Please <newIssueLink>create a new issue</newIssueLink> " + "Please <newIssueLink>create a new issue</newIssueLink> " +
"on GitHub so that we can investigate this bug.", {}, { "on GitHub so that we can investigate this bug.", {}, {
@ -94,6 +96,13 @@ export default class ErrorBoundary extends React.PureComponent {
<AccessibleButton onClick={this._onBugReport} kind='primary'> <AccessibleButton onClick={this._onBugReport} kind='primary'>
{_t("Submit debug logs")} {_t("Submit debug logs")}
</AccessibleButton> </AccessibleButton>
</React.Fragment>;
}
return <div className="mx_ErrorBoundary">
<div className="mx_ErrorBoundary_body">
<h1>{_t("Something went wrong!")}</h1>
{ bugReportSection }
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'> <AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")} {_t("Clear cache and reload")}
</AccessibleButton> </AccessibleButton>

View File

@ -19,6 +19,7 @@ import classNames from 'classnames';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
export default class TileErrorBoundary extends React.Component { export default class TileErrorBoundary extends React.Component {
constructor(props) { constructor(props) {
@ -54,14 +55,20 @@ export default class TileErrorBoundary extends React.Component {
mx_EventTile_content: true, mx_EventTile_content: true,
mx_EventTile_tileError: true, mx_EventTile_tileError: true,
}; };
let submitLogsButton;
if (SdkConfig.get().bug_report_endpoint_url) {
submitLogsButton = <a onClick={this._onBugReport} href="#">
{_t("Submit logs")}
</a>;
}
return (<div className={classNames(classes)}> return (<div className={classNames(classes)}>
<div className="mx_EventTile_line"> <div className="mx_EventTile_line">
<span> <span>
{_t("Can't load this message")} {_t("Can't load this message")}
{ mxEvent && ` (${mxEvent.getType()})` } { mxEvent && ` (${mxEvent.getType()})` }
<a onClick={this._onBugReport} href="#"> { submitLogsButton }
{_t("Submit logs")}
</a>
</span> </span>
</div> </div>
</div>); </div>);

View File

@ -42,6 +42,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import WidgetStore, {IApp} from "../../../stores/WidgetStore"; import WidgetStore, {IApp} from "../../../stores/WidgetStore";
import { E2EStatus } from "../../../utils/ShieldUtils"; import { E2EStatus } from "../../../utils/ShieldUtils";
import RoomContext from "../../../contexts/RoomContext"; import RoomContext from "../../../contexts/RoomContext";
import {UIFeature} from "../../../settings/UIFeature";
interface IProps { interface IProps {
room: Room; room: Room;
@ -242,7 +243,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
</Button> </Button>
</Group> </Group>
<AppsSection room={room} /> { SettingsStore.getValue(UIFeature.Widgets) && <AppsSection room={room} /> }
</BaseCard>; </BaseCard>;
}; };

View File

@ -952,30 +952,26 @@ function useRoomPermissions(cli, room, user) {
const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => { const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => {
const [isEditing, setEditing] = useState(false); const [isEditing, setEditing] = useState(false);
if (room && user.roomId) { // is in room if (isEditing) {
if (isEditing) { return (<PowerLevelEditor
return (<PowerLevelEditor user={user} room={room} roomPermissions={roomPermissions}
user={user} room={room} roomPermissions={roomPermissions} onFinished={() => setEditing(false)} />);
onFinished={() => setEditing(false)} />);
} else {
const IconButton = sdk.getComponent('elements.IconButton');
const powerLevelUsersDefault = powerLevels.users_default || 0;
const powerLevel = parseInt(user.powerLevel, 10);
const modifyButton = roomPermissions.canEdit ?
(<IconButton icon="edit" onClick={() => setEditing(true)} />) : null;
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
const label = _t("<strong>%(role)s</strong> in %(roomName)s",
{role, roomName: room.name},
{strong: label => <strong>{label}</strong>},
);
return (
<div className="mx_UserInfo_profileField">
<div className="mx_UserInfo_roleDescription">{label}{modifyButton}</div>
</div>
);
}
} else { } else {
return null; const IconButton = sdk.getComponent('elements.IconButton');
const powerLevelUsersDefault = powerLevels.users_default || 0;
const powerLevel = parseInt(user.powerLevel, 10);
const modifyButton = roomPermissions.canEdit ?
(<IconButton icon="edit" onClick={() => setEditing(true)} />) : null;
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
const label = _t("<strong>%(role)s</strong> in %(roomName)s",
{role, roomName: room.name},
{strong: label => <strong>{label}</strong>},
);
return (
<div className="mx_UserInfo_profileField">
<div className="mx_UserInfo_roleDescription">{label}{modifyButton}</div>
</div>
);
} }
}; };
@ -1268,14 +1264,15 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />; spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
} }
const memberDetails = ( let memberDetails;
<PowerLevelSection if (room && member.roomId) {
memberDetails = <PowerLevelSection
powerLevels={powerLevels} powerLevels={powerLevels}
user={member} user={member}
room={room} room={room}
roomPermissions={roomPermissions} roomPermissions={roomPermissions}
/> />;
); }
// only display the devices list if our client supports E2E // only display the devices list if our client supports E2E
const cryptoEnabled = cli.isCryptoEnabled(); const cryptoEnabled = cli.isCryptoEnabled();

View File

@ -28,6 +28,7 @@ import RateLimitedFunc from '../../../ratelimitedfunc';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import CallView from "../voip/CallView"; import CallView from "../voip/CallView";
import {UIFeature} from "../../../settings/UIFeature";
export default class AuxPanel extends React.Component { export default class AuxPanel extends React.Component {
@ -198,18 +199,21 @@ export default class AuxPanel extends React.Component {
/> />
); );
const appsDrawer = <AppsDrawer let appsDrawer;
room={this.props.room} if (SettingsStore.getValue(UIFeature.Widgets)) {
userId={this.props.userId} appsDrawer = <AppsDrawer
maxHeight={this.props.maxHeight} room={this.props.room}
showApps={this.props.showApps} userId={this.props.userId}
hide={this.props.hideAppsDrawer} maxHeight={this.props.maxHeight}
resizeNotifier={this.props.resizeNotifier} showApps={this.props.showApps}
/>; hide={this.props.hideAppsDrawer}
resizeNotifier={this.props.resizeNotifier}
/>;
}
let stateViews = null; let stateViews = null;
if (this.state.counters && SettingsStore.getValue("feature_state_counters")) { if (this.state.counters && SettingsStore.getValue("feature_state_counters")) {
let counters = []; const counters = [];
this.state.counters.forEach((counter, idx) => { this.state.counters.forEach((counter, idx) => {
const title = counter.title; const title = counter.title;
@ -218,7 +222,7 @@ export default class AuxPanel extends React.Component {
const severity = counter.severity; const severity = counter.severity;
const stateKey = counter.stateKey; const stateKey = counter.stateKey;
let span = <span>{ title }: { value }</span> let span = <span>{ title }: { value }</span>;
if (link) { if (link) {
span = ( span = (

View File

@ -31,6 +31,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu"; import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import ReplyPreview from "./ReplyPreview"; import ReplyPreview from "./ReplyPreview";
import {UIFeature} from "../../../settings/UIFeature";
function ComposerAvatar(props) { function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@ -384,9 +385,12 @@ export default class MessageComposer extends React.Component {
permalinkCreator={this.props.permalinkCreator} />, permalinkCreator={this.props.permalinkCreator} />,
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />, <UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />, <EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />,
); );
if (SettingsStore.getValue(UIFeature.Widgets)) {
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
}
if (this.state.showCallButtons) { if (this.state.showCallButtons) {
if (callInProgress) { if (callInProgress) {
controls.push( controls.push(

View File

@ -37,6 +37,7 @@ import {abbreviateUrl} from "../../../../../utils/UrlUtils";
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids'; import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
import Spinner from "../../../elements/Spinner"; import Spinner from "../../../elements/Spinner";
import {SettingLevel} from "../../../../../settings/SettingLevel"; import {SettingLevel} from "../../../../../settings/SettingLevel";
import {UIFeature} from "../../../../../settings/UIFeature";
export default class GeneralUserSettingsTab extends React.Component { export default class GeneralUserSettingsTab extends React.Component {
static propTypes = { static propTypes = {
@ -366,6 +367,8 @@ export default class GeneralUserSettingsTab extends React.Component {
} }
_renderIntegrationManagerSection() { _renderIntegrationManagerSection() {
if (!SettingsStore.getValue(UIFeature.Widgets)) return null;
const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager"); const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager");
return ( return (

View File

@ -329,6 +329,29 @@ export default class SecurityUserSettingsTab extends React.Component {
</div>; </div>;
} }
let privacySection;
if (Analytics.canEnable()) {
privacySection = <React.Fragment>
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<div className="mx_SettingsTab_subsectionText">
{_t(
"%(brand)s collects anonymous analytics to allow us to improve the application.",
{ brand },
)}
&nbsp;
{_t("Privacy is important to us, so we don't collect any personal or " +
"identifiable data for our analytics.")}
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
{_t("Learn more about how we use analytics.")}
</AccessibleButton>
</div>
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this._updateAnalytics} />
</div>
</React.Fragment>;
}
return ( return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab"> <div className="mx_SettingsTab mx_SecurityUserSettingsTab">
{warning} {warning}
@ -357,24 +380,7 @@ export default class SecurityUserSettingsTab extends React.Component {
{crossSigning} {crossSigning}
{this._renderCurrentDeviceInfo()} {this._renderCurrentDeviceInfo()}
</div> </div>
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div> { privacySection }
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t(
"%(brand)s collects anonymous analytics to allow us to improve the application.",
{ brand },
)}
&nbsp;
{_t("Privacy is important to us, so we don't collect any personal or " +
"identifiable data for our analytics.")}
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
{_t("Learn more about how we use analytics.")}
</AccessibleButton>
</div>
<SettingsFlag name='analyticsOptIn' level={SettingLevel.DEVICE}
onChange={this._updateAnalytics} />
</div>
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div> <div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
{this._renderIgnoredUsers()} {this._renderIgnoredUsers()}

View File

@ -912,13 +912,13 @@
"Message search": "Message search", "Message search": "Message search",
"Cross-signing": "Cross-signing", "Cross-signing": "Cross-signing",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.", "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
"Where youre logged in": "Where youre logged in",
"Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.",
"A session's public name is visible to people you communicate with": "A session's public name is visible to people you communicate with",
"Privacy": "Privacy", "Privacy": "Privacy",
"%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s collects anonymous analytics to allow us to improve the application.", "%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s collects anonymous analytics to allow us to improve the application.",
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.",
"Learn more about how we use analytics.": "Learn more about how we use analytics.", "Learn more about how we use analytics.": "Learn more about how we use analytics.",
"Where youre logged in": "Where youre logged in",
"Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.",
"A session's public name is visible to people you communicate with": "A session's public name is visible to people you communicate with",
"No media permissions": "No media permissions", "No media permissions": "No media permissions",
"You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam", "You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam",
"Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.",
@ -1440,8 +1440,8 @@
"Click to view edits": "Click to view edits", "Click to view edits": "Click to view edits",
"Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.", "Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.",
"edited": "edited", "edited": "edited",
"Can't load this message": "Can't load this message",
"Submit logs": "Submit logs", "Submit logs": "Submit logs",
"Can't load this message": "Can't load this message",
"Failed to load group members": "Failed to load group members", "Failed to load group members": "Failed to load group members",
"Filter community members": "Filter community members", "Filter community members": "Filter community members",
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?",
@ -2132,10 +2132,10 @@
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
"Failed to find the general chat for this community": "Failed to find the general chat for this community", "Failed to find the general chat for this community": "Failed to find the general chat for this community",
"Feedback": "Feedback",
"Notification settings": "Notification settings", "Notification settings": "Notification settings",
"Security & privacy": "Security & privacy", "Security & privacy": "Security & privacy",
"All settings": "All settings", "All settings": "All settings",
"Feedback": "Feedback",
"Community settings": "Community settings", "Community settings": "Community settings",
"User settings": "User settings", "User settings": "User settings",
"Switch to light mode": "Switch to light mode", "Switch to light mode": "Switch to light mode",

View File

@ -588,6 +588,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
"showCallButtonsInComposer": { "showCallButtonsInComposer": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
default: true, default: true,
controller: new UIFeatureController(UIFeature.Voip),
}, },
"e2ee.manuallyVerifyAllSessions": { "e2ee.manuallyVerifyAllSessions": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
@ -618,6 +619,18 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_UI_FEATURE, supportedLevels: LEVELS_UI_FEATURE,
default: true, default: true,
}, },
[UIFeature.Widgets]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
[UIFeature.Voip]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
[UIFeature.Feedback]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
[UIFeature.Flair]: { [UIFeature.Flair]: {
supportedLevels: LEVELS_UI_FEATURE, supportedLevels: LEVELS_UI_FEATURE,
default: true, default: true,

View File

@ -17,5 +17,8 @@ limitations under the License.
// see settings.md for documentation on conventions // see settings.md for documentation on conventions
export enum UIFeature { export enum UIFeature {
URLPreviews = "UIFeature.urlPreviews", URLPreviews = "UIFeature.urlPreviews",
Widgets = "UIFeature.widgets",
Voip = "UIFeature.voip",
Feedback = "UIFeature.feedback",
Flair = "UIFeature.flair", Flair = "UIFeature.flair",
} }