sort components into widgets and views

This commit is contained in:
or-else
2018-10-24 18:04:16 +03:00
parent 5367c71784
commit 99dfb0afeb
48 changed files with 454 additions and 453 deletions
+44
View File
@@ -0,0 +1,44 @@
/* ContactsView holds all contacts-related stuff */
import React from 'react';
import ContactList from '../widgets/contact-list.jsx';
export default class ContactsView extends React.Component {
constructor(props) {
super(props);
this.state = ContactsView.getDerivedStateFromProps(props, {});
}
static getDerivedStateFromProps(nextProps, prevState) {
let contacts = [];
let unreadThreads = 0;
nextProps.chatList.map((c) => {
unreadThreads += c.unread > 0 ? 1 : 0;
contacts.push(c);
});
contacts.sort(function(a, b){
return (b.touched || 0) - (a.touched || 0);
});
updateFavicon(unreadThreads);
return {contactList: contacts};
}
render() {
return (
<ContactList
connected={this.props.connected}
contacts={this.state.contactList}
emptyListMessage={<span>You have no chats<br />¯\_(ツ)_/¯</span>}
topicSelected={this.props.topicSelected}
myUserId={this.props.myUserId}
showOnline={true}
showUnread={true}
onTopicSelected={this.props.onTopicSelected}
showContextMenu={this.props.showContextMenu} />
);
}
};
+106
View File
@@ -0,0 +1,106 @@
/* Account registration */
import React from 'react';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import CheckBox from '../widgets/checkbox.jsx';
import VisiblePassword from '../widgets/visible-password.jsx';
export default class CreateAccountView extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
login: '',
password: '',
email: '',
fn: '', // full/formatted name
imageDataUrl: null,
errorCleared: false,
saveToken: localStorage.getObject('keep-logged-in')
};
this.handleLoginChange = this.handleLoginChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.handleEmailChange = this.handleEmailChange.bind(this);
this.handleFnChange = this.handleFnChange.bind(this);
this.handleImageChanged = this.handleImageChanged.bind(this);
this.handleToggleSaveToken = this.handleToggleSaveToken.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleLoginChange(e) {
this.setState({login: e.target.value});
}
handlePasswordChange(password) {
this.setState({password: password});
}
handleEmailChange(e) {
this.setState({email: e.target.value})
}
handleFnChange(e) {
this.setState({fn: e.target.value});
}
handleImageChanged(img) {
this.setState({imageDataUrl: img});
}
handleToggleSaveToken() {
localStorage.setObject('keep-logged-in', !this.state.saveToken);
this.setState({saveToken: !this.state.saveToken});
}
handleSubmit(e) {
e.preventDefault();
// TODO: check email for validity
this.setState({errorCleared: false});
this.props.onCreateAccount(
this.state.login.trim(),
this.state.password.trim(),
vcard(this.state.fn, this.state.imageDataUrl),
{'meth': 'email', 'val': this.state.email});
}
render() {
var submitClasses = 'blue';
if (this.props.disabled) {
submitClasses += ' disabled';
}
return (
<form className="panel-form-column" onSubmit={this.handleSubmit}>
<div className="panel-form-row">
<div className="panel-form-column">
<input type="text" placeholder="Login" autoComplete="user-name"
value={this.state.login} onChange={this.handleLoginChange} required autoFocus />
<VisiblePassword placeholder="Password" autoComplete="new-password"
value={this.state.password} onFinished={this.handlePasswordChange}
required={true} />
</div>
<AvatarUpload
onImageChanged={this.handleImageChanged}
onError={this.props.onError} />
</div>
<div className="panel-form-row">
<input type="text" placeholder="Full name, e.g. John Doe" autoComplete="name"
value={this.state.fn} onChange={this.handleFnChange} required/>
</div>
<div className="panel-form-row">
<input type="email" placeholder="Email, e.g john.doe@example.com"
autoComplete="email" value={this.state.email} onChange={this.handleEmailChange} required/>
</div>
<div className="panel-form-row">
<CheckBox id="save-token" name="save-token" checked={this.state.saveToken}
onChange={this.handleToggleSaveToken} />
<label htmlFor="save-token">&nbsp;Stay logged in</label>
</div>
<div className="dialog-buttons">
<button className={submitClasses} type="submit">Sign up</button>
</div>
</form>
);
}
};
+192
View File
@@ -0,0 +1,192 @@
import React from 'react';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import CheckBox from '../widgets/checkbox.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx';
import TagManager from '../widgets/tag-manager.jsx';
/* Edit account parameters */
export default class EditAccountView extends React.Component {
constructor(props) {
super(props);
let me = this.props.tinode.getMeTopic();
let defacs = me.getDefaultAccess();
let fnd = this.props.tinode.getFndTopic();
this.state = {
fullName: me.public ? me.public.fn : undefined,
avatar: makeImageUrl(me.public ? me.public.photo : null),
auth: defacs ? defacs.auth : null,
anon: defacs ? defacs.anon : null,
tags: fnd.tags(),
showPermissionEditorFor: undefined,
previousOnTags: fnd.onTagsUpdated
};
this.tnNewTags = this.tnNewTags.bind(this);
this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
this.handlePasswordUpdate = this.handlePasswordUpdate.bind(this);
this.handleImageChanged = this.handleImageChanged.bind(this);
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
}
componentDidMount() {
let fnd = this.props.tinode.getFndTopic();
fnd.onTagsUpdated = this.tnNewTags;
if (!fnd.isSubscribed()) {
fnd.subscribe(fnd.startMetaQuery().withLaterDesc().withTags().build()).catch((err) => {
this.props.onError(err.message, 'err');
});
}
}
componentWillUnmount() {
var fnd = this.props.tinode.getFndTopic();
fnd.onTagsUpdated = this.state.previousOnTags;
}
tnNewTags(tags) {
this.setState({tags: tags});
}
handleFullNameUpdate(fn) {
this.setState({fullName: fn});
this.props.onUpdateAccount(undefined, vcard(fn, this.state.avatar));
}
handlePasswordUpdate(pwd) {
this.setState({password: pwd});
this.props.onUpdateAccount(pwd);
}
handleImageChanged(img) {
this.setState({avatar: img});
this.props.onUpdateAccount(undefined, vcard(this.state.fullName, img));
}
handleCheckboxClick(what, checked) {
if (what == 'sound') {
this.props.onToggleMessageSounds(checked);
} else if (what == 'alert') {
this.props.onTogglePushNotifications(checked);
}
}
handleLaunchPermissionsEditor(which) {
this.setState({
showPermissionEditorFor: which,
editedPermissions: this.state[which]
});
}
handleHidePermissionsEditor() {
this.setState({showPermissionEditorFor: undefined});
}
handlePermissionsChanged(perm) {
let defacs = {};
defacs[this.state.showPermissionEditorFor] = perm;
this.props.onUpdateAccount(undefined, undefined, defacs);
let newState = {showPermissionEditorFor: undefined};
newState[this.state.showPermissionEditorFor] = perm;
this.setState(newState);
}
handleTagsUpdated(tags) {
// Check if tags have actually changed.
if (arrayEqual(this.state.tags.slice(0), tags.slice(0))) {
return;
}
this.props.onUpdateTags(tags);
}
render() {
var tags = [];
this.state.tags.map(function(tag) {
tags.push(<span className="badge" key={tags.length}>{tag}</span>);
});
if (tags.length == 0) {
tags = <i>No tags defined. Add some.</i>;
}
return (
<React.Fragment>{this.state.showPermissionEditorFor ?
<PermissionsEditor
mode={this.state.editedPermissions}
skip="O"
onSubmit={this.handlePermissionsChanged}
onCancel={this.handleHidePermissionsEditor} />
:
<div id="edit-account" className="scrollable-panel">
<div className="panel-form-row">
<div className="panel-form-column">
<div><label className="small">Your name</label></div>
<div><InPlaceEdit
placeholder="Full name, e.g. John Doe"
value={this.state.fullName}
onFinished={this.handleFullNameUpdate} /></div>
<div><label className="small">Password</label></div>
<div><InPlaceEdit
placeholder="Unchanged"
type="password"
onFinished={this.handlePasswordUpdate} /></div>
</div>
<AvatarUpload
avatar={this.state.avatar}
uid={this.props.myUserId}
title={this.state.fullName}
onImageChanged={this.handleImageChanged}
onError={this.props.onError} />
</div>
<div className="hr" />
<div className="panel-form-column">
<div className="panel-form-row">
<label>Address:</label>
<tt>{this.props.myUserId}</tt>
</div>
<div>
<label className="small">Default access mode:</label>
</div>
<div className="quoted">
<div>Auth: <tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'auth')}>{this.state.auth}</tt></div>
<div>Anon: <tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'anon')}>{this.state.anon}</tt></div>
</div>
</div>
<div className="hr" />
<div className="panel-form-row">
<label htmlFor="message-sound">Message sound:</label>
<CheckBox name="sound" id="message-sound"
checked={this.props.messageSounds}
onChange={this.handleCheckboxClick} />
</div>
<div className="panel-form-row">
<label htmlFor="desktop-alerts">Notification
alerts{!this.props.desktopAlertsEnabled ? ' (requires HTTPS)' : null}:</label>
<CheckBox name="alert" id="desktop-alerts"
checked={this.props.desktopAlerts}
onChange={this.props.desktopAlertsEnabled ? this.handleCheckboxClick : null} />
</div>
<div className="hr" />
<TagManager
title="Tags (user discovery)"
activated={false}
tags={this.state.tags}
onSubmit={this.handleTagsUpdated} />
<div className="hr" />
<div className="panel-form-column">
<a href="javascript:;" className="red flat-button" onClick={this.props.onLogout}>
<i className="material-icons">exit_to_app</i> Logout
</a>
</div>
</div>
}</React.Fragment>
);
}
};
+554
View File
@@ -0,0 +1,554 @@
/* InfoView: panel with topic/user info */
import React from 'react';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import ContactList from '../widgets/contact-list.jsx';
import GroupManager from '../widgets/group-manager.jsx';
import InPlaceEdit from '../widgets/in-place-edit.jsx';
import MenuCancel from '../widgets/menu-cancel.jsx';
import MoreButton from '../widgets/more-button.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx';
import TagManager from '../widgets/tag-manager.jsx';
export default class InfoView extends React.Component {
constructor(props) {
super(props);
this.state = {
topic: null,
owner: false,
admin: false,
sharer: false,
muted: false,
address: null,
groupTopic: undefined,
fullName: undefined,
avatar: null,
private: null,
selectedContact: null,
access: null,
modeGiven: null,
modeWant: null,
modeGiven2: null, // P2P topic, the other user mode given
modeWant2: null, // P2P topic, the other user mode want
auth: null,
anon: null,
contactList: [],
tags: [],
showMemberPanel: false,
showPermissionEditorFor: undefined,
moreInfoExpanded: false,
previousMetaDesc: undefined,
previousSubsUpdated: undefined,
previousTagsUpdated: undefined
};
this.resetSubs = this.resetSubs.bind(this);
this.resetDesc = this.resetDesc.bind(this);
this.onMetaDesc = this.onMetaDesc.bind(this);
this.onSubsUpdated = this.onSubsUpdated.bind(this);
this.onTagsUpdated = this.onTagsUpdated.bind(this);
this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
this.handlePrivateUpdate = this.handlePrivateUpdate.bind(this);
this.handleImageChanged = this.handleImageChanged.bind(this);
this.handleMuted = this.handleMuted.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this);
this.handleShowAddMembers = this.handleShowAddMembers.bind(this);
this.handleHideAddMembers = this.handleHideAddMembers.bind(this);
this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this);
this.handleLeave = this.handleLeave.bind(this);
this.handleMemberSelected = this.handleMemberSelected.bind(this);
this.handleMoreInfo = this.handleMoreInfo.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
this.handleContextMenu = this.handleContextMenu.bind(this);
}
// No need to separately handle component mount.
UNSAFE_componentWillReceiveProps(props) {
var topic = this.props.tinode.getTopic(props.topic);
if (!topic) {
return;
}
if (this.onMetaDesc != topic.onMetaDesc) {
this.previousMetaDesc = topic.onMetaDesc;
topic.onMetaDesc = this.onMetaDesc;
this.previousSubsUpdated = topic.onSubsUpdated;
topic.onSubsUpdated = this.onSubsUpdated;
if (topic.getType() == 'grp') {
this.previousTagsUpdated = topic.onTagsUpdated;
topic.onTagsUpdated = this.onTagsUpdated;
} else {
this.previousTagsUpdated = undefined;
}
}
if (this.state.topic != props.topic) {
this.setState({topic: props.topic});
this.resetDesc(topic, props);
this.resetSubs(topic, props);
}
}
componentWillUnmount() {
var topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
this.setState({topic: null});
topic.onMetaDesc = this.previousMetaDesc;
topic.onSubsUpdated = this.previousSubsUpdated;
topic.onTagsUpdated = this.previousTagsUpdated;
}
resetSubs(topic, props) {
var newState = {contactList: []};
if (topic.getType() == 'p2p') {
// Fetch the other party in the p2p conversation.
// Topic may not be ready yet, so check if user is found.
var user2 = topic.subscriber(props.topic);
if (user2) {
newState.modeGiven2 = user2.acs.getGiven();
newState.modeWant2 = user2.acs.getWant();
}
} else {
topic.subscribers(function(sub) {
newState.contactList.push(sub);
}, this);
}
this.setState(newState);
}
resetDesc(topic, props) {
var defacs = topic.getDefaultAccess() || {};
var acs = topic.getAccessMode();
this.setState({
owner: acs && acs.isOwner(),
admin: acs && acs.isAdmin(),
sharer: acs && (acs.isAdmin() || acs.isSharer()),
muted: acs && acs.isMuted(),
fullName: topic.public ? topic.public.fn : undefined,
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
private: topic.private ? topic.private.comment : null,
address: topic.name,
groupTopic: (topic.getType() == 'grp'),
showMemberPanel: false,
access: acs ? acs.getMode() : undefined,
modeGiven: acs ? acs.getGiven() : undefined,
modeWant: acs ? acs.getWant() : undefined,
auth: defacs.auth,
anon: defacs.anon
});
if (topic.getType() == 'grp' && acs && acs.isOwner()) {
// Requesting tags: owner is editing the topic.
topic.getMeta(topic.startMetaQuery().withTags().build());
}
}
onMetaDesc(desc) {
var topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
this.resetDesc(topic, this.props);
if (this.previousMetaDesc && this.previousMetaDesc != this.onMetaDesc) {
this.previousMetaDesc(desc);
}
}
onSubsUpdated() {
var topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
this.resetSubs(topic, this.props);
if (this.previousSubsUpdated && this.previousSubsUpdated != this.onSubsUpdated) {
this.previousSubsUpdated();
}
}
onTagsUpdated(tags) {
this.setState({tags: tags});
if (this.previousTagsUpdated && this.previousTagsUpdated != this.onTagsUpdated) {
this.previousTagsUpdated();
}
}
handleFullNameUpdate(fn) {
if (this.state.fullName !== fn) {
this.setState({fullName: fn});
this.props.onTopicDescUpdate(this.props.topic,
vcard(fn, this.state.avatar),
null);
}
}
handlePrivateUpdate(priv) {
if (this.state.priv !== priv) {
this.setState({private: priv});
this.props.onTopicDescUpdate(this.props.topic,
null,
priv);
}
}
handleImageChanged(img) {
this.setState({avatar: img});
this.props.onTopicDescUpdate(this.props.topic,
vcard(this.state.fullName, img),
null);
}
handleMuted(ignored, checked) {
this.setState({muted: checked});
this.props.onChangePermissions(this.props.topic, checked ? '-P' : '+P');
}
handlePermissionsChanged(perm) {
switch (this.state.showPermissionEditorFor) {
case 'auth':
this.props.onTopicDescUpdate(this.props.topic, null, null, {auth: perm});
break;
case 'anon':
this.props.onTopicDescUpdate(this.props.topic, null, null, {anon: perm});
break;
case 'mode':
case 'want':
this.props.onChangePermissions(this.props.topic, perm);
break;
case 'given':
this.props.onChangePermissions(this.props.topic, perm, this.props.topic);
break;
case 'user':
this.props.onChangePermissions(this.props.topic, perm, this.state.userPermissionsEdited);
break;
}
this.setState({showPermissionEditorFor: undefined});
}
handleLaunchPermissionsEditor(which, uid) {
let toEdit, toCompare, toSkip, titleEdit, titleCompare, userTitle, userAvatar;
switch (which) {
case 'mode':
toEdit = this.state.access;
break;
case 'want':
toEdit = this.state.modeWant;
toCompare = this.state.modeGiven;
toSkip = this.state.groupTopic ? 'O' : 'ASDO';
titleEdit = "Requested";
titleCompare = "Granted";
break;
case 'given':
toEdit = this.state.modeGiven2;
toCompare = this.state.modeWant2;
toSkip = this.state.groupTopic ? (this.state.owner ? '' : 'O') : 'ASDO';
titleEdit = "Granted";
titleCompare = "Requested";
break;
case 'auth':
toEdit = this.state.auth;
toSkip = 'O';
break;
case 'anon':
toEdit = this.state.anon;
toSkip = 'O';
break;
case 'user': {
let topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
var user = topic.subscriber(uid);
if (!user || !user.acs) {
return;
}
toEdit = user.acs.getGiven();
toCompare = user.acs.getWant();
toSkip = this.state.owner ? '' : 'O';
titleEdit = "Granted";
titleCompare = "Requested";
if (user.public) {
userTitle = user.public.fn;
userAvatar = user.public.photo;
}
break;
}
default:
console.log("Unknown permission editing mode '" + which + "'");
break;
}
this.setState({
showPermissionEditorFor: which,
userPermissionsEdited: uid,
userPermissionsTitle: userTitle,
userPermissionsAvatar: userAvatar,
editedPermissions: toEdit,
immutablePermissions: toCompare,
editedPermissionsTitle: titleEdit,
immutablePermissionsTitle: titleCompare,
editedPermissionsSkipped: toSkip,
});
}
handleHidePermissionsEditor() {
this.setState({showPermissionEditorFor: undefined});
}
handleShowAddMembers() {
this.props.onInitFind();
this.setState({showMemberPanel: true});
}
handleHideAddMembers() {
this.setState({showMemberPanel: false});
}
handleMemberUpdateRequest(members, added, removed) {
this.props.onMemberUpdateRequest(this.props.topic, added, removed);
this.setState({showMemberPanel: false});
}
handleLeave() {
this.props.onLeaveTopic(this.props.topic);
}
handleMemberSelected(uid) {
this.setState({selectedContact: uid});
}
handleMoreInfo(open) {
this.setState({moreInfoExpanded: open});
}
handleTagsUpdated(tags) {
if (!arrayEqual(this.state.tags.slice(0), tags.slice(0))) {
this.props.onTopicTagsUpdate(this.props.topic, tags);
}
}
handleContextMenu(params) {
var instance = this;
var topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
var user = topic.subscriber(params.topicName);
if (!user || !user.acs) {
return;
}
var menuItems = [
{title: "Edit permissions", handler: function() {
instance.handleLaunchPermissionsEditor('user', params.topicName);
}},
'member_delete',
user.acs.isMuted() ? 'member_unmute' : 'member_mute',
user.acs.isJoiner() ? 'member_block' : 'member_unblock'
];
this.props.showContextMenu({
topicName: this.props.topic,
x: params.x,
y: params.y,
user: params.topicName}, menuItems);
}
render() {
return (
<div id="info-view">
<div className="caption-panel" id="info-caption-panel">
<div className="panel-title" id="info-title">Info</div>
<div>
<MenuCancel onCancel={this.props.onCancel} />
</div>
</div>
{this.props.displayMobile ?
<ErrorPanel
level={this.props.errorLevel}
text={this.props.errorText}
onClearError={this.props.onError} /> : null}
{this.state.showMemberPanel ?
<GroupManager
members={this.state.contactList}
requiredMember={this.props.myUserId}
myUserId={this.props.myUserId}
contacts={this.props.searchableContacts}
onCancel={this.handleHideAddMembers}
onSubmit={this.handleMemberUpdateRequest} />
:
this.state.showPermissionEditorFor ?
<PermissionsEditor
mode={this.state.editedPermissions}
compare={this.state.immutablePermissions}
skip={this.state.editedPermissionsSkipped}
modeTitle={this.state.editedPermissionsTitle}
compareTitle={this.state.immutablePermissionsTitle}
userTitle={this.state.userPermissionsTitle}
item={this.state.userPermissionsEdited}
userAvatar={this.state.userPermissionsAvatar}
onSubmit={this.handlePermissionsChanged}
onCancel={this.handleHidePermissionsEditor}
/>
:
<div id="info-view-content" className="scrollable-panel">
<div className="panel-form-row">
<div className="panel-form-column">
<div><label className="small">Name</label></div>
<div><InPlaceEdit
placeholder={this.state.groupTopic ? "Group name" : <i>Unknown</i>}
readOnly={!this.state.owner}
value={this.state.fullName}
onFinished={this.handleFullNameUpdate} /></div>
<div><label className="small">Private comment</label></div>
<div><InPlaceEdit
placeholder="Visible to you only"
value={this.state.private}
onFinished={this.handlePrivateUpdate} /></div>
</div>
<AvatarUpload
avatar={this.state.avatar}
readOnly={!this.state.owner}
uid={this.props.topic}
title={this.state.fullName}
onImageChanged={this.handleImageChanged}
onError={this.props.onError} />
</div>
<div className="hr" />
<div className="panel-form-column">
<div className="panel-form-row">
<label>Muted:</label>
<CheckBox name="P" checked={this.state.muted}
onChange={this.handleMuted} />
</div>
<MoreButton
title="More"
open={this.state.moreInfoExpanded}
onToggle={this.handleMoreInfo} />
{this.state.moreInfoExpanded ?
<div className="panel-form-column">
<div className="panel-form-row">
<label>Address:</label>
<tt>{this.state.address}</tt>
</div>
{this.state.groupTopic ?
<div className="panel-form-row">
<label>Your permissions:</label>
<tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'want')}>
{this.state.access}
</tt>
</div>
:
<div>
<div><label className="small">Permissions:</label></div>
<div className="quoted">
<div>Yours: &nbsp;<tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'want')}>
{this.state.access}
</tt></div>
<div>{this.state.fullName ? this.state.fullName : "Other"}&prime;s:
&nbsp;<tt className="clickable" onClick={this.handleLaunchPermissionsEditor.bind(this, 'given')}>
{this.state.modeGiven2}
</tt>
</div>
</div>
</div>
}
{this.state.sharer && (this.state.auth || this.state.anon) ?
<div>
<div><label className="small">Default access mode:</label></div>
<div className="quoted">
<div>Auth: {this.state.admin ?
<tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'auth')}>
{this.state.auth}
</tt>
:
<tt>{this.state.auth}</tt>
}
</div>
<div>Anon: {this.state.admin ?
<tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'anon')}>
{this.state.anon}
</tt>
:
<tt>{this.state.anon}</tt>
}
</div>
</div>
</div>
:
null
}
</div>
:
null
}
</div>
<div className="hr" />
{this.state.owner ?
<TagManager
title="Tags"
tags={this.state.tags}
activated={false}
onSubmit={this.handleTagsUpdated} />
:
null
}
{this.state.owner ? <div className="hr" /> : null }
{this.state.groupTopic ?
<div className="panel-form-column">
<div className="panel-form-row">
<label className="small">Group members:</label>
</div>
<div className="panel-form-row">
{this.state.sharer ?
<a href="javascript:;" className="flat-button" onClick={this.handleShowAddMembers}>
<i className="material-icons">person_add</i> Add members
</a>
: null}
{!this.state.owner ?
<a href="javascript:;" className="red flat-button" onClick={this.handleLeave}>
<i className="material-icons">exit_to_app</i> Leave
</a>
: null}
</div>
<ContactList
contacts={this.state.contactList}
myUserId={this.props.myUserId}
emptyListMessage="No members"
topicSelected={this.state.selectedContact}
showOnline={false}
showUnread={false}
showMode={true}
noScroll={true}
onTopicSelected={this.handleMemberSelected}
showContextMenu={this.state.admin ? this.handleContextMenu : false}
/>
</div>
:
<div className="panel-form-row">
<a href="javascript:;" className="red flat-button" onClick={this.handleLeave}>
<i className="material-icons">exit_to_app</i> Leave
</a>
</div>
}
</div>
}
</div>
);
}
};
+90
View File
@@ -0,0 +1,90 @@
// Login form
import React from 'react';
import {FormattedMessage} from 'react-intl';
import CheckBox from '../widgets/checkbox.jsx';
import VisiblePassword from '../widgets/visible-password.jsx';
export default class LoginView extends React.Component {
constructor(props) {
super(props);
this.state = {
login: props.login,
password: '',
hostName: props.serverAddress,
saveToken: localStorage.getObject('keep-logged-in')
};
this.handleLoginChange = this.handleLoginChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.handleToggleSaveToken = this.handleToggleSaveToken.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleLoginChange(e) {
this.setState({login: e.target.value});
}
handlePasswordChange(e) {
this.setState({password: e.target.value});
}
handleToggleSaveToken() {
localStorage.setObject('keep-logged-in', !this.state.saveToken);
this.setState({saveToken: !this.state.saveToken});
}
handleSubmit(e) {
e.preventDefault();
this.props.onLogin(this.state.login.trim(), this.state.password.trim());
}
render() {
var submitClasses = 'blue';
if (this.props.disabled) {
submitClasses += ' disabled';
}
return (
<form id="login-form" onSubmit={this.handleSubmit}>
<FormattedMessage id="login.login_prompt" defaultMessage="Login">
{
(login_prompt) => <input type="text" id="inputLogin"
placeholder={login_prompt}
autoComplete="username"
value={this.state.login}
onChange={this.handleLoginChange}
required autoFocus />
}
</FormattedMessage>
<FormattedMessage id="login.password_prompt" defaultMessage="Password">
{
(password_prompt) => <VisiblePassword type="password" id="inputPassword"
placeholder={password_prompt}
autoComplete="current-password"
value={this.state.password}
onChange={this.handlePasswordChange}
required={true} />
}
</FormattedMessage>
<div className="panel-form-row">
<CheckBox id="save-token" name="save-token" checked={this.state.saveToken}
onChange={this.handleToggleSaveToken} />
<label htmlFor="save-token">&nbsp;
<FormattedMessage id="login.stay_logged_in" defaultMessage="Stay logged in" />
</label>
<a href="#reset">
<FormattedMessage id="login.forgot_password_link" defaultMessage="Forgot password?" />
</a>
</div>
<div className="dialog-buttons">
<button className={submitClasses} type="submit">
<FormattedMessage id="login.sign_in_button" defaultMessage="Sign in" />
</button>
</div>
</form>
);
}
};
/* END Login */
+21
View File
@@ -0,0 +1,21 @@
/* This is just a static page to display when no conversation is selected. */
import React from 'react';
export default class LogoView extends React.PureComponent {
render() {
var version = APP_NAME + ' (' + Tinode.getLibrary() + ')';
return (
<div id="dummy-view" className={this.props.hideSelf ? 'nodisplay' : null}>
<div>
<a href="https://github.com/tinode/chat/">
<img id="logo" alt="logo" src="img/logo.svg" />
<h2>Tinode Web</h2>
</a>
<p>Client: {version}</p>
<p>Server: {this.props.serverVersion} ({this.props.serverAddress})</p>
</div>
</div>
);
}
};
+495
View File
@@ -0,0 +1,495 @@
import React from 'react';
import ChatMessage from '../widgets/chat-message.jsx';
import ErrorPanel from '../widgets/error-panel.jsx';
import GroupSubs from '../widgets/group-subs.jsx';
import ImagePreview from '../widgets/image-preview.jsx';
import LetterTile from '../widgets/letter-tile.jsx';
import LoadSpinner from '../widgets/load-spinner.jsx';
import LogoView from './logo-view.jsx';
import SendMessage from '../widgets/send-message.jsx';
const NOT_FOUND_TOPIC_TITLE = "Not found";
export default class MessagesView extends React.Component {
constructor(props) {
super(props);
this.state = {
messages: [],
onlineSubs: [],
topic: '',
title: '',
avatar: null,
scrollPosition: 0,
readOnly: props.acs ? !props.acs.isWriter() : true,
writeOnly: props.acs ? !props.acs.isReader() : true,
typingIndicator: false,
imagePreview: null
};
this.propsChange = this.propsChange.bind(this);
this.leave = this.leave.bind(this);
this.handleScrollReference = this.handleScrollReference.bind(this);
this.fetchMoreMessages = this.fetchMoreMessages.bind(this);
this.handleDescChange = this.handleDescChange.bind(this);
this.handleSubsUpdated = this.handleSubsUpdated.bind(this);
this.handleNewMessage = this.handleNewMessage.bind(this);
this.handleAllMessagesReceived = this.handleAllMessagesReceived.bind(this);
this.handleInfoReceipt = this.handleInfoReceipt.bind(this);
this.handleImagePreview = this.handleImagePreview.bind(this);
this.handleCloseImagePreview = this.handleCloseImagePreview.bind(this);
this.handleContextClick = this.handleContextClick.bind(this);
this.handleShowContextMenuMessage = this.handleShowContextMenuMessage.bind(this);
this.handleBackNavigation = this.handleBackNavigation.bind(this);
}
// Scroll last message into view on component update e.g. on message received.
componentDidUpdate(prevProps, prevState) {
if (this.messagesScroller &&
(prevState.title != this.state.title ||
prevState.messages.length != this.state.messages.length)) {
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition;
}
}
componentDidMount() {
this.propsChange(this.props);
if (this.messagesScroller) {
this.messagesScroller.addEventListener('scroll', this.fetchMoreMessages);
}
}
componentWillUnmount() {
this.leave(this.state.topic);
if (this.messagesScroller) {
this.messagesScroller.removeEventListener('scroll', this.fetchMoreMessages);
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
this.propsChange(nextProps);
}
propsChange(props) {
if (!props || !props.topic) {
let oldTopic = this.state.topic;
this.setState({messages: [], onlineSubs: [], topic: null}, this.leave(oldTopic));
return;
}
if (!props.connected) {
// connection lost, clear online subs
this.setState({onlineSubs: []});
return;
}
let topic = this.props.tinode.getTopic(props.topic);
if (!topic) {
return;
}
let newGroupTopic = Tinode.isNewGroupTopicName(props.topic);
let tryToResubscribe = !this.props.connected && props.connected;
if (props.topic != this.state.topic) {
let msgs = [];
let subs = [];
// Bind the new topic to component.
topic.onData = this.handleNewMessage;
topic.onAllMessagesReceived = this.handleAllMessagesReceived;
topic.onInfo = this.handleInfoReceipt;
topic.onMetaDesc = this.handleDescChange;
topic.onSubsUpdated = this.handleSubsUpdated;
topic.onPres = this.handleSubsUpdated;
// Unbind the previous topic from this component.
this.leave(this.state.topic);
this.handleDescChange(topic);
topic.subscribers((sub) => {
if (sub.online && sub.user != this.props.myUserId) {
subs.push(sub);
}
});
topic.messages(function(msg) {
if (!msg.deleted) {
msgs = msgs.concat(msg);
}
});
this.setState({
messages: msgs,
onlineSubs: subs,
topic: props.topic,
imagePreview: null,
scrollPosition: 0
});
tryToResubscribe = true;
// The user switched to the new topic before the timer for
// the previous topic has triggered, kill it.
this.props.readTimerHandler(null);
}
this.setState({
readOnly: props.acs ? !props.acs.isWriter() : true,
writeOnly: props.acs ? !props.acs.isReader() : true
});
if (!topic.isSubscribed() && tryToResubscribe) {
// Don't request the tags. They are useless unless the user
// is the owner and is editing the topic.
let getQuery = topic.startMetaQuery()
.withLaterDesc()
.withLaterSub()
.withLaterData(MESSAGES_PAGE)
.withLaterDel();
let setQuery = newGroupTopic ? props.newGroupTopicParams : undefined;
// Show "loading" spinner.
this.setState({ fetchingMessages: true });
topic.subscribe(getQuery.build(), setQuery)
.then((ctrl) => {
this.setState({topic: ctrl.topic});
this.props.onNewTopicCreated(props.topic, ctrl.topic);
// If there are unsent messages, try sending them now.
topic.queuedMessages((pub) => {
if (!pub._sending && topic.isSubscribed()) {
topic.publishMessage(pub);
}
});
})
.catch((err) => {
this.props.onError(err.message, 'err');
this.setState({
title: NOT_FOUND_TOPIC_TITLE,
avatar: null,
readOnly: true,
writeOnly: true,
fetchingMessages: false,
messages: [],
onlineSubs: [],
typingIndicator: false,
imagePreview: null
});
});
}
}
leave(oldTopicName) {
if (!oldTopicName) {
return;
}
let oldTopic = this.props.tinode.getTopic(oldTopicName);
if (oldTopic && oldTopic.isSubscribed()) {
oldTopic.leave(false)
.catch(() => { /* do nothing here */ })
.finally(() => {
// We don't care if the request succeeded or failed.
// The topic is dead regardless.
this.setState({fetchingMessages: false});
oldTopic.onData = undefined;
oldTopic.onAllMessagesReceived = undefined;
oldTopic.onInfo = undefined;
oldTopic.onMetaDesc = undefined;
oldTopic.onSubsUpdated = undefined;
oldTopic.onPres = undefined;
});
}
}
handleScrollReference(node) {
if (node) {
node.addEventListener('scroll', this.fetchMoreMessages);
this.messagesScroller = node;
}
}
// Get older messages
fetchMoreMessages(event) {
if (event.target.scrollTop <= 0) {
let newState = {scrollPosition: event.target.scrollHeight - event.target.scrollTop};
this.setState((prevState, props) => {
if (!prevState.fetchingMessages) {
var topic = this.props.tinode.getTopic(this.state.topic);
if (topic && topic.isSubscribed() && topic.msgHasMoreMessages()) {
newState.fetchingMessages = true;
topic.getMessagesPage(MESSAGES_PAGE).catch((err) => {
this.setState({fetchingMessages: false});
this.props.onError(err.message, 'err');
});
}
}
return newState;
});
}
}
handleDescChange(desc) {
if (desc.public) {
this.setState({
title: desc.public.fn,
avatar: makeImageUrl(desc.public.photo)
});
} else {
this.setState({
title: '',
avatar: null
});
}
if (desc.acs) {
this.setState({
readOnly: !desc.acs.isWriter(),
writeOnly: !desc.acs.isReader()
});
}
}
handleSubsUpdated() {
if (this.state.topic) {
var subs = [];
var topic = this.props.tinode.getTopic(this.state.topic);
topic.subscribers((sub) => {
if (sub.online && sub.user != this.props.myUserId) {
subs.push(sub);
}
});
this.setState({onlineSubs: subs});
}
}
handleNewMessage(msg) {
// Regenerate messages list
var topic = this.props.tinode.getTopic(this.state.topic);
var newState = {messages: []};
topic.messages(function(m) {
if (!m.deleted) {
newState.messages = newState.messages.concat(m);
}
});
// msg could be null if one or more messages were deleted.
if (msg && !msg.deleted) {
// If the message is added to the end of the message list,
// scroll to the bottom.
if (topic.isNewMessage(msg.seq)) {
newState.scrollPosition = 0;
}
// Aknowledge all messages, including own messages.
let status = topic.msgStatus(msg);
if (status >= Tinode.MESSAGE_STATUS_SENT) {
this.props.readTimerHandler(() => {
topic.noteRead(msg.seq);
});
}
this.props.onData(msg);
}
this.setState(newState);
}
handleAllMessagesReceived(count) {
this.setState({fetchingMessages: false});
}
handleInfoReceipt(info) {
switch (info.what) {
case 'kp': {
clearTimeout(this.keyPressTimer);
var instance = this;
this.keyPressTimer = setTimeout(function() {
instance.setState({typingIndicator: false});
}, KEYPRESS_DELAY + 1000);
if (!this.state.typingIndicator) {
this.setState({typingIndicator: true});
}
break;
}
case 'read':
case 'recv':
// Redraw due to changed recv/read status.
this.forceUpdate();
break;
default:
console.log("Other change in topic: ", info.what);
}
}
handleImagePreview(content) {
this.setState({ imagePreview: content });
}
handleCloseImagePreview() {
this.setState({ imagePreview: null });
}
handleContextClick(e) {
e.preventDefault();
e.stopPropagation();
this.props.showContextMenu({ topicName: this.state.topic, y: e.pageY, x: e.pageX });
}
handleShowContextMenuMessage(params) {
params.topicName = this.state.topic;
var menuItems = ['message_delete'];
var topic = this.props.tinode.getTopic(params.topicName);
if (topic) {
var acs = topic.getAccessMode();
if (acs && acs.isDeleter()) {
menuItems.push('message_delete_hard');
}
}
this.props.showContextMenu(params, menuItems);
}
handleBackNavigation() {
this.props.onHideMessagesView();
}
render() {
var component = null;
if (this.state.topic) {
var messageNodes = [];
var topic = this.props.tinode.getTopic(this.state.topic);
var groupTopic = topic.getType() == 'grp';
var previousFrom = null;
for (var i=0; i<this.state.messages.length; i++) {
var msg = this.state.messages[i];
var nextFrom = null;
if (i + 1 < this.state.messages.length) {
nextFrom = this.state.messages[i+1].from
}
var sequence = 'single';
if (msg.from == previousFrom) {
if (msg.from == nextFrom) {
sequence = 'middle';
} else {
sequence = 'last';
}
} else if (msg.from == nextFrom) {
sequence = 'first';
}
previousFrom = msg.from;
var isReply = !(msg.from == this.props.myUserId);
var deliveryStatus = topic.msgStatus(msg);
var userName, userAvatar, userFrom, chatBoxClass;
if (groupTopic) {
var user = topic.userDesc(msg.from);
if (user && user.public) {
userName = user.public.fn;
userAvatar = makeImageUrl(user.public.photo);
}
userFrom = msg.from;
chatBoxClass='chat-box group';
} else {
chatBoxClass='chat-box';
}
messageNodes.push(
<ChatMessage
tinode={this.props.tinode}
content={msg.content}
mimeType={msg.head ? msg.head.mime : null}
timestamp={msg.ts} response={isReply} seq={msg.seq}
userFrom={userFrom} userName={userName} userAvatar={userAvatar}
sequence={sequence} received={deliveryStatus} uploader={msg._uploader}
viewportWidth={this.props.viewportWidth}
showContextMenu={this.handleShowContextMenuMessage}
onImagePreview={this.handleImagePreview}
onError={this.props.onError}
key={msg.seq} />
);
}
let lastSeen = null;
let cont = this.props.tinode.getMeTopic().getContact(this.state.topic);
if (cont && Tinode.topicType(cont.topic) == 'p2p') {
if (cont.online) {
lastSeen = "online now";
} else if (cont.seen) {
lastSeen = "Last active: " + shortDateFormat(cont.seen.when);
// TODO(gene): also handle user agent in c.seen.ua
}
}
var avatar = this.state.avatar || true;
var online = this.props.online ? 'online' + (this.state.typingIndicator ? ' typing' : '') : 'offline';
component = (
<div id="topic-view" className={this.props.hideSelf ? 'nodisplay' : null}>
<div id="topic-caption-panel" className="caption-panel">
{this.props.displayMobile ?
<a href="javascript:;" id="hide-message-view" onClick={this.handleBackNavigation}>
<i className="material-icons">arrow_back</i>
</a>
:
null}
<div className="avatar-box">
<LetterTile
avatar={avatar}
topic={this.state.topic}
title={this.state.title} />
<span className={online} />
</div>
<div id="topic-title-group">
<div id="topic-title" className="panel-title">{this.state.title || <i>Unknown</i>}</div>
<div id="topic-last-seen">{lastSeen}</div>
</div>
{groupTopic ?
<GroupSubs
subscribers={this.state.onlineSubs} /> :
<div id="topic-users" />
}
<div>
<a href="javascript:;" onClick={this.handleContextClick}>
<i className="material-icons">more_vert</i>
</a>
</div>
</div>
{this.props.displayMobile ?
<ErrorPanel
level={this.props.errorLevel}
text={this.props.errorText}
onClearError={this.props.onError} />
: null}
<LoadSpinner show={this.state.fetchingMessages} />
<div id="messages-container">
<div id="messages-panel" ref={this.handleScrollReference}>
<ul id="scroller" className={chatBoxClass}>
{messageNodes}
</ul>
</div>
{this.state.writeOnly ?
<div id="write-only-background">
<div id="write-only-note">no access to messages</div>
</div>
:
null
}
</div>
<SendMessage
tinode={this.props.tinode}
topic={this.props.topic}
disabled={this.state.readOnly}
sendMessage={this.props.sendMessage}
onError={this.props.onError} />
{this.state.imagePreview ?
<ImagePreview content={this.state.imagePreview}
onClose={this.handleCloseImagePreview} /> : null}
</div>
);
} else {
component = (
<LogoView hideSelf={this.props.hideSelf}
serverVersion={this.props.serverVersion}
serverAddress={this.props.serverAddress} />
);
}
return component;
}
};
+94
View File
@@ -0,0 +1,94 @@
/* Create new topic and invite users or send an invite */
import React from 'react';
import ContactList from '../widgets/contact-list.jsx';
import NewTopicById from '../widgets/new-topic-by-id.jsx';
import NewTopicGroup from '../widgets/new-topic-group.jsx';
import SearchContacts from '../widgets/search-contacts.jsx';
export default class NewTopicView extends React.Component {
constructor(props) {
super(props);
this.state = {
tabSelected: 'p2p',
contactList: props.searchResults
};
this.handleTabClick = this.handleTabClick.bind(this);
this.handleContactSelected = this.handleContactSelected.bind(this);
this.handleNewGroupSubmit = this.handleNewGroupSubmit.bind(this);
this.handleGroupByID = this.handleGroupByID.bind(this);
}
componentDidMount() {
this.props.onInitFind();
}
static getDerivedStateFromProps(nextProps, prevState) {
return {
contactList: nextProps.searchResults
};
}
handleTabClick(e) {
e.preventDefault();
navigateTo(addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id));
this.setState({tabSelected: e.currentTarget.dataset.id});
}
handleContactSelected(sel) {
if (this.state.tabSelected === 'p2p') {
navigateTo(removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(sel, undefined);
}
}
handleNewGroupSubmit(name, dataUrl, priv, tags) {
navigateTo(removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(undefined, vcard(name, dataUrl), priv, tags);
}
handleGroupByID(topicName) {
navigateTo(removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(topicName);
}
render() {
return (
<div className="flex-column">
<ul className="tabbar">
<li className={this.state.tabSelected === "p2p" ? "active" : null}>
<a href="javascript:;" data-id="p2p" onClick={this.handleTabClick}>find</a>
</li>
<li className={this.state.tabSelected === "grp" ? "active" : null}>
<a href="javascript:;" data-id="grp" onClick={this.handleTabClick}>new group</a>
</li>
<li className={this.state.tabSelected === "byid" ? "active" : null}>
<a href="javascript:;" data-id="byid" onClick={this.handleTabClick}>by id</a>
</li>
</ul>
{this.state.tabSelected === 'grp' ?
<NewTopicGroup onSubmit={this.handleNewGroupSubmit} /> :
this.state.tabSelected === 'byid' ?
<NewTopicById
onSubmit={this.handleGroupByID}
onError={this.props.onError} /> :
<div className="flex-column">
<SearchContacts type="p2p"
onSearchContacts={this.props.onSearchContacts} />
<ContactList
contacts={this.state.contactList}
myUserId={this.props.myUserId}
emptyListMessage="Use search to find contacts"
topicSelected={this.state.selectedContact}
showOnline={false}
showUnread={false}
showContextMenu={false}
onTopicSelected={this.handleContactSelected} />
</div>}
</div>
);
}
};
+71
View File
@@ -0,0 +1,71 @@
/* A password reset form */
import React from 'react';
import VisiblePassword from '../widgets/visible-password.jsx';
export default class PasswordResetView extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
email: '',
password: ''
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleEmailChange = this.handleEmailChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
}
componentDidMount() {
let parsed = parseUrlHash(window.location.hash);
this.setState({token: parsed.params.token, scheme: parsed.params.scheme});
}
handleSubmit(e) {
e.preventDefault();
if (this.state.token) {
this.props.onReset(this.state.scheme, this.state.password.trim(), this.state.token);
} else {
this.props.onRequest("email", this.state.email.trim());
}
}
handleEmailChange(e) {
this.setState({email: e.target.value});
}
handlePasswordChange(e) {
this.setState({password: e.target.value});
}
render() {
let reset = (this.state.token && this.state.scheme);
return (
<form id="password-reset-form" onSubmit={this.handleSubmit}>
{reset ?
<VisiblePassword
placeholder="Enter new password"
autoComplete="new-password"
value={this.state.password}
required={true} autoFocus={true}
onChange={this.handlePasswordChange} />
:
<React.Fragment>
<label htmlFor="inputEmail">Send a password reset email:</label>
<input type="email" id="inputEmail"
placeholder="Your registration email"
autoComplete="email"
value={this.state.email}
onChange={this.handleEmailChange}
required autoFocus />
</React.Fragment>
}
<div className="dialog-buttons">
<button className="blue" type="submit">{reset ? "Reset" : "Send request"}</button>
</div>
</form>
);
}
}
+75
View File
@@ -0,0 +1,75 @@
// Tinode config panel.
import React from 'react';
import HostSelector from '../widgets/host-selector.jsx';
export default class SettingsView extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
transport: props.transport || 'def',
serverAddress: props.serverAddress,
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleTransportSelected = this.handleTransportSelected.bind(this);
this.handleServerAddressChange = this.handleServerAddressChange.bind(this);
}
handleSubmit(e) {
e.preventDefault();
this.props.onUpdate({
transport: this.state.transport,
serverAddress: this.state.serverAddress
});
}
handleTransportSelected(e) {
this.setState({transport: e.currentTarget.value});
}
handleServerAddressChange(name) {
this.setState({serverAddress: name});
this.props.onServerAddressChange(name);
}
render() {
var names = {def: "default", ws: "websocket", lp: "long polling"};
var transportOptions = [];
var instance = this;
['def', 'ws', 'lp'].map(function(item){
var id = 'transport-' + item;
var name = names[item];
transportOptions.push(
<li key={item}>
<input type="radio" id={id} name="transport-select" value={item}
checked={instance.state.transport === item}
onChange={instance.handleTransportSelected} />
<label htmlFor={id}>{name}</label>
</li>
);
});
return (
<form id="settings-form" className="panel-form" onSubmit={this.handleSubmit}>
<div className="panel-form-row">
<label className="small">Server to use:</label>
</div>
<HostSelector serverAddress={this.state.serverAddress}
onServerAddressChange={this.handleServerAddressChange} />
<div className="panel-form-row">
<label className="small">Wire transport:</label>
</div>
<div className="panel-form-row">
<ul className="quoted">
{transportOptions}
</ul>
</div>
<div className="dialog-buttons">
<button type="submit" className="blue">Update</button>
</div>
</form>
);
}
};
+151
View File
@@ -0,0 +1,151 @@
import React from 'react';
import SideNavbar from '../widgets/side-navbar.jsx';
import ErrorPanel from '../widgets/error-panel.jsx';
import ContactsView from './contacts-view.jsx';
import CreateAccountView from './create-account-view.jsx';
import EditAccountView from './edit-account-view.jsx';
import LoginView from './login-view.jsx';
import NewTopicView from './new-topic-view.jsx';
import PasswordResetView from './password-reset-view.jsx';
import SettingsView from './settings-view.jsx';
import ValidationView from './validation-view.jsx';
export default class SidepanelView extends React.Component {
constructor(props) {
super(props);
this.handleLoginRequested = this.handleLoginRequested.bind(this);
}
handleLoginRequested(login, password) {
this.props.onLoginRequest(login, password);
}
render() {
var title = null;
var avatar = false;
var onCancel = undefined;
var view = this.props.state || (this.props.myUserId ? 'contacts' : 'login');
switch (view) {
case 'login':
title = "Sign In";
break;
case 'register':
title = "Create Account";
onCancel = this.props.onCancel;
break;
case 'settings':
title = "Settings";
onCancel = this.props.onCancel;
break;
case 'edit':
title = "Edit Account";
onCancel = this.props.onCancel;
break;
case 'contacts':
title = this.props.title;
avatar = this.props.avatar ? this.props.avatar : true;
break;
case 'newtpk':
title = "Start New Chat";
onCancel = this.props.onCancel;
break;
case 'cred':
title = "Confirm Credentials";
onCancel = this.props.onCancel;
break;
case 'reset':
title = "Reset Password";
onCancel = this.props.onCancel;
break;
default:;
};
return (
<div id="sidepanel" className={this.props.hideSelf ? 'nodisplay' : null}>
<SideNavbar
state={view}
title={title}
avatar={avatar}
myUserId={this.props.myUserId}
onSignUp={this.props.onSignUp}
onSettings={this.props.onSettings}
onNewTopic={this.props.onNewTopic}
onCancel={onCancel} />
<ErrorPanel
level={this.props.errorLevel}
text={this.props.errorText}
onClearError={this.props.onError} />
{view === 'login' ?
<LoginView
login={this.props.login}
disabled={this.props.loginDisabled}
onLogin={this.handleLoginRequested} /> :
view === 'register' ?
<CreateAccountView
onCreateAccount={this.props.onCreateAccount}
onCancel={this.props.onCancel}
onError={this.props.onError} /> :
view === 'settings' ?
<SettingsView
transport={this.props.transport}
serverAddress={this.props.serverAddress}
onServerAddressChange={this.props.onGlobalSettings}
onCancel={this.props.onCancel}
onUpdate={this.props.onGlobalSettings} /> :
view === 'edit' ?
<EditAccountView
tinode={this.props.tinode}
myUserId={this.props.myUserId}
messageSounds={this.props.messageSounds}
desktopAlerts={this.props.desktopAlerts}
desktopAlertsEnabled={this.props.desktopAlertsEnabled}
onUpdateAccount={this.props.onUpdateAccount}
onUpdateTags={this.props.onUpdateAccountTags}
onTogglePushNotifications={this.props.onTogglePushNotifications}
onToggleMessageSounds={this.props.onToggleMessageSounds}
onLogout={this.props.onLogout}
onCancel={this.props.onCancel}
onError={this.props.onError} /> :
view === 'contacts' ?
<ContactsView
tinode={this.props.tinode}
myUserId={this.props.myUserId}
connected={this.props.connected}
topicSelected={this.props.topicSelected}
chatList={this.props.chatList}
showContextMenu={this.props.showContextMenu}
onTopicSelected={this.props.onTopicSelected} /> :
view === 'newtpk' ?
<NewTopicView
searchResults={this.props.searchResults}
onInitFind={this.props.onInitFind}
onSearchContacts={this.props.onSearchContacts}
onCreateTopic={this.props.onCreateTopic}
onError={this.props.onError} /> :
view === 'cred' ?
<ValidationView
credCode={this.props.credCode}
credMethod={this.props.credMethod}
onSubmit={this.props.onValidateCredentials}
onCancel={this.props.onCancel}
onError={this.props.onError} /> :
view === 'reset' ?
<PasswordResetView
onRequest={this.props.onPasswordResetRequest}
onReset={this.props.onResetPassword} /> :
null}
</div>
);
}
};
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
// ValidationView: panel for confirming credentials, like email or phone.
import React from 'react';
export default class ValidationView extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
code: props.credCode || ''
};
this.handleChange = this.handleChange.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(e) {
this.setState({code: e.target.value});
}
handleKeyPress(e) {
if (e.key === 'Enter') {
this.handleSubmit(e);
}
}
handleSubmit(e) {
e.preventDefault();
if (this.state.code && this.state.code.trim()) {
this.props.onSubmit(this.props.credMethod, this.state.code.trim());
}
}
render() {
var methods = {'email': 'email', 'tel': 'phone'};
var method = methods[this.props.credMethod] || this.props.credMethod;
return (
<div className="panel-form">
<div className="panel-form-row">
<label className="small" htmlFor="enter-confirmation-code">
Enter confirmation code sent to you by {method}:
</label>
</div>
<div className="panel-form-row">
<input type="text" id="enter-confirmation-code" placeholder="Numbers only"
value={this.state.code} onChange={this.handleChange}
onKeyPress={this.handleKeyPress} required />
</div>
<div className="dialog-buttons">
<button className="blue" onClick={this.handleSubmit}>Confirm</button>
</div>
</div>
);
}
};