add iris-lib to this repo, fix channel issue

This commit is contained in:
Martti Malmi 2022-06-16 13:08:06 +03:00
parent 80f634896d
commit 5f29071063
34 changed files with 3720 additions and 41 deletions

View File

@ -45,12 +45,15 @@
"dependencies": {
"@zxing/library": "^0.18.6",
"autolinker": "^3.14.3",
"fuse.js": "^6.6.2",
"gun": "mmalmi/gun#old",
"htm": "^3.1.0",
"iris-lib": "^0.0.158",
"identicon.js": "^2.3.3",
"jquery": "^3.6.0",
"jsxstyle": "^2.5.1",
"lodash": "^4.17.21",
"preact": "^10.5.14",
"preact-custom-element": "^4.2.1",
"preact-render-to-string": "^5.1.19",
"preact-router": "^3.2.1",
"preact-scroll-viewport": "^0.2.0",

View File

@ -1,7 +1,7 @@
import {translate as t} from './Translation.js';
import $ from 'jquery';
import _ from 'lodash';
import iris from 'iris-lib';
import iris from './iris-lib';
import Autolinker from 'autolinker';
const emojiRegex = /[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]+/ug;

View File

@ -3,7 +3,7 @@ import Session from './Session.js';
import { route } from 'preact-router';
import State from './State.js';
import _ from 'lodash';
import iris from 'iris-lib';
import iris from './iris-lib';
import Gun from 'gun';
import $ from 'jquery';

View File

@ -4,7 +4,7 @@ import Notifications from './Notifications.js';
import Helpers from './Helpers.js';
import PeerManager from './PeerManager.js';
import { route } from 'preact-router';
import iris from 'iris-lib';
import iris from './iris-lib';
import _ from 'lodash';
import Fuse from "./lib/fuse.basic.esm.min";

View File

@ -1,6 +1,5 @@
import Gun from 'gun';
import 'gun/sea';
import 'gun/nts.js';
import 'gun/lib/open';
import 'gun/lib/radix';
import 'gun/lib/radisk';
@ -9,7 +8,7 @@ import 'gun/lib/rindexed';
import _ from 'lodash';
import PeerManager from './PeerManager.js';
import iris from 'iris-lib';
import iris from './iris-lib';
import Helpers from './Helpers.js';
const State = {

View File

@ -3,7 +3,7 @@ import Helpers from '../Helpers.js';
import { html } from 'htm/preact';
import {translate as t} from '../Translation.js';
import $ from 'jquery';
import iris from 'iris-lib';
import iris from '../iris-lib';
class CopyButton extends Component {
copy(e, copyStr) {

View File

@ -9,7 +9,7 @@ import SafeImg from './SafeImg.js';
import Torrent from './Torrent.js';
import $ from 'jquery';
import EmojiButton from '../lib/emoji-button.js';
import iris from 'iris-lib';
import iris from '../iris-lib';
import SearchBox from './SearchBox';
import MessageForm from './MessageForm';

View File

@ -8,7 +8,7 @@ import { route } from 'preact-router';
import Identicon from './Identicon.js';
import SearchBox from './SearchBox.js';
import Icons from '../Icons.js';
import iris from 'iris-lib';
import iris from '../iris-lib';
import {Link} from "preact-router/match";
import logo from '../../assets/img/icon128.png';

View File

@ -2,7 +2,7 @@ import Component from '../BaseComponent';
import { html } from 'htm/preact';
import State from '../State.js';
import SafeImg from './SafeImg.js';
import iris from 'iris-lib';
import iris from '../iris-lib';
import $ from 'jquery';
class Identicon extends Component {

View File

@ -4,7 +4,7 @@ import { html } from 'htm/preact';
import Session from '../Session.js';
import Torrent from './Torrent.js';
import Autolinker from 'autolinker';
import iris from 'iris-lib';
import iris from '../iris-lib';
import $ from 'jquery';
import State from '../State.js';

View File

@ -1,7 +1,7 @@
import { Component } from 'preact';
import State from '../State.js';
import Session from '../Session.js';
import iris from 'iris-lib';
import iris from '../iris-lib';
function twice(f) {
f();

View File

@ -10,7 +10,7 @@ import Session from '../Session.js';
import Torrent from './Torrent.js';
import Icons from '../Icons.js';
import Autolinker from 'autolinker';
import iris from 'iris-lib';
import iris from '../iris-lib';
import $ from 'jquery';
import {Helmet} from "react-helmet";
import Notifications from '../Notifications';

View File

@ -0,0 +1,178 @@
/*eslint no-useless-escape: "off", camelcase: "off" */
import Identicon from 'identicon.js';
import util from './util';
const UNIQUE_ID_VALIDATORS = {
email: /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i,
bitcoin: /^[13][a-km-zA-HJ-NP-Z0-9]{26,33}$/,
bitcoin_address: /^[13][a-km-zA-HJ-NP-Z0-9]{26,33}$/,
ip: /^(([1-9]?\d|1\d\d|2[0-5][0-5]|2[0-4]\d)\.){3}([1-9]?\d|1\d\d|2[0-5][0-5]|2[0-4]\d)$/,
ipv6: /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/,
gpg_fingerprint: null,
gpg_keyid: null,
google_oauth2: null,
tel: /^\d{7,}$/,
phone: /^\d{7,}$/,
keyID: null,
url: /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi,
account: /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i,
uuid: /[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}/
};
/**
* A simple key-value pair with helper functions.
*
* Constructor: new Attribute(value), new Attribute(type, value) or new Attribute({type, value})
*/
class Attribute {
/**
* @param {string} a
* @param {string} b
*/
constructor(a, b) {
if (typeof a === `object`) {
if (typeof a.value !== `string`) { throw new Error(`param1.value must be a string, got ${typeof a.value}: ${JSON.stringify(a.value)}`); }
if (typeof a.type !== `string`) { throw new Error(`param1.type must be a string, got ${typeof a.type}: ${JSON.stringify(a.type)}`); }
b = a.value;
a = a.type;
}
if (typeof a !== `string`) { throw new Error(`First param must be a string, got ${typeof a}: ${JSON.stringify(a)}`); }
if (!a.length) { throw new Error(`First param string is empty`); }
if (b) {
if (typeof b !== `string`) { throw new Error(`Second parameter must be a string, got ${typeof b}: ${JSON.stringify(b)}`); }
if (!b.length) { throw new Error(`Second param string is empty`); }
this.type = a;
this.value = b;
} else {
this.value = a;
const t = Attribute.guessTypeOf(this.value);
if (t) {
this.type = t;
} else {
throw new Error(`Type of attribute was omitted and could not be guessed`);
}
}
}
/**
* @returns {Attribute} uuid
*/
static getUuid() {
const b = a => a ? (a ^ Math.random() * 16 >> a / 4).toString(16) : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, b);
return new Attribute(`uuid`, b());
}
/**
* @returns {Object} an object with attribute types as keys and regex patterns as values
*/
static getUniqueIdValidators() {
return UNIQUE_ID_VALIDATORS;
}
/**
* @param {string} type attribute type
* @returns {boolean} true if the attribute type is unique
*/
static isUniqueType(type) {
return Object.keys(UNIQUE_ID_VALIDATORS).indexOf(type) > -1;
}
/**
* @returns {boolean} true if the attribute type is unique
*/
isUniqueType() {
return Attribute.isUniqueType(this.type);
}
/**
* @param {string} value guess type of this attribute value
* @returns {string} type of attribute value or undefined
*/
static guessTypeOf(value) {
for (const key in UNIQUE_ID_VALIDATORS) {
if (value.match(UNIQUE_ID_VALIDATORS[key])) {
return key;
}
}
}
/**
* @param {Attribute} a
* @param {Attribute} b
* @returns {boolean} true if params are equal
*/
static equals(a, b) {
return a.equals(b);
}
/**
* @param {Attribute} a attribute to compare to
* @returns {boolean} true if attribute matches param
*/
equals(a) {
return a && this.type === a.type && this.value === a.value;
}
/**
* @returns {string} uri - `${encodeURIComponent(this.value)}:${encodeURIComponent(this.type)}`
* @example
* user%20example.com:email
*/
uri() {
return `${encodeURIComponent(this.value)}:${encodeURIComponent(this.type)}`;
}
identiconXml(options = {}) {
return util.getHash(`${encodeURIComponent(this.type)}:${encodeURIComponent(this.value)}`, `hex`)
.then(hash => {
const identicon = new Identicon(hash, {width: options.width, format: `svg`});
return identicon.toString(true);
});
}
identiconSrc(options = {}) {
return util.getHash(`${encodeURIComponent(this.type)}:${encodeURIComponent(this.value)}`, `hex`)
.then(hash => {
const identicon = new Identicon(hash, {width: options.width, format: `svg`});
return `data:image/svg+xml;base64,${identicon.toString()}`;
});
}
/**
* Generate a visually recognizable representation of the attribute
* @param {object} options {width}
* @returns {HTMLElement} identicon div element
*/
identicon(options = {}) {
options = Object.assign({
width: 50,
showType: true,
}, options);
util.injectCss(); // some other way that is not called on each identicon generation?
const div = document.createElement(`div`);
div.className = `iris-identicon`;
div.style.width = `${options.width}px`;
div.style.height = `${options.width}px`;
const img = document.createElement(`img`);
img.alt = ``;
img.width = options.width;
img.height = options.width;
this.identiconSrc(options).then(src => img.src = src);
if (options.showType) {
const 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);
return div;
}
}
export default Attribute;

1228
src/js/iris-lib/Channel.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,163 @@
import Gun from 'gun'; // eslint-disable-line no-unused-vars
// eslint-disable-line no-unused-vars
/**
* Gun object collection that provides tools for indexing and search. Decentralize everything!
*
* If opt.class is passed, object.serialize() and opt.class.deserialize() must be defined.
*
* Supports search from multiple indexes.
* For example, retrieve message feed from your own index and your friends' indexes.
*
* TODO: aggregation
* TODO: example
* TODO: scrollable and stretchable "search result window"
* @param {Object} opt {gun, class, indexes = [], askPeers = true, name = class.name}
*/
class Collection {
constructor(opt = {}) {
if (!opt.gun) {
throw new Error(`Missing opt.gun`);
}
if (!(opt.class || opt.name)) {
throw new Error(`You must supply either opt.name or opt.class`);
}
this.class = opt.class;
this.serializer = opt.serializer;
if (this.class && !this.class.deserialize && !this.serializer) {
throw new Error(`opt.class must have deserialize() method or opt.serializer must be defined`);
}
this.name = opt.name || opt.class.name;
this.gun = opt.gun;
this.indexes = opt.indexes || [];
this.indexer = opt.indexer;
this.askPeers = typeof opt.askPeers === `undefined` ? true : opt.askPeers;
}
/**
* @return {String} id of added object, which can be used for collection.get(id)
*/
put(object, opt = {}) {
let data = object;
if (this.serializer) {
data = this.serializer.serialize(object);
} if (this.class) {
data = object.serialize();
}
// TODO: optionally use gun hash table
let node;
if (opt.id || data.id) {
node = this.gun.get(this.name).get(`id`).get(opt.id || data.id).put(data); // TODO: use .top()
} else if (object.getId) {
node = this.gun.get(this.name).get(`id`).get(object.getId()).put(data);
} else {
node = this.gun.get(this.name).get(`id`).set(data);
}
this._addToIndexes(data, node);
return data.id || Gun.node.soul(node) || node._.link;
}
async _addToIndexes(serializedObject, node) {
if (Gun.node.is(serializedObject)) {
serializedObject = await serializedObject.open();
}
const addToIndex = (indexName, indexKey) => {
this.gun.get(this.name).get(indexName).get(indexKey).put(node);
};
if (this.indexer) {
const customIndexes = await this.indexer(serializedObject);
const customIndexKeys = Object.keys(customIndexes);
for (let i = 0;i < customIndexKeys;i++) {
const key = customIndexKeys[i];
addToIndex(key, customIndexes[key]);
}
}
for (let i = 0;i < this.indexes.length; i++) {
const indexName = this.indexes[i];
if (Object.prototype.hasOwnProperty.call(serializedObject, indexName)) {
addToIndex(indexName, serializedObject[indexName]);
}
}
}
// TODO: method for terminating the query
// TODO: query ttl. https://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html
/**
* @param {Object} opt {callback, id, selector, limit, orderBy}
*/
get(opt = {}) {
if (!opt.callback) { return; }
let results = 0;
const matcher = (data, id, node) => {
if (!data) { return; }
if (opt.limit && results++ >= opt.limit) {
return; // TODO: terminate query
}
if (opt.selector) { // TODO: deep compare selector object?
const keys = Object.keys(opt.selector);
for (let i = 0;i < keys.length;i++) {
const key = keys[i];
if (!Object.prototype.hasOwnProperty.call(data, key)) { return; }
let v1, v2;
if (opt.caseSensitive === false) {
v1 = data[key].toLowerCase();
v2 = opt.selector[key].toLowerCase();
} else {
v1 = data[key];
v2 = opt.selector[key];
}
if (v1 !== v2) { return; }
}
}
if (opt.query) { // TODO: use gun.get() lt / gt operators
const keys = Object.keys(opt.query);
for (let i = 0;i < keys.length;i++) {
const key = keys[i];
if (!Object.prototype.hasOwnProperty.call(data, key)) { return; }
let v1, v2;
if (opt.caseSensitive === false) {
v1 = data[key].toLowerCase();
v2 = opt.query[key].toLowerCase();
} else {
v1 = data[key];
v2 = opt.query[key];
}
if (v1.indexOf(v2) !== 0) { return; }
}
}
if (this.serializer) {
opt.callback(this.serializer.deserialize(data, {id, gun: node.$}));
} else if (this.class) {
opt.callback(this.class.deserialize(data, {id, gun: node.$}));
} else {
opt.callback(data);
}
};
if (opt.id) {
opt.limit = 1;
this.gun.get(this.name).get(`id`).get(opt.id).on(matcher);
return;
}
let indexName = `id`;
if (opt.orderBy && this.indexes.indexOf(opt.orderBy) > -1) {
indexName = opt.orderBy;
}
// TODO: query from indexes
this.gun.get(this.name).get(indexName).map().on(matcher); // TODO: limit .open recursion
if (this.askPeers) {
this.gun.get(`trustedIndexes`).on((val, key) => {
this.gun.user(key).get(this.name).get(indexName).map().on(matcher);
});
}
}
delete() {
// gun.unset()
}
}
export default Collection;

339
src/js/iris-lib/Contact.js Normal file
View File

@ -0,0 +1,339 @@
import Identicon from 'identicon.js';
import Attribute from './Attribute';
import util from './util';
/**
* An Iris Contact, such as person, organization or group. More abstractly speaking: an Identity.
*
* Usually you don't create Contacts yourself, but get them
* from SocialNetwork methods such as get() and search().
*/
class Contact {
/**
* @param {Object} gun node where the Contact data lives
*/
constructor(gun, linkTo) {
this.gun = gun;
this.linkTo = linkTo;
}
static create(gun, data, index) {
if (!data.linkTo && !data.attrs) {
throw new Error(`You must specify either data.linkTo or data.attrs`);
}
if (data.linkTo && !data.attrs) {
const linkTo = new Attribute(data.linkTo);
data.attrs = {};
if (!Object.prototype.hasOwnProperty.call(data.attrs, linkTo.uri())) {
data.attrs[linkTo.uri()] = linkTo;
}
} else {
data.linkTo = Contact.getLinkTo(data.attrs);
}
const uri = data.linkTo.uri();
const attrs = gun.top(`${uri}/attrs`).put(data.attrs);
delete data['attrs'];
gun.put(data);
gun.get(`attrs`).put(attrs);
return new Contact(gun, uri, index);
}
static getLinkTo(attrs) {
const mva = Contact.getMostVerifiedAttributes(attrs);
const keys = Object.keys(mva);
let linkTo;
for (let i = 0;i < keys.length;i++) {
if (keys[i] === `keyID`) {
linkTo = mva[keys[i]].attribute;
break;
} else if (Attribute.isUniqueType(keys[i])) {
linkTo = mva[keys[i]].attribute;
}
}
return linkTo;
}
static getMostVerifiedAttributes(attrs) {
const mostVerifiedAttributes = {};
Object.keys(attrs).forEach(k => {
const a = attrs[k];
const keyExists = Object.keys(mostVerifiedAttributes).indexOf(a.type) > -1;
a.verifications = isNaN(a.verifications) ? 1 : a.verifications;
a.unverifications = isNaN(a.unverifications) ? 0 : a.unverifications;
if (a.verifications * 2 > a.unverifications * 3 && (!keyExists || a.verifications - a.unverifications > mostVerifiedAttributes[a.type].verificationScore)) {
mostVerifiedAttributes[a.type] = {
attribute: a,
verificationScore: a.verifications - a.unverifications
};
if (a.verified) {
mostVerifiedAttributes[a.type].verified = true;
}
}
});
return mostVerifiedAttributes;
}
static async getAttrs(identity) {
const attrs = await util.loadGunDepth(identity.get(`attrs`), 2);
if (attrs && attrs['_'] !== undefined) {
delete attrs['_'];
}
return attrs || {};
}
getId() {
return this.linkTo.value;
}
/**
* Get sent Messages
* @param {Object} index
* @param {Object} options
*/
sent(index, options) {
index._getSentMsgs(this, options);
}
/**
* Get received Messages
* @param {Object} index
* @param {Object} options
*/
received(index, options) {
index._getReceivedMsgs(this, options);
}
/**
* @param {string} attribute attribute type
* @returns {string} most verified value of the param type
*/
async verified(attribute) {
const attrs = await Contact.getAttrs(this.gun).then();
const mva = Contact.getMostVerifiedAttributes(attrs);
return Object.prototype.hasOwnProperty.call(mva, attribute) ? mva[attribute].attribute.value : undefined;
}
/**
* @returns {HTMLElement} profile card html element describing the identity
*/
profileCard() {
const card = document.createElement(`div`);
card.className = `iris-card`;
const identicon = this.identicon({width: 60});
identicon.style.order = 1;
identicon.style.flexShrink = 0;
identicon.style.marginRight = `15px`;
const details = document.createElement(`div`);
details.style.padding = `5px`;
details.style.order = 2;
details.style.flexGrow = 1;
const linkEl = document.createElement(`span`);
const links = document.createElement(`small`);
card.appendChild(identicon);
card.appendChild(details);
details.appendChild(linkEl);
details.appendChild(links);
this.gun.on(async data => {
if (!data) {
return;
}
const attrs = await Contact.getAttrs(this.gun);
const linkTo = await this.gun.get(`linkTo`).then();
const link = `https://iris.to/#/identities/${linkTo.type}/${linkTo.value}`;
const mva = Contact.getMostVerifiedAttributes(attrs);
linkEl.innerHTML = `<a href="${link}">${(mva.type && mva.type.attribute.value) || (mva.nickname && mva.nickname.attribute.value) || `${linkTo.type}:${linkTo.value}`}</a><br>`;
linkEl.innerHTML += `<small>Received: <span class="iris-pos">+${data.receivedPositive || 0}</span> / <span class="iris-neg">-${data.receivedNegative || 0}</span></small><br>`;
links.innerHTML = ``;
Object.keys(attrs).forEach(k => {
const a = attrs[k];
if (a.link) {
links.innerHTML += `${a.type}: <a href="${a.link}">${a.value}</a> `;
}
});
});
/*
const template = ```
<tr ng-repeat="result in ids.list" id="result{$index}" ng-hide="!result.linkTo" ui-sref="identities.show({ type: result.linkTo.type, value: result.linkTo.value })" class="search-result-row" ng-class="{active: result.active}">
<td class="gravatar-col"><identicon id="result" border="3" width="46" positive-score="result.pos" negative-score="result.neg"></identicon></td>
<td>
<span ng-if="result.distance == 0" class="label label-default pull-right">rootContact</span>
<span ng-if="result.distance > 0" ng-bind="result.distance | ordinal" class="label label-default pull-right"></span>
<a ng-bind-html="result.name|highlight:query.term" ui-sref="identities.show({ type: result.linkTo.type, value: result.linkTo.value })"></a>
<small ng-if="!result.name" class="list-group-item-text">
<span ng-bind-html="result[0][0]|highlight:query.term"></span>
</small><br>
<small>
<span ng-if="result.nickname && result.name != result.nickname" ng-bind-html="result.nickname|highlight:query.term" class="mar-right10"></span>
<span ng-if="result.email" class="mar-right10">
<span class="glyphicon glyphicon-envelope"></span> <span ng-bind-html="result.email|highlight:query.term"></span>
</span>
<span ng-if="result.facebook" class="mar-right10">
<span class="fa fa-facebook"></span> <span ng-bind-html="result.facebook|highlight:query.term"></span>
</span>
<span ng-if="result.twitter" class="mar-right10">
<span class="fa fa-twitter"></span> <span ng-bind-html="result.twitter|highlight:query.term"></span>
</span>
<span ng-if="result.googlePlus" class="mar-right10">
<span class="fa fa-google-plus"></span> <span ng-bind-html="result.googlePlus|highlight:query.term"></span>
</span>
<span ng-if="result.bitcoin" class="mar-right10">
<span class="fa fa-bitcoin"></span> <span ng-bind-html="result.bitcoin|highlight:query.term"></span>
</span>
</small>
</td>
</tr>
```;*/
return card;
}
/**
* Appends an identity search widget to the given HTMLElement
* @param {HTMLElement} parentElement element where the search widget is added and event listener attached
* @param {Index} index index root to use for search
*/
static appendSearchWidget(parentElement, index) {
const form = document.createElement(`form`);
const input = document.createElement(`input`);
input.type = `text`;
input.placeholder = `Search`;
input.id = `irisSearchInput`;
form.innerHTML += `<div id="irisSearchResults"></div>`;
const searchResults = document.createElement(`div`);
parentElement.appendChild(form);
form.appendChild(input);
form.appendChild(searchResults);
input.addEventListener(`keyup`, async () => {
const r = await index.search(input.value);
searchResults.innerHTML = ``;
r.sort((a, b) => {return a.trustDistance - b.trustDistance;});
r.forEach(i => {
searchResults.appendChild(i.profileCard());
});
});
return form;
}
static _ordinal(n) {
if (n === 0) {
return '';
}
const s = [`th`, `st`, `nd`, `rd`];
const v = n % 100;
return n + (s[(v - 20) % 10] || s[v] || s[0]);
}
/**
* @param {Object} options {width: 50, border: 4, showDistance: true, outerGlow: false}
* @returns {HTMLElement} identicon element that can be appended to DOM
*/
identicon(options = {}) {
options = Object.assign({
width: 50,
border: 4,
showDistance: true,
outerGlow: false,
}, options);
util.injectCss(); // some other way that is not called on each identicon generation?
const identicon = document.createElement(`div`);
identicon.className = `iris-identicon`;
identicon.style.width = `${options.width}px`;
identicon.style.height = `${options.width}px`;
const pie = document.createElement(`div`);
pie.className = `iris-pie`;
pie.style.width = `${options.width}px`;
const img = document.createElement(`img`);
img.alt = ``;
img.width = options.width;
img.height = options.width;
img.style.borderWidth = `${options.border}px`;
let distance;
if (options.border) {
distance = document.createElement(`span`);
distance.className = `iris-distance`;
distance.style.fontSize = options.width > 50 ? `${options.width / 4}px` : `10px`;
identicon.appendChild(distance);
}
identicon.appendChild(pie);
identicon.appendChild(img);
function setPie(data) {
if (!data) {
return;
}
// Define colors etc
let bgColor = `rgba(0,0,0,0.2)`;
let bgImage = `none`;
let transform = ``;
if (options.outerGlow) {
let boxShadow = `0px 0px 0px 0px #82FF84`;
if (data.receivedPositive > data.receivedNegative * 20) {
boxShadow = `0px 0px ${options.border * data.receivedPositive / 50}px 0px #82FF84`;
} else if (data.receivedPositive < data.receivedNegative * 3) {
boxShadow = `0px 0px ${options.border * data.receivedNegative / 10}px 0px #BF0400`;
}
pie.style.boxShadow = boxShadow;
}
if (data.receivedPositive + data.receivedNegative > 0) {
if (data.receivedPositive > data.receivedNegative) {
transform = `rotate(${((-data.receivedPositive / (data.receivedPositive + data.receivedNegative) * 360 - 180) / 2)}deg)`;
bgColor = `#A94442`;
bgImage = `linear-gradient(${data.receivedPositive / (data.receivedPositive + data.receivedNegative) * 360}deg, transparent 50%, #3C763D 50%), linear-gradient(0deg, #3C763D 50%, transparent 50%)`;
} else {
transform = `rotate(${((-data.receivedNegative / (data.receivedPositive + data.receivedNegative) * 360 - 180) / 2) + 180}deg)`;
bgColor = `#3C763D`;
bgImage = `linear-gradient(${data.receivedNegative / (data.receivedPositive + data.receivedNegative) * 360}deg, transparent 50%, #A94442 50%), linear-gradient(0deg, #A94442 50%, transparent 50%)`;
}
}
pie.style.backgroundColor = bgColor;
pie.style.backgroundImage = bgImage;
pie.style.transform = transform;
pie.style.opacity = (data.receivedPositive + data.receivedNegative) / 10 * 0.5 + 0.35;
if (options.showDistance) {
distance.textContent = typeof data.trustDistance === `number` ? Contact._ordinal(data.trustDistance) : ``;
}
}
function setIdenticonImg(data) {
util.getHash(`${encodeURIComponent(data.type)}:${encodeURIComponent(data.value)}`, `hex`)
.then(hash => {
const identiconImg = new Identicon(hash, {width: options.width, format: `svg`});
img.src = img.src || `data:image/svg+xml;base64,${identiconImg.toString()}`;
});
}
if (this.linkTo) {
setIdenticonImg(this.linkTo);
} else {
this.gun.get(`linkTo`).on(setIdenticonImg);
}
this.gun.on(setPie);
return identicon;
}
serialize() {
return this.gun;
}
static deserialize(data, opt) {
const linkTo = new Attribute({type: `uuid`, value: opt.id});
return new Contact(opt.gun, linkTo);
}
}
export default Contact;

144
src/js/iris-lib/Key.js Normal file
View File

@ -0,0 +1,144 @@
/*eslint no-useless-escape: "off", camelcase: "off" */
import util from './util';
import Gun from 'gun'; // eslint-disable-line no-unused-vars
import 'gun/sea';
// eslint-disable-line no-unused-vars
let myKey;
/**
* Key management utils. Wraps GUN's Gun.SEA. https://gun.eco/docs/Gun.SEA
*/
class Key {
/**
* Load private key from datadir/iris.key on node.js or from local storage 'iris.myKey' in browser.
*
* If the key does not exist, it is generated.
* @param {string} datadir directory to find key from. In browser, localStorage is used instead.
* @param {string} keyfile keyfile name (within datadir)
* @param {Object} fs node: require('fs'); browser: leave empty.
* @returns {Promise<Object>} keypair object
*/
static async getActiveKey(datadir = `.`, keyfile = `iris.key`, fs) {
if (myKey) {
return myKey;
}
if (fs) {
const privKeyFile = `${datadir}/${keyfile}`;
if (fs.existsSync(privKeyFile)) {
const f = fs.readFileSync(privKeyFile, `utf8`);
myKey = Key.fromString(f);
} else {
const newKey = await Key.generate();
myKey = myKey || newKey; // eslint-disable-line require-atomic-updates
fs.writeFileSync(privKeyFile, Key.toString(myKey));
fs.chmodSync(privKeyFile, 400);
}
if (!myKey) {
throw new Error(`loading default key failed - check ${datadir}/${keyfile}`);
}
} else {
const str = window.localStorage.getItem(`iris.myKey`);
if (str) {
myKey = Key.fromString(str);
} else {
const newKey = await Key.generate();
myKey = myKey || newKey; // eslint-disable-line require-atomic-updates
window.localStorage.setItem(`iris.myKey`, Key.toString(myKey));
}
if (!myKey) {
throw new Error(`loading default key failed - check localStorage iris.myKey`);
}
}
return myKey;
}
static getDefault(datadir = `.`, keyfile = `iris.key`) {
return Key.getActiveKey(datadir, keyfile);
}
static async getActivePub(datadir = `.`, keyfile = `iris.key`) {
const key = await Key.getActiveKey(datadir, keyfile);
return key.pub;
}
/**
*
*/
static setActiveKey(key, save = true, datadir = `.`, keyfile = `iris.key`, fs) {
myKey = key;
if (!save) return;
if (util.isNode) {
const privKeyFile = `${datadir}/${keyfile}`;
fs.writeFileSync(privKeyFile, Key.toString(myKey));
fs.chmodSync(privKeyFile, 400);
} else {
window.localStorage.setItem(`iris.myKey`, Key.toString(myKey));
}
}
/**
* Serialize key as JSON string
* @param {Object} key key to serialize
* @returns {String} JSON Web Key string
*/
static toString(key) {
return JSON.stringify(key);
}
/**
* Get keyID
* @param {Object} key key to get an id for. Currently just returns the public key string.
* @returns {String} public key string
*/
static getId(key) {
if (!(key && key.pub)) {
throw new Error(`missing param`);
}
return key.pub; // hack until GUN supports lookups by keyID
//return util.getHash(key.pub);
}
/**
* Get a keypair from a JSON string.
* @param {String} str key JSON
* @returns {Object} Gun.SEA keypair object
*/
static fromString(str) {
return JSON.parse(str);
}
/**
* Generate a new keypair
* @returns {Promise<Object>} Gun.SEA keypair object
*/
static generate() {
return Gun.SEA.pair();
}
/**
* Sign a message
* @param {String} msg message to sign
* @param {Object} pair signing keypair
* @returns {Promise<String>} signed message string
*/
static async sign(msg, pair) {
const sig = await Gun.SEA.sign(msg, pair);
return `a${sig}`;
}
/**
* Verify a signed message
* @param {String} msg message to verify
* @param {Object} pubKey public key of the signer
* @returns {Promise<String>} signature string
*/
static verify(msg, pubKey) {
return Gun.SEA.verify(msg.slice(1), pubKey);
}
}
export default Key;

View File

@ -0,0 +1,408 @@
/*jshint unused: false */
`use strict`;
import util from './util';
import Attribute from './Attribute';
import Key from './Key';
const errorMsg = `Invalid message:`;
class ValidationError extends Error {}
/**
* Signed message object. Your friends can index and relay your messages, while others can still verify that they were signed by you.
*
* Fields: signedData, signer (public key) and signature.
*
* signedData has an author, signer, type, time and optionally other fields.
*
* signature covers the utf8 string representation of signedData. Since messages are digitally signed, users only need to care about the message signer and not who relayed it or whose index it was found from.
*
* signer is the entity that verified its origin. In other words: message author and signer can be different entities, and only the signer needs to use Iris.
*
* For example, a crawler can import and sign other people's messages from Twitter. Only the users who trust the crawler will see the messages.
*
* "Rating" type messages, when added to an SocialNetwork, can add or remove Identities from the web of trust. Verification/unverification messages can add or remove Attributes from an Contact. Other types of messages such as social media "post" are just indexed by their author, recipient and time.
*
* Constructor: creates a message from the param obj.signedData that must contain at least the mandatory fields: author, recipient, type and time. You can use createRating() and createVerification() to automatically populate some of these fields and optionally sign the message.
* @param obj
*
* @example
* https://github.com/irislib/iris-lib/blob/master/__tests__/SignedMessage.js
*
* Rating message:
* {
* signedData: {
* author: {name:'Alice', key:'ABCD1234'},
* recipient: {name:'Bob', email:'bob@example.com'},
* type: 'rating',
* rating: 1,
* maxRating: 10,
* minRating: -10,
* text: 'Traded 1 BTC'
* },
* signer: 'ABCD1234',
* signature: '1234ABCD'
* }
*
* Verification message:
* {
* signedData: {
* author: {name:'Alice', key:'ABCD1234'},
* recipient: {
* name: 'Bob',
* email: ['bob@example.com', 'bob.saget@example.com'],
* bitcoin: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'
* },
* type: 'verification'
* },
* signer: 'ABCD1234',
* signature: '1234ABCD'
* }
*/
class SignedMessage {
constructor(obj) {
if (obj.signedData) {
this.signedData = obj.signedData;
}
if (obj.pubKey) {
this.pubKey = obj.pubKey;
}
if (obj.sig) {
if (typeof obj.sig !== `string`) {
throw new ValidationError(`SignedMessage signature must be a string`);
}
this.sig = obj.sig;
this.getHash();
}
this._validate();
}
static _getArray(authorOrRecipient) {
const arr = [];
const keys = Object.keys(authorOrRecipient);
for (let i = 0;i < keys.length;i++) {
const type = keys[i];
const value = authorOrRecipient[keys[i]];
if (typeof value === `string`) {
arr.push(new Attribute(type, value));
} else { // array
for (let j = 0;j < value.length;j++) {
const elementValue = value[j];
arr.push(new Attribute(type, elementValue));
}
}
}
return arr;
}
static _getIterable(authorOrRecipient) {
return {
*[Symbol.iterator]() {
const keys = Object.keys(authorOrRecipient);
for (let i = 0;i < keys.length;i++) {
const type = keys[i];
const value = authorOrRecipient[keys[i]];
if (typeof value === `string`) {
yield new Attribute(type, value);
} else { // array
for (let j = 0;j < value.length;j++) {
const elementValue = value[j];
yield new Attribute(type, elementValue);
}
}
}
}
};
}
/**
* @returns {object} Javascript iterator over author attributes
*/
getAuthorIterable() {
return SignedMessage._getIterable(this.signedData.author);
}
/**
* @returns {object} Javascript iterator over recipient attributes
*/
getRecipientIterable() {
return SignedMessage._getIterable(this.signedData.recipient);
}
/**
* @returns {array} Array containing author attributes
*/
getAuthorArray() {
return SignedMessage._getArray(this.signedData.author);
}
/**
* @returns {array} Array containing recipient attributes
*/
getRecipientArray() {
return this.signedData.recipient ? SignedMessage._getArray(this.signedData.recipient) : [];
}
/**
* @returns {string} SignedMessage signer keyID, i.e. base64 hash of public key
*/
getSignerKeyID() {
return this.pubKey; // hack until gun supports keyID lookups
//return util.getHash(this.pubKey);
}
_validate() {
if (!this.signedData) {throw new ValidationError(`${errorMsg} Missing signedData`);}
if (typeof this.signedData !== `object`) {throw new ValidationError(`${errorMsg} signedData must be an object`);}
const d = this.signedData;
if (!d.type) {throw new ValidationError(`${errorMsg} Missing type definition`);}
if (!d.author) {throw new ValidationError(`${errorMsg} Missing author`);}
if (typeof d.author !== `object`) {throw new ValidationError(`${errorMsg} Author must be object`);}
if (Array.isArray(d.author)) {throw new ValidationError(`${errorMsg} Author must not be an array`);}
if (Object.keys(d.author).length === 0) {throw new ValidationError(`${errorMsg} Author empty`);}
if (this.pubKey) {
this.signerKeyHash = this.getSignerKeyID();
}
for (const attr in d.author) {
const t = typeof d.author[attr];
if (t !== `string`) {
if (Array.isArray(d.author[attr])) {
for (let i = 0;i < d.author[attr].length;i++) {
if (typeof d.author[attr][i] !== `string`) {throw new ValidationError(`${errorMsg} Author attribute must be string, got ${attr}: [${d.author[attr][i]}]`);}
if (d.author[attr][i].length === 0) {
throw new ValidationError(`${errorMsg} author ${attr} in array[${i}] is empty`);
}
}
} else {
throw new ValidationError(`${errorMsg} Author attribute must be string or array, got ${attr}: ${d.author[attr]}`);
}
}
if (attr === `keyID`) {
if (t !== `string`) {throw new ValidationError(`${errorMsg} Author keyID must be string, got ${t}`);}
if (this.signerKeyHash && d.author[attr] !== this.signerKeyHash) {throw new ValidationError(`${errorMsg} If message has a keyID author, it must be signed by the same key`);}
}
}
if (d.recipient) {
if (typeof d.recipient !== `object`) {throw new ValidationError(`${errorMsg} Recipient must be object`);}
if (Array.isArray(d.recipient)) {throw new ValidationError(`${errorMsg} Recipient must not be an array`);}
if (Object.keys(d.recipient).length === 0) {throw new ValidationError(`${errorMsg} Recipient empty`);}
for (const attr in d.recipient) {
const t = typeof d.recipient[attr];
if (t !== `string`) {
if (Array.isArray(d.recipient[attr])) {
for (let i = 0;i < d.recipient[attr].length;i++) {
if (typeof d.recipient[attr][i] !== `string`) {throw new ValidationError(`${errorMsg} Recipient attribute must be string, got ${attr}: [${d.recipient[attr][i]}]`);}
if (d.recipient[attr][i].length === 0) {
throw new ValidationError(`${errorMsg} recipient ${attr} in array[${i}] is empty`);
}
}
} else {
throw new ValidationError(`${errorMsg} Recipient attribute must be string or array, got ${attr}: ${d.recipient[attr]}`);
}
}
}
}
if (!(d.time || d.timestamp)) {throw new ValidationError(`${errorMsg} Missing time field`);}
if (!Date.parse(d.time || d.timestamp)) {throw new ValidationError(`${errorMsg} Invalid time field`);}
if (d.type === `rating`) {
if (isNaN(d.rating)) {throw new ValidationError(`${errorMsg} Invalid rating`);}
if (isNaN(d.maxRating)) {throw new ValidationError(`${errorMsg} Invalid maxRating`);}
if (isNaN(d.minRating)) {throw new ValidationError(`${errorMsg} Invalid minRating`);}
if (d.rating > d.maxRating) {throw new ValidationError(`${errorMsg} Rating is above maxRating`);}
if (d.rating < d.minRating) {throw new ValidationError(`${errorMsg} Rating is below minRating`);}
if (typeof d.context !== `string` || !d.context.length) {throw new ValidationError(`${errorMsg} Rating messages must have a context field`);}
}
if (d.type === `verification` || d.type === `unverification`) {
if (d.recipient.length < 2) {throw new ValidationError(`${errorMsg} At least 2 recipient attributes are needed for a connection / disconnection. Got: ${d.recipient}`);}
}
return true;
}
/**
* @returns {boolean} true if message has a positive rating
*/
isPositive() {
return this.signedData.type === `rating` && this.signedData.rating > (this.signedData.maxRating + this.signedData.minRating) / 2;
}
/**
* @returns {boolean} true if message has a negative rating
*/
isNegative() {
return this.signedData.type === `rating` && this.signedData.rating < (this.signedData.maxRating + this.signedData.minRating) / 2;
}
/**
* @returns {boolean} true if message has a neutral rating
*/
isNeutral() {
return this.signedData.type === `rating` && this.signedData.rating === (this.signedData.maxRating + this.signedData.minRating) / 2;
}
/**
* @param {Object} key Gun.SEA keypair to sign the message with
*/
async sign(key) {
this.sig = await Key.sign(this.signedData, key);
this.pubKey = key.pub;
await this.getHash();
return true;
}
/**
* Create an iris message. SignedMessage time is automatically set. If signingKey is specified and author omitted, signingKey will be used as author.
* @param {Object} signedData message data object including author, recipient and other possible attributes
* @param {Object} signingKey optionally, you can set the key to sign the message with
* @returns {Promise<SignedMessage>} message
*/
static async create(signedData, signingKey) {
if (!signedData.author && signingKey) {
signedData.author = {keyID: Key.getId(signingKey)};
}
signedData.time = signedData.time || (new Date()).toISOString();
const m = new SignedMessage({signedData});
if (signingKey) {
await m.sign(signingKey);
}
return m;
}
/**
* Create an verification message. SignedMessage signedData's type and time are automatically set. Recipient must be set. If signingKey is specified and author omitted, signingKey will be used as author.
* @returns {Promise<Object>} message object promise
*/
static createVerification(signedData, signingKey) {
signedData.type = `verification`;
return SignedMessage.create(signedData, signingKey);
}
/**
* Create an rating message. SignedMessage signedData's type, maxRating, minRating, time and context are set automatically. Recipient and rating must be set. If signingKey is specified and author omitted, signingKey will be used as author.
* @returns {Promise<Object>} message object promise
*/
static createRating(signedData, signingKey) {
signedData.type = `rating`;
signedData.context = signedData.context || `iris`;
signedData.maxRating = signedData.maxRating || 10;
signedData.minRating = signedData.minRating || -10;
return SignedMessage.create(signedData, signingKey);
}
/**
* @param {Index} index index to look up the message author from
* @returns {Contact} message author identity
*/
getAuthor(index) {
for (const a of this.getAuthorIterable()) {
if (a.isUniqueType()) {
return index.getContacts(a);
}
}
}
/**
* @param {Index} index index to look up the message recipient from
* @returns {Contact} message recipient identity or undefined
*/
getRecipient(index) {
if (!this.signedData.recipient) {
return undefined;
}
for (const a of this.getRecipientIterable()) {
if (a.isUniqueType()) {
return index.getContacts(a);
}
}
}
/**
* @returns {string} base64 sha256 hash of message
*/
async getHash() {
if (this.sig && !this.hash) {
this.hash = await util.getHash(this.sig);
}
return this.hash;
}
getId() {
return this.getHash();
}
static async fromSig(obj) {
if (!obj.sig) {
throw new Error(`Missing signature in object:`, obj);
}
if (!obj.pubKey) {
throw new Error(`Missing pubKey in object:`);
}
const signedData = await Key.verify(obj.sig, obj.pubKey);
const o = {signedData, sig: obj.sig, pubKey: obj.pubKey};
return new SignedMessage(o);
}
/**
* @return {boolean} true if message signature is valid. Otherwise throws ValidationError.
*/
async verify() {
if (!this.pubKey) {
throw new ValidationError(`${errorMsg} SignedMessage has no .pubKey`);
}
if (!this.sig) {
throw new ValidationError(`${errorMsg} SignedMessage has no .sig`);
}
this.signedData = await Key.verify(this.sig, this.pubKey);
if (!this.signedData) {
throw new ValidationError(`${errorMsg} Invalid signature`);
}
if (this.hash) {
if (this.hash !== (await util.getHash(this.sig))) {
throw new ValidationError(`${errorMsg} Invalid message hash`);
}
} else {
this.getHash();
}
return true;
}
/**
* @returns {string}
*/
serialize() {
return {sig: this.sig, pubKey: this.pubKey};
}
toString() {
return JSON.stringify(this.serialize());
}
/**
* @returns {Promise<SignedMessage>}
*/
static async deserialize(s) {
return SignedMessage.fromSig(s);
}
static async fromString(s) {
return SignedMessage.fromSig(JSON.parse(s));
}
/**
*
*/
static async setReaction(gun, msg, reaction) {
const hash = await msg.getHash();
gun.get(`reactions`).get(hash).put(reaction);
gun.get(`reactions`).get(hash).put(reaction);
gun.get(`messagesByHash`).get(hash).get(`reactions`).get(this.rootContact.value).put(reaction);
gun.get(`messagesByHash`).get(hash).get(`reactions`).get(this.rootContact.value).put(reaction);
}
}
export default SignedMessage;

23
src/js/iris-lib/Social.js Normal file
View File

@ -0,0 +1,23 @@
class Social {
groupGet(path, callback, groupNode = State.local.get('groups').get('follows')) {
const follows = {};
groupNode.map((isFollowing, user) => {
if (follows[user] && follows[user] === isFollowing) { return; }
follows[user] = isFollowing;
if (isFollowing) { // TODO: callback on unfollow, for unsubscribe
const node = _.reduce(path.split('/'), (sum, s) => sum.get(decodeURIComponent(s)), State.public.user(user));
callback(node, user);
}
});
}
groupMap(path, callback, groupNode = State.local.get('groups').get('follows')) {
groupGet(path, (node, from) => node.map((...args) => callback(...args, from)), groupNode);
}
groupOn(path, callback, groupNode = State.local.get('groups').get('follows')) {
groupGet(path, (node, from) => node.on((...args) => callback(...args, from)), groupNode);
}
}
export default Social;

View File

@ -0,0 +1,65 @@
import register from 'preact-custom-element';
import {Component} from 'preact';
import {html} from 'htm/preact';
import util from '../util';
class CopyButton extends Component {
copyToClipboard(text) {
if (window.clipboardData && window.clipboardData.setData) {
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
return window.clipboardData.setData('Text', text);
}
else if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
const 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);
}
}
}
copy(e, str) {
this.copyToClipboard(str);
const tgt = e.target;
this.originalWidth = this.originalWidth || tgt.offsetWidth + 1;
tgt.style.width = this.originalWidth;
this.setState({copied: true});
clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.setState({copied: false}), 2000);
}
onClick(e) {
e.preventDefault();
const str = typeof this.props.str === 'function' ? this.props.str() : this.props.str;
if (navigator.share && util.isMobile && !this.props['not-shareable']) {
navigator.share({url: str, title: this.props.title}).catch(err => {
console.error('share failed', err);
this.copy(e, str);
});
} else {
this.copy(e, str);
}
}
render() {
const text = this.state.copied ? (this.props['copied-text'] || 'Copied') : (this.props.text || 'Copy');
return html`<button class=${this.props['inner-class'] || 'copy-button'} onClick=${e => this.onClick(e)}>${text}</button>`;
}
}
!util.isNode && register(CopyButton, 'iris-copy-button', ['str', 'not-shareable', 'text', 'copied-text', 'title', 'inner-class']);
export default CopyButton;

View File

@ -0,0 +1,46 @@
import {Component} from 'preact';
import util from '../util';
import {html} from 'htm/preact';
import Key from '../Key';
import register from 'preact-custom-element';
class FollowButton extends Component {
constructor() {
super();
this.eventListeners = {};
}
onClick(e) {
e.preventDefault();
const follow = !this.state.following;
util.getPublicState().user().get('follow').get(this.props.user).put(follow);
}
componentDidMount() {
util.injectCss();
Key.getDefault().then(key => {
util.getPublicState().user().auth(key);
util.getPublicState().user().get('follow').get(this.props.user).on((following, a, b, e) => {
this.setState({following});
this.eventListeners['follow'] = e;
});
});
}
componentWillUnmount() {
Object.values(this.eventListeners).forEach(e => e.off());
}
render() {
return html`
<button class="iris-follow-button ${this.state.following ? 'following' : ''} ${this.props['inner-class'] || ''}" onClick=${e => this.onClick(e)}>
<span class="nonhover">${this.state.following ? 'Following' : 'Follow'}</span>
<span class="hover">Unfollow</span>
</button>
`;
}
}
!util.isNode && register(FollowButton, 'iris-follow-button', ['user']);
export default FollowButton;

View File

@ -0,0 +1,68 @@
import register from 'preact-custom-element';
import {Component} from 'preact';
import {html} from 'htm/preact';
import {InlineBlock} from 'jsxstyle/preact';
import util from '../util';
import Attribute from '../Attribute';
const DEFAULT_WIDTH = 80;
class Identicon extends Component {
constructor() {
super();
this.eventListeners = {};
}
componentDidUpdate(prevProps) {
if (prevProps.user !== this.props.user) {
this.resetEventListeners();
this.setState({name: '', photo: ''});
this.componentDidMount();
}
}
componentDidMount() {
if (!this.props.user) return;
new Attribute({type: 'keyID', value: this.props.user}).identiconSrc({width: this.props.width, showType: false}).then(identicon => {
this.setState({identicon});
});
util.getPublicState().user(this.props.user).get('profile').get('photo').on(photo => {
if (typeof photo === 'string' && photo.indexOf('data:image') === 0) {
this.setState({photo});
}
});
if (this.props.showTooltip) {
util.getPublicState().user(this.props.user).get('profile').get('name').on((name, a, b, e) => {
this.eventListeners['name'] = e;
this.setState({name});
});
}
}
resetEventListeners() {
Object.values(this.eventListeners).forEach(e => e.off());
this.eventListeners = {};
}
componentWillUnmount() {
this.resetEventListeners();
}
render() {
return html`
<${InlineBlock}
onClick=${this.props.onClick}
cursor=${this.props.onClick ? 'pointer' : ''}
borderRadius=${parseInt(this.props.width) || DEFAULT_WIDTH}
overflow="hidden"
userSelect="none"
class="identicon-container ${this.props.showTooltip ? 'tooltip' : ''}">
${this.props.showTooltip && this.state.name ? html`<span class="tooltiptext">${this.state.name}</span>` : ''}
<img width=${this.props.width || DEFAULT_WIDTH} height=${this.props.width || DEFAULT_WIDTH} src="${this.state.photo || this.state.identicon}" alt="${this.state.name || ''}"/>
<//>`;
}
}
!util.isNode && register(Identicon, 'iris-identicon', ['user', 'onClick', 'width', 'showTooltip']);
export default Identicon;

View File

@ -0,0 +1,77 @@
import register from 'preact-custom-element';
import {html} from 'htm/preact';
import util from '../util';
import TextNode from './TextNode';
const toBase64 = file => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
class ImageNode extends TextNode {
getValue(user) {
this.getNode(user).on((value, a, b, e) => {
this.eventListeners[this.path] = e;
this.setState({value});
});
}
async onChange(e) {
const file = e.target.files[0];
const data = await toBase64(file);
this.getNode().put(data);
}
renderInput() {
return html`
<input
type="text"
value=${this.state.value}
placeholder=${this.props.placeholder || this.path}
onInput=${e => this.onInput(e)}
disabled=${!this.isEditable()} />
`;
}
renderTag() {
const placeholder = this.props.placeholder || this.props.path;
const tag = this.props.tag || 'span';
return html`
<${tag} ref=${this.ref} contenteditable=${this.isEditable()} placeholder=${placeholder} onInput=${e => this.onInput(e)}>
${this.state.value}
</${tag}>
`;
}
onClick() {
if (this.isEditable()) {
this.base.firstChild.click();
}
}
render() {
const editable = this.isEditable();
const val = this.state.value;
const src = val && val.indexOf('data:image') === 0 ? val : this.props.placeholder;
const {alt, width, height} = this.props;
let el;
if (src) {
const style = editable ? 'cursor: pointer;' : '';
el = html`<img style=${style} onClick=${e => this.onClick(e)} src=${val} ...${{alt, width, height}}/>`;
} else if (editable) {
el = html`<button class=${this.props['btn-class']} onClick=${e => this.onClick(e)}>Add image</button>`;
}
return html`
<span>
<input name="profile-photo-input" type="file" style="display:none;" onChange=${e => this.onChange(e)} accept="image/*"/>
${el}
</span>
`;
}
}
!util.isNode && register(ImageNode, 'iris-img', ['path', 'user', 'placeholder', 'editable', 'alt', 'width', 'height']);
export default ImageNode;

View File

@ -0,0 +1,189 @@
import register from 'preact-custom-element';
import {Component} from 'preact';
import {html} from 'htm/preact';
import {Row, Col} from 'jsxstyle/preact';
import util from '../util';
import Key from '../Key';
import Identicon from './Identicon.js';
import Fuse from 'fuse.js';
const suggestedFollow = 'hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU';
class Search extends Component {
constructor() {
super();
this.eventListeners = {};
this.state = {results: []};
this.follows = {};
this.debouncedIndexAndSearch = util.debounce(() => {
const options = {keys: ['name'], includeScore: true, includeMatches: true, threshold: 0.3};
this.fuse = new Fuse(Object.values(this.follows), options);
this.search();
}, 200);
Key.getDefault().then(key => {
this.key = key;
util.getPublicState().user().auth(key);
this.getFollowsFn(() => this.debouncedIndexAndSearch());
});
}
onInput() {
this.search();
}
close() {
this.base.querySelector('input').value = '';
this.setState({results: [], query: ''});
}
getFollowsFn(callback, k, maxDepth = 2, currentDepth = 1) {
k = k || this.key.pub;
const addFollow = (k, followDistance, follower) => {
if (this.follows[k]) {
if (this.follows[k].followDistance > followDistance) {
this.follows[k].followDistance = followDistance;
}
this.follows[k].followers.add(follower);
} else {
this.follows[k] = {key: k, followDistance, followers: new Set([follower])};
util.getPublicState().user(k).get('profile').get('name').on(name => {
this.follows[k].name = name;
callback(k, this.follows[k]);
});
}
callback(k, this.follows[k]);
};
addFollow(k, currentDepth - 1);
util.getPublicState().user(k).get('follow').map().once((isFollowing, followedKey) => { // TODO: .on for unfollow
if (isFollowing) {
this.hasFollows = true;
addFollow(followedKey, currentDepth, k);
if (currentDepth < maxDepth) {
this.getFollowsFn(callback, followedKey, maxDepth, currentDepth + 1);
}
}
});
return this.follows;
}
componentDidMount() {
this.adjustResultsPosition();
}
componentDidUpdate() {
this.adjustResultsPosition();
}
adjustResultsPosition() {
const input = this.base.querySelector('input');
this.offsetLeft = input.offsetLeft;
}
componentWillUnmount() {
Object.values(this.eventListeners).forEach(e => e.off());
}
onSubmit(e) {
e.preventDefault();
const links = this.base.querySelector('a:not(.follow-someone)');
links.length && links[0].click();
this.base.querySelector('input').blur();
}
search() {
const query = this.base.querySelector('input').value;
if (this.props['on-select']) {
const s = query.split('https://iris.to/profile/');
if (s.length > 1) {
return this.props['on-select']({key: s[1]});
}
const key = null;//Helpers.getUrlParameter('chatWith', s[1]);
if (key) {
return this.props['on-select']({key});
}
}
//if (followChatLink(query)) return;
if (query && this.fuse) {
const results = this.fuse.search(query).slice(0, 5);
if (results.length) {
const onKeyUp = e => {
if (e.key === 'Escape') { // escape key maps to keycode `27`
document.removeEventListener('keyup', onKeyUp);
this.close();
}
};
document.removeEventListener('keyup', onKeyUp);
document.addEventListener('keyup', onKeyUp);
}
this.setState({results, query});
} else {
this.setState({results: [], query});
}
}
onClick(e, item) {
this.close();
const onSelect = this.props.onSelect || window.onIrisSearchSelect;
if (onSelect) {
e.preventDefault();
e.stopPropagation();
onSelect(item);
}
}
render() {
return html`
<div class="iris-search-box" style="position: relative;">
<form onSubmit=${e => this.onSubmit(e)}>
<label>
<input class="${this.props['inner-class'] || ''}" type="text" placeholder="Search" onInput=${() => this.onInput()}/>
</label>
</form>
<${Col} class="search-box-results" style="position: absolute; background-color: white; border: 1px solid #eee; border-radius: 8px; left: ${this.offsetLeft || ''}">
${this.state.results.map(r => {
const i = r.item;
let followText = '';
if (i.followDistance === 1) {
followText = 'Following';
}
if (i.followDistance === 2) {
if (i.followers.size === 1 && this.follows[[...i.followers][0]] && this.follows[[...i.followers][0]].name) {
followText = `Followed by ${ this.follows[[...i.followers][0]].name}`;
} else {
followText = `Followed by ${ i.followers.size } users you follow`;
}
}
return html`
<a style="width: 300px; display: flex; padding: 5px; flex-direction: row" href="https://iris.to/profile/${i.key}" onClick=${e => this.onClick(e, i)}>
<${Identicon} user=${i.key} width=40/>
<${Col} marginLeft="5px">
${i.name || ''}<br/>
<small>
${followText}
</small>
<//>
<//>
`;
})}
${this.state.query && !this.hasFollows ? html`
<a class="follow-someone" style="padding:5px;">Follow someone to see more search results</a>
<a style="width: 300px; display: flex; padding: 5px; flex-direction: row" onClick=${e => this.onClick(e, {key: suggestedFollow})} href="https://iris.to/profile/${suggestedFollow}" class="suggested">
<${Identicon} user=${suggestedFollow} width=40/>
<${Row} alignItems="center" marginLeft="5px"><i>Suggested</i><//>
</a>
` : ''}
<//>
</div>
`;
}
}
!util.isNode && register(Search, 'iris-search', ['on-select', 'inner-class']);
export default Search;

View File

@ -0,0 +1,122 @@
import register from 'preact-custom-element';
import {Component, createRef} from 'preact';
import {html} from 'htm/preact';
import util from '../util';
import Key from '../Key';
class TextNode extends Component {
constructor() {
super();
this.ref = createRef();
this.eventListeners = {};
this.state = {value: ''};
}
componentDidUpdate(prevProps) {
if (prevProps.user !== this.props.user || prevProps.path !== this.props.path) {
this.setState({value: ''});
this.eventListenersOff();
this.componentDidMount();
}
}
componentDidMount() {
if (!this.props.path || this.props.user === undefined) {
return;
}
util.injectCss();
this.path = this.props.path;
this.user = this.props.user;
this.props.user && this.path && this.getValue(this.props.user);
const ps = util.getPublicState();
const myPub = ps._.user && ps._.user.is.pub;
const setMyPub = myPub => {
this.setState({myPub});
if (!this.props.user) {
this.user = myPub;
this.getValue(myPub);
}
};
if (myPub) {
setMyPub(myPub);
} else {
Key.getDefault().then(key => {
setMyPub(key.pub);
});
}
}
getNode(user) {
const base = util.getPublicState().user(user);
const path = this.path.split('/');
return path.reduce((sum, current) => sum.get(decodeURIComponent(current)), base);
}
getValue(user) {
this.getNode(user).once();
this.getNode(user).on((value, a, b, e) => {
this.eventListeners[this.path] = e;
if (!(this.ref.current && this.ref.current === document.activeElement)) {
this.setState({value, class: typeof value === 'string' ? '' : 'iris-non-string'});
}
});
}
eventListenersOff() {
Object.values(this.eventListeners).forEach(e => e.off());
this.eventListeners = {};
}
componentWillUnmount() {
this.eventListenersOff();
}
getParsedValue(s) {
if (this.props.json) {
try {
s = JSON.parse(s);
} catch (e) { null; }
}
return s;
}
onInput(e) {
const val = this.getParsedValue(e.target.value || e.target.innerText);
this.getNode().put(val);
this.setState({class: typeof val === 'string' ? '' : 'iris-non-string'});
}
isEditable() {
return (!this.props.user || this.props.user === this.state.myPub) && String(this.props.editable) !== 'false';
}
renderInput() {
return html`
<input
type="text"
value=${this.state.value}
placeholder=${this.props.placeholder || this.path}
class=${this.getClass()}
onInput=${e => this.onInput(e)}
disabled=${!this.isEditable()} />
`;
}
renderTag() {
const placeholder = this.props.placeholder || this.props.path;
const tag = this.props.tag || 'span';
return html`
<${tag} class=${this.state.class} ref=${this.ref} contenteditable=${this.isEditable()} placeholder=${placeholder} onInput=${e => this.onInput(e)}>
${this.props.json ? JSON.stringify(this.state.value) : this.state.value}
</${tag}>
`;
}
render() {
return (this.props.tag === 'input' ? this.renderInput() : this.renderTag());
}
}
!util.isNode && register(TextNode, 'iris-text', ['path', 'user', 'placeholder', 'editable', 'tag']);
export default TextNode;

33
src/js/iris-lib/index.js Normal file
View File

@ -0,0 +1,33 @@
/*eslint no-useless-escape: "off", camelcase: "off" */
import Collection from './Collection';
import SignedMessage from './SignedMessage';
import Contact from './Contact';
import Attribute from './Attribute';
import util from './util';
import Key from './Key';
import Channel from './Channel';
import Identicon from './components/Identicon';
import TextNode from './components/TextNode';
import ImageNode from './components/ImageNode';
import CopyButton from './components/CopyButton';
import FollowButton from './components/FollowButton';
import Search from './components/Search';
export default {
Collection,
SignedMessage,
Contact,
Attribute,
Key,
Channel,
util,
components: {
Identicon,
TextNode,
ImageNode,
CopyButton,
FollowButton,
Search
}
};

606
src/js/iris-lib/util.js Normal file
View File

@ -0,0 +1,606 @@
/* eslint no-useless-escape: "off", camelcase: "off" */
import Gun from 'gun'; // eslint-disable-line no-unused-vars
import 'gun/sea';
// eslint-disable-line no-unused-vars
let isNode = false;
try {
isNode = Object.prototype.toString.call(global.process) === `[object process]`;
} catch (e) { null; }
const userAgent = !isNode && navigator && navigator.userAgent && navigator.userAgent.toLowerCase();
const isElectron = (userAgent && userAgent.indexOf(' electron/') > -1);
const isMobile = !isNode && (function() {
if (isElectron) { return false; }
let check = false;
(function(a) {if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true;})(navigator.userAgent || navigator.vendor || window.opera || '');
return check;
})();
function gunAsAnotherUser(gun, key, f) { // Hacky way to use multiple users with gun
const gun2 = new Gun({radisk: false, peers: Object.keys(gun._.opt.peers)}); // TODO: copy other options too
const user = gun2.user();
user.auth(key);
setTimeout(() => {
const peers = Object.values(gun2.back('opt.peers'));
peers.forEach(peer => {
gun2.on('bye', peer);
});
}, 20000);
return f(user);
}
function gunOnceDefined(node) {
return new Promise(resolve => {
node.on((val, k, a, eve) => {
if (val !== undefined) {
eve.off();
resolve(val);
}
});
});
}
async function loadGunDepth(chain, maxDepth = 2, opts = {}) {
opts.maxBreadth = opts.maxBreadth || 50;
opts.cache = opts.cache || {};
return chain.then().then(layer => {
// Depth limit reached, or non-object, or array value returned
if (maxDepth < 1 || !layer || typeof layer !== 'object' || layer.constructor === Array) {
return layer;
}
let bcount = 0;
const promises = Object.keys(layer).map(key => {
// Only fetch links & restrict total search queries to maxBreadth ^ maxDepth requests
if (!Gun.val.link.is(layer[key]) || ++bcount >= opts.maxBreadth) {
return;
}
// During one recursive lookup, don't fetch the same key multiple times
if (opts.cache[key]) {
return opts.cache[key].then(data => {
layer[key] = data;
});
}
return opts.cache[key] = loadGunDepth(chain.get(key), maxDepth - 1, opts).then(data => {
layer[key] = data;
});
});
return Promise.all(promises).then(() => layer);
});
}
export default {
loadGunDepth: loadGunDepth,
gunOnceDefined: gunOnceDefined,
gunAsAnotherUser: gunAsAnotherUser,
getHash: async function(str, format = `base64`) {
if (!str) {
return undefined;
}
const hash = await Gun.SEA.work(str, undefined, undefined, {name: `SHA-256`});
if (hash.length > 44) {
throw new Error(`Gun.SEA.work returned an invalid SHA-256 hash longer than 44 chars: ${hash}. This is probably due to a sea.js bug on Safari.`);
}
if (format === `hex`) {
return this.base64ToHex(hash);
}
return hash;
},
base64ToHex(str) {
const raw = atob(str);
let result = '';
for (let i = 0; i < raw.length; i++) {
const hex = raw.charCodeAt(i).toString(16);
result += (hex.length === 2 ? hex : `0${ hex}`);
}
return result;
},
timeoutPromise(promise, timeout) {
return Promise.race([
promise,
new Promise((resolve => {
setTimeout(() => {
resolve();
}, timeout);
})),
]);
},
getCaret(el) {
if (el.selectionStart) {
return el.selectionStart;
} else if (document.selection) {
el.focus();
const r = document.selection.createRange();
if (r == null) {
return 0;
}
const re = el.createTextRange(), rc = re.duplicate();
re.moveToBookmark(r.getBookmark());
rc.setEndPoint('EndToStart', re);
return rc.text.length;
}
return 0;
},
injectCss() {
const elementId = `irisStyle`;
if (document.getElementById(elementId)) {
return;
}
const sheet = document.createElement(`style`);
sheet.id = elementId;
sheet.innerHTML = `
.iris-follow-button .hover {
display: none;
}
.iris-follow-button.following:hover .hover {
display: inline;
}
.iris-follow-button.following:hover .nonhover {
display: none;
}
.iris-identicon * {
box-sizing: border-box;
}
.iris-identicon {
vertical-align: middle;
border-radius: 50%;
text-align: center;
display: inline-block;
position: relative;
max-width: 100%;
}
.iris-distance {
z-index: 2;
position: absolute;
left:0%;
top:2px;
width: 100%;
text-align: right;
color: #fff;
text-shadow: 0 0 1px #000;
font-size: 75%;
line-height: 75%;
font-weight: bold;
}
.iris-pie {
border-radius: 50%;
position: absolute;
top: 0;
left: 0;
box-shadow: 0px 0px 0px 0px #82FF84;
padding-bottom: 100%;
max-width: 100%;
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
}
.iris-card {
padding: 10px;
background-color: #f7f7f7;
color: #777;
border: 1px solid #ddd;
display: flex;
flex-direction: row;
overflow: hidden;
}
.iris-card a {
-webkit-transition: color 150ms;
transition: color 150ms;
text-decoration: none;
color: #337ab7;
}
.iris-card a:hover, .iris-card a:active {
text-decoration: underline;
color: #23527c;
}
.iris-pos {
color: #3c763d;
}
.iris-neg {
color: #a94442;
}
.iris-identicon img {
position: absolute;
top: 0;
left: 0;
max-width: 100%;
border-radius: 50%;
border-color: transparent;
border-style: solid;
}
.iris-chat-open-button {
background-color: #1e1e1e;
color: #fff;
padding: 15px;
cursor: pointer;
user-select: none;
}
.iris-chat-open-button svg {
width: 1em;
}
.iris-chat-open-button, .iris-chat-box {
position: fixed;
bottom: 0.5rem;
right: 0.5rem;
border-radius: 8px;
font-family: system-ui;
font-size: 15px;
}
.iris-chat-box {
background-color: #fff;
max-height: 25rem;
box-shadow: 2px 2px 20px rgba(0, 0, 0, 0.2);
height: calc(100% - 44px);
display: flex;
flex-direction: column;
width: 320px;
color: rgb(38, 38, 38);
}
.iris-chat-box.minimized {
height: auto;
}
.iris-chat-box.minimized .iris-chat-header {
border-radius: 8px;
cursor: pointer;
}
.iris-chat-box.minimized .iris-chat-messages, .iris-chat-box.minimized .iris-typing-indicator, .iris-chat-box.minimized .iris-chat-input-wrapper, .iris-chat-box.minimized .iris-chat-minimize, .iris-chat-box.minimized .iris-chat-close {
display: none;
}
.iris-chat-header {
background-color: #1e1e1e;
height: 44px;
color: #fff;
border-radius: 8px 8px 0 0;
text-align: center;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex: none;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.iris-chat-header-text {
flex: 1;
}
.iris-online-indicator {
color: #bfbfbf;
margin-right: 5px;
font-size: 12px;
user-select: none;
flex: none;
}
.iris-online-indicator.yes {
color: #80bf5f;
}
.iris-typing-indicator {
display: none;
background-color: rgba(255, 255, 255, 0.5);
font-size: 12px;
padding: 2px;
color: #777;
}
.iris-typing-indicator.yes {
display: block;
}
.iris-chat-messages {
flex: 1;
padding: 15px;
overflow-y: scroll;
}
.iris-chat-input-wrapper {
flex: none;
padding: 15px;
background-color: #efefef;
display: flex;
flex-direction: row;
border-radius: 0 0 8px 8px;
}
.iris-chat-input-wrapper textarea {
padding: 15px 8px;
border-radius: 4px;
border: 1px solid rgba(0,0,0,0);
width: auto;
font-size: 15px;
resize: none;
flex: 1;
}
.iris-chat-input-wrapper textarea:focus {
outline: none;
border: 1px solid #6dd0ed;
}
.iris-chat-input-wrapper button svg {
display: inline-block;
font-size: inherit;
height: 1em;
width: 1em;
overflow: visible;
vertical-align: -0.125em;
}
.iris-chat-input-wrapper button, .iris-chat-input-wrapper button:hover, .iris-chat-input-wrapper button:active, .iris-chat-input-wrapper button:focus {
flex: none;
color: #999;
background-color: transparent;
font-size: 30px;
padding: 5px;
border: 1px solid rgba(0,0,0,0);
border-radius: 4px;
margin-left: 5px;
}
.iris-chat-input-wrapper button:active, .iris-chat-input-wrapper button:focus {
outline: none;
border: 1px solid #6dd0ed;
}
.iris-chat-message {
display: flex;
flex-direction: column;
margin-bottom: 2px;
overflow-wrap: break-word;
}
.iris-msg-content {
background-color: #efefef;
padding: 6px 10px;
border-radius: 8px;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1);
flex: none;
max-width: 75%;
}
.emoji {
font-size: 1.3em;
line-height: 1em;
}
.iris-chat-message .emoji-only {
font-size: 3em;
text-align: center;
}
.iris-seen {
color: rgba(0, 0, 0, 0.45);
user-select: none;
}
.iris-seen.yes {
color: #4fc3f7;
}
.iris-seen svg {
width: 18px;
}
.iris-delivered-checkmark {
display: none;
}
.delivered .iris-delivered-checkmark {
display: initial;
}
.iris-chat-minimize, .iris-chat-close {
user-select: none;
cursor: pointer;
width: 45px;
line-height: 44px;
}
.iris-chat-message.their {
align-items: flex-start;
}
.iris-chat-message.their + .iris-chat-message.our .iris-msg-content, .day-separator + .iris-chat-message.our .iris-msg-content {
margin-top: 15px;
border-radius: 8px 0px 8px 8px;
}
.iris-chat-message.their:first-of-type .iris-msg-content {
border-radius: 0px 8px 8px 8px;
}
.iris-chat-message.our:first-of-type .iris-msg-content {
border-radius: 8px 0px 8px 8px;
}
.iris-chat-message.our + .iris-chat-message.their .iris-msg-content, .day-separator + .iris-chat-message.their .iris-msg-content {
margin-top: 15px;
border-radius: 0px 8px 8px 8px;
}
.iris-chat-message.our {
align-items: flex-end;
}
.iris-chat-message.our .iris-msg-content {
background-color: #c5ecf7;
}
.iris-chat-message .time {
text-align: right;
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.iris-non-string {
color: blue;
}
.day-separator {
display: inline-block;
border-radius: 8px;
background-color: rgba(227, 249, 255, 0.91);
padding: 6px 10px;
margin-top: 15px;
margin-left: auto;
margin-right: auto;
text-transform: uppercase;
font-size: 13px;
color: rgba(74, 74, 74, 0.88);
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1);
user-select: none;
}
.day-separator:first-of-type {
margin-top: 0;
}
*[contenteditable="true"]:not(:focus) {
cursor: pointer;
}
*[contenteditable="true"] {
outline: none;
}
[placeholder]:empty:before {
content: attr(placeholder);
color: #999;
}
[placeholder]:empty:focus {
cursor: text;
}
`;
document.head.prepend(sheet);
},
getUrlParameter(sParam, sParams) {
const sPageURL = sParams || window.location.search.substring(1);
const sURLVariables = sPageURL.split('&');
let 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]);
}
}
},
formatTime(date) {
const t = date.toLocaleTimeString(undefined, {timeStyle: 'short'});
const s = t.split(':');
if (s.length === 3) { // safari tries to display seconds
return `${s[0] }:${ s[1] }${s[2].slice(2)}`;
}
return t;
},
formatDate(date) {
const t = date.toLocaleString(undefined, {dateStyle: 'short', timeStyle: 'short'});
const s = t.split(':');
if (s.length === 3) { // safari tries to display seconds
return `${s[0] }:${ s[1] }${s[2].slice(2)}`;
}
return t;
},
debounce(func, wait, immediate) {
let timeout;
return function() {
const context = this, args = arguments;
const later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
},
getDaySeparatorText(date, dateStr, now, nowStr) {
if (!now) {
now = new Date();
nowStr = now.toLocaleDateString({dateStyle: 'short'});
}
if (dateStr === nowStr) {
return 'today';
}
const dayDifference = Math.round((now - date) / (1000 * 60 * 60 * 24));
if (dayDifference <= 1) {
return 'yesterday';
}
if (dayDifference <= 5) {
return date.toLocaleDateString(undefined, {weekday: 'long'});
}
return dateStr;
},
setPublicState(gun) {
this.publicState = gun;
},
getPublicState() {
if (!this.publicState) {
this.publicState = new Gun('https://gun-us.herokuapp.com/gun');
}
return this.publicState;
},
createElement(type, cls, parent) {
const el = document.createElement(type);
if (cls) {
el.setAttribute('class', cls);
}
if (parent) {
parent.appendChild(el);
}
return el;
},
isNode,
isElectron,
isMobile
};

View File

@ -13,7 +13,7 @@ import SearchBox from '../components/SearchBox.js';
import {SMS_VERIFIER_PUB} from '../SMS';
import $ from 'jquery';
import QRCode from '../lib/qrcode.min.js';
import iris from 'iris-lib';
import iris from '../iris-lib';
function deleteChat(pub) {
if (confirm("Delete chat?")) {

View File

@ -4,7 +4,7 @@ import Identicon from '../components/Identicon.js';
import {translate as t} from '../Translation.js';
import Name from '../components/Name.js';
import View from './View.js';
import iris from 'iris-lib';
import iris from '../iris-lib';
import PublicMessage from "../components/PublicMessage";
import NotificationTools from "../Notifications";

View File

@ -17,7 +17,7 @@ import View from './View.js';
import { Link } from 'preact-router/match';
import $ from 'jquery';
import QRCode from '../lib/qrcode.min.js';
import iris from 'iris-lib';
import iris from '../iris-lib';
import {Helmet} from "react-helmet";
import {SMS_VERIFIER_PUB} from '../SMS';

View File

@ -4,7 +4,7 @@ import { translate as t } from '../../Translation.js';
import Torrent from '../../components/Torrent';
import State from '../../State.js';
import Session from '../../Session.js';
import iris from 'iris-lib';
import iris from '../../iris-lib';
import _ from 'lodash';
import $ from 'jquery';
import EmojiButton from '../../lib/emoji-button.js';

View File

@ -7,7 +7,7 @@ import CopyButton from '../../components/CopyButton.js';
import { route } from 'preact-router';
import $ from 'jquery';
import QRCode from '../../lib/qrcode.min.js';
import iris from 'iris-lib';
import iris from '../../iris-lib';
import Component from '../../BaseComponent';
import {Helmet} from 'react-helmet';

View File

@ -12,7 +12,7 @@ import Notifications from '../../Notifications.js';
import NewChat from './NewChat.js';
import _ from 'lodash';
import $ from 'jquery';
import iris from 'iris-lib';
import iris from '../../iris-lib';
import {Helmet} from 'react-helmet';
import Component from '../../BaseComponent';

View File

@ -3918,9 +3918,9 @@ cssstyle@^2.3.0:
cssom "~0.3.6"
csstype@^2.6.7:
version "2.6.19"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.19.tgz#feeb5aae89020bb389e1f63669a5ed490e391caa"
integrity sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==
version "2.6.20"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.20.tgz#9229c65ea0b260cf4d3d997cb06288e36a8d6dda"
integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==
csstype@^3.0.2:
version "3.0.10"
@ -5332,10 +5332,10 @@ functions-have-names@^1.2.2:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21"
integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==
fuse.js@^6.4.2:
version "6.5.3"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.5.3.tgz#7446c0acbc4ab0ab36fa602e97499bdb69452b93"
integrity sha512-sA5etGE7yD/pOqivZRBvUBd/NaL2sjAu6QuSaFoe1H2BrJSkH/T/UXAJ8CdXdw7DvY3Hs8CXKYkDWX7RiP5KOg==
fuse.js@^6.6.2:
version "6.6.2"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111"
integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
@ -5696,7 +5696,7 @@ hsla-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
htm@^3.0.4, htm@^3.1.0:
htm@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/htm/-/htm-3.1.0.tgz#0c305493b60da9f6ed097a2aaf4c994bd85ea022"
integrity sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q==
@ -6060,18 +6060,6 @@ ipaddr.js@1.9.1, ipaddr.js@^1.9.0:
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0"
integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==
iris-lib@^0.0.158:
version "0.0.158"
resolved "https://registry.yarnpkg.com/iris-lib/-/iris-lib-0.0.158.tgz#4c7eaabd601de89e6fa9b0221b1e0f5e3592ee65"
integrity sha512-JTeHAFyOdBJf1NanmqX2dFSb+AnMb+SrmnR598HKNawcG9rSR8Cg4NoR76klnwPo8b59LJyCXHniE1xRcXpTlA==
dependencies:
fuse.js "^6.4.2"
htm "^3.0.4"
identicon.js "^2.3.3"
jsxstyle "^2.4.0"
preact "^10.5.5"
preact-custom-element "^4.2.1"
is-absolute-url@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
@ -7196,7 +7184,7 @@ jsxstyle-utils@^2.5.0:
dependencies:
csstype "^2.6.7"
jsxstyle@^2.4.0:
jsxstyle@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/jsxstyle/-/jsxstyle-2.5.1.tgz#53e9b435673857a19db59a70d95de918529387fd"
integrity sha512-JboG+WT/+O93e2+QjMi4orfBY97S6IPv541gJav2crCajvRM7X4Z+zXHoBfK5LRkGVzXrXllP/63kgupjhi88w==
@ -9358,7 +9346,7 @@ preact-scroll-viewport@^0.2.0:
resolved "https://registry.yarnpkg.com/preact-scroll-viewport/-/preact-scroll-viewport-0.2.0.tgz#7c5c86dd9cb02c6b23d6b37834c08620a63068dc"
integrity sha1-fFyG3ZywLGsj1rN4NMCGIKYwaNw=
preact@^10.5.14, preact@^10.5.5:
preact@^10.5.14:
version "10.6.4"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.6.4.tgz#ad12c409ff1b4316158486e0a7b8d43636f7ced8"
integrity sha512-WyosM7pxGcndU8hY0OQlLd54tOU+qmG45QXj2dAYrL11HoyU/EzOSTlpJsirbBr1QW7lICxSsVJJmcmUglovHQ==