merge next into next-reply

This commit is contained in:
or-else
2021-08-29 11:10:13 -07:00
17 changed files with 993 additions and 615 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -57,6 +57,7 @@ const fbMessaging = firebase.messaging();
// This method shows the push notifications while the window is in background. // This method shows the push notifications while the window is in background.
fbMessaging.onBackgroundMessage((payload) => { fbMessaging.onBackgroundMessage((payload) => {
if (payload.data.silent == 'true') { if (payload.data.silent == 'true') {
// TODO: if this is an 'msg', fetch the new message in the background.
return; return;
} }
@@ -68,8 +69,8 @@ fbMessaging.onBackgroundMessage((payload) => {
const pushType = payload.data.what || 'msg'; const pushType = payload.data.what || 'msg';
const title = payload.data.title || self.i18nMessage(pushType == 'msg' ? 'new_message' : 'new_chat'); const title = payload.data.title || self.i18nMessage(pushType == 'msg' ? 'new_message' : 'new_chat');
const options = { const options = {
body: payload.data.content || '', body: payload.data.content || '', // TODO: content for 'sub' should be topic's or user's title.
icon: '/img/logo96.png', icon: '/img/logo96.png', // TODO: use topic's or user's avatar (would have to fetch for 'sub', read from db for 'msg').
badge: '/img/badge96.png', badge: '/img/badge96.png',
tag: payload.data.topic || undefined, tag: payload.data.topic || undefined,
data: { data: {
+1 -1
View File
@@ -39,7 +39,7 @@ export function bytesToHumanSize(bytes) {
// Get 32 bit integer hash value for a string. Ideally it should produce the same value // Get 32 bit integer hash value for a string. Ideally it should produce the same value
// as Java's String#hash(). // as Java's String#hash().
function stringToColorHash(value) { export function stringToColorHash(value) {
let hash = 0; let hash = 0;
value = '' + value; value = '' + value;
for (let i = 0; i < value.length; i++) { for (let i = 0; i < value.length; i++) {
+2 -2
View File
@@ -2,11 +2,11 @@
import Tinode from 'tinode-sdk'; import Tinode from 'tinode-sdk';
import { stringHash } from './strformat.js'; import { stringToColorHash } from './strformat.js';
// User avatar letter color id. // User avatar letter color id.
export function letterTileColor(userId) { export function letterTileColor(userId) {
return Math.abs(stringHash(userId)) % 16; return Math.abs(stringToColorHash(userId)) % 16;
} }
// Make shortcut icon appear with a green dot + show unread count in title. // Make shortcut icon appear with a green dot + show unread count in title.
+91 -93
View File
@@ -91,9 +91,11 @@ class MessagesView extends React.Component {
this.state = MessagesView.getDerivedStateFromProps(props, {}); this.state = MessagesView.getDerivedStateFromProps(props, {});
this.leave = this.leave.bind(this); this.leave = this.leave.bind(this);
this.sendMessage = this.sendMessage.bind(this);
this.sendImageAttachment = this.sendImageAttachment.bind(this); this.sendImageAttachment = this.sendImageAttachment.bind(this);
this.sendFileAttachment = this.sendFileAttachment.bind(this); this.sendFileAttachment = this.sendFileAttachment.bind(this);
this.sendKeyPress = this.sendKeyPress.bind(this); this.sendKeyPress = this.sendKeyPress.bind(this);
this.subscribe = this.subscribe.bind(this);
this.handleScrollReference = this.handleScrollReference.bind(this); this.handleScrollReference = this.handleScrollReference.bind(this);
this.handleScrollEvent = this.handleScrollEvent.bind(this); this.handleScrollEvent = this.handleScrollEvent.bind(this);
this.handleDescChange = this.handleDescChange.bind(this); this.handleDescChange = this.handleDescChange.bind(this);
@@ -114,8 +116,6 @@ class MessagesView extends React.Component {
this.postReadNotification = this.postReadNotification.bind(this); this.postReadNotification = this.postReadNotification.bind(this);
this.clearNotificationQueue = this.clearNotificationQueue.bind(this); this.clearNotificationQueue = this.clearNotificationQueue.bind(this);
this.doSendMessage = this.doSendMessage.bind(this);
this.handleSendMessage = this.handleSendMessage.bind(this);
this.handlePickReply = this.handlePickReply.bind(this); this.handlePickReply = this.handlePickReply.bind(this);
this.handleCancelReply = this.handleCancelReply.bind(this); this.handleCancelReply = this.handleCancelReply.bind(this);
this.handleQuoteClick = this.handleQuoteClick.bind(this); this.handleQuoteClick = this.handleQuoteClick.bind(this);
@@ -145,7 +145,7 @@ class MessagesView extends React.Component {
// or vertical shrinking. // or vertical shrinking.
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (this.messagesScroller) { if (this.messagesScroller) {
if (prevState.topic != this.state.topic || prevState.latestMsgSeq != this.state.latestMsgSeq) { if (prevState.topic != this.state.topic || prevState.messageCount != this.state.messageCount) {
// New message // New message
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition; this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition;
} else if (prevProps.viewportHeight > this.props.viewportHeight) { } else if (prevProps.viewportHeight > this.props.viewportHeight) {
@@ -178,49 +178,8 @@ class MessagesView extends React.Component {
this.postReadNotification(0); this.postReadNotification(0);
} }
if (topic && !topic.isSubscribed() && this.props.ready && if ((this.state.topic != prevState.topic) || !prevProps.ready) {
((this.state.topic != prevState.topic) || !prevProps.ready)) { this.subscribe(topic);
// Is this a new topic?
const newTopic = (this.props.newTopicParams && this.props.newTopicParams._topicName == this.props.topic);
// 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();
if (this.state.isReader || newTopic) {
// Reading is either permitted or we don't know because it's a new topic. Ask for messages.
getQuery = getQuery.withLaterData(MESSAGES_PAGE);
if (this.state.isReader) {
getQuery = getQuery.withLaterDel();
}
// And show "loading" spinner.
this.setState({ fetchingMessages: true });
}
const setQuery = newTopic ? this.props.newTopicParams : undefined;
topic.subscribe(getQuery.build(), setQuery)
.then((ctrl) => {
if (ctrl.code == 303) {
// Redirect to another topic requested.
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', ctrl.params.topic));
return;
}
if (this.state.topic != ctrl.topic) {
this.setState({topic: ctrl.topic});
}
this.props.onNewTopicCreated(this.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) => {
console.log("Failed subscription to", this.state.topic);
this.props.onError(err.message, 'err');
const blankState = MessagesView.getDerivedStateFromProps({}, {});
blankState.title = this.props.intl.formatMessage(messages.not_found);
this.setState(blankState);
});
} }
} }
@@ -229,7 +188,7 @@ class MessagesView extends React.Component {
if (!nextProps.topic) { if (!nextProps.topic) {
// Default state: no topic. // Default state: no topic.
nextState = { nextState = {
latestMsgSeq: -1, messageCount: 0,
latestClearId: -1, latestClearId: -1,
onlineSubs: [], onlineSubs: [],
topic: null, topic: null,
@@ -300,14 +259,14 @@ class MessagesView extends React.Component {
}); });
} }
Object.assign(nextState, { Object.assign(nextState, {
latestMsgSeq: topic.maxMsgSeq(), messageCount: topic.messageCount(),
latestClearId: topic.maxClearId(), latestClearId: topic.maxClearId(),
channel: topic.isChannelType() channel: topic.isChannelType()
}); });
} else { } else {
// Invalid topic. // Invalid topic.
Object.assign(nextState, { Object.assign(nextState, {
latestMsgSeq: -1, messageCount: 0,
latestClearId: -1, latestClearId: -1,
onlineSubs: [], onlineSubs: [],
title: '', title: '',
@@ -358,6 +317,55 @@ class MessagesView extends React.Component {
return nextState; return nextState;
} }
subscribe(topic) {
if (!topic || topic.isSubscribed() || !this.props.ready) {
return;
}
// Is this a new topic?
const newTopic = (this.props.newTopicParams && this.props.newTopicParams._topicName == this.props.topic);
// 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();
if (this.state.isReader || newTopic) {
// Reading is either permitted or we don't know because it's a new topic. Ask for messages.
getQuery = getQuery.withLaterData(MESSAGES_PAGE);
if (this.state.isReader) {
getQuery = getQuery.withLaterDel();
}
// And show "loading" spinner.
this.setState({ fetchingMessages: true });
}
const setQuery = newTopic ? this.props.newTopicParams : undefined;
topic.subscribe(getQuery.build(), setQuery)
.then((ctrl) => {
if (ctrl.code == 303) {
// Redirect to another topic requested.
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', ctrl.params.topic));
return;
}
if (this.state.topic != ctrl.topic) {
this.setState({topic: ctrl.topic});
}
this.props.onNewTopicCreated(this.props.topic, ctrl.topic);
// If there are unsent messages, try sending them now.
topic.queuedMessages((pub) => {
if (!pub._sending && topic.isSubscribed()) {
this.sendMessage(pub);
}
});
})
.catch((err) => {
console.log("Failed subscription to", this.state.topic);
this.props.onError(err.message, 'err');
const blankState = MessagesView.getDerivedStateFromProps({}, {});
blankState.title = this.props.intl.formatMessage(messages.not_found);
this.setState(blankState);
});
}
leave(oldTopicName) { leave(oldTopicName) {
if (!oldTopicName || !this.props.tinode.isTopicCached(oldTopicName)) { if (!oldTopicName || !this.props.tinode.isTopicCached(oldTopicName)) {
return; return;
@@ -391,23 +399,19 @@ class MessagesView extends React.Component {
// Get older messages // Get older messages
handleScrollEvent(event) { handleScrollEvent(event) {
this.setState({scrollPosition: event.target.scrollHeight - event.target.scrollTop}); this.setState({scrollPosition: event.target.scrollHeight - event.target.scrollTop});
if (this.state.fetchingMessages) {
return;
}
if (event.target.scrollTop <= 0) { if (event.target.scrollTop <= 0) {
this.setState((prevState, props) => { const topic = this.props.tinode.getTopic(this.state.topic);
const newState = {}; if (topic && topic.isSubscribed() && topic.msgHasMoreMessages()) {
if (!prevState.fetchingMessages) { this.setState({fetchingMessages: true}, () => {
const topic = this.props.tinode.getTopic(this.state.topic); topic.getMessagesPage(MESSAGES_PAGE)
if (topic && topic.isSubscribed() && topic.msgHasMoreMessages()) { .catch((err) => this.props.onError(err.message, 'err'))
newState.fetchingMessages = true; .finally(() => this.setState({fetchingMessages: false}));
topic.getMessagesPage(MESSAGES_PAGE) });
.then(() => this.setState({fetchingMessages: false})) }
.catch((err) => {
this.setState({fetchingMessages: false});
this.props.onError(err.message, 'err');
});
}
}
return newState;
});
} }
} }
@@ -530,10 +534,11 @@ class MessagesView extends React.Component {
return; return;
} }
this.setState({latestMsgSeq: topic.maxMsgSeq()}); clearTimeout(this.keyPressTimer)
this.setState({messageCount: topic.messageCount(), typingIndicator: false});
// If the message is added to the end of the message list, // Scroll to the bottom if the message is added to the end of the message list.
// scroll to the bottom. // TODO: This should be replaced by showing a "scroll to bottom" button.
if (topic.isNewMessage(msg.seq)) { if (topic.isNewMessage(msg.seq)) {
this.setState({scrollPosition: 0}); this.setState({scrollPosition: 0});
} }
@@ -560,9 +565,8 @@ class MessagesView extends React.Component {
switch (info.what) { switch (info.what) {
case 'kp': { case 'kp': {
clearTimeout(this.keyPressTimer); clearTimeout(this.keyPressTimer);
var instance = this; this.keyPressTimer = setTimeout(() => {
this.keyPressTimer = setTimeout(function() { this.setState({typingIndicator: false});
instance.setState({typingIndicator: false});
}, KEYPRESS_DELAY + 1000); }, KEYPRESS_DELAY + 1000);
if (!this.state.typingIndicator) { if (!this.state.typingIndicator) {
this.setState({typingIndicator: true}); this.setState({typingIndicator: true});
@@ -592,7 +596,7 @@ class MessagesView extends React.Component {
handleFormResponse(action, text, data) { handleFormResponse(action, text, data) {
if (action == 'pub') { if (action == 'pub') {
this.props.sendMessage(Drafty.attachJSON(Drafty.parse(text), data)); this.sendMessage(Drafty.attachJSON(Drafty.parse(text), data));
} else if (action == 'url') { } else if (action == 'url') {
const url = new URL(data.ref); const url = new URL(data.ref);
const params = url.searchParams; const params = url.searchParams;
@@ -653,15 +657,19 @@ class MessagesView extends React.Component {
} }
} }
doSendMessage(msg, uploadCompletionPromise, uploader) { // sendMessage sends the message with an optional subscription to topic first.
let head = null; sendMessage(msg, uploadCompletionPromise, uploader) {
let head;
if (this.state.reply && this.state.reply.content) { if (this.state.reply && this.state.reply.content) {
head = {replyToSeq: this.state.reply.seq}; head = {replyToSeq: this.state.reply.seq};
let quote = this.state.reply.content; // Turn it into Drafty so we can make a quoted Drafty object later.
msg = Drafty.attachQuote(msg, quote); if (typeof msg == 'string') {
msg = Drafty.parse(msg);
}
msg = Drafty.attachQuote(msg, this.state.reply.content);
this.setState({reply: null});
} }
this.props.sendMessage(msg, uploadCompletionPromise, uploader, head); this.props.sendMessage(msg, uploadCompletionPromise, uploader, head);
this.setState({reply: null});
} }
// Send attachment as Drafty message: // Send attachment as Drafty message:
@@ -683,13 +691,12 @@ class MessagesView extends React.Component {
urlPromise: uploadCompletionPromise urlPromise: uploadCompletionPromise
}); });
// Pass data and the uploader to the TinodeWeb. // Pass data and the uploader to the TinodeWeb.
this.doSendMessage(msg, uploadCompletionPromise, uploader); this.sendMessage(msg, uploadCompletionPromise, uploader);
} else { } else {
// Small enough to send inband. // Small enough to send inband.
fileToBase64(file, fileToBase64(file,
(mime, bits, fname) => { (mime, bits, fname) => {
const msg = Drafty.attachFile(null, {mime: mime, data: bits, filename: fname}); this.sendMessage(Drafty.attachFile(null, {mime: mime, data: bits, filename: fname}));
this.doSendMessage(msg, undefined, undefined);
}, },
this.props.onError this.props.onError
); );
@@ -753,7 +760,7 @@ class MessagesView extends React.Component {
msg = Drafty.append(msg, Drafty.parse(caption)); msg = Drafty.append(msg, Drafty.parse(caption));
} }
// Pass data and the uploader to the TinodeWeb. // Pass data and the uploader to the TinodeWeb.
this.doSendMessage(msg, uploadCompletionPromise, uploader); this.sendMessage(msg, uploadCompletionPromise, uploader);
} }
)}, )},
// Failure // Failure
@@ -778,7 +785,7 @@ class MessagesView extends React.Component {
msg = Drafty.appendLineBreak(msg); msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.parse(caption)); msg = Drafty.append(msg, Drafty.parse(caption));
} }
this.doSendMessage(msg, undefined, undefined); this.sendMessage(msg);
}); });
} }
@@ -920,14 +927,6 @@ class MessagesView extends React.Component {
} }
} }
handleSendMessage(msg) {
if (this.state.reply && this.state.reply.content && typeof msg == 'string') {
// Turn it into Drafty so we can make a quoted Drafty object later.
msg = Drafty.parse(msg);
}
this.doSendMessage(msg, undefined, undefined);
}
handleCancelReply() { handleCancelReply() {
this.setState({reply: null}) this.setState({reply: null})
} }
@@ -1159,16 +1158,15 @@ class MessagesView extends React.Component {
<Invitation onAction={this.handleNewChatAcceptance} /> <Invitation onAction={this.handleNewChatAcceptance} />
: :
<SendMessage <SendMessage
tinode={this.props.tinode}
disabled={!this.state.isWriter} disabled={!this.state.isWriter}
onKeyPress={this.sendKeyPress} onKeyPress={this.sendKeyPress}
onSendMessage={this.sendMessage}
onAttachFile={this.handleAttachFile} onAttachFile={this.handleAttachFile}
onAttachImage={this.handleAttachImage} onAttachImage={this.handleAttachImage}
onError={this.props.onError} onError={this.props.onError}
tinode={this.props.tinode}
replyTo={this.state.reply} replyTo={this.state.reply}
onQuoteClick={this.handleQuoteClick} onQuoteClick={this.handleQuoteClick}
onSendMessage={this.handleSendMessage}
onFormatQuote={draftyFormatter} onFormatQuote={draftyFormatter}
onCancelReply={this.handleCancelReply} />} onCancelReply={this.handleCancelReply} />}
</> </>
+6 -4
View File
@@ -56,16 +56,17 @@ class NewTopicView extends React.Component {
this.setState({searchQuery: Tinode.isNullValue(query) ? null : query}); this.setState({searchQuery: Tinode.isNullValue(query) ? null : query});
} }
handleContactSelected(sel) { handleContactSelected(uid) {
if (this.state.tabSelected === 'p2p') { if (this.state.tabSelected == 'p2p') {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab')); HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(sel); this.props.onCreateTopic(uid);
} }
} }
handleNewGroupSubmit(name, description, dataUrl, priv, tags, isChannel) { handleNewGroupSubmit(name, description, dataUrl, priv, tags, isChannel) {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab')); HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(undefined, theCard(name, dataUrl, null, description), priv, tags, isChannel); this.props.onCreateTopic(undefined,
{public: theCard(name, dataUrl, null, description), private: priv, tags: tags}, isChannel);
} }
handleGroupByID(topicName) { handleGroupByID(topicName) {
@@ -112,6 +113,7 @@ class NewTopicView extends React.Component {
type="p2p" type="p2p"
onSearchContacts={this.handleSearchContacts} /> onSearchContacts={this.handleSearchContacts} />
<ContactList <ContactList
tinode={this.props.tinode}
contacts={this.props.searchResults} contacts={this.props.searchResults}
myUserId={this.props.myUserId} myUserId={this.props.myUserId}
emptyListMessage={no_contacts_placeholder} emptyListMessage={no_contacts_placeholder}
+15 -5
View File
@@ -917,10 +917,20 @@ class TinodeWeb extends React.Component {
} }
if (!topic.isSubscribed()) { if (!topic.isSubscribed()) {
// Topic is not subscribed yet. Subscribe.
if (!promise) { if (!promise) {
promise = Promise.resolve(); promise = Promise.resolve();
} }
promise = promise.then(() => { return topic.subscribe(); }); promise = promise
.then(() => topic.subscribe())
.then(() => {
// If there are unsent messages, try sending them now.
topic.queuedMessages((pub) => {
if (!pub._sending && topic.isSubscribed()) {
this.sendMessage(pub);
}
});
});
} }
if (promise) { if (promise) {
@@ -1184,7 +1194,7 @@ class TinodeWeb extends React.Component {
} }
// Request to start a topic, new or selected from search results, or "by ID". // Request to start a topic, new or selected from search results, or "by ID".
handleStartTopicRequest(topicName, pub, priv, tags, isChannel) { handleStartTopicRequest(topicName, newTopicParams, isChannel) {
// Check if topic is indeed new. If not, launch it. // Check if topic is indeed new. If not, launch it.
if (topicName && this.tinode.isTopicCached(topicName)) { if (topicName && this.tinode.isTopicCached(topicName)) {
this.handleTopicSelected(topicName); this.handleTopicSelected(topicName);
@@ -1199,14 +1209,14 @@ class TinodeWeb extends React.Component {
params.desc = {defacs: {auth: DEFAULT_P2P_ACCESS_MODE}}; params.desc = {defacs: {auth: DEFAULT_P2P_ACCESS_MODE}};
} else { } else {
topicName = topicName || this.tinode.newGroupTopicName(isChannel); topicName = topicName || this.tinode.newGroupTopicName(isChannel);
params.desc = {public: pub, private: {comment: priv}}; params.desc = {public: newTopicParams.public, private: {comment: newTopicParams.private}};
params.tags = tags; params.tags = newTopicParams.tags;
} }
params._topicName = topicName; params._topicName = topicName;
this.setState({newTopicParams: params}, () => {this.handleTopicSelected(topicName)}); this.setState({newTopicParams: params}, () => {this.handleTopicSelected(topicName)});
} }
// New topic was creted, here is the new topic name. // New topic was created, here is the new topic name.
handleNewTopicCreated(oldName, newName) { handleNewTopicCreated(oldName, newName) {
if (this.state.topicSelected == oldName && oldName != newName) { if (this.state.topicSelected == oldName && oldName != newName) {
// If the current URl contains the old topic name, replace it with new. // If the current URl contains the old topic name, replace it with new.
+3 -3
View File
@@ -354,7 +354,7 @@ class ContextMenu extends React.Component {
} }
if (!item) { if (!item) {
console.log("Invalid menu item ID", e.currentTarget.dataset.id); console.error("Invalid menu item ID", e.currentTarget.dataset.id);
} else { } else {
this.props.onAction( this.props.onAction(
item.id, item.id,
@@ -425,12 +425,12 @@ class ContextMenu extends React.Component {
replyToMessage(params, errorHandler) { replyToMessage(params, errorHandler) {
console.log("Reply to message", params); console.log("Reply to message", params);
params.pickReply({ seq: params.seq, content: params.content }) params.pickReply({ seq: params.seq, content: params.content });
} }
render() { render() {
const menu = [];
let count = 0; let count = 0;
let menu = [];
this.props.items.map((item) => { this.props.items.map((item) => {
if (typeof item == 'string') { if (typeof item == 'string') {
item = this.MenuItems[item]; item = this.MenuItems[item];
+3 -3
View File
@@ -35,7 +35,7 @@ export default class InPlaceEdit extends React.Component {
} }
handeTextChange(e) { handeTextChange(e) {
this.setState({value: e.target.value}); this.setState({value: e.target.value || ''});
} }
handleKeyDown(e) { handleKeyDown(e) {
@@ -59,13 +59,13 @@ export default class InPlaceEdit extends React.Component {
} }
handleEditingFinished(event) { handleEditingFinished(event) {
if (this.props.required && !event.target.checkValidity()) { const value = this.state.value.trim();
if (this.props.required && (!event.target.checkValidity() || !value)) {
// Empty input // Empty input
this.setState({value: this.props.value, active: false}); this.setState({value: this.props.value, active: false});
return; return;
} }
this.setState({active: false}); this.setState({active: false});
const value = this.state.value.trim();
if ((value || this.props.value) && (value !== this.props.value)) { if ((value || this.props.value) && (value !== this.props.value)) {
this.props.onFinished(value); this.props.onFinished(value);
} }
+4 -4
View File
@@ -34,7 +34,7 @@ export default class NewTopicGroup extends React.PureComponent {
} }
handleFieldEdit(name, e) { handleFieldEdit(name, e) {
this.setState({[name]: e.target.value}); this.setState({[name]: e.target.value || ''});
} }
handleImageChanged(img) { handleImageChanged(img) {
@@ -82,7 +82,7 @@ export default class NewTopicGroup extends React.PureComponent {
<FormattedMessage id="topic_name_editing_placeholder" defaultMessage="Freeform name of the group" <FormattedMessage id="topic_name_editing_placeholder" defaultMessage="Freeform name of the group"
description="Prompt for entering topic name">{ description="Prompt for entering topic name">{
(placeholder) => <input type="text" id="new-topic-fn" placeholder={placeholder} (placeholder) => <input type="text" id="new-topic-fn" placeholder={placeholder}
ref={this.fullName} value={this.state.fullName} onChange={this.handleFieldEdit.bind('fullName')} ref={this.fullName} value={this.state.fullName} onChange={this.handleFieldEdit.bind(this, 'fullName')}
autoFocus required tabIndex={0} /> autoFocus required tabIndex={0} />
}</FormattedMessage> }</FormattedMessage>
</div> </div>
@@ -94,7 +94,7 @@ export default class NewTopicGroup extends React.PureComponent {
<FormattedMessage id="private_editing_placeholder" defaultMessage="Visible to you only" <FormattedMessage id="private_editing_placeholder" defaultMessage="Visible to you only"
description="Placeholder for editing 'private'">{ description="Placeholder for editing 'private'">{
(placeholder) => <input type="text" id="new-topic-priv" placeholder={placeholder} (placeholder) => <input type="text" id="new-topic-priv" placeholder={placeholder}
value={this.state.private} onChange={this.handleFieldEdit.bind('private')} tabIndex={1} /> value={this.state.private} onChange={this.handleFieldEdit.bind(this, 'private')} tabIndex={1} />
}</FormattedMessage> }</FormattedMessage>
</div> </div>
<div className="group"> <div className="group">
@@ -105,7 +105,7 @@ export default class NewTopicGroup extends React.PureComponent {
<FormattedMessage id="description_editing_placeholder" defaultMessage="Description (optional)" <FormattedMessage id="description_editing_placeholder" defaultMessage="Description (optional)"
description="Placeholder for editing topic description">{ description="Placeholder for editing topic description">{
(placeholder) => <input type="text" id="new-topic-desc" placeholder={placeholder} (placeholder) => <input type="text" id="new-topic-desc" placeholder={placeholder}
value={this.state.description} onChange={this.handleFieldEdit.bind('description')} tabIndex={2} /> value={this.state.description} onChange={this.handleFieldEdit.bind(this, 'description')} tabIndex={2} />
}</FormattedMessage> }</FormattedMessage>
</div> </div>
</div> </div>
+15 -13
View File
@@ -196,11 +196,11 @@ export default class TopicDescEdit extends React.Component {
<div> <div>
<FormattedMessage id="full_name_prompt" defaultMessage="Full name, e.g. John Doe" <FormattedMessage id="full_name_prompt" defaultMessage="Full name, e.g. John Doe"
description="Input placeholder for person's full name">{ description="Input placeholder for person's full name">{
(full_name_placeholder) => <InPlaceEdit (full_name_placeholder) => <InPlaceEdit
placeholder={full_name_placeholder} placeholder={full_name_placeholder}
value={this.state.fullName} value={this.state.fullName}
required={true} required={true}
onFinished={this.handleFullNameUpdate} /> onFinished={this.handleFullNameUpdate} />
}</FormattedMessage> }</FormattedMessage>
</div> </div>
</div> </div>
@@ -212,12 +212,15 @@ export default class TopicDescEdit extends React.Component {
description="Label for editing topic name" /> description="Label for editing topic name" />
</label></div> </label></div>
<div> <div>
<InPlaceEdit <FormattedMessage id="topic_name_editing_placeholder" defaultMessage="Freeform name of the group"
placeholder={this.state.groupTopic ? "Group name" : <i>Unknown</i>} description="Prompt for entering topic name">{
readOnly={!editable} (group_name_placeholder) => <InPlaceEdit
value={this.state.fullName} placeholder={group_name_placeholder}
required={true} readOnly={!editable}
onFinished={this.handleFullNameUpdate} /> value={this.state.fullName}
required={true}
onFinished={this.handleFullNameUpdate} />
}</FormattedMessage>
</div> </div>
</div> </div>
<div className="group"> <div className="group">
@@ -228,8 +231,7 @@ export default class TopicDescEdit extends React.Component {
</label> </label>
</div> </div>
<div> <div>
<FormattedMessage id="private_editing_placeholder" <FormattedMessage id="private_editing_placeholder" defaultMessage="Visible to you only"
defaultMessage="Visible to you only"
description="Placeholder for editing 'private'">{ description="Placeholder for editing 'private'">{
(private_placeholder) => <InPlaceEdit (private_placeholder) => <InPlaceEdit
placeholder={private_placeholder} placeholder={private_placeholder}
+320 -347
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+525 -133
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long