support for pinning topics; truncate text with support for RTL languages
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label for send on enter settings",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label for send on enter settings",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label for send on enter settings",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "Pin",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "Unpin",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label for send on enter settings",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,6 +820,10 @@
|
||||
"defaultMessage": "phone",
|
||||
"description": "Dative case of 'phone', i.e. 'phone' in 'by phone'"
|
||||
},
|
||||
"pin_chat": {
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top."
|
||||
},
|
||||
"pin_message": {
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning message to the top."
|
||||
@@ -1020,6 +1024,10 @@
|
||||
"defaultMessage": "Unnamed",
|
||||
"description": "Title shown when the topic has no name"
|
||||
},
|
||||
"unpin_chat": {
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat."
|
||||
},
|
||||
"unpin_message": {
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the message."
|
||||
|
||||
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label for send on enter settings",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label adjusting text size",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label for send on enter settings",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label for send on enter settings",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label for send on enter settings",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label for send on enter settings",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label for send on enter settings",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label for send on enter settings",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,5 +1822,19 @@
|
||||
"description": "Label for send on enter settings",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"pin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Pin",
|
||||
"description": "Menu item [Pin] for pinning chat to the top.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"unpin_chat": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Unpin",
|
||||
"description": "Menu item [Unpin] for un-pinning the chat.",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,3 +108,18 @@ export function flagEmoji(countryCode) {
|
||||
.map(char => 0x1F1A5 + char.charCodeAt());
|
||||
return String.fromCodePoint(...codePoints);
|
||||
}
|
||||
|
||||
// Truncate a string to a specific length and add ellipsis if needed.
|
||||
// Support both LTR and RTL languages.
|
||||
export function truncateString(str, maxLength) {
|
||||
if (typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
if (str.length <= maxLength) {
|
||||
return str;
|
||||
}
|
||||
|
||||
// Detect RTL characters in the string and truncate accordingly.
|
||||
const isRTL = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(str);
|
||||
return isRTL ? '…' + str.slice(0, maxLength - 1) : str.slice(0, maxLength - 1) + '…';
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
bytesToHumanSize,
|
||||
shortenFileName,
|
||||
idToColorClass,
|
||||
flagEmoji
|
||||
flagEmoji,
|
||||
truncateString
|
||||
} from './strformat.js';
|
||||
|
||||
describe('shortDateFormat', () => {
|
||||
@@ -152,3 +153,37 @@ describe('flagEmoji', () => {
|
||||
expect(flagEmoji('jp')).toBe('🇯🇵');
|
||||
});
|
||||
});
|
||||
|
||||
describe('truncateString', () => {
|
||||
test('should not truncate if not needed', () => {
|
||||
expect(truncateString('short', 10)).toBe('short');
|
||||
});
|
||||
|
||||
test('should truncate and add ellipsis for long strings', () => {
|
||||
expect(truncateString('this is a long string', 10)).toBe('this is a…');
|
||||
});
|
||||
|
||||
test('should handle RTL strings (Arabic)', () => {
|
||||
expect(truncateString('هذا مثال لنص عربي طويل يحتاج.', 10)).toBe('…هذا مثال ');
|
||||
});
|
||||
|
||||
test('should handle RTL strings (Hebrew)', () => {
|
||||
expect(truncateString('זה דוגמה לטקסט עברי ארוך', 10)).toBe('…זה דוגמה ');
|
||||
});
|
||||
|
||||
test('should handle RTL strings (Persian/Farsi)', () => {
|
||||
expect(truncateString('این یک نمونه متن فارسی طولانی است', 10)).toBe('…این یک نم');
|
||||
});
|
||||
|
||||
test('should handle RTL strings (Urdu)', () => {
|
||||
expect(truncateString('یہ اردو زبان کا ایک طویل جملہ ہے', 10)).toBe('…یہ اردو ز');
|
||||
});
|
||||
|
||||
test('should handle RTL strings (Arabic with mixed punctuation)', () => {
|
||||
expect(truncateString('النص العربي الطويل! متحف الفن', 10)).toBe('…النص العر');
|
||||
});
|
||||
|
||||
test('should handle RTL strings (Hebrew with English mixed)', () => {
|
||||
expect(truncateString('טקסט עברי עם English מעורב', 10)).toBe('…טקסט עברי');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,7 +248,3 @@ export function cancelablePromise(promise) {
|
||||
};
|
||||
};
|
||||
|
||||
// Clips string to the specified length.
|
||||
export function clipStr(str, length) {
|
||||
return str && str.substring(0, length);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
// This is a generated file. Don't edit.
|
||||
|
||||
export const PACKAGE_VERSION = "0.25.0";
|
||||
export const PACKAGE_VERSION = "0.25.0-rc1";
|
||||
|
||||
@@ -5,13 +5,14 @@ import { parsePhoneNumber } from 'libphonenumber-js/mobile';
|
||||
|
||||
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||
import BadgeList from '../widgets/badge-list.jsx';
|
||||
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
|
||||
import CredentialEdit from '../widgets/credential-edit.jsx';
|
||||
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { truncateString } from '../lib/strformat.js';
|
||||
import { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
|
||||
|
||||
function _clip(str, length) {
|
||||
return str && str.substring(0, length);
|
||||
return truncateString(str, length);
|
||||
}
|
||||
|
||||
export default class AccountSettingsView extends React.Component {
|
||||
|
||||
@@ -27,8 +27,12 @@ export default class ContactsView extends React.Component {
|
||||
const contacts = [];
|
||||
let unreadThreads = 0;
|
||||
let archivedCount = 0;
|
||||
|
||||
const me = props.tinode.getMeTopic();
|
||||
props.chatList.forEach(c => {
|
||||
const blocked = c.acs && !c.acs.isJoiner();
|
||||
c.pinned = me.pinnedTopicRank(c.topic);
|
||||
|
||||
// Show only blocked contacts only when props.blocked == true.
|
||||
if (blocked && props.blocked) {
|
||||
contacts.push(c);
|
||||
@@ -50,7 +54,8 @@ export default class ContactsView extends React.Component {
|
||||
});
|
||||
|
||||
contacts.sort((a, b) => {
|
||||
return (b.touched || 0) - (a.touched || 0);
|
||||
const pin = b.pinned - a.pinned;
|
||||
return pin != 0 ? pin : (b.touched || 0) - (a.touched || 0);
|
||||
});
|
||||
|
||||
if (archivedCount > 0) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import VisiblePassword from '../widgets/visible-password.jsx';
|
||||
import LocalStorageUtil from '../lib/local-storage.js';
|
||||
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { theCard } from '../lib/utils.js';
|
||||
import { truncateString } from '../lib/strformat.js';
|
||||
|
||||
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE,
|
||||
MAX_TITLE_LENGTH } from '../config.js';
|
||||
@@ -91,7 +92,7 @@ export default class CreateAccountView extends React.PureComponent {
|
||||
this.props.onCreateAccount(
|
||||
this.state.login.trim(),
|
||||
this.state.password.trim(),
|
||||
theCard(this.state.fn.trim().substring(0, MAX_TITLE_LENGTH), this.state.uploadUrl),
|
||||
theCard(truncateString(this.state.fn.trim(), MAX_TITLE_LENGTH), this.state.uploadUrl),
|
||||
{
|
||||
'meth': this.state.meth,
|
||||
'val': this.state.meth == 'email' ?
|
||||
|
||||
@@ -20,7 +20,8 @@ import { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH,
|
||||
NO_ACCESS_MODE, TOAST_DURATION } from '../config.js';
|
||||
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { theCard, clipStr } from '../lib/utils.js';
|
||||
import { theCard } from '../lib/utils.js';
|
||||
import { truncateString } from '../lib/strformat.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
info: {
|
||||
@@ -231,13 +232,13 @@ class InfoView extends React.Component {
|
||||
|
||||
fullName: isSelf ?
|
||||
props.intl.formatMessage(messages.self_topic_name) :
|
||||
clipStr(topic.public && topic.public.fn, MAX_TITLE_LENGTH),
|
||||
truncateString(topic.public && topic.public.fn, MAX_TITLE_LENGTH),
|
||||
description: isSelf ?
|
||||
props.intl.formatMessage(messages.self_topic_comment) :
|
||||
clipStr(topic.public && topic.public.note, MAX_TOPIC_DESCRIPTION_LENGTH),
|
||||
truncateString(topic.public && topic.public.note, MAX_TOPIC_DESCRIPTION_LENGTH),
|
||||
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
|
||||
trustedBadges: badges,
|
||||
private: clipStr(topic.private && topic.private.comment, MAX_TITLE_LENGTH),
|
||||
private: truncateString(topic.private && topic.private.comment, MAX_TITLE_LENGTH),
|
||||
archived: topic.isArchived(),
|
||||
address: topic.name,
|
||||
groupTopic: topic.isGroupType(),
|
||||
|
||||
@@ -963,7 +963,8 @@ class TinodeWeb extends React.Component {
|
||||
newState.ready = true;
|
||||
}
|
||||
|
||||
this.tinode.getMeTopic().contacts(c => {
|
||||
const me = this.tinode.getMeTopic();
|
||||
me.contacts(c => {
|
||||
if (!c.topic && !c.user) {
|
||||
// Contacts expect c.topic to be set.
|
||||
c.topic = c.name;
|
||||
@@ -978,7 +979,14 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
const past = new Date(0);
|
||||
newState.chatList.sort((a, b) => {
|
||||
return (a.touched || past).getTime() - (b.touched || past).getTime();
|
||||
// Pinned topics take precedence.
|
||||
const pins = me.pinnedTopicRank(b.topic) - me.pinnedTopicRank(a.topic);
|
||||
// Then sort by last touched time.
|
||||
return pins != 0 ? pins : (b.touched || past).getTime() - (a.touched || past).getTime();
|
||||
});
|
||||
|
||||
newState.chatList.forEach(c => {
|
||||
console.log("sorted:", c.topic, me.pinnedTopicRank(c.topic));
|
||||
});
|
||||
|
||||
// Merge search results and chat list.
|
||||
@@ -1643,11 +1651,13 @@ class TinodeWeb extends React.Component {
|
||||
];
|
||||
}
|
||||
|
||||
const me = this.tinode.getMeTopic();
|
||||
let muted = false, blocked = false, self_blocked = false, subscribed = false, deleter = false,
|
||||
archived = false, webrtc = false, writer = false, p2p = false, self = false;
|
||||
archived = false, pinned = false, webrtc = false, writer = false, p2p = false, self = false;
|
||||
if (topic) {
|
||||
subscribed = topic.isSubscribed();
|
||||
archived = topic.isArchived();
|
||||
pinned = me.pinnedTopicRank(topicName) > 0;
|
||||
|
||||
const acs = topic.getAccessMode();
|
||||
if (acs) {
|
||||
@@ -1680,6 +1690,7 @@ class TinodeWeb extends React.Component {
|
||||
subscribed && deleter && !self ? 'messages_clear_hard' : null,
|
||||
self ? null : (muted ? (blocked ? null : 'topic_unmute') : 'topic_mute'),
|
||||
self ? null : (self_blocked ? 'topic_unblock' : 'topic_block'),
|
||||
subscribed ? (pinned ? 'topic_unpin' : 'topic_pin') : null,
|
||||
archived ? 'topic_restore' : 'topic_archive',
|
||||
'topic_delete'
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MAX_TITLE_LENGTH, MAX_PEER_TITLE_LENGTH } from '../config.js';
|
||||
import { CALL_STATE_INCOMING_RECEIVED } from '../constants.js';
|
||||
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { clipStr } from '../lib/utils.js'
|
||||
import { truncateString } from '../lib/strformat.js';
|
||||
|
||||
const RING_SOUND = new Audio('audio/call-in.m4a');
|
||||
|
||||
@@ -88,7 +88,7 @@ export default class CallIncoming extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({
|
||||
fullName: clipStr(topic.public ? topic.public.fn : undefined, MAX_TITLE_LENGTH),
|
||||
fullName: truncateString(topic.public ? topic.public.fn : undefined, MAX_TITLE_LENGTH),
|
||||
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
|
||||
trustedBadges: badges,
|
||||
});
|
||||
@@ -127,7 +127,7 @@ export default class CallIncoming extends React.Component {
|
||||
topic={this.props.topic}
|
||||
title={this.state.fullName} />
|
||||
</div>
|
||||
<div className="caller-name">{clipStr(this.state.fullName, MAX_PEER_TITLE_LENGTH)}
|
||||
<div className="caller-name">{truncateString(this.state.fullName, MAX_PEER_TITLE_LENGTH)}
|
||||
<BadgeList short={true} trustedBadges={this.state.trustedBadges} /></div>
|
||||
</div>
|
||||
<div className="controls">
|
||||
|
||||
@@ -6,8 +6,7 @@ import LetterTile from './letter-tile.jsx';
|
||||
|
||||
import { MAX_PEER_TITLE_LENGTH } from '../config.js';
|
||||
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
|
||||
|
||||
import { clipStr } from '../lib/utils.js'
|
||||
import { truncateString } from '../lib/strformat.js';
|
||||
|
||||
const RING_SOUND = new Audio('audio/call-out.m4a');
|
||||
RING_SOUND.loop = true;
|
||||
@@ -584,7 +583,7 @@ class CallPanel extends React.PureComponent {
|
||||
const disabled = !this.state.pc || !this.state.dataChannel || !(audioTracks && audioTracks[0]);
|
||||
const audioIcon = audioTracks && audioTracks[0] && audioTracks[0].enabled ? 'mic' : 'mic_off';
|
||||
const videoIcon = videoTracks && videoTracks[0] && videoTracks[0].enabled && videoTracks[0].readyState == 'live' ? 'videocam' : 'videocam_off';
|
||||
const peerTitle = clipStr(this.props.title, MAX_PEER_TITLE_LENGTH);
|
||||
const peerTitle = truncateString(this.props.title, MAX_PEER_TITLE_LENGTH);
|
||||
const pulseAnimation = this.state.waitingForPeer ? ' pulse' : '';
|
||||
|
||||
let remoteActive = false;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Drafty } from 'tinode-sdk';
|
||||
|
||||
import { previewFormatter } from '../lib/formatters.js';
|
||||
import { deliveryMarker } from '../lib/utils.js';
|
||||
import { truncateString } from '../lib/strformat.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
self_topic_name: {
|
||||
@@ -65,8 +66,8 @@ class Contact extends React.Component {
|
||||
description="Title shown when the topic has no name" /></i>;
|
||||
}
|
||||
} else if (title.length > 30) {
|
||||
// FIXME: this is probably wrong for RTL languages.
|
||||
title = title.substring(0, 28) + '…';
|
||||
// Have to support RTL languages too.
|
||||
title = truncateString(title, 30);
|
||||
}
|
||||
const online = this.props.now ? 'online' : 'offline';
|
||||
const avatar = this.props.avatar ? this.props.avatar : true;
|
||||
@@ -115,8 +116,10 @@ class Contact extends React.Component {
|
||||
|
||||
const comment = preview || this.props.comment ||
|
||||
(self ? this.props.intl.formatMessage(messages.self_topic_comment) : '\u00A0');
|
||||
const bgColor = this.props.showCheckmark ? null :
|
||||
(this.props.selected ? 'selected' : (this.props.pinned ? 'pinned' : null));
|
||||
return (
|
||||
<li className={!this.props.showCheckmark && this.props.selected ? 'selected' : null} onClick={this.handleClick}>
|
||||
<li className={bgColor} onClick={this.handleClick}>
|
||||
<div className="avatar-box">
|
||||
<LetterTile
|
||||
tinode={this.props.tinode}
|
||||
|
||||
@@ -126,6 +126,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Unpin',
|
||||
description: 'Menu item [Unpin] for un-pinning the message.'
|
||||
},
|
||||
pin_chat: {
|
||||
id: 'pin_chat',
|
||||
defaultMessage: 'Pin',
|
||||
description: 'Menu item [Pin] for pinning chat to the top.'
|
||||
},
|
||||
unpin_chat: {
|
||||
id: 'unpin_chat',
|
||||
defaultMessage: 'Unpin',
|
||||
description: 'Menu item [Unpin] for un-pinning the chat.'
|
||||
},
|
||||
});
|
||||
|
||||
class ContextMenu extends React.Component {
|
||||
@@ -326,6 +336,38 @@ class ContextMenu extends React.Component {
|
||||
});
|
||||
}
|
||||
},
|
||||
'topic_pin': {
|
||||
id: 'topic_pin',
|
||||
title: formatMessage(messages.pin_chat),
|
||||
handler: (params, errorHandler) => {
|
||||
const me = this.props.tinode.getMeTopic();
|
||||
if (!me) {
|
||||
console.warn("'me' topic not found");
|
||||
return;
|
||||
}
|
||||
return me.pinTopic(params.topicName, true).catch(err => {
|
||||
if (errorHandler) {
|
||||
errorHandler(err.message, 'err');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
'topic_unpin': {
|
||||
id: 'topic_unpin',
|
||||
title: formatMessage(messages.unpin_chat),
|
||||
handler: (params, errorHandler) => {
|
||||
const me = this.props.tinode.getMeTopic();
|
||||
if (!me) {
|
||||
console.warn("'me' topic not found");
|
||||
return;
|
||||
}
|
||||
return me.pinTopic(params.topicName, false).catch(err => {
|
||||
if (errorHandler) {
|
||||
errorHandler(err.message, 'err');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
// menu_item_edit_permissions is defined elsewhere.
|
||||
'permissions': {
|
||||
id: 'permissions',
|
||||
|
||||
@@ -9,6 +9,7 @@ import TagManager from './tag-manager.jsx';
|
||||
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE, MAX_TITLE_LENGTH,
|
||||
MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
|
||||
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { truncateString } from '../lib/strformat.js';
|
||||
|
||||
export default class NewTopicGroup extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@@ -100,9 +101,9 @@ export default class NewTopicGroup extends React.PureComponent {
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const fn = this.state.fullName.trim().substring(0, MAX_TITLE_LENGTH);
|
||||
const comment = this.state.private.trim().substring(0, MAX_TITLE_LENGTH);
|
||||
const description = this.state.description.trim().substring(0, MAX_TOPIC_DESCRIPTION_LENGTH);
|
||||
const fn = truncateString(this.state.fullName.trim(), MAX_TITLE_LENGTH);
|
||||
const comment = truncateString(this.state.private.trim(), MAX_TITLE_LENGTH);
|
||||
const description = truncateString(this.state.description.trim(), MAX_TOPIC_DESCRIPTION_LENGTH);
|
||||
if (fn) {
|
||||
this.props.onSubmit(fn, description, this.state.imageUrl, comment, this.state.tags, this.state.isChannel);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import TagManager from './tag-manager.jsx';
|
||||
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE, MAX_TITLE_LENGTH,
|
||||
MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
|
||||
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { truncateString } from '../lib/strformat.js';
|
||||
import { arrayEqual, theCard } from '../lib/utils.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -98,7 +99,7 @@ class TopicDescEdit extends React.Component {
|
||||
}
|
||||
|
||||
handleFullNameUpdate(fn) {
|
||||
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
|
||||
fn = truncateString(fn.trim(), MAX_TITLE_LENGTH);
|
||||
if (fn && this.state.fullName !== fn) {
|
||||
this.setState({fullName: fn});
|
||||
this.props.onUpdateTopicDesc(this.props.topic, theCard(fn, null));
|
||||
|
||||
Reference in New Issue
Block a user