Merge pull request #2748 from matrix-org/dbkr/shameless_plugging

Support linking to hosting providers
This commit is contained in:
David Baker 2019-03-05 18:22:02 +00:00 committed by GitHub
commit c1056025ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 167 additions and 18 deletions

View file

@ -62,6 +62,10 @@ limitations under the License.
mask-image: url('$(res)/img/icons-share.svg'); mask-image: url('$(res)/img/icons-share.svg');
} }
.mx_GroupView_hostingSignup img {
margin-left: 5px;
}
.mx_GroupView_editable { .mx_GroupView_editable {
border-bottom: 1px solid $strong-input-border-color ! important; border-bottom: 1px solid $strong-input-border-color ! important;
min-width: 150px; min-width: 150px;

View file

@ -15,17 +15,39 @@ limitations under the License.
*/ */
.mx_TopLeftMenu { .mx_TopLeftMenu {
min-width: 180px; min-width: 210px;
border-radius: 4px; border-radius: 4px;
.mx_TopLeftMenu_greyedText {
font-size: 12px;
opacity: 0.5;
}
.mx_TopLeftMenu_upgradeLink {
font-size: 12px;
img {
margin-left: 5px;
}
}
.mx_TopLeftMenu_section:not(:last-child) { .mx_TopLeftMenu_section:not(:last-child) {
border-bottom: 1px solid $menu-border-color; border-bottom: 1px solid $menu-border-color;
} }
.mx_TopLeftMenu_section { .mx_TopLeftMenu_section_noIcon {
list-style: none; margin: 5px 0;
padding: 5px 20px 5px 15px;
div:not(:first-child) {
margin-top: 5px;
}
}
.mx_TopLeftMenu_section_withIcon {
margin: 5px 0; margin: 5px 0;
padding: 0; padding: 0;
list-style: none;
li.mx_TopLeftMenu_icon_home::after { li.mx_TopLeftMenu_icon_home::after {
mask-image: url('$(res)/img/feather-customised/home.svg'); mask-image: url('$(res)/img/feather-customised/home.svg');

View file

@ -35,6 +35,14 @@ limitations under the License.
margin-top: 0; margin-top: 0;
} }
.mx_ProfileSettings_hostingSignup {
margin-left: 20px;
img {
margin-left: 5px;
}
}
.mx_ProfileSettings_avatar { .mx_ProfileSettings_avatar {
width: 88px; width: 88px;
height: 88px; height: 88px;

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="10" viewBox="0 0 11 10">
<g fill="none" fill-rule="evenodd" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round">
<path d="M8.5 5.5v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h3M7 .5h3v3M4.5 6L10 .5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 304 B

View file

@ -21,6 +21,7 @@ import Promise from 'bluebird';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
import sdk from '../../index'; import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import { getHostingLink } from '../../utils/HostingLink';
import { sanitizedHtmlNode } from '../../HtmlUtils'; import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t, _td } from '../../languageHandler'; import { _t, _td } from '../../languageHandler';
import AccessibleButton from '../views/elements/AccessibleButton'; import AccessibleButton from '../views/elements/AccessibleButton';
@ -816,6 +817,23 @@ export default React.createClass({
}); });
const header = this.state.editing ? <h2> { _t('Community Settings') } </h2> : <div />; const header = this.state.editing ? <h2> { _t('Community Settings') } </h2> : <div />;
const hostingSignupLink = getHostingLink('community-settings');
let hostingSignup = null;
if (hostingSignupLink) {
hostingSignup = <div className="mx_GroupView_hostingSignup">
{_t(
"Want more than a community? <a>Get your own server</a>", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noopener">
<img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;
}
const changeDelayWarning = this.state.editing && this.state.isUserPrivileged ? const changeDelayWarning = this.state.editing && this.state.isUserPrivileged ?
<div className="mx_GroupView_changeDelayWarning"> <div className="mx_GroupView_changeDelayWarning">
{ _t( { _t(
@ -830,6 +848,7 @@ export default React.createClass({
</div> : <div />; </div> : <div />;
return <div className={groupSettingsSectionClasses}> return <div className={groupSettingsSectionClasses}>
{ header } { header }
{ hostingSignup }
{ changeDelayWarning } { changeDelayWarning }
{ this._getJoinableNode() } { this._getJoinableNode() }
{ this._getLongDescriptionNode() } { this._getLongDescriptionNode() }

View file

@ -68,17 +68,18 @@ export default class TopLeftMenuButton extends React.Component {
} }
} }
render() { _getDisplayName() {
const fallbackUserId = MatrixClientPeg.get().getUserId();
const profileInfo = this.state.profileInfo;
let name;
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
name = _t("Guest"); return _t("Guest");
} else if (profileInfo) { } else if (this.state.profileInfo) {
name = profileInfo.name; return this.state.profileInfo.name;
} else { } else {
name = fallbackUserId; return MatrixClientPeg.get().getUserId();
} }
}
render() {
const name = this._getDisplayName();
let nameElement; let nameElement;
if (!this.props.collapsed) { if (!this.props.collapsed) {
nameElement = <div className="mx_TopLeftMenuButton_name"> nameElement = <div className="mx_TopLeftMenuButton_name">
@ -89,9 +90,9 @@ export default class TopLeftMenuButton extends React.Component {
return ( return (
<AccessibleButton className="mx_TopLeftMenuButton" onClick={this.onToggleMenu}> <AccessibleButton className="mx_TopLeftMenuButton" onClick={this.onToggleMenu}>
<BaseAvatar <BaseAvatar
idName={fallbackUserId} idName={MatrixClientPeg.get().getUserId()}
name={name} name={name}
url={profileInfo && profileInfo.avatarUrl} url={this.state.profileInfo && this.state.profileInfo.avatarUrl}
width={AVATAR_SIZE} width={AVATAR_SIZE}
height={AVATAR_SIZE} height={AVATAR_SIZE}
resizeMethod="crop" resizeMethod="crop"
@ -114,6 +115,8 @@ export default class TopLeftMenuButton extends React.Component {
chevronFace: "none", chevronFace: "none",
left: x, left: x,
top: y, top: y,
userId: MatrixClientPeg.get().getUserId(),
displayName: this._getDisplayName(),
onFinished: () => { onFinished: () => {
this.setState({ menuDisplayed: false }); this.setState({ menuDisplayed: false });
}, },

View file

@ -15,14 +15,22 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import LogoutDialog from "../dialogs/LogoutDialog"; import LogoutDialog from "../dialogs/LogoutDialog";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import { getHostingLink } from '../../../utils/HostingLink';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
export class TopLeftMenu extends React.Component { export class TopLeftMenu extends React.Component {
static propTypes = {
displayName: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
onFinished: PropTypes.func,
};
constructor() { constructor() {
super(); super();
this.viewHomePage = this.viewHomePage.bind(this); this.viewHomePage = this.viewHomePage.bind(this);
@ -46,27 +54,48 @@ export class TopLeftMenu extends React.Component {
render() { render() {
const isGuest = MatrixClientPeg.get().isGuest(); const isGuest = MatrixClientPeg.get().isGuest();
const hostingSignupLink = getHostingLink('user-context-menu');
let hostingSignup = null;
if (hostingSignupLink) {
hostingSignup = <div className="mx_TopLeftMenu_upgradeLink">
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noopener">
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;
}
let homePageSection = null; let homePageSection = null;
if (this.hasHomePage()) { if (this.hasHomePage()) {
homePageSection = <ul className="mx_TopLeftMenu_section"> homePageSection = <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>{_t("Home")}</li> <li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>{_t("Home")}</li>
</ul>; </ul>;
} }
let signInOutSection; let signInOutSection;
if (isGuest) { if (isGuest) {
signInOutSection = <ul className="mx_TopLeftMenu_section"> signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>{_t("Sign in")}</li> <li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>{_t("Sign in")}</li>
</ul>; </ul>;
} else { } else {
signInOutSection = <ul className="mx_TopLeftMenu_section"> signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>{_t("Sign out")}</li> <li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>{_t("Sign out")}</li>
</ul>; </ul>;
} }
return <div className="mx_TopLeftMenu"> return <div className="mx_TopLeftMenu">
<div className="mx_TopLeftMenu_section_noIcon">
<div>{this.props.displayName}</div>
<div className="mx_TopLeftMenu_greyedText">{this.props.userId}</div>
{hostingSignup}
</div>
{homePageSection} {homePageSection}
<ul className="mx_TopLeftMenu_section"> <ul className="mx_TopLeftMenu_section_withIcon">
<li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li> <li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li>
</ul> </ul>
{signInOutSection} {signInOutSection}

View file

@ -21,6 +21,7 @@ import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import classNames from 'classnames'; import classNames from 'classnames';
import {User} from "matrix-js-sdk"; import {User} from "matrix-js-sdk";
import { getHostingLink } from '../../../utils/HostingLink';
export default class ProfileSettings extends React.Component { export default class ProfileSettings extends React.Component {
constructor() { constructor() {
@ -137,13 +138,32 @@ export default class ProfileSettings extends React.Component {
</div> </div>
); );
const hostingSignupLink = getHostingLink('user-settings');
let hostingSignup = null;
if (hostingSignupLink) {
hostingSignup = <span className="mx_ProfileSettings_hostingSignup">
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noopener">
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</span>;
}
return ( return (
<form onSubmit={this._saveProfile} autoComplete={false} noValidate={true}> <form onSubmit={this._saveProfile} autoComplete={false} noValidate={true}>
<input type="file" ref="avatarUpload" className="mx_ProfileSettings_avatarUpload" <input type="file" ref="avatarUpload" className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" /> onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile"> <div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls"> <div className="mx_ProfileSettings_controls">
<p>{this.state.userId}</p> <p>
{this.state.userId}
{hostingSignup}
</p>
<Field id="profileDisplayName" label={_t("Display Name")} <Field id="profileDisplayName" label={_t("Display Name")}
type="text" value={this.state.displayName} autoComplete="off" type="text" value={this.state.displayName} autoComplete="off"
onChange={this._onDisplayNameChanged} /> onChange={this._onDisplayNameChanged} />

View file

@ -502,6 +502,7 @@
"Phone Number": "Phone Number", "Phone Number": "Phone Number",
"Profile picture": "Profile picture", "Profile picture": "Profile picture",
"Upload profile picture": "Upload profile picture", "Upload profile picture": "Upload profile picture",
"<a>Upgrade</a> to your own domain": "<a>Upgrade</a> to your own domain",
"Display Name": "Display Name", "Display Name": "Display Name",
"Save": "Save", "Save": "Save",
"Flair": "Flair", "Flair": "Flair",
@ -1317,6 +1318,7 @@
"Leave %(groupName)s?": "Leave %(groupName)s?", "Leave %(groupName)s?": "Leave %(groupName)s?",
"Unable to leave community": "Unable to leave community", "Unable to leave community": "Unable to leave community",
"Community Settings": "Community Settings", "Community Settings": "Community Settings",
"Want more than a community? <a>Get your own server</a>": "Want more than a community? <a>Get your own server</a>",
"Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.": "Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.", "Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.": "Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.",
"These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.", "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.",
"Featured Rooms:": "Featured Rooms:", "Featured Rooms:": "Featured Rooms:",

37
src/utils/HostingLink.js Normal file
View file

@ -0,0 +1,37 @@
/*
Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import url from 'url';
import qs from 'qs';
import SdkConfig from '../SdkConfig';
export function getHostingLink(campaign) {
const hostingLink = SdkConfig.get().hosting_signup_link;
if (!hostingLink) return null;
if (!campaign) return hostingLink;
try {
const hostingUrl = url.parse(hostingLink);
const params = qs.parse(hostingUrl.query);
params.utm_campaign = campaign;
hostingUrl.search = undefined;
hostingUrl.query = params;
return hostingUrl.format();
} catch (e) {
return hostingLink;
}
}