Merge branch 'master' into compact-posts

This commit is contained in:
Toby Zerner
2015-09-15 11:27:49 +09:30
55 changed files with 1465 additions and 461 deletions

View File

@ -44,6 +44,7 @@ export default class AppearancePage extends Component {
{Button.component({
className: 'Button Button--primary',
type: 'submit',
children: 'Save Changes',
loading: this.loading
})}

View File

@ -30,6 +30,7 @@ export default class EditCustomCssModal extends Modal {
<div className="Form-group">
{Button.component({
className: 'Button Button--primary',
type: 'submit',
children: 'Save Changes',
loading: this.loading
})}

View File

@ -4,6 +4,7 @@ import Search from 'flarum/components/Search';
import Composer from 'flarum/components/Composer';
import ReplyComposer from 'flarum/components/ReplyComposer';
import DiscussionPage from 'flarum/components/DiscussionPage';
import SignUpModal from 'flarum/components/SignUpModal';
export default class ForumApp extends App {
constructor(...args) {
@ -76,4 +77,27 @@ export default class ForumApp extends App {
return this.current instanceof DiscussionPage &&
this.current.discussion === discussion;
}
/**
* Callback for when an external authenticator (social login) action has
* completed.
*
* If the payload indicates that the user has been logged in, then the page
* will be reloaded. Otherwise, a SignUpModal will be opened, prefilled
* with the provided details.
*
* @param {Object} payload A dictionary of props to pass into the sign up
* modal. A truthy `authenticated` prop indicates that the user has logged
* in, and thus the page is reloaded.
* @public
*/
authenticationComplete(payload) {
if (payload.authenticated) {
window.location.reload();
} else {
const modal = new SignUpModal(payload);
this.modal.show(modal);
modal.$('[name=password]').focus();
}
}
}

View File

@ -1,66 +0,0 @@
import Component from 'flarum/Component';
import humanTime from 'flarum/helpers/humanTime';
import avatar from 'flarum/helpers/avatar';
/**
* The `Activity` component represents a piece of activity of a user's activity
* feed. Subclasses should implement the `description` and `content` methods.
*
* ### Props
*
* - `activity`
*
* @abstract
*/
export default class Activity extends Component {
view() {
const activity = this.props.activity;
return (
<div className="Activity">
{avatar(this.user(), {className: 'Activity-avatar'})}
<div className="Activity-header">
<strong className="Activity-description">{this.description()}</strong>
{humanTime(this.time())}
</div>
{this.content()}
</div>
);
}
/**
* Get the user whose avatar should be displayed.
*
* @return {User}
* @abstract
*/
user() {
}
/**
* Get the time of the activity.
*
* @return {Date}
* @abstract
*/
time() {
}
/**
* Get the description of the activity.
*
* @return {VirtualElement}
*/
description() {
}
/**
* Get the content to show below the activity description.
*
* @return {VirtualElement}
*/
content() {
}
}

View File

@ -0,0 +1,30 @@
import Button from 'flarum/components/Button';
/**
* The `LogInButton` component displays a social login button which will open
* a popup window containing the specified path.
*
* ### Props
*
* - `path`
*/
export default class LogInButton extends Button {
static initProps(props) {
props.className = (props.className || '') + ' LogInButton';
props.onclick = function() {
const width = 620;
const height = 400;
const $window = $(window);
window.open(app.forum.attribute('baseUrl') + props.path, 'logInPopup',
`width=${width},` +
`height=${height},` +
`top=${$window.height() / 2 - height / 2},` +
`left=${$window.width() / 2 - width / 2},` +
'status=no,scrollbars=no,resizable=no');
};
super.initProps(props);
}
}

View File

@ -0,0 +1,25 @@
import Component from 'flarum/Component';
import ItemList from 'flarum/utils/ItemList';
/**
* The `LogInButtons` component displays a collection of social login buttons.
*/
export default class LogInButtons extends Component {
view() {
return (
<div className="LogInButtons">
{this.items().toArray()}
</div>
);
}
/**
* Build a list of LogInButton components.
*
* @return {ItemList}
* @public
*/
items() {
return new ItemList();
}
}

View File

@ -3,6 +3,7 @@ import ForgotPasswordModal from 'flarum/components/ForgotPasswordModal';
import SignUpModal from 'flarum/components/SignUpModal';
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import LogInButtons from 'flarum/components/LogInButtons';
/**
* The `LogInModal` component displays a modal dialog with a login form.
@ -42,6 +43,8 @@ export default class LogInModal extends Modal {
content() {
return [
<div className="Modal-body">
<LogInButtons/>
<div className="Form Form--centered">
<div className="Form-group">
<input className="FormControl" name="email" placeholder={app.trans('core.username_or_email')}
@ -71,6 +74,7 @@ export default class LogInModal extends Modal {
<p className="LogInModal-forgotPassword">
<a onclick={this.forgotPassword.bind(this)}>{app.trans('core.forgot_password_link')}</a>
</p>
{app.forum.attribute('allowSignUp') ? (
<p className="LogInModal-signUp">
{app.trans('core.before_sign_up_link')}{' '}
@ -84,6 +88,8 @@ export default class LogInModal extends Modal {
/**
* Open the forgot password modal, prefilling it with an email if the user has
* entered one.
*
* @public
*/
forgotPassword() {
const email = this.email();
@ -95,6 +101,8 @@ export default class LogInModal extends Modal {
/**
* Open the sign up modal, prefilling it with an email/username/password if
* the user has entered one.
*
* @public
*/
signUp() {
const props = {password: this.password()};

View File

@ -2,6 +2,7 @@ import Modal from 'flarum/components/Modal';
import LogInModal from 'flarum/components/LogInModal';
import avatar from 'flarum/helpers/avatar';
import Button from 'flarum/components/Button';
import LogInButtons from 'flarum/components/LogInButtons';
/**
* The `SignUpModal` component displays a modal dialog with a singup form.
@ -11,6 +12,7 @@ import Button from 'flarum/components/Button';
* - `username`
* - `email`
* - `password`
* - `token` An email token to sign up with.
*/
export default class SignUpModal extends Modal {
constructor(...args) {
@ -65,7 +67,9 @@ export default class SignUpModal extends Modal {
}
body() {
const body = [(
const body = [
this.props.token ? '' : <LogInButtons/>,
<div className="Form Form--centered">
<div className="Form-group">
<input className="FormControl" name="username" placeholder={app.trans('core.username')}
@ -78,26 +82,28 @@ export default class SignUpModal extends Modal {
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.email')}
value={this.email()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading} />
disabled={this.loading || this.props.token} />
</div>
<div className="Form-group">
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.password')}
value={this.password()}
onchange={m.withAttr('value', this.password)}
disabled={this.loading} />
</div>
{this.props.token ? '' : (
<div className="Form-group">
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.password')}
value={this.password()}
onchange={m.withAttr('value', this.password)}
disabled={this.loading} />
</div>
)}
<div className="Form-group">
{Button.component({
className: 'Button Button--primary Button--block',
type: 'submit',
loading: this.loading,
children: app.trans('core.sign_up')
})}
<Button
className="Button Button--primary Button--block"
type="submit"
loading={this.loading}>
{app.trans('core.sign_up')}
</Button>
</div>
</div>
)];
];
if (this.welcomeUser) {
const user = this.welcomeUser;
@ -115,20 +121,12 @@ export default class SignUpModal extends Modal {
{avatar(user)}
<h3>{app.trans('core.welcome_user', {user})}</h3>
{!user.isActivated() ? [
<p>{app.trans('core.confirmation_email_sent', {email: <strong>{user.email()}</strong>})}</p>,
<p>
<a href={`http://${emailProviderName}`} className="Button Button--primary" target="_blank">
{app.trans('core.go_to', {location: emailProviderName})}
</a>
</p>
] : (
<p>
<button className="Button Button--primary" onclick={this.hide.bind(this)}>
{app.trans('core.dismiss')}
</button>
</p>
)}
<p>{app.trans('core.confirmation_email_sent', {email: <strong>{user.email()}</strong>})}</p>,
<p>
<a href={`http://${emailProviderName}`} className="Button Button--primary" target="_blank">
{app.trans('core.go_to', {location: emailProviderName})}
</a>
</p>
</div>
</div>
</div>
@ -150,6 +148,8 @@ export default class SignUpModal extends Modal {
/**
* Open the log in modal, prefilling it with an email/username/password if
* the user has entered one.
*
* @public
*/
logIn() {
const props = {
@ -161,7 +161,7 @@ export default class SignUpModal extends Modal {
}
onready() {
if (this.props.username) {
if (this.props.username && !this.props.token) {
this.$('[name=email]').select();
} else {
super.onready();
@ -175,24 +175,50 @@ export default class SignUpModal extends Modal {
const data = this.submitData();
app.store.createRecord('users').save(data).then(
user => {
this.welcomeUser = user;
this.loading = false;
m.redraw();
app.request({
url: app.forum.attribute('baseUrl') + '/register',
method: 'POST',
data
}).then(
payload => {
const user = app.store.pushPayload(payload);
// If the user's new account has been activated, then we can assume
// that they have been logged in too. Thus, we will reload the page.
// Otherwise, we will show a message asking them to check their email.
if (user.isActivated()) {
window.location.reload();
} else {
this.welcomeUser = user;
this.loading = false;
m.redraw();
}
},
response => {
this.loading = false;
this.handleErrors(response.errors);
this.handleErrors(response);
}
);
}
/**
* Get the data that should be submitted in the sign-up request.
*
* @return {Object}
* @public
*/
submitData() {
return {
const data = {
username: this.username(),
email: this.email(),
password: this.password()
email: this.email()
};
if (this.props.token) {
data.token = this.props.token;
} else {
data.password = this.password();
}
return data;
}
}

View File

@ -16,6 +16,7 @@ export default class Session {
* The token that was used for authentication.
*
* @type {String|null}
* @public
*/
this.token = token;
}
@ -26,6 +27,7 @@ export default class Session {
* @param {String} identification The username/email.
* @param {String} password
* @return {Promise}
* @public
*/
login(identification, password) {
return app.request({
@ -38,6 +40,8 @@ export default class Session {
/**
* Log the user out.
*
* @public
*/
logout() {
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.token;
@ -48,8 +52,11 @@ export default class Session {
* XMLHttpRequest object.
*
* @param {XMLHttpRequest} xhr
* @public
*/
authorize(xhr) {
xhr.setRequestHeader('Authorization', 'Token ' + this.token);
if (this.token) {
xhr.setRequestHeader('Authorization', 'Token ' + this.token);
}
}
}

View File

@ -25,7 +25,8 @@ export default class Button extends Component {
delete attrs.children;
attrs.className = (attrs.className || '');
attrs.className = attrs.className || '';
attrs.type = attrs.type || 'button';
const iconName = extract(attrs, 'icon');
if (iconName) attrs.className += ' hasIcon';

View File

@ -129,7 +129,7 @@ export default class Modal extends Component {
m.redraw();
if (errors) {
this.$('form [name=' + errors[0].path + ']').select();
this.$('form [name=' + errors[0].source.pointer.replace('/data/attributes/', '') + ']').select();
} else {
this.$('form :input:first').select();
}