mirror of
https://github.com/amark/gun.git
synced 2025-11-24 06:25:58 +00:00
lots of new features and UI improvements
This commit is contained in:
parent
074b2d130a
commit
c74ac11750
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>Iris Chat</title>
|
<title>Iris Chat</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||||
<link rel="stylesheet" type="text/css" href="./style.css">
|
<link rel="stylesheet" type="text/css" href="./style.css">
|
||||||
<link rel="shortcut icon" href="./favicon.ico">
|
<link rel="shortcut icon" href="./favicon.ico">
|
||||||
@ -14,22 +15,51 @@
|
|||||||
<div class="user-info"></div>
|
<div class="user-info"></div>
|
||||||
<div class="chat-list">
|
<div class="chat-list">
|
||||||
<div class="chat-item new">
|
<div class="chat-item new">
|
||||||
New chat
|
Start new chat
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="main">
|
<section class="main">
|
||||||
<div class="online-user-list"></div>
|
<div id="topbar"></div>
|
||||||
<div class="message-list" id="message-list" style="display: none"></div>
|
<div class="main-view" id="message-list" style="display: none"></div>
|
||||||
<div class="message-list" id="new-chat">
|
<div id="not-seen-by-them" style="display: none">
|
||||||
<p><input id="paste-chat-link" type="text" placeholder="Paste someone's chat link"></p>
|
<p>The other person hasn't seen this chat yet. Give them your chat link through some other channel:</p>
|
||||||
<p><button class="copy-chat-link">Copy your chat link</button></p>
|
<p><button class="copy-chat-link">Copy your chat link</button></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-form">
|
<div class="main-view" id="new-chat">
|
||||||
|
<p><input id="paste-chat-link" type="text" placeholder="Paste someone's chat link"></p>
|
||||||
|
<p><button class="copy-chat-link">Copy your chat link</button></p>
|
||||||
|
<p>Give your chat link to someone you want to chat with!</p>
|
||||||
|
</div>
|
||||||
|
<div class="main-view" id="settings" style="display: none">
|
||||||
|
<h3>Profile</h3>
|
||||||
|
<p>
|
||||||
|
Your name:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<input id="settings-name" placeholder="Your name">
|
||||||
|
</p>
|
||||||
|
<h3>Account</h3>
|
||||||
|
<p>
|
||||||
|
<button>Log out</button>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button>Switch account</button>
|
||||||
|
</p>
|
||||||
|
<h3>Private key</h3>
|
||||||
|
<p>
|
||||||
|
Private key is used to log in to your account. Keep your private key safe!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button id="download-private-key">Download private key</button>
|
||||||
|
<button id="copy-private-key">Copy private key</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="message-form" style="display:none">
|
||||||
<form autocomplete="off">
|
<form autocomplete="off">
|
||||||
<input id="new-msg" type="text" placeholder="Type a message">
|
<input id="new-msg" type="text" placeholder="Type a message">
|
||||||
<button>Send</button>
|
<button>▶</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -40,186 +70,7 @@
|
|||||||
<script src="/gun/sea.js"></script>
|
<script src="/gun/sea.js"></script>
|
||||||
<script src="/gun/nts.js"></script>
|
<script src="/gun/nts.js"></script>
|
||||||
<script src="./irisLib.js"></script>
|
<script src="./irisLib.js"></script>
|
||||||
<script>
|
<script src="./script.js"></script>
|
||||||
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;
|
|
||||||
gun.user().auth(key);
|
|
||||||
myIdenticon = $(new irisLib.Attribute({type:'keyID', value: key.pub}).identicon({width:40}));
|
|
||||||
$(".user-info").append(myIdenticon);
|
|
||||||
irisLib.Chat.getChats(gun, key, addChat);
|
|
||||||
irisLib.Chat.setOnline(gun, true);
|
|
||||||
var chatWith = getUrlParameter('chatWith');
|
|
||||||
if (chatWith) {
|
|
||||||
addChat(chatWith);
|
|
||||||
showChat(pub);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#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);
|
|
||||||
|
|
||||||
function showNewChat() {
|
|
||||||
$(".message-form").hide();
|
|
||||||
$('#message-list').hide();
|
|
||||||
$('#new-chat').show();
|
|
||||||
$(".online-user-list").empty();
|
|
||||||
$(".online-user-list").text('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);
|
|
||||||
});
|
|
||||||
|
|
||||||
function showChat(pub) {
|
|
||||||
if (!pub || !Object.prototype.hasOwnProperty.call(chats, pub)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
activeChat = pub;
|
|
||||||
$(".chat-item").toggleClass('active', false);
|
|
||||||
$('.chat-item[data-pub="' + pub +'"]').toggleClass('active', true);
|
|
||||||
$("#message-list").empty();
|
|
||||||
$("#message-list").show();
|
|
||||||
$("#new-chat").hide();
|
|
||||||
$(".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('');
|
|
||||||
});
|
|
||||||
$(".online-user-list").empty();
|
|
||||||
$(".online-user-list").append(chats[pub].identicon.clone());
|
|
||||||
var msgs = Object.values(chats[pub].messages);
|
|
||||||
msgs.forEach(addMessage);
|
|
||||||
$('#message-list').scrollTop($('#message-list')[0].scrollHeight - $('#message-list')[0].clientHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addMessage(msg) {
|
|
||||||
var msgEl = $(
|
|
||||||
'<div class="msg"><div class="text">' + msg.text + '</div><div class="time">' +
|
|
||||||
formatDate(msg.time) + '</div></div>'
|
|
||||||
);
|
|
||||||
msgEl.toggleClass('our', msg.selfAuthored ? true : false);
|
|
||||||
msgEl.data('time', msg.time);
|
|
||||||
$("#message-list").append(msgEl); // TODO: jquery insertAfter element with smaller timestamp
|
|
||||||
var sorted = $(".msg").sort((a, b) => $(a).data('time') - $(b).data('time')); // TODO: more efficient sort
|
|
||||||
$("#message-list").append(sorted);
|
|
||||||
$('#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 = $('<div class="chat-item"><span class="latest"></span></div>');
|
|
||||||
el.attr('data-pub', pub);
|
|
||||||
chats[pub] = new irisLib.Chat({gun, key, participants: pub, onMessage: (msg, info) => {
|
|
||||||
chats[pub].messages = chats[pub].messages || [];
|
|
||||||
msg.selfAuthored = info.selfAuthored;
|
|
||||||
chats[pub].messages[msg.time] = msg;
|
|
||||||
msg.time = new Date(msg.time);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
if (activeChat === pub) {
|
|
||||||
addMessage(msg);
|
|
||||||
}
|
|
||||||
}});
|
|
||||||
chats[pub].identicon = $(new irisLib.Attribute({type: 'keyID', value: pub}).identicon({width:40}));
|
|
||||||
el.prepend(chats[pub].identicon);
|
|
||||||
el.click(() => showChat(pub));
|
|
||||||
$(".chat-list").append(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -11413,7 +11413,7 @@
|
|||||||
}
|
}
|
||||||
var sheet = document.createElement('style');
|
var sheet = document.createElement('style');
|
||||||
sheet.id = elementId;
|
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);
|
document.body.appendChild(sheet);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -11421,6 +11421,57 @@
|
|||||||
isNode: isNode$2
|
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) {
|
var pnglib = createCommonjsModule(function (module) {
|
||||||
/**
|
/**
|
||||||
* A handy class to calculate color values.
|
* A handy class to calculate color values.
|
||||||
@ -11995,8 +12046,12 @@
|
|||||||
|
|
||||||
|
|
||||||
Attribute.prototype.identicon = function identicon$$1() {
|
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?
|
util$1.injectCss(); // some other way that is not called on each identicon generation?
|
||||||
|
|
||||||
var div = document.createElement('div');
|
var div = document.createElement('div');
|
||||||
@ -12012,11 +12067,13 @@
|
|||||||
var identicon$$1 = new identicon(hash, { width: options.width, format: 'svg' });
|
var identicon$$1 = new identicon(hash, { width: options.width, format: 'svg' });
|
||||||
img.src = 'data:image/svg+xml;base64,' + identicon$$1.toString();
|
img.src = 'data:image/svg+xml;base64,' + identicon$$1.toString();
|
||||||
|
|
||||||
|
if (options.showType) {
|
||||||
var name = document.createElement('span');
|
var name = document.createElement('span');
|
||||||
name.className = 'iris-distance';
|
name.className = 'iris-distance';
|
||||||
name.style.fontSize = options.width > 50 ? options.width / 4 + 'px' : '10px';
|
name.style.fontSize = options.width > 50 ? options.width / 4 + 'px' : '10px';
|
||||||
name.textContent = this.type.slice(0, 5);
|
name.textContent = this.type.slice(0, 5);
|
||||||
div.appendChild(name);
|
div.appendChild(name);
|
||||||
|
}
|
||||||
|
|
||||||
div.appendChild(img);
|
div.appendChild(img);
|
||||||
|
|
||||||
@ -12761,57 +12818,6 @@
|
|||||||
return Message;
|
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.
|
* An Iris Contact, such as person, organization or group. More abstractly speaking: an Identity.
|
||||||
*
|
*
|
||||||
@ -12900,6 +12906,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get sent Messages
|
* Get sent Messages
|
||||||
|
* @param {Object} index
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -12910,6 +12917,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get received Messages
|
* Get received Messages
|
||||||
|
* @param {Object} index
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -13258,6 +13266,9 @@
|
|||||||
* Messages are encrypted and chat ids obfuscated, but it is possible to guess
|
* 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.
|
* 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}
|
* @param {Object} options {key, gun, onMessage, participants}
|
||||||
* @example https://github.com/irislib/iris-lib/blob/master/__tests__/chat.js
|
* @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;
|
return Chat;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
|
|||||||
313
examples/iris-chat/script.js
Normal file
313
examples/iris-chat/script.js
Normal file
@ -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 = $('<span class="name"></span>');
|
||||||
|
if (chats[pub].name) {
|
||||||
|
nameEl.text(chats[pub].name);
|
||||||
|
nameEl.show();
|
||||||
|
}
|
||||||
|
$("#topbar").append(chats[pub].identicon.clone());
|
||||||
|
$("#topbar").append(nameEl);
|
||||||
|
$("#topbar").append($('<small class="last-seen"></small>'));
|
||||||
|
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 = $(
|
||||||
|
'<div class="msg"><div class="text">' +
|
||||||
|
msg.text + '</div>' +
|
||||||
|
'<div class="time"><span class="seen">✔</span> ' + formatDate(msg.time) + '</div></div>'
|
||||||
|
);
|
||||||
|
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 = $('<div class="chat-item"><span class="name"></span> <small class="latest"></small></div>');
|
||||||
|
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');
|
||||||
|
};
|
||||||
@ -22,17 +22,21 @@ input {
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
background: #ddd;
|
background: #ddd;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover, button:active, button:focus {
|
button:hover, button:active, button:focus {
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.iris-identicon {
|
.iris-identicon {
|
||||||
margin-right: 15px !important;
|
margin-right: 15px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-info .iris-identicon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.chat {
|
.chat {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -56,14 +60,20 @@ button:hover, button:active, button:focus {
|
|||||||
|
|
||||||
/*----- Main------- */
|
/*----- Main------- */
|
||||||
|
|
||||||
.online-user-list {
|
#topbar {
|
||||||
flex:1;
|
flex:1;
|
||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
max-height:60px;
|
max-height:60px;
|
||||||
padding: 10px 15px;
|
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;
|
flex:1;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
@ -72,16 +82,43 @@ button:hover, button:active, button:focus {
|
|||||||
|
|
||||||
.msg {
|
.msg {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
margin: 0 90px 15px 0;
|
margin-bottom: 5px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
border-radius: 15px;
|
||||||
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1);
|
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;
|
border-radius: 0px 15px 15px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg.our {
|
.msg.our {
|
||||||
background-color: #d7f7c5;
|
background-color: #d7f7c5;
|
||||||
border-radius: 15px 0px 15px 15px;
|
margin-left: 90px;
|
||||||
margin: 0 0px 15px 90px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg .time {
|
.msg .time {
|
||||||
@ -107,8 +144,12 @@ button:hover, button:active, button:focus {
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-form button {
|
.message-form button, .message-form button:hover, .message-form button:active, .message-form button:focus {
|
||||||
flex: none;
|
flex: none;
|
||||||
|
color: #999;
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: 30px;
|
||||||
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*----- Sidebar------- */
|
/*----- Sidebar------- */
|
||||||
@ -138,12 +179,12 @@ button:hover, button:active, button:focus {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-item .latest {
|
small {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(0, 0, 0, 0.45);
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-item.active {
|
.chat-item.active, .chat-item.active:hover {
|
||||||
background: #eaeaea;
|
background: #eaeaea;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,14 +193,22 @@ button:hover, button:active, button:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-item.new {
|
.chat-item.new {
|
||||||
background-color: #9ee8ff;
|
|
||||||
padding: 22px 15px;
|
padding: 22px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-item.new:hover {
|
|
||||||
background-color: #99e1f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
#new-chat input {
|
#new-chat input {
|
||||||
width: 200px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user