Bug 1142515: implement updating a room with changed context information. r=Standard8

This commit is contained in:
Mike de Boer 2015-05-07 11:38:57 +02:00
Родитель 7acc550f67
Коммит 18d196064c
13 изменённых файлов: 701 добавлений и 168 удалений

Просмотреть файл

@ -436,7 +436,7 @@ loop.panel = (function(_, mozL10n) {
propTypes: {
mozLoop: React.PropTypes.object.isRequired,
roomUrls: React.PropTypes.object
roomUrls: React.PropTypes.array
},
handleClick: function(event) {

Просмотреть файл

@ -436,7 +436,7 @@ loop.panel = (function(_, mozL10n) {
propTypes: {
mozLoop: React.PropTypes.object.isRequired,
roomUrls: React.PropTypes.object
roomUrls: React.PropTypes.array
},
handleClick: function(event) {

Просмотреть файл

@ -91,9 +91,9 @@ loop.store = loop.store || {};
"getAllRooms",
"getAllRoomsError",
"openRoom",
"renameRoom",
"renameRoomError",
"shareRoomUrl",
"updateRoomContext",
"updateRoomContextError",
"updateRoomList"
],
@ -469,29 +469,79 @@ loop.store = loop.store || {};
},
/**
* Renames a room.
* Updates the context data attached to a room.
*
* @param {sharedActions.RenameRoom} actionData
* @param {sharedActions.UpdateRoomContext} actionData
*/
renameRoom: function(actionData) {
var oldRoomName = this.getStoreState("roomName");
var newRoomName = actionData.newRoomName.trim();
updateRoomContext: function(actionData) {
this._mozLoop.rooms.get(actionData.roomToken, function(err, room) {
if (err) {
this.dispatchAction(new sharedActions.UpdateRoomContextError({
error: err
}));
return;
}
// Skip update if name is unchanged or empty.
if (!newRoomName || oldRoomName === newRoomName) {
return;
}
this.setStoreState({error: null});
this._mozLoop.rooms.rename(actionData.roomToken, newRoomName,
function(err) {
if (err) {
this.dispatchAction(new sharedActions.RenameRoomError({error: err}));
var roomData = {};
var context = room.decryptedContext;
var oldRoomName = context.roomName;
var newRoomName = actionData.newRoomName.trim();
if (newRoomName && oldRoomName != newRoomName) {
roomData.roomName = newRoomName;
}
var oldRoomURLs = context.urls;
var oldRoomURL = oldRoomURLs && oldRoomURLs[0];
// Since we want to prevent storing falsy (i.e. empty) values for context
// data, there's no need to send that to the server as an update.
var newRoomURL = loop.shared.utils.stripFalsyValues({
location: actionData.newRoomURL ? actionData.newRoomURL.trim() : "",
thumbnail: actionData.newRoomURL ? actionData.newRoomThumbnail.trim() : "",
description: actionData.newRoomDescription ?
actionData.newRoomDescription.trim() : ""
});
// Only attach a context to the room when
// 1) there was already a URL set,
// 2) a new URL is provided as of now,
// 3) the URL data has changed.
var diff = loop.shared.utils.objectDiff(oldRoomURL, newRoomURL);
if (diff.added.length || diff.updated.length) {
newRoomURL = _.extend(oldRoomURL || {}, newRoomURL);
var isValidURL = false;
try {
isValidURL = new URL(newRoomURL.location);
} catch(ex) {}
if (isValidURL) {
roomData.urls = [newRoomURL];
}
}.bind(this));
}
// TODO: there currently is no clear UX defined on what to do when all
// context data was cleared, e.g. when diff.removed contains all the
// context properties. Until then, we can't deal with context removal here.
// When no properties have been set on the roomData object, there's nothing
// to save.
if (!Object.getOwnPropertyNames(roomData).length) {
return;
}
this.setStoreState({error: null});
this._mozLoop.rooms.update(actionData.roomToken, roomData,
function(err, data) {
if (err) {
this.dispatchAction(new sharedActions.UpdateRoomContextError({
error: err
}));
}
}.bind(this));
}.bind(this));
},
renameRoomError: function(actionData) {
/**
* Updating the context data attached to a room error.
*
* @param {sharedActions.UpdateRoomContextError} actionData
*/
updateRoomContextError: function(actionData) {
this.setStoreState({error: actionData.error});
}
});

Просмотреть файл

@ -170,11 +170,12 @@ loop.roomViews = (function(mozL10n) {
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({displayName: "DesktopRoomInvitationView",
mixins: [React.addons.LinkedStateMixin, sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
error: React.PropTypes.object,
mozLoop: React.PropTypes.object.isRequired,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired,
@ -184,29 +185,10 @@ loop.roomViews = (function(mozL10n) {
getInitialState: function() {
return {
copiedUrl: false,
newRoomName: ""
editMode: false
};
},
handleTextareaKeyDown: function(event) {
// Submit the form as soon as the user press Enter in that field
// Note: We're using a textarea instead of a simple text input to display
// placeholder and entered text on two lines, to circumvent l10n
// rendering/UX issues for some locales.
if (event.which === 13) {
this.handleFormSubmit(event);
}
},
handleFormSubmit: function(event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.RenameRoom({
roomToken: this.props.roomData.roomToken,
newRoomName: this.state.newRoomName
}));
},
handleEmailButtonClick: function(event) {
event.preventDefault();
@ -229,27 +211,35 @@ loop.roomViews = (function(mozL10n) {
this.toggleDropdownMenu();
},
handleAddContextClick: function(event) {
event.preventDefault();
this.handleEditModeChange(true);
},
handleEditModeChange: function(newEditMode) {
this.setState({ editMode: newEditMode });
},
render: function() {
if (!this.props.show) {
return null;
}
var canAddContext = this.props.mozLoop.getLoopPref("contextInConversations.enabled") &&
!this.props.showContext && !this.state.editMode;
var cx = React.addons.classSet;
return (
React.createElement("div", {className: "room-invitation-overlay"},
React.createElement("div", {className: "room-invitation-content"},
React.createElement("p", {className: cx({"error": !!this.props.error,
"error-display-area": true})},
mozL10n.get("rooms_name_change_failed_label")
React.createElement("p", {className: cx({hide: this.state.editMode})},
mozL10n.get("invite_header_text")
),
React.createElement("form", {onSubmit: this.handleFormSubmit},
React.createElement("textarea", {rows: "2", type: "text", className: "input-room-name",
valueLink: this.linkState("newRoomName"),
onBlur: this.handleFormSubmit,
onKeyDown: this.handleTextareaKeyDown,
placeholder: mozL10n.get("rooms_name_this_room_label")})
React.createElement("a", {className: cx({hide: !canAddContext, "room-invitation-addcontext": true}),
onClick: this.handleAddContextClick},
mozL10n.get("context_add_some_label")
),
React.createElement("p", null, mozL10n.get("invite_header_text")),
React.createElement("div", {className: "btn-group call-action-group"},
React.createElement("button", {className: "btn btn-info btn-email",
onClick: this.handleEmailButtonClick},
@ -275,47 +265,257 @@ loop.roomViews = (function(mozL10n) {
ref: "menu"})
),
React.createElement(DesktopRoomContextView, {
dispatcher: this.props.dispatcher,
editMode: this.state.editMode,
error: this.props.error,
mozLoop: this.props.mozLoop,
onEditModeChange: this.handleEditModeChange,
roomData: this.props.roomData,
show: this.props.showContext})
show: this.props.showContext || this.state.editMode})
)
);
}
});
var DesktopRoomContextView = React.createClass({displayName: "DesktopRoomContextView",
mixins: [React.addons.LinkedStateMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
editMode: React.PropTypes.bool,
error: React.PropTypes.object,
mozLoop: React.PropTypes.object.isRequired,
onEditModeChange: React.PropTypes.func,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired
},
componentWillReceiveProps: function(nextProps) {
var newState = {};
// When the 'show' prop is changed from outside this component, we do need
// to update the state.
if (("show" in nextProps) && nextProps.show !== this.props.show) {
this.setState({ show: nextProps.show });
newState.show = nextProps.show;
}
if (("editMode" in nextProps && nextProps.editMode !== this.props.editMode)) {
newState.editMode = nextProps.editMode;
// If we're switching to edit mode, fetch the metadata of the current tab.
// But _only_ if there's no context currently attached to the room; the
// checkbox will be disabled in that case.
if (nextProps.editMode) {
this.props.mozLoop.getSelectedTabMetadata(function(metadata) {
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
var description = metadata.description || metadata.title;
var url = metadata.url;
this.setState({
availableContext: {
previewImage: previewImage,
description: description,
url: url
}
});
}.bind(this));
}
}
// When we receive an update for the `roomData` property, make sure that
// the current form fields reflect reality. This is necessary, because the
// form state is maintained in the components' state.
if (nextProps.roomData) {
// Right now it's only necessary to update the form input states when
// they contain no text yet.
if (!this.state.newRoomName && nextProps.roomData.roomName) {
newState.newRoomName = nextProps.roomData.roomName;
}
var url = this._getURL(nextProps.roomData);
if (url) {
if (!this.state.newRoomURL && url.location) {
newState.newRoomURL = url.location;
}
if (!this.state.newRoomDescription && url.description) {
newState.newRoomDescription = url.description;
}
if (!this.state.newRoomThumbnail && url.thumbnail) {
newState.newRoomThumbnail = url.thumbnail;
}
}
}
if (Object.getOwnPropertyNames(newState).length) {
this.setState(newState);
}
},
getDefaultProps: function() {
return { editMode: false };
},
getInitialState: function() {
return { show: this.props.show };
var url = this._getURL();
return {
// `availableContext` prop only used in tests.
availableContext: this.props.availableContext,
editMode: this.props.editMode,
show: this.props.show,
newRoomName: this.props.roomData.roomName || "",
newRoomURL: url && url.location || "",
newRoomDescription: url && url.description || "",
newRoomThumbnail: url && url.thumbnail || ""
};
},
handleCloseClick: function() {
handleCloseClick: function(event) {
event.preventDefault();
if (this.state.editMode) {
this.setState({ editMode: false });
if (this.props.onEditModeChange) {
this.props.onEditModeChange(false);
}
return;
}
this.setState({ show: false });
},
handleEditClick: function(event) {
event.preventDefault();
this.setState({ editMode: true });
if (this.props.onEditModeChange) {
this.props.onEditModeChange(true);
}
},
handleCheckboxChange: function(state) {
if (state.checked) {
// The checkbox was checked, prefill the fields with the values available
// in `availableContext`.
var context = this.state.availableContext;
this.setState({
newRoomURL: context.url,
newRoomDescription: context.description,
newRoomThumbnail: context.previewImage
}, this.handleFormSubmit);
} else {
this.setState({
newRoomURL: "",
newRoomDescription: "",
newRoomThumbnail: ""
}, this.handleFormSubmit);
}
},
handleFormSubmit: function(event) {
event && event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.UpdateRoomContext({
roomToken: this.props.roomData.roomToken,
newRoomName: this.state.newRoomName,
newRoomURL: this.state.newRoomURL,
newRoomDescription: this.state.newRoomDescription,
newRoomThumbnail: this.state.newRoomThumbnail
}));
},
handleTextareaKeyDown: function(event) {
// Submit the form as soon as the user press Enter in that field
// Note: We're using a textarea instead of a simple text input to display
// placeholder and entered text on two lines, to circumvent l10n
// rendering/UX issues for some locales.
if (event.which === 13) {
this.handleFormSubmit(event);
}
},
/**
* Utility function to extract URL context data from the `roomData` property
* that can also be supplied as an argument.
*
* @param {Object} roomData Optional room data object to use, equivalent to
* the activeRoomStore state.
* @return {Object} The first context URL found on the `roomData` object.
*/
_getURL: function(roomData) {
roomData = roomData || this.props.roomData;
return this.props.roomData.roomContextUrls &&
this.props.roomData.roomContextUrls[0];
},
/**
* Truncate a string if it exceeds the length as defined in `maxLen`, which
* is defined as '72' characters by default. If the string needs trimming,
* it'll be suffixed with the unicode ellipsis char, \u2026.
*
* @param {String} str The string to truncate, if needed.
* @param {Number} maxLen Maximum number of characters that the string is
* allowed to contain. Optional, defaults to 72.
* @return {String} Truncated version of `str`.
*/
_truncate: function(str, maxLen) {
if (!maxLen) {
maxLen = 72;
}
return (str.length > maxLen) ? str.substr(0, maxLen) + "…" : str;
},
render: function() {
if (!this.state.show)
if (!this.state.show && !this.state.editMode)
return null;
var URL = this.props.roomData.roomContextUrls && this.props.roomData.roomContextUrls[0];
var thumbnail = URL && URL.thumbnail || "";
var URLDescription = URL && URL.description || "";
var location = URL && URL.location || "";
var url = this._getURL();
var thumbnail = url && url.thumbnail || "";
var urlDescription = url && url.description || "";
var location = url && url.location || "";
var checkboxLabel = null;
var locationData = null;
if (location) {
locationData = sharedUtils.formatURL(location);
locationData = checkboxLabel = sharedUtils.formatURL(location);
}
if (!checkboxLabel) {
checkboxLabel = sharedUtils.formatURL((this.state.availableContext ?
this.state.availableContext.url : ""));
}
var cx = React.addons.classSet;
if (this.state.editMode) {
return (
React.createElement("div", {className: "room-context"},
React.createElement("div", {className: "room-context-content"},
React.createElement("p", {className: cx({"error": !!this.props.error,
"error-display-area": true})},
mozL10n.get("rooms_change_failed_label")
),
React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")),
React.createElement(sharedViews.Checkbox, {
checked: !!url,
disabled: !!url || !checkboxLabel,
label: mozL10n.get("context_edit_activate_label", {
title: checkboxLabel ? checkboxLabel.hostname : ""
}),
onChange: this.handleCheckboxChange,
value: location}),
React.createElement("form", {onSubmit: this.handleFormSubmit},
React.createElement("textarea", {rows: "2", type: "text", className: "room-context-name",
onBlur: this.handleFormSubmit,
onKeyDown: this.handleTextareaKeyDown,
placeholder: mozL10n.get("context_edit_name_placeholder"),
valueLink: this.linkState("newRoomName")}),
React.createElement("input", {type: "text", className: "room-context-url",
onBlur: this.handleFormSubmit,
onKeyDown: this.handleTextareaKeyDown,
placeholder: "https://",
valueLink: this.linkState("newRoomURL")}),
React.createElement("textarea", {rows: "4", type: "text", className: "room-context-comments",
onBlur: this.handleFormSubmit,
onKeyDown: this.handleTextareaKeyDown,
placeholder: mozL10n.get("context_edit_comments_placeholder"),
valueLink: this.linkState("newRoomDescription")})
),
React.createElement("button", {className: "room-context-btn-close",
onClick: this.handleCloseClick})
)
)
);
}
if (!locationData) {
@ -327,7 +527,8 @@ loop.roomViews = (function(mozL10n) {
React.createElement("img", {className: "room-context-thumbnail", src: thumbnail}),
React.createElement("div", {className: "room-context-content"},
React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")),
React.createElement("div", {className: "room-context-description"}, URLDescription),
React.createElement("div", {className: "room-context-description",
title: urlDescription}, this._truncate(urlDescription)),
React.createElement("a", {className: "room-context-url",
href: location,
target: "_blank",
@ -336,7 +537,9 @@ loop.roomViews = (function(mozL10n) {
React.createElement("div", {className: "room-context-comment"}, this.props.roomData.roomDescription) :
null,
React.createElement("button", {className: "room-context-btn-close",
onClick: this.handleCloseClick})
onClick: this.handleCloseClick}),
React.createElement("button", {className: "room-context-btn-edit",
onClick: this.handleEditClick})
)
)
);
@ -457,6 +660,7 @@ loop.roomViews = (function(mozL10n) {
React.createElement(DesktopRoomInvitationView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
mozLoop: this.props.mozLoop,
roomData: roomData,
show: shouldRenderInvitationOverlay,
showContext: shouldRenderContextView,
@ -481,6 +685,9 @@ loop.roomViews = (function(mozL10n) {
)
),
React.createElement(DesktopRoomContextView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
mozLoop: this.props.mozLoop,
roomData: roomData,
show: !shouldRenderInvitationOverlay && shouldRenderContextView})
)

Просмотреть файл

@ -170,11 +170,12 @@ loop.roomViews = (function(mozL10n) {
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({
mixins: [React.addons.LinkedStateMixin, sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
error: React.PropTypes.object,
mozLoop: React.PropTypes.object.isRequired,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired,
@ -184,29 +185,10 @@ loop.roomViews = (function(mozL10n) {
getInitialState: function() {
return {
copiedUrl: false,
newRoomName: ""
editMode: false
};
},
handleTextareaKeyDown: function(event) {
// Submit the form as soon as the user press Enter in that field
// Note: We're using a textarea instead of a simple text input to display
// placeholder and entered text on two lines, to circumvent l10n
// rendering/UX issues for some locales.
if (event.which === 13) {
this.handleFormSubmit(event);
}
},
handleFormSubmit: function(event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.RenameRoom({
roomToken: this.props.roomData.roomToken,
newRoomName: this.state.newRoomName
}));
},
handleEmailButtonClick: function(event) {
event.preventDefault();
@ -229,27 +211,35 @@ loop.roomViews = (function(mozL10n) {
this.toggleDropdownMenu();
},
handleAddContextClick: function(event) {
event.preventDefault();
this.handleEditModeChange(true);
},
handleEditModeChange: function(newEditMode) {
this.setState({ editMode: newEditMode });
},
render: function() {
if (!this.props.show) {
return null;
}
var canAddContext = this.props.mozLoop.getLoopPref("contextInConversations.enabled") &&
!this.props.showContext && !this.state.editMode;
var cx = React.addons.classSet;
return (
<div className="room-invitation-overlay">
<div className="room-invitation-content">
<p className={cx({"error": !!this.props.error,
"error-display-area": true})}>
{mozL10n.get("rooms_name_change_failed_label")}
<p className={cx({hide: this.state.editMode})}>
{mozL10n.get("invite_header_text")}
</p>
<form onSubmit={this.handleFormSubmit}>
<textarea rows="2" type="text" className="input-room-name"
valueLink={this.linkState("newRoomName")}
onBlur={this.handleFormSubmit}
onKeyDown={this.handleTextareaKeyDown}
placeholder={mozL10n.get("rooms_name_this_room_label")} />
</form>
<p>{mozL10n.get("invite_header_text")}</p>
<a className={cx({hide: !canAddContext, "room-invitation-addcontext": true})}
onClick={this.handleAddContextClick}>
{mozL10n.get("context_add_some_label")}
</a>
<div className="btn-group call-action-group">
<button className="btn btn-info btn-email"
onClick={this.handleEmailButtonClick}>
@ -275,47 +265,257 @@ loop.roomViews = (function(mozL10n) {
ref="menu" />
</div>
<DesktopRoomContextView
dispatcher={this.props.dispatcher}
editMode={this.state.editMode}
error={this.props.error}
mozLoop={this.props.mozLoop}
onEditModeChange={this.handleEditModeChange}
roomData={this.props.roomData}
show={this.props.showContext} />
show={this.props.showContext || this.state.editMode} />
</div>
);
}
});
var DesktopRoomContextView = React.createClass({
mixins: [React.addons.LinkedStateMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
editMode: React.PropTypes.bool,
error: React.PropTypes.object,
mozLoop: React.PropTypes.object.isRequired,
onEditModeChange: React.PropTypes.func,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired
},
componentWillReceiveProps: function(nextProps) {
var newState = {};
// When the 'show' prop is changed from outside this component, we do need
// to update the state.
if (("show" in nextProps) && nextProps.show !== this.props.show) {
this.setState({ show: nextProps.show });
newState.show = nextProps.show;
}
if (("editMode" in nextProps && nextProps.editMode !== this.props.editMode)) {
newState.editMode = nextProps.editMode;
// If we're switching to edit mode, fetch the metadata of the current tab.
// But _only_ if there's no context currently attached to the room; the
// checkbox will be disabled in that case.
if (nextProps.editMode) {
this.props.mozLoop.getSelectedTabMetadata(function(metadata) {
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
var description = metadata.description || metadata.title;
var url = metadata.url;
this.setState({
availableContext: {
previewImage: previewImage,
description: description,
url: url
}
});
}.bind(this));
}
}
// When we receive an update for the `roomData` property, make sure that
// the current form fields reflect reality. This is necessary, because the
// form state is maintained in the components' state.
if (nextProps.roomData) {
// Right now it's only necessary to update the form input states when
// they contain no text yet.
if (!this.state.newRoomName && nextProps.roomData.roomName) {
newState.newRoomName = nextProps.roomData.roomName;
}
var url = this._getURL(nextProps.roomData);
if (url) {
if (!this.state.newRoomURL && url.location) {
newState.newRoomURL = url.location;
}
if (!this.state.newRoomDescription && url.description) {
newState.newRoomDescription = url.description;
}
if (!this.state.newRoomThumbnail && url.thumbnail) {
newState.newRoomThumbnail = url.thumbnail;
}
}
}
if (Object.getOwnPropertyNames(newState).length) {
this.setState(newState);
}
},
getDefaultProps: function() {
return { editMode: false };
},
getInitialState: function() {
return { show: this.props.show };
var url = this._getURL();
return {
// `availableContext` prop only used in tests.
availableContext: this.props.availableContext,
editMode: this.props.editMode,
show: this.props.show,
newRoomName: this.props.roomData.roomName || "",
newRoomURL: url && url.location || "",
newRoomDescription: url && url.description || "",
newRoomThumbnail: url && url.thumbnail || ""
};
},
handleCloseClick: function() {
handleCloseClick: function(event) {
event.preventDefault();
if (this.state.editMode) {
this.setState({ editMode: false });
if (this.props.onEditModeChange) {
this.props.onEditModeChange(false);
}
return;
}
this.setState({ show: false });
},
handleEditClick: function(event) {
event.preventDefault();
this.setState({ editMode: true });
if (this.props.onEditModeChange) {
this.props.onEditModeChange(true);
}
},
handleCheckboxChange: function(state) {
if (state.checked) {
// The checkbox was checked, prefill the fields with the values available
// in `availableContext`.
var context = this.state.availableContext;
this.setState({
newRoomURL: context.url,
newRoomDescription: context.description,
newRoomThumbnail: context.previewImage
}, this.handleFormSubmit);
} else {
this.setState({
newRoomURL: "",
newRoomDescription: "",
newRoomThumbnail: ""
}, this.handleFormSubmit);
}
},
handleFormSubmit: function(event) {
event && event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.UpdateRoomContext({
roomToken: this.props.roomData.roomToken,
newRoomName: this.state.newRoomName,
newRoomURL: this.state.newRoomURL,
newRoomDescription: this.state.newRoomDescription,
newRoomThumbnail: this.state.newRoomThumbnail
}));
},
handleTextareaKeyDown: function(event) {
// Submit the form as soon as the user press Enter in that field
// Note: We're using a textarea instead of a simple text input to display
// placeholder and entered text on two lines, to circumvent l10n
// rendering/UX issues for some locales.
if (event.which === 13) {
this.handleFormSubmit(event);
}
},
/**
* Utility function to extract URL context data from the `roomData` property
* that can also be supplied as an argument.
*
* @param {Object} roomData Optional room data object to use, equivalent to
* the activeRoomStore state.
* @return {Object} The first context URL found on the `roomData` object.
*/
_getURL: function(roomData) {
roomData = roomData || this.props.roomData;
return this.props.roomData.roomContextUrls &&
this.props.roomData.roomContextUrls[0];
},
/**
* Truncate a string if it exceeds the length as defined in `maxLen`, which
* is defined as '72' characters by default. If the string needs trimming,
* it'll be suffixed with the unicode ellipsis char, \u2026.
*
* @param {String} str The string to truncate, if needed.
* @param {Number} maxLen Maximum number of characters that the string is
* allowed to contain. Optional, defaults to 72.
* @return {String} Truncated version of `str`.
*/
_truncate: function(str, maxLen) {
if (!maxLen) {
maxLen = 72;
}
return (str.length > maxLen) ? str.substr(0, maxLen) + "…" : str;
},
render: function() {
if (!this.state.show)
if (!this.state.show && !this.state.editMode)
return null;
var URL = this.props.roomData.roomContextUrls && this.props.roomData.roomContextUrls[0];
var thumbnail = URL && URL.thumbnail || "";
var URLDescription = URL && URL.description || "";
var location = URL && URL.location || "";
var url = this._getURL();
var thumbnail = url && url.thumbnail || "";
var urlDescription = url && url.description || "";
var location = url && url.location || "";
var checkboxLabel = null;
var locationData = null;
if (location) {
locationData = sharedUtils.formatURL(location);
locationData = checkboxLabel = sharedUtils.formatURL(location);
}
if (!checkboxLabel) {
checkboxLabel = sharedUtils.formatURL((this.state.availableContext ?
this.state.availableContext.url : ""));
}
var cx = React.addons.classSet;
if (this.state.editMode) {
return (
<div className="room-context">
<div className="room-context-content">
<p className={cx({"error": !!this.props.error,
"error-display-area": true})}>
{mozL10n.get("rooms_change_failed_label")}
</p>
<div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
<sharedViews.Checkbox
checked={!!url}
disabled={!!url || !checkboxLabel}
label={mozL10n.get("context_edit_activate_label", {
title: checkboxLabel ? checkboxLabel.hostname : ""
})}
onChange={this.handleCheckboxChange}
value={location} />
<form onSubmit={this.handleFormSubmit}>
<textarea rows="2" type="text" className="room-context-name"
onBlur={this.handleFormSubmit}
onKeyDown={this.handleTextareaKeyDown}
placeholder={mozL10n.get("context_edit_name_placeholder")}
valueLink={this.linkState("newRoomName")} />
<input type="text" className="room-context-url"
onBlur={this.handleFormSubmit}
onKeyDown={this.handleTextareaKeyDown}
placeholder="https://"
valueLink={this.linkState("newRoomURL")} />
<textarea rows="4" type="text" className="room-context-comments"
onBlur={this.handleFormSubmit}
onKeyDown={this.handleTextareaKeyDown}
placeholder={mozL10n.get("context_edit_comments_placeholder")}
valueLink={this.linkState("newRoomDescription")} />
</form>
<button className="room-context-btn-close"
onClick={this.handleCloseClick}/>
</div>
</div>
);
}
if (!locationData) {
@ -327,7 +527,8 @@ loop.roomViews = (function(mozL10n) {
<img className="room-context-thumbnail" src={thumbnail}/>
<div className="room-context-content">
<div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
<div className="room-context-description">{URLDescription}</div>
<div className="room-context-description"
title={urlDescription}>{this._truncate(urlDescription)}</div>
<a className="room-context-url"
href={location}
target="_blank"
@ -337,6 +538,8 @@ loop.roomViews = (function(mozL10n) {
null}
<button className="room-context-btn-close"
onClick={this.handleCloseClick}/>
<button className="room-context-btn-edit"
onClick={this.handleEditClick}/>
</div>
</div>
);
@ -457,6 +660,7 @@ loop.roomViews = (function(mozL10n) {
<DesktopRoomInvitationView
dispatcher={this.props.dispatcher}
error={this.state.error}
mozLoop={this.props.mozLoop}
roomData={roomData}
show={shouldRenderInvitationOverlay}
showContext={shouldRenderContextView}
@ -481,6 +685,9 @@ loop.roomViews = (function(mozL10n) {
</div>
</div>
<DesktopRoomContextView
dispatcher={this.props.dispatcher}
error={this.state.error}
mozLoop={this.props.mozLoop}
roomData={roomData}
show={!shouldRenderInvitationOverlay && shouldRenderContextView} />
</div>

Просмотреть файл

@ -874,7 +874,6 @@ html, .fx-embedded, #main,
align-items: center;
}
.room-invitation-overlay .error-display-area.error,
.room-invitation-overlay input[type="text"] {
display: block;
background-color: rgba(0,0,0,.5);
@ -882,35 +881,31 @@ html, .fx-embedded, #main,
padding: .5em;
}
.room-invitation-overlay .error-display-area {
display: none;
}
.room-invitation-overlay .error-display-area.error {
position: absolute;
top: 2em;
left: 1em;
right: 1em;
text-align: start;
width: calc(258px - 2em);
color: #d74345;
}
.room-invitation-overlay .btn-group {
padding: 0;
}
.room-invitation-overlay textarea {
display: block;
background: rgba(0, 0, 0, .5);
color: #fff;
font-family: "Helvetica Neue", Arial, sans;
font-size: 1.2em;
border: none;
width: 200px;
margin: 0 auto;
padding: .2em .4em;
border-radius: .5em;
.room-invitation-addcontext {
color: #0095dd;
padding-left: 1.5em;
margin-bottom: 1em;
background-image: url("../img/icons-10x10.svg#edit-active");
background-size: 1em 1em;
background-repeat: no-repeat;
background-position: left top;
font-size: 1em;
cursor: pointer;
}
.room-invitation-addcontext:hover,
.room-invitation-addcontext:hover:active {
text-decoration: underline;
}
body[dir="rtl"] .room-invitation-addcontext {
padding-left: auto;
padding-right: 1.5em;
background-position: right top;
}
.share-service-dropdown {
@ -975,7 +970,7 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
border-top: 2px solid #444;
border-bottom: 2px solid #444;
padding: .5rem;
max-height: 120px;
max-height: 400px;
position: absolute;
left: 0;
bottom: 0;
@ -985,6 +980,8 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
flex-flow: row nowrap;
align-content: flex-start;
align-items: flex-start;
overflow-x: hidden;
overflow-y: auto;
}
.room-invitation-overlay .room-context {
@ -997,7 +994,6 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
.room-context-thumbnail {
width: 100px;
max-height: 200px;
-moz-margin-end: 1ch;
margin-bottom: 1em;
order: 1;
@ -1022,12 +1018,34 @@ body[dir=rtl] .room-context-content {
order: 1;
}
.room-context-content > .error-display-area.error {
display: block;
background-color: rgba(215,67,69,.8);
border-radius: 3px;
padding: .5em;
}
.room-context-content > .error-display-area {
display: none;
}
.room-context-content > .error-display-area.error {
margin: 1em 0 .5em 0;
text-align: center;
text-shadow: 1px 1px 0 rgba(0,0,0,.3);
}
.room-context-content > .checkbox-wrapper {
margin-bottom: .5em;
}
.room-context-label {
margin-bottom: 1em;
}
.room-context-label,
.room-context-description {
.room-context-description,
.room-context-content > .checkbox-wrapper > label {
color: #fff;
}
@ -1051,7 +1069,27 @@ body[dir=rtl] .room-context-content {
text-decoration: underline;
}
.room-context-btn-close {
.room-context-content > form > textarea,
.room-context-content > form > input[type="text"] {
display: block;
background: rgba(0,0,0,.5);
color: #fff;
font-family: "Helvetica Neue", Arial, sans;
font-size: 1em;
border: 1px solid #999;
width: 100%;
padding: .2em .4em;
border-radius: 3px;
resize: none;
}
.room-context-content > form > textarea:not(:last-of-type),
.room-context-content > form > input[type="text"] {
margin: 0 0 .5em 0;
}
.room-context-btn-close,
.room-context-btn-edit {
position: absolute;
right: 5px;
top: 5px;
@ -1066,16 +1104,31 @@ body[dir=rtl] .room-context-content {
cursor: pointer;
}
.room-context-btn-edit {
right: 18px;
background-image: url("../img/icons-10x10.svg#edit");
}
.room-context-btn-edit:hover,
.room-context-btn-edit:hover:active {
background-image: url("../img/icons-10x10.svg#edit-active");
}
.room-context-btn-close:hover,
.room-context-btn-close:hover:active {
background-image: url("../img/icons-10x10.svg#close-active");
}
body[dir=rtl] .room-context-btn-close {
body[dir=rtl] .room-context-btn-close,
body[dir=rtl] .room-context-btn-edit {
right: auto;
left: 5px;
}
body[dir=rtl] .room-context-btn-edit {
left: 18px;
}
/* Standalone rooms */
.standalone .room-conversation-wrapper {

Просмотреть файл

@ -39,6 +39,11 @@ use[id$="-disabled"] {
<path id="dropdown-shape" fill-rule="evenodd" clip-rule="evenodd" d="M9,3L4.984,7L1,3H9z"/>
<polygon id="expand-shape" fill-rule="evenodd" clip-rule="evenodd" points="10,0 4.838,0 6.506,1.669 0,8.175 1.825,10 8.331,3.494
10,5.162"/>
<path id="edit-shape" d="M5.493,1.762l2.745,2.745L2.745,10H0V7.255L5.493,1.762z M2.397,9.155l0.601-0.601L1.446,7.002L0.845,7.603
V8.31H1.69v0.845H2.397z M5.849,3.028c0-0.096-0.048-0.144-0.146-0.144c-0.044,0-0.081,0.015-0.112,0.046L2.014,6.508
C1.983,6.538,1.968,6.577,1.968,6.619c0,0.098,0.048,0.146,0.144,0.146c0.044,0,0.081-0.015,0.112-0.046l3.579-3.577
C5.834,3.111,5.849,3.073,5.849,3.028z M10,2.395c0,0.233-0.081,0.431-0.245,0.595L8.66,4.085L5.915,1.34L7.01,0.25
C7.168,0.083,7.366,0,7.605,0c0.233,0,0.433,0.083,0.601,0.25l1.55,1.544C9.919,1.966,10,2.166,10,2.395z"/>
<rect id="minimize-shape" y="3.6" fill-rule="evenodd" clip-rule="evenodd" width="10" height="2.8"/>
</defs>
<use id="close" xlink:href="#close-shape"/>
@ -48,6 +53,9 @@ use[id$="-disabled"] {
<use id="dropdown-white" xlink:href="#dropdown-shape"/>
<use id="dropdown-active" xlink:href="#dropdown-shape"/>
<use id="dropdown-disabled" xlink:href="#dropdown-shape"/>
<use id="edit" xlink:href="#edit-shape"/>
<use id="edit-active" xlink:href="#edit-shape"/>
<use id="edit-disabled" xlink:href="#edit-shape"/>
<use id="expand" xlink:href="#expand-shape"/>
<use id="expand-active" xlink:href="#expand-shape"/>
<use id="expand-disabled" xlink:href="#expand-shape"/>

До

Ширина:  |  Высота:  |  Размер: 2.0 KiB

После

Ширина:  |  Высота:  |  Размер: 2.8 KiB

Просмотреть файл

@ -330,19 +330,21 @@ loop.shared.actions = (function() {
}),
/**
* Renames a room.
* Updates the context data attached to a room.
* XXX: should move to some roomActions module - refs bug 1079284
*/
RenameRoom: Action.define("renameRoom", {
UpdateRoomContext: Action.define("updateRoomContext", {
roomToken: String,
newRoomName: String
// newRoomDescription: String, Optional.
// newRoomThumbnail: String, Optional.
// newRoomURL: String Optional.
}),
/**
* Renaming a room error.
* XXX: should move to some roomActions module - refs bug 1079284
* Updating the context data attached to a room error.
*/
RenameRoomError: Action.define("renameRoomError", {
UpdateRoomContextError: Action.define("updateRoomContextError", {
error: [Error, Object]
}),

Просмотреть файл

@ -710,29 +710,36 @@ let LoopRoomsInternal = {
},
/**
* Renames a room.
* Updates a room.
*
* @param {String} roomToken The room token
* @param {String} newRoomName The new name for the room
* @param {Function} callback Function that will be invoked once the operation
* finished. The first argument passed will be an
* `Error` object or `null`.
* @param {String} roomToken The room token
* @param {Object} roomData Updated context data for the room. The following
* properties are expected: `roomName` and `urls`.
* IMPORTANT: Data in the `roomData::urls` array
* will be stored as-is, so any data omitted therein
* will be gone forever.
* @param {Function} callback Function that will be invoked once the operation
* finished. The first argument passed will be an
* `Error` object or `null`.
*/
rename: function(roomToken, newRoomName, callback) {
update: function(roomToken, roomData, callback) {
let room = this.rooms.get(roomToken);
let url = "/rooms/" + encodeURIComponent(roomToken);
let roomData = this.rooms.get(roomToken);
if (!roomData.decryptedContext) {
roomData.decryptedContext = {
roomName: newRoomName
if (!room.decryptedContext) {
room.decryptedContext = {
roomName: roomData.roomName || room.roomName
};
} else {
roomData.decryptedContext.roomName = newRoomName;
room.decryptedContext.roomName = roomData.roomName || room.roomName;
}
if (roomData.urls && roomData.urls.length) {
// For now we only support adding one URL to the room context.
room.decryptedContext.urls = [roomData.urls[0]];
}
Task.spawn(function* () {
let {all, encrypted} = yield this.promiseEncryptRoomData(roomData);
let {all, encrypted} = yield this.promiseEncryptRoomData(room);
// For patch, we only send the context data.
let sendData = {
@ -743,7 +750,7 @@ let LoopRoomsInternal = {
// XXX This should go away once bug 1153788 is fixed.
if (!sendData.context) {
sendData = {
roomName: newRoomName
roomName: room.decryptedContext.roomName
};
} else {
// This might be an upgrade to encrypted rename, so store the key
@ -864,8 +871,8 @@ this.LoopRooms = {
return LoopRoomsInternal.sendConnectionStatus(roomToken, sessionToken, status, callback);
},
rename: function(roomToken, newRoomName, callback) {
return LoopRoomsInternal.rename(roomToken, newRoomName, callback);
update: function(roomToken, roomData, callback) {
return LoopRoomsInternal.update(roomToken, roomData, callback);
},
getGuestCreatedRoom: function() {

Просмотреть файл

@ -115,7 +115,6 @@ rooms_leave_button_label=Leave
rooms_list_copy_url_tooltip=Copy Link
rooms_list_delete_tooltip=Delete conversation
rooms_list_deleteConfirmation_label=Are you sure?
rooms_name_this_room_label=Name this conversation
rooms_new_room_button_label=Start a conversation
rooms_only_occupant_label=You're the first one here.
rooms_panel_title=Choose a conversation or start a new one

Просмотреть файл

@ -164,9 +164,9 @@
var SVGIcons = React.createClass({displayName: "SVGIcons",
shapes: {
"10x10": ["close", "close-active", "close-disabled", "dropdown",
"dropdown-white", "dropdown-active", "dropdown-disabled", "expand",
"expand-active", "expand-disabled", "minimize", "minimize-active",
"minimize-disabled"
"dropdown-white", "dropdown-active", "dropdown-disabled", "edit",
"edit-active", "edit-disabled", "expand", "expand-active", "expand-disabled",
"minimize", "minimize-active", "minimize-disabled"
],
"14x14": ["audio", "audio-active", "audio-disabled", "facemute",
"facemute-active", "facemute-disabled", "hangup", "hangup-active",

Просмотреть файл

@ -164,9 +164,9 @@
var SVGIcons = React.createClass({
shapes: {
"10x10": ["close", "close-active", "close-disabled", "dropdown",
"dropdown-white", "dropdown-active", "dropdown-disabled", "expand",
"expand-active", "expand-disabled", "minimize", "minimize-active",
"minimize-disabled"
"dropdown-white", "dropdown-active", "dropdown-disabled", "edit",
"edit-active", "edit-disabled", "expand", "expand-active", "expand-disabled",
"minimize", "minimize-active", "minimize-disabled"
],
"14x14": ["audio", "audio-active", "audio-disabled", "facemute",
"facemute-active", "facemute-disabled", "hangup", "hangup-active",

Просмотреть файл

@ -306,8 +306,7 @@ rooms_list_current_conversations=Current conversation;Current conversations
rooms_list_delete_tooltip=Delete conversation
rooms_list_deleteConfirmation_label=Are you sure?
rooms_list_no_current_conversations=No current conversations
rooms_name_this_room_label=Name this conversation
rooms_name_change_failed_label=Conversation cannot be renamed
rooms_change_failed_label=Conversation cannot be updated
rooms_new_room_button_label=Start a conversation
rooms_only_occupant_label=You're the first one here.
rooms_panel_title=Choose a conversation or start a new one
@ -340,3 +339,4 @@ context_inroom_label=Let's talk about:
context_edit_activate_label=Talk about "{{title}}"
context_edit_name_placeholder=Conversation Name
context_edit_comments_placeholder=Comments
context_add_some_label=Add some context