From c74ac117503ce8124f9276497ab6ae5c276628ec Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Fri, 10 Jan 2020 22:27:10 +0200 Subject: [PATCH] lots of new features and UI improvements --- examples/iris-chat/index.html | 225 +++++------------------- examples/iris-chat/irisLib.js | 145 +++++++++------- examples/iris-chat/script.js | 313 ++++++++++++++++++++++++++++++++++ examples/iris-chat/style.css | 79 +++++++-- 4 files changed, 502 insertions(+), 260 deletions(-) create mode 100644 examples/iris-chat/script.js diff --git a/examples/iris-chat/index.html b/examples/iris-chat/index.html index 1b8a5f78..8886aca0 100644 --- a/examples/iris-chat/index.html +++ b/examples/iris-chat/index.html @@ -3,6 +3,7 @@ Iris Chat + @@ -14,22 +15,51 @@
- New chat + Start new chat
-
- -
-

+
+ + -
+
+

+

+

Give your chat link to someone you want to chat with!

+
+ +
@@ -40,186 +70,7 @@ - + diff --git a/examples/iris-chat/irisLib.js b/examples/iris-chat/irisLib.js index 8f63f08e..1b4c053f 100644 --- a/examples/iris-chat/irisLib.js +++ b/examples/iris-chat/irisLib.js @@ -11413,7 +11413,7 @@ } var sheet = document.createElement('style'); sheet.id = elementId; - sheet.innerHTML = '\n .iris-identicon * {\n box-sizing: border-box;\n }\n\n .iris-identicon {\n vertical-align: middle;\n margin: auto;\n border-radius: 50%;\n text-align: center;\n display: inline-block;\n position: relative;\n margin: auto;\n max-width: 100%;\n }\n\n .iris-distance {\n z-index: 2;\n position: absolute;\n left:0%;\n top:2px;\n width: 100%;\n text-align: right;\n color: #fff;\n text-shadow: 0 0 1px #000;\n font-size: 75%;\n line-height: 75%;\n font-weight: bold;\n }\n\n .iris-pie {\n border-radius: 50%;\n position: absolute;\n top: 0;\n left: 0;\n box-shadow: 0px 0px 0px 0px #82FF84;\n padding-bottom: 100%;\n max-width: 100%;\n -webkit-transition: all 0.2s ease-in-out;\n -moz-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n }\n\n .iris-card {\n padding: 10px;\n background-color: #f7f7f7;\n color: #777;\n border: 1px solid #ddd;\n display: flex;\n flex-direction: row;\n overflow: hidden;\n }\n\n .iris-card a {\n -webkit-transition: color 150ms;\n transition: color 150ms;\n text-decoration: none;\n color: #337ab7;\n }\n\n .iris-card a:hover, .iris-card a:active {\n text-decoration: underline;\n color: #23527c;\n }\n\n .iris-pos {\n color: #3c763d;\n }\n\n .iris-neg {\n color: #a94442;\n }\n\n .iris-identicon img {\n position: absolute;\n top: 0;\n left: 0;\n max-width: 100%;\n border-radius: 50%;\n border-color: transparent;\n border-style: solid;\n }'; + sheet.innerHTML = '\n .iris-identicon * {\n box-sizing: border-box;\n }\n\n .iris-identicon {\n vertical-align: middle;\n border-radius: 50%;\n text-align: center;\n display: inline-block;\n position: relative;\n max-width: 100%;\n }\n\n .iris-distance {\n z-index: 2;\n position: absolute;\n left:0%;\n top:2px;\n width: 100%;\n text-align: right;\n color: #fff;\n text-shadow: 0 0 1px #000;\n font-size: 75%;\n line-height: 75%;\n font-weight: bold;\n }\n\n .iris-pie {\n border-radius: 50%;\n position: absolute;\n top: 0;\n left: 0;\n box-shadow: 0px 0px 0px 0px #82FF84;\n padding-bottom: 100%;\n max-width: 100%;\n -webkit-transition: all 0.2s ease-in-out;\n -moz-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n }\n\n .iris-card {\n padding: 10px;\n background-color: #f7f7f7;\n color: #777;\n border: 1px solid #ddd;\n display: flex;\n flex-direction: row;\n overflow: hidden;\n }\n\n .iris-card a {\n -webkit-transition: color 150ms;\n transition: color 150ms;\n text-decoration: none;\n color: #337ab7;\n }\n\n .iris-card a:hover, .iris-card a:active {\n text-decoration: underline;\n color: #23527c;\n }\n\n .iris-pos {\n color: #3c763d;\n }\n\n .iris-neg {\n color: #a94442;\n }\n\n .iris-identicon img {\n position: absolute;\n top: 0;\n left: 0;\n max-width: 100%;\n border-radius: 50%;\n border-color: transparent;\n border-style: solid;\n }'; document.body.appendChild(sheet); }, @@ -11421,6 +11421,57 @@ isNode: isNode$2 }; + // 19.1.2.1 Object.assign(target, source, ...) + + + + + + + var $assign = Object.assign; + + // should work with symbols and should have deterministic property order (V8 bug) + var _objectAssign = !$assign || _fails(function () { + var A = {}; + var B = {}; + // eslint-disable-next-line no-undef + var S = Symbol(); + var K = 'abcdefghijklmnopqrst'; + A[S] = 7; + K.split('').forEach(function (k) { B[k] = k; }); + return $assign({}, A)[S] != 7 || Object.keys($assign({}, B)).join('') != K; + }) ? function assign(target, source) { // eslint-disable-line no-unused-vars + var T = _toObject(target); + var aLen = arguments.length; + var index = 1; + var getSymbols = _objectGops.f; + var isEnum = _objectPie.f; + while (aLen > index) { + var S = _iobject(arguments[index++]); + var keys = getSymbols ? _objectKeys(S).concat(getSymbols(S)) : _objectKeys(S); + var length = keys.length; + var j = 0; + var key; + while (length > j) { + key = keys[j++]; + if (!_descriptors || isEnum.call(S, key)) T[key] = S[key]; + } + } return T; + } : $assign; + + // 19.1.3.1 Object.assign(target, source) + + + _export(_export.S + _export.F, 'Object', { assign: _objectAssign }); + + var assign = _core.Object.assign; + + var assign$1 = createCommonjsModule(function (module) { + module.exports = { "default": assign, __esModule: true }; + }); + + var _Object$assign = unwrapExports(assign$1); + var pnglib = createCommonjsModule(function (module) { /** * A handy class to calculate color values. @@ -11995,8 +12046,12 @@ Attribute.prototype.identicon = function identicon$$1() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { width: 50 }; + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + options = _Object$assign({ + width: 50, + showType: true + }, options); util$1.injectCss(); // some other way that is not called on each identicon generation? var div = document.createElement('div'); @@ -12012,11 +12067,13 @@ var identicon$$1 = new identicon(hash, { width: options.width, format: 'svg' }); img.src = 'data:image/svg+xml;base64,' + identicon$$1.toString(); - var name = document.createElement('span'); - name.className = 'iris-distance'; - name.style.fontSize = options.width > 50 ? options.width / 4 + 'px' : '10px'; - name.textContent = this.type.slice(0, 5); - div.appendChild(name); + if (options.showType) { + var name = document.createElement('span'); + name.className = 'iris-distance'; + name.style.fontSize = options.width > 50 ? options.width / 4 + 'px' : '10px'; + name.textContent = this.type.slice(0, 5); + div.appendChild(name); + } div.appendChild(img); @@ -12761,57 +12818,6 @@ return Message; }(); - // 19.1.2.1 Object.assign(target, source, ...) - - - - - - - var $assign = Object.assign; - - // should work with symbols and should have deterministic property order (V8 bug) - var _objectAssign = !$assign || _fails(function () { - var A = {}; - var B = {}; - // eslint-disable-next-line no-undef - var S = Symbol(); - var K = 'abcdefghijklmnopqrst'; - A[S] = 7; - K.split('').forEach(function (k) { B[k] = k; }); - return $assign({}, A)[S] != 7 || Object.keys($assign({}, B)).join('') != K; - }) ? function assign(target, source) { // eslint-disable-line no-unused-vars - var T = _toObject(target); - var aLen = arguments.length; - var index = 1; - var getSymbols = _objectGops.f; - var isEnum = _objectPie.f; - while (aLen > index) { - var S = _iobject(arguments[index++]); - var keys = getSymbols ? _objectKeys(S).concat(getSymbols(S)) : _objectKeys(S); - var length = keys.length; - var j = 0; - var key; - while (length > j) { - key = keys[j++]; - if (!_descriptors || isEnum.call(S, key)) T[key] = S[key]; - } - } return T; - } : $assign; - - // 19.1.3.1 Object.assign(target, source) - - - _export(_export.S + _export.F, 'Object', { assign: _objectAssign }); - - var assign = _core.Object.assign; - - var assign$1 = createCommonjsModule(function (module) { - module.exports = { "default": assign, __esModule: true }; - }); - - var _Object$assign = unwrapExports(assign$1); - /** * An Iris Contact, such as person, organization or group. More abstractly speaking: an Identity. * @@ -12900,6 +12906,7 @@ /** * Get sent Messages + * @param {Object} index * @param {Object} options */ @@ -12910,6 +12917,7 @@ /** * Get received Messages + * @param {Object} index * @param {Object} options */ @@ -13258,6 +13266,9 @@ * Messages are encrypted and chat ids obfuscated, but it is possible to guess * who are communicating with each other by looking at Gun timestamps and subscriptions. * + * options.onMessage callback is not guaranteed to receive messages ordered by timestamp. + * You should sort them in the presentation layer. + * * @param {Object} options {key, gun, onMessage, participants} * @example https://github.com/irislib/iris-lib/blob/master/__tests__/chat.js */ @@ -13555,6 +13566,24 @@ }); }; + /** + * In order to receive messages from others, this method must be called for newly created + * users that have not started a chat with an existing user yet. + * + * It saves the user's key.epub (public key for encryption) into their gun user space, + * so others can find it and write encrypted messages to them. + * + * If you start a chat with an existing user, key.epub is saved automatically and you don't need + * to call this method. + */ + + + Chat.initUser = function initUser(gun, key) { + var user = gun.user(); + user.auth(key); + user.put({ epub: key.epub }); + }; + return Chat; }(); diff --git a/examples/iris-chat/script.js b/examples/iris-chat/script.js new file mode 100644 index 00000000..837d9f8f --- /dev/null +++ b/examples/iris-chat/script.js @@ -0,0 +1,313 @@ +var gun = Gun([location.origin + '/gun', 'https://gun-us.herokuapp.com/gun']); +var chat = gun.get('converse/' + location.hash.slice(1)); +var chats = {}; +var activeChat; + +var getKey = new Promise(resolve => { + var pair = localStorage.getItem('chatKeyPair'); + if (pair) { + resolve(JSON.parse(pair)); + } else { + Gun.SEA.pair(pair => { + localStorage.setItem('chatKeyPair', JSON.stringify(pair)); + resolve(pair); + }); + } +}); +var key, myIdenticon; +getKey.then(k => { + key = k; + irisLib.Chat.initUser(gun, key); + myIdenticon = $(new irisLib.Attribute({type:'keyID', value: key.pub}).identicon({width:40, showType: false})); + $(".user-info").append(myIdenticon); + myIdenticon.click(showSettings); + irisLib.Chat.getChats(gun, key, addChat); + irisLib.Chat.setOnline(gun, true); + var chatWith = getUrlParameter('chatWith'); + if (chatWith) { + addChat(chatWith); + showChat(chatWith); + window.history.pushState({}, "Iris Chat", "/"+window.location.href.substring(window.location.href.lastIndexOf('/') + 1).split("?")[0]); // remove param + } + gun.user().get('profile').get('name').on(name => { + if (name && typeof name === 'string') { + var el = $('#settings-name'); + if (!el.is(':focus')) { + $('#settings-name').val(name); + } + } + }); +}); + +$('#paste-chat-link').on('keyup paste', event => { + var val = $(event.target).val(); + if (val.length < 30 || val.indexOf('chatWith') === -1) { + return; + } + var s = val.split('?'); + if (s.length !== 2) { return; } + $(event.target).val(''); + var pub = getUrlParameter('chatWith', s[1]); + addChat(pub); + showChat(pub); +}); + +$('.chat-item.new').click(showNewChat); + +$('#settings-name').on('keyup paste', event => { + var name = $(event.target).val().trim(); + gun.user().get('profile').get('name').put(name); +}); + +function hideStuff() { + $('.chat-item').toggleClass('active', false); + $('.main-view').hide(); + $('#not-seen-by-them').hide(); + $(".message-form").hide(); + $("#topbar").empty(); +} + +function showSettings() { + hideStuff(); + $('#topbar').text('Settings'); + $('#settings').show(); +} + +function showNewChat() { + hideStuff(); + $('.chat-item.new').toggleClass('active', true); + $('#new-chat').show(); + $("#topbar").text('Start new chat'); +} + +showNewChat(); + +$('.copy-chat-link').click(event => { + copyToClipboard('https://chat.iris.to/?chatWith=' + key.pub); + var t = $(event.target); + var originalText = t.text(); + var originalWidth = t.width(); + t.width(originalWidth); + t.text('Copied'); + setTimeout(() => { + t.text(originalText); + t.css('width', ''); + }, 2000); +}); + +$('#copy-private-key').click(event => { + copyToClipboard(JSON.stringify(key)); + var t = $(event.target); + var originalText = t.text(); + var originalWidth = t.width(); + t.width(originalWidth); + t.text('Copied'); + setTimeout(() => { + t.text(originalText); + t.css('width', ''); + }, 2000); +}); + +$('#download-private-key').click(downloadKey); + +function showChat(pub) { + if (!pub || !Object.prototype.hasOwnProperty.call(chats, pub)) { + return; + } + activeChat = pub; + hideStuff(); + $('.chat-item[data-pub="' + pub +'"]').toggleClass('active', true); + $("#message-list").empty(); + $("#message-list").show(); + $(".message-form").show(); + $(".message-form form").off('submit'); + $(".message-form form").on('submit', event => { + event.preventDefault(); + var text = $('#new-msg').val(); + if (!text.length) { return; } + chats[pub].send(text); + $('#new-msg').val(''); + }); + var nameEl = $(''); + if (chats[pub].name) { + nameEl.text(chats[pub].name); + nameEl.show(); + } + $("#topbar").append(chats[pub].identicon.clone()); + $("#topbar").append(nameEl); + $("#topbar").append($('')); + var msgs = Object.values(chats[pub].messages); + msgs.forEach(addMessage); + sortMessagesByTime(); + lastSeenTimeChanged(pub); + $('#message-list').scrollTop($('#message-list')[0].scrollHeight - $('#message-list')[0].clientHeight); + chats[pub].setMyMsgsLastSeenTime(); + function setOnlineStatus() { + var online = chats[pub].online; + if (activeChat === pub) { + if (online.isOnline) { + $('#topbar .last-seen').text('online'); + } else if (online.lastActive) { + $('#topbar .last-seen').text('last seen ' + formatDate(new Date(online.lastActive * 1000))); + } + } + } + if (!chats[pub].online) { + chats[pub].online = {}; + irisLib.Chat.getOnline(gun, pub, (online) => { + chats[pub].online = online; + setOnlineStatus(); + }); + } + setOnlineStatus(); +} + +function sortChatsByLatest() { + var sorted = $(".chat-item").sort((a, b) => $(b).data('latestTime') - $(a).data('latestTime')); + $(".chat-list").append(sorted); +} + +function sortMessagesByTime() { + var sorted = $(".msg").sort((a, b) => $(a).data('time') - $(b).data('time')); + $("#message-list").append(sorted); +} + +function addMessage(msg) { + var msgEl = $( + '
' + + msg.text + '
' + + '
' + formatDate(msg.time) + '
' + ); + msgEl.toggleClass('our', msg.selfAuthored ? true : false); + msgEl.toggleClass('their', msg.selfAuthored ? false : true); + msgEl.data('time', msg.time); + $("#message-list").append(msgEl); // TODO: jquery insertAfter element with smaller timestamp + $('#message-list').scrollTop($('#message-list')[0].scrollHeight - $('#message-list')[0].clientHeight); +} + +function addChat(pub) { + if (!pub || Object.prototype.hasOwnProperty.call(chats, pub)) { + return; + } + var el = $('
'); + el.attr('data-pub', pub); + chats[pub] = new irisLib.Chat({gun, key, participants: pub, onMessage: (msg, info) => { + msg.selfAuthored = info.selfAuthored; + chats[pub].messages[msg.time] = msg; + msg.time = new Date(msg.time); + if (!info.selfAuthored && msg.time > chats[pub].theirLastSeenTime) { + chats[pub].theirLastSeenTime = msg.time; + lastSeenTimeChanged(pub); + } + if (!chats[pub].latest || msg.time > chats[pub].latest.time) { + chats[pub].latest = msg; + var text = msg.text.length > 30 ? msg.text.slice(0,30) + '...' : msg.text; + el.find('.latest').text(text); + el.data('latestTime', msg.time); + sortChatsByLatest(); + } + if (activeChat === pub) { + addMessage(msg); + sortMessagesByTime(); // this is slow if message history is loaded while chat active + if (chats[pub].latest.time === msg.time) { + chats[pub].setMyMsgsLastSeenTime(); + } + } + }}); + chats[pub].messages = chats[pub].messages || []; + chats[pub].identicon = $(new irisLib.Attribute({type: 'keyID', value: pub}).identicon({width:40, showType: false})); + gun.user(pub).get('profile').get('name').on(name => { + if (name && typeof name === 'string') { + chats[pub].name = name; + el.find('.name').text(name); + if (pub === activeChat) { + $('#topbar .name').text(name); + } + } + }); + el.prepend(chats[pub].identicon); + el.click(() => showChat(pub)); + $(".chat-list").append(el); + chats[pub].getTheirMsgsLastSeenTime(time => { + chats[pub].theirLastSeenTime = new Date(time); + lastSeenTimeChanged(pub); + }); +} + +function lastSeenTimeChanged(pub) { + if (pub === activeChat) { + if (chats[pub].theirLastSeenTime) { + $('#not-seen-by-them').hide(); + $('.msg.our').each(function() { + var el = $(this); + if (el.data('time') <= chats[pub].theirLastSeenTime) { + el.find('.seen').show(); + } + }); + // set seen msgs + } else { + $('#not-seen-by-them').show(); + } + } +} + +/* Helpers */ + +function formatDate(date) { + var s = date.toISOString().split('T'); + return s[0] + ' ' + s[1].slice(0,5); +} + +function copyToClipboard(text) { + if (window.clipboardData && window.clipboardData.setData) { + // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible. + return clipboardData.setData("Text", text); + } + else if (document.queryCommandSupported && document.queryCommandSupported("copy")) { + var textarea = document.createElement("textarea"); + textarea.textContent = text; + textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge. + document.body.appendChild(textarea); + textarea.select(); + try { + return document.execCommand("copy"); // Security exception may be thrown by some browsers. + } + catch (ex) { + console.warn("Copy to clipboard failed.", ex); + return false; + } + finally { + document.body.removeChild(textarea); + } + } +} + +function getUrlParameter(sParam, sParams) { + var sPageURL = sParams || window.location.search.substring(1), + sURLVariables = sPageURL.split('&'), + sParameterName, + i; + + for (i = 0; i < sURLVariables.length; i++) { + sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] === sParam) { + return sParameterName[1] === undefined ? true : decodeURIComponent(sParameterName[1]); + } + } +}; + +function download(filename, data, type, charset, href) { + var hiddenElement; + if (charset == null) { + charset = 'utf-8'; + } + hiddenElement = document.createElement('a'); + hiddenElement.href = href || ("data:" + type + ";charset=" + charset + "," + (encodeURI(data))); + hiddenElement.target = '_blank'; + hiddenElement.download = filename; + return hiddenElement.click(); +}; + +function downloadKey() { + return download('iris_private_key.txt', JSON.stringify(key), 'text/csv', 'utf-8'); +}; diff --git a/examples/iris-chat/style.css b/examples/iris-chat/style.css index 9e7db601..188ebe8a 100644 --- a/examples/iris-chat/style.css +++ b/examples/iris-chat/style.css @@ -22,17 +22,21 @@ input { button { background: #ddd; + cursor: pointer; } button:hover, button:active, button:focus { background: #ccc; - cursor: pointer; } .iris-identicon { margin-right: 15px !important; } +.user-info .iris-identicon { + cursor: pointer; +} + .chat { display: flex; flex-direction: row; @@ -56,14 +60,20 @@ button:hover, button:active, button:focus { /*----- Main------- */ -.online-user-list { +#topbar { flex:1; background-color: #efefef; max-height:60px; padding: 10px 15px; + border-bottom: 1px solid rgba(0,0,0,.08); } -.message-list { +#topbar .name { + margin-right: 15px; + display: none; +} + +.main-view { flex:1; overflow-y: scroll; padding: 10px 15px; @@ -72,16 +82,43 @@ button:hover, button:active, button:focus { .msg { background-color: #ffffff; - margin: 0 90px 15px 0; + margin-bottom: 5px; padding: 8px 10px; + border-radius: 15px; box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1); +} + +.msg .seen { + color: #4fc3f7; + display: none; + user-select: none; +} + +.msg.their { + margin-right: 90px; +} + +.msg.their + .msg.our { + margin-top: 15px; + border-radius: 15px 0px 15px 15px; +} + +.msg.their:first-of-type { + border-radius: 0px 15px 15px 15px; +} + +.msg.our:first-of-type { + border-radius: 15px 0px 15px 15px; +} + +.msg.our + .msg.their { + margin-top: 15px; border-radius: 0px 15px 15px 15px; } .msg.our { background-color: #d7f7c5; - border-radius: 15px 0px 15px 15px; - margin: 0 0px 15px 90px; + margin-left: 90px; } .msg .time { @@ -107,8 +144,12 @@ button:hover, button:active, button:focus { width: auto; } -.message-form button { +.message-form button, .message-form button:hover, .message-form button:active, .message-form button:focus { flex: none; + color: #999; + background-color: transparent; + font-size: 30px; + padding: 5px; } /*----- Sidebar------- */ @@ -138,12 +179,12 @@ button:hover, button:active, button:focus { cursor: pointer; } -.chat-item .latest { +small { font-size: 12px; - color: rgba(0, 0, 0, 0.45); + color: #777; } -.chat-item.active { +.chat-item.active, .chat-item.active:hover { background: #eaeaea; } @@ -152,14 +193,22 @@ button:hover, button:active, button:focus { } .chat-item.new { - background-color: #9ee8ff; padding: 22px 15px; } -.chat-item.new:hover { - background-color: #99e1f7; -} - #new-chat input { width: 200px; } + +#not-seen-by-them { + padding: 10px 15px; + background-color: #9ee8ff; +} + +#not-seen-by-them button { + background: #fff; +} + +#not-seen-by-them button:hover, #not-seen-by-them button:active, #not-seen-by-them button:focus { + background: #eee; +}