Compare commits
22
Commits
e05e75ed28
...
6f47cc50b0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f47cc50b0 | ||
|
|
1172adefb4 | ||
|
|
4f38a2d8b1 | ||
|
|
d75448c548 | ||
|
|
9c69b7129c | ||
|
|
4ea82f85ba | ||
|
|
3132294cce | ||
|
|
6f278f5bb5 | ||
|
|
92e3f15c01 | ||
|
|
1f5a76e481 | ||
|
|
4445a90437 | ||
|
|
0afa8bcdcd | ||
|
|
55d7e0d5cb | ||
|
|
4b07c3b5c9 | ||
|
|
a319e2b0bc | ||
|
|
924459f84b | ||
|
|
396b23ee88 | ||
|
|
775bc954e3 | ||
|
|
6fabb6c111 | ||
|
|
e328e6a4c2 | ||
|
|
443245280f | ||
|
|
a248151243 |
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+5
-5
@@ -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
@@ -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>
|
||||
|
||||
Generated
+2050
-3578
File diff suppressed because it is too large
Load Diff
+17
-29
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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(() => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1371
-703
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+6
-2
@@ -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,
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user