Compare commits

...
22 Commits
Author SHA1 Message Date
or-else 6f47cc50b0 resolving merge conflict 2021-01-27 09:42:25 -08:00
or-else 1172adefb4 fix formatjs setup, #62 2021-01-26 13:58:59 -08:00
or-else 4f38a2d8b1 fixing formatjs build 2021-01-25 17:29:43 -08:00
or-else d75448c548 rebuilt with sdk fixes 2021-01-24 19:03:51 -08:00
or-else 9c69b7129c tinode setup is async now 2021-01-24 17:41:41 -08:00
or-else 4ea82f85ba rebuilt with refactored sdk 2021-01-23 19:08:24 -08:00
or-else 3132294cce persistent cached hooked up on cold start; warm start still crashes 2021-01-22 16:55:01 -08:00
or-else 6f278f5bb5 indexeddb cache kind of works but buggy and incomplete 2021-01-21 20:17:24 -08:00
or-else 92e3f15c01 adding indexedDB persistent cache 2021-01-20 17:45:01 -08:00
or-else 1f5a76e481 received marker in message previews 2021-01-19 18:50:18 -08:00
or-else 4445a90437 fix UI jerk on updating message preview 2021-01-17 18:08:16 -08:00
or-else 0afa8bcdcd more work on supporting message previews 2021-01-17 17:26:57 -08:00
or-else 55d7e0d5cb don't show time in dates long in the past 2021-01-14 13:59:20 -08:00
or-else 4b07c3b5c9 drafty previews 2021-01-13 15:17:56 -08:00
or-else a319e2b0bc CDN changed react-int paths 2021-01-11 19:39:24 -08:00
or-else 924459f84b update build due to changes in webpack and react-intl 2021-01-11 16:21:26 -08:00
or-else 396b23ee88 dependencies updated 2020-12-27 20:55:46 -08:00
or-else 775bc954e3 bump version and rebuild 2020-12-27 10:38:53 -08:00
or-else 6fabb6c111 fix missing file in npm package, remove screenshots from package 2020-12-20 13:11:48 -08:00
or-else e328e6a4c2 dependencies updated 2020-12-20 13:01:55 -08:00
or-else 443245280f rebuild with sdk changes 2020-12-20 12:14:42 -08:00
or-else a248151243 rebuilt with a fix in the sdk 2020-12-18 12:11:07 -08:00
26 changed files with 6034 additions and 6066 deletions
+7 -3
View File
@@ -2,8 +2,12 @@
"presets": ["@babel/preset-react", "@babel/preset-env"],
"comments": false,
"plugins": [
["react-intl", {
"messagesDir": "./src/i18n/"
}]
[
"formatjs",
{
"idInterpolationPattern": "[sha512:contenthash:base64:6]",
"ast": true
}
]
]
}
+35 -9
View File
@@ -42,7 +42,7 @@ a.gray:focus {
color: #999;
}
a.flat-button {
.flat-button {
display: flex;
align-items: center;
margin: 0.15rem 0.35rem;
@@ -55,11 +55,21 @@ a.flat-button {
white-space: nowrap;
text-transform: uppercase;
}
a.flat-button:hover,
a.flat-button:focus {
.flat-button:hover,
.flat-button:focus {
text-decoration: none;
}
.flat-button .faux {
height: 1.5em;
line-height: 1.5em;
text-transform: none;
padding-left: .5rem;
padding-right: .5rem;
border: 1px solid gray;
border-radius: 3px;
}
form {
font-size: 1.1rem;
padding: 0.5rem 0.75rem;
@@ -541,6 +551,11 @@ div.alert .content {
margin-left: 1rem;
}
/* Extra spece on the right */
.space-right {
margin-right: 0.25rem;
}
/* Container with whitish background and rounded corners */
.rounded-container {
position: relative;
@@ -825,7 +840,7 @@ i.search {
display: flex;
position: relative;
padding: 0.35rem 0.25rem 0.5rem 0.35rem;
min-height: 3.5rem;
min-height: 3.75rem;
flex-grow: 0;
flex-shrink: 0;
/* partial bottom border */
@@ -1075,11 +1090,11 @@ i.search {
}
.avatar-upload .inputfile.hidden {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
}
.avatar-upload .inputfile + label.round {
@@ -2066,3 +2081,14 @@ ul.tabbar li.active > a:after {
background-color: #1e88e5;
border-radius: 0.08rem;
}
/* Highlighted text */
.highlight {
color: teal;
font-weight: bolder;
}
.highlight .preview {
color: #5f9ea0;
font-weight: inherit;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -29,13 +29,13 @@
<!-- Styles for local elements -->
<link rel="stylesheet" href="css/base.css" />
<!-- ReactJS -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@16/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@16/umd/react-dom.development.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.development.js"></script>
<!-- React-Intl for translations -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@4/dist/react-intl.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5/react-intl.umd.js"></script>
<!-- Firebase for push notifications -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-messaging.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7/firebase-messaging.js"></script>
</head>
<body>
+5 -5
View File
@@ -22,11 +22,11 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="css/base.min.css" />
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@4.7.6/dist/react-intl.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-messaging.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17.0.1/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17.0.1/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5.10.14/react-intl.umd.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.24.0/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.24.0/firebase-messaging.js"></script>
</head>
<body>
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
+2050 -3578
View File
File diff suppressed because it is too large Load Diff
+17 -29
View File
@@ -1,7 +1,7 @@
{
"name": "tinode-webapp",
"description": "Tinode messenger for the web",
"version": "0.16.9-rc1",
"version": "0.17.0",
"repository": {
"type": "git",
"url": "https://github.com/tinode/example-react-js.git"
@@ -31,25 +31,14 @@
"audio/msg.mp3",
"css/base.css",
"css/base.min.css",
"img/bg_messages.png",
"img/logo.svg",
"img/logo32x32.png",
"img/logo32x32a.png",
"img/logo96.png",
"img/logo192.png",
"img/badge96.png",
"img/broken_image.png",
"img/og-logo.jpeg",
"img/*.png",
"img/*.svg",
"img/*.jpeg",
"index-dev.html",
"index.html",
"manifest.json",
"service-worker.js",
"firebase-init.js",
"web-desktop-2.png",
"web-mob-chat-1.png",
"web-mob-contacts-1.png",
"web-mob-info-1.png",
"web-mob-new-chat-1.png"
"firebase-init.js"
],
"keywords": [
"chat",
@@ -69,24 +58,23 @@
"license": "Apache-2.0",
"dependencies": {
"firebase": "^7.24.0",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-intl": "^4.7.6",
"tinode-sdk": "^0.16.9-rc2"
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-intl": "^5.10.14",
"tinode-sdk": "^0.16.10"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"babel-loader": "^8.2.2",
"babel-plugin-react-intl": "^7.9.4",
"browserslist": "^4.16.0",
"copy-webpack-plugin": "^6.4.0",
"babel-plugin-formatjs": "^9.0.2",
"browserslist": "^4.16.1",
"copy-webpack-plugin": "^6.4.1",
"cssnano": "^4.1.10",
"postcss": "^7.0.35",
"postcss-cli": "^7.1.2",
"terser-webpack-plugin": "^3.1.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
"postcss": "^8.2.4",
"postcss-cli": "^8.3.1",
"webpack": "^5.13.0",
"webpack-cli": "^4.3.1"
}
}
+3
View File
@@ -81,6 +81,9 @@ export const MAX_ONLINE_IN_TOPIC = 4;
// Maximum length of user name, topic title, and private comment.
export const MAX_TITLE_LENGTH = 60;
// Length of message previews in chat list, in characters.
export const MESSAGE_PREVIEW_LENGTH = 80;
// Link for "Contact Us".
export const LINK_CONTACT_US = 'email:info@tinode.co';
+8 -6
View File
@@ -3,15 +3,17 @@ export function shortDateFormat(then, locale) {
locale = locale || window.navigator.userLanguage || window.navigator.language;
const now = new Date();
if (then.getFullYear() == now.getFullYear()) {
// Same year.
if (then.getMonth() == now.getMonth() && then.getDate() == now.getDate()) {
return then.toLocaleTimeString(locale, {hour12: false, hour: '2-digit', minute: '2-digit'});
} else {
return then.toLocaleDateString(locale,
{hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
// Same month and day, show time only.
return then.toLocaleTimeString(locale, {hour12: false, hour: '2-digit', minute: '2-digit'});
}
// Different month and/or day, show month day, time.
return then.toLocaleDateString(locale,
{hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
}
return then.toLocaleDateString(locale,
{hour12: false, year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
// Different year: just show the date.
return then.toLocaleDateString(locale, {year: 'numeric', month: 'short', day: 'numeric'});
}
// Convert seconds to minutes:seconds, i.e. 156 sec -> 2:36.
+19
View File
@@ -1,5 +1,7 @@
// Odds and ends
import Tinode from 'tinode-sdk';
// Make shortcut icon appear with a green dot + show unread count in title.
export function updateFavicon(count) {
const oldIcon = document.getElementById('shortcut-icon');
@@ -144,3 +146,20 @@ export function sanitizeImageUrl(url) {
return null;
}
// Given message's received status, return name and color of a delivery indicator icon.
export function deliveryMarker(received) {
switch (received) {
case Tinode.MESSAGE_STATUS_SENDING:
return { name: 'access_time' }; // watch face
case Tinode.MESSAGE_STATUS_FAILED:
return { name: 'warning', color: 'amber' }; // yellow icon /!\
case Tinode.MESSAGE_STATUS_SENT:
return { name: 'done' }; // checkmark
case Tinode.MESSAGE_STATUS_RECEIVED:
return { name: 'done_all' }; // double checkmark
case Tinode.MESSAGE_STATUS_READ:
return { name: 'done_all', color: 'blue' }; // blue double checkmark
}
return null;
}
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.16.9-rc1";
export const PACKAGE_VERSION = "0.17.0";
+2 -2
View File
@@ -230,8 +230,8 @@ class InfoView extends React.Component {
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
private: topic.private ? topic.private.comment : null,
address: topic.name,
groupTopic: (topic.getType() == 'grp'),
channel: topic.isChannel(),
groupTopic: topic.isGroupType(),
channel: topic.isChannelType(),
showMemberPanel: false,
access: acs ? acs.getMode() : undefined,
modeGiven: acs ? acs.getGiven() : undefined,
+5 -9
View File
@@ -159,11 +159,7 @@ class MessagesView extends React.Component {
// Don't request the tags. They are useless unless the user
// is the owner and is editing the topic.
let getQuery = topic.startMetaQuery().withLaterDesc();
if (this.state.isSharer || (newTopic && !topic.isChannel())) {
// Request subscriptions only if one of S,O,A is given or it's a new non-channel topic.
getQuery = getQuery.withLaterSub();
}
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);
@@ -280,7 +276,7 @@ class MessagesView extends React.Component {
});
}
Object.assign(nextState, {
channel: topic.isChannel()
channel: topic.isChannelType()
});
} else {
// Invalid topic.
@@ -601,7 +597,7 @@ class MessagesView extends React.Component {
const menuItems = messageSpecificMenuItems || [];
const topic = this.props.tinode.getTopic(params.topicName);
if (topic) {
if (!topic.isChannel()) {
if (!topic.isChannelType()) {
menuItems.push('message_delete');
}
const acs = topic.getAccessMode();
@@ -821,8 +817,8 @@ class MessagesView extends React.Component {
);
} else {
const topic = this.props.tinode.getTopic(this.state.topic);
const isChannel = topic.isChannel();
const groupTopic = topic.getType() == 'grp' && !isChannel;
const isChannel = topic.isChannelType();
const groupTopic = topic.isGroupType() && !isChannel;
let messageNodes = [];
let previousFrom = null;
let chatBoxClass = null;
+70 -61
View File
@@ -212,66 +212,69 @@ class TinodeWeb extends React.Component {
viewportHeight: document.documentElement.clientHeight
});
const keepLoggedIn = LocalStorageUtil.getObject('keep-logged-in');
const {formatMessage, locale} = this.props.intl;
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport, locale);
this.tinode.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect;
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
// Initialize desktop alerts.
if (this.state.desktopAlertsEnabled) {
try {
this.fbPush = firebase.initializeApp(FIREBASE_INIT, APP_NAME).messaging();
this.fbPush.usePublicVapidKey(FIREBASE_INIT.messagingVapidKey);
navigator.serviceWorker.register('/service-worker.js').then((reg) => {
this.checkForAppUpdate(reg);
this.fbPush.useServiceWorker(reg);
reg.active.postMessage(JSON.stringify({locale: locale}));
this.initDesktopAlerts();
if (this.state.desktopAlerts) {
if (!this.state.firebaseToken) {
this.togglePushToken(true);
} else {
this.tinode.setDeviceToken(this.state.firebaseToken, true);
new Promise((resolve, reject) => {
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport, locale, keepLoggedIn, resolve);
this.tinode.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect;
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
}).then(() => {
// Initialize desktop alerts.
if (this.state.desktopAlertsEnabled) {
try {
this.fbPush = firebase.initializeApp(FIREBASE_INIT, APP_NAME).messaging();
this.fbPush.usePublicVapidKey(FIREBASE_INIT.messagingVapidKey);
navigator.serviceWorker.register('/service-worker.js').then((reg) => {
this.checkForAppUpdate(reg);
this.fbPush.useServiceWorker(reg);
reg.active.postMessage(JSON.stringify({locale: locale}));
this.initDesktopAlerts();
if (this.state.desktopAlerts) {
if (!this.state.firebaseToken) {
this.togglePushToken(true);
} else {
this.tinode.setDeviceToken(this.state.firebaseToken, true);
}
}
}
}).catch((err) => {
// registration failed :(
console.log("Failed to register service worker:", err);
});
} catch (err) {
this.handleError(formatMessage({id: 'push_init_failed'}), 'err');
console.log("Failed to initialize push notifications", err);
this.setState({desktopAlertsEnabled: false});
}).catch((err) => {
// registration failed :(
console.log("Failed to register service worker:", err);
});
} catch (err) {
this.handleError(formatMessage(messages.push_init_failed), 'err');
console.log("Failed to initialize push notifications", err);
this.setState({desktopAlertsEnabled: false});
}
}
}
const token = LocalStorageUtil.getObject('keep-logged-in') ?
LocalStorageUtil.getObject('auth-token') : undefined;
const token = keepLoggedIn ? LocalStorageUtil.getObject('auth-token') : undefined;
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
if (token) {
this.setState({autoLogin: true});
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
if (token) {
this.setState({autoLogin: true});
// When reading from storage, date is returned as string.
token.expires = new Date(token.expires);
this.tinode.setAuthToken(token);
this.tinode.connect().catch((err) => {
// Socket error
this.handleError(err.message, 'err');
});
delete parsedNav.params.info;
delete parsedNav.params.tab;
parsedNav.path[0] = '';
HashNavigation.navigateTo(HashNavigation.composeUrlHash(parsedNav.path, parsedNav.params));
} else if (!parsedNav.params.token) {
HashNavigation.navigateTo('');
}
// When reading from storage, date is returned as string.
token.expires = new Date(token.expires);
this.tinode.setAuthToken(token);
this.tinode.connect().catch((err) => {
// Socket error
this.handleError(err.message, 'err');
});
delete parsedNav.params.info;
delete parsedNav.params.tab;
parsedNav.path[0] = '';
HashNavigation.navigateTo(HashNavigation.composeUrlHash(parsedNav.path, parsedNav.params));
} else if (!parsedNav.params.token) {
HashNavigation.navigateTo('');
}
this.readTimer = null;
this.readTimerCallback = null;
this.readTimer = null;
this.readTimerCallback = null;
this.handleHashRoute();
this.handleHashRoute();
});
}
componentWillUnmount() {
@@ -281,8 +284,9 @@ class TinodeWeb extends React.Component {
}
// Setup transport (usually websocket) and server address. This will terminate connection with the server.
static tnSetup(serverAddress, transport, locale) {
const tinode = new Tinode(APP_NAME, serverAddress, API_KEY, transport, isSecureConnection());
static tnSetup(serverAddress, transport, locale, persistentCache, onSetupCompleted) {
const tinode = new Tinode({appName: APP_NAME, host: serverAddress, apiKey: API_KEY, transport: transport,
secure: isSecureConnection(), persist: persistentCache}, onSetupCompleted);
tinode.setHumanLanguage(locale);
tinode.enableLogging(LOGGING_ENABLED, true);
return tinode;
@@ -361,7 +365,7 @@ class TinodeWeb extends React.Component {
if (online) {
this.handleError();
} else {
this.handleError(this.props.intl.formatMessage({id: 'no_connection'}), 'warn');
this.handleError(this.props.intl.formatMessage(messages.no_connection), 'warn');
}
this.setState({liveConnection: online});
}
@@ -484,7 +488,7 @@ class TinodeWeb extends React.Component {
if (ctrl.code >= 300 && ctrl.text === 'validate credentials') {
this.setState({loadSpinnerVisible: false});
if (cred) {
this.handleError(this.props.intl.formatMessage({id: 'code_doesnot_match'}), 'warn');
this.handleError(this.props.intl.formatMessage(messages.code_doesnot_match), 'warn');
}
this.handleCredentialsRequest(ctrl.params);
} else {
@@ -627,7 +631,7 @@ class TinodeWeb extends React.Component {
}
// Merge search results and contact list to create a single flat
// list of kown contacts for GroupManager to use.
// list of known contacts for GroupManager to use.
static prepareSearchableContacts(chatList, foundContacts) {
const merged = {};
@@ -816,7 +820,7 @@ class TinodeWeb extends React.Component {
// Accept given permissions.
const mode = topic.getAccessMode().getGiven();
response = topic.setMeta({sub: {mode: mode}});
if (topic.isP2P()) {
if (topic.isP2PType()) {
// For P2P topics change 'given' permission of the peer too.
// In p2p topics the other user has the same name as the topic.
response = response.then((ctrl) => {
@@ -924,10 +928,12 @@ class TinodeWeb extends React.Component {
const serverAddress = settings.serverAddress || this.state.serverAddress;
const transport = settings.transport || this.state.transport;
if (this.tinode) {
this.tinode.clearStorage();
this.tinode.onDisconnect = undefined;
this.tinode.disconnect();
}
this.tinode = TinodeWeb.tnSetup(serverAddress, transport, this.props.intl.locale);
this.tinode = TinodeWeb.tnSetup(serverAddress, transport, this.props.intl.locale,
LocalStorageUtil.getObject('keep-logged-in'));
this.tinode.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect;
@@ -1156,11 +1162,14 @@ class TinodeWeb extends React.Component {
}
if (this.tinode) {
this.tinode.clearStorage();
this.tinode.onDisconnect = undefined;
this.tinode.disconnect();
}
this.setState(this.getBlankState());
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport, this.props.intl.locale);
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress,
this.state.transport, this.props.intl.locale, LocalStorageUtil.getObject('keep-logged-in'));
this.tinode.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect;
HashNavigation.navigateTo('');
@@ -1375,7 +1384,7 @@ class TinodeWeb extends React.Component {
handleResetPassword(scheme, newPassword, token) {
token = base64ReEncode(token);
if (!token) {
this.handleError(this.props.intl.formatMessage({id: 'invalid_security_token'}), 'err');
this.handleError(this.props.intl.formatMessage(messages.invalid_security_token), 'err');
} else {
this.tinode.connect()
.then(() => {
+20 -2
View File
@@ -1,7 +1,7 @@
// Single message, sent or received.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Drafty } from 'tinode-sdk'
import { Drafty } from 'tinode-sdk';
import Attachment from './attachment.jsx';
import LetterTile from './letter-tile.jsx';
@@ -165,12 +165,23 @@ export default class ChatMessage extends React.Component {
import { BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
import { fitImageSize } from '../lib/blob-helpers.js';
// Converts Drafty elements into React classes.
// 'this' is set by the caller.
function draftyFormatter(style, data, values, key) {
if (style == 'EX') {
// attachments are handled elsewhere.
return null;
}
let el = Drafty.tagName(style);
if (el) {
const attr = Drafty.attrValue(style, data) || {};
attr.key = key;
switch (style) {
case 'HL':
// Highlighted text. Assign class name.
attr.className = 'highlight';
break;
case 'IM':
// Additional processing for images
if (data) {
@@ -210,9 +221,16 @@ function draftyFormatter(style, data, values, key) {
// Form
attr.className = 'bot-form';
break;
case 'FE':
case 'RW':
// Form element formatting is dependent on element content.
break;
default:
if (el == '_UNKN') {
// Unknown element.
// TODO: make it prettier.
el = <><span className="material-icons">extension</span></>;
}
break;
}
return React.createElement(el, attr, values);
} else {
+18 -2
View File
@@ -3,6 +3,7 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { Drafty } from 'tinode-sdk';
import Tinode from 'tinode-sdk';
import Contact from './contact.jsx';
@@ -10,6 +11,8 @@ import ContactAction from './contact-action.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { MESSAGE_PREVIEW_LENGTH } from '../config.js';
const messages = defineMessages({
badge_you: {
id: 'badge_you',
@@ -25,7 +28,7 @@ const messages = defineMessages({
class ContactList extends React.Component {
render() {
const {formatMessage} = this.props.intl;
const { formatMessage } = this.props.intl;
const showCheckmark = Array.isArray(this.props.topicSelected);
const contactNodes = [];
let contactsCount = 0;
@@ -70,14 +73,27 @@ class ContactList extends React.Component {
badges.push({name: formatMessage(messages.badge_owner), color: 'blue'});
}
}
const comment = Array.isArray(c.private) ?
c.private.join(',') : (c.private ? c.private.comment : null);
let preview;
let deliveryStatus;
if (!this.props.showMode) {
const msg = c.latestMessage();
if (msg) {
deliveryStatus = c.msgStatus(msg);
preview = typeof msg.content == 'string' ?
msg.content.substr(0, MESSAGE_PREVIEW_LENGTH) :
Drafty.preview(msg.content, MESSAGE_PREVIEW_LENGTH);
}
}
contactNodes.push(
<Contact
title={c.public ? c.public.fn : null}
avatar={makeImageUrl(c.public ? c.public.photo : null)}
comment={comment}
preview={preview}
received={deliveryStatus}
unread={this.props.showUnread ? c.unread : 0}
now={c.online && this.props.connected}
acs={c.acs}
+65 -5
View File
@@ -2,10 +2,15 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import LetterTile from './letter-tile.jsx';
import ContactBadges from './contact-badges.jsx';
import LetterTile from './letter-tile.jsx';
import ReceivedMarker from './received-marker.jsx'
import UnreadBadge from './unread-badge.jsx';
import { Drafty } from 'tinode-sdk';
import { deliveryMarker } from '../lib/utils.js';
export default class Contact extends React.Component {
constructor(props) {
super(props);
@@ -35,7 +40,7 @@ export default class Contact extends React.Component {
title = <i><FormattedMessage id="unnamed_topic" /></i>;
} else if (title.length > 30) {
// FIXME: this is probably wrong for RTL languages.
title = title.substring(0, 28) + '...';
title = title.substring(0, 28) + '…';
}
const online = this.props.now ? 'online' : 'offline';
const avatar = this.props.avatar ? this.props.avatar : true;
@@ -53,9 +58,16 @@ export default class Contact extends React.Component {
}
}
const subtitle = this.props.preview ?
(typeof this.props.preview == 'string' ? this.props.preview :
React.createElement(React.Fragment, null, Drafty.format(this.props.preview, draftyFormatter, this))) :
this.props.comment;
const icon = deliveryMarker(this.props.received);
const marker = icon ? <i className={'material-icons small space-right ' + icon.color}>{icon.name}</i> : null;
return (
<li className={!this.props.showCheckmark && this.props.selected ? "selected" : null}
onClick={this.handleClick}>
<li className={!this.props.showCheckmark && this.props.selected ? 'selected' : null} onClick={this.handleClick}>
<div className="avatar-box">
<LetterTile
avatar={avatar}
@@ -71,7 +83,7 @@ export default class Contact extends React.Component {
{this.props.isChannel ? <img src="/img/channel.png" className="channel" alt="channel" /> : null}
<UnreadBadge count={this.props.unread} /><ContactBadges badges={icon_badges} />
</div>
{this.props.comment ? <div className="contact-comment">{this.props.comment}</div> : null}
<div className="contact-comment">{marker}{subtitle || '\u00A0'}</div>
<span><ContactBadges badges={badges} /></span>
</div>
{this.props.showContextMenu ?
@@ -84,3 +96,51 @@ export default class Contact extends React.Component {
);
}
};
// Converts Drafty object into a one-line preview.
function draftyFormatter(style, data, values, key) {
let el = Drafty.tagName(style);
const attr = { key: key };
if (el) {
switch (style) {
case 'HL':
// Make highlight less prominent in preview.
attr.className = 'highlight preview';
break;
case 'LN':
// Disable links in previews.
el = 'span';
break;
case 'IM':
// Replace image with '[icon] Image'.
el = React.Fragment;
values = [<i className="material-icons">photo</i>, 'Picture'];
break;
case 'BN':
el = 'span';
attr.className = 'flat-button faux';
break;
case 'FM':
el = React.Fragment;
values = [<i className="material-icons">dashboard</i>, 'Form:'];
break;
case 'RW':
el = React.Fragment;
break;
case 'EX':
el = React.Fragment;
values = [<i className="material-icons">attachment</i>, 'Attachment'];
break;
default:
if (el == '_UNKN') {
el = React.Fragment;
values = [<i className="material-icons">extension</i>];
}
break;
}
return React.createElement(el, attr, values);
} else {
return values;
}
};
+4 -4
View File
@@ -107,7 +107,7 @@ class ContextMenu extends React.Component {
title: formatMessage(messages.clear_messages),
handler: (params, errorHandler) => {
return props.onShowAlert(
formatMessage({id: 'menu_item_clear_messages'}), // title
formatMessage(messages.menu_item_clear_messages), // title
formatMessage({id: 'clear_messages_warning'}), // content
(() => { this.deleteMessages(true, false, params, errorHandler); }), // onConfirm
null, // "OK"
@@ -121,7 +121,7 @@ class ContextMenu extends React.Component {
title: formatMessage(messages.clear_for_all),
handler: (params, errorHandler) => {
return props.onShowAlert(
formatMessage({id: 'menu_item_clear_messages_for_all'}), // title
formatMessage(messages.menu_item_clear_messages_for_all), // title
formatMessage({id: 'delete_messages_warning'}), // content
(() => { return this.deleteMessages(true, true, params, errorHandler); }),
null, // "OK"
@@ -171,7 +171,7 @@ class ContextMenu extends React.Component {
title: formatMessage(messages.block),
handler: (params, errorHandler) => {
return props.onShowAlert(
formatMessage({id: 'menu_item_block'}), // title
formatMessage(messages.menu_item_block), // title
formatMessage(messages.topic_block_warning), // content
(() => {
return this.topicPermissionSetter('-JP', params, errorHandler).then((ctrl) => {
@@ -190,7 +190,7 @@ class ContextMenu extends React.Component {
title: formatMessage(messages.topic_delete),
handler: (params, errorHandler) => {
return props.onShowAlert(
formatMessage({id: 'menu_item_delete_topic'}), // title
formatMessage(messages.menu_item_delete_topic), // title
formatMessage(messages.topic_delete_warning), // content
(() => {
const topic = this.props.tinode.getTopic(params.topicName);
+3 -12
View File
@@ -5,6 +5,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import Tinode from 'tinode-sdk';
import { shortDateFormat } from '../lib/strformat.js';
import { deliveryMarker } from '../lib/utils.js';
const messages = defineMessages({
'sending': {
@@ -31,18 +32,8 @@ class ReceivedMarker extends React.PureComponent {
timestamp = shortDateFormat(this.props.timestamp, this.props.intl.locale);
}
let marker = null;
if (this.props.received <= Tinode.MESSAGE_STATUS_SENDING) {
marker = (<i className="material-icons small">access_time</i>); // watch face
} else if (this.props.received == Tinode.MESSAGE_STATUS_FAILED) {
marker = (<i className="material-icons small amber">warning</i>); // yellow icon /!\
} else if (this.props.received == Tinode.MESSAGE_STATUS_SENT) {
marker = (<i className="material-icons small">done</i>); // checkmark
} else if (this.props.received == Tinode.MESSAGE_STATUS_RECEIVED) {
marker = (<i className="material-icons small">done_all</i>); // double checkmark
} else if (this.props.received == Tinode.MESSAGE_STATUS_READ) {
marker = (<i className="material-icons small blue">done_all</i>); // blue double checkmark
}
const icon = deliveryMarker(this.props.received);
const marker = icon ? <i className={'material-icons small ' + icon.color}>{icon.name}</i> : null;
return (
<span className="timestamp">
+2315 -1623
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
+1371 -703
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
+6 -2
View File
@@ -13,7 +13,7 @@ module.exports = (env, argv) => {
rules: [
{
test: /\.jsx?$/,
loaders: [
use: [
'babel-loader',
],
exclude: /node_modules/,
@@ -26,14 +26,17 @@ module.exports = (env, argv) => {
publicPath: '/umd/'
},
optimization: {
minimize: (argv.mode === 'production'),
minimizer: [
new TerserPlugin({
sourceMap: true,
terserOptions: {
ecma: undefined,
warnings: false,
parse: {},
compress: {},
format: {
comments: false,
},
mangle: true, // Note `mangle.properties` is `false` by default.
module: false,
output: null,
@@ -44,6 +47,7 @@ module.exports = (env, argv) => {
keep_fnames: false,
safari10: false,
},
extractComments: false,
})
]
},