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

View File

@ -44,6 +44,8 @@ import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
import { Action } from "./dispatcher/actions";
import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership";
import SdkConfig from "./SdkConfig";
import SettingsStore from "./settings/SettingsStore";
import {UIFeature} from "./settings/UIFeature";
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
interface HTMLInputEvent extends Event {
@ -797,6 +799,7 @@ export const Commands = [
command: 'addwidget',
args: '<url | embed code | Jitsi url>',
description: _td('Adds a custom widget by URL to the room'),
isEnabled: () => SettingsStore.getValue(UIFeature.Widgets),
runFn: function(roomId, widgetUrl) {
if (!widgetUrl) {
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 CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog";
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
import {UIFeature} from "../../settings/UIFeature";
/** constants for MatrixChat.state.view */
export enum Views {
@ -1233,8 +1234,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
StorageManager.tryPersistStorage();
if (SettingsStore.getValue("showCookieBar") && this.props.config.piwik && navigator.doNotTrack !== "1") {
showAnalyticsToast(this.props.config.piwik && this.props.config.piwik.policyUrl);
if (SettingsStore.getValue("showCookieBar") && Analytics.canEnable()) {
showAnalyticsToast(this.props.config.piwik?.policyUrl);
}
}
@ -1372,15 +1373,19 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
ready: true,
});
});
cli.on('Call.incoming', function(call) {
// we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if
// we get an immediate hangup, we don't get a stuck call)
dis.dispatch({
action: 'incoming_call',
call: call,
}, true);
});
if (SettingsStore.getValue(UIFeature.Voip)) {
cli.on('Call.incoming', function(call) {
// we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if
// we get an immediate hangup, we don't get a stuck call)
dis.dispatch({
action: 'incoming_call',
call: call,
}, true);
});
}
cli.on('Session.logged_out', function(errObj) {
if (Lifecycle.isLoggingOut()) return;

View File

@ -70,10 +70,10 @@ export default class RoomDirectory extends React.Component {
this.scrollPanel = null;
this.protocols = null;
this.setState({protocolsLoading: true});
this.state.protocolsLoading = true;
if (!MatrixClientPeg.get()) {
// We may not have a client yet when invoked from welcome page
this.setState({protocolsLoading: false});
this.state.protocolsLoading = false;
return;
}
@ -102,14 +102,16 @@ export default class RoomDirectory extends React.Component {
});
} else {
// 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
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
this.setState({communityName: profile.name});
});
}
}
componentDidMount() {
this.refreshRoomList();
}

View File

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

View File

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

View File

@ -20,6 +20,7 @@ import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
/**
* 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) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
return <div className="mx_ErrorBoundary">
<div className="mx_ErrorBoundary_body">
<h1>{_t("Something went wrong!")}</h1>
let bugReportSection;
if (SdkConfig.get().bug_report_endpoint_url) {
bugReportSection = <React.Fragment>
<p>{_t(
"Please <newIssueLink>create a new issue</newIssueLink> " +
"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'>
{_t("Submit debug logs")}
</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'>
{_t("Clear cache and reload")}
</AccessibleButton>

View File

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

View File

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

View File

@ -952,30 +952,26 @@ function useRoomPermissions(cli, room, user) {
const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => {
const [isEditing, setEditing] = useState(false);
if (room && user.roomId) { // is in room
if (isEditing) {
return (<PowerLevelEditor
user={user} room={room} roomPermissions={roomPermissions}
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>
);
}
if (isEditing) {
return (<PowerLevelEditor
user={user} room={room} roomPermissions={roomPermissions}
onFinished={() => setEditing(false)} />);
} 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" />;
}
const memberDetails = (
<PowerLevelSection
let memberDetails;
if (room && member.roomId) {
memberDetails = <PowerLevelSection
powerLevels={powerLevels}
user={member}
room={room}
roomPermissions={roomPermissions}
/>
);
/>;
}
// only display the devices list if our client supports E2E
const cryptoEnabled = cli.isCryptoEnabled();

View File

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

View File

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

View File

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

View File

@ -329,6 +329,29 @@ export default class SecurityUserSettingsTab extends React.Component {
</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 (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
{warning}
@ -357,24 +380,7 @@ export default class SecurityUserSettingsTab extends React.Component {
{crossSigning}
{this._renderCurrentDeviceInfo()}
</div>
<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>
{ privacySection }
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_section">
{this._renderIgnoredUsers()}

View File

@ -912,13 +912,13 @@
"Message search": "Message search",
"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.",
"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",
"%(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.",
"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",
"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.",
@ -1440,8 +1440,8 @@
"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": "edited",
"Can't load this message": "Can't load this message",
"Submit logs": "Submit logs",
"Can't load this message": "Can't load this message",
"Failed to load group members": "Failed to load group 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?",
@ -2132,10 +2132,10 @@
"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",
"Failed to find the general chat for this community": "Failed to find the general chat for this community",
"Feedback": "Feedback",
"Notification settings": "Notification settings",
"Security & privacy": "Security & privacy",
"All settings": "All settings",
"Feedback": "Feedback",
"Community settings": "Community settings",
"User settings": "User settings",
"Switch to light mode": "Switch to light mode",

View File

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

View File

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