fixing alignment issues with long user and topic names

This commit is contained in:
or-else
2021-08-04 21:46:59 -07:00
parent 47304c7baf
commit dafc3161c5
12 changed files with 176 additions and 92 deletions
+2
View File
@@ -84,6 +84,8 @@ export const MAX_ONLINE_IN_TOPIC = 4;
// Maximum length of user name, topic title, and private comment.
export const MAX_TITLE_LENGTH = 60;
// Maximum length of topic description.
export const MAX_TOPIC_DESCRIPTION_LENGTH = 360;
// Length of message previews in chat list, in characters.
export const MESSAGE_PREVIEW_LENGTH = 80;
+1 -1
View File
@@ -8,7 +8,7 @@ export const MIME_EXTENSIONS = ['jpg', 'gif', 'png',
// Get an URL from a theCard photo: either create a data: URL or return reference URL.
export function makeImageUrl(photo) {
if (photo) {
if (typeof photo == 'object') {
if (photo.data && photo.type) {
const mime = photo.type.startsWith('image/') ? photo.type : ('image/' + photo.type);
return 'data:' + mime + ';base64,' + photo.data;
+7 -1
View File
@@ -20,9 +20,10 @@ export function updateFavicon(count) {
}
// Create theCard which represents user's or topic's "public" info.
export function theCard(fn, imageUrl, imageMimeType) {
export function theCard(fn, imageUrl, imageMimeType, note) {
let card = null;
fn = fn && fn.trim();
note = note && note.trim();
if (fn) {
card = {
@@ -30,6 +31,11 @@ export function theCard(fn, imageUrl, imageMimeType) {
};
}
if (typeof note == 'string') {
card = card || {};
card.note = note ? note : Tinode.DEL_CHAR;
}
if (imageUrl) {
card = card || {};
let mimeType = imageMimeType;
+1 -1
View File
@@ -31,7 +31,7 @@ export default class AccountSettingsView extends React.Component {
<div>
<label className="small"><FormattedMessage id="label_user_id" defaultMessage="ID:"
description="Label for user address (ID)" /></label>&nbsp;
<tt>{this.props.myUserId}</tt>
<tt style={{whiteSpace: 'nowrap'}}>{this.props.myUserId}</tt>
</div>
<BadgeList trustedBadges={this.props.trustedBadges} />
</div>
+10 -7
View File
@@ -65,7 +65,7 @@ export default class InPlaceEdit extends React.Component {
return;
}
this.setState({active: false});
let value = this.state.value.trim();
const value = this.state.value.trim();
if ((value || this.props.value) && (value !== this.props.value)) {
this.props.onFinished(value);
}
@@ -86,17 +86,18 @@ export default class InPlaceEdit extends React.Component {
spanText = this.props.placeholder;
spanClass += ' placeholder';
}
if (spanText.length > 20) {
// FIXME: this is wrong for RTL languages.
spanText = spanText.substring(0, 19) + '...';
if (!this.props.multiline || this.props.multiline == 1) {
spanClass += ' short';
}
return (<span className={spanClass} onClick={this.handleStartEditing}>
<span className="content">{spanText}</span>
<span>{spanText}</span>
</span>);
}
let element;
let attr = {};
const attr = {};
let value = null;
if (this.props.type == 'password') {
element = VisiblePassword;
attr.onFinished = this.handlePasswordFinished;
@@ -105,9 +106,11 @@ export default class InPlaceEdit extends React.Component {
element = 'textarea';
attr.rows = this.props.multiline;
attr.className = 'inplace-edit';
value = this.state.value;
} else {
element = 'input';
attr.type = this.props.type || 'text';
attr.value = this.state.value;
}
attr.onChange = this.handeTextChange;
attr.onKeyDown = this.handleKeyDown;
@@ -119,6 +122,6 @@ export default class InPlaceEdit extends React.Component {
attr.autoFocus = true;
attr.ref = this.selfRef;
return React.createElement(element, attr, null);
return React.createElement(element, attr, value);
}
};
+74 -54
View File
@@ -11,7 +11,7 @@ import InPlaceEdit from './in-place-edit.jsx';
import TagManager from './tag-manager.jsx';
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE,
MAX_INBAND_ATTACHMENT_SIZE, MAX_TITLE_LENGTH } from '../config.js';
MAX_INBAND_ATTACHMENT_SIZE, MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, theCard } from '../lib/utils.js';
@@ -25,6 +25,8 @@ export default class TopicDescEdit extends React.Component {
isMe: Tinode.isMeTopicName(this.props.topic),
owner: acs && acs.isOwner(),
fullName: topic.public ? topic.public.fn : undefined,
private: topic.private ? topic.private.comment : null,
description: topic.public ? topic.public.note : undefined,
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
tags: topic.tags(),
newAvatar: null,
@@ -36,6 +38,8 @@ export default class TopicDescEdit extends React.Component {
this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
this.handleImageUpdated = this.handleImageUpdated.bind(this);
this.handleAvatarCropped = this.handleAvatarCropped.bind(this);
this.handlePrivateUpdate = this.handlePrivateUpdate.bind(this);
this.handleDescriptionUpdate = this.handleDescriptionUpdate.bind(this);
this.uploadAvatar = this.uploadAvatar.bind(this);
this.handleAvatarCropCancel = this.handleAvatarCropCancel.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
@@ -57,12 +61,28 @@ export default class TopicDescEdit extends React.Component {
handleFullNameUpdate(fn) {
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
if (fn) {
if (fn && this.state.fullName !== fn) {
this.setState({fullName: fn});
this.props.onUpdateTopicDesc(this.props.topic, theCard(fn, null));
}
}
handlePrivateUpdate(comment) {
comment = comment.trim().substring(0, MAX_TITLE_LENGTH);
if (this.state.private !== comment) {
this.setState({private: comment});
this.props.onTopicDescUpdate(this.props.topic, null, comment || Tinode.DEL_CHAR);
}
}
handleDescriptionUpdate(desc) {
desc = desc.trim().substring(0, MAX_TOPIC_DESCRIPTION_LENGTH);
if (desc) {
this.setState({description: desc});
this.props.onUpdateTopicDesc(this.props.topic, theCard(null, null, null, desc));
}
}
// AvatarUpload widget has changed the avatar: received a new file or deleted the avatar.
handleImageUpdated(mime, img) {
// This will show the AvatarCropView is img is not null.
@@ -153,59 +173,59 @@ export default class TopicDescEdit extends React.Component {
return (
<>
<div className="panel-form-row">
<div className="panel-form-column">
{this.state.isMe ? <>
<label className="small">
<FormattedMessage id="label_your_name" defaultMessage="Your name"
description="Label for full name editing" />
</label>
<div><FormattedMessage id="full_name_prompt" defaultMessage="Full name, e.g. John Doe"
description="Input placeholder for person's full name">{
(full_name_placeholder) => <InPlaceEdit
placeholder={full_name_placeholder}
value={this.state.fullName}
required={true}
onFinished={this.handleFullNameUpdate} />
}</FormattedMessage></div>
</> : <>
<div><label className="small">
<FormattedMessage id="label_topic_name" defaultMessage="Name"
description="Label for editing topic name" />
</label></div>
<div><InPlaceEdit
placeholder={this.state.groupTopic ? "Group name" : <i>Unknown</i>}
readOnly={!this.state.owner}
value={this.state.fullName}
required={true}
onFinished={this.handleFullNameUpdate} /></div>
<div><label className="small">
<FormattedMessage id="label_private" defaultMessage="Private comment"
description="Label for editing 'private'" />
</label></div>
<div>
<FormattedMessage id="private_editing_placeholder"
defaultMessage="Visible to you only"
description="Placeholder for editing 'private'">{
(private_placeholder) => <InPlaceEdit
placeholder={private_placeholder}
value={this.state.private}
onFinished={this.handlePrivateUpdate} />
}</FormattedMessage>
</div>
</>
}
</div>
<AvatarUpload
tinode={this.props.tinode}
avatar={this.state.avatar}
readOnly={!editable}
uid={this.props.topic}
title={this.state.fullName}
onImageUpdated={this.handleImageUpdated}
onError={this.props.onError} />
</div>
<div className="panel-form-column">
<div className="panel-form-row">
<div className="panel-form-column">
{this.state.isMe ? <>
<label className="small">
<FormattedMessage id="label_your_name" defaultMessage="Your name"
description="Label for full name editing" />
</label>
<div><FormattedMessage id="full_name_prompt" defaultMessage="Full name, e.g. John Doe"
description="Input placeholder for person's full name">{
(full_name_placeholder) => <InPlaceEdit
placeholder={full_name_placeholder}
value={this.state.fullName}
required={true}
onFinished={this.handleFullNameUpdate} />
}</FormattedMessage></div>
</> : <>
<div><label className="small">
<FormattedMessage id="label_topic_name" defaultMessage="Name"
description="Label for editing topic name" />
</label></div>
<div><InPlaceEdit
placeholder={this.state.groupTopic ? "Group name" : <i>Unknown</i>}
readOnly={!this.state.owner}
value={this.state.fullName}
required={true}
onFinished={this.handleFullNameUpdate} /></div>
<div><label className="small">
<FormattedMessage id="label_private" defaultMessage="Private comment"
description="Label for editing 'private'" />
</label></div>
<div>
<FormattedMessage id="private_editing_placeholder"
defaultMessage="Visible to you only"
description="Placeholder for editing 'private'">{
(private_placeholder) => <InPlaceEdit
placeholder={private_placeholder}
value={this.state.private}
onFinished={this.handlePrivateUpdate} />
}</FormattedMessage>
</div>
</>
}
</div>
<AvatarUpload
tinode={this.props.tinode}
avatar={this.state.avatar}
readOnly={!editable}
uid={this.props.topic}
title={this.state.fullName}
onImageUpdated={this.handleImageUpdated}
onError={this.props.onError} />
</div>
<div><label className="small">
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for editing topic description" />