From 7d01356be76445b85e1b13844f4dcda05d9b41b0 Mon Sep 17 00:00:00 2001 From: or-else Date: Sat, 8 Sep 2018 19:51:50 +0300 Subject: [PATCH] moving contact management to --- css/base.css | 5 +- index-dev.html | 8 +- index.html | 1 - package.json | 5 +- src/index.js | 2 +- src/webapp.jsx | 260 ++++++++++++++++++++++++------------------------- 6 files changed, 141 insertions(+), 140 deletions(-) diff --git a/css/base.css b/css/base.css index afa2fabb..0e433043 100644 --- a/css/base.css +++ b/css/base.css @@ -1339,12 +1339,13 @@ ul.tabbar > li { ul.tabbar > li > a { position: relative; padding-top: 0.75rem; + padding-left: 1rem; + padding-right: 1rem; display: block; - min-width: 6.5rem; text-align: center; line-height: 2rem; font-weight: 400; - font-size: 1.25rem; + font-size: 1.3rem; font-variant: small-caps; text-decoration: none; color: rgba(33,150,243,0.6); diff --git a/index-dev.html b/index-dev.html index 1c15984c..5d9c632e 100644 --- a/index-dev.html +++ b/index-dev.html @@ -28,7 +28,6 @@ - @@ -36,7 +35,12 @@
- + + + diff --git a/index.html b/index.html index b3ef7eb4..9dacd552 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,6 @@ -
diff --git a/package.json b/package.json index a945db96..edfdd450 100644 --- a/package.json +++ b/package.json @@ -51,12 +51,13 @@ "react", "web" ], - "main": "index.js", + "main": "./umd/tinode.prod.js", "bugs": { "url": "https://github.com/tinode/example-react-js/issues" }, "homepage": "https://github.com/tinode/chat", - "email": "gene@tinode.co", + "email": "info@tinode.co", + "author": "Tinode Authors ", "license": "Apache-2.0", "dependencies": { "react": "^16.4.2", diff --git a/src/index.js b/src/index.js index f3353dd2..859ff0b4 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,7 @@ var React = require('react'); var ReactDOM = require('react-dom'); var Tinode = require('tinode-sdk'); -var TinodeWeb = require('../webapp.js') +var TinodeWeb = require('../webapp.js'); ReactDOM.render( React.createElement(TinodeWeb, null), diff --git a/src/webapp.jsx b/src/webapp.jsx index b2e51fb8..29582254 100644 --- a/src/webapp.jsx +++ b/src/webapp.jsx @@ -11,9 +11,6 @@ const DEFAULT_HOST = KNOWN_HOSTS.hosted; // Sound to play on message received. const POP_SOUND = new Audio('audio/msg.mp3'); -// Unicode symbol used to clear objects -const DEL_CHAR = "\u2421"; - // API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own const API_KEY = "AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K"; @@ -59,9 +56,6 @@ const MAX_IMAGE_DIM = 768; const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml']; const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg']; -// Tinode is defined in 'tinode.js'. -var Drafty = Tinode.Drafty; - // Helper functions for storing values in localStorage. // By default localStorage can store only strings, not objects or other types. Storage.prototype.setObject = function(key, value) { @@ -482,7 +476,7 @@ class ContextMenu extends React.Component { console.log("Topic not found: ", params.topicName); return; } - topic.delTopic().catch(function(err) { + topic.delTopic().catch((err) => { if (errorHandler) { errorHandler(err.message, "err"); } @@ -1394,16 +1388,15 @@ class AvatarUpload extends React.Component { } handleFileUpload(e) { - var instance = this; imageFileScaledToBase64(e.target.files[0], AVATAR_SIZE, AVATAR_SIZE, true, // Success - function(base64bits, mime) { + (base64bits, mime) => { var du = makeImageUrl({data: base64bits, type: mime}); - instance.setState({dataUrl: du}); - instance.props.onImageChanged(du); + this.setState({dataUrl: du}); + this.props.onImageChanged(du); }, // Failure - function(err) { + (err) => { this.props.onError(err, "err"); }); // Clear the value so the same file can be uploaded again. @@ -1676,6 +1669,7 @@ class SidepanelView extends React.Component { myUserId={this.props.myUserId} connected={this.props.connected} topicSelected={this.props.topicSelected} + chatList={this.props.chatList} showContextMenu={this.props.showContextMenu} messageSounds={this.props.messageSounds} onTopicSelected={this.props.onTopicSelected} @@ -1685,7 +1679,7 @@ class SidepanelView extends React.Component { view === 'newtpk' ? { - // Force to integers; - c.seq = ~~c.seq; - c.read = ~~c.read; - c.unread = c.seq - c.read; + static getDerivedStateFromProps(nextProps, prevState) { + let contacts = []; + let unreadThreads = 0; + nextProps.chatList.map((c) => { unreadThreads += c.unread > 0 ? 1 : 0; contacts.push(c); - if (this.props.topicSelected == c.topic) { - this.props.onOnlineChange(c.online); - this.props.onAcsChange(c.acs); - } }); - contacts.sort(function(a,b){ + + contacts.sort(function(a, b){ return b.touched - a.touched; }); - updateFavicon(unreadThreads); - return contacts; - } - resetContactList() { - this.setState({contactList: this.prepareContactList()}); + updateFavicon(unreadThreads); + + return {contactList: contacts}; } render() { @@ -2498,7 +2400,7 @@ class NewTopicView extends React.Component { this.state = { tabSelected: "p2p", searchQuery: props.contactsSearchQuery, - contactList: props.foundContacts + contactList: props.searchResults }; this.handleTabClick = this.handleTabClick.bind(this); @@ -2514,7 +2416,7 @@ class NewTopicView extends React.Component { static getDerivedStateFromProps(nextProps, prevState) { return { searchQuery: nextProps.contactsSearchQuery, - contactList: nextProps.foundContacts + contactList: nextProps.searchResults }; } @@ -2676,7 +2578,7 @@ class SearchContacts extends React.Component { componentWillUnmount() { if (this.state.search) { this.setState({search: ''}); - this.props.onSearchContacts(DEL_CHAR); + this.props.onSearchContacts(Tinode.DEL_CHAR); } } @@ -2691,13 +2593,13 @@ class SearchContacts extends React.Component { this.setState({edited: true}); this.props.onSearchContacts(query); } else if (this.props.searchQuery) { - this.props.onSearchContacts(DEL_CHAR); + this.props.onSearchContacts(Tinode.DEL_CHAR); } } handleClear() { if (this.state.edited || this.state.search) { - this.props.onSearchContacts(DEL_CHAR); + this.props.onSearchContacts(Tinode.DEL_CHAR); } this.setState({search: '', edited: false}); } @@ -3213,7 +3115,7 @@ class InfoView extends React.Component { members={this.state.contactList} requiredMember={this.props.myUserId} myUserId={this.props.myUserId} - contacts={this.props.foundContacts} + contacts={this.props.searchableContacts} onCancel={this.handleHideAddMembers} onSubmit={this.handleMemberUpdateRequest} /> : @@ -4363,6 +4265,9 @@ class TinodeWeb extends React.Component { this.handleLoginSuccessful = this.handleLoginSuccessful.bind(this); this.handleDisconnect = this.handleDisconnect.bind(this); this.tnMeMetaDesc = this.tnMeMetaDesc.bind(this); + this.tnMeContactUpdate = this.tnMeContactUpdate.bind(this); + this.tnMeSubsUpdated = this.tnMeSubsUpdated.bind(this); + this.resetContactList = this.resetContactList.bind(this); this.tnData = this.tnData.bind(this); this.tnInitFind = this.tnInitFind.bind(this); this.tnFndSubsUpdated = this.tnFndSubsUpdated.bind(this); @@ -4406,11 +4311,15 @@ class TinodeWeb extends React.Component { connected: false, transport: settings.transport || null, serverAddress: settings.serverAddress || detectServerAddress(), + serverVersion: 'no connection', // "On" is the default, so saving the "off" state. messageSounds: !settings.messageSoundsOff, desktopAlerts: settings.desktopAlerts, desktopAlertsEnabled: isSecureConnection(), + errorText: '', + errorLevel: null, + sidePanelSelected: 'login', sidePanelTitle: null, sidePanelAvatar: null, @@ -4419,8 +4328,6 @@ class TinodeWeb extends React.Component { login: '', password: '', myUserId: null, - errorText: '', - errorLevel: null, liveConnection: navigator.onLine, topicSelected: '', topicSelectedOnline: false, @@ -4429,14 +4336,20 @@ class TinodeWeb extends React.Component { displayMobile: (window.innerWidth <= MEDIA_BREAKPOINT), showInfoPanel: false, mobilePanel: 'sidepanel', + contextMenuVisible: false, contextMenuBounds: null, contextMenuClickAt: null, contextMenuParams: null, contextMenuItems: [], - serverVersion: 'no connection', + contactsSearchQuery: undefined, - foundContacts: [], + // Chats as shown in the ContactsView + chatList: [], + // Contacts returned by a search query. + searchResults: [], + // Merged results of a search query and p2p chats. + searchableContacts: [], credMethod: undefined, credCode: undefined }; @@ -4664,6 +4577,8 @@ class TinodeWeb extends React.Component { // Logged in fine, subscribe to 'me' attaching callbacks from the contacts view. var me = this.state.tinode.getMeTopic(); me.onMetaDesc = this.tnMeMetaDesc; + me.onContactUpdate = this.tnMeContactUpdate; + me.onSubsUpdated = this.tnMeSubsUpdated; this.setState({ connected: true, credMethod: undefined, @@ -4706,6 +4621,77 @@ class TinodeWeb extends React.Component { } } + // Reactions to updates to the contact list. + tnMeContactUpdate(what, cont) { + if (what == "on" || what == "off") { + this.resetContactList(); + if (this.state.topicSelected == cont.topic) { + this.setState({topicSelectedOnline: (what === "on")}); + } + } else if (what == "read") { + this.resetContactList(); + } else if (what == "msg") { + // New message received + // Skip update if the topic is currently open, otherwise the badge will annoyingly flash. + if (this.this.topicSelected !== cont.topic) { + if (this.state.messageSounds) { + POP_SOUND.play(); + } + this.resetContactList(); + } else if (document.hidden && this.state.messageSounds) { + POP_SOUND.play(); + } + } else if (what == "recv") { + // Explicitly ignoring "recv" -- it causes no visible updates to contact list. + } else if (what == "gone" || what == "unsub") { + // Topic deleted or user unsubscribed. Remove topic from view. + // If the currently selected topic is gone, clear the selection. + if (this.state.topicSelected == cont.topic) { + this.handleTopicSelected(null); + } + // Redraw without the deleted topic. + this.resetContactList(); + } else if (what == "acs") { + // Permissions changed. If it's for the currently selected topic, + // update the views. + if (this.state.topicSelected == cont.topic) { + this.setState({topicSelectedAcs: cont.acs}); + } + } else if (what == "del") { + // messages deleted (hard or soft) -- update pill counter. + } else { + // TODO(gene): handle other types of notifications: + // * ua -- user agent changes (maybe display a pictogram for mobile/desktop). + // * upd -- topic 'public' updated, issue getMeta(). + console.log("Unsupported (yet) presence update:" + what + " in: " + cont.topic); + } + } + + tnMeSubsUpdated(unused) { + this.resetContactList(); + } + + resetContactList() { + let newState = { + chatList: [] + }; + this.state.tinode.getMeTopic().contacts((c) => { + newState.chatList.push(c); + if (this.state.topicSelected == c.topic) { + newState.topicSelected = c.online; + newState.topicSelectedAcs = c.acs; + } + }); + // Merge search results and chat list. + let merged = {}; + for (const c of newState.chatList.concat(this.state.searchResults)) { + merged[c.topic] = c; + } + newState.searchableContacts = Object.values(merged); + console.log("Resetting contact list 2", newState); + this.setState(newState); + } + // Sending "received" notifications tnData(data) { let topic = this.state.tinode.getTopic(data.topic); @@ -4720,7 +4706,7 @@ class TinodeWeb extends React.Component { /* Fnd topic: find contacts by tokens */ tnInitFind() { - var fnd = this.state.tinode.getFndTopic(); + let fnd = this.state.tinode.getFndTopic(); if (fnd.isSubscribed()) { this.setState({contactsSearchQuery: ''}); this.tnFndSubsUpdated(); @@ -4733,12 +4719,15 @@ class TinodeWeb extends React.Component { } tnFndSubsUpdated() { - var contacts = []; + let contacts = []; // Don't attempt to create P2P topics which already exist. Server will reject the duplicates. this.state.tinode.getFndTopic().contacts((s) => { contacts.push(s); }); - this.setState({foundContacts: contacts}); + this.setState({ + searchResults: contacts, + searchableContacts: mergeContacts(contacts, this.state.chatList) + }); } /** Called when the user enters a contact into the contact search field in the NewAccount panel @@ -4746,7 +4735,7 @@ class TinodeWeb extends React.Component { */ handleSearchContacts(query) { var fnd = this.state.tinode.getFndTopic(); - this.setState({contactsSearchQuery: query == DEL_CHAR ? '' : query}); + this.setState({contactsSearchQuery: query == Tinode.DEL_CHAR ? '' : query}); fnd.setMeta({desc: {public: query}}).then((ctrl) => { return fnd.getMeta(fnd.startMetaQuery().withSub().build()); }).catch((err) => { @@ -4807,10 +4796,16 @@ class TinodeWeb extends React.Component { handleSendMessage(msg, promise, uploader) { let topic = this.state.tinode.getTopic(this.state.topicSelected); let dft = Drafty.parse(msg); + let mimeType, attachments; if (dft && !Drafty.isPlainText(dft)) { msg = dft; + mimeType = Drafty.getContentType(); + if (Drafty.hasAttachments(data)) { + attachments = []; + Drafty.attachments(data, (val) => { attachments.push(val); }); + } } - msg = topic.createMessage(msg); + msg = topic.createMessage(msg, false, mimeType, attachments); msg._uploader = uploader; if (promise) { @@ -5124,6 +5119,7 @@ class TinodeWeb extends React.Component { errorText={this.state.errorText} errorLevel={this.state.errorLevel} topicSelected={this.state.topicSelected} + chatList={this.state.chatList} credMethod={this.state.credMethod} credCode={this.state.credCode} @@ -5152,7 +5148,7 @@ class TinodeWeb extends React.Component { onInitFind={this.tnInitFind} contactsSearchQuery={this.state.contactsSearchQuery} - foundContacts={this.state.foundContacts} + searchResults={this.state.searchResults} onSearchContacts={this.handleSearchContacts} showContextMenu={this.handleShowContextMenu} /> @@ -5186,7 +5182,7 @@ class TinodeWeb extends React.Component { connected={this.state.connected} displayMobile={this.state.displayMobile} topic={this.state.topicSelected} - foundContacts={this.state.foundContacts} + searchableContacts={this.state.searchableContacts} myUserId={this.state.myUserId} errorText={this.state.errorText} errorLevel={this.state.errorLevel}