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"], "presets": ["@babel/preset-react", "@babel/preset-env"],
"comments": false, "comments": false,
"plugins": [ "plugins": [
["react-intl", { [
"messagesDir": "./src/i18n/" "formatjs",
}] {
"idInterpolationPattern": "[sha512:contenthash:base64:6]",
"ast": true
}
]
] ]
} }
+30 -4
View File
@@ -42,7 +42,7 @@ a.gray:focus {
color: #999; color: #999;
} }
a.flat-button { .flat-button {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 0.15rem 0.35rem; margin: 0.15rem 0.35rem;
@@ -55,11 +55,21 @@ a.flat-button {
white-space: nowrap; white-space: nowrap;
text-transform: uppercase; text-transform: uppercase;
} }
a.flat-button:hover, .flat-button:hover,
a.flat-button:focus { .flat-button:focus {
text-decoration: none; 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 { form {
font-size: 1.1rem; font-size: 1.1rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
@@ -541,6 +551,11 @@ div.alert .content {
margin-left: 1rem; margin-left: 1rem;
} }
/* Extra spece on the right */
.space-right {
margin-right: 0.25rem;
}
/* Container with whitish background and rounded corners */ /* Container with whitish background and rounded corners */
.rounded-container { .rounded-container {
position: relative; position: relative;
@@ -825,7 +840,7 @@ i.search {
display: flex; display: flex;
position: relative; position: relative;
padding: 0.35rem 0.25rem 0.5rem 0.35rem; padding: 0.35rem 0.25rem 0.5rem 0.35rem;
min-height: 3.5rem; min-height: 3.75rem;
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
/* partial bottom border */ /* partial bottom border */
@@ -2066,3 +2081,14 @@ ul.tabbar li.active > a:after {
background-color: #1e88e5; background-color: #1e88e5;
border-radius: 0.08rem; 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 --> <!-- Styles for local elements -->
<link rel="stylesheet" href="css/base.css" /> <link rel="stylesheet" href="css/base.css" />
<!-- ReactJS --> <!-- 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@17/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-dom@17/umd/react-dom.development.js"></script>
<!-- React-Intl for translations --> <!-- 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 --> <!-- 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/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-messaging.js"></script>
</head> </head>
<body> <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/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="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="css/base.min.css" /> <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@17.0.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-dom@17.0.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/react-intl@5.10.14/react-intl.umd.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.24.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.24.0/firebase-messaging.js"></script>
</head> </head>
<body> <body>
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div> <noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
+1969 -3497
View File
File diff suppressed because it is too large Load Diff
+17 -29
View File
@@ -1,7 +1,7 @@
{ {
"name": "tinode-webapp", "name": "tinode-webapp",
"description": "Tinode messenger for the web", "description": "Tinode messenger for the web",
"version": "0.16.9-rc1", "version": "0.17.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/tinode/example-react-js.git" "url": "https://github.com/tinode/example-react-js.git"
@@ -31,25 +31,14 @@
"audio/msg.mp3", "audio/msg.mp3",
"css/base.css", "css/base.css",
"css/base.min.css", "css/base.min.css",
"img/bg_messages.png", "img/*.png",
"img/logo.svg", "img/*.svg",
"img/logo32x32.png", "img/*.jpeg",
"img/logo32x32a.png",
"img/logo96.png",
"img/logo192.png",
"img/badge96.png",
"img/broken_image.png",
"img/og-logo.jpeg",
"index-dev.html", "index-dev.html",
"index.html", "index.html",
"manifest.json", "manifest.json",
"service-worker.js", "service-worker.js",
"firebase-init.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"
], ],
"keywords": [ "keywords": [
"chat", "chat",
@@ -69,24 +58,23 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"firebase": "^7.24.0", "firebase": "^7.24.0",
"react": "^16.14.0", "react": "^17.0.1",
"react-dom": "^16.14.0", "react-dom": "^17.0.1",
"react-intl": "^4.7.6", "react-intl": "^5.10.14",
"tinode-sdk": "^0.16.9-rc2" "tinode-sdk": "^0.16.10"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.10", "@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.10", "@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10", "@babel/preset-react": "^7.12.10",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"babel-plugin-react-intl": "^7.9.4", "babel-plugin-formatjs": "^9.0.2",
"browserslist": "^4.16.0", "browserslist": "^4.16.1",
"copy-webpack-plugin": "^6.4.0", "copy-webpack-plugin": "^6.4.1",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"postcss": "^7.0.35", "postcss": "^8.2.4",
"postcss-cli": "^7.1.2", "postcss-cli": "^8.3.1",
"terser-webpack-plugin": "^3.1.0", "webpack": "^5.13.0",
"webpack": "^4.44.2", "webpack-cli": "^4.3.1"
"webpack-cli": "^3.3.12"
} }
} }
+3
View File
@@ -81,6 +81,9 @@ export const MAX_ONLINE_IN_TOPIC = 4;
// Maximum length of user name, topic title, and private comment. // Maximum length of user name, topic title, and private comment.
export const MAX_TITLE_LENGTH = 60; 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". // Link for "Contact Us".
export const LINK_CONTACT_US = 'email:info@tinode.co'; export const LINK_CONTACT_US = 'email:info@tinode.co';
+6 -4
View File
@@ -3,15 +3,17 @@ export function shortDateFormat(then, locale) {
locale = locale || window.navigator.userLanguage || window.navigator.language; locale = locale || window.navigator.userLanguage || window.navigator.language;
const now = new Date(); const now = new Date();
if (then.getFullYear() == now.getFullYear()) { if (then.getFullYear() == now.getFullYear()) {
// Same year.
if (then.getMonth() == now.getMonth() && then.getDate() == now.getDate()) { if (then.getMonth() == now.getMonth() && then.getDate() == now.getDate()) {
// Same month and day, show time only.
return then.toLocaleTimeString(locale, {hour12: false, hour: '2-digit', minute: '2-digit'}); return then.toLocaleTimeString(locale, {hour12: false, hour: '2-digit', minute: '2-digit'});
} else { }
// Different month and/or day, show month day, time.
return then.toLocaleDateString(locale, return then.toLocaleDateString(locale,
{hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'}); {hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
} }
} // Different year: just show the date.
return then.toLocaleDateString(locale, return then.toLocaleDateString(locale, {year: 'numeric', month: 'short', day: 'numeric'});
{hour12: false, year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
} }
// Convert seconds to minutes:seconds, i.e. 156 sec -> 2:36. // Convert seconds to minutes:seconds, i.e. 156 sec -> 2:36.
+19
View File
@@ -1,5 +1,7 @@
// Odds and ends // Odds and ends
import Tinode from 'tinode-sdk';
// 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.
export function updateFavicon(count) { export function updateFavicon(count) {
const oldIcon = document.getElementById('shortcut-icon'); const oldIcon = document.getElementById('shortcut-icon');
@@ -144,3 +146,20 @@ export function sanitizeImageUrl(url) {
return null; 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. // 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), avatar: makeImageUrl(topic.public ? topic.public.photo : null),
private: topic.private ? topic.private.comment : null, private: topic.private ? topic.private.comment : null,
address: topic.name, address: topic.name,
groupTopic: (topic.getType() == 'grp'), groupTopic: topic.isGroupType(),
channel: topic.isChannel(), channel: topic.isChannelType(),
showMemberPanel: false, showMemberPanel: false,
access: acs ? acs.getMode() : undefined, access: acs ? acs.getMode() : undefined,
modeGiven: acs ? acs.getGiven() : 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 // Don't request the tags. They are useless unless the user
// is the owner and is editing the topic. // is the owner and is editing the topic.
let getQuery = topic.startMetaQuery().withLaterDesc(); let getQuery = topic.startMetaQuery().withLaterDesc().withLaterSub();
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();
}
if (this.state.isReader || newTopic) { if (this.state.isReader || newTopic) {
// Reading is either permitted or we don't know because it's a new topic. Ask for messages. // Reading is either permitted or we don't know because it's a new topic. Ask for messages.
getQuery = getQuery.withLaterData(MESSAGES_PAGE); getQuery = getQuery.withLaterData(MESSAGES_PAGE);
@@ -280,7 +276,7 @@ class MessagesView extends React.Component {
}); });
} }
Object.assign(nextState, { Object.assign(nextState, {
channel: topic.isChannel() channel: topic.isChannelType()
}); });
} else { } else {
// Invalid topic. // Invalid topic.
@@ -601,7 +597,7 @@ class MessagesView extends React.Component {
const menuItems = messageSpecificMenuItems || []; const menuItems = messageSpecificMenuItems || [];
const topic = this.props.tinode.getTopic(params.topicName); const topic = this.props.tinode.getTopic(params.topicName);
if (topic) { if (topic) {
if (!topic.isChannel()) { if (!topic.isChannelType()) {
menuItems.push('message_delete'); menuItems.push('message_delete');
} }
const acs = topic.getAccessMode(); const acs = topic.getAccessMode();
@@ -821,8 +817,8 @@ class MessagesView extends React.Component {
); );
} else { } else {
const topic = this.props.tinode.getTopic(this.state.topic); const topic = this.props.tinode.getTopic(this.state.topic);
const isChannel = topic.isChannel(); const isChannel = topic.isChannelType();
const groupTopic = topic.getType() == 'grp' && !isChannel; const groupTopic = topic.isGroupType() && !isChannel;
let messageNodes = []; let messageNodes = [];
let previousFrom = null; let previousFrom = null;
let chatBoxClass = null; let chatBoxClass = null;
+23 -14
View File
@@ -212,12 +212,15 @@ class TinodeWeb extends React.Component {
viewportHeight: document.documentElement.clientHeight viewportHeight: document.documentElement.clientHeight
}); });
const keepLoggedIn = LocalStorageUtil.getObject('keep-logged-in');
const {formatMessage, locale} = this.props.intl; const {formatMessage, locale} = this.props.intl;
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport, locale); new Promise((resolve, reject) => {
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport, locale, keepLoggedIn, resolve);
this.tinode.onConnect = this.handleConnected; this.tinode.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect; this.tinode.onDisconnect = this.handleDisconnect;
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration; this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
}).then(() => {
// Initialize desktop alerts. // Initialize desktop alerts.
if (this.state.desktopAlertsEnabled) { if (this.state.desktopAlertsEnabled) {
try { try {
@@ -240,14 +243,13 @@ class TinodeWeb extends React.Component {
console.log("Failed to register service worker:", err); console.log("Failed to register service worker:", err);
}); });
} catch (err) { } catch (err) {
this.handleError(formatMessage({id: 'push_init_failed'}), 'err'); this.handleError(formatMessage(messages.push_init_failed), 'err');
console.log("Failed to initialize push notifications", err); console.log("Failed to initialize push notifications", err);
this.setState({desktopAlertsEnabled: false}); this.setState({desktopAlertsEnabled: false});
} }
} }
const token = LocalStorageUtil.getObject('keep-logged-in') ? const token = keepLoggedIn ? LocalStorageUtil.getObject('auth-token') : undefined;
LocalStorageUtil.getObject('auth-token') : undefined;
const parsedNav = HashNavigation.parseUrlHash(window.location.hash); const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
if (token) { if (token) {
@@ -272,6 +274,7 @@ class TinodeWeb extends React.Component {
this.readTimerCallback = null; this.readTimerCallback = null;
this.handleHashRoute(); this.handleHashRoute();
});
} }
componentWillUnmount() { componentWillUnmount() {
@@ -281,8 +284,9 @@ class TinodeWeb extends React.Component {
} }
// Setup transport (usually websocket) and server address. This will terminate connection with the server. // Setup transport (usually websocket) and server address. This will terminate connection with the server.
static tnSetup(serverAddress, transport, locale) { static tnSetup(serverAddress, transport, locale, persistentCache, onSetupCompleted) {
const tinode = new Tinode(APP_NAME, serverAddress, API_KEY, transport, isSecureConnection()); const tinode = new Tinode({appName: APP_NAME, host: serverAddress, apiKey: API_KEY, transport: transport,
secure: isSecureConnection(), persist: persistentCache}, onSetupCompleted);
tinode.setHumanLanguage(locale); tinode.setHumanLanguage(locale);
tinode.enableLogging(LOGGING_ENABLED, true); tinode.enableLogging(LOGGING_ENABLED, true);
return tinode; return tinode;
@@ -361,7 +365,7 @@ class TinodeWeb extends React.Component {
if (online) { if (online) {
this.handleError(); this.handleError();
} else { } 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}); this.setState({liveConnection: online});
} }
@@ -484,7 +488,7 @@ class TinodeWeb extends React.Component {
if (ctrl.code >= 300 && ctrl.text === 'validate credentials') { if (ctrl.code >= 300 && ctrl.text === 'validate credentials') {
this.setState({loadSpinnerVisible: false}); this.setState({loadSpinnerVisible: false});
if (cred) { 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); this.handleCredentialsRequest(ctrl.params);
} else { } else {
@@ -627,7 +631,7 @@ class TinodeWeb extends React.Component {
} }
// Merge search results and contact list to create a single flat // 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) { static prepareSearchableContacts(chatList, foundContacts) {
const merged = {}; const merged = {};
@@ -816,7 +820,7 @@ class TinodeWeb extends React.Component {
// Accept given permissions. // Accept given permissions.
const mode = topic.getAccessMode().getGiven(); const mode = topic.getAccessMode().getGiven();
response = topic.setMeta({sub: {mode: mode}}); response = topic.setMeta({sub: {mode: mode}});
if (topic.isP2P()) { if (topic.isP2PType()) {
// For P2P topics change 'given' permission of the peer too. // For P2P topics change 'given' permission of the peer too.
// In p2p topics the other user has the same name as the topic. // In p2p topics the other user has the same name as the topic.
response = response.then((ctrl) => { response = response.then((ctrl) => {
@@ -924,10 +928,12 @@ class TinodeWeb extends React.Component {
const serverAddress = settings.serverAddress || this.state.serverAddress; const serverAddress = settings.serverAddress || this.state.serverAddress;
const transport = settings.transport || this.state.transport; const transport = settings.transport || this.state.transport;
if (this.tinode) { if (this.tinode) {
this.tinode.clearStorage();
this.tinode.onDisconnect = undefined; this.tinode.onDisconnect = undefined;
this.tinode.disconnect(); 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.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect; this.tinode.onDisconnect = this.handleDisconnect;
@@ -1156,11 +1162,14 @@ class TinodeWeb extends React.Component {
} }
if (this.tinode) { if (this.tinode) {
this.tinode.clearStorage();
this.tinode.onDisconnect = undefined; this.tinode.onDisconnect = undefined;
this.tinode.disconnect(); this.tinode.disconnect();
} }
this.setState(this.getBlankState()); 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.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect; this.tinode.onDisconnect = this.handleDisconnect;
HashNavigation.navigateTo(''); HashNavigation.navigateTo('');
@@ -1375,7 +1384,7 @@ class TinodeWeb extends React.Component {
handleResetPassword(scheme, newPassword, token) { handleResetPassword(scheme, newPassword, token) {
token = base64ReEncode(token); token = base64ReEncode(token);
if (!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 { } else {
this.tinode.connect() this.tinode.connect()
.then(() => { .then(() => {
+20 -2
View File
@@ -1,7 +1,7 @@
// Single message, sent or received. // Single message, sent or received.
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Drafty } from 'tinode-sdk' import { Drafty } from 'tinode-sdk';
import Attachment from './attachment.jsx'; import Attachment from './attachment.jsx';
import LetterTile from './letter-tile.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 { BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
import { fitImageSize } from '../lib/blob-helpers.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) { function draftyFormatter(style, data, values, key) {
if (style == 'EX') {
// attachments are handled elsewhere.
return null;
}
let el = Drafty.tagName(style); let el = Drafty.tagName(style);
if (el) { if (el) {
const attr = Drafty.attrValue(style, data) || {}; const attr = Drafty.attrValue(style, data) || {};
attr.key = key; attr.key = key;
switch (style) { switch (style) {
case 'HL':
// Highlighted text. Assign class name.
attr.className = 'highlight';
break;
case 'IM': case 'IM':
// Additional processing for images // Additional processing for images
if (data) { if (data) {
@@ -210,9 +221,16 @@ function draftyFormatter(style, data, values, key) {
// Form // Form
attr.className = 'bot-form'; attr.className = 'bot-form';
break; break;
case 'FE': case 'RW':
// Form element formatting is dependent on element content. // Form element formatting is dependent on element content.
break; 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); return React.createElement(el, attr, values);
} else { } else {
+17 -1
View File
@@ -3,6 +3,7 @@
import React from 'react'; import React from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { Drafty } from 'tinode-sdk';
import Tinode from 'tinode-sdk'; import Tinode from 'tinode-sdk';
import Contact from './contact.jsx'; import Contact from './contact.jsx';
@@ -10,6 +11,8 @@ import ContactAction from './contact-action.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js'; import { makeImageUrl } from '../lib/blob-helpers.js';
import { MESSAGE_PREVIEW_LENGTH } from '../config.js';
const messages = defineMessages({ const messages = defineMessages({
badge_you: { badge_you: {
id: 'badge_you', id: 'badge_you',
@@ -70,14 +73,27 @@ class ContactList extends React.Component {
badges.push({name: formatMessage(messages.badge_owner), color: 'blue'}); badges.push({name: formatMessage(messages.badge_owner), color: 'blue'});
} }
} }
const comment = Array.isArray(c.private) ? const comment = Array.isArray(c.private) ?
c.private.join(',') : (c.private ? c.private.comment : null); 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( contactNodes.push(
<Contact <Contact
title={c.public ? c.public.fn : null} title={c.public ? c.public.fn : null}
avatar={makeImageUrl(c.public ? c.public.photo : null)} avatar={makeImageUrl(c.public ? c.public.photo : null)}
comment={comment} comment={comment}
preview={preview}
received={deliveryStatus}
unread={this.props.showUnread ? c.unread : 0} unread={this.props.showUnread ? c.unread : 0}
now={c.online && this.props.connected} now={c.online && this.props.connected}
acs={c.acs} acs={c.acs}
+65 -5
View File
@@ -2,10 +2,15 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import LetterTile from './letter-tile.jsx';
import ContactBadges from './contact-badges.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 UnreadBadge from './unread-badge.jsx';
import { Drafty } from 'tinode-sdk';
import { deliveryMarker } from '../lib/utils.js';
export default class Contact extends React.Component { export default class Contact extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -35,7 +40,7 @@ export default class Contact extends React.Component {
title = <i><FormattedMessage id="unnamed_topic" /></i>; title = <i><FormattedMessage id="unnamed_topic" /></i>;
} else if (title.length > 30) { } else if (title.length > 30) {
// FIXME: this is probably wrong for RTL languages. // 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 online = this.props.now ? 'online' : 'offline';
const avatar = this.props.avatar ? this.props.avatar : true; 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 ( return (
<li className={!this.props.showCheckmark && this.props.selected ? "selected" : null} <li className={!this.props.showCheckmark && this.props.selected ? 'selected' : null} onClick={this.handleClick}>
onClick={this.handleClick}>
<div className="avatar-box"> <div className="avatar-box">
<LetterTile <LetterTile
avatar={avatar} 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} {this.props.isChannel ? <img src="/img/channel.png" className="channel" alt="channel" /> : null}
<UnreadBadge count={this.props.unread} /><ContactBadges badges={icon_badges} /> <UnreadBadge count={this.props.unread} /><ContactBadges badges={icon_badges} />
</div> </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> <span><ContactBadges badges={badges} /></span>
</div> </div>
{this.props.showContextMenu ? {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), title: formatMessage(messages.clear_messages),
handler: (params, errorHandler) => { handler: (params, errorHandler) => {
return props.onShowAlert( return props.onShowAlert(
formatMessage({id: 'menu_item_clear_messages'}), // title formatMessage(messages.menu_item_clear_messages), // title
formatMessage({id: 'clear_messages_warning'}), // content formatMessage({id: 'clear_messages_warning'}), // content
(() => { this.deleteMessages(true, false, params, errorHandler); }), // onConfirm (() => { this.deleteMessages(true, false, params, errorHandler); }), // onConfirm
null, // "OK" null, // "OK"
@@ -121,7 +121,7 @@ class ContextMenu extends React.Component {
title: formatMessage(messages.clear_for_all), title: formatMessage(messages.clear_for_all),
handler: (params, errorHandler) => { handler: (params, errorHandler) => {
return props.onShowAlert( 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 formatMessage({id: 'delete_messages_warning'}), // content
(() => { return this.deleteMessages(true, true, params, errorHandler); }), (() => { return this.deleteMessages(true, true, params, errorHandler); }),
null, // "OK" null, // "OK"
@@ -171,7 +171,7 @@ class ContextMenu extends React.Component {
title: formatMessage(messages.block), title: formatMessage(messages.block),
handler: (params, errorHandler) => { handler: (params, errorHandler) => {
return props.onShowAlert( return props.onShowAlert(
formatMessage({id: 'menu_item_block'}), // title formatMessage(messages.menu_item_block), // title
formatMessage(messages.topic_block_warning), // content formatMessage(messages.topic_block_warning), // content
(() => { (() => {
return this.topicPermissionSetter('-JP', params, errorHandler).then((ctrl) => { return this.topicPermissionSetter('-JP', params, errorHandler).then((ctrl) => {
@@ -190,7 +190,7 @@ class ContextMenu extends React.Component {
title: formatMessage(messages.topic_delete), title: formatMessage(messages.topic_delete),
handler: (params, errorHandler) => { handler: (params, errorHandler) => {
return props.onShowAlert( return props.onShowAlert(
formatMessage({id: 'menu_item_delete_topic'}), // title formatMessage(messages.menu_item_delete_topic), // title
formatMessage(messages.topic_delete_warning), // content formatMessage(messages.topic_delete_warning), // content
(() => { (() => {
const topic = this.props.tinode.getTopic(params.topicName); 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 Tinode from 'tinode-sdk';
import { shortDateFormat } from '../lib/strformat.js'; import { shortDateFormat } from '../lib/strformat.js';
import { deliveryMarker } from '../lib/utils.js';
const messages = defineMessages({ const messages = defineMessages({
'sending': { 'sending': {
@@ -31,18 +32,8 @@ class ReceivedMarker extends React.PureComponent {
timestamp = shortDateFormat(this.props.timestamp, this.props.intl.locale); timestamp = shortDateFormat(this.props.timestamp, this.props.intl.locale);
} }
let marker = null; const icon = deliveryMarker(this.props.received);
if (this.props.received <= Tinode.MESSAGE_STATUS_SENDING) { const marker = icon ? <i className={'material-icons small ' + icon.color}>{icon.name}</i> : null;
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
}
return ( return (
<span className="timestamp"> <span className="timestamp">
+2263 -1571
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
+1367 -699
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: [ rules: [
{ {
test: /\.jsx?$/, test: /\.jsx?$/,
loaders: [ use: [
'babel-loader', 'babel-loader',
], ],
exclude: /node_modules/, exclude: /node_modules/,
@@ -26,14 +26,17 @@ module.exports = (env, argv) => {
publicPath: '/umd/' publicPath: '/umd/'
}, },
optimization: { optimization: {
minimize: (argv.mode === 'production'),
minimizer: [ minimizer: [
new TerserPlugin({ new TerserPlugin({
sourceMap: true,
terserOptions: { terserOptions: {
ecma: undefined, ecma: undefined,
warnings: false, warnings: false,
parse: {}, parse: {},
compress: {}, compress: {},
format: {
comments: false,
},
mangle: true, // Note `mangle.properties` is `false` by default. mangle: true, // Note `mangle.properties` is `false` by default.
module: false, module: false,
output: null, output: null,
@@ -44,6 +47,7 @@ module.exports = (env, argv) => {
keep_fnames: false, keep_fnames: false,
safari10: false, safari10: false,
}, },
extractComments: false,
}) })
] ]
}, },