added prettier, eslint, spanish translations (#161)

* fix: handle missing user on metion properly

* prettier eslint

* use dep from npm registry

* fix: send from to sub callback
This commit is contained in:
Fernando López Guevara 2022-11-11 03:00:32 -03:00 committed by GitHub
parent a4828cd8b1
commit aaa421c92b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
112 changed files with 22951 additions and 8556 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
src/js/lib/*.js

40
.eslintrc.js Normal file
View File

@ -0,0 +1,40 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
],
overrides: [],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['simple-import-sort', '@typescript-eslint'],
rules: {
'simple-import-sort/imports': [
'error',
{
groups: [
// Packages `react` related packages come first.
['^react', '^@?\\w'],
// Internal packages.
['^(@|components)(/.*|$)'],
// Side effect imports.
['^\\u0000'],
// Parent imports. Put `..` last.
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
// Other relative imports. Put same-folder imports and `.` last.
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
// Style imports.
['^.+\\.?(css)$'],
],
},
],
},
};

View File

@ -11,7 +11,7 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: 16.x
cache: "yarn"
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Build
@ -24,7 +24,7 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: 16.x
cache: "yarn"
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Lint
@ -37,7 +37,7 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: 16.x
cache: "yarn"
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Test

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
src/css/cropper.min.css
build

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"useTabs": false,
"singleQuote": true,
"printWidth": 100,
"semi": true,
"trailingComma": "all",
"arrowParens": "always",
"tabWidth": 2
}

View File

@ -1,27 +1,33 @@
# Iris
Iris is like the social networking apps we're used to, but better.
* No phone number or signup required. Just type in your name or alias and go!
* Secure: It's open source. Users can validate that big brother doesn't read your private messages.
* Available: It works offline-first and is not dependent on any single centrally managed server. Users can even connect directly to each other.
- No phone number or signup required. Just type in your name or alias and go!
- Secure: It's open source. Users can validate that big brother doesn't read your private messages.
- Available: It works offline-first and is not dependent on any single centrally managed server. Users can even connect directly to each other.
![Screenshot](screenshot.png)
## Use
Browser application: [iris.to](https://iris.to)
* No installation required
* Progressive web app
* Use offline
* Save as an app to home screen or desktop
- No installation required
- Progressive web app
- Use offline
- Save as an app to home screen or desktop
Desktop application: ([download](https://github.com/irislib/iris-electron/releases), [source code](https://github.com/irislib/iris-electron)):
* Communicate and synchronize with local network peers without Internet access
* When local peers eventually connect to the Internet, your messages are relayed globally
* Bluetooth support upcoming
* Opens to background on login: stay online and get message notifications!
* More secure and available: no need to open the browser application from a server.
- Communicate and synchronize with local network peers without Internet access
- When local peers eventually connect to the Internet, your messages are relayed globally
- Bluetooth support upcoming
- Opens to background on login: stay online and get message notifications!
- More secure and available: no need to open the browser application from a server.
## Develop
``` bash
```bash
# install dependencies
yarn
@ -41,6 +47,7 @@ yarn test
[iris-lib](https://github.com/irislib/iris-lib) is a core part of the application. You can clone it and run `yarn link` in the iris-lib directory. Then run `yarn link iris-lib` in the iris-messenger directory.
## Privacy
The application is an unaudited proof-of-concept implementation, so don't use it for security critical purposes.
Private messages are end-to-end encrypted, but message timestamps and the number of chats aren't. In a decentralized network this information is potentially available to anyone.
@ -52,6 +59,7 @@ In that regard, Iris prioritizes decentralization and availability over perfect
Profile names, photos and online status are currently public. That can be changed when advanced group permissions are developed.
## Contact
Join our [Discord](https://discord.gg/4CJc74JEUY) (will be moved onto Iris when group chat is ready) or send me a message on [Iris](https://iris.to/?chatWith=hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU&s=HlzYzNrhUsrn2PLi4yuRt6DiFUNM3hOmN8nFpgw6T-g&k=zvDfsInsMOI1).
---

View File

@ -6,7 +6,9 @@
"build": "preact build --no-prerender",
"serve": "sirv build --port 8080 --cors --single",
"dev": "preact watch --host localhost --sw",
"lint": "eslint src",
"lint": "eslint 'src/**/*.{js,ts,tsx}'",
"lint:fix": "eslint --fix --quiet 'src/**/*.{js,ts,tsx}'",
"format": "prettier --plugin-search-dir . --write .",
"test": "jest"
},
"eslintConfig": {
@ -33,16 +35,23 @@
},
"devDependencies": {
"@types/jquery": "3.5.14",
"@types/lodash": "4.14.186",
"@types/lodash": "4.14.187",
"@types/react-helmet": "6.1.5",
"@types/webtorrent": "0.109.3",
"@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^5.42.0",
"enzyme": "^3.11.0",
"enzyme-adapter-preact-pure": "^4.0.1",
"eslint": "^8.24.0",
"eslint": "^8.26.0",
"eslint-config-preact": "^1.3.0",
"jest": "^27.0.6",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-simple-import-sort": "^8.0.0",
"gun": "^0.2020.1238",
"jest": "^29.2.2",
"jest-preset-preact": "^4.0.5",
"preact-cli": "^3.4.1",
"prettier": "^2.7.1",
"sirv-cli": "2.0.2",
"webpack-build-notifier": "^2.3.0"
},
@ -50,7 +59,7 @@
"@walletconnect/web3-provider": "^1.8.0",
"@zxing/library": "^0.19.1",
"aether-torrent": "^0.3.0",
"alchemy-sdk": "^2.1.0",
"alchemy-sdk": "^2.2.0",
"elliptic": "^6.5.4",
"fuse.js": "^6.6.2",
"history": "5.3.0",
@ -61,10 +70,10 @@
"jsxstyle": "^2.5.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"preact": "^10.11.0",
"preact": "^10.11.2",
"preact-async-route": "2.2.1",
"preact-custom-element": "^4.2.1",
"preact-render-to-string": "^5.2.4",
"preact-render-to-string": "^5.2.6",
"preact-router": "^4.1.0",
"preact-scroll-viewport": "^0.2.0",
"react-helmet": "^6.1.0",

View File

@ -7,12 +7,12 @@ export default {
config.plugins = config.plugins || [];
config.plugins.push(
new WebpackBuildNotifierPlugin({
title: "Iris Webpack Build",
logo: path.resolve("./src/assets/img/icon128.png"),
title: 'Iris Webpack Build',
logo: path.resolve('./src/assets/img/icon128.png'),
suppressSuccess: true, // don't spam success notifications
warningSound: false,
suppressWarning: true,
})
)
}
}
}),
);
},
};

View File

@ -1,5 +1,6 @@
import {AVAILABLE_LANGUAGE_KEYS} from "../src/js/translations/Translation.mjs";
import fs from "fs";
import fs from 'fs';
import { AVAILABLE_LANGUAGE_KEYS } from '../src/js/translations/Translation.mjs';
// Create a csv file where each row is a translation key and the column is the translation in different languages.
// The file is created in the current working directory.
@ -8,68 +9,69 @@ import fs from "fs";
// TODO: read translations from .mjs files in ../src/js/translations/
async function translationsToCsv() {
let csv = '';
let languages = [];
let translationKeys = [];
let translations = {};
let csv = '';
let languages = [];
let translationKeys = [];
let translations = {};
for (let lang of AVAILABLE_LANGUAGE_KEYS) {
const translation = (await import (`../src/js/translations/${lang}.mjs`)).default;
translations[lang] = translation;
languages.push(lang);
for (let key in translation) {
if (translationKeys.indexOf(key) === -1) {
translationKeys.push(key);
}
}
for (let lang of AVAILABLE_LANGUAGE_KEYS) {
const translation = (await import(`../src/js/translations/${lang}.mjs`)).default;
translations[lang] = translation;
languages.push(lang);
for (let key in translation) {
if (translationKeys.indexOf(key) === -1) {
translationKeys.push(key);
}
}
}
languages.sort();
translationKeys.sort();
languages.sort();
translationKeys.sort();
// add language names to csv
csv += '"","';
for (let i = 0; i < languages.length; i++) {
csv += languages[i];
if (i < languages.length - 1) {
csv += '","';
} else {
csv += '"\n';
}
// add language names to csv
csv += '"","';
for (let i = 0; i < languages.length; i++) {
csv += languages[i];
if (i < languages.length - 1) {
csv += '","';
} else {
csv += '"\n';
}
}
csv += '"';
for (let key of translationKeys) {
let row = key;
for (let lang of languages) {
row += '","' + (translations[lang][key] || '').replace(/"/g, '""');
}
csv += row + '"\n';
if (key !== translationKeys[translationKeys.length - 1]) {
csv += '"';
};
csv += '"';
for (let key of translationKeys) {
let row = key;
for (let lang of languages) {
row += '","' + (translations[lang][key] || '').replace(/"/g, '""');
}
csv += row + '"\n';
if (key !== translationKeys[translationKeys.length - 1]) {
csv += '"';
}
}
// output csv to file
fs.writeFileSync('translations.csv', csv);
console.log('wrote translations.csv');
// output csv to file
fs.writeFileSync('translations.csv', csv);
console.log('wrote translations.csv');
}
// convert the csv back to Translations.mjs in the same format as the original Translations.mjs file
function csvToTranslations() { // TODO: work in progress
let csv = fs.readFileSync('translations.csv', 'utf8');
let lines = csv.split('\n');
let translations = {};
let languages = lines[0].split(',');
languages.shift();
for (let i = 1; i < lines.length; i++) {
let line = lines[i].split(',');
let key = line[0];
line.shift();
for (let j = 0; j < languages.length; j++) {
translations[key][languages[j]] = line[j] || null;
}
function csvToTranslations() {
// TODO: work in progress
let csv = fs.readFileSync('translations.csv', 'utf8');
let lines = csv.split('\n');
let translations = {};
let languages = lines[0].split(',');
languages.shift();
for (let i = 1; i < lines.length; i++) {
let line = lines[i].split(',');
let key = line[0];
line.shift();
for (let j = 0; j < languages.length; j++) {
translations[key][languages[j]] = line[j] || null;
}
}
}
translationsToCsv();
translationsToCsv();

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +1,44 @@
import { PureComponent } from 'preact/compat';
/* eslint-disable @typescript-eslint/no-explicit-any */
import { PureComponent } from 'react';
import type { GunCallbackOn, GunSchema, IGunOnEvent } from 'gun';
type EL = {
off: Function;
};
type EL = IGunOnEvent;
type OwnState = {
ogImageUrl?: any;
};
export default abstract class BaseComponent<Props = {}, State = {}> extends PureComponent<Props, State & OwnState> {
export default abstract class BaseComponent<Props = any, State = any> extends PureComponent<
Props,
State & OwnState
> {
unmounted?: boolean;
eventListeners: Record<string, EL | undefined> = {};
sub(callback: Function, path?: string) {
return (v: unknown, k: string, x: unknown, e: EL | undefined, f: unknown) => {
sub(callback: CallableFunction, path?: string): GunCallbackOn<GunSchema, string> {
const cb = (data, key, message, event, f): void => {
if (this.unmounted) {
e && e.off();
event && event.off();
return;
}
this.eventListeners[path ?? k] = e;
callback(v,k,x,e,f);
}
this.eventListeners[path ?? key] = event;
callback(data, key, message, event, f);
};
return cb as any;
}
inject(name?: string, path?: string) {
inject(name?: string, path?: string): GunCallbackOn<GunSchema, string> {
return this.sub((v: unknown, k: string) => {
const newState: Partial<State> = {};
const newState: any = {};
newState[(name ?? k) as keyof State] = v as any;
this.setState(newState);
}, path);
}
unsubscribe() {
Object.keys(this.eventListeners).forEach(k => {
Object.keys(this.eventListeners).forEach((k) => {
const l = this.eventListeners[k];
l && l.off();
delete this.eventListeners[k];
@ -48,10 +53,12 @@ export default abstract class BaseComponent<Props = {}, State = {}> extends Pure
isUserAgentCrawler() {
// return true; // for testing
const ua = navigator.userAgent.toLowerCase();
return (ua.indexOf('prerender') !== -1 ||
return (
ua.indexOf('prerender') !== -1 ||
ua.indexOf('whatsapp') !== -1 ||
ua.indexOf('crawl') !== -1 ||
ua.indexOf('bot') !== -1);
ua.indexOf('bot') !== -1
);
}
async setOgImageUrl(imgSrc?: string) {
@ -66,10 +73,12 @@ export default abstract class BaseComponent<Props = {}, State = {}> extends Pure
const { default: pica } = await import('./lib/pica.min');
await pica().resize(image, resizedCanvas);
const ogImage = resizedCanvas.toDataURL('image/jpeg', 0.1);
const ogImageUrl = `https://iris-base64-decoder.herokuapp.com/?s=${encodeURIComponent(ogImage)}`;
const ogImageUrl = `https://iris-base64-decoder.herokuapp.com/?s=${encodeURIComponent(
ogImage,
)}`;
console.log(ogImageUrl);
this.state.ogImageUrl
this.setState({ogImageUrl});
this.state.ogImageUrl;
this.setState({ ogImageUrl });
};
image.src = imgSrc;
}

View File

@ -1,13 +1,16 @@
import {translate as t} from './translations/Translation';
import $ from 'jquery';
import _ from 'lodash';
import { route } from 'preact-router';
/* eslint-disable @typescript-eslint/no-explicit-any */
import reactStringReplace from 'react-string-replace';
import iris from 'iris-lib';
import Name from "./components/Name";
import $ from 'jquery';
import throttle from 'lodash/throttle';
import { route } from 'preact-router';
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;
const pubKeyRegex = /(\B\@[\w-_]{20,50}\.[\w-_]{20,50}\b)/g;
import Name from './components/Name';
import { translate as t } from './translations/Translation';
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}]+)/gu;
const pubKeyRegex = /(\B@[\w-_]{20,50}\.[\w-_]{20,50}\b)/g;
function setImgSrc(el: JQuery<HTMLElement>, src: string): JQuery<HTMLElement> {
if (src && src.indexOf('data:image') === 0) {
@ -17,7 +20,7 @@ function setImgSrc(el: JQuery<HTMLElement>, src: string): JQuery<HTMLElement> {
}
const userAgent = navigator.userAgent.toLowerCase();
const isElectron = (userAgent.indexOf(' electron/') > -1);
const isElectron = userAgent.indexOf(' electron/') > -1;
export default {
wtClient: undefined as any,
@ -35,16 +38,24 @@ export default {
highlightEverything(s: string): any[] {
let replacedText = reactStringReplace(s, emojiRegex, (match, i) => {
return <span key={match + i} className="emoji">{match}</span>;
return (
<span key={match + i} className="emoji">
{match}
</span>
);
});
replacedText = reactStringReplace(replacedText, pubKeyRegex, (match, i) => {
const link = `/profile/${match.slice(1)}`;
return (<a href={link}>
@<Name key={match + i} pub={match.slice(1)} />
</a>);
return (
<a href={link}>
@<Name key={match + i} pub={match.slice(1)} />
</a>
);
});
replacedText = reactStringReplace(replacedText, /(https?:\/\/\S+)/g, (match, i) => (
<a key={match + i} href={match}>{match}</a>
<a key={match + i} href={match}>
{match}
</a>
));
return replacedText;
},
@ -54,11 +65,13 @@ export default {
const s = str.split('?');
let chatId;
if (s.length === 2) {
chatId = iris.util.getUrlParameter('chatWith', s[1]) || iris.util.getUrlParameter('channelId', s[1]);
chatId =
iris.util.getUrlParameter('chatWith', s[1]) ||
iris.util.getUrlParameter('channelId', s[1]);
}
if (chatId) {
iris.session.newChannel(chatId, str);
route(`/chat/${ chatId}`); // TODO
route(`/chat/${chatId}`); // TODO
return true;
}
if (str.indexOf('https://iris.to') === 0) {
@ -71,63 +84,66 @@ export default {
copyToClipboard(text: string): boolean {
if (window.clipboardData && window.clipboardData.setData) {
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
window.clipboardData.setData("Text", text);
window.clipboardData.setData('Text', text);
return true;
}
else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
let textarea = document.createElement("textarea");
} 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.
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 document.execCommand('copy'); // Security exception may be thrown by some browsers.
} catch (ex) {
console.warn('Copy to clipboard failed.', ex);
return false;
}
finally {
} finally {
document.body.removeChild(textarea);
return true;
}
}
},
getUrlParameter(sParam: string, sParams?: string) {
let sPageURL = sParams ?? window.location.search.substring(1),
sURLVariables = sPageURL.split('&'),
sParameterName,
i;
const sPageURL = sParams ?? window.location.search.substring(1),
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]);
return sParameterName[1] === undefined ? '' : decodeURIComponent(sParameterName[1]);
}
}
},
showConsoleWarning(): void {
let i = "Stop!",
j = "This is a browser feature intended for developers. If someone told you to copy-paste something here to enable a feature or \"hack\" someone's account, it is a scam and will give them access to your account.";
const i = 'Stop!',
j =
'This is a browser feature intended for developers. If someone told you to copy-paste something here to enable a feature or "hack" someone\'s account, it is a scam and will give them access to your account.';
if ((window.chrome || window.safari)) {
let l = 'font-family:helvetica; font-size:20px; ';
if (window.chrome || window.safari) {
const l = 'font-family:helvetica; font-size:20px; ';
[
[i, `${l }font-size:50px; font-weight:bold; color:red; -webkit-text-stroke:1px black;`],
[j, l],
['', '']
[i, `${l}font-size:50px; font-weight:bold; color:red; -webkit-text-stroke:1px black;`],
[j, l],
['', ''],
].map((r) => {
setTimeout(console.log.bind(console, `\n%c${ r[0]}`, r[1]));
setTimeout(console.log.bind(console, `\n%c${r[0]}`, r[1]));
});
}
},
getRelativeTimeText(date: Date): string {
let text = date && iris.util.getDaySeparatorText(date, date.toLocaleDateString(undefined, {dateStyle:'short'}));
let text =
date &&
iris.util.getDaySeparatorText(
date,
date.toLocaleDateString(undefined, { dateStyle: 'short' }),
);
text = t(text);
if (text === t('today')) { text = iris.util.formatTime(date); }
if (text === t('today')) {
text = iris.util.formatTime(date);
}
return text;
},
@ -140,37 +156,38 @@ export default {
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) } ${ sizes[i]}`;
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
},
download(filename: string, data: string, type: string, charset: string, href: string): void {
let hiddenElement;
if (charset === null) {
charset = 'utf-8';
}
hiddenElement = document.createElement('a');
hiddenElement.href = href || (`data:${type};charset=${charset},${encodeURI(data)}`);
const hiddenElement = document.createElement('a');
hiddenElement.href = href || `data:${type};charset=${charset},${encodeURI(data)}`;
hiddenElement.target = '_blank';
hiddenElement.download = filename;
hiddenElement.click();
},
getBase64(file: Blob): Promise<string | ArrayBuffer | null> {
let reader = new FileReader();
const reader = new FileReader();
reader.readAsDataURL(file);
return new Promise((resolve, reject) => {
reader.onload = function () {
resolve(reader.result);
};
reader.onerror = function (error) {
reject(`Error: ${ error}`);
reject(`Error: ${error}`);
};
});
},
scrollToMessageListBottom: _.throttle(() => {
scrollToMessageListBottom: throttle(() => {
if ($('#message-view')[0]) {
$('#message-view').scrollTop($('#message-view')[0].scrollHeight - $('#message-view')[0].clientHeight);
$('#message-view').scrollTop(
$('#message-view')[0].scrollHeight - $('#message-view')[0].clientHeight,
);
}
}, 100),
@ -178,17 +195,29 @@ export default {
animateScrollTop: (selector: string): void => {
const el = $(selector);
el.css({overflow:'hidden'});
el.css({ overflow: 'hidden' });
setTimeout(() => {
el.css({overflow:''});
el.on("scroll mousedown wheel DOMMouseScroll mousewheel keyup touchstart", e => {
if (e.which && e.which > 0 || e.type === "mousedown" || e.type === "mousewheel" || e.type === 'touchstart') {
el.css({ overflow: '' });
el.on('scroll mousedown wheel DOMMouseScroll mousewheel keyup touchstart', (e) => {
if (
(e.which && e.which > 0) ||
e.type === 'mousedown' ||
e.type === 'mousewheel' ||
e.type === 'touchstart'
) {
el.stop(true);
}
});
el.stop().animate({ scrollTop: 0 }, {duration: 400, queue: false, always: () => {
el.off("scroll mousedown wheel DOMMouseScroll mousewheel keyup touchstart");
}});
el.stop().animate(
{ scrollTop: 0 },
{
duration: 400,
queue: false,
always: () => {
el.off('scroll mousedown wheel DOMMouseScroll mousewheel keyup touchstart');
},
},
);
}, 10);
},
@ -197,5 +226,5 @@ export default {
},
isElectron,
pubKeyRegex
pubKeyRegex,
};

View File

@ -1,34 +1,179 @@
export default {
settings: <svg version="1.1" x="0px" y="0px" width="25px" height="25.001px" viewBox="0 0 25 25.001" style="enable-background:new 0 0 25 25.001;" xmlSpace="preserve">
<g><path fill="currentColor" d="M24.38,10.175l-2.231-0.268c-0.228-0.851-0.562-1.655-0.992-2.401l1.387-1.763c0.212-0.271,0.188-0.69-0.057-0.934 l-2.299-2.3c-0.242-0.243-0.662-0.269-0.934-0.057l-1.766,1.389c-0.743-0.43-1.547-0.764-2.396-0.99L14.825,0.62 C14.784,0.279,14.469,0,14.125,0h-3.252c-0.344,0-0.659,0.279-0.699,0.62L9.906,2.851c-0.85,0.227-1.655,0.562-2.398,0.991 L5.743,2.455c-0.27-0.212-0.69-0.187-0.933,0.056L2.51,4.812C2.268,5.054,2.243,5.474,2.456,5.746L3.842,7.51 c-0.43,0.744-0.764,1.549-0.991,2.4l-2.23,0.267C0.28,10.217,0,10.532,0,10.877v3.252c0,0.344,0.279,0.657,0.621,0.699l2.231,0.268 c0.228,0.848,0.561,1.652,0.991,2.396l-1.386,1.766c-0.211,0.271-0.187,0.69,0.057,0.934l2.296,2.301 c0.243,0.242,0.663,0.269,0.933,0.057l1.766-1.39c0.744,0.43,1.548,0.765,2.398,0.991l0.268,2.23 c0.041,0.342,0.355,0.62,0.699,0.62h3.252c0.345,0,0.659-0.278,0.699-0.62l0.268-2.23c0.851-0.228,1.655-0.562,2.398-0.991 l1.766,1.387c0.271,0.212,0.69,0.187,0.933-0.056l2.299-2.301c0.244-0.242,0.269-0.662,0.056-0.935l-1.388-1.764 c0.431-0.744,0.764-1.548,0.992-2.397l2.23-0.268C24.721,14.785,25,14.473,25,14.127v-3.252 C25.001,10.529,24.723,10.216,24.38,10.175z M12.501,18.75c-3.452,0-6.25-2.798-6.25-6.25s2.798-6.25,6.25-6.25 s6.25,2.798,6.25,6.25S15.954,18.75,12.501,18.75z" /></g></svg>,
settings: (
<svg
version="1.1"
x="0px"
y="0px"
width="25px"
height="25.001px"
viewBox="0 0 25 25.001"
style="enable-background:new 0 0 25 25.001;"
xmlSpace="preserve"
>
<g>
<path
fill="currentColor"
d="M24.38,10.175l-2.231-0.268c-0.228-0.851-0.562-1.655-0.992-2.401l1.387-1.763c0.212-0.271,0.188-0.69-0.057-0.934 l-2.299-2.3c-0.242-0.243-0.662-0.269-0.934-0.057l-1.766,1.389c-0.743-0.43-1.547-0.764-2.396-0.99L14.825,0.62 C14.784,0.279,14.469,0,14.125,0h-3.252c-0.344,0-0.659,0.279-0.699,0.62L9.906,2.851c-0.85,0.227-1.655,0.562-2.398,0.991 L5.743,2.455c-0.27-0.212-0.69-0.187-0.933,0.056L2.51,4.812C2.268,5.054,2.243,5.474,2.456,5.746L3.842,7.51 c-0.43,0.744-0.764,1.549-0.991,2.4l-2.23,0.267C0.28,10.217,0,10.532,0,10.877v3.252c0,0.344,0.279,0.657,0.621,0.699l2.231,0.268 c0.228,0.848,0.561,1.652,0.991,2.396l-1.386,1.766c-0.211,0.271-0.187,0.69,0.057,0.934l2.296,2.301 c0.243,0.242,0.663,0.269,0.933,0.057l1.766-1.39c0.744,0.43,1.548,0.765,2.398,0.991l0.268,2.23 c0.041,0.342,0.355,0.62,0.699,0.62h3.252c0.345,0,0.659-0.278,0.699-0.62l0.268-2.23c0.851-0.228,1.655-0.562,2.398-0.991 l1.766,1.387c0.271,0.212,0.69,0.187,0.933-0.056l2.299-2.301c0.244-0.242,0.269-0.662,0.056-0.935l-1.388-1.764 c0.431-0.744,0.764-1.548,0.992-2.397l2.23-0.268C24.721,14.785,25,14.473,25,14.127v-3.252 C25.001,10.529,24.723,10.216,24.38,10.175z M12.501,18.75c-3.452,0-6.25-2.798-6.25-6.25s2.798-6.25,6.25-6.25 s6.25,2.798,6.25,6.25S15.954,18.75,12.501,18.75z"
/>
</g>
</svg>
),
home: <svg fill="currentColor" viewBox="0 0 48 48" width="24px" height="24px"><path d="M39.5,43h-9c-1.381,0-2.5-1.119-2.5-2.5v-9c0-1.105-0.895-2-2-2h-4c-1.105,0-2,0.895-2,2v9c0,1.381-1.119,2.5-2.5,2.5h-9 C7.119,43,6,41.881,6,40.5V21.413c0-2.299,1.054-4.471,2.859-5.893L23.071,4.321c0.545-0.428,1.313-0.428,1.857,0L39.142,15.52 C40.947,16.942,42,19.113,42,21.411V40.5C42,41.881,40.881,43,39.5,43z" /></svg>,
home: (
<svg fill="currentColor" viewBox="0 0 48 48" width="24px" height="24px">
<path d="M39.5,43h-9c-1.381,0-2.5-1.119-2.5-2.5v-9c0-1.105-0.895-2-2-2h-4c-1.105,0-2,0.895-2,2v9c0,1.381-1.119,2.5-2.5,2.5h-9 C7.119,43,6,41.881,6,40.5V21.413c0-2.299,1.054-4.471,2.859-5.893L23.071,4.321c0.545-0.428,1.313-0.428,1.857,0L39.142,15.52 C40.947,16.942,42,19.113,42,21.411V40.5C42,41.881,40.881,43,39.5,43z" />
</svg>
),
videoCall: <svg enable-background="new 0 0 50 50" version="1.1" viewBox="0 0 50 50"><rect fill="none" style="height:24px;width:24px" /><polygon fill="none" points="49,14 36,21 36,29 49,36 " stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="4" /><path d="M36,36c0,2.209-1.791,4-4,4 H5c-2.209,0-4-1.791-4-4V14c0-2.209,1.791-4,4-4h27c2.209,0,4,1.791,4,4V36z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="4" /></svg>,
videoCall: (
<svg enable-background="new 0 0 50 50" version="1.1" viewBox="0 0 50 50">
<rect fill="none" style="height:24px;width:24px" />
<polygon
fill="none"
points="49,14 36,21 36,29 49,36 "
stroke="currentColor"
stroke-linecap="round"
stroke-miterlimit="10"
stroke-width="4"
/>
<path
d="M36,36c0,2.209-1.791,4-4,4 H5c-2.209,0-4-1.791-4-4V14c0-2.209,1.791-4,4-4h27c2.209,0,4,1.791,4,4V36z"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-miterlimit="10"
stroke-width="4"
/>
</svg>
),
//voiceCall: <svg enable-background="new 0 0 50 50" style="height:20px;width:20px" version="1.1" viewBox="0 0 50 50" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><rect fill="none" height="50" width="50"/><path d="M30.217,35.252c0,0,4.049-2.318,5.109-2.875 c1.057-0.559,2.152-0.7,2.817-0.294c1.007,0.616,9.463,6.241,10.175,6.739c0.712,0.499,1.055,1.924,0.076,3.32 c-0.975,1.396-5.473,6.916-7.379,6.857c-1.909-0.062-9.846-0.236-24.813-15.207C1.238,18.826,1.061,10.887,1,8.978 C0.939,7.07,6.459,2.571,7.855,1.595c1.398-0.975,2.825-0.608,3.321,0.078c0.564,0.781,6.124,9.21,6.736,10.176 c0.419,0.66,0.265,1.761-0.294,2.819c-0.556,1.06-2.874,5.109-2.874,5.109s1.634,2.787,7.16,8.312 C27.431,33.615,30.217,35.252,30.217,35.252z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="4"/></svg>,
chat: (
<svg
x="0px"
y="0px"
viewBox="0 0 486.736 486.736"
style="enable-background:new 0 0 486.736 486.736;"
width="24px"
height="24px"
fill="currentColor"
stroke="#000000"
stroke-width="0"
>
<path
fill="currentColor"
d="M481.883,61.238l-474.3,171.4c-8.8,3.2-10.3,15-2.6,20.2l70.9,48.4l321.8-169.7l-272.4,203.4v82.4c0,5.6,6.3,9,11,5.9 l60-39.8l59.1,40.3c5.4,3.7,12.8,2.1,16.3-3.5l214.5-353.7C487.983,63.638,485.083,60.038,481.883,61.238z"
/>
</svg>
),
chat: <svg x="0px" y="0px" viewBox="0 0 486.736 486.736" style="enable-background:new 0 0 486.736 486.736;" width="24px" height="24px" fill="currentColor" stroke="#000000" stroke-width="0"><path fill="currentColor" d="M481.883,61.238l-474.3,171.4c-8.8,3.2-10.3,15-2.6,20.2l70.9,48.4l321.8-169.7l-272.4,203.4v82.4c0,5.6,6.3,9,11,5.9 l60-39.8l59.1,40.3c5.4,3.7,12.8,2.1,16.3-3.5l214.5-353.7C487.983,63.638,485.083,60.038,481.883,61.238z" /></svg>,
circle: (
<svg
x="0px"
y="0px"
viewBox="0 0 300 300"
width="24px"
fill="currentColor"
height="24px"
style="enable-background:new 0 0 300 300;"
>
<path d="M150,0C67.29,0,0,67.29,0,150s67.29,150,150,150s150-67.29,150-150S232.71,0,150,0z M150,270c-66.169,0-120-53.832-120-120 S83.831,30,150,30s120,53.832,120,120S216.168,270,150,270z" />
</svg>
),
circle: <svg x="0px" y="0px" viewBox="0 0 300 300" width="24px" fill="currentColor" height="24px" style="enable-background:new 0 0 300 300;"><path d="M150,0C67.29,0,0,67.29,0,150s67.29,150,150,150s150-67.29,150-150S232.71,0,150,0z M150,270c-66.169,0-120-53.832-120-120 S83.831,30,150,30s120,53.832,120,120S216.168,270,150,270z" /></svg>,
folder: (
<svg
enable-background="new 0 0 512 512"
height="24"
viewBox="0 0 512 512"
width="24"
fill="currentColor"
>
<path d="m255.964 90c-13.696-18.207-35.478-30-59.964-30h-196v392h512v-362zm226.036 332h-452v-332h166c24.813 0 45 20.187 45 45v45h241zm0-272h-211v-15c0-5.137-.519-10.151-1.507-15h212.507z" />
</svg>
),
folder: <svg enable-background="new 0 0 512 512" height="24" viewBox="0 0 512 512" width="24" fill="currentColor"><path d="m255.964 90c-13.696-18.207-35.478-30-59.964-30h-196v392h512v-362zm226.036 332h-452v-332h166c24.813 0 45 20.187 45 45v45h241zm0-272h-211v-15c0-5.137-.519-10.151-1.507-15h212.507z" /></svg>,
feed: (
<svg
x="0px"
y="0px"
width="24px"
height="24px"
fill="currentColor"
viewBox="0 0 124 124"
style="enable-background:new 0 0 124 124;"
xmlSpace="preserve"
>
<circle cx="20.3" cy="103.749" r="20" />
<path d="M67,113.95c0,5.5,4.5,10,10,10s10-4.5,10-10c0-42.4-34.5-77-77-77c-5.5,0-10,4.5-10,10s4.5,10,10,10 C41.5,56.95,67,82.55,67,113.95z" />
<path d="M114,123.95c5.5,0,10-4.5,10-10c0-62.8-51.1-113.9-113.9-113.9c-5.5,0-10,4.5-10,10s4.5,10,10,10 c51.8,0,93.9,42.1,93.9,93.9C104,119.45,108.4,123.95,114,123.95z" />
</svg>
),
feed: <svg x="0px" y="0px" width="24px" height="24px" fill="currentColor" viewBox="0 0 124 124" style="enable-background:new 0 0 124 124;" xmlSpace="preserve"><circle cx="20.3" cy="103.749" r="20" /><path d="M67,113.95c0,5.5,4.5,10,10,10s10-4.5,10-10c0-42.4-34.5-77-77-77c-5.5,0-10,4.5-10,10s4.5,10,10,10 C41.5,56.95,67,82.55,67,113.95z" /><path d="M114,123.95c5.5,0,10-4.5,10-10c0-62.8-51.1-113.9-113.9-113.9c-5.5,0-10,4.5-10,10s4.5,10,10,10 c51.8,0,93.9,42.1,93.9,93.9C104,119.45,108.4,123.95,114,123.95z" /></svg>,
store: (
<svg viewBox="0 0 74 74" width="24" height="24" fill="currentColor">
<g>
<path d="M71,22H3a1,1,0,0,1-.908-1.419l6-13A1,1,0,0,1,9,7H65a1,1,0,0,1,.908.581l6,13A1,1,0,0,1,71,22ZM4.563,20H69.437L64.36,9H9.64Z" />
<path d="M7.857,34A5.864,5.864,0,0,1,2,28.143V21a1,1,0,0,1,1-1h9.714a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,7.857,34ZM4,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" />
<path d="M17.571,34a5.864,5.864,0,0,1-5.857-5.857V21a1,1,0,0,1,1-1h9.714a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,17.571,34ZM13.714,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" />
<path d="M27.286,34a5.864,5.864,0,0,1-5.857-5.857V21a1,1,0,0,1,1-1h9.714a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,27.286,34ZM23.429,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" />
<path d="M37,34a5.864,5.864,0,0,1-5.857-5.857V21a1,1,0,0,1,1-1h9.714a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,37,34ZM33.143,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" />
<path d="M46.714,34a5.864,5.864,0,0,1-5.857-5.857V21a1,1,0,0,1,1-1h9.714a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,46.714,34ZM42.857,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" />
<path d="M56.429,34a5.864,5.864,0,0,1-5.857-5.857V21a1,1,0,0,1,1-1h9.714a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,56.429,34ZM52.571,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" />
<path d="M66.143,34a5.864,5.864,0,0,1-5.857-5.857V21a1,1,0,0,1,1-1H71a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,66.143,34ZM62.286,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" />
<path d="M68,70H5.91a1,1,0,0,1-1-1V32h2V68H67V32h2V69A1,1,0,0,1,68,70Z" />
<path d="M60,70H43a1,1,0,0,1-1-1V41a1,1,0,0,1,1-1H60a1,1,0,0,1,1,1V69A1,1,0,0,1,60,70ZM44,68H59V42H44Z" />
<path d="M37,61.5H12a1,1,0,0,1-1-1v-20a1,1,0,0,1,1-1H37a1,1,0,0,1,1,1v20A1,1,0,0,1,37,61.5Zm-24-2H36v-18H13Z" />
</g>
</svg>
),
store: <svg viewBox="0 0 74 74" width="24" height="24" fill="currentColor"><g><path d="M71,22H3a1,1,0,0,1-.908-1.419l6-13A1,1,0,0,1,9,7H65a1,1,0,0,1,.908.581l6,13A1,1,0,0,1,71,22ZM4.563,20H69.437L64.36,9H9.64Z" /><path d="M7.857,34A5.864,5.864,0,0,1,2,28.143V21a1,1,0,0,1,1-1h9.714a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,7.857,34ZM4,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" /><path d="M17.571,34a5.864,5.864,0,0,1-5.857-5.857V21a1,1,0,0,1,1-1h9.714a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,17.571,34ZM13.714,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" /><path d="M27.286,34a5.864,5.864,0,0,1-5.857-5.857V21a1,1,0,0,1,1-1h9.714a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,27.286,34ZM23.429,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" /><path d="M37,34a5.864,5.864,0,0,1-5.857-5.857V21a1,1,0,0,1,1-1h9.714a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,37,34ZM33.143,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" /><path d="M46.714,34a5.864,5.864,0,0,1-5.857-5.857V21a1,1,0,0,1,1-1h9.714a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,46.714,34ZM42.857,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" /><path d="M56.429,34a5.864,5.864,0,0,1-5.857-5.857V21a1,1,0,0,1,1-1h9.714a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,56.429,34ZM52.571,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" /><path d="M66.143,34a5.864,5.864,0,0,1-5.857-5.857V21a1,1,0,0,1,1-1H71a1,1,0,0,1,1,1v7.143A5.864,5.864,0,0,1,66.143,34ZM62.286,22v6.143a3.857,3.857,0,0,0,7.714,0V22Z" /><path d="M68,70H5.91a1,1,0,0,1-1-1V32h2V68H67V32h2V69A1,1,0,0,1,68,70Z" /><path d="M60,70H43a1,1,0,0,1-1-1V41a1,1,0,0,1,1-1H60a1,1,0,0,1,1,1V69A1,1,0,0,1,60,70ZM44,68H59V42H44Z" /><path d="M37,61.5H12a1,1,0,0,1-1-1v-20a1,1,0,0,1,1-1H37a1,1,0,0,1,1,1v20A1,1,0,0,1,37,61.5Zm-24-2H36v-18H13Z" /></g></svg>,
close: (
<svg height="25px" viewBox="0 0 329.26933 329" width="25px" fill="currentColor">
<path d="m194.800781 164.769531 128.210938-128.214843c8.34375-8.339844 8.34375-21.824219 0-30.164063-8.339844-8.339844-21.824219-8.339844-30.164063 0l-128.214844 128.214844-128.210937-128.214844c-8.34375-8.339844-21.824219-8.339844-30.164063 0-8.34375 8.339844-8.34375 21.824219 0 30.164063l128.210938 128.214843-128.210938 128.214844c-8.34375 8.339844-8.34375 21.824219 0 30.164063 4.15625 4.160156 9.621094 6.25 15.082032 6.25 5.460937 0 10.921875-2.089844 15.082031-6.25l128.210937-128.214844 128.214844 128.214844c4.160156 4.160156 9.621094 6.25 15.082032 6.25 5.460937 0 10.921874-2.089844 15.082031-6.25 8.34375-8.339844 8.34375-21.824219 0-30.164063zm0 0" />
</svg>
),
close: <svg height="25px" viewBox="0 0 329.26933 329" width="25px" fill="currentColor"><path d="m194.800781 164.769531 128.210938-128.214843c8.34375-8.339844 8.34375-21.824219 0-30.164063-8.339844-8.339844-21.824219-8.339844-30.164063 0l-128.214844 128.214844-128.210937-128.214844c-8.34375-8.339844-21.824219-8.339844-30.164063 0-8.34375 8.339844-8.34375 21.824219 0 30.164063l128.210938 128.214843-128.210938 128.214844c-8.34375 8.339844-8.34375 21.824219 0 30.164063 4.15625 4.160156 9.621094 6.25 15.082032 6.25 5.460937 0 10.921875-2.089844 15.082031-6.25l128.210937-128.214844 128.214844 128.214844c4.160156 4.160156 9.621094 6.25 15.082032 6.25 5.460937 0 10.921874-2.089844 15.082031-6.25 8.34375-8.339844 8.34375-21.824219 0-30.164063zm0 0" /></svg>,
play: (
<svg
x="0px"
y="0px"
height="25px"
width="25px"
viewBox="0 0 30.051 30.051"
fill="currentColor"
style="enable-background:new 0 0 30.051 30.051;"
>
<path d="M19.982,14.438l-6.24-4.536c-0.229-0.166-0.533-0.191-0.784-0.062c-0.253,0.128-0.411,0.388-0.411,0.669v9.069 c0,0.284,0.158,0.543,0.411,0.671c0.107,0.054,0.224,0.081,0.342,0.081c0.154,0,0.31-0.049,0.442-0.146l6.24-4.532 c0.197-0.145,0.312-0.369,0.312-0.607C20.295,14.803,20.177,14.58,19.982,14.438z" />
<path d="M15.026,0.002C6.726,0.002,0,6.728,0,15.028c0,8.297,6.726,15.021,15.026,15.021c8.298,0,15.025-6.725,15.025-15.021 C30.052,6.728,23.324,0.002,15.026,0.002z M15.026,27.542c-6.912,0-12.516-5.601-12.516-12.514c0-6.91,5.604-12.518,12.516-12.518 c6.911,0,12.514,5.607,12.514,12.518C27.541,21.941,21.937,27.542,15.026,27.542z" />
</svg>
),
play: <svg x="0px" y="0px" height="25px" width="25px" viewBox="0 0 30.051 30.051" fill="currentColor" style="enable-background:new 0 0 30.051 30.051;"><path d="M19.982,14.438l-6.24-4.536c-0.229-0.166-0.533-0.191-0.784-0.062c-0.253,0.128-0.411,0.388-0.411,0.669v9.069 c0,0.284,0.158,0.543,0.411,0.671c0.107,0.054,0.224,0.081,0.342,0.081c0.154,0,0.31-0.049,0.442-0.146l6.24-4.532 c0.197-0.145,0.312-0.369,0.312-0.607C20.295,14.803,20.177,14.58,19.982,14.438z" /><path d="M15.026,0.002C6.726,0.002,0,6.728,0,15.028c0,8.297,6.726,15.021,15.026,15.021c8.298,0,15.025-6.725,15.025-15.021 C30.052,6.728,23.324,0.002,15.026,0.002z M15.026,27.542c-6.912,0-12.516-5.601-12.516-12.514c0-6.91,5.604-12.518,12.516-12.518 c6.911,0,12.514,5.607,12.514,12.518C27.541,21.941,21.937,27.542,15.026,27.542z" /></svg>,
pause: (
<svg
enable-background="new 0 0 511.448 511.448"
height="25px"
width="25px"
viewBox="0 0 511.448 511.448"
fill="currentColor"
>
<path d="m436.508 74.94c-99.913-99.913-261.64-99.928-361.567 0-99.913 99.913-99.928 261.64 0 361.567 99.913 99.913 261.64 99.928 361.567 0 99.912-99.912 99.927-261.639 0-361.567zm-180.784 394.45c-117.816 0-213.667-95.851-213.667-213.667s95.851-213.666 213.667-213.666 213.666 95.851 213.666 213.667-95.85 213.666-213.666 213.666z" />
<path d="m298.39 160.057c-11.598 0-21 9.402-21 21v149.333c0 11.598 9.402 21 21 21s21-9.402 21-21v-149.333c0-11.598-9.401-21-21-21z" />
<path d="m213.057 160.057c-11.598 0-21 9.402-21 21v149.333c0 11.598 9.402 21 21 21s21-9.402 21-21v-149.333c0-11.598-9.401-21-21-21z" />
</svg>
),
pause:
<svg enable-background="new 0 0 511.448 511.448" height="25px" width="25px" viewBox="0 0 511.448 511.448" fill="currentColor"><path d="m436.508 74.94c-99.913-99.913-261.64-99.928-361.567 0-99.913 99.913-99.928 261.64 0 361.567 99.913 99.913 261.64 99.928 361.567 0 99.912-99.912 99.927-261.639 0-361.567zm-180.784 394.45c-117.816 0-213.667-95.851-213.667-213.667s95.851-213.666 213.667-213.666 213.666 95.851 213.666 213.667-95.85 213.666-213.666 213.666z" /><path d="m298.39 160.057c-11.598 0-21 9.402-21 21v149.333c0 11.598 9.402 21 21 21s21-9.402 21-21v-149.333c0-11.598-9.401-21-21-21z" /><path d="m213.057 160.057c-11.598 0-21 9.402-21 21v149.333c0 11.598 9.402 21 21 21s21-9.402 21-21v-149.333c0-11.598-9.401-21-21-21z" /></svg>,
user:
<svg fill="currentColor" width="24px" height="24px" viewBox="0 0 478.024 478.024" style="enable-background:new 0 0 478.024 478.024;">
<path d="M411.703,73.561c-45.117-47.093-107.542-73.67-172.76-73.55C107.145-0.155,0.166,106.554,0,238.353
user: (
<svg
fill="currentColor"
width="24px"
height="24px"
viewBox="0 0 478.024 478.024"
style="enable-background:new 0 0 478.024 478.024;"
>
<path
d="M411.703,73.561c-45.117-47.093-107.542-73.67-172.76-73.55C107.145-0.155,0.166,106.554,0,238.353
c-0.082,65.17,26.492,127.538,73.55,172.623c0.137,0.136,0.188,0.341,0.324,0.461c1.382,1.331,2.884,2.458,4.284,3.738
c3.84,3.413,7.68,6.946,11.725,10.24c2.167,1.707,4.42,3.413,6.639,4.983c3.823,2.85,7.646,5.7,11.639,8.329
c2.714,1.707,5.513,3.413,8.294,5.12c3.686,2.219,7.356,4.454,11.162,6.485c3.226,1.707,6.519,3.174,9.796,4.727
@ -59,75 +204,222 @@ export default {
C172.612,211.334,160.951,169.713,179.43,136.849z M405.753,357.336L405.753,357.336c-10.952-51.083-44.59-94.39-91.375-117.64
c38.245-41.661,35.475-106.438-6.186-144.683c-41.661-38.245-106.438-35.475-144.683,6.186
c-35.954,39.166-35.954,99.332,0,138.497c-46.785,23.251-80.423,66.557-91.375,117.64C6.69,265.153,28.366,137.371,120.549,71.927
s219.965-43.768,285.409,48.415c24.601,34.653,37.807,76.104,37.786,118.602C443.744,281.405,430.46,322.802,405.753,357.336z" />
</svg>
,
group:
<svg x="0px" y="0px" width="24px" height="24px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" fill="currentColor">
<g>
s219.965-43.768,285.409,48.415c24.601,34.653,37.807,76.104,37.786,118.602C443.744,281.405,430.46,322.802,405.753,357.336z"
/>
</svg>
),
group: (
<svg
x="0px"
y="0px"
width="24px"
height="24px"
viewBox="0 0 512 512"
style="enable-background:new 0 0 512 512;"
fill="currentColor"
>
<g>
<path d="M331.109,31.459c-12.983,0-25.378,2.544-36.743,7.121c20.261,25.057,32.417,56.924,32.417,91.583
<g>
<path
d="M331.109,31.459c-12.983,0-25.378,2.544-36.743,7.121c20.261,25.057,32.417,56.924,32.417,91.583
s-12.157,66.525-32.417,91.583c11.365,4.577,23.76,7.121,36.743,7.121c54.426,0,98.704-44.277,98.704-98.703
S385.535,31.459,331.109,31.459z" />
</g>
</g>
<g>
<g>
<path d="M331.109,276.055c-3.682,0-7.358,0.116-11.026,0.341c54.027,41.747,88.888,107.149,88.888,180.55
c0,8.271-1.439,16.209-4.058,23.594h83.493c13.03,0,23.594-10.564,23.594-23.594C512,357.203,430.852,276.055,331.109,276.055z" />
</g>
</g>
<g>
<g>
<path d="M256,66.228c-18.119-21.252-45.058-34.769-75.109-34.769c-54.426,0-98.704,44.277-98.704,98.704
s44.277,98.703,98.704,98.703c30.052,0,56.99-13.516,75.109-34.769c14.695-17.238,23.594-39.558,23.594-63.935
S270.695,83.466,256,66.228z" />
</g>
</g>
<g>
<g>
<path d="M255.964,292.386c-22.88-10.479-48.304-16.331-75.073-16.331C81.148,276.055,0,357.203,0,456.946
c0,13.03,10.564,23.594,23.594,23.594h314.593c0.002,0,0.002,0,0.002,0c13.03-0.002,23.593-10.566,23.593-23.594
C361.782,383.969,318.341,320.956,255.964,292.386z" />
</g>
</g>
</svg>
,
heartEmpty: <svg width="24" viewBox="0 -28 512.001 512"><path fill="currentColor" d="m256 455.515625c-7.289062 0-14.316406-2.640625-19.792969-7.4375-20.683593-18.085937-40.625-35.082031-58.21875-50.074219l-.089843-.078125c-51.582032-43.957031-96.125-81.917969-127.117188-119.3125-34.644531-41.804687-50.78125-81.441406-50.78125-124.742187 0-42.070313 14.425781-80.882813 40.617188-109.292969 26.503906-28.746094 62.871093-44.578125 102.414062-44.578125 29.554688 0 56.621094 9.34375 80.445312 27.769531 12.023438 9.300781 22.921876 20.683594 32.523438 33.960938 9.605469-13.277344 20.5-24.660157 32.527344-33.960938 23.824218-18.425781 50.890625-27.769531 80.445312-27.769531 39.539063 0 75.910156 15.832031 102.414063 44.578125 26.191406 28.410156 40.613281 67.222656 40.613281 109.292969 0 43.300781-16.132812 82.9375-50.777344 124.738281-30.992187 37.398437-75.53125 75.355469-127.105468 119.308594-17.625 15.015625-37.597657 32.039062-58.328126 50.167969-5.472656 4.789062-12.503906 7.429687-19.789062 7.429687zm-112.96875-425.523437c-31.066406 0-59.605469 12.398437-80.367188 34.914062-21.070312 22.855469-32.675781 54.449219-32.675781 88.964844 0 36.417968 13.535157 68.988281 43.882813 105.605468 29.332031 35.394532 72.960937 72.574219 123.476562 115.625l.09375.078126c17.660156 15.050781 37.679688 32.113281 58.515625 50.332031 20.960938-18.253907 41.011719-35.34375 58.707031-50.417969 50.511719-43.050781 94.136719-80.222656 123.46875-115.617188 30.34375-36.617187 43.878907-69.1875 43.878907-105.605468 0-34.515625-11.605469-66.109375-32.675781-88.964844-20.757813-22.515625-49.300782-34.914062-80.363282-34.914062-22.757812 0-43.652344 7.234374-62.101562 21.5-16.441406 12.71875-27.894532 28.796874-34.609375 40.046874-3.453125 5.785157-9.53125 9.238282-16.261719 9.238282s-12.808594-3.453125-16.261719-9.238282c-6.710937-11.25-18.164062-27.328124-34.609375-40.046874-18.449218-14.265626-39.34375-21.5-62.097656-21.5zm0 0" /></svg>,
heartFull: <svg width="24" viewBox="0 -28 512.00002 512"><path fill="currentColor" d="m471.382812 44.578125c-26.503906-28.746094-62.871093-44.578125-102.410156-44.578125-29.554687 0-56.621094 9.34375-80.449218 27.769531-12.023438 9.300781-22.917969 20.679688-32.523438 33.960938-9.601562-13.277344-20.5-24.660157-32.527344-33.960938-23.824218-18.425781-50.890625-27.769531-80.445312-27.769531-39.539063 0-75.910156 15.832031-102.414063 44.578125-26.1875 28.410156-40.613281 67.222656-40.613281 109.292969 0 43.300781 16.136719 82.9375 50.78125 124.742187 30.992188 37.394531 75.535156 75.355469 127.117188 119.3125 17.613281 15.011719 37.578124 32.027344 58.308593 50.152344 5.476563 4.796875 12.503907 7.4375 19.792969 7.4375 7.285156 0 14.316406-2.640625 19.785156-7.429687 20.730469-18.128907 40.707032-35.152344 58.328125-50.171876 51.574219-43.949218 96.117188-81.90625 127.109375-119.304687 34.644532-41.800781 50.777344-81.4375 50.777344-124.742187 0-42.066407-14.425781-80.878907-40.617188-109.289063zm0 0" /></svg>,
herokuButton:
<svg width="147px" height="32px" viewBox="0 0 147 32" version="1.1">
<g>
<g>
<rect fill="#7056BF" x="0" y="0" width="147" height="32" rx="4" />
<g transform="translate(10.000000, 8.000000)" fill="#FFFFFF">
<path d="M14.819,3.216 L9.103,0.25 C8.464,-0.082 7.536,-0.081 6.898,0.25 L1.181,3.216 C0.496,3.571 0,4.365 0,5.102 L0,11.035 C0,11.774 0.497,12.566 1.181,12.921 L4.039,14.404 C4.529,14.656 5.134,14.467 5.388,13.978 C5.642,13.487 5.451,12.884 4.961,12.629 L2.106,11.148 C2.068,11.124 2.008,11.039 2,11.035 L1.996,5.143 C2.008,5.098 2.068,5.013 2.103,4.991 L7.816,2.026 C7.897,1.991 8.106,1.992 8.181,2.025 L13.894,4.989 C13.932,5.013 13.992,5.098 14,5.102 L14.003,10.995 C13.992,11.039 13.932,11.124 13.898,11.146 L11.039,12.629 C10.549,12.884 10.358,13.487 10.612,13.978 C10.79,14.32 11.14,14.517 11.501,14.517 C11.656,14.517 11.814,14.481 11.961,14.404 L14.818,12.921 C15.503,12.566 16,11.774 16,11.035 L16,5.102 C16,4.365 15.504,3.571 14.819,3.216" />
<path d="M11.707,9.707 C12.098,9.316 12.098,8.684 11.707,8.293 L8.708,5.294 C8.616,5.201 8.505,5.128 8.382,5.077 C8.138,4.976 7.862,4.976 7.618,5.077 C7.495,5.128 7.385,5.201 7.292,5.294 L4.293,8.293 C3.902,8.684 3.902,9.316 4.293,9.707 C4.488,9.902 4.744,10 5,10 C5.256,10 5.512,9.902 5.707,9.707 L7,8.414 L7,15 C7,15.553 7.447,16 8,16 C8.553,16 9,15.553 9,15 L9,8.414 L10.293,9.707 C10.488,9.902 10.744,10 11,10 C11.256,10 11.512,9.902 11.707,9.707" />
</g>
<path d="M81.393,21.091 C81.744,21.091 82.173,21.052 82.368,21.013 L82.368,20.09 C82.186,20.142 81.913,20.181 81.666,20.181 C80.834,20.181 80.6,19.817 80.6,19.089 L80.6,15.059 L82.381,15.059 L82.381,14.136 L80.6,14.136 L80.6,11.692 L79.482,11.692 L79.482,14.136 L78.286,14.136 L78.286,15.059 L79.482,15.059 L79.482,19.336 C79.482,20.532 79.95,21.091 81.393,21.091 Z M86.697,21.143 C88.374,21.143 89.882,19.921 89.882,17.568 C89.882,15.202 88.374,13.993 86.697,13.993 C85.007,13.993 83.499,15.202 83.499,17.568 C83.499,19.921 85.02,21.143 86.697,21.143 Z M86.697,20.194 C85.306,20.194 84.63,19.024 84.63,17.568 C84.63,16.021 85.384,14.955 86.697,14.955 C88.062,14.955 88.751,16.138 88.751,17.568 C88.751,19.141 87.997,20.194 86.697,20.194 Z M94.705,21 L95.849,21 L95.849,16.463 L100.802,16.463 L100.802,21 L101.946,21 L101.946,11.38 L100.802,11.38 L100.802,15.41 L95.849,15.41 L95.849,11.38 L94.705,11.38 L94.705,21 Z M106.834,21.143 C108.121,21.143 108.992,20.597 109.629,19.687 L108.979,19.115 C108.459,19.83 107.9,20.233 106.912,20.233 C105.781,20.233 104.884,19.401 104.845,17.854 L109.694,17.854 L109.694,17.62 C109.694,15.137 108.459,13.993 106.834,13.993 C105.391,13.993 103.727,15.072 103.727,17.568 C103.727,19.96 105.209,21.143 106.834,21.143 Z M104.871,16.983 C105.027,15.566 105.898,14.929 106.821,14.929 C107.952,14.929 108.537,15.761 108.628,16.983 L104.871,16.983 Z M111.293,21 L112.411,21 L112.411,16.567 C112.918,15.592 113.737,15.02 114.829,15.02 C114.868,15.02 115.115,15.02 115.154,15.033 L115.232,13.993 L115.089,13.993 C113.75,13.993 112.944,14.617 112.437,15.41 L112.411,15.41 L112.411,14.136 L111.293,14.136 L111.293,21 Z M119.301,21.143 C120.978,21.143 122.486,19.921 122.486,17.568 C122.486,15.202 120.978,13.993 119.301,13.993 C117.611,13.993 116.103,15.202 116.103,17.568 C116.103,19.921 117.624,21.143 119.301,21.143 Z M119.301,20.194 C117.91,20.194 117.234,19.024 117.234,17.568 C117.234,16.021 117.988,14.955 119.301,14.955 C120.666,14.955 121.355,16.138 121.355,17.568 C121.355,19.141 120.601,20.194 119.301,20.194 Z M124.072,21 L125.19,21 L125.19,18.894 L126.555,17.425 L128.583,21 L129.779,21 L127.283,16.593 L129.532,14.136 L128.258,14.136 L125.19,17.568 L125.19,11.38 L124.072,11.38 L124.072,21 Z M133.055,21.13 C134.173,21.13 135.031,20.558 135.629,19.973 L135.629,21 L136.76,21 L136.76,14.136 L135.629,14.136 L135.629,19.089 C134.914,19.765 134.238,20.194 133.406,20.194 C132.535,20.194 132.145,19.765 132.145,18.855 L132.145,14.136 L131.027,14.136 L131.027,19.089 C131.027,20.389 131.742,21.13 133.055,21.13 L133.055,21.13 Z" fill="#B7A7D5" />
<path d="M34.183,21 L36.718,21 C39.773,21 41.567,19.427 41.567,16.164 C41.567,12.94 39.812,11.38 36.718,11.38 L34.183,11.38 L34.183,21 Z M35.327,19.973 L35.327,12.433 L36.835,12.433 C39.175,12.433 40.423,13.577 40.423,16.164 C40.423,18.842 39.188,19.973 36.861,19.973 L35.327,19.973 Z M46,21.143 C47.287,21.143 48.158,20.597 48.795,19.687 L48.145,19.115 C47.625,19.83 47.066,20.233 46.078,20.233 C44.947,20.233 44.05,19.401 44.011,17.854 L48.86,17.854 L48.86,17.62 C48.86,15.137 47.625,13.993 46,13.993 C44.557,13.993 42.893,15.072 42.893,17.568 C42.893,19.96 44.375,21.143 46,21.143 Z M44.037,16.983 C44.193,15.566 45.064,14.929 45.987,14.929 C47.118,14.929 47.703,15.761 47.794,16.983 L44.037,16.983 Z M50.459,23.6 L51.577,23.6 L51.577,20.116 C52.162,20.727 52.877,21.104 53.722,21.104 C55.399,21.104 56.634,19.947 56.634,17.555 C56.634,15.163 55.412,13.993 53.839,13.993 C52.812,13.993 52.097,14.513 51.577,15.085 L51.577,14.136 L50.459,14.136 L50.459,23.6 Z M53.566,20.194 C52.838,20.194 52.175,19.83 51.577,19.154 L51.577,16.008 C52.149,15.384 52.786,14.955 53.579,14.955 C54.684,14.955 55.516,15.813 55.516,17.568 C55.516,19.388 54.762,20.194 53.566,20.194 Z M58.324,21 L59.442,21 L59.442,11.38 L58.324,11.38 L58.324,21 Z M64.304,21.143 C65.981,21.143 67.489,19.921 67.489,17.568 C67.489,15.202 65.981,13.993 64.304,13.993 C62.614,13.993 61.106,15.202 61.106,17.568 C61.106,19.921 62.627,21.143 64.304,21.143 Z M64.304,20.194 C62.913,20.194 62.237,19.024 62.237,17.568 C62.237,16.021 62.991,14.955 64.304,14.955 C65.669,14.955 66.358,16.138 66.358,17.568 C66.358,19.141 65.604,20.194 64.304,20.194 Z M69.465,23.639 C70.804,23.639 71.337,22.989 71.805,21.78 L74.743,14.136 L73.612,14.136 L71.597,19.687 L71.571,19.687 L69.556,14.136 L68.373,14.136 L71.025,21.039 L70.765,21.715 C70.492,22.391 70.154,22.69 69.452,22.69 C68.971,22.69 68.633,22.625 68.438,22.573 L68.178,23.47 C68.477,23.561 68.854,23.639 69.465,23.639 L69.465,23.639 Z" fill="#FFFFFF" />
S385.535,31.459,331.109,31.459z"
/>
</g>
</g>
</svg>
,
menu: <svg fill="currentColor" x="0px" y="0px" viewBox="0 0 384 384" width="24px" height="24px" enable-background="new 0 0 384 384;"><g><rect x="0" y="277.333" width="384" height="42.667" /><rect x="0" y="170.667" width="384" height="42.667" /><rect x="0" y="64" width="384" height="42.667" /></g></svg>,
language: <svg width="14" height="14" style="margin-bottom: -1px; enable-background:new 0 0 469.333 469.333;" x="0px" y="0px" viewBox="0 0 469.333 469.333"><path fill="currentColor" d="M253.227,300.267L253.227,300.267L199.04,246.72l0.64-0.64c37.12-41.387,63.573-88.96,79.147-139.307h62.507V64H192 V21.333h-42.667V64H0v42.453h238.293c-14.4,41.173-36.907,80.213-67.627,114.347c-19.84-22.08-36.267-46.08-49.28-71.467H78.72 c15.573,34.773,36.907,67.627,63.573,97.28l-108.48,107.2L64,384l106.667-106.667l66.347,66.347L253.227,300.267z" /><path fill="currentColor" d="M373.333,192h-42.667l-96,256h42.667l24-64h101.333l24,64h42.667L373.333,192z M317.333,341.333L352,248.853 l34.667,92.48H317.333z" /></svg>,
search: <svg x='0px' y='0px' width='24px' height='24px' viewBox='0 0 28.931 28.932' style='enable-background:new 0 0 28.931 28.932;'><path fill='currentColor' d='M28.344,25.518l-6.114-6.115c1.486-2.067,2.303-4.537,2.303-7.137c0-3.275-1.275-6.355-3.594-8.672 C18.625,1.278,15.543,0,12.266,0C8.99,0,5.909,1.275,3.593,3.594C1.277,5.909,0.001,8.99,0.001,12.266 c0,3.276,1.275,6.356,3.592,8.674c2.316,2.316,5.396,3.594,8.673,3.594c2.599,0,5.067-0.813,7.136-2.303l6.114,6.115 c0.392,0.391,0.902,0.586,1.414,0.586c0.513,0,1.024-0.195,1.414-0.586C29.125,27.564,29.125,26.299,28.344,25.518z M6.422,18.111 c-1.562-1.562-2.421-3.639-2.421-5.846S4.86,7.983,6.422,6.421c1.561-1.562,3.636-2.422,5.844-2.422s4.284,0.86,5.845,2.422 c1.562,1.562,2.422,3.638,2.422,5.845s-0.859,4.283-2.422,5.846c-1.562,1.562-3.636,2.42-5.845,2.42S7.981,19.672,6.422,18.111z' /></svg>,
backArrow:
<svg fill="currentColor" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 447.243 447.243" style="enable-background:new 0 0 447.243 447.243;" xmlSpace="preserve">
</g>
<g>
<g>
<path d="M420.361,192.229c-1.83-0.297-3.682-0.434-5.535-0.41H99.305l6.88-3.2c6.725-3.183,12.843-7.515,18.08-12.8l88.48-88.48
<path
d="M331.109,276.055c-3.682,0-7.358,0.116-11.026,0.341c54.027,41.747,88.888,107.149,88.888,180.55
c0,8.271-1.439,16.209-4.058,23.594h83.493c13.03,0,23.594-10.564,23.594-23.594C512,357.203,430.852,276.055,331.109,276.055z"
/>
</g>
</g>
<g>
<g>
<path
d="M256,66.228c-18.119-21.252-45.058-34.769-75.109-34.769c-54.426,0-98.704,44.277-98.704,98.704
s44.277,98.703,98.704,98.703c30.052,0,56.99-13.516,75.109-34.769c14.695-17.238,23.594-39.558,23.594-63.935
S270.695,83.466,256,66.228z"
/>
</g>
</g>
<g>
<g>
<path
d="M255.964,292.386c-22.88-10.479-48.304-16.331-75.073-16.331C81.148,276.055,0,357.203,0,456.946
c0,13.03,10.564,23.594,23.594,23.594h314.593c0.002,0,0.002,0,0.002,0c13.03-0.002,23.593-10.566,23.593-23.594
C361.782,383.969,318.341,320.956,255.964,292.386z"
/>
</g>
</g>
</svg>
),
heartEmpty: (
<svg width="24" viewBox="0 -28 512.001 512">
<path
fill="currentColor"
d="m256 455.515625c-7.289062 0-14.316406-2.640625-19.792969-7.4375-20.683593-18.085937-40.625-35.082031-58.21875-50.074219l-.089843-.078125c-51.582032-43.957031-96.125-81.917969-127.117188-119.3125-34.644531-41.804687-50.78125-81.441406-50.78125-124.742187 0-42.070313 14.425781-80.882813 40.617188-109.292969 26.503906-28.746094 62.871093-44.578125 102.414062-44.578125 29.554688 0 56.621094 9.34375 80.445312 27.769531 12.023438 9.300781 22.921876 20.683594 32.523438 33.960938 9.605469-13.277344 20.5-24.660157 32.527344-33.960938 23.824218-18.425781 50.890625-27.769531 80.445312-27.769531 39.539063 0 75.910156 15.832031 102.414063 44.578125 26.191406 28.410156 40.613281 67.222656 40.613281 109.292969 0 43.300781-16.132812 82.9375-50.777344 124.738281-30.992187 37.398437-75.53125 75.355469-127.105468 119.308594-17.625 15.015625-37.597657 32.039062-58.328126 50.167969-5.472656 4.789062-12.503906 7.429687-19.789062 7.429687zm-112.96875-425.523437c-31.066406 0-59.605469 12.398437-80.367188 34.914062-21.070312 22.855469-32.675781 54.449219-32.675781 88.964844 0 36.417968 13.535157 68.988281 43.882813 105.605468 29.332031 35.394532 72.960937 72.574219 123.476562 115.625l.09375.078126c17.660156 15.050781 37.679688 32.113281 58.515625 50.332031 20.960938-18.253907 41.011719-35.34375 58.707031-50.417969 50.511719-43.050781 94.136719-80.222656 123.46875-115.617188 30.34375-36.617187 43.878907-69.1875 43.878907-105.605468 0-34.515625-11.605469-66.109375-32.675781-88.964844-20.757813-22.515625-49.300782-34.914062-80.363282-34.914062-22.757812 0-43.652344 7.234374-62.101562 21.5-16.441406 12.71875-27.894532 28.796874-34.609375 40.046874-3.453125 5.785157-9.53125 9.238282-16.261719 9.238282s-12.808594-3.453125-16.261719-9.238282c-6.710937-11.25-18.164062-27.328124-34.609375-40.046874-18.449218-14.265626-39.34375-21.5-62.097656-21.5zm0 0"
/>
</svg>
),
heartFull: (
<svg width="24" viewBox="0 -28 512.00002 512">
<path
fill="currentColor"
d="m471.382812 44.578125c-26.503906-28.746094-62.871093-44.578125-102.410156-44.578125-29.554687 0-56.621094 9.34375-80.449218 27.769531-12.023438 9.300781-22.917969 20.679688-32.523438 33.960938-9.601562-13.277344-20.5-24.660157-32.527344-33.960938-23.824218-18.425781-50.890625-27.769531-80.445312-27.769531-39.539063 0-75.910156 15.832031-102.414063 44.578125-26.1875 28.410156-40.613281 67.222656-40.613281 109.292969 0 43.300781 16.136719 82.9375 50.78125 124.742187 30.992188 37.394531 75.535156 75.355469 127.117188 119.3125 17.613281 15.011719 37.578124 32.027344 58.308593 50.152344 5.476563 4.796875 12.503907 7.4375 19.792969 7.4375 7.285156 0 14.316406-2.640625 19.785156-7.429687 20.730469-18.128907 40.707032-35.152344 58.328125-50.171876 51.574219-43.949218 96.117188-81.90625 127.109375-119.304687 34.644532-41.800781 50.777344-81.4375 50.777344-124.742187 0-42.066407-14.425781-80.878907-40.617188-109.289063zm0 0"
/>
</svg>
),
herokuButton: (
<svg width="147px" height="32px" viewBox="0 0 147 32" version="1.1">
<g>
<g>
<rect fill="#7056BF" x="0" y="0" width="147" height="32" rx="4" />
<g transform="translate(10.000000, 8.000000)" fill="#FFFFFF">
<path d="M14.819,3.216 L9.103,0.25 C8.464,-0.082 7.536,-0.081 6.898,0.25 L1.181,3.216 C0.496,3.571 0,4.365 0,5.102 L0,11.035 C0,11.774 0.497,12.566 1.181,12.921 L4.039,14.404 C4.529,14.656 5.134,14.467 5.388,13.978 C5.642,13.487 5.451,12.884 4.961,12.629 L2.106,11.148 C2.068,11.124 2.008,11.039 2,11.035 L1.996,5.143 C2.008,5.098 2.068,5.013 2.103,4.991 L7.816,2.026 C7.897,1.991 8.106,1.992 8.181,2.025 L13.894,4.989 C13.932,5.013 13.992,5.098 14,5.102 L14.003,10.995 C13.992,11.039 13.932,11.124 13.898,11.146 L11.039,12.629 C10.549,12.884 10.358,13.487 10.612,13.978 C10.79,14.32 11.14,14.517 11.501,14.517 C11.656,14.517 11.814,14.481 11.961,14.404 L14.818,12.921 C15.503,12.566 16,11.774 16,11.035 L16,5.102 C16,4.365 15.504,3.571 14.819,3.216" />
<path d="M11.707,9.707 C12.098,9.316 12.098,8.684 11.707,8.293 L8.708,5.294 C8.616,5.201 8.505,5.128 8.382,5.077 C8.138,4.976 7.862,4.976 7.618,5.077 C7.495,5.128 7.385,5.201 7.292,5.294 L4.293,8.293 C3.902,8.684 3.902,9.316 4.293,9.707 C4.488,9.902 4.744,10 5,10 C5.256,10 5.512,9.902 5.707,9.707 L7,8.414 L7,15 C7,15.553 7.447,16 8,16 C8.553,16 9,15.553 9,15 L9,8.414 L10.293,9.707 C10.488,9.902 10.744,10 11,10 C11.256,10 11.512,9.902 11.707,9.707" />
</g>
<path
d="M81.393,21.091 C81.744,21.091 82.173,21.052 82.368,21.013 L82.368,20.09 C82.186,20.142 81.913,20.181 81.666,20.181 C80.834,20.181 80.6,19.817 80.6,19.089 L80.6,15.059 L82.381,15.059 L82.381,14.136 L80.6,14.136 L80.6,11.692 L79.482,11.692 L79.482,14.136 L78.286,14.136 L78.286,15.059 L79.482,15.059 L79.482,19.336 C79.482,20.532 79.95,21.091 81.393,21.091 Z M86.697,21.143 C88.374,21.143 89.882,19.921 89.882,17.568 C89.882,15.202 88.374,13.993 86.697,13.993 C85.007,13.993 83.499,15.202 83.499,17.568 C83.499,19.921 85.02,21.143 86.697,21.143 Z M86.697,20.194 C85.306,20.194 84.63,19.024 84.63,17.568 C84.63,16.021 85.384,14.955 86.697,14.955 C88.062,14.955 88.751,16.138 88.751,17.568 C88.751,19.141 87.997,20.194 86.697,20.194 Z M94.705,21 L95.849,21 L95.849,16.463 L100.802,16.463 L100.802,21 L101.946,21 L101.946,11.38 L100.802,11.38 L100.802,15.41 L95.849,15.41 L95.849,11.38 L94.705,11.38 L94.705,21 Z M106.834,21.143 C108.121,21.143 108.992,20.597 109.629,19.687 L108.979,19.115 C108.459,19.83 107.9,20.233 106.912,20.233 C105.781,20.233 104.884,19.401 104.845,17.854 L109.694,17.854 L109.694,17.62 C109.694,15.137 108.459,13.993 106.834,13.993 C105.391,13.993 103.727,15.072 103.727,17.568 C103.727,19.96 105.209,21.143 106.834,21.143 Z M104.871,16.983 C105.027,15.566 105.898,14.929 106.821,14.929 C107.952,14.929 108.537,15.761 108.628,16.983 L104.871,16.983 Z M111.293,21 L112.411,21 L112.411,16.567 C112.918,15.592 113.737,15.02 114.829,15.02 C114.868,15.02 115.115,15.02 115.154,15.033 L115.232,13.993 L115.089,13.993 C113.75,13.993 112.944,14.617 112.437,15.41 L112.411,15.41 L112.411,14.136 L111.293,14.136 L111.293,21 Z M119.301,21.143 C120.978,21.143 122.486,19.921 122.486,17.568 C122.486,15.202 120.978,13.993 119.301,13.993 C117.611,13.993 116.103,15.202 116.103,17.568 C116.103,19.921 117.624,21.143 119.301,21.143 Z M119.301,20.194 C117.91,20.194 117.234,19.024 117.234,17.568 C117.234,16.021 117.988,14.955 119.301,14.955 C120.666,14.955 121.355,16.138 121.355,17.568 C121.355,19.141 120.601,20.194 119.301,20.194 Z M124.072,21 L125.19,21 L125.19,18.894 L126.555,17.425 L128.583,21 L129.779,21 L127.283,16.593 L129.532,14.136 L128.258,14.136 L125.19,17.568 L125.19,11.38 L124.072,11.38 L124.072,21 Z M133.055,21.13 C134.173,21.13 135.031,20.558 135.629,19.973 L135.629,21 L136.76,21 L136.76,14.136 L135.629,14.136 L135.629,19.089 C134.914,19.765 134.238,20.194 133.406,20.194 C132.535,20.194 132.145,19.765 132.145,18.855 L132.145,14.136 L131.027,14.136 L131.027,19.089 C131.027,20.389 131.742,21.13 133.055,21.13 L133.055,21.13 Z"
fill="#B7A7D5"
/>
<path
d="M34.183,21 L36.718,21 C39.773,21 41.567,19.427 41.567,16.164 C41.567,12.94 39.812,11.38 36.718,11.38 L34.183,11.38 L34.183,21 Z M35.327,19.973 L35.327,12.433 L36.835,12.433 C39.175,12.433 40.423,13.577 40.423,16.164 C40.423,18.842 39.188,19.973 36.861,19.973 L35.327,19.973 Z M46,21.143 C47.287,21.143 48.158,20.597 48.795,19.687 L48.145,19.115 C47.625,19.83 47.066,20.233 46.078,20.233 C44.947,20.233 44.05,19.401 44.011,17.854 L48.86,17.854 L48.86,17.62 C48.86,15.137 47.625,13.993 46,13.993 C44.557,13.993 42.893,15.072 42.893,17.568 C42.893,19.96 44.375,21.143 46,21.143 Z M44.037,16.983 C44.193,15.566 45.064,14.929 45.987,14.929 C47.118,14.929 47.703,15.761 47.794,16.983 L44.037,16.983 Z M50.459,23.6 L51.577,23.6 L51.577,20.116 C52.162,20.727 52.877,21.104 53.722,21.104 C55.399,21.104 56.634,19.947 56.634,17.555 C56.634,15.163 55.412,13.993 53.839,13.993 C52.812,13.993 52.097,14.513 51.577,15.085 L51.577,14.136 L50.459,14.136 L50.459,23.6 Z M53.566,20.194 C52.838,20.194 52.175,19.83 51.577,19.154 L51.577,16.008 C52.149,15.384 52.786,14.955 53.579,14.955 C54.684,14.955 55.516,15.813 55.516,17.568 C55.516,19.388 54.762,20.194 53.566,20.194 Z M58.324,21 L59.442,21 L59.442,11.38 L58.324,11.38 L58.324,21 Z M64.304,21.143 C65.981,21.143 67.489,19.921 67.489,17.568 C67.489,15.202 65.981,13.993 64.304,13.993 C62.614,13.993 61.106,15.202 61.106,17.568 C61.106,19.921 62.627,21.143 64.304,21.143 Z M64.304,20.194 C62.913,20.194 62.237,19.024 62.237,17.568 C62.237,16.021 62.991,14.955 64.304,14.955 C65.669,14.955 66.358,16.138 66.358,17.568 C66.358,19.141 65.604,20.194 64.304,20.194 Z M69.465,23.639 C70.804,23.639 71.337,22.989 71.805,21.78 L74.743,14.136 L73.612,14.136 L71.597,19.687 L71.571,19.687 L69.556,14.136 L68.373,14.136 L71.025,21.039 L70.765,21.715 C70.492,22.391 70.154,22.69 69.452,22.69 C68.971,22.69 68.633,22.625 68.438,22.573 L68.178,23.47 C68.477,23.561 68.854,23.639 69.465,23.639 L69.465,23.639 Z"
fill="#FFFFFF"
/>
</g>
</g>
</svg>
),
menu: (
<svg
fill="currentColor"
x="0px"
y="0px"
viewBox="0 0 384 384"
width="24px"
height="24px"
enable-background="new 0 0 384 384;"
>
<g>
<rect x="0" y="277.333" width="384" height="42.667" />
<rect x="0" y="170.667" width="384" height="42.667" />
<rect x="0" y="64" width="384" height="42.667" />
</g>
</svg>
),
language: (
<svg
width="14"
height="14"
style="margin-bottom: -1px; enable-background:new 0 0 469.333 469.333;"
x="0px"
y="0px"
viewBox="0 0 469.333 469.333"
>
<path
fill="currentColor"
d="M253.227,300.267L253.227,300.267L199.04,246.72l0.64-0.64c37.12-41.387,63.573-88.96,79.147-139.307h62.507V64H192 V21.333h-42.667V64H0v42.453h238.293c-14.4,41.173-36.907,80.213-67.627,114.347c-19.84-22.08-36.267-46.08-49.28-71.467H78.72 c15.573,34.773,36.907,67.627,63.573,97.28l-108.48,107.2L64,384l106.667-106.667l66.347,66.347L253.227,300.267z"
/>
<path
fill="currentColor"
d="M373.333,192h-42.667l-96,256h42.667l24-64h101.333l24,64h42.667L373.333,192z M317.333,341.333L352,248.853 l34.667,92.48H317.333z"
/>
</svg>
),
search: (
<svg
x="0px"
y="0px"
width="24px"
height="24px"
viewBox="0 0 28.931 28.932"
style="enable-background:new 0 0 28.931 28.932;"
>
<path
fill="currentColor"
d="M28.344,25.518l-6.114-6.115c1.486-2.067,2.303-4.537,2.303-7.137c0-3.275-1.275-6.355-3.594-8.672 C18.625,1.278,15.543,0,12.266,0C8.99,0,5.909,1.275,3.593,3.594C1.277,5.909,0.001,8.99,0.001,12.266 c0,3.276,1.275,6.356,3.592,8.674c2.316,2.316,5.396,3.594,8.673,3.594c2.599,0,5.067-0.813,7.136-2.303l6.114,6.115 c0.392,0.391,0.902,0.586,1.414,0.586c0.513,0,1.024-0.195,1.414-0.586C29.125,27.564,29.125,26.299,28.344,25.518z M6.422,18.111 c-1.562-1.562-2.421-3.639-2.421-5.846S4.86,7.983,6.422,6.421c1.561-1.562,3.636-2.422,5.844-2.422s4.284,0.86,5.845,2.422 c1.562,1.562,2.422,3.638,2.422,5.845s-0.859,4.283-2.422,5.846c-1.562,1.562-3.636,2.42-5.845,2.42S7.981,19.672,6.422,18.111z"
/>
</svg>
),
backArrow: (
<svg
fill="currentColor"
x="0px"
y="0px"
width="24px"
height="24px"
viewBox="0 0 447.243 447.243"
style="enable-background:new 0 0 447.243 447.243;"
xmlSpace="preserve"
>
<g>
<path
d="M420.361,192.229c-1.83-0.297-3.682-0.434-5.535-0.41H99.305l6.88-3.2c6.725-3.183,12.843-7.515,18.08-12.8l88.48-88.48
c11.653-11.124,13.611-29.019,4.64-42.4c-10.441-14.259-30.464-17.355-44.724-6.914c-1.152,0.844-2.247,1.764-3.276,2.754
l-160,160C-3.119,213.269-3.13,233.53,9.36,246.034c0.008,0.008,0.017,0.017,0.025,0.025l160,160
c12.514,12.479,32.775,12.451,45.255-0.063c0.982-0.985,1.899-2.033,2.745-3.137c8.971-13.381,7.013-31.276-4.64-42.4
l-88.32-88.64c-4.695-4.7-10.093-8.641-16-11.68l-9.6-4.32h314.24c16.347,0.607,30.689-10.812,33.76-26.88
C449.654,211.494,437.806,195.059,420.361,192.229z" />
C449.654,211.494,437.806,195.059,420.361,192.229z"
/>
</g>
</svg>
),
info: (
<svg fill="currentColor" viewBox="0 0 24 24" width="24px" height="24px">
<path d="M 12 2 C 6.4889971 2 2 6.4889971 2 12 C 2 17.511003 6.4889971 22 12 22 C 17.511003 22 22 17.511003 22 12 C 22 6.4889971 17.511003 2 12 2 z M 12 4 C 16.430123 4 20 7.5698774 20 12 C 20 16.430123 16.430123 20 12 20 C 7.5698774 20 4 16.430123 4 12 C 4 7.5698774 7.5698774 4 12 4 z M 11 7 L 11 9 L 13 9 L 13 7 L 11 7 z M 11 11 L 11 17 L 13 17 L 13 11 L 11 11 z" />
</svg>
),
network: (
<svg
fill="currentColor"
enable-background="new 0 0 512 512"
height="16px"
width="16px"
viewBox="0 0 512 512"
>
<g>
<g>
<g>
<path d="m256 150.5c-41.353 0-75-33.647-75-75s33.647-75 75-75 75 33.647 75 75-33.647 75-75 75z" />
</g>
<g>
<path d="m10.026 429c-20.669-35.815-8.35-81.768 27.466-102.451 36.551-21.085 82.083-7.806 102.451 27.451 20.722 35.87 8.44 81.717-27.451 102.451-35.96 20.737-81.757 8.396-102.466-27.451z" />
</g>
<g>
<path d="m399.508 456.451c-35.867-20.721-48.185-66.561-27.451-102.451 20.367-35.256 65.898-48.537 102.451-27.451 35.815 20.684 48.135 66.636 27.466 102.451-20.683 35.802-66.455 48.218-102.466 27.451z" />
</g>
</g>
</svg>
,
info: <svg fill="currentColor" viewBox="0 0 24 24" width="24px" height="24px"><path d="M 12 2 C 6.4889971 2 2 6.4889971 2 12 C 2 17.511003 6.4889971 22 12 22 C 17.511003 22 22 17.511003 22 12 C 22 6.4889971 17.511003 2 12 2 z M 12 4 C 16.430123 4 20 7.5698774 20 12 C 20 16.430123 16.430123 20 12 20 C 7.5698774 20 4 16.430123 4 12 C 4 7.5698774 7.5698774 4 12 4 z M 11 7 L 11 9 L 13 9 L 13 7 L 11 7 z M 11 11 L 11 17 L 13 17 L 13 11 L 11 11 z" /></svg>,
network: <svg fill="currentColor" enable-background="new 0 0 512 512" height="16px" width="16px" viewBox="0 0 512 512"><g><g><g><path d="m256 150.5c-41.353 0-75-33.647-75-75s33.647-75 75-75 75 33.647 75 75-33.647 75-75 75z" /></g><g><path d="m10.026 429c-20.669-35.815-8.35-81.768 27.466-102.451 36.551-21.085 82.083-7.806 102.451 27.451 20.722 35.87 8.44 81.717-27.451 102.451-35.96 20.737-81.757 8.396-102.466-27.451z" /></g><g><path d="m399.508 456.451c-35.867-20.721-48.185-66.561-27.451-102.451 20.367-35.256 65.898-48.537 102.451-27.451 35.815 20.684 48.135 66.636 27.466 102.451-20.683 35.802-66.455 48.218-102.466 27.451z" /></g></g><g><path d="m61.293 275.587-29.941-1.641c3.896-70.957 41.807-136.641 101.396-175.723l16.465 25.078c-51.665 33.883-84.522 90.821-87.92 152.286z" /></g><g><path d="m450.707 275.587c-3.398-61.465-36.255-118.403-87.92-152.285l16.465-25.078c59.59 39.082 97.5 104.766 101.396 175.723z" /></g><g><path d="m256 511.5c-35.684 0-69.8-8.115-101.426-24.097l13.535-26.777c54.785 27.715 120.996 27.715 175.781 0l13.535 26.777c-31.625 15.982-65.741 24.097-101.425 24.097z" /></g></g></svg>,
QRcode: <svg fill="currentColor" id="Layer_1" x="0px" y="0px" viewBox="0 0 122.88 122.7" style="enable-background:new 0 0 122.88 122.7" width="24px" height="24px"><g><path class="st0" d="M0.18,0h44.63v44.45H0.18V0L0.18,0z M111.5,111.5h11.38v11.2H111.5V111.5L111.5,111.5z M89.63,111.48h11.38 v10.67H89.63h-0.01H78.25v-21.82h11.02V89.27h11.21V67.22h11.38v10.84h10.84v11.2h-10.84v11.2h-11.21h-0.17H89.63V111.48 L89.63,111.48z M55.84,89.09h11.02v-11.2H56.2v-11.2h10.66v-11.2H56.02v11.2H44.63v-11.2h11.2V22.23h11.38v33.25h11.02v11.2h10.84 v-11.2h11.38v11.2H89.63v11.2H78.25v22.05H67.22v22.23H55.84V89.09L55.84,89.09z M111.31,55.48h11.38v11.2h-11.38V55.48 L111.31,55.48z M22.41,55.48h11.38v11.2H22.41V55.48L22.41,55.48z M0.18,55.48h11.38v11.2H0.18V55.48L0.18,55.48z M55.84,0h11.38 v11.2H55.84V0L55.84,0z M0,78.06h44.63v44.45H0V78.06L0,78.06z M10.84,88.86h22.95v22.86H10.84V88.86L10.84,88.86z M78.06,0h44.63 v44.45H78.06V0L78.06,0z M88.91,10.8h22.95v22.86H88.91V10.8L88.91,10.8z M11.02,10.8h22.95v22.86H11.02V10.8L11.02,10.8z"/></g></svg>
<g>
<path d="m61.293 275.587-29.941-1.641c3.896-70.957 41.807-136.641 101.396-175.723l16.465 25.078c-51.665 33.883-84.522 90.821-87.92 152.286z" />
</g>
<g>
<path d="m450.707 275.587c-3.398-61.465-36.255-118.403-87.92-152.285l16.465-25.078c59.59 39.082 97.5 104.766 101.396 175.723z" />
</g>
<g>
<path d="m256 511.5c-35.684 0-69.8-8.115-101.426-24.097l13.535-26.777c54.785 27.715 120.996 27.715 175.781 0l13.535 26.777c-31.625 15.982-65.741 24.097-101.425 24.097z" />
</g>
</g>
</svg>
),
QRcode: (
<svg
fill="currentColor"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 122.88 122.7"
style="enable-background:new 0 0 122.88 122.7"
width="24px"
height="24px"
>
<g>
<path
class="st0"
d="M0.18,0h44.63v44.45H0.18V0L0.18,0z M111.5,111.5h11.38v11.2H111.5V111.5L111.5,111.5z M89.63,111.48h11.38 v10.67H89.63h-0.01H78.25v-21.82h11.02V89.27h11.21V67.22h11.38v10.84h10.84v11.2h-10.84v11.2h-11.21h-0.17H89.63V111.48 L89.63,111.48z M55.84,89.09h11.02v-11.2H56.2v-11.2h10.66v-11.2H56.02v11.2H44.63v-11.2h11.2V22.23h11.38v33.25h11.02v11.2h10.84 v-11.2h11.38v11.2H89.63v11.2H78.25v22.05H67.22v22.23H55.84V89.09L55.84,89.09z M111.31,55.48h11.38v11.2h-11.38V55.48 L111.31,55.48z M22.41,55.48h11.38v11.2H22.41V55.48L22.41,55.48z M0.18,55.48h11.38v11.2H0.18V55.48L0.18,55.48z M55.84,0h11.38 v11.2H55.84V0L55.84,0z M0,78.06h44.63v44.45H0V78.06L0,78.06z M10.84,88.86h22.95v22.86H10.84V88.86L10.84,88.86z M78.06,0h44.63 v44.45H78.06V0L78.06,0z M88.91,10.8h22.95v22.86H88.91V10.8L88.91,10.8z M11.02,10.8h22.95v22.86H11.02V10.8L11.02,10.8z"
/>
</g>
</svg>
),
};

View File

@ -1,34 +1,31 @@
import Component from './BaseComponent';
import { Router, RouterOnChangeArgs, CustomHistory } from 'preact-router';
import AsyncRoute from 'preact-async-route';
import { Helmet } from 'react-helmet';
import { createHashHistory } from 'history';
import {Helmet} from "react-helmet";
import { translationLoaded } from "./translations/Translation";
import Helpers from './Helpers';
import QRScanner from './QRScanner';
import Settings from './views/settings/Settings';
import LogoutConfirmation from './views/LogoutConfirmation';
import Chat from './views/chat/Chat';
import Notifications from './views/Notifications';
import Hashtags from './views/Hashtags';
import Login from './views/Login';
import Profile from './views/Profile';
import Group from './views/Group';
import Message from './views/Message';
import Follows from './views/Follows';
import Feed from './views/Feed';
import About from './views/About';
import Contacts from './views/Contacts';
import Torrent from './views/Torrent';
import iris from 'iris-lib';
import AsyncRoute from 'preact-async-route';
import { CustomHistory, Router, RouterOnChangeArgs } from 'preact-router';
import Footer from './components/Footer';
import MediaPlayer from './components/MediaPlayer';
import Menu from './components/Menu';
import VideoCall from './components/VideoCall';
import MediaPlayer from './components/MediaPlayer';
import Footer from './components/Footer';
import iris from 'iris-lib';
import { translationLoaded } from './translations/Translation';
import About from './views/About';
import Chat from './views/chat/Chat';
import Contacts from './views/Contacts';
import Feed from './views/Feed';
import Follows from './views/Follows';
import Group from './views/Group';
import Hashtags from './views/Hashtags';
import Login from './views/Login';
import LogoutConfirmation from './views/LogoutConfirmation';
import Message from './views/Message';
import Notifications from './views/Notifications';
import Profile from './views/Profile';
import Settings from './views/settings/Settings';
import Torrent from './views/Torrent';
import Component from './BaseComponent';
import Helpers from './Helpers';
import QRScanner from './QRScanner';
import '../css/style.css';
import '../css/cropper.min.css';
@ -37,7 +34,7 @@ if (window.location.host === 'iris.to' && window.location.pathname !== '/') {
window.location.href = window.location.href.replace(window.location.pathname, '/');
}
type Props = {};
type Props = Record<string, unknown>;
type ReactState = {
loggedIn: boolean;
@ -46,82 +43,94 @@ type ReactState = {
activeRoute: string;
platform: string;
translationLoaded: boolean;
}
};
iris.session.init({autologin: window.location.hash.length > 2});
iris.session.init({ autologin: window.location.hash.length > 2 });
class Main extends Component<Props,ReactState> {
class Main extends Component<Props, ReactState> {
componentDidMount() {
iris.local().get('loggedIn').on(this.inject());
iris.local().get('toggleMenu').put(false);
iris.local().get('toggleMenu').on((show: boolean) => this.toggleMenu(show));
iris
.local()
.get('toggleMenu')
.on((show: boolean) => this.toggleMenu(show));
iris.electron && iris.electron.get('platform').on(this.inject());
iris.local().get('unseenMsgsTotal').on(this.inject());
translationLoaded.then(() => this.setState({translationLoaded: true}));
translationLoaded.then(() => this.setState({ translationLoaded: true }));
}
handleRoute(e: RouterOnChangeArgs) {
let activeRoute = e.url;
this.setState({activeRoute});
const activeRoute = e.url;
this.setState({ activeRoute });
iris.local().get('activeRoute').put(activeRoute);
QRScanner.cleanupScanner();
}
onClickOverlay(): void {
if (this.state.showMenu) {
this.setState({showMenu: false});
this.setState({ showMenu: false });
}
}
toggleMenu(show: boolean): void {
this.setState({showMenu: typeof show === 'undefined' ? !this.state.showMenu : show});
this.setState({
showMenu: typeof show === 'undefined' ? !this.state.showMenu : show,
});
}
electronCmd(name: string): void {
iris.electron.get('cmd').put({name, time: new Date().toISOString()});
iris.electron.get('cmd').put({ name, time: new Date().toISOString() });
}
render() {
let title = "";
let title = '';
const s = this.state;
if (s.activeRoute && s.activeRoute.length > 1) {
title = Helpers.capitalize(s.activeRoute.replace('/', ''));
}
const isDesktopNonMac = s.platform && s.platform !== 'darwin';
const titleTemplate = s.unseenMsgsTotal ? `(${s.unseenMsgsTotal}) %s | iris` : "%s | iris";
const titleTemplate = s.unseenMsgsTotal ? `(${s.unseenMsgsTotal}) %s | iris` : '%s | iris';
const defaultTitle = s.unseenMsgsTotal ? `(${s.unseenMsgsTotal}) iris` : 'iris';
if (!s.translationLoaded) {
return (
<div id="main-content" />
);
return <div id="main-content" />;
}
if (!s.loggedIn && window.location.pathname.length > 2) {
return (
<div id="main-content" />
);
return <div id="main-content" />;
}
if (!s.loggedIn) {
return (
<div id="main-content">
<Login/>
<Login />
</div>
)
);
}
const history = createHashHistory() as unknown; // TODO: align types between 'history' and 'preact-router'
return (
<div id="main-content">
{isDesktopNonMac ? (
<div className="windows-titlebar">
<span>iris</span>
<div className="title-bar-btns">
<button className="min-btn" onClick={() => this.electronCmd('minimize')}>-</button>
<button className="max-btn" onClick={() => this.electronCmd('maximize')}>+</button>
<button className="close-btn" onClick={() => this.electronCmd('close')}>x</button>
</div>
</div>
<span>iris</span>
<div className="title-bar-btns">
<button className="min-btn" onClick={() => this.electronCmd('minimize')}>
-
</button>
<button className="max-btn" onClick={() => this.electronCmd('maximize')}>
+
</button>
<button className="close-btn" onClick={() => this.electronCmd('close')}>
x
</button>
</div>
</div>
) : null}
<section className={`main ${isDesktopNonMac ? 'desktop-non-mac' : ''} ${s.showMenu ? 'menu-visible-xs' : ''}`} style="flex-direction: row;">
<Menu/>
<section
className={`main ${isDesktopNonMac ? 'desktop-non-mac' : ''} ${
s.showMenu ? 'menu-visible-xs' : ''
}`}
style="flex-direction: row;"
>
<Menu />
<Helmet titleTemplate={titleTemplate} defaultTitle={defaultTitle}>
<title>{title}</title>
<meta name="description" content="Social Networking Freedom" />
@ -134,65 +143,65 @@ class Main extends Component<Props,ReactState> {
</Helmet>
<div className="overlay" onClick={() => this.onClickOverlay()}></div>
<div className="view-area">
<Router history={history as CustomHistory} onChange={e => this.handleRoute(e)}>
<Feed path="/"/>
<Feed path="/feed"/>
<Hashtags path="/hashtag"/>
<Feed path="/hashtag/:hashtag+"/>
<Feed path="/search/:term?/:type?"/>
<Feed path="/media" index="media" thumbnails/>
<Login path="/login"/>
<Notifications path="/notifications"/>
<Chat path="/chat/hashtag/:hashtag?"/>
<Chat path="/chat/:id?"/>
<Chat path="/chat/new/:id"/>
<Message path="/post/:hash+"/>
<Torrent path="/torrent/:id+"/>
<About path="/about"/>
<Settings path="/settings/:page?"/>
<LogoutConfirmation path="/logout"/>
<Profile path="/profile/:id+" tab="profile"/>
<Profile path="/replies/:id+" tab="replies"/>
<Profile path="/likes/:id+" tab="likes"/>
<Profile path="/media/:id+" tab="media"/>
<Profile path="/nfts/:id+" tab="nfts"/>
<Group path="/group/:id+"/>
<Router history={history as CustomHistory} onChange={(e) => this.handleRoute(e)}>
<Feed path="/" />
<Feed path="/feed" />
<Hashtags path="/hashtag" />
<Feed path="/hashtag/:hashtag+" />
<Feed path="/search/:term?/:type?" />
<Feed path="/media" index="media" thumbnails />
<Login path="/login" />
<Notifications path="/notifications" />
<Chat path="/chat/hashtag/:hashtag?" />
<Chat path="/chat/:id?" />
<Chat path="/chat/new/:id" />
<Message path="/post/:hash+" />
<Torrent path="/torrent/:id+" />
<About path="/about" />
<Settings path="/settings/:page?" />
<LogoutConfirmation path="/logout" />
<Profile path="/profile/:id+" tab="profile" />
<Profile path="/replies/:id+" tab="replies" />
<Profile path="/likes/:id+" tab="likes" />
<Profile path="/media/:id+" tab="media" />
<Profile path="/nfts/:id+" tab="nfts" />
<Group path="/group/:id+" />
{/* Lazy load stuff that is used less often */}
<AsyncRoute
path="/store/:store?"
getComponent={() => import('./views/Store').then(module => module.default)}
path="/store/:store?"
getComponent={() => import('./views/Store').then((module) => module.default)}
/>
<AsyncRoute
path="/checkout/:store?"
getComponent={() => import('./views/Checkout').then(module => module.default)}
path="/checkout/:store?"
getComponent={() => import('./views/Checkout').then((module) => module.default)}
/>
<AsyncRoute
path="/product/:product/:store"
getComponent={() => import('./views/Product').then(module => module.default)}
path="/product/:product/:store"
getComponent={() => import('./views/Product').then((module) => module.default)}
/>
<AsyncRoute
path="/product/new"
store={iris.session.getPubKey()}
getComponent={() => import('./views/Product').then(module => module.default)}
path="/product/new"
store={iris.session.getPubKey()}
getComponent={() => import('./views/Product').then((module) => module.default)}
/>
<AsyncRoute
path="/explorer/:node"
getComponent={() => import('./views/Explorer').then(module => module.default)}
path="/explorer/:node"
getComponent={() => import('./views/Explorer').then((module) => module.default)}
/>
<AsyncRoute
path="/explorer"
store={iris.session.getPubKey()}
getComponent={() => import('./views/Explorer').then(module => module.default)}
path="/explorer"
store={iris.session.getPubKey()}
getComponent={() => import('./views/Explorer').then((module) => module.default)}
/>
<Follows path="/follows/:id"/>
<Follows followers={true} path="/followers/:id"/>
<Contacts path="/contacts"/>
<Follows path="/follows/:id" />
<Follows followers={true} path="/followers/:id" />
<Contacts path="/contacts" />
</Router>
</div>
</section>
<MediaPlayer/>
<Footer/>
<VideoCall/>
<MediaPlayer />
<Footer />
<VideoCall />
</div>
);
}

View File

@ -1,8 +1,8 @@
let codeReader;
function startPrivKeyQRScanner() {
return new Promise(resolve => {
startQRScanner('privkey-qr-video', result => {
return new Promise((resolve) => {
startQRScanner('privkey-qr-video', (result) => {
let qr = JSON.parse(result.text);
if (qr.priv !== undefined) {
resolve(qr);
@ -17,28 +17,35 @@ function startChatLinkQRScanner(callback) {
}
async function startQRScanner(videoElementId, callback) {
const { BrowserQRCodeReader } = await import('@zxing/library');
codeReader = new BrowserQRCodeReader();
codeReader.decodeFromInputVideoDevice(undefined, videoElementId)
.then(result => {
if (callback(result)) {
cleanupScanner();
}
}).catch(err => {
if (err != undefined) {
console.error(err)
}
if (codeReader != undefined && codeReader != null) {
cleanupScanner()
}
});
const { BrowserQRCodeReader } = await import('@zxing/library');
codeReader = new BrowserQRCodeReader();
codeReader
.decodeFromInputVideoDevice(undefined, videoElementId)
.then((result) => {
if (callback(result)) {
cleanupScanner();
}
})
.catch((err) => {
if (err != undefined) {
console.error(err);
}
if (codeReader != undefined && codeReader != null) {
cleanupScanner();
}
});
}
function cleanupScanner() {
if (codeReader != undefined || codeReader != null) {
codeReader.reset();
codeReader = null;
}
if (codeReader != undefined || codeReader != null) {
codeReader.reset();
codeReader = null;
}
}
export default {cleanupScanner, startQRScanner, startChatLinkQRScanner, startPrivKeyQRScanner};
export default {
cleanupScanner,
startQRScanner,
startChatLinkQRScanner,
startPrivKeyQRScanner,
};

View File

@ -1 +1,2 @@
export const SMS_VERIFIER_PUB = 'ysavwX9TVnlDw93w9IxezCJqSDMyzIU-qpD8VTN5yko.3ll1dFdxLkgyVpejFkEMOFkQzp_tRrkT3fImZEx94Co';
export const SMS_VERIFIER_PUB =
'ysavwX9TVnlDw93w9IxezCJqSDMyzIU-qpD8VTN5yko.3ll1dFdxLkgyVpejFkEMOFkQzp_tRrkT3fImZEx94Co';

View File

@ -1,10 +1,12 @@
import { Component } from 'preact';
import Helpers from '../Helpers';
import {translate as t} from '../translations/Translation';
import $ from 'jquery';
import { OptionalGetter } from '../types';
import Button from './basic/Button';
import iris from 'iris-lib';
import $ from 'jquery';
import { Component } from 'preact';
import Helpers from '../Helpers';
import { translate as t } from '../translations/Translation';
import { OptionalGetter } from '../types';
import Button from './basic/Button';
type Props = {
copyStr: OptionalGetter<string>;
@ -22,7 +24,7 @@ class CopyButton extends Component<Props, State> {
timeout?: ReturnType<typeof setTimeout>;
copy(e: MouseEvent, copyStr: string) {
if (e.target === null){
if (e.target === null) {
return;
}
Helpers.copyToClipboard(copyStr);
@ -35,19 +37,20 @@ class CopyButton extends Component<Props, State> {
this.originalWidth = this.originalWidth || width + 1;
target.width(this.originalWidth);
this.setState({copied:true});
this.setState({ copied: true });
if (this.timeout !== undefined) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => this.setState({copied:false}), 2000);
this.timeout = setTimeout(() => this.setState({ copied: false }), 2000);
}
onClick(e: MouseEvent) {
e.preventDefault();
const copyStr = typeof this.props.copyStr === 'function' ? this.props.copyStr() : this.props.copyStr;
const copyStr =
typeof this.props.copyStr === 'function' ? this.props.copyStr() : this.props.copyStr;
if (iris.util.isMobile && !this.props.notShareable) {
navigator.share({url: copyStr, title: this.props.title}).catch(err => {
navigator.share({ url: copyStr, title: this.props.title }).catch((err) => {
console.error('share failed', err);
this.copy(e, copyStr);
});
@ -57,9 +60,9 @@ class CopyButton extends Component<Props, State> {
}
render() {
const text = this.state.copied ? t('copied') : (this.props.text || t('copy'));
const text = this.state.copied ? t('copied') : this.props.text || t('copy');
return (
<Button className="copy-button" onClick={e => this.onClick(e)}>
<Button className="copy-button" onClick={(e) => this.onClick(e)}>
{text}
</Button>
);

View File

@ -1,31 +1,50 @@
import BaseComponent from "../BaseComponent";
import iris from "iris-lib";
import {html} from "htm/preact";
import Name from "./Name";
import Text from "./Text";
import Button from "./basic/Button";
import { html } from 'htm/preact';
import iris from 'iris-lib';
import BaseComponent from '../BaseComponent';
import Button from './basic/Button';
import Name from './Name';
import Text from './Text';
const hashRegex = /^(?:[A-Za-z0-9+/]{4}){10}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)+$/;
const pubKeyRegex = /^[A-Za-z0-9\-\_]{40,50}\.[A-Za-z0-9\_\-]{40,50}$/;
const pubKeyRegex = /^[A-Za-z0-9\-_]{40,50}\.[A-Za-z0-9_-]{40,50}$/;
const SHOW_CHILDREN_COUNT = 50;
const chevronDown = html`
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-chevron-down" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
fill="currentColor"
class="bi bi-chevron-down"
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"
/>
</svg>
`;
const chevronRight = html`
<svg width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
<svg width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
<path
fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
/>
</svg>
`;
class ExplorerNode extends BaseComponent {
constructor() {
super();
this.children = [];
this.state = {groups:{}, children: {}, shownChildrenCount: SHOW_CHILDREN_COUNT};
this.state = {
groups: {},
children: {},
shownChildrenCount: SHOW_CHILDREN_COUNT,
};
}
getNode() {
@ -35,7 +54,12 @@ class ExplorerNode extends BaseComponent {
path = path.replace('/Users', '');
}
path = path.split('/');
return path.slice(1).reduce((sum, current) => (current && sum.get(decodeURIComponent(current))) || sum, this.props.gun);
return path
.slice(1)
.reduce(
(sum, current) => (current && sum.get(decodeURIComponent(current))) || sum,
this.props.gun,
);
}
return this.props.gun;
}
@ -44,8 +68,9 @@ class ExplorerNode extends BaseComponent {
return true;
}
componentDidMount() { // TODO: this is messy; create separate classes for Public / Group / Local
this.isMine = this.props.path.indexOf(`Public/Users/~${ iris.session.getPubKey()}`) === 0;
componentDidMount() {
// TODO: this is messy; create separate classes for Public / Group / Local
this.isMine = this.props.path.indexOf(`Public/Users/~${iris.session.getPubKey()}`) === 0;
this.isGroup = this.props.path.indexOf('Group/') === 0;
this.isPublicRoot = this.props.path === 'Public';
this.isUserList = this.props.path === 'Public/Users';
@ -53,66 +78,77 @@ class ExplorerNode extends BaseComponent {
this.isLocal = this.props.path.indexOf('Local') === 0;
this.children = {};
if (this.props.children && typeof this.props.children === "object") {
if (this.props.children && typeof this.props.children === 'object') {
this.children = Object.assign(this.children, this.props.children);
}
if (this.isPublicRoot) {
this.children = Object.assign(this.children, {
'#':{value:{_:1}, displayName: "ContentAddressed"},
Users:{value:{_:1}}
'#': { value: { _: 1 }, displayName: 'ContentAddressed' },
Users: { value: { _: 1 } },
});
}
if (this.isUserList) { // always add yourself to the user list
if (this.isUserList) {
// always add yourself to the user list
const obj = {};
obj[`~${iris.session.getPubKey()}`] = {value:{_:1}};
obj[`~${iris.session.getPubKey()}`] = { value: { _: 1 } };
this.children = Object.assign(this.children, obj);
}
if (this.isGroupRoot) {
const groups = {};
iris.local().get('groups').map(this.sub(
(v,k) => {
if (v) {
groups[k] = true;
} else {
delete groups[k];
}
this.setState({groups});
}
));
}
this.setState({children: this.children, shownChildrenCount: SHOW_CHILDREN_COUNT});
const cb = this.sub(
async (v, k, c, e, from) => {
if (k === '_') { return; }
if (this.isPublicRoot && k.indexOf('~') === 0) { return; }
if (this.isUserList && !k.substr(1).match(pubKeyRegex)) { return; }
let encryption;
if (typeof v === 'string' && v.indexOf('SEA{') === 0) {
try {
const myKey = iris.session.getKey();
let dec = await iris.SEA.decrypt(v, myKey);
if (dec === undefined) {
if (!this.mySecret) {
this.mySecret = await iris.SEA.secret(myKey.epub, myKey);
dec = await iris.SEA.decrypt(v, this.mySecret);
}
}
if (dec !== undefined) {
v = dec;
encryption = 'Decrypted';
iris
.local()
.get('groups')
.map(
this.sub((v, k) => {
if (v) {
groups[k] = true;
} else {
encryption = 'Encrypted';
delete groups[k];
}
} catch(e) {
null;
}
}
const prev = this.children[k] || {};
this.children[k] = Object.assign(prev, { value: v, encryption, from });
this.setState({children: this.children});
this.setState({ groups });
}),
);
}
this.setState({
children: this.children,
shownChildrenCount: SHOW_CHILDREN_COUNT,
});
const cb = this.sub(async (v, k, c, e, from) => {
if (k === '_') {
return;
}
);
if (this.isPublicRoot && k.indexOf('~') === 0) {
return;
}
if (this.isUserList && !k.substr(1).match(pubKeyRegex)) {
return;
}
let encryption;
if (typeof v === 'string' && v.indexOf('SEA{') === 0) {
try {
const myKey = iris.session.getKey();
let dec = await iris.SEA.decrypt(v, myKey);
if (dec === undefined) {
if (!this.mySecret) {
this.mySecret = await iris.SEA.secret(myKey.epub, myKey);
dec = await iris.SEA.decrypt(v, this.mySecret);
}
}
if (dec !== undefined) {
v = dec;
encryption = 'Decrypted';
} else {
encryption = 'Encrypted';
}
} catch (e) {
null;
}
}
const prev = this.children[k] || {};
this.children[k] = Object.assign(prev, { value: v, encryption, from });
this.setState({ children: this.children });
});
if (this.isGroupRoot) {
return;
@ -127,31 +163,41 @@ class ExplorerNode extends BaseComponent {
onChildObjectClick(e, k) {
e.preventDefault();
this.children[k].open = !this.children[k].open;
this.setState({children: this.children});
this.setState({ children: this.children });
}
onShowMoreClick(e, k) {
e.preventDefault();
this.children[k].showMore = !this.children[k].showMore;
this.setState({children: this.children});
this.setState({ children: this.children });
}
renderChildObject(k, displayName) {
const path = `${this.props.path }/${ encodeURIComponent(k)}`;
const path = `${this.props.path}/${encodeURIComponent(k)}`;
const substr = k.substr(1);
return html`
<div class="explorer-row" style="padding-left: ${this.props.indent}em">
<span onClick=${e => this.onChildObjectClick(e, k)}>${this.state.children[k].open ? chevronDown : chevronRight}</span>
<span onClick=${(e) => this.onChildObjectClick(e, k)}
>${this.state.children[k].open ? chevronDown : chevronRight}</span
>
<a href="/explorer/${encodeURIComponent(path)}">
<b>
${typeof k === 'string' && substr.match(pubKeyRegex) ?
html`<${Name} key=${k} pub=${substr} placeholder="user"/>` :
(displayName || k)}
</b>
<b>
${typeof k === 'string' && substr.match(pubKeyRegex)
? html`<${Name} key=${k} pub=${substr} placeholder="user" />`
: displayName || k}
</b>
</a>
${iris.session.getPubKey() === substr ? html`<small class="mar-left5">(you)</small>` : ''}
</div>
${this.state.children[k].open ? html`<${ExplorerNode} gun=${this.props.gun} indent=${this.props.indent + 1} key=${path} path=${path} isGroup=${this.props.isGroup}/>` : ''}
${this.state.children[k].open
? html`<${ExplorerNode}
gun=${this.props.gun}
indent=${this.props.indent + 1}
key=${path}
path=${path}
isGroup=${this.props.isGroup}
/>`
: ''}
`;
}
@ -160,10 +206,18 @@ class ExplorerNode extends BaseComponent {
const encryption = this.children[k].encryption;
const from = this.children[k].from;
const decrypted = encryption === 'Decrypted';
const lnk = (href, text, cls) => html`<a class=${cls === undefined ? "mar-left5" : cls} href=${href}>${text}</a>`;
const lnk = (href, text, cls) =>
html`<a class=${cls === undefined ? 'mar-left5' : cls} href=${href}>${text}</a>`;
const keyLinks = html`
${typeof k === 'string' && k.match(hashRegex) ? lnk(`/post/${encodeURIComponent(k)}`, '#') : ''}
${typeof k === 'string' && k.match(pubKeyRegex) ? lnk(`/explorer/Public%2F~${encodeURIComponent(encodeURIComponent(k))}`, html`<${Name} key=${k} pub=${k} placeholder="user"/>`) : ''}
${typeof k === 'string' && k.match(hashRegex)
? lnk(`/post/${encodeURIComponent(k)}`, '#')
: ''}
${typeof k === 'string' && k.match(pubKeyRegex)
? lnk(
`/explorer/Public%2F~${encodeURIComponent(encodeURIComponent(k))}`,
html`<${Name} key=${k} pub=${k} placeholder="user" />`,
)
: ''}
`;
if (encryption) {
if (!decrypted) {
@ -173,12 +227,14 @@ class ExplorerNode extends BaseComponent {
}
} else {
const pub = iris.session.getPubKey();
const path = (this.isMine || this.isLocal) && (`${this.props.path }/${ encodeURIComponent(k)}`)
.replace(`Public/Users/~${ pub }/`, '')
.replace(`Local/`, '');
const path =
(this.isMine || this.isLocal) &&
`${this.props.path}/${encodeURIComponent(k)}`
.replace(`Public/Users/~${pub}/`, '')
.replace(`Local/`, '');
if (typeof v === 'string' && v.indexOf('data:image') === 0) {
s = this.isMine ? html`<iris-img user=${pub} path=${path}/>` : html`<img src=${v}/>`;
s = this.isMine ? html`<iris-img user=${pub} path=${path} />` : html`<img src=${v} />`;
} else {
let stringified = JSON.stringify(v);
let showToggle;
@ -190,45 +246,79 @@ class ExplorerNode extends BaseComponent {
}
const valueLinks = html`
${typeof v === 'string' && v.match(hashRegex) ? lnk(`/post/${encodeURIComponent(v)}`, '#') : ''}
${k !== 'epub' && typeof v === 'string' && v.match(pubKeyRegex) ? lnk(`/explorer/Public%2F~${encodeURIComponent(encodeURIComponent(v))}`, html`<${Name} key=${v} pub=${v} placeholder="user"/>`) : ''}
${typeof from === 'string' ? html`<small> from ${lnk(`/explorer/Public%2F~${encodeURIComponent(encodeURIComponent(from))}`, html`<${Name} key=${from} pub=${from} placeholder="user"/>`, '')}</small>` : ''}
${typeof v === 'string' && v.match(hashRegex)
? lnk(`/post/${encodeURIComponent(v)}`, '#')
: ''}
${k !== 'epub' && typeof v === 'string' && v.match(pubKeyRegex)
? lnk(
`/explorer/Public%2F~${encodeURIComponent(encodeURIComponent(v))}`,
html`<${Name} key=${v} pub=${v} placeholder="user" />`,
)
: ''}
${typeof from === 'string'
? html`<small>
from
${lnk(
`/explorer/Public%2F~${encodeURIComponent(encodeURIComponent(from))}`,
html`<${Name} key=${from} pub=${from} placeholder="user" />`,
'',
)}</small
>`
: ''}
`;
// TODO: || isGroup where you're participating
s = this.isMine || this.isLocal ? html`
<${Text} gun=${this.props.gun} placeholder="empty" key=${path} user=${this.isLocal ? null : pub} path=${path} editable=${true} json=${true}/>
${valueLinks}
` :
html`
<span class=${typeof v === 'string' ? '' : 'iris-non-string'}>
${stringified}
${showToggle ? html`
<a onClick=${e => this.onShowMoreClick(e, k)} href="">${this.state.children[k].showMore ? 'less' : 'more'}</a>
` : ''}
${valueLinks}
</span>
`;
s =
this.isMine || this.isLocal
? html`
<${Text}
gun=${this.props.gun}
placeholder="empty"
key=${path}
user=${this.isLocal ? null : pub}
path=${path}
editable=${true}
json=${true}
/>
${valueLinks}
`
: html`
<span class=${typeof v === 'string' ? '' : 'iris-non-string'}>
${stringified}
${showToggle
? html`
<a onClick=${(e) => this.onShowMoreClick(e, k)} href=""
>${this.state.children[k].showMore ? 'less' : 'more'}</a
>
`
: ''}
${valueLinks}
</span>
`;
}
}
return html`
<div class="explorer-row" style="padding-left: ${this.props.indent}em">
<b class="val">${k} ${keyLinks}</b>:
${encryption ? html`
<span class="tooltip"><span class="tooltiptext">${encryption} value</span>
${decrypted ? '🔓' : ''}
</span>
` : ''} ${s}
${encryption
? html`
<span class="tooltip"
><span class="tooltiptext">${encryption} value</span>
${decrypted ? '🔓' : ''}
</span>
`
: ''}
${s}
</div>
`;
}
onExpandClicked() {
const expandAll = !this.state.expandAll;
Object.keys(this.children).forEach(k => {
Object.keys(this.children).forEach((k) => {
this.children[k].open = expandAll;
});
this.setState({expandAll, children: this.children});
this.setState({ expandAll, children: this.children });
}
onNewItemSubmit(e) {
@ -236,20 +326,22 @@ class ExplorerNode extends BaseComponent {
if (this.state.newItemName) {
let name = this.state.newItemName.trim();
if (this.state.newItemType === 'object') {
this.getNode().get(name).put({a:null});
this.getNode().get(name).put({ a: null });
} else {
this.getNode().get(name).put('');
}
this.setState({newItemType: false, newItemName: ''});
this.setState({ newItemType: false, newItemName: '' });
}
}
onNewItemNameInput(e) {
this.setState({newItemName: e.target.value.trimStart().replace(' ', ' ')});
this.setState({
newItemName: e.target.value.trimStart().replace(' ', ' '),
});
}
showNewItemClicked(type) {
this.setState({newItemType:type});
this.setState({ newItemType: type });
setTimeout(() => document.querySelector('#newItemNameInput').focus(), 0);
}
@ -261,54 +353,80 @@ class ExplorerNode extends BaseComponent {
childrenKeys.unshift(a[0]);
}
const renderChildren = children => {
return children.map(k => {
const renderChildren = (children) => {
return children.map((k) => {
const v = this.state.children[k].value;
const n = this.state.children[k].displayName;
if (typeof v === 'object' && v && v['_']) {
return this.renderChildObject(k, n);
}
return this.renderChildValue(k, v);
return this.renderChildValue(k, v);
});
}
};
const showMoreBtn = childrenKeys.length > this.state.shownChildrenCount;
return html`
${this.props.indent === 0 ? html`
<div class="explorer-row" style="padding-left: ${this.props.indent}em">
${this.props.showTools ? html`
<p class="explorer-tools">
<a onClick=${() => this.onExpandClicked()}>${this.state.expandAll ? 'Close all' : 'Expand all'}</a>
<a onClick=${() => this.showNewItemClicked('object')}>New object</a>
<a onClick=${() => this.showNewItemClicked('value')}>New value</a>
${childrenKeys.length} items
</p>
`: ''}
${this.state.newItemType ? html`
${this.props.indent === 0
? html`
<div class="explorer-row" style="padding-left: ${this.props.indent}em">
${this.props.showTools
? html`
<p class="explorer-tools">
<a onClick=${() => this.onExpandClicked()}
>${this.state.expandAll ? 'Close all' : 'Expand all'}</a
>
<a onClick=${() => this.showNewItemClicked('object')}>New object</a>
<a onClick=${() => this.showNewItemClicked('value')}>New value</a>
${childrenKeys.length} items
</p>
`
: ''}
${this.state.newItemType
? html`
<p>
<form onSubmit=${(e) => this.onNewItemSubmit(e)}>
<input id="newItemNameInput" type="text" onInput=${e => this.onNewItemNameInput(e)} value=${this.state.newItemName} placeholder="New ${this.state.newItemType} name"/>
<input id="newItemNameInput" type="text" onInput=${(e) =>
this.onNewItemNameInput(e)} value=${this.state.newItemName} placeholder="New ${
this.state.newItemType
} name"/>
<${Button} type="submit">Create<//>
<${Button} onClick=${() => this.setState({newItemType: false})}>Cancel<//>
<${Button} onClick=${() => this.setState({ newItemType: false })}>Cancel<//>
</form>
</p>
` : ''}
</div>
`: ''}
${this.isGroupRoot ? Object.keys(this.state.groups).map(group => html`
<div class="explorer-row" style="padding-left: 1em">
${chevronRight}
<a href="/explorer/Group%2F${encodeURIComponent(encodeURIComponent(group))}"><b>${group}</b></a>
</div>
`) : renderChildren(childrenKeys.slice(0, this.state.shownChildrenCount))}
${showMoreBtn ? html`
<a style="padding-left: ${this.props.indent + 1}em" href="" onClick=${e => {e.preventDefault();this.setState({shownChildrenCount: this.state.shownChildrenCount + SHOW_CHILDREN_COUNT})}}>More (${childrenKeys.length - this.state.shownChildrenCount})</a>
` : ''}
`
: ''}
</div>
`
: ''}
${this.isGroupRoot
? Object.keys(this.state.groups).map(
(group) => html`
<div class="explorer-row" style="padding-left: 1em">
${chevronRight}
<a href="/explorer/Group%2F${encodeURIComponent(encodeURIComponent(group))}"
><b>${group}</b></a
>
</div>
`,
)
: renderChildren(childrenKeys.slice(0, this.state.shownChildrenCount))}
${showMoreBtn
? html`
<a
style="padding-left: ${this.props.indent + 1}em"
href=""
onClick=${(e) => {
e.preventDefault();
this.setState({
shownChildrenCount: this.state.shownChildrenCount + SHOW_CHILDREN_COUNT,
});
}}
>More (${childrenKeys.length - this.state.shownChildrenCount})</a
>
`
: ''}
`;
}
}
export default ExplorerNode;
export default ExplorerNode;

View File

@ -1,25 +1,26 @@
import { createRef } from 'preact';
import Helpers from '../Helpers';
import { html } from 'htm/preact';
import { translate as t } from '../translations/Translation';
import iris from 'iris-lib';
import SafeImg from './SafeImg';
import Torrent from './Torrent';
import $ from 'jquery';
import EmojiButton from '../lib/emoji-button';
import SearchBox from './SearchBox';
import MessageForm from './MessageForm';
import { createRef } from 'preact';
const mentionRegex = /\B\@[\u00BF-\u1FFF\u2C00-\uD7FF\w]*$/;
import Helpers from '../Helpers';
import EmojiButton from '../lib/emoji-button';
import { translate as t } from '../translations/Translation';
import MessageForm from './MessageForm';
import SafeImg from './SafeImg';
import SearchBox from './SearchBox';
import Torrent from './Torrent';
const mentionRegex = /\B@[\u00BF-\u1FFF\u2C00-\uD7FF\w]*$/;
class FeedMessageForm extends MessageForm {
newMsgRef = createRef();
componentDidMount() {
const textEl = $(this.newMsgRef.current);
this.picker = new EmojiButton({position: 'top-start'});
this.picker.on('emoji', emoji => {
this.picker = new EmojiButton({ position: 'top-start' });
this.picker.on('emoji', (emoji) => {
textEl.val(textEl.val() + emoji);
textEl.focus();
});
@ -27,7 +28,12 @@ class FeedMessageForm extends MessageForm {
textEl.focus();
}
if (!this.props.replyingTo) {
iris.local().get('channels').get('public').get('msgDraft').once(t => !textEl.val() && textEl.val(t));
iris
.local()
.get('channels')
.get('public')
.get('msgDraft')
.once((t) => !textEl.val() && textEl.val(t));
}
}
@ -38,9 +44,13 @@ class FeedMessageForm extends MessageForm {
}
const textEl = $(this.newMsgRef.current);
const text = textEl.val();
if (!text.length && !this.state.attachments && !this.state.torrentId) { return; }
if (this.props.index === 'media' && !this.state.torrentId) { return; }
const msg = {text};
if (!text.length && !this.state.attachments && !this.state.torrentId) {
return;
}
if (this.props.index === 'media' && !this.state.torrentId) {
return;
}
const msg = { text };
if (this.props.replyingTo) {
msg.replyingTo = this.props.replyingTo;
}
@ -50,23 +60,32 @@ class FeedMessageForm extends MessageForm {
if (this.state.torrentId) {
msg.torrentId = this.state.torrentId;
}
this.sendPublic(msg).then(hash => {
this.sendPublic(msg).then((hash) => {
if (this.props.replyingToUser && this.props.replyingToUser !== iris.session.getPubKey()) {
const title = `${iris.session.getMyName() } replied to your message`;
const body = `'${text.length > 100 ? `${text.slice(0, 100) }...` : text}'`;
iris.notifications.sendIrisNotification(this.props.replyingToUser, {event:'reply', target: hash});
iris.notifications.sendWebPushNotification(this.props.replyingToUser, {title,body});
const title = `${iris.session.getMyName()} replied to your message`;
const body = `'${text.length > 100 ? `${text.slice(0, 100)}...` : text}'`;
iris.notifications.sendIrisNotification(this.props.replyingToUser, {
event: 'reply',
target: hash,
});
iris.notifications.sendWebPushNotification(this.props.replyingToUser, {
title,
body,
});
}
const mentions = text.match(Helpers.pubKeyRegex);
if (mentions) {
mentions.forEach(match => {
iris.notifications.sendIrisNotification(match.slice(1), {event:'mention', target: hash});
mentions.forEach((match) => {
iris.notifications.sendIrisNotification(match.slice(1), {
event: 'mention',
target: hash,
});
});
}
});
this.setState({attachments:null, torrentId:null});
this.setState({ attachments: null, torrentId: null });
textEl.val('');
textEl.height("");
textEl.height('');
this.props.onSubmit && this.props.onSubmit(msg);
}
@ -76,16 +95,19 @@ class FeedMessageForm extends MessageForm {
}
setTextareaHeight(textarea) {
textarea.style.height = "";
textarea.style.height = `${textarea.scrollHeight }px`;
textarea.style.height = '';
textarea.style.height = `${textarea.scrollHeight}px`;
}
onMsgTextPaste(event) {
const pasted = (event.clipboardData || window.clipboardData).getData('text');
const magnetRegex = /^magnet:\?xt=urn:btih:*/;
if (pasted !== this.state.torrentId && pasted.indexOf('.torrent') > -1 || pasted.match(magnetRegex)) {
if (
(pasted !== this.state.torrentId && pasted.indexOf('.torrent') > -1) ||
pasted.match(magnetRegex)
) {
event.preventDefault();
this.setState({torrentId: pasted});
this.setState({ torrentId: pasted });
}
}
@ -111,11 +133,11 @@ class FeedMessageForm extends MessageForm {
attachmentsChanged(event) {
let files = event.target.files;
if (files) {
for (let i = 0;i < files.length;i++) {
Helpers.getBase64(files[i]).then(base64 => {
for (let i = 0; i < files.length; i++) {
Helpers.getBase64(files[i]).then((base64) => {
const a = this.state.attachments || [];
a.push({type: 'image', data: base64});
this.setState({attachments: a});
a.push({ type: 'image', data: base64 });
this.setState({ attachments: a });
});
}
$(event.target).val(null);
@ -126,51 +148,149 @@ class FeedMessageForm extends MessageForm {
onSelectMention(item) {
const textarea = $(this.base).find('textarea').get(0);
const pos = textarea.selectionStart;
const join = [textarea.value.slice(0, pos).replace(mentionRegex, '@'), item.key, textarea.value.slice(pos)].join('')
const join = [
textarea.value.slice(0, pos).replace(mentionRegex, '@'),
item.key,
textarea.value.slice(pos),
].join('');
textarea.value = `${join} `;
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = pos + item.key.length;
}
render() {
const textareaPlaceholder = this.props.index === 'media' ? 'type_a_message_or_paste_a_magnet_link' : 'type_a_message';
return html`<form autocomplete="off" class="message-form ${this.props.class || ''} public" onSubmit=${e => this.onMsgFormSubmit(e)}>
<input name="attachment-input" type="file" class="hidden attachment-input" accept="image/*" multiple onChange=${e => this.attachmentsChanged(e)}/>
${this.props.index === 'media' ? html`
<p>
<small dangerouslySetInnerHTML=${{ __html: t('download_webtorrent', 'href="https://webtorrent.io/desktop/"')}}/>
</p>
`: ''}
<textarea onKeyUp=${e => this.onKeyUp(e)} onPaste=${e => this.onMsgTextPaste(e)} onInput=${e => this.onMsgTextInput(e)} ref=${this.newMsgRef} class="new-msg" type="text" placeholder="${t(textareaPlaceholder)}" autocomplete="off" autocorrect="off" autocapitalize="sentences" spellcheck="off"/>
${this.state.mentioning ? html`
<${SearchBox} resultsOnly=${true} query=${this.state.mentioning} onSelect=${item => this.onSelectMention(item)} />
` : ''}
const textareaPlaceholder =
this.props.index === 'media' ? 'type_a_message_or_paste_a_magnet_link' : 'type_a_message';
return html`<form
autocomplete="off"
class="message-form ${this.props.class || ''} public"
onSubmit=${(e) => this.onMsgFormSubmit(e)}
>
<input
name="attachment-input"
type="file"
class="hidden attachment-input"
accept="image/*"
multiple
onChange=${(e) => this.attachmentsChanged(e)}
/>
${this.props.index === 'media'
? html`
<p>
<small
dangerouslySetInnerHTML=${{
__html: t('download_webtorrent', 'href="https://webtorrent.io/desktop/"'),
}}
/>
</p>
`
: ''}
<textarea
onKeyUp=${(e) => this.onKeyUp(e)}
onPaste=${(e) => this.onMsgTextPaste(e)}
onInput=${(e) => this.onMsgTextInput(e)}
ref=${this.newMsgRef}
class="new-msg"
type="text"
placeholder="${t(textareaPlaceholder)}"
autocomplete="off"
autocorrect="off"
autocapitalize="sentences"
spellcheck="off"
/>
${this.state.mentioning
? html`
<${SearchBox}
resultsOnly=${true}
query=${this.state.mentioning}
onSelect=${(item) => this.onSelectMention(item)}
/>
`
: ''}
<div>
<button type="button" class="attach-file-btn" onClick=${e => this.attachFileClicked(e)}>
<svg width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M21.586 10.461l-10.05 10.075c-1.95 1.949-5.122 1.949-7.071 0s-1.95-5.122 0-7.072l10.628-10.585c1.17-1.17 3.073-1.17 4.243 0 1.169 1.17 1.17 3.072 0 4.242l-8.507 8.464c-.39.39-1.024.39-1.414 0s-.39-1.024 0-1.414l7.093-7.05-1.415-1.414-7.093 7.049c-1.172 1.172-1.171 3.073 0 4.244s3.071 1.171 4.242 0l8.507-8.464c.977-.977 1.464-2.256 1.464-3.536 0-2.769-2.246-4.999-5-4.999-1.28 0-2.559.488-3.536 1.465l-10.627 10.583c-1.366 1.368-2.05 3.159-2.05 4.951 0 3.863 3.13 7 7 7 1.792 0 3.583-.684 4.95-2.05l10.05-10.075-1.414-1.414z"/></svg>
<button type="button" class="attach-file-btn" onClick=${(e) => this.attachFileClicked(e)}>
<svg width="24" height="24" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M21.586 10.461l-10.05 10.075c-1.95 1.949-5.122 1.949-7.071 0s-1.95-5.122 0-7.072l10.628-10.585c1.17-1.17 3.073-1.17 4.243 0 1.169 1.17 1.17 3.072 0 4.242l-8.507 8.464c-.39.39-1.024.39-1.414 0s-.39-1.024 0-1.414l7.093-7.05-1.415-1.414-7.093 7.049c-1.172 1.172-1.171 3.073 0 4.244s3.071 1.171 4.242 0l8.507-8.464c.977-.977 1.464-2.256 1.464-3.536 0-2.769-2.246-4.999-5-4.999-1.28 0-2.559.488-3.536 1.465l-10.627 10.583c-1.366 1.368-2.05 3.159-2.05 4.951 0 3.863 3.13 7 7 7 1.792 0 3.583-.684 4.95-2.05l10.05-10.075-1.414-1.414z"
/>
</svg>
</button>
<button class="emoji-picker-btn hidden-xs" type="button" onClick=${e => this.onEmojiButtonClick(e)}>
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="smile" class="svg-inline--fa fa-smile fa-w-16" role="img" viewBox="0 0 496 512"><path fill="currentColor" d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm4 72.6c-20.8 25-51.5 39.4-84 39.4s-63.2-14.3-84-39.4c-8.5-10.2-23.7-11.5-33.8-3.1-10.2 8.5-11.5 23.6-3.1 33.8 30 36 74.1 56.6 120.9 56.6s90.9-20.6 120.9-56.6c8.5-10.2 7.1-25.3-3.1-33.8-10.1-8.4-25.3-7.1-33.8 3.1z"></path></svg>
<button
class="emoji-picker-btn hidden-xs"
type="button"
onClick=${(e) => this.onEmojiButtonClick(e)}
>
<svg
aria-hidden="true"
focusable="false"
data-prefix="far"
data-icon="smile"
class="svg-inline--fa fa-smile fa-w-16"
role="img"
viewBox="0 0 496 512"
>
<path
fill="currentColor"
d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm4 72.6c-20.8 25-51.5 39.4-84 39.4s-63.2-14.3-84-39.4c-8.5-10.2-23.7-11.5-33.8-3.1-10.2 8.5-11.5 23.6-3.1 33.8 30 36 74.1 56.6 120.9 56.6s90.9-20.6 120.9-56.6c8.5-10.2 7.1-25.3-3.1-33.8-10.1-8.4-25.3-7.1-33.8 3.1z"
></path>
</svg>
</button>
<button type="submit">
<svg class="svg-inline--fa fa-w-16" x="0px" y="0px" viewBox="0 0 486.736 486.736" style="enable-background:new 0 0 486.736 486.736;" width="100px" height="100px" fill="currentColor" stroke="#000000" stroke-width="0"><path fill="currentColor" d="M481.883,61.238l-474.3,171.4c-8.8,3.2-10.3,15-2.6,20.2l70.9,48.4l321.8-169.7l-272.4,203.4v82.4c0,5.6,6.3,9,11,5.9 l60-39.8l59.1,40.3c5.4,3.7,12.8,2.1,16.3-3.5l214.5-353.7C487.983,63.638,485.083,60.038,481.883,61.238z"></path></svg>
<svg
class="svg-inline--fa fa-w-16"
x="0px"
y="0px"
viewBox="0 0 486.736 486.736"
style="enable-background:new 0 0 486.736 486.736;"
width="100px"
height="100px"
fill="currentColor"
stroke="#000000"
stroke-width="0"
>
<path
fill="currentColor"
d="M481.883,61.238l-474.3,171.4c-8.8,3.2-10.3,15-2.6,20.2l70.9,48.4l321.8-169.7l-272.4,203.4v82.4c0,5.6,6.3,9,11,5.9 l60-39.8l59.1,40.3c5.4,3.7,12.8,2.1,16.3-3.5l214.5-353.7C487.983,63.638,485.083,60.038,481.883,61.238z"
></path>
</svg>
</button>
</div>
<div class="attachment-preview">
${this.state.torrentId ? html`
<p><a href="" onClick=${e => {e.preventDefault();this.setState({torrentId:null})}}>${t('remove_attachment')}</a></p>
<${Torrent} preview=${true} torrentId=${this.state.torrentId}/>
`:''}
${this.state.attachments && this.state.attachments.length ? html`
<p><a href="" onClick=${e => {e.preventDefault();this.setState({attachments:null})}}>${t('remove_attachment')}</a></p>
` : ''}
${this.state.attachments && this.state.attachments.map(a => html`
<${SafeImg} src=${a.data}/>
`)}
${this.state.torrentId
? html`
<p>
<a
href=""
onClick=${(e) => {
e.preventDefault();
this.setState({ torrentId: null });
}}
>${t('remove_attachment')}</a
>
</p>
<${Torrent} preview=${true} torrentId=${this.state.torrentId} />
`
: ''}
${this.state.attachments && this.state.attachments.length
? html`
<p>
<a
href=""
onClick=${(e) => {
e.preventDefault();
this.setState({ attachments: null });
}}
>${t('remove_attachment')}</a
>
</p>
`
: ''}
${this.state.attachments &&
this.state.attachments.map((a) => html` <${SafeImg} src=${a.data} /> `)}
</div>
</form>`;
}
}
export default FeedMessageForm;

View File

@ -1,20 +1,21 @@
import Component from '../BaseComponent';
import iris from 'iris-lib';
import {translate as t} from '../translations/Translation';
type Props = {};
import Component from '../BaseComponent';
import { translate as t } from '../translations/Translation';
type Props = Record<string, unknown>;
type State = {
group: string;
}
};
export default class Filters extends Component<Props, State> {
componentDidMount(): void {
iris.local().get('filters').get('group').on(this.inject());
iris.local().get('filters').get('group').on(this.inject());
}
toggleGroup(group: string): void {
iris.local().get('filters').get('group').put(group);
iris.local().get('filters').get('group').put(group);
}
render() {
@ -22,13 +23,13 @@ export default class Filters extends Component<Props, State> {
return (
<div className="msg filters">
<div className="msg-content">
<input checked={s.group === "follows"} type="radio"/>
<label onClick={() => this.toggleGroup("follows")} style="margin-right:15px">
<input checked={s.group === 'follows'} type="radio" />
<label onClick={() => this.toggleGroup('follows')} style="margin-right:15px">
{t('follows')}
</label>
<input checked={s.group === "everyone"} type="radio"/>
<label for="filterGroupChoice3" onClick={() => this.toggleGroup("everyone")}>
<input checked={s.group === 'everyone'} type="radio" />
<label for="filterGroupChoice3" onClick={() => this.toggleGroup('everyone')}>
{t('everyone')}
</label>
</div>

View File

@ -1,12 +1,13 @@
import Component from '../BaseComponent';
import {translate as t} from '../translations/Translation';
import iris from 'iris-lib';
import Component from '../BaseComponent';
import { translate as t } from '../translations/Translation';
import Button from './basic/Button';
type Props = {
id: string;
}
};
class FollowButton extends Component<Props> {
key: string;
@ -31,7 +32,9 @@ class FollowButton extends Component<Props> {
if (value && this.key === 'follow') {
iris.session.newChannel(this.props.id);
iris.public().get('block').get(this.props.id).put(false);
iris.notifications.sendIrisNotification(this.props.id, {event:'follow'});
iris.notifications.sendIrisNotification(this.props.id, {
event: 'follow',
});
}
if (value && this.key === 'block') {
iris.public().get('follow').get(this.props.id).put(false);
@ -41,18 +44,25 @@ class FollowButton extends Component<Props> {
}
componentDidMount() {
iris.public().get(this.key).get(this.props.id).on(this.sub(
value => {
const s = {};
s[this.key] = value;
this.setState(s);
}
));
iris
.public()
.get(this.key)
.get(this.props.id)
.on(
this.sub((value) => {
const s = {};
s[this.key] = value;
this.setState(s);
}),
);
}
render() {
return (
<Button className={`${this.cls || this.key} ${this.state[this.key] ? this.activeClass : ''}`} onClick={e => this.onClick(e)}>
<Button
className={`${this.cls || this.key} ${this.state[this.key] ? this.activeClass : ''}`}
onClick={(e) => this.onClick(e)}
>
<span className="nonhover">{t(this.state[this.key] ? this.actionDone : this.action)}</span>
<span className="hover">{t(this.hoverAction)}</span>
</Button>

View File

@ -1,39 +1,52 @@
import Component from '../BaseComponent';
import iris from 'iris-lib';
import Identicon from './Identicon';
import Component from '../BaseComponent';
import Icons from '../Icons';
const plusIcon = <svg width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm6 13h-5v5h-2v-5h-5v-2h5v-5h2v5h5v2z"/></svg>;
import Identicon from './Identicon';
type Props = {}
const plusIcon = (
<svg width="24" height="24" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm6 13h-5v5h-2v-5h-5v-2h5v-5h2v5h5v2z"
/>
</svg>
);
type Props = Record<string, unknown>;
type State = {
activeRoute: string;
unseenMsgsTotal: number;
chatId?: string;
}
activeRoute: string;
unseenMsgsTotal: number;
chatId?: string;
};
class Footer extends Component<Props, State> {
constructor() {
super();
this.state = {unseenMsgsTotal: 0, activeRoute: '/'};
this.state = { unseenMsgsTotal: 0, activeRoute: '/' };
}
componentDidMount() {
iris.local().get('unseenMsgsTotal').on(this.inject());
iris.local().get('activeRoute').on(this.sub(
activeRoute => {
const replaced = activeRoute.replace('/chat/new', '').replace('/chat/', '');
const chatId = replaced.length < activeRoute.length ? replaced : null;
this.setState({activeRoute, chatId});
}
));
iris
.local()
.get('activeRoute')
.on(
this.sub((activeRoute) => {
const replaced = activeRoute.replace('/chat/new', '').replace('/chat/', '');
const chatId = replaced.length < activeRoute.length ? replaced : null;
this.setState({ activeRoute, chatId });
}),
);
}
render() {
const key = iris.session.getPubKey();
if (!key) { return; }
if (!key) {
return;
}
const activeRoute = this.state.activeRoute;
if (this.state.chatId) {
@ -41,20 +54,33 @@ class Footer extends Component<Props, State> {
}
return (
<footer class="visible-xs-flex nav footer">
<div class="header-content" onClick={() => iris.local().get('scrollUp').put(true)}>
<a href="/" class={`btn ${activeRoute === '/' ? 'active' : ''}`}>{Icons.home}</a>
<a href="/chat" class={`btn ${activeRoute.indexOf('/chat') === 0 ? 'active' : ''}`}>
{this.state.unseenMsgsTotal ? <span class="unseen unseen-total">{this.state.unseenMsgsTotal}</span>: ''}
{Icons.chat}
</a>
<a href="/post/new" class={`btn ${activeRoute === '/post/new' ? 'active' : ''}`}>{plusIcon}</a>
<a href="/contacts" class={`btn ${activeRoute === '/contacts' ? 'active' : ''}`}>{Icons.user}</a>
<a href={`/profile/${key}`} class={`${activeRoute === `/profile/${ key}` ? 'active' : ''} my-profile`}>
<Identicon str={key} width={34} />
</a>
</div>
</footer>
<footer class="visible-xs-flex nav footer">
<div class="header-content" onClick={() => iris.local().get('scrollUp').put(true)}>
<a href="/" class={`btn ${activeRoute === '/' ? 'active' : ''}`}>
{Icons.home}
</a>
<a href="/chat" class={`btn ${activeRoute.indexOf('/chat') === 0 ? 'active' : ''}`}>
{this.state.unseenMsgsTotal ? (
<span class="unseen unseen-total">{this.state.unseenMsgsTotal}</span>
) : (
''
)}
{Icons.chat}
</a>
<a href="/post/new" class={`btn ${activeRoute === '/post/new' ? 'active' : ''}`}>
{plusIcon}
</a>
<a href="/contacts" class={`btn ${activeRoute === '/contacts' ? 'active' : ''}`}>
{Icons.user}
</a>
<a
href={`/profile/${key}`}
class={`${activeRoute === `/profile/${key}` ? 'active' : ''} my-profile`}
>
<Identicon str={key} width={34} />
</a>
</div>
</footer>
);
}
}

View File

@ -1,27 +1,29 @@
import Component from '../BaseComponent';
import {Fragment, createRef, RefObject, JSX} from 'preact';
import iris from 'iris-lib';
import {Link} from "preact-router/match";
import {route} from 'preact-router';
import {translate as t} from '../translations/Translation';
import { createRef, Fragment, JSX, RefObject } from 'preact';
import { route } from 'preact-router';
import { Link } from 'preact-router/match';
import Component from '../BaseComponent';
import { translate as t } from '../translations/Translation';
import Button from './basic/Button';
type Props = {};
type Props = Record<string, boolean>;
type State = {
showAddHashtagForm: boolean;
hashtags: Object;
hashtags: Record<string, boolean>;
popularHashtags: string[];
}
};
export default class HashtagList extends Component<Props, State> {
hashtagSubscribers: Object;
hashtagSubscribers: Set<string>;
addHashtagInputRef: RefObject<HTMLInputElement>;
constructor() {
super();
this.addHashtagInputRef = createRef();
this.hashtagSubscribers = {};
this.hashtagSubscribers = new Set();
this.state = {
hashtags: {},
showAddHashtagForm: undefined,
@ -31,50 +33,64 @@ export default class HashtagList extends Component<Props, State> {
componentDidMount() {
const hashtags = {};
iris.public().get('hashtagSubscriptions').map().on(this.sub(
(isSubscribed: boolean, hashtag: string) => {
if (hashtag.indexOf('~') === 0) { return; }
if (isSubscribed) {
hashtags[hashtag] = true;
} else {
delete hashtags[hashtag];
}
this.setState({hashtags});
}
));
iris.group().map('hashtagSubscriptions', this.sub((isSubscribed, hashtag, a, b, from) => {
if (hashtag.indexOf('~') === 0) { return; }
if (!this.hashtagSubscribers[hashtag]) {
this.hashtagSubscribers[hashtag] = new Set();
}
const subs = this.hashtagSubscribers[hashtag];
isSubscribed ? subs.add(from) : subs.delete(from);
const popularHashtags = Object.keys(this.hashtagSubscribers)
.filter(k => this.hashtagSubscribers[k].size > 0)
.filter(k => !hashtags[k])
.sort((tag1,tag2) => {
const set1 = this.hashtagSubscribers[tag1];
const set2 = this.hashtagSubscribers[tag2];
if (set1.size !== set2.size) {
return set1.size > set2.size ? -1 : 1;
iris
.public()
.get('hashtagSubscriptions')
.map()
.on(
this.sub((isSubscribed: boolean, hashtag: string) => {
if (hashtag.indexOf('~') === 0) {
return;
}
return tag1 > tag2 ? 1 : -1;
}).slice(0,8);
this.setState({popularHashtags});
}));
if (isSubscribed) {
hashtags[hashtag] = true;
} else {
delete hashtags[hashtag];
}
this.setState({ hashtags });
}),
);
iris.group().map(
'hashtagSubscriptions',
this.sub((isSubscribed, hashtag, a, b, from) => {
if (hashtag.indexOf('~') === 0) {
return;
}
if (!this.hashtagSubscribers[hashtag]) {
this.hashtagSubscribers[hashtag] = new Set();
}
const subs = this.hashtagSubscribers[hashtag];
isSubscribed ? subs.add(from) : subs.delete(from);
const popularHashtags = Object.keys(this.hashtagSubscribers)
.filter((k) => this.hashtagSubscribers[k].size > 0)
.filter((k) => !hashtags[k])
.sort((tag1, tag2) => {
const set1 = this.hashtagSubscribers[tag1];
const set2 = this.hashtagSubscribers[tag2];
if (set1.size !== set2.size) {
return set1.size > set2.size ? -1 : 1;
}
return tag1 > tag2 ? 1 : -1;
})
.slice(0, 8);
this.setState({ popularHashtags });
}),
);
}
addHashtagClicked(e: JSX.TargetedMouseEvent<HTMLAnchorElement>) {
e.preventDefault();
this.setState({showAddHashtagForm: !this.state.showAddHashtagForm});
this.setState({ showAddHashtagForm: !this.state.showAddHashtagForm });
}
onAddHashtag(e: JSX.TargetedEvent<HTMLFormElement>) {
e.preventDefault();
const hashtag = ((e.target as HTMLFormElement).firstChild as HTMLInputElement).value.replace('#', '').trim();
const hashtag = ((e.target as HTMLFormElement).firstChild as HTMLInputElement).value
.replace('#', '')
.trim();
if (hashtag) {
iris.public().get('hashtagSubscriptions').get(hashtag).put(true);
this.setState({showAddHashtagForm: false});
this.setState({ showAddHashtagForm: false });
route(`/hashtag/${hashtag}`);
}
}
@ -96,35 +112,61 @@ export default class HashtagList extends Component<Props, State> {
<div className="msg-content">
{this.state.showAddHashtagForm ? (
<Fragment>
<form onSubmit={e => this.onAddHashtag(e)}>
<input placeholder="#hashtag" ref={this.addHashtagInputRef} style="margin-bottom: 7px" />
<Button type="submit">{t('add')}</Button>
<Button onClick={() => this.setState({showAddHashtagForm:false})}>{t('cancel')}</Button>
<form onSubmit={(e) => this.onAddHashtag(e)}>
<input
placeholder="#hashtag"
ref={this.addHashtagInputRef}
style="margin-bottom: 7px"
/>
<Button type="submit">{t('add')}</Button>
<Button onClick={() => this.setState({ showAddHashtagForm: false })}>
{t('cancel')}
</Button>
</form>
<br/>
<br />
</Fragment>
) : (
<Fragment>
<a href="" onClick={e => this.addHashtagClicked(e)}>{t('add_hashtag')}</a>
<br/>
<a href="" onClick={(e) => this.addHashtagClicked(e)}>
{t('add_hashtag')}
</a>
<br />
</Fragment>
)}
<Link activeClassName="active" href="/">{t('all')}</Link>
{Object.keys(this.state.hashtags).sort().map(hashtag =>
<Link activeClassName="active" className="channel-listing" href={`/hashtag/${hashtag}`}>#{hashtag}</Link>
)}
<Link activeClassName="active" href="/">
{t('all')}
</Link>
{Object.keys(this.state.hashtags)
.sort()
.map((hashtag) => (
<Link
activeClassName="active"
className="channel-listing"
href={`/hashtag/${hashtag}`}
>
#{hashtag}
</Link>
))}
</div>
</div>
{this.state.popularHashtags && this.state.popularHashtags.length ? (
<div className="msg hashtag-list">
<div className="msg-content">
{t('popular_hashtags')}<br/><br/>
{this.state.popularHashtags.map(hashtag =>
<Link activeClassName="active" className="channel-listing" href={`/hashtag/${hashtag}`}>#{hashtag}</Link>
)}
{t('popular_hashtags')}
<br />
<br />
{this.state.popularHashtags.map((hashtag) => (
<Link
activeClassName="active"
className="channel-listing"
href={`/hashtag/${hashtag}`}
>
#{hashtag}
</Link>
))}
</div>
</div>
):null}
) : null}
</Fragment>
);
}

View File

@ -1,8 +1,10 @@
import Component from '../BaseComponent';
import { html } from 'htm/preact';
import iris from 'iris-lib';
import Name from './Name';
import Component from '../BaseComponent';
import Identicon from './Identicon';
import Name from './Name';
export default class HashtagSubscriberList extends Component {
constructor() {
@ -11,12 +13,13 @@ export default class HashtagSubscriberList extends Component {
}
componentDidMount() {
iris.group().on(`hashtagSubscriptions/${this.props.hashtag}`, this.sub(
(isSubscribed, hashtag, a, b, from) => {
iris.group().on(
`hashtagSubscriptions/${this.props.hashtag}`,
this.sub((isSubscribed, hashtag, a, b, from) => {
isSubscribed ? this.subs.add(from) : this.subs.delete(from);
this.setState({});
}
));
}),
);
}
shouldComponentUpdate() {
@ -26,23 +29,27 @@ export default class HashtagSubscriberList extends Component {
render() {
const subs = Array.from(this.subs);
return html`
${subs.length ? html`
<div class="msg hashtag-list">
<div class="msg-content">
#${this.props.hashtag} subscribers (${subs.length})<br/><br/>
${subs.map(k =>
html`
<a href="/profile/${k}">
<span class="text">
<${Identicon} key="i${k}" str=${k} width=30 activity=${true}/> <${Name} pub=${k} key="t${k}" />
</span>
</a>
`
)}
</div>
</div>
`:''}
${subs.length
? html`
<div class="msg hashtag-list">
<div class="msg-content">
#${this.props.hashtag} subscribers (${subs.length})<br /><br />
${subs.map(
(k) =>
html`
<a href="/profile/${k}">
<span class="text">
<${Identicon} key="i${k}" str=${k} width="30" activity=${true} />
<${Name} pub=${k} key="t${k}" />
</span>
</a>
`,
)}
</div>
</div>
`
: ''}
`;
}
}

View File

@ -1,29 +1,29 @@
import { html } from 'htm/preact';
import iris from 'iris-lib';
import $ from 'jquery';
import filter from 'lodash/filter';
import { route } from 'preact-router';
import { Link } from 'preact-router/match';
import Component from '../BaseComponent';
import Helpers from '../Helpers';
import { html } from 'htm/preact';
import Icons from '../Icons';
import { translate as t } from '../translations/Translation';
import iris from 'iris-lib';
import { route } from 'preact-router';
import Identicon from './Identicon';
import SearchBox from './SearchBox';
import Icons from '../Icons';
import {Link} from "preact-router/match";
import $ from 'jquery';
import _ from "lodash";
class Header extends Component {
constructor() {
super();
this.state = {latest: {}, topicPeerCount: 0};
this.state = { latest: {}, topicPeerCount: 0 };
this.chatId = null;
this.escFunction = this.escFunction.bind(this);
}
escFunction(event){
if(event.keyCode === 27) {
this.state.showMobileSearch && this.setState({showMobileSearch: false});
escFunction(event) {
if (event.keyCode === 27) {
this.state.showMobileSearch && this.setState({ showMobileSearch: false });
}
}
@ -32,16 +32,18 @@ class Header extends Component {
const activity = channel.activity;
if (activity) {
if (activity.isActive) {
return(t('online'));
return t('online');
} else if (activity.lastActive) {
const d = new Date(activity.lastActive);
let lastSeenText = t(iris.util.getDaySeparatorText(d, d.toLocaleDateString({dateStyle:'short'})));
let lastSeenText = t(
iris.util.getDaySeparatorText(d, d.toLocaleDateString({ dateStyle: 'short' })),
);
if (lastSeenText === t('today')) {
lastSeenText = iris.util.formatTime(d);
} else {
lastSeenText = iris.util.formatDate(d);
}
return (`${t('last_active') } ${ lastSeenText}`);
return `${t('last_active')} ${lastSeenText}`;
}
}
}
@ -53,37 +55,57 @@ class Header extends Component {
componentWillUnmount() {
super.componentWillUnmount();
clearInterval(this.iv);
document.removeEventListener("keydown", this.escFunction, false);
document.removeEventListener('keydown', this.escFunction, false);
}
componentDidMount() {
document.addEventListener("keydown", this.escFunction, false);
document.addEventListener('keydown', this.escFunction, false);
iris.local().get('showParticipants').on(this.inject());
iris.local().get('unseenMsgsTotal').on(this.inject());
iris.local().get('unseenNotificationCount').on(this.inject());
iris.local().get('activeRoute').on(this.sub(
activeRoute => {
this.setState({about:null, title: '', activeRoute, showMobileSearch: false});
const replaced = activeRoute.replace('/chat/new', '').replace('/chat/', '');
this.chatId = replaced.length < activeRoute.length ? replaced : null;
if (this.chatId) {
iris.local().get('channels').get(this.chatId).get('isTyping').on(this.inject());
iris.local().get('channels').get(this.chatId).get('theirLastActiveTime').on(this.inject());
}
if (activeRoute.indexOf('/chat/') === 0 && activeRoute.indexOf('/chat/new') !== 0) {
if (activeRoute.indexOf('/chat/') === 0 && iris.session.getKey() && this.chatId === iris.session.getKey().pub) {
const title = html`<b style="margin-right:5px">📝</b> <b>${t('note_to_self')}</b>`;
this.setState({title});
} else if (activeRoute.indexOf('/chat/hashtag/') === 0) {
this.setState({title: `#${activeRoute.replace('/chat/hashtag/','')}`, about: 'Public'})
} else {
iris.local().get('channels').get(this.chatId).get('name').on(this.inject('title'));
iris.local().get('channels').get(this.chatId).get('about').on(this.inject());
iris
.local()
.get('activeRoute')
.on(
this.sub((activeRoute) => {
this.setState({
about: null,
title: '',
activeRoute,
showMobileSearch: false,
});
const replaced = activeRoute.replace('/chat/new', '').replace('/chat/', '');
this.chatId = replaced.length < activeRoute.length ? replaced : null;
if (this.chatId) {
iris.local().get('channels').get(this.chatId).get('isTyping').on(this.inject());
iris
.local()
.get('channels')
.get(this.chatId)
.get('theirLastActiveTime')
.on(this.inject());
}
}
}
));
if (activeRoute.indexOf('/chat/') === 0 && activeRoute.indexOf('/chat/new') !== 0) {
if (
activeRoute.indexOf('/chat/') === 0 &&
iris.session.getKey() &&
this.chatId === iris.session.getKey().pub
) {
const title = html`<b style="margin-right:5px">📝</b> <b>${t('note_to_self')}</b>`;
this.setState({ title });
} else if (activeRoute.indexOf('/chat/hashtag/') === 0) {
this.setState({
title: `#${activeRoute.replace('/chat/hashtag/', '')}`,
about: 'Public',
});
} else {
iris.local().get('channels').get(this.chatId).get('name').on(this.inject('title'));
iris.local().get('channels').get(this.chatId).get('about').on(this.inject());
}
}
}),
);
this.updatePeersFromGun();
this.iv = setInterval(() => this.updatePeersFromGun(), 1000);
}
@ -99,116 +121,190 @@ class Header extends Component {
e.preventDefault();
e.stopPropagation();
$('a.logo').blur();
($(window).width() > 625) && route('/');
$(window).width() > 625 && route('/');
iris.local().get('toggleMenu').put(true);
}
updatePeersFromGun() {
const peersFromGun = iris.global().back('opt.peers') || {};
const connectedPeers = _.filter(Object.values(peersFromGun), (peer) => {
const connectedPeers = filter(Object.values(peersFromGun), (peer) => {
if (peer && peer.wire && peer.wire.constructor.name !== 'WebSocket') {
console.log('WebRTC peer', peer);
}
return peer && peer.wire && peer.wire.readyState === 1 && peer.wire.bufferedAmount === 0 && peer.wire.constructor.name === 'WebSocket';
return (
peer &&
peer.wire &&
peer.wire.readyState === 1 &&
peer.wire.bufferedAmount === 0 &&
peer.wire.constructor.name === 'WebSocket'
);
});
this.setState({connectedPeers});
this.setState({ connectedPeers });
}
render() {
const key = iris.session.getPubKey();
if (!key) { return; }
if (!key) {
return;
}
const activeRoute = this.state.activeRoute;
const chat = activeRoute && activeRoute.indexOf('/chat') === 0 && this.chatId && iris.private(this.chatId);
const chat =
activeRoute && activeRoute.indexOf('/chat') === 0 && this.chatId && iris.private(this.chatId);
const isTyping = chat && chat.isTyping;
const onlineStatus = chat && chat.uuid && activeRoute && activeRoute.length > 20 && !isTyping && this.getOnlineStatusText();
const searchBox = this.chatId ? '' : html`
<${SearchBox} onSelect=${item => route(item.uuid ? `/chat/${item.uuid}` : `/profile/${item.key}`)} />
`;
const chatting = (activeRoute && activeRoute.indexOf('/chat/') === 0);
const onlineStatus =
chat &&
chat.uuid &&
activeRoute &&
activeRoute.length > 20 &&
!isTyping &&
this.getOnlineStatusText();
const searchBox = this.chatId
? ''
: html`
<${SearchBox}
onSelect=${(item) => route(item.uuid ? `/chat/${item.uuid}` : `/profile/${item.key}`)}
/>
`;
const chatting = activeRoute && activeRoute.indexOf('/chat/') === 0;
const peerCount = (this.state.connectedPeers ? this.state.connectedPeers.length : 0) + this.state.topicPeerCount;
const peerCount =
(this.state.connectedPeers ? this.state.connectedPeers.length : 0) +
this.state.topicPeerCount;
return html`
<header class="nav header">
${activeRoute && activeRoute.indexOf('/chat/') === 0 ? html`
<div id="back-button" class="visible-xs-inline-block" onClick=${() => this.backButtonClicked()}>
</div>
` : ''}
return html` <header class="nav header">
${activeRoute && activeRoute.indexOf('/chat/') === 0
? html`
<div
id="back-button"
class="visible-xs-inline-block"
onClick=${() => this.backButtonClicked()}
>
</div>
`
: ''}
<div class="header-content">
<div class="mobile-search-hidden ${this.state.showMobileSearch ? 'hidden-xs':''}">
${Helpers.isElectron || chatting ? '' : html`
<a href="/" onClick=${e => this.onLogoClick(e)} class="visible-xs-flex logo">
<div class="mobile-menu-icon">${Icons.menu}</div>
</a>
`}
<div class="mobile-search-hidden ${this.state.showMobileSearch ? 'hidden-xs' : ''}">
${Helpers.isElectron || chatting
? ''
: html`
<a href="/" onClick=${(e) => this.onLogoClick(e)} class="visible-xs-flex logo">
<div class="mobile-menu-icon">${Icons.menu}</div>
</a>
`}
</div>
${chatting ? '' : html`
<a class="mobile-search-visible ${this.state.showMobileSearch ? '' : 'hidden-xs'}" href="" onClick=${e => {
e.preventDefault();
this.setState({showMobileSearch: false})
}}>
<span class="visible-xs-inline-block">${Icons.backArrow}</span>
</a>
`}
<a href="/settings/peer" class="connected-peers tooltip mobile-search-hidden ${this.state.showMobileSearch ? 'hidden-xs' : ''} ${peerCount ? 'connected' : ''}">
${chatting
? ''
: html`
<a
class="mobile-search-visible ${this.state.showMobileSearch ? '' : 'hidden-xs'}"
href=""
onClick=${(e) => {
e.preventDefault();
this.setState({ showMobileSearch: false });
}}
>
<span class="visible-xs-inline-block">${Icons.backArrow}</span>
</a>
`}
<a
href="/settings/peer"
class="connected-peers tooltip mobile-search-hidden ${this.state.showMobileSearch
? 'hidden-xs'
: ''} ${peerCount ? 'connected' : ''}"
>
<span class="tooltiptext">${t('connected_peers')}</span>
<small>
<span class="icon">${Icons.network}</span>
<span>${peerCount}</span>
</small>
</a>
<div class="text" style=${this.chatId ? 'cursor:pointer;text-align:center' : ''} onClick=${() => this.onTitleClicked()}>
${this.state.title && chatting ? html`
<div class="name">
${this.state.title}
</div>
`: ''}
<div
class="text"
style=${this.chatId ? 'cursor:pointer;text-align:center' : ''}
onClick=${() => this.onTitleClicked()}
>
${this.state.title && chatting ? html` <div class="name">${this.state.title}</div> ` : ''}
${isTyping ? html`<small class="typing-indicator">${t('typing')}</small>` : ''}
${this.state.about ? html`<small class="participants">${this.state.about}</small>` : ''}
${this.chatId ? html`<small class="last-seen">${onlineStatus || ''}</small>` : ''}
${chatting ? '':html`
<div id="mobile-search" class="mobile-search-visible ${this.state.showMobileSearch ? '' : 'hidden-xs'}">
${searchBox}
</div>
<div id="mobile-search-btn" class="mobile-search-hidden ${this.state.showMobileSearch ? 'hidden' : 'visible-xs-inline-block'}" onClick=${() => {
// also synchronously make element visible so it can be focused
$('.mobile-search-visible').removeClass('hidden-xs hidden');
$('.mobile-search-hidden').removeClass('visible-xs-inline-block').addClass('hidden');
const input = document.querySelector('.search-box input');
input && input.focus();
this.setState({showMobileSearch: true});
}}>
${Icons.search}
</div>
`}
${chatting
? ''
: html`
<div
id="mobile-search"
class="mobile-search-visible ${this.state.showMobileSearch ? '' : 'hidden-xs'}"
>
${searchBox}
</div>
<div
id="mobile-search-btn"
class="mobile-search-hidden ${this.state.showMobileSearch
? 'hidden'
: 'visible-xs-inline-block'}"
onClick=${() => {
// also synchronously make element visible so it can be focused
$('.mobile-search-visible').removeClass('hidden-xs hidden');
$('.mobile-search-hidden')
.removeClass('visible-xs-inline-block')
.addClass('hidden');
const input = document.querySelector('.search-box input');
input && input.focus();
this.setState({ showMobileSearch: true });
}}
>
${Icons.search}
</div>
`}
</div>
${chat && this.chatId !== key && !chat.uuid ? html`
<a class="tooltip" style="width:24px; height:24px; color: var(--msg-form-button-color)" id="<start-video-call" onClick=${() => iris.local().get('outgoingCall').put(this.chatId)}>
<span class="tooltiptext">${t('video_call')}</span>
${Icons.videoCall}
</a>
<!-- <a id="start-voice-call" style="width:20px; height:20px; margin-right: 20px">
${chat && this.chatId !== key && !chat.uuid
? html`
<a
class="tooltip"
style="width:24px; height:24px; color: var(--msg-form-button-color)"
id="<start-video-call"
onClick=${() => iris.local().get('outgoingCall').put(this.chatId)}
>
<span class="tooltiptext">${t('video_call')}</span>
${Icons.videoCall}
</a>
<!-- <a id="start-voice-call" style="width:20px; height:20px; margin-right: 20px">
Icons.voiceCall
</a> -->
`: ''}
${this.chatId && this.chatId.length > 10 && this.chatId.length < 40 ? html`
<a class="tooltip hidden-xs" onClick=${() => iris.local().get('showParticipants').put(!this.state.showParticipants)}>
<span class="tooltiptext">${t('participant_list')}</span>
${Icons.group}
</a>
` : ''}
<${Link} activeClassName="active"
href="/notifications"
class="notifications-button mobile-search-hidden ${this.state.showMobileSearch ? 'hidden' : ''}">
`
: ''}
${this.chatId && this.chatId.length > 10 && this.chatId.length < 40
? html`
<a
class="tooltip hidden-xs"
onClick=${() =>
iris.local().get('showParticipants').put(!this.state.showParticipants)}
>
<span class="tooltiptext">${t('participant_list')}</span>
${Icons.group}
</a>
`
: ''}
<${Link}
activeClassName="active"
href="/notifications"
class="notifications-button mobile-search-hidden ${this.state.showMobileSearch
? 'hidden'
: ''}"
>
${Icons.heartEmpty}
${this.state.unseenNotificationCount ? html`
<span class="unseen">${this.state.unseenNotificationCount}</span>
` : ''}
${this.state.unseenNotificationCount
? html` <span class="unseen">${this.state.unseenNotificationCount}</span> `
: ''}
<//>
<${Link} activeClassName="active" href="/profile/${key}" onClick=${() => iris.local().get('scrollUp').put(true)} class="hidden-xs my-profile">
<${Identicon} str=${key} width=34 />
<${Link}
activeClassName="active"
href="/profile/${key}"
onClick=${() => iris.local().get('scrollUp').put(true)}
class="hidden-xs my-profile"
>
<${Identicon} str=${key} width="34" />
<//>
</div>
</header>`;

View File

@ -1,13 +1,15 @@
import Component from '../BaseComponent';
import iris from 'iris-lib';
import Identicon from 'identicon.js';
import SafeImg from './SafeImg';
import iris from 'iris-lib';
import styled from 'styled-components';
import Component from '../BaseComponent';
import SafeImg from './SafeImg';
type Activity = {
time: string;
status: string;
}
};
type Props = {
str: unknown;
@ -27,30 +29,36 @@ type State = {
};
const IdenticonContainer = styled.div`
max-width: ${(props: Props) => props.width}px;
max-height: ${(props: Props) => props.width}px;
display: inline-block;
align-items: center;
justify-content: center;
flex-direction: column;
position: relative;
user-select: none;
max-width: ${(props: Props) => props.width}px;
max-height: ${(props: Props) => props.width}px;
display: inline-block;
align-items: center;
justify-content: center;
flex-direction: column;
position: relative;
user-select: none;
`;
class MyIdenticon extends Component<Props, State> {
activityTimeout?: ReturnType<typeof setTimeout>;
updateIdenticon() {
iris.util.getHash(this.props.str as string, `hex`)
.then(hash => {
const identicon = new Identicon(hash, {width: this.props.width, format: `svg`});
this.setState({identicon: `data:image/svg+xml;base64,${identicon.toString()}`});
iris.util.getHash(this.props.str as string, `hex`).then((hash) => {
const identicon = new Identicon(hash, {
width: this.props.width,
format: `svg`,
});
this.setState({
identicon: `data:image/svg+xml;base64,${identicon.toString()}`,
});
});
}
componentDidMount() {
const pub = this.props.str as string;
if (!pub) { return; }
if (!pub) {
return;
}
this.updateIdenticon();
@ -59,26 +67,32 @@ class MyIdenticon extends Component<Props, State> {
iris.public(pub).get('profile').get('nftPfp').on(this.inject());
}
this.setState({activity: null});
this.setState({ activity: null });
if (this.props.showTooltip) {
iris.public(pub).get('profile').get('name').on(this.inject());
}
if (this.props.activity) {
iris.public(pub).get('activity').on(this.sub(
(activity?: Activity) => {
if (activity) {
if (activity.time && ((new Date()).getTime() - (new Date(activity.time)).getTime() < 30000)) {
if (this.activityTimeout !== undefined) {
clearTimeout(this.activityTimeout);
iris
.public(pub)
.get('activity')
.on(
this.sub((activity?: Activity) => {
if (activity) {
if (
activity.time &&
new Date().getTime() - new Date(activity.time).getTime() < 30000
) {
if (this.activityTimeout !== undefined) {
clearTimeout(this.activityTimeout);
}
this.activityTimeout = setTimeout(() => this.setState({ activity: null }), 30000);
this.setState({ activity: activity.status });
}
this.activityTimeout = setTimeout(() => this.setState({activity:null}), 30000);
this.setState({activity: activity.status});
} else {
this.setState({ activity: null });
}
} else {
this.setState({activity: null});
}
}
));
}),
);
}
}
@ -91,14 +105,21 @@ class MyIdenticon extends Component<Props, State> {
render() {
const width = this.props.width;
const activity = ['online', 'active'].indexOf(this.state.activity ?? '') > -1 ? this.state.activity : '';
const hasPhoto = this.state.photo && !this.props.hidePhoto && this.state.photo.indexOf('data:image') === 0;
const activity =
['online', 'active'].indexOf(this.state.activity ?? '') > -1 ? this.state.activity : '';
const hasPhoto =
this.state.photo && !this.props.hidePhoto && this.state.photo.indexOf('data:image') === 0;
const hasPhotoStyle = hasPhoto ? 'has-photo' : '';
const showTooltip = this.props.showTooltip ? 'tooltip' : '';
const imgSrc = this.state.photo || this.state.identicon;
const photoElement = this.state.nftPfp ? (
<svg style={`max-width:${width}px;max-height:${width}px`} width="327.846" height="318.144" viewBox="0 0 327.846 318.144">
<svg
style={`max-width:${width}px;max-height:${width}px`}
width="327.846"
height="318.144"
viewBox="0 0 327.846 318.144"
>
<defs>
<style>
{`.a {
@ -108,23 +129,42 @@ class MyIdenticon extends Component<Props, State> {
stroke-linejoin:round;
}`}
</style>
<mask id="msk">
<path class="a" transform="translate(111.598) rotate(30)" d="M172.871,0a28.906,28.906,0,0,1,25.009,14.412L245.805,97.1a28.906,28.906,0,0,1,0,28.989L197.88,208.784A28.906,28.906,0,0,1,172.871,223.2H76.831a28.906,28.906,0,0,1-25.009-14.412L3.9,126.092A28.906,28.906,0,0,1,3.9,97.1L51.821,14.412A28.906,28.906,0,0,1,76.831,0Z"/>
<mask id="msk">
<path
class="a"
transform="translate(111.598) rotate(30)"
d="M172.871,0a28.906,28.906,0,0,1,25.009,14.412L245.805,97.1a28.906,28.906,0,0,1,0,28.989L197.88,208.784A28.906,28.906,0,0,1,172.871,223.2H76.831a28.906,28.906,0,0,1-25.009-14.412L3.9,126.092A28.906,28.906,0,0,1,3.9,97.1L51.821,14.412A28.906,28.906,0,0,1,76.831,0Z"
/>
</mask>
</defs>
<image mask="url(#msk)" height="100%" width="100%" href={imgSrc} preserveAspectRatio="xMidYMin slice"></image>
<image
mask="url(#msk)"
height="100%"
width="100%"
href={imgSrc}
preserveAspectRatio="xMidYMin slice"
></image>
</svg>
) : (<SafeImg src={this.state.photo} width={width} />);
) : (
<SafeImg src={this.state.photo} width={width} />
);
return (
<IdenticonContainer width={width} onClick={this.props.onClick} style={{cursor: this.props.onClick ? 'pointer' : undefined}} className={`identicon-container ${hasPhotoStyle} ${showTooltip} ${activity}`}>
<div style={{width: width, height: width}} class="identicon">
<IdenticonContainer
width={width}
onClick={this.props.onClick}
style={{ cursor: this.props.onClick ? 'pointer' : undefined }}
className={`identicon-container ${hasPhotoStyle} ${showTooltip} ${activity}`}
>
<div style={{ width: width, height: width }} class="identicon">
{hasPhoto ? photoElement : <img width={width} src={this.state.identicon} />}
</div>
{this.props.showTooltip && this.state.name ? (<span class="tooltiptext">{this.state.name}</span>) : ''}
{this.props.activity ? <div class="online-indicator"/> : null}
{this.props.showTooltip && this.state.name ? (
<span class="tooltiptext">{this.state.name}</span>
) : (
''
)}
{this.props.activity ? <div class="online-indicator" /> : null}
</IdenticonContainer>
);
}

View File

@ -1,7 +1,12 @@
import { html } from 'htm/preact';
import {AVAILABLE_LANGUAGES, AVAILABLE_LANGUAGE_KEYS, language} from '../translations/Translation';
import $ from 'jquery';
import Icons from '../Icons';
import {
AVAILABLE_LANGUAGE_KEYS,
AVAILABLE_LANGUAGES,
language,
} from '../translations/Translation';
function onLanguageChange(e) {
const l = $(e.target).val();
@ -13,11 +18,11 @@ function onLanguageChange(e) {
const LanguageSelector = () => html`
${Icons.language}
<select class="language-selector" onChange=${e => onLanguageChange(e)} value=${language}>${
Object.keys(AVAILABLE_LANGUAGES).map(l =>
html`<option value=${l}>${AVAILABLE_LANGUAGES[l]}</option>`
)
}</select>
<select class="language-selector" onChange=${(e) => onLanguageChange(e)} value=${language}>
${Object.keys(AVAILABLE_LANGUAGES).map(
(l) => html`<option value=${l}>${AVAILABLE_LANGUAGES[l]}</option>`,
)}
</select>
`;
export default LanguageSelector;

View File

@ -1,31 +1,43 @@
import { Component } from 'preact';
import { html } from 'htm/preact';
import iris from 'iris-lib';
import Icons from '../Icons';
import $ from 'jquery';
import { Component } from 'preact';
const isOfType = (f, types) => types.indexOf(f.name.slice(-4)) !== -1;
const isImage = f => isOfType(f, ['.jpg', 'jpeg', '.gif', '.png']);
import Icons from '../Icons';
const isOfType = (f, types) => types.indexOf(f.name.slice(-4)) !== -1;
const isImage = (f) => isOfType(f, ['.jpg', 'jpeg', '.gif', '.png']);
class MediaPlayer extends Component {
componentDidMount() {
iris.local().get('player').on(player => {
const torrentId = player && player.torrentId;
const filePath = player && player.filePath;
if (torrentId !== this.torrentId) {
this.filePath = filePath;
this.torrentId = torrentId;
this.setState({torrentId, isOpen: !!player, splitPath: filePath && filePath.split('/')});
if (torrentId) {
this.startTorrenting();
iris
.local()
.get('player')
.on((player) => {
const torrentId = player && player.torrentId;
const filePath = player && player.filePath;
if (torrentId !== this.torrentId) {
this.filePath = filePath;
this.torrentId = torrentId;
this.setState({
torrentId,
isOpen: !!player,
splitPath: filePath && filePath.split('/'),
});
if (torrentId) {
this.startTorrenting();
}
} else if (filePath && filePath !== this.filePath) {
this.filePath = filePath;
this.setState({ splitPath: filePath && filePath.split('/') });
this.openFile();
}
} else if (filePath && filePath !== this.filePath) {
this.filePath = filePath;
this.setState({splitPath: filePath && filePath.split('/')})
this.openFile();
}
});
iris.local().get('player').get('paused').on(p => this.setPaused(p));
});
iris
.local()
.get('player')
.get('paused')
.on((p) => this.setPaused(p));
}
setPaused(paused) {
@ -35,25 +47,27 @@ class MediaPlayer extends Component {
onTorrent(torrent) {
this.torrent = torrent;
const img = torrent.files.find(f => isImage(f));
let poster = torrent.files.find(f => isImage(f) && (f.name.indexOf('cover') > -1 || f.name.indexOf('poster') > -1));
const img = torrent.files.find((f) => isImage(f));
let poster = torrent.files.find(
(f) => isImage(f) && (f.name.indexOf('cover') > -1 || f.name.indexOf('poster') > -1),
);
poster = poster || img;
const el = $(this.base).find('.cover');
el.empty();
poster && poster.appendTo(el.get(0));
this.setState({isOpen: true});
this.setState({ isOpen: true });
this.openFile();
}
openFile() {
if (this.torrent) {
const file = this.torrent.files.find(f => f.path === this.filePath);
const file = this.torrent.files.find((f) => f.path === this.filePath);
const el = $(this.base).find('.player');
el.empty();
file && file.appendTo(el.get(0), {autoplay: true, muted: false});
file && file.appendTo(el.get(0), { autoplay: true, muted: false });
const audio = el.find('audio').get(0);
if (audio) {
audio.onpause = audio.onplay = e => {
audio.onpause = audio.onplay = (e) => {
iris.local().get('player').get('paused').put(!!e.target.paused);
};
}
@ -79,22 +93,21 @@ class MediaPlayer extends Component {
render() {
const s = this.state;
return html`
<div class="media-player" style="${s.isOpen ? '':'display:none'}">
<div class="media-player" style="${s.isOpen ? '' : 'display:none'}">
<div class="player"></div>
<div class="cover"></div>
<a href="/torrent/${encodeURIComponent(this.state.torrentId)}" class="info">
${s.splitPath ? s.splitPath.map(
(str, i) => {
if (i === s.splitPath.length - 1) {
if (s.isAudioOpen) {
str = str.split('.').slice(0, -1).join('.');
${s.splitPath
? s.splitPath.map((str, i) => {
if (i === s.splitPath.length - 1) {
if (s.isAudioOpen) {
str = str.split('.').slice(0, -1).join('.');
}
return html`<p><b>${str}</b></p>`;
}
return html`<p><b>${str}</b></p>`;
}
return html`<p>${str}</p>`
}
):''}
return html`<p>${str}</p>`;
})
: ''}
</a>
<div class="close" onClick=${() => this.closeClicked()}>${Icons.close}</div>
</div>

View File

@ -1,21 +1,23 @@
import Component from "../BaseComponent";
import { html } from 'htm/preact';
import iris from 'iris-lib';
import {html} from "htm/preact";
import Helpers from "../Helpers";
import logo from "../../assets/img/icon128.png";
import {Link} from "preact-router/match";
import {translate as t} from "../translations/Translation";
import Icons from "../Icons";
import { Link } from 'preact-router/match';
const APPLICATIONS = [ // TODO: move editable shortcuts to localState gun
{url: '/', text: 'home', icon: Icons.home},
{url: '/media', text: 'media', icon: Icons.play},
{url: '/chat', text: 'messages', icon: Icons.chat},
{url: '/store', text: 'market', icon: Icons.store, beta: true },
{url: '/contacts', text: 'contacts', icon: Icons.user},
{url: '/settings', text: 'settings', icon: Icons.settings},
{url: '/explorer', text: 'explorer', icon: Icons.folder, beta: true },
{url: '/about', text: 'about', icon: Icons.info},
import logo from '../../assets/img/icon128.png';
import Component from '../BaseComponent';
import Helpers from '../Helpers';
import Icons from '../Icons';
import { translate as t } from '../translations/Translation';
const APPLICATIONS = [
// TODO: move editable shortcuts to localState gun
{ url: '/', text: 'home', icon: Icons.home },
{ url: '/media', text: 'media', icon: Icons.play },
{ url: '/chat', text: 'messages', icon: Icons.chat },
{ url: '/store', text: 'market', icon: Icons.store, beta: true },
{ url: '/contacts', text: 'contacts', icon: Icons.user },
{ url: '/settings', text: 'settings', icon: Icons.settings },
{ url: '/explorer', text: 'explorer', icon: Icons.folder, beta: true },
{ url: '/about', text: 'about', icon: Icons.info },
];
export default class Menu extends Component {
@ -32,26 +34,33 @@ export default class Menu extends Component {
render() {
return html`
<div class="application-list">
${Helpers.isElectron ? html`<div class="electron-padding"/>` : html`
<a tabindex="3" href="/" onClick=${() => this.menuLinkClicked()} class="logo">
<div class="mobile-menu-icon visible-xs-inline-block">${Icons.menu}</div>
<img src=${logo} width=30 height=30/>
<span style="font-size: 1.5em">iris</span>
</a>
`}
${APPLICATIONS.map(a => {
${Helpers.isElectron
? html`<div class="electron-padding" />`
: html`
<a tabindex="3" href="/" onClick=${() => this.menuLinkClicked()} class="logo">
<div class="mobile-menu-icon visible-xs-inline-block">${Icons.menu}</div>
<img src=${logo} width="30" height="30" />
<span style="font-size: 1.5em">iris</span>
</a>
`}
${APPLICATIONS.map((a) => {
if (a.url && (!a.beta || this.state.showBetaFeatures)) {
return html`
<${a.native ? 'a' : Link} onClick=${() => this.menuLinkClicked()} activeClassName="active" href=${a.url}>
<span class="icon">
${a.text === 'messages' && this.state.unseenMsgsTotal ? html`<span class="unseen unseen-total">${this.state.unseenMsgsTotal}</span>`: ''}
${a.icon || Icons.circle}
</span>
<span class="text">${t(a.text)}</span>
<//>`;
return html` <${a.native ? 'a' : Link}
onClick=${() => this.menuLinkClicked()}
activeClassName="active"
href=${a.url}
>
<span class="icon">
${a.text === 'messages' && this.state.unseenMsgsTotal
? html`<span class="unseen unseen-total">${this.state.unseenMsgsTotal}</span>`
: ''}
${a.icon || Icons.circle}
</span>
<span class="text">${t(a.text)}</span>
<//>`;
}
})}
</div>
`;
}
}
}

View File

@ -1,14 +1,27 @@
import { html } from 'htm/preact';
import iris from 'iris-lib';
import $ from 'jquery';
import { route } from 'preact-router';
import Component from '../BaseComponent';
import Helpers from '../Helpers';
import { html } from 'htm/preact';
import Torrent from './Torrent';
import $ from 'jquery';
import iris from 'iris-lib';
import {route} from 'preact-router';
const ANIMATE_DURATION = 200;
const seenIndicator = html`<span class="seen-indicator"><svg viewBox="0 0 59 42"><polygon fill="currentColor" points="40.6,12.1 17,35.7 7.4,26.1 4.6,29 17,41.3 43.4,14.9"></polygon><polygon class="iris-delivered-checkmark" fill="currentColor" points="55.6,12.1 32,35.7 29.4,33.1 26.6,36 32,41.3 58.4,14.9"></polygon></svg></span>`;
const seenIndicator = html`<span class="seen-indicator"
><svg viewBox="0 0 59 42">
<polygon
fill="currentColor"
points="40.6,12.1 17,35.7 7.4,26.1 4.6,29 17,41.3 43.4,14.9"
></polygon>
<polygon
class="iris-delivered-checkmark"
fill="currentColor"
points="55.6,12.1 32,35.7 29.4,33.1 26.6,36 32,41.3 58.4,14.9"
></polygon></svg
></span>`;
class Message extends Component {
constructor() {
@ -17,34 +30,46 @@ class Message extends Component {
}
componentDidMount() {
$(this.base).find('a').click(e => {
const href = $(e.target).attr('href');
if (href && href.indexOf('https://iris.to/') === 0) {
e.preventDefault();
window.location = href.replace('https://iris.to/', '');
}
});
$(this.base)
.find('a')
.click((e) => {
const href = $(e.target).attr('href');
if (href && href.indexOf('https://iris.to/') === 0) {
e.preventDefault();
window.location = href.replace('https://iris.to/', '');
}
});
const status = this.getSeenStatus();
if (!status.seen && !status.delivered) {
iris.local().get('channels').get(this.props.chatId).get('theirLastActiveTime').on(
this.sub((v, k, a, e) => {
if (this.getSeenStatus().delivered) {
this.setState({delivered:true});
e.off();
}
})
);
iris
.local()
.get('channels')
.get(this.props.chatId)
.get('theirLastActiveTime')
.on(
this.sub((v, k, a, e) => {
if (this.getSeenStatus().delivered) {
this.setState({ delivered: true });
e.off();
}
}),
);
}
if (!status.seen) {
iris.local().get('channels').get(this.props.chatId).get('theirMsgsLastSeenTime').on(this.sub(
(v, k, a, e) => {
if (this.getSeenStatus().seen) {
this.setState({seen:true});
e.off();
}
}
));
iris
.local()
.get('channels')
.get(this.props.chatId)
.get('theirMsgsLastSeenTime')
.on(
this.sub((v, k, a, e) => {
if (this.getSeenStatus().seen) {
this.setState({ seen: true });
e.off();
}
}),
);
}
}
@ -53,60 +78,86 @@ class Message extends Component {
const chat = iris.private(chatId);
const time = typeof this.props.time === 'object' ? this.props.time : new Date(this.props.time);
const seen = chat && chat.theirMsgsLastSeenDate >= time;
const delivered = chat && chat.activity && chat.activity.lastActive && new Date(chat.activity.lastActive) >= time;
return {seen, delivered};
const delivered =
chat &&
chat.activity &&
chat.activity.lastActive &&
new Date(chat.activity.lastActive) >= time;
return { seen, delivered };
}
onNameClick() {
route(`/chat/${ this.props.from}`);
route(`/chat/${this.props.from}`);
}
openAttachmentsGallery(event) {
const msg = this.state.msg || this.props;
$('#floating-day-separator').remove();
const attachmentsPreview = $('<div>').attr('id', 'attachment-gallery').addClass('gallery').addClass('attachment-preview');
const attachmentsPreview = $('<div>')
.attr('id', 'attachment-gallery')
.addClass('gallery')
.addClass('attachment-preview');
$('body').append(attachmentsPreview);
attachmentsPreview.fadeIn(ANIMATE_DURATION);
let left, top, width, img;
if (msg.attachments) {
msg.attachments.forEach(a => {
msg.attachments.forEach((a) => {
if (a.type.indexOf('image') === 0 && a.data) {
img = Helpers.setImgSrc($('<img>'), a.data);
if (msg.attachments.length === 1) {
attachmentsPreview.css({'justify-content': 'center'});
attachmentsPreview.css({ 'justify-content': 'center' });
let original = $(event.target);
left = original.offset().left;
top = original.offset().top - $(window).scrollTop();
width = original.width();
let transitionImg = img.clone().attr('id', 'transition-img').data('originalDimensions', {left,top,width});
transitionImg.css({position: 'fixed', left, top, width, 'max-width': 'none', 'max-height': 'none'});
img.css({visibility: 'hidden', 'align-self': 'center'});
let transitionImg = img
.clone()
.attr('id', 'transition-img')
.data('originalDimensions', { left, top, width });
transitionImg.css({
position: 'fixed',
left,
top,
width,
'max-width': 'none',
'max-height': 'none',
});
img.css({ visibility: 'hidden', 'align-self': 'center' });
attachmentsPreview.append(img);
$('body').append(transitionImg);
let o = img.offset();
transitionImg.animate({width: img.width(), left: o.left, top: o.top}, {duration: ANIMATE_DURATION, complete: () => {
img.css({visibility: 'visible'});
transitionImg.hide();
}});
transitionImg.animate(
{ width: img.width(), left: o.left, top: o.top },
{
duration: ANIMATE_DURATION,
complete: () => {
img.css({ visibility: 'visible' });
transitionImg.hide();
},
},
);
} else {
attachmentsPreview.css({'justify-content': ''});
attachmentsPreview.css({ 'justify-content': '' });
attachmentsPreview.append(img);
}
}
})
});
}
attachmentsPreview.one('click', () => {
this.closeAttachmentsGallery();
});
$(document).off('keyup').on('keyup', e => {
if (e.key === "Escape") { // escape key maps to keycode `27`
$(document).off('keyup');
if ($('#attachment-gallery:visible').length) {
this.closeAttachmentsGallery();
$(document)
.off('keyup')
.on('keyup', (e) => {
if (e.key === 'Escape') {
// escape key maps to keycode `27`
$(document).off('keyup');
if ($('#attachment-gallery:visible').length) {
this.closeAttachmentsGallery();
}
}
}
});
});
}
closeAttachmentsGallery() {
@ -115,16 +166,22 @@ class Message extends Component {
let originalDimensions = transitionImg.data('originalDimensions');
transitionImg.show();
$('#attachment-gallery img').remove();
transitionImg.animate(originalDimensions, {duration: ANIMATE_DURATION, complete: () => {
transitionImg.remove();
}});
transitionImg.animate(originalDimensions, {
duration: ANIMATE_DURATION,
complete: () => {
transitionImg.remove();
},
});
}
$('#attachment-gallery').fadeOut({duration: ANIMATE_DURATION, complete: () => $('#attachment-gallery').remove()});
const activeChat = window.location.hash.replace('#/profile/','').replace('#/chat/','');
$('#attachment-gallery').fadeOut({
duration: ANIMATE_DURATION,
complete: () => $('#attachment-gallery').remove(),
});
const activeChat = window.location.hash.replace('#/profile/', '').replace('#/chat/', '');
if (activeChat) {
iris.private(activeChat).attachments = null;
}
if ("activeElement" in document) {
if ('activeElement' in document) {
document.activeElement.blur();
}
}
@ -140,7 +197,8 @@ class Message extends Component {
name = profile && profile.name;
color = profile && profile.color;
}
const emojiOnly = this.props.text && this.props.text.length === 2 && Helpers.isEmoji(this.props.text);
const emojiOnly =
this.props.text && this.props.text.length === 2 && Helpers.isEmoji(this.props.text);
let text = Helpers.highlightEverything(this.props.text);
@ -155,29 +213,51 @@ class Message extends Component {
<div class="msg ${whose} ${seen} ${delivered}">
<div class="msg-content">
<div class="msg-sender">
${name && this.props.showName && html`<small onclick=${() => this.onNameClick()} class="msgSenderName" style="color: ${color}">${name}</small>`}
${name &&
this.props.showName &&
html`<small
onclick=${() => this.onNameClick()}
class="msgSenderName"
style="color: ${color}"
>${name}</small
>`}
</div>
${this.props.torrentId ? html`
<${Torrent} torrentId=${this.props.torrentId}/>
`:''}
${this.props.attachments && this.props.attachments.map(a =>
html`<div class="img-container"><img src=${a.data} onclick=${e => { this.openAttachmentsGallery(e); }}/></div>` // TODO: escape a.data
${this.props.torrentId ? html` <${Torrent} torrentId=${this.props.torrentId} /> ` : ''}
${this.props.attachments &&
this.props.attachments.map(
(a) =>
html`<div class="img-container">
<img
src=${a.data}
onclick=${(e) => {
this.openAttachmentsGallery(e);
}}
/>
</div>`, // TODO: escape a.data
)}
<div class="text ${emojiOnly && 'emoji-only'}">
${text}
</div>
${this.props.replyingTo ? html`
<div><a href="/post/${encodeURIComponent(this.props.replyingTo)}">Show replied message</a></div>
` : ''}
<div class="text ${emojiOnly && 'emoji-only'}">${text}</div>
${this.props.replyingTo
? html`
<div>
<a href="/post/${encodeURIComponent(this.props.replyingTo)}"
>Show replied message</a
>
</div>
`
: ''}
<div class="below-text">
<div class="time">
${this.props.hash ? html`<a href="/post/${encodeURIComponent(this.props.hash)}">${Helpers.getRelativeTimeText(time)}</a>` : iris.util.formatTime(time)}
${this.props.hash
? html`<a href="/post/${encodeURIComponent(this.props.hash)}"
>${Helpers.getRelativeTimeText(time)}</a
>`
: iris.util.formatTime(time)}
${this.props.selfAuthored && seenIndicator}
</div>
</div>
</div>
</div>
`;
`;
}
}

View File

@ -2,8 +2,8 @@ import Component from '../BaseComponent';
import Helpers from '../Helpers';
import PublicMessage from './PublicMessage';
import iris from 'iris-lib';
import {debounce} from 'lodash';
import {translate as t} from '../translations/Translation';
import { debounce } from 'lodash';
import { translate as t } from '../translations/Translation';
import Button from '../components/basic/Button';
const INITIAL_PAGE_SIZE = 20;
@ -11,31 +11,38 @@ const INITIAL_PAGE_SIZE = 20;
class MessageFeed extends Component {
constructor() {
super();
this.state = {sortedMessages:[], displayCount: INITIAL_PAGE_SIZE};
this.state = { sortedMessages: [], displayCount: INITIAL_PAGE_SIZE };
this.mappedMessages = new Map();
}
updateSortedMessages = debounce(() => {
if (this.unmounted) { return; }
let sortedMessages = Array.from(this.mappedMessages.keys()).sort().map(k => this.mappedMessages.get(k));
if (this.unmounted) {
return;
}
let sortedMessages = Array.from(this.mappedMessages.keys())
.sort()
.map((k) => this.mappedMessages.get(k));
if (!this.props.reverse) {
sortedMessages = sortedMessages.reverse();
}
this.setState({sortedMessages})
this.setState({ sortedMessages });
}, 100);
handleMessage(v, k, x, e, from) {
if (from) { k = k + from; }
if (from) {
k = k + from;
}
if (v) {
if (this.props.keyIsMsgHash) {
// likes and replies are not indexed by timestamp, so we need to fetch all the messages to sort them by timestamp
PublicMessage.fetchByHash(this, k).then(msg => {
PublicMessage.fetchByHash(this, k).then((msg) => {
if (msg) {
this.mappedMessages.set(msg.signedData.time, k);
this.updateSortedMessages();
}
});
} else if (v.length < 45) { // filter out invalid hashes. TODO: where are they coming from?
} else if (v.length < 45) {
// filter out invalid hashes. TODO: where are they coming from?
this.mappedMessages.set(k, v);
}
} else {
@ -47,53 +54,71 @@ class MessageFeed extends Component {
componentDidMount() {
let first = true;
iris.local().get('scrollUp').on(this.sub(
() => {
!first && Helpers.animateScrollTop('.main-view');
first = false;
}
));
iris
.local()
.get('scrollUp')
.on(
this.sub(() => {
!first && Helpers.animateScrollTop('.main-view');
first = false;
}),
);
if (this.props.node) {
this.props.node.map().on(this.sub(
(...args) => this.handleMessage(...args)
));
} else if (this.props.group && this.props.path) { // TODO: make group use the same basic gun api
iris.group(this.props.group).map(this.props.path, this.sub(
(...args) => this.handleMessage(...args)
));
this.props.node.map().on(this.sub((...args) => this.handleMessage(...args)));
} else if (this.props.group && this.props.path) {
// TODO: make group use the same basic gun api
iris.group(this.props.group).map(
this.props.path,
this.sub((...args) => this.handleMessage(...args)),
);
}
}
componentDidUpdate(prevProps) {
const prevNodeId = prevProps.node && prevProps.node._ && prevProps.node._.id;
const newNodeId = this.props.node && this.props.node._ && this.props.node._.id;
if (prevNodeId !== newNodeId || this.props.group !== prevProps.group || this.props.path !== prevProps.path || this.props.filter !== prevProps.filter) {
if (
prevNodeId !== newNodeId ||
this.props.group !== prevProps.group ||
this.props.path !== prevProps.path ||
this.props.filter !== prevProps.filter
) {
this.mappedMessages = new Map();
this.setState({sortedMessages: []});
this.setState({ sortedMessages: [] });
this.componentDidMount();
}
}
render() {
if (!this.props.scrollElement || this.unmounted) { return; }
if (!this.props.scrollElement || this.unmounted) {
return;
}
const displayCount = this.state.displayCount;
return (
<>
<div>
{this.state.sortedMessages.slice(0, displayCount).map(hash => (
{this.state.sortedMessages.slice(0, displayCount).map((hash) => (
<PublicMessage key={hash} hash={hash} showName={true} />
))}
</div>
{displayCount < this.state.sortedMessages.length ? (
<p>
<Button onClick={() => this.setState({displayCount: displayCount + INITIAL_PAGE_SIZE})}>
<Button
onClick={() =>
this.setState({
displayCount: displayCount + INITIAL_PAGE_SIZE,
})
}
>
{t('show_more')}
</Button>
</p>
) : ''}
) : (
''
)}
</>
);
}
}
export default MessageFeed;
export default MessageFeed;

View File

@ -1,12 +1,12 @@
import { Component } from 'preact';
import iris from 'iris-lib';
import { Component } from 'preact';
function twice(f) {
f();
setTimeout(f, 100); // write many times and maybe it goes through :D
}
const mentionRegex = /\B\@[\u00BF-\u1FFF\u2C00-\uD7FF\w]*$/;
const mentionRegex = /\B@[\u00BF-\u1FFF\u2C00-\uD7FF\w]*$/;
export default class MessageForm extends Component {
async sendPublic(msg) {
@ -22,18 +22,20 @@ export default class MessageForm extends Component {
twice(() => iris.public().get('replies').get(msg.replyingTo).get(msg.time).put(hash));
} else {
let node = iris.public();
(this.props.index || (this.props.hashtag && `hashtags/${this.props.hashtag}`) || 'msgs').split('/').forEach(s => {
node.put({});
node = node.get(s);
});
(this.props.index || (this.props.hashtag && `hashtags/${this.props.hashtag}`) || 'msgs')
.split('/')
.forEach((s) => {
node.put({});
node = node.get(s);
});
twice(() => node.get(msg.time).put(hash));
}
const hashtags = msg.text && msg.text.match(/\B\#\w\w+\b/g);
const hashtags = msg.text && msg.text.match(/\B#\w\w+\b/g);
if (hashtags) {
hashtags.forEach(match => {
hashtags.forEach((match) => {
const hashtag = match.replace('#', '');
iris.public().get('hashtags').get(hashtag).put({a:null});
iris.public().get('hashtags').get(hashtag).get(msg.time).put(hash)
iris.public().get('hashtags').get(hashtag).put({ a: null });
iris.public().get('hashtags').get(hashtag).get(msg.time).put(hash);
});
}
msg.torrentId && iris.public().get('media').get(msg.time).put(hash);
@ -44,9 +46,12 @@ export default class MessageForm extends Component {
onMsgTextPaste(event) {
const pasted = (event.clipboardData || window.clipboardData).getData('text');
const magnetRegex = /^magnet:\?xt=urn:btih:*/;
if (pasted !== this.state.torrentId && pasted.indexOf('.torrent') > -1 || pasted.match(magnetRegex)) {
if (
(pasted !== this.state.torrentId && pasted.indexOf('.torrent') > -1) ||
pasted.match(magnetRegex)
) {
event.preventDefault();
this.setState({torrentId: pasted});
this.setState({ torrentId: pasted });
}
}
@ -54,9 +59,9 @@ export default class MessageForm extends Component {
const val = event.target.value.slice(0, event.target.selectionStart);
const matches = val.match(mentionRegex);
if (matches) {
this.setState({mentioning: matches[0].slice(1)});
this.setState({ mentioning: matches[0].slice(1) });
} else if (this.state.mentioning) {
this.setState({mentioning: null});
this.setState({ mentioning: null });
}
}
}

View File

@ -1,6 +1,7 @@
import Component from '../BaseComponent';
import iris from 'iris-lib';
import Component from '../BaseComponent';
type Props = {
pub: string;
placeholder?: string;
@ -16,7 +17,7 @@ class Name extends Component<Props, State> {
}
render() {
return this.state.name ?? this.props.placeholder ?? '';
return this.state.name ?? this.props.placeholder ?? this.props.pub ?? '';
}
}

View File

@ -1,14 +1,17 @@
import {html} from "htm/preact";
import {translate as t} from "../translations/Translation";
import Identicon from "./Identicon";
import FollowButton from "./FollowButton";
import CopyButton from "./CopyButton";
import Text from "./Text";
import Helpers from "../Helpers";
import Component from "../BaseComponent";
import { html } from 'htm/preact';
import iris from 'iris-lib';
const SUGGESTED_FOLLOW = 'hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU';
import Component from '../BaseComponent';
import Helpers from '../Helpers';
import { translate as t } from '../translations/Translation';
import CopyButton from './CopyButton';
import FollowButton from './FollowButton';
import Identicon from './Identicon';
import Text from './Text';
const SUGGESTED_FOLLOW =
'hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU';
export default class OnboardingNotification extends Component {
componentDidMount() {
@ -24,28 +27,44 @@ export default class OnboardingNotification extends Component {
<p>${t('follow_someone_info')}</p>
<div class="profile-link-container">
<a href="/profile/${SUGGESTED_FOLLOW}" class="profile-link">
<${Identicon} str=${SUGGESTED_FOLLOW} width=40 />
<${Text} path="profile/name" user=${SUGGESTED_FOLLOW} placeholder="Suggested follow"/>
<${Identicon} str=${SUGGESTED_FOLLOW} width="40" />
<${Text}
path="profile/name"
user=${SUGGESTED_FOLLOW}
placeholder="Suggested follow"
/>
</a>
<${FollowButton} id=${SUGGESTED_FOLLOW} />
</div>
<p>${t('alternatively')} <a href="/profile/${iris.session.getPubKey()}">${t('give_your_profile_link_to_someone')}</a>.</p>
<p>
${t('alternatively')}
<a href="/profile/${iris.session.getPubKey()}"
>${t('give_your_profile_link_to_someone')}</a
>.
</p>
</div>
</div>
`
`;
}
if (this.state.noFollowers) {
return html`
<div class="msg">
<div class="msg-content">
<p>${t('no_followers_yet')}</p>
<p><${CopyButton} text=${t('copy_link')} copyStr=${Helpers.getProfileLink(iris.session.getPubKey())}/></p>
<p dangerouslySetInnerHTML=${{
<p>
<${CopyButton}
text=${t('copy_link')}
copyStr=${Helpers.getProfileLink(iris.session.getPubKey())}
/>
</p>
<p
dangerouslySetInnerHTML=${{
__html: t(
'alternatively_get_sms_verified',
`href="https://iris-sms-auth.herokuapp.com/?pub=${iris.session.getPubKey()}"`
)}}>
</p>
`href="https://iris-sms-auth.herokuapp.com/?pub=${iris.session.getPubKey()}"`,
),
}}
></p>
<small>${t('no_followers_yet_info')}</small>
</div>
</div>
@ -53,4 +72,4 @@ export default class OnboardingNotification extends Component {
}
return '';
}
}
}

View File

@ -1,81 +1,112 @@
import {Component} from 'preact';
import Helpers from '../Helpers';
import $ from 'jquery';
import iris from 'iris-lib';
import SafeImg from './SafeImg';
import { html } from 'htm/preact';
import iris from 'iris-lib';
import $ from 'jquery';
import { Component } from 'preact';
type Props = { photo?: string;};
import Helpers from '../Helpers';
import SafeImg from './SafeImg';
type Props = { photo?: string };
const ANIMATE_DURATION = 200;
class ProfilePhoto extends Component<Props> {
render() {
return html`<${SafeImg}
class="profile-photo"
src=${this.props.photo}
onClick=${(e) => {
this.imageClicked(e);
}}
/>`;
}
render() {
openAttachmentsGallery(event) {
$('#floating-day-separator').remove();
const attachmentsPreview = $('<div>')
.attr('id', 'attachment-gallery')
.addClass('gallery')
.addClass('attachment-preview');
$('body').append(attachmentsPreview);
attachmentsPreview.fadeIn(ANIMATE_DURATION);
let left, top, width, img;
return (
html`<${SafeImg} class="profile-photo" src=${this.props.photo} onClick=${e => { this.imageClicked(e); }} />`
);
}
openAttachmentsGallery(event) {
$('#floating-day-separator').remove();
const attachmentsPreview = $('<div>').attr('id', 'attachment-gallery').addClass('gallery').addClass('attachment-preview');
$('body').append(attachmentsPreview);
attachmentsPreview.fadeIn(ANIMATE_DURATION);
let left, top, width, img;
if (this.props.photo) {
img = Helpers.setImgSrc($('<img>'), this.props.photo);
attachmentsPreview.css({'justify-content': 'center'});
let original = $(event.target);
left = original.offset().left;
top = original.offset().top - $(window).scrollTop();
width = original.width();
let transitionImg = img.clone().attr('id', 'transition-img').data('originalDimensions', {left,top,width});
transitionImg.css({position: 'fixed', left, top, width, 'max-width': 'none', 'max-height': 'none'});
img.css({visibility: 'hidden', 'align-self': 'center'});
attachmentsPreview.append(img);
$('body').append(transitionImg);
let o = img.offset();
transitionImg.animate({width: img.width(), left: o.left, top: o.top}, {duration: ANIMATE_DURATION, complete: () => {
img.css({visibility: 'visible'});
transitionImg.hide();
}});
}
attachmentsPreview.one('click', () => {
this.closeAttachmentsGallery();
if (this.props.photo) {
img = Helpers.setImgSrc($('<img>'), this.props.photo);
attachmentsPreview.css({ 'justify-content': 'center' });
const original = $(event.target);
left = original.offset().left;
top = original.offset().top - $(window).scrollTop();
width = original.width();
const transitionImg = img
.clone()
.attr('id', 'transition-img')
.data('originalDimensions', { left, top, width });
transitionImg.css({
position: 'fixed',
left,
top,
width,
'max-width': 'none',
'max-height': 'none',
});
$(document).off('keyup').on('keyup', e => {
if (e.key === "Escape") { // escape key maps to keycode `27`
img.css({ visibility: 'hidden', 'align-self': 'center' });
attachmentsPreview.append(img);
$('body').append(transitionImg);
const o = img.offset();
transitionImg.animate(
{ width: img.width(), left: o.left, top: o.top },
{
duration: ANIMATE_DURATION,
complete: () => {
img.css({ visibility: 'visible' });
transitionImg.hide();
},
},
);
}
attachmentsPreview.one('click', () => {
this.closeAttachmentsGallery();
});
$(document)
.off('keyup')
.on('keyup', (e) => {
if (e.key === 'Escape') {
// escape key maps to keycode `27`
$(document).off('keyup');
if ($('#attachment-gallery:visible').length) {
this.closeAttachmentsGallery();
}
}
});
}
closeAttachmentsGallery() {
let transitionImg = $('#transition-img');
if (transitionImg.length) {
let originalDimensions = transitionImg.data('originalDimensions');
transitionImg.show();
$('#attachment-gallery img').remove();
transitionImg.animate(originalDimensions, {duration: ANIMATE_DURATION, complete: () => {
}
closeAttachmentsGallery() {
const transitionImg = $('#transition-img');
if (transitionImg.length) {
const originalDimensions = transitionImg.data('originalDimensions');
transitionImg.show();
$('#attachment-gallery img').remove();
transitionImg.animate(originalDimensions, {
duration: ANIMATE_DURATION,
complete: () => {
transitionImg.remove();
}});
}
$('#attachment-gallery').fadeOut({duration: ANIMATE_DURATION, complete: () => $('#attachment-gallery').remove()});
const activeChat = window.location.hash.replace('#/profile/','').replace('#/chat/','');
iris.private(activeChat).attachments = null;
},
});
}
$('#attachment-gallery').fadeOut({
duration: ANIMATE_DURATION,
complete: () => $('#attachment-gallery').remove(),
});
const activeChat = window.location.hash.replace('#/profile/', '').replace('#/chat/', '');
iris.private(activeChat).attachments = null;
}
imageClicked(event) {
event.preventDefault();
if (window.innerWidth >= 625) {
this.openAttachmentsGallery(event);
}
}
imageClicked(event) {
event.preventDefault();
if (window.innerWidth >= 625) {
this.openAttachmentsGallery(event);
}
}
}
export default ProfilePhoto;
export default ProfilePhoto;

View File

@ -1,11 +1,13 @@
import { Component } from 'preact';
import Helpers from '../Helpers';
import { html } from 'htm/preact';
import {translate as t} from '../translations/Translation';
import SafeImg from './SafeImg';
import Identicon from './Identicon';
import $ from 'jquery';
import { Component } from 'preact';
import Helpers from '../Helpers';
import { translate as t } from '../translations/Translation';
import Button from './basic/Button';
import Identicon from './Identicon';
import SafeImg from './SafeImg';
class ProfilePhotoPicker extends Component {
async useProfilePhotoClicked() {
@ -13,18 +15,20 @@ class ProfilePhotoPicker extends Component {
let resizedCanvas = document.createElement('canvas');
resizedCanvas.width = resizedCanvas.height = Math.min(canvas.width, 800);
const { default: pica } = await import('../lib/pica.min');
pica().resize(canvas, resizedCanvas).then(() => {
let src = resizedCanvas.toDataURL('image/jpeg');
// var src = $('#profile-photo-preview').attr('src');
if (this.props.callback) {
this.props.callback(src);
}
this.setState({preview: null});
});
pica()
.resize(canvas, resizedCanvas)
.then(() => {
let src = resizedCanvas.toDataURL('image/jpeg');
// var src = $('#profile-photo-preview').attr('src');
if (this.props.callback) {
this.props.callback(src);
}
this.setState({ preview: null });
});
}
cancelProfilePhotoClicked() {
this.setState({preview:null});
this.setState({ preview: null });
}
clickProfilePhotoInput() {
@ -42,8 +46,8 @@ class ProfilePhotoPicker extends Component {
}
*/
// show preview
Helpers.getBase64(file).then(base64 => {
this.setState({preview: base64});
Helpers.getBase64(file).then((base64) => {
this.setState({ preview: base64 });
});
}
$(e.target).val('');
@ -52,36 +56,63 @@ class ProfilePhotoPicker extends Component {
componentDidUpdate() {
this.cropper && this.cropper.destroy();
if (this.state.preview) {
import('../lib/cropper.min').then(Cropper => {
import('../lib/cropper.min').then((Cropper) => {
this.cropper = new Cropper.default($('#profile-photo-preview')[0], {
aspectRatio:1,
aspectRatio: 1,
autoCropArea: 1,
viewMode: 1,
background: false,
zoomable: false
zoomable: false,
});
});
}
}
render() {
const currentPhotoEl = this.state.preview ? '' : html`<${SafeImg} class="picker profile-photo" src=${this.props.currentPhoto} onClick=${() => this.clickProfilePhotoInput()}/>`;
const previewPhotoEl = this.state.preview ? html`<img id="profile-photo-preview" src=${this.state.preview}/>` : '';
const addProfilePhotoBtn = (this.props.currentPhoto || this.state.preview) ? '' : html`<div class="picker profile-photo"><${Identicon} str=${this.props.placeholder} width=250 onClick=${() => this.clickProfilePhotoInput()}/></div>`;
const currentPhotoEl = this.state.preview
? ''
: html`<${SafeImg}
class="picker profile-photo"
src=${this.props.currentPhoto}
onClick=${() => this.clickProfilePhotoInput()}
/>`;
const previewPhotoEl = this.state.preview
? html`<img id="profile-photo-preview" src=${this.state.preview} />`
: '';
const addProfilePhotoBtn =
this.props.currentPhoto || this.state.preview
? ''
: html`<div class="picker profile-photo">
<${Identicon}
str=${this.props.placeholder}
width="250"
onClick=${() => this.clickProfilePhotoInput()}
/>
</div>`;
return html`
<div class="profile-photo-picker ${this.state.preview ? 'open' : ''}">
${currentPhotoEl}
${addProfilePhotoBtn}
<div id="profile-photo-preview-container">
${previewPhotoEl}
</div>
${currentPhotoEl} ${addProfilePhotoBtn}
<div id="profile-photo-preview-container">${previewPhotoEl}</div>
<p>
<input name="profile-photo-input" type="file" class="hidden" id="profile-photo-input" onChange=${e => this.onProfilePhotoInputChange(e)} accept="image/*"/>
<input
name="profile-photo-input"
type="file"
class="hidden"
id="profile-photo-input"
onChange=${(e) => this.onProfilePhotoInputChange(e)}
accept="image/*"
/>
</p>
<p id="profile-photo-error" class="${this.state.hasError ? '' : 'hidden'}">
${t('profile_photo_too_big')}
</p>
<p id="profile-photo-error" class="${this.state.hasError ? '' : 'hidden'}">${t('profile_photo_too_big')}</p>
<p class=${this.state.preview ? '' : 'hidden'}>
<${Button} id="cancel-profile-photo" onClick=${() => this.cancelProfilePhotoClicked()}>${t('cancel')}<//>
<${Button} id="use-profile-photo" onClick=${() => this.useProfilePhotoClicked()}>${t('use_photo')}<//>
<${Button} id="cancel-profile-photo" onClick=${() => this.cancelProfilePhotoClicked()}
>${t('cancel')}<//
>
<${Button} id="use-profile-photo" onClick=${() => this.useProfilePhotoClicked()}
>${t('use_photo')}<//
>
</p>
</div>
`;

View File

@ -1,21 +1,35 @@
import Helpers from '../Helpers';
import { Helmet } from 'react-helmet';
import { html } from 'htm/preact';
import Identicon from './Identicon';
import FeedMessageForm from './FeedMessageForm';
import iris from 'iris-lib';
import $ from 'jquery';
import { route } from 'preact-router';
import Helpers from '../Helpers';
import Icons from '../Icons';
import { translate as t } from '../translations/Translation';
import FeedMessageForm from './FeedMessageForm';
import Identicon from './Identicon';
import Message from './Message';
import SafeImg from './SafeImg';
import Torrent from './Torrent';
import Icons from '../Icons';
import $ from 'jquery';
import {Helmet} from "react-helmet";
const MSG_TRUNCATE_LENGTH = 1000;
const replyIcon = html`<svg width="24" version="1.1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;"><path fill="currentColor" d="M256,21.952c-141.163,0-256,95.424-256,212.715c0,60.267,30.805,117.269,84.885,157.717l-41.109,82.219 c-2.176,4.331-1.131,9.579,2.496,12.779c2.005,1.771,4.501,2.667,7.04,2.667c2.069,0,4.139-0.597,5.952-1.813l89.963-60.395
c33.877,12.971,69.781,19.541,106.752,19.541C397.141,447.381,512,351.957,512,234.667S397.163,21.952,256,21.952z M255.979,426.048c-36.16,0-71.168-6.741-104.043-20.032c-3.264-1.323-6.997-0.96-9.941,1.024l-61.056,40.981l27.093-54.187 c2.368-4.757,0.896-10.517-3.477-13.547c-52.907-36.629-83.243-89.707-83.243-145.6c0-105.536,105.28-191.381,234.667-191.381 s234.667,85.824,234.667,191.36S385.365,426.048,255.979,426.048z"/></svg>`;
const replyIcon = html`<svg
width="24"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 512 512"
style="enable-background:new 0 0 512 512;"
>
<path
fill="currentColor"
d="M256,21.952c-141.163,0-256,95.424-256,212.715c0,60.267,30.805,117.269,84.885,157.717l-41.109,82.219 c-2.176,4.331-1.131,9.579,2.496,12.779c2.005,1.771,4.501,2.667,7.04,2.667c2.069,0,4.139-0.597,5.952-1.813l89.963-60.395
c33.877,12.971,69.781,19.541,106.752,19.541C397.141,447.381,512,351.957,512,234.667S397.163,21.952,256,21.952z M255.979,426.048c-36.16,0-71.168-6.741-104.043-20.032c-3.264-1.323-6.997-0.96-9.941,1.024l-61.056,40.981l27.093-54.187 c2.368-4.757,0.896-10.517-3.477-13.547c-52.907-36.629-83.243-89.707-83.243-145.6c0-105.536,105.28-191.381,234.667-191.381 s234.667,85.824,234.667,191.36S385.365,426.048,255.979,426.048z"
/>
</svg>`;
class PublicMessage extends Message {
constructor() {
@ -57,11 +71,15 @@ class PublicMessage extends Message {
componentDidMount() {
this.unmounted = false;
const p = this.fetchByHash();
if (!p) { return; }
p.then(r => {
if (this.unmounted) { return; }
if (!p) {
return;
}
p.then((r) => {
if (this.unmounted) {
return;
}
const msg = r.signedData;
msg.info = {from: r.signerKeyHash};
msg.info = { from: r.signerKeyHash };
if (this.props.filter) {
if (!this.props.filter(msg)) {
return;
@ -70,56 +88,67 @@ class PublicMessage extends Message {
if (this.props.standalone && msg.attachments && msg.attachments.length) {
this.setOgImageUrl(msg.attachments[0].data);
}
this.setState({msg});
this.setState({ msg });
if (this.props.showName && !this.props.name) {
iris.public(msg.info.from).get('profile').get('name').on(this.inject());
}
iris.group().on(`likes/${encodeURIComponent(this.props.hash)}`, this.sub(
(liked,a,b,e,from) => {
iris.group().on(
`likes/${encodeURIComponent(this.props.hash)}`,
this.sub((liked, a, b, e, from) => {
this.eventListeners[`${from}likes`] = e;
liked ? this.likedBy.add(from) : this.likedBy.delete(from);
const s = {likes: this.likedBy.size};
const s = { likes: this.likedBy.size };
if (from === iris.session.getPubKey()) s['liked'] = liked;
this.setState(s);
}
));
iris.group().map(`replies/${encodeURIComponent(this.props.hash)}`, this.sub(
(hash,time,b,e,from) => {
}),
);
iris.group().map(
`replies/${encodeURIComponent(this.props.hash)}`,
this.sub((hash, time, b, e, from) => {
const k = from + time;
if (hash && this.replies[k]) return;
if (hash) {
this.replies[k] = {hash, time};
this.replies[k] = { hash, time };
} else {
delete this.replies[k];
}
this.eventListeners[`${from}replies`] = e;
const sortedReplies = Object.values(this.replies).sort((a,b) => a.time > b.time ? 1 : -1);
this.setState({replyCount: Object.keys(this.replies).length, sortedReplies });
}
));
const sortedReplies = Object.values(this.replies).sort((a, b) =>
a.time > b.time ? 1 : -1,
);
this.setState({
replyCount: Object.keys(this.replies).length,
sortedReplies,
});
}),
);
});
}
componentDidUpdate() {
if (this.state.msg && !this.linksDone) {
$(this.base).find('a').off().on('click', e => {
const href = $(e.target).attr('href');
if (href && href.indexOf('https://iris.to/') === 0) {
e.preventDefault();
window.location = href.replace('https://iris.to/', '');
}
});
$(this.base)
.find('a')
.off()
.on('click', (e) => {
const href = $(e.target).attr('href');
if (href && href.indexOf('https://iris.to/') === 0) {
e.preventDefault();
window.location = href.replace('https://iris.to/', '');
}
});
this.linksDone = true;
}
}
toggleReplies() {
const showReplyForm = !this.state.showReplyForm;
this.setState({showReplyForm});
this.setState({ showReplyForm });
}
onClickName() {
route(`/profile/${ this.state.msg.info.from}`);
route(`/profile/${this.state.msg.info.from}`);
}
likeBtnClicked(e) {
@ -133,20 +162,28 @@ class PublicMessage extends Message {
const author = this.state.msg && this.state.msg.info && this.state.msg.info.from;
if (author !== iris.session.getPubKey()) {
const t = (this.state.msg.text || '').trim();
const title = `${iris.session.getMyName() } liked your post`;
const title = `${iris.session.getMyName()} liked your post`;
const body = `'${t.slice(0, 100)}${t.length > 100 ? '...' : ''}'`;
iris.notifications.sendIrisNotification(author, {event:'like', target: this.props.hash});
iris.notifications.sendWebPushNotification(author, {title, body});
iris.notifications.sendIrisNotification(author, {
event: 'like',
target: this.props.hash,
});
iris.notifications.sendWebPushNotification(author, { title, body });
}
}
}
onDelete(e) {
e.preventDefault();
if (confirm('Delete message?')) { // TODO: remove from hashtag indexes
if (confirm('Delete message?')) {
// TODO: remove from hashtag indexes
const msg = this.state.msg;
msg.torrentId && iris.public().get('media').get(msg.time).put(null);
iris.public().get(this.props.index || 'msgs').get(msg.time).put(null);
iris
.public()
.get(this.props.index || 'msgs')
.get(msg.time)
.put(null);
msg.replyingTo && iris.public().get('replies').get(msg.replyingTo).get(msg.time).put(null);
}
}
@ -172,121 +209,181 @@ class PublicMessage extends Message {
}
render() {
if (!this.state.msg) { return ''; }
if (!this.state.msg) {
return '';
}
//if (++this.i > 1) console.log(this.i);
let name = this.props.name || this.state.name;
const emojiOnly = this.state.msg.text && this.state.msg.text.length === 2 && Helpers.isEmoji(this.state.msg.text);
const emojiOnly =
this.state.msg.text &&
this.state.msg.text.length === 2 &&
Helpers.isEmoji(this.state.msg.text);
const isThumbnail = this.props.thumbnail ? 'thumbnail-item' : '';
let text = this.state.msg.text;
const shortText = text.length > 128 ? `${text.slice(0,128) }...` : text;
const shortText = text.length > 128 ? `${text.slice(0, 128)}...` : text;
const quotedShortText = `"${shortText}"`;
if (isThumbnail) {
text = shortText;
}
const title = `${name || 'User'} on Iris`;
text = (text.length > MSG_TRUNCATE_LENGTH) && !this.state.showMore && !this.props.standalone ?
`${text.slice(0, MSG_TRUNCATE_LENGTH) }...` : text;
text =
text.length > MSG_TRUNCATE_LENGTH && !this.state.showMore && !this.props.standalone
? `${text.slice(0, MSG_TRUNCATE_LENGTH)}...`
: text;
text = Helpers.highlightEverything(text);
//console.log('text', text);
const time = typeof this.state.msg.time === 'object' ? this.state.msg.time : new Date(this.state.msg.time);
const dateStr = time.toLocaleString(window.navigator.language, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
const timeStr = time.toLocaleTimeString(window.navigator.language, {timeStyle: 'short'});
const time =
typeof this.state.msg.time === 'object' ? this.state.msg.time : new Date(this.state.msg.time);
const dateStr = time.toLocaleString(window.navigator.language, {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
const timeStr = time.toLocaleTimeString(window.navigator.language, {
timeStyle: 'short',
});
const s = this.state;
return html`
<div ref=${this.ref} class="msg ${isThumbnail} ${this.props.asReply ? 'reply' : ''} ${this.props.standalone ? 'standalone' : ''}">
<div
ref=${this.ref}
class="msg ${isThumbnail} ${this.props.asReply ? 'reply' : ''} ${this.props.standalone
? 'standalone'
: ''}"
>
<div class="msg-content">
<div class="msg-sender">
<div class="msg-sender-link" onclick=${() => this.onClickName()}>
${s.msg.info.from ? html`<${Identicon} str=${s.msg.info.from} width=40/>` : ''}
${s.msg.info.from ? html`<${Identicon} str=${s.msg.info.from} width="40" />` : ''}
${name && this.props.showName && html`<small class="msgSenderName">${name}</small>`}
</div>
${s.msg.info.from === iris.session.getPubKey() ? html`
<div class="msg-menu-btn">
<div class="dropdown">
<div class="dropbtn">\u2026</div>
<div class="dropdown-content">
<a href="#" onClick=${e => this.onDelete(e)}>Delete</a>
${s.msg.info.from === iris.session.getPubKey()
? html`
<div class="msg-menu-btn">
<div class="dropdown">
<div class="dropbtn"></div>
<div class="dropdown-content">
<a href="#" onClick=${(e) => this.onDelete(e)}>Delete</a>
</div>
</div>
</div>
</div>
</div>
`: ''}
`
: ''}
</div>
${this.props.standalone ? html`
<${Helmet} titleTemplate="%s">
<title>${title}: ${quotedShortText}</title>
<meta name="description" content=${quotedShortText} />
<meta property="og:type" content="article" />
${s.ogImageUrl ? html`<meta property="og:image" content=${s.ogImageUrl} />` : ''}
<meta property="og:title" content=${title} />
<meta property="og:description" content=${quotedShortText} />
<//>
` : ''}
${s.msg.torrentId ? html`
<${Torrent} torrentId=${s.msg.torrentId} autopause=${!this.props.standalone}/>
`:''}
${s.msg.attachments && s.msg.attachments.map((a, i) => {
${this.props.standalone
? html`
<${Helmet} titleTemplate="%s">
<title>${title}: ${quotedShortText}</title>
<meta name="description" content=${quotedShortText} />
<meta property="og:type" content="article" />
${s.ogImageUrl ? html`<meta property="og:image" content=${s.ogImageUrl} />` : ''}
<meta property="og:title" content=${title} />
<meta property="og:description" content=${quotedShortText} />
<//>
`
: ''}
${s.msg.torrentId
? html`
<${Torrent} torrentId=${s.msg.torrentId} autopause=${!this.props.standalone} />
`
: ''}
${s.msg.attachments &&
s.msg.attachments.map((a, i) => {
if (i > 0 && !this.props.standalone && !this.state.showMore) {
return;
}
return html`<div class="img-container">
<div class="heart"></div>
<${SafeImg} src=${a.data} onClick=${e => { this.imageClicked(e); }}/>
<${SafeImg}
src=${a.data}
onClick=${(e) => {
this.imageClicked(e);
}}
/>
</div>`;
})}
<div class="text ${emojiOnly && 'emoji-only'}">
${text}
</div>
${!this.props.standalone && (s.msg.attachments && (s.msg.attachments.length > 1) ||
(this.state.msg.text.length > MSG_TRUNCATE_LENGTH)) ? html`
<a onClick=${e => {
e.preventDefault();
this.setState({showMore: !s.showMore});
}}>Show ${s.showMore ? 'less' : 'more'}</a>
` : ''}
${s.msg.replyingTo && !this.props.asReply ? html`
<div><a href="/post/${encodeURIComponent(s.msg.replyingTo)}">Show replied message</a></div>
` : ''}
<div class="text ${emojiOnly && 'emoji-only'}">${text}</div>
${!this.props.standalone &&
((s.msg.attachments && s.msg.attachments.length > 1) ||
this.state.msg.text.length > MSG_TRUNCATE_LENGTH)
? html`
<a
onClick=${(e) => {
e.preventDefault();
this.setState({ showMore: !s.showMore });
}}
>
${t(`show_${s.showMore ? 'less' : 'more'}`)}</a
>
`
: ''}
${s.msg.replyingTo && !this.props.asReply
? html`
<div>
<a href="/post/${encodeURIComponent(s.msg.replyingTo)}">Show replied message</a>
</div>
`
: ''}
<div class="below-text">
<a class="msg-btn reply-btn" onClick=${() => this.toggleReplies()}>
${replyIcon}
</a>
<span class="count" onClick=${() => this.toggleReplies()}>
${s.replyCount || ''}
</span>
<a class="msg-btn like-btn ${s.liked ? 'liked' : ''}" onClick=${e => this.likeBtnClicked(e)}>
<a class="msg-btn reply-btn" onClick=${() => this.toggleReplies()}> ${replyIcon} </a>
<span class="count" onClick=${() => this.toggleReplies()}> ${s.replyCount || ''} </span>
<a
class="msg-btn like-btn ${s.liked ? 'liked' : ''}"
onClick=${(e) => this.likeBtnClicked(e)}
>
${s.liked ? Icons.heartFull : Icons.heartEmpty}
</a>
<span class="count" onClick=${() => this.setState({showLikes: !s.showLikes})}>
<span class="count" onClick=${() => this.setState({ showLikes: !s.showLikes })}>
${s.likes || ''}
</span>
<div class="time">
<a href="/post/${encodeURIComponent(this.props.hash)}" class="tooltip">
${Helpers.getRelativeTimeText(time)}
<span class="tooltiptext">
${dateStr} ${timeStr}
</span>
${Helpers.getRelativeTimeText(time)}
<span class="tooltiptext"> ${dateStr} ${timeStr} </span>
</a>
</div>
</div>
${s.showLikes ? html`
<div class="likes">
${Array.from(this.likedBy).map(key => {
return html`<${Identicon} showTooltip=${true} onClick=${() => route(`/profile/${ key}`)} str=${key} width=32/>`;
})}
</div>
`: ''}
${(this.props.showReplies || s.showReplyForm) && s.sortedReplies && s.sortedReplies.length ? s.sortedReplies.map(r =>
html`<${PublicMessage} key=${r.hash} hash=${r.hash} asReply=${true} showName=${true} showReplies=${true} />`
) : ''}
${this.props.standalone || s.showReplyForm ? html`
<${FeedMessageForm} autofocus=${!this.props.standalone} replyingTo=${this.props.hash} replyingToUser=${s.msg.info.from} />
` : ''}
${s.showLikes
? html`
<div class="likes">
${Array.from(this.likedBy).map((key) => {
return html`<${Identicon}
showTooltip=${true}
onClick=${() => route(`/profile/${key}`)}
str=${key}
width="32"
/>`;
})}
</div>
`
: ''}
${(this.props.showReplies || s.showReplyForm) && s.sortedReplies && s.sortedReplies.length
? s.sortedReplies.map(
(r) =>
html`<${PublicMessage}
key=${r.hash}
hash=${r.hash}
asReply=${true}
showName=${true}
showReplies=${true}
/>`,
)
: ''}
${this.props.standalone || s.showReplyForm
? html`
<${FeedMessageForm}
autofocus=${!this.props.standalone}
replyingTo=${this.props.hash}
replyingToUser=${s.msg.info.from}
/>
`
: ''}
</div>
</div>
`;
`;
}
}

View File

@ -9,6 +9,6 @@ const SafeImg = (props: Props) => {
props.src = '';
}
return <img {...props} />;
}
};
export default SafeImg;

View File

@ -1,17 +1,20 @@
import iris from 'iris-lib';
import $ from 'jquery';
import isEqual from 'lodash/isEqual';
import Component from '../BaseComponent';
import Helpers from '../Helpers';
import iris from 'iris-lib';
import { translate as t } from '../translations/Translation';
import Identicon from './Identicon';
import Text from './Text';
import {translate as t} from '../translations/Translation';
import $ from 'jquery';
import _ from 'lodash';
const RESULTS_MAX = 5;
const suggestedFollow = 'hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU';
const suggestedFollow =
'hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU';
type Props = {
onSelect?: (result: Object) => void;
onSelect?: (result: Pick<ResultItem, 'key'>) => void;
query?: string;
focus?: boolean;
resultsOnly?: boolean;
@ -21,16 +24,16 @@ type Props = {
type Result = {
item: ResultItem;
}
};
type ResultItem = {
key: string;
followers: Map<string, any>;
followDistance: number;
name?: string;
photo?: string;
uuid?: string;
}
key: string;
followers: Map<string, unknown>;
followDistance: number;
name?: string;
photo?: string;
uuid?: string;
};
type State = {
results: Array<Result>;
@ -38,15 +41,21 @@ type State = {
noFollows: boolean;
offsetLeft: number;
selected: number;
}
};
class SearchBox extends Component<Props, State> {
constructor() {
super();
this.state = {results:[], query: '', noFollows: true, offsetLeft: 0, selected: 0};
this.state = {
results: [],
query: '',
noFollows: true,
offsetLeft: 0,
selected: 0,
};
}
onInput(_e) {
onInput() {
this.search();
}
@ -57,36 +66,42 @@ class SearchBox extends Component<Props, State> {
const selected = this.state.selected;
let next = e.keyCode === 40 ? selected + 1 : selected - 1;
next = Math.max(0, Math.min(this.state.results.length - 1, next));
this.setState({selected: next});
this.setState({ selected: next });
}
}
close() {
$(this.base).find('input').val('');
this.setState({results:[], query: ''});
this.setState({ results: [], query: '' });
}
componentDidMount() {
iris.local().get('noFollows').on(this.inject());
iris.local().get('searchIndexUpdated').on(this.sub(
() => this.search()
));
iris.local().get('activeRoute').on(this.sub(
() => {
this.close();
}
));
iris
.local()
.get('searchIndexUpdated')
.on(this.sub(() => this.search()));
iris
.local()
.get('activeRoute')
.on(
this.sub(() => {
this.close();
}),
);
this.adjustResultsPosition();
this.search();
$(document).off('keydown').on('keydown', (e) => {
if (e.key === "Tab" && document.activeElement.tagName === 'BODY') {
$(document)
.off('keydown')
.on('keydown', (e) => {
if (e.key === 'Tab' && document.activeElement.tagName === 'BODY') {
e.preventDefault();
$(this.base).find('input').focus();
} else if (e.key === "Escape") {
this.close();
$(this.base).find('input').blur();
}
});
} else if (e.key === 'Escape') {
this.close();
$(this.base).find('input').blur();
}
});
}
componentDidUpdate(prevProps, prevState) {
@ -98,8 +113,13 @@ class SearchBox extends Component<Props, State> {
this.search();
}
// if first 5 results are different, set selected = 0
if (!_.isEqual(this.state.results.slice(0, this.state.selected + 1), prevState.results.slice(0, this.state.selected + 1))) {
this.setState({selected: 0});
if (
!isEqual(
this.state.results.slice(0, this.state.selected + 1),
prevState.results.slice(0, this.state.selected + 1),
)
) {
this.setState({ selected: 0 });
}
}
@ -111,16 +131,15 @@ class SearchBox extends Component<Props, State> {
adjustResultsPosition() {
const input = $(this.base).find('input');
if (input.length) {
this.setState({offsetLeft: input[0].offsetLeft});
this.setState({ offsetLeft: input[0].offsetLeft });
}
}
onSubmit(e) {
e.preventDefault();
const el = $(this.base).find('input');
const query = el.val();
el.val('');
el.blur();
el.trigger('blur');
// TODO go to first result
const selected = $(this.base).find('.result.selected');
if (selected.length) {
@ -144,20 +163,20 @@ class SearchBox extends Component<Props, State> {
if (this.props.onSelect) {
const s = query.split('/profile/');
if (s.length > 1) {
return this.props.onSelect({key: s[1]});
return this.props.onSelect({ key: s[1] });
}
const key = Helpers.getUrlParameter('chatWith', s[1]);
if (key) {
return this.props.onSelect({key});
return this.props.onSelect({ key });
}
}
if (Helpers.followChatLink(query)) return;
if (query) {
const results = iris.session.getSearchIndex().search(query).slice(0,RESULTS_MAX);
this.setState({results, query});
const results = iris.session.getSearchIndex().search(query).slice(0, RESULTS_MAX);
this.setState({ results, query });
} else {
this.setState({results:[], query});
this.setState({ results: [], query });
}
}
@ -171,26 +190,34 @@ class SearchBox extends Component<Props, State> {
}
onResultFocus(e, index) {
this.setState({selected: index});
this.setState({ selected: index });
}
render() {
return (
<div class={`search-box ${this.props.class}`}>
{this.props.resultsOnly ? '' : (
<form onSubmit={e => this.onSubmit(e)}>
{this.props.resultsOnly ? (
''
) : (
<form onSubmit={(e) => this.onSubmit(e)}>
<label>
<input type="text"
onKeyPress={e => this.preventUpDownDefault(e)}
onKeyDown={e => this.preventUpDownDefault(e)}
onKeyUp={e => this.onKeyUp(e)}
placeholder={t('search')}
tabIndex={1}
onInput={(e) => this.onInput(e)}/>
<input
type="text"
onKeyPress={(e) => this.preventUpDownDefault(e)}
onKeyDown={(e) => this.preventUpDownDefault(e)}
onKeyUp={(e) => this.onKeyUp(e)}
placeholder={t('search')}
tabIndex={1}
onInput={() => this.onInput()}
/>
</label>
</form>
)}
<div onKeyUp={e => this.onKeyUp(e)} class="search-box-results" style="left: ${this.state.offsetLeft || ''}">
<div
onKeyUp={(e) => this.onKeyUp(e)}
class="search-box-results"
style="left: ${this.state.offsetLeft || ''}"
>
{this.state.results.map((r, index) => {
const i = r.item;
let followText = '';
@ -200,18 +227,28 @@ class SearchBox extends Component<Props, State> {
} else if (i.followDistance === 1) {
followText = 'Following';
} else {
followText = `${ i.followers.size } followers`;
followText = `${i.followers.size} followers`;
}
}
return (
<a onFocus={e => this.onResultFocus(e, index)} tabIndex={2} className={'result ' + (index === this.state.selected ? 'selected' : '')} href={i.uuid ? `/group/${i.uuid}` : `/profile/${i.key}`} onClick={e => this.onClick(e, i)}>
{i.photo ? <div class="identicon-container"><img src={i.photo} class="round-borders" height={40} width={40} alt=""/></div> : <Identicon key={`${i.key }ic`} str={i.key} width={40} />}
<a
onFocus={(e) => this.onResultFocus(e, index)}
tabIndex={2}
className={'result ' + (index === this.state.selected ? 'selected' : '')}
href={i.uuid ? `/group/${i.uuid}` : `/profile/${i.key}`}
onClick={(e) => this.onClick(e, i)}
>
{i.photo ? (
<div class="identicon-container">
<img src={i.photo} class="round-borders" height={40} width={40} alt="" />
</div>
) : (
<Identicon key={`${i.key}ic`} str={i.key} width={40} />
)}
<div>
{i.name || ''}<br/>
<small>
{followText}
</small>
{i.name || ''}
<br />
<small>{followText}</small>
</div>
</a>
);
@ -220,14 +257,17 @@ class SearchBox extends Component<Props, State> {
<>
<a class="follow-someone">Follow someone to see more search results!</a>
<a href={`/profile/${suggestedFollow}`} class="suggested">
<Identicon str={suggestedFollow} width={40}/>
<Identicon str={suggestedFollow} width={40} />
<div>
<Text user={suggestedFollow} path="profile/name" /><br/>
<Text user={suggestedFollow} path="profile/name" />
<br />
<small>Suggested</small>
</div>
</a>
</>
) : ''}
) : (
''
)}
</div>
</div>
);

View File

@ -10,4 +10,4 @@ export default class SubscribeHashtagButton extends FollowButton {
this.activeClass = 'following';
this.hoverAction = 'unsubscribe';
}
}
}

View File

@ -1,7 +1,7 @@
import Component from '../BaseComponent';
import iris from 'iris-lib';
import { createRef, JSX, RefObject } from 'preact';
import {createRef, RefObject, JSX} from "preact";
import Component from '../BaseComponent';
type Props = {
json?: boolean;
@ -24,22 +24,29 @@ class Text extends Component<Props, State> {
super();
this.ref = createRef();
this.eventListeners = {};
this.state = {value: '', class: ''};
this.state = { value: '', class: '' };
}
componentDidMount() {
this.getNode().on(this.sub(value => {
if (!(this.ref.current && this.ref.current === document.activeElement)) {
this.setState({value, class: typeof value === 'string' ? '' : 'iris-non-string'});
}
}));
this.getNode().on(
this.sub((value) => {
if (!(this.ref.current && this.ref.current === document.activeElement)) {
this.setState({
value,
class: typeof value === 'string' ? '' : 'iris-non-string',
});
}
}),
);
}
getParsedValue(s: string): string {
if (this.props.json) {
try {
s = JSON.parse(s);
} catch (e) { null; }
} catch (e) {
null;
}
}
return s;
}
@ -50,26 +57,34 @@ class Text extends Component<Props, State> {
if (user) {
base = base.user(user);
}
this.setState({editable: !user || user === iris.session.getPubKey()});
this.setState({ editable: !user || user === iris.session.getPubKey() });
const path = this.props.path.split('/');
return path.reduce((sum, current) => (current && sum.get(decodeURIComponent(current))) || sum, base);
return path.reduce(
(sum, current) => (current && sum.get(decodeURIComponent(current))) || sum,
base,
);
}
onInput(e: JSX.TargetedEvent<HTMLSpanElement>) {
const val = this.getParsedValue(e.currentTarget.innerText);
this.getNode().put(val);
this.setState({class: typeof val === 'string' ? '' : 'iris-non-string'});
this.setState({ class: typeof val === 'string' ? '' : 'iris-non-string' });
}
render() {
const val = this.props.json ? JSON.stringify(this.state.value) : this.state.value;
return this.state.editable
? (
<span class={this.state.class} ref={this.ref} contentEditable onInput={e => this.onInput(e)}>
{val}
</span>
)
: val;
return this.state.editable ? (
<span
class={this.state.class}
ref={this.ref}
contentEditable
onInput={(e) => this.onInput(e)}
>
{val}
</span>
) : (
val
);
}
}

View File

@ -1,38 +1,45 @@
import Component from '../BaseComponent';
import { createRef } from 'preact';
import Helpers from '../Helpers';
import { Helmet } from 'react-helmet';
import { html } from 'htm/preact';
import { translate as tr } from '../translations/Translation';
import $ from 'jquery';
import iris from 'iris-lib';
import Icons from '../Icons';
import {Helmet} from 'react-helmet';
import $ from 'jquery';
import { createRef } from 'preact';
const isOfType = (f, types) => types.indexOf(f.name.slice(-4)) !== -1;
const isVideo = f => isOfType(f, ['webm', '.mp4', '.ogg']);
const isAudio = f => isOfType(f, ['.mp3', '.wav', '.m4a']);
const isImage = f => isOfType(f, ['.jpg', 'jpeg', '.gif', '.png']);
import Component from '../BaseComponent';
import Helpers from '../Helpers';
import Icons from '../Icons';
import { translate as tr } from '../translations/Translation';
const isOfType = (f, types) => types.indexOf(f.name.slice(-4)) !== -1;
const isVideo = (f) => isOfType(f, ['webm', '.mp4', '.ogg']);
const isAudio = (f) => isOfType(f, ['.mp3', '.wav', '.m4a']);
const isImage = (f) => isOfType(f, ['.jpg', 'jpeg', '.gif', '.png']);
class Torrent extends Component {
coverRef = createRef();
state = { settings: {}};
state = { settings: {} };
componentDidMount() {
iris.local().get('player').on(this.sub(
(player) => {
this.player = player;
this.setState({player});
if (this.torrent && this.player && this.player.filePath !== this.state.activeFilePath) {
const file = this.getActiveFile(this.torrent);
file && this.openFile(file);
}
}
));
iris
.local()
.get('player')
.on(
this.sub((player) => {
this.player = player;
this.setState({ player });
if (this.torrent && this.player && this.player.filePath !== this.state.activeFilePath) {
const file = this.getActiveFile(this.torrent);
file && this.openFile(file);
}
}),
);
const showFiles = this.props.showFiles;
showFiles && this.setState({showFiles});
showFiles && this.setState({ showFiles });
iris.local().get('settings').on(this.inject());
(async () => {
if (this.props.standalone || (await iris.local().get('settings').get('enableWebtorrent').once())) {
if (
this.props.standalone ||
(await iris.local().get('settings').get('enableWebtorrent').once())
) {
this.startTorrenting();
}
})();
@ -51,7 +58,7 @@ class Torrent extends Component {
}
async startTorrenting(clicked) {
this.setState({torrenting: true});
this.setState({ torrenting: true });
const torrentId = this.props.torrentId;
const { default: AetherTorrent } = await import('aether-torrent');
const client = new AetherTorrent();
@ -65,12 +72,12 @@ class Torrent extends Component {
playAudio(filePath, e) {
e && e.preventDefault();
iris.local().get('player').put({torrentId: this.props.torrentId, filePath, paused: false});
iris.local().get('player').put({ torrentId: this.props.torrentId, filePath, paused: false });
}
pauseAudio(e) {
e && e.preventDefault();
iris.local().get('player').put({paused: true});
iris.local().get('player').put({ paused: true });
}
openFile(file, clicked) {
@ -91,7 +98,7 @@ class Torrent extends Component {
if (!isVid) {
splitPath = file.path.split('/');
}
this.setState({activeFilePath: file.path, splitPath, isAudioOpen: isAud});
this.setState({ activeFilePath: file.path, splitPath, isAudioOpen: isAud });
let autoplay, muted;
if (clicked) {
autoplay = true;
@ -106,7 +113,7 @@ class Torrent extends Component {
this.playAudio(file.path);
}
if (!isAud) {
file.appendTo(el.get(0), {autoplay, muted});
file.appendTo(el.get(0), { autoplay, muted });
}
if (isVid && this.props.autopause) {
const vid = base.find('video').get(0);
@ -118,10 +125,10 @@ class Torrent extends Component {
vid.pause();
}
});
}
};
const options = {
rootMargin: "0px",
threshold: [0.25, 0.75]
rootMargin: '0px',
threshold: [0.25, 0.75],
};
this.observer = new IntersectionObserver(handlePlay, options);
this.observer.observe(vid);
@ -140,10 +147,10 @@ class Torrent extends Component {
getNextIndex(typeCheck) {
const files = this.state.torrent.files;
const currentIndex = files.findIndex(f => f.path === this.state.activeFilePath);
const currentIndex = files.findIndex((f) => f.path === this.state.activeFilePath);
let nextIndex = files.findIndex((f, i) => i > currentIndex && typeCheck(f));
if (nextIndex === -1) {
nextIndex = files.findIndex(f => typeCheck(f));
nextIndex = files.findIndex((f) => typeCheck(f));
}
if (nextIndex === -1) {
nextIndex = currentIndex;
@ -160,7 +167,7 @@ class Torrent extends Component {
const p = this.player;
let file;
if (p && p.torrentId === this.props.torrentId) {
file = torrent.files.find(f => {
file = torrent.files.find((f) => {
return f.path === p.filePath;
});
}
@ -168,37 +175,41 @@ class Torrent extends Component {
}
onTorrent(torrent, clicked) {
if (!this.coverRef.current) { return; }
this.torrent = torrent;
if (!torrent.files) {
console.error("no files found in torrent:", torrent);
if (!this.coverRef.current) {
return;
}
const video = torrent.files.find(f => isVideo(f));
const audio = torrent.files.find(f => isAudio(f));
const img = torrent.files.find(f => isImage(f));
this.torrent = torrent;
if (!torrent.files) {
console.error('no files found in torrent:', torrent);
return;
}
const video = torrent.files.find((f) => isVideo(f));
const audio = torrent.files.find((f) => isAudio(f));
const img = torrent.files.find((f) => isImage(f));
const file = this.getActiveFile(torrent) || video || audio || img || torrent.files[0];
this.setState({torrent, cover: img});
this.setState({ torrent, cover: img });
file && this.openFile(file, clicked);
let poster = torrent.files.find(f => isImage(f) && (f.name.indexOf('cover') > -1 || f.name.indexOf('poster') > -1));
let poster = torrent.files.find(
(f) => isImage(f) && (f.name.indexOf('cover') > -1 || f.name.indexOf('poster') > -1),
);
poster = poster || img;
if (poster) {
poster.appendTo(this.coverRef.current);
if (this.props.standalone && this.isUserAgentCrawler()) {
const imgEl = this.coverRef.current.firstChild;
imgEl.onload = async () => {
const blob = await fetch(imgEl.src).then(r => r.blob());
Helpers.getBase64(blob).then(src => this.setOgImageUrl(src));
}
const blob = await fetch(imgEl.src).then((r) => r.blob());
Helpers.getBase64(blob).then((src) => this.setOgImageUrl(src));
};
}
}
}
showFilesClicked(e) {
e.preventDefault();
this.setState({showFiles: !this.state.showFiles});
this.setState({ showFiles: !this.state.showFiles });
}
openTorrentClicked(e) {
@ -213,61 +224,71 @@ class Torrent extends Component {
const playing = p && p.torrentId === this.props.torrentId && !p.paused;
let playButton = '';
if (s.isAudioOpen) {
playButton = playing ? html`
<a href="#" onClick=${e => this.pauseAudio(e)}>${Icons.pause}</a>
`: html`
<a href="#" onClick=${e => this.playAudio(s.activeFilePath, e)}>${Icons.play}</a>
`;
playButton = playing
? html` <a href="#" onClick=${(e) => this.pauseAudio(e)}>${Icons.pause}</a> `
: html`
<a href="#" onClick=${(e) => this.playAudio(s.activeFilePath, e)}>${Icons.play}</a>
`;
}
return html`
${s.torrenting && !s.torrent ? html`<p>Loading attachment...</p>`:''}
${s.torrenting && !s.torrent ? html`<p>Loading attachment...</p>` : ''}
<div class="cover" ref=${this.coverRef} style=${s.isAudioOpen ? '' : 'display:none'}></div>
<div class="info">
${s.splitPath ? s.splitPath.map(
(str, i) => {
${s.splitPath
? s.splitPath.map((str, i) => {
if (i === s.splitPath.length - 1) {
if (s.isAudioOpen) {
str = str.split('.').slice(0, -1).join('.');
}
return html`<p><b>${str}</b></p>`;
}
return html`<p>${str}</p>`
}
):''}
return html`<p>${str}</p>`;
})
: ''}
</div>
${s.hasNext ? html`<b>prev</b>`:''}
<div class="player">
${playButton}
</div>
${s.hasNext ? html`<b>next</b>`:''}
${(this.props.standalone || this.props.preview) ? html`
<a href=${this.props.torrentId}>Magnet link</a>
${t && t.files ? html`
<a href="" style="margin-left:30px;" onClick=${e => this.showFilesClicked(e)}>${tr(
s.showFiles ? 'hide_files' : 'show_files'
)}</a>
`:''}
` : html`
<a href="/torrent/${encodeURIComponent(this.props.torrentId)}">${tr('show_files')}</a>
`}
${s.showFiles && t && t.files ? html`
<p>${tr('peers')}: ${t.numPeers}</p>
<div class="flex-table details">
${t.files.map(f => html`
<div onClick=${() => this.openFile(f, true)} class="flex-row ${s.activeFilePath === f.path ? 'active' : ''}">
<div class="flex-cell">${f.name}</div>
<div class="flex-cell no-flex">${Helpers.formatBytes(f.length)}</div>
${s.hasNext ? html`<b>prev</b>` : ''}
<div class="player">${playButton}</div>
${s.hasNext ? html`<b>next</b>` : ''}
${this.props.standalone || this.props.preview
? html`
<a href=${this.props.torrentId}>Magnet link</a>
${t && t.files
? html`
<a href="" style="margin-left:30px;" onClick=${(e) => this.showFilesClicked(e)}
>${tr(s.showFiles ? 'hide_files' : 'show_files')}</a
>
`
: ''}
`
: html`
<a href="/torrent/${encodeURIComponent(this.props.torrentId)}">${tr('show_files')}</a>
`}
${s.showFiles && t && t.files
? html`
<p>${tr('peers')}: ${t.numPeers}</p>
<div class="flex-table details">
${t.files.map(
(f) => html`
<div
onClick=${() => this.openFile(f, true)}
class="flex-row ${s.activeFilePath === f.path ? 'active' : ''}"
>
<div class="flex-cell">${f.name}</div>
<div class="flex-cell no-flex">${Helpers.formatBytes(f.length)}</div>
</div>
`,
)}
</div>
`)}
</div>
` : ''}
`
: ''}
`;
}
renderMeta() {
const s = this.state;
const title = s.splitPath && s.splitPath[s.splitPath.length - 1].split('.').slice(0, -1).join('.') || 'File sharing';
const title =
(s.splitPath && s.splitPath[s.splitPath.length - 1].split('.').slice(0, -1).join('.')) ||
'File sharing';
const ogTitle = `${title} | Iris`;
const description = 'Shared files';
const ogType = s.isAudioOpen ? 'music:song' : 'video.movie';
@ -285,12 +306,16 @@ class Torrent extends Component {
render() {
return html`
<div class="torrent">
${this.props.standalone ? this.renderMeta() : ''}
${!this.state.settings.enableWebtorrent && !this.state.settings.torrenting && !this.props.standalone ? html`
<a href="" onClick=${e => this.openTorrentClicked(e)}>${tr('show_attachment')}</a>
`: this.renderLoadingTorrent()}
</div>
<div class="torrent">
${this.props.standalone ? this.renderMeta() : ''}
${!this.state.settings.enableWebtorrent &&
!this.state.settings.torrenting &&
!this.props.standalone
? html`
<a href="" onClick=${(e) => this.openTorrentClicked(e)}>${tr('show_attachment')}</a>
`
: this.renderLoadingTorrent()}
</div>
`;
}
}

View File

@ -1,11 +1,12 @@
import { Component } from 'preact';
import { html } from 'htm/preact';
import { route } from 'preact-router';
import $ from 'jquery';
import {translate as t} from '../translations/Translation';
import iris from 'iris-lib';
import $ from 'jquery';
import noop from 'lodash/noop';
import { Component } from 'preact';
import { route } from 'preact-router';
import { translate as t } from '../translations/Translation';
import Button from './basic/Button';
const ringSound = new Audio('../../assets/audio/ring.mp3');
@ -19,7 +20,9 @@ let userMediaStream;
let ourIceCandidates;
let localStorageIce = localStorage.getItem('rtcConfig');
let DEFAULT_RTC_CONFIG = {iceServers: [ { urls: ["stun:turn.hepic.tel"] }, { urls: ["stun:stun.l.google.com:19302"] } ]};
let DEFAULT_RTC_CONFIG = {
iceServers: [{ urls: ['stun:turn.hepic.tel'] }, { urls: ['stun:stun.l.google.com:19302'] }],
};
let RTC_CONFIG = localStorageIce ? JSON.parse(localStorageIce) : DEFAULT_RTC_CONFIG;
function getRTCConfig() {
@ -43,27 +46,36 @@ class VideoCall extends Component {
/*iris.local().get('call').open(call => {
this.onCallMessage(call.pub, call.call);
});*/
iris.local().get('incomingCall').on(incomingCall => {
if (!incomingCall) {
clearTimeout(callTimeout);
ringSound.pause();
ringSound.currentTime = 0;
incomingCallNotification && incomingCallNotification.close();
} else {
if (this.state.activeCall) return;
ringSound.play().catch(() => {});
this.notifyIfNotVisible(incomingCall, t('incoming_call'));
}
this.setState({incomingCall});
});
iris.local().get('activeCall').on(activeCall => {
this.setState({activeCall})
this.stopCalling();
});
iris.local().get('outgoingCall').on(outgoingCall => {
outgoingCall && this.onCallUser(outgoingCall);
this.setState({outgoingCall});
});
iris
.local()
.get('incomingCall')
.on((incomingCall) => {
if (!incomingCall) {
clearTimeout(callTimeout);
ringSound.pause();
ringSound.currentTime = 0;
incomingCallNotification && incomingCallNotification.close();
} else {
if (this.state.activeCall) return;
ringSound.play().catch(noop);
this.notifyIfNotVisible(incomingCall, t('incoming_call'));
}
this.setState({ incomingCall });
});
iris
.local()
.get('activeCall')
.on((activeCall) => {
this.setState({ activeCall });
this.stopCalling();
});
iris
.local()
.get('outgoingCall')
.on((outgoingCall) => {
outgoingCall && this.onCallUser(outgoingCall);
this.setState({ outgoingCall });
});
}
async answerCall(pub) {
@ -81,7 +93,7 @@ class VideoCall extends Component {
return;
}
if (call.offer) {
if (iris.private(pub).rejectedTime && (new Date() - iris.private(pub).rejectedTime < 5000)) {
if (iris.private(pub).rejectedTime && new Date() - iris.private(pub).rejectedTime < 5000) {
this.rejectCall(pub);
return;
}
@ -95,15 +107,15 @@ class VideoCall extends Component {
}
notifyIfNotVisible(pub, text) {
if (document.visibilityState !== 'visible') {
if (document.visibilityState !== 'visible') {
incomingCallNotification = new Notification(iris.private(pub).name, {
icon: '/assets/img/icon128.png',
body: text,
requireInteraction: true,
silent: true
silent: true,
});
incomingCallNotification.onclick = function() {
route(`/chat/${ pub}`);
incomingCallNotification.onclick = function () {
route(`/chat/${pub}`);
window.focus();
};
}
@ -128,22 +140,22 @@ class VideoCall extends Component {
}
if (iris.private(pub)) {
iris.private(pub).pc && iris.private(pub).pc.close();
iris.private(pub).pc = null;
iris.private(pub).pc = null;
}
}
async addStreamToPeerConnection(pc) {
let constraints = {
audio: true,
video: true
video: true,
};
userMediaStream = await navigator.mediaDevices.getUserMedia(constraints);
userMediaStream.getTracks().forEach(track => {
userMediaStream.getTracks().forEach((track) => {
pc.addTrack(track, userMediaStream);
});
const localVideo = $('#localvideo');
localVideo[0].srcObject = userMediaStream;
localVideo[0].onloadedmetadata = function() {
localVideo[0].onloadedmetadata = function () {
localVideo[0].muted = true;
localVideo[0].play();
};
@ -155,15 +167,18 @@ class VideoCall extends Component {
}
async onCallUser(pub, video = true) {
if (this.state.outgoingCall) { return; }
if (this.state.outgoingCall) {
return;
}
await this.initConnection(true, pub);
console.log('calling', pub);
let call = () => iris.private(pub).put('call', {
time: new Date().toISOString(),
type: video ? 'video' : 'voice',
offer: true,
});
let call = () =>
iris.private(pub).put('call', {
time: new Date().toISOString(),
type: video ? 'video' : 'voice',
offer: true,
});
callingInterval = setInterval(call, 1000);
call();
callSound.addEventListener('ended', () => this.timeoutPlayCallSound());
@ -181,7 +196,7 @@ class VideoCall extends Component {
}
stopUserMedia() {
userMediaStream.getTracks().forEach(track => track.stop());
userMediaStream.getTracks().forEach((track) => track.stop());
}
stopCalling() {
@ -219,12 +234,14 @@ class VideoCall extends Component {
await this.addStreamToPeerConnection(chat.pc);
async function createOfferFn() {
try {
if (chat.isNegotiating) { return; }
if (chat.isNegotiating) {
return;
}
chat.isNegotiating = true;
let offer = await chat.pc.createOffer();
chat.pc.setLocalDescription(offer);
console.log('sending our sdp', offer);
chat.put('sdp', {time: new Date().toISOString(), data: offer});
chat.put('sdp', { time: new Date().toISOString(), data: offer });
} finally {
chat.isNegotiating = false;
}
@ -232,59 +249,76 @@ class VideoCall extends Component {
if (createOffer) {
await createOfferFn();
}
chat.onTheir('sdp', async sdp => {
chat.onTheir('sdp', async (sdp) => {
console.log('got their sdp', sdp);
if (!chat.pc) { console.log(1); return; }
if (createOffer && chat.pc.signalingState === 'stable') { console.log(2); return; }
if (sdp.data && sdp.time && new Date(sdp.time).getTime() < (new Date() - 5000)) { console.log(3); return; }
if (!chat.pc) {
console.log(1);
return;
}
if (createOffer && chat.pc.signalingState === 'stable') {
console.log(2);
return;
}
if (sdp.data && sdp.time && new Date(sdp.time).getTime() < new Date() - 5000) {
console.log(3);
return;
}
chat.pc.setRemoteDescription(new RTCSessionDescription(sdp.data));
console.log(4);
});
chat.onTheir('icecandidates', c => {
chat.onTheir('icecandidates', (c) => {
console.log('got their icecandidates', c);
if (!chat.pc || chat.pc.signalingState === 'closed') { return; }
if (c.data && c.time && new Date(c.time).getTime() < (new Date() - 5000)) { return; }
Object.keys(c.data).forEach(k => {
if (!chat.pc || chat.pc.signalingState === 'closed') {
return;
}
if (c.data && c.time && new Date(c.time).getTime() < new Date() - 5000) {
return;
}
Object.keys(c.data).forEach((k) => {
if (theirIceCandidateKeys.indexOf(k) === -1) {
theirIceCandidateKeys.push(k);
chat.pc.addIceCandidate(new RTCIceCandidate(c.data[k])).then(console.log, console.error);
}
});
});
chat.pc.onicecandidate = chat.pc.onicecandidate || (({candidate}) => {
if (!candidate) return;
console.log('sending our ice candidate');
let i = btoa(Math.random().toString()).slice(0, 12);
ourIceCandidates[i] = candidate;
chat.put('icecandidates', {time: new Date().toISOString(), data: ourIceCandidates});
});
chat.pc.onicecandidate =
chat.pc.onicecandidate ||
(({ candidate }) => {
if (!candidate) return;
console.log('sending our ice candidate');
let i = btoa(Math.random().toString()).slice(0, 12);
ourIceCandidates[i] = candidate;
chat.put('icecandidates', {
time: new Date().toISOString(),
data: ourIceCandidates,
});
});
if (createOffer) {
chat.pc.onnegotiationneeded = async () => {
createOfferFn();
};
}
chat.pc.onsignalingstatechange = async () => {
if (!chat.pc) { return; }
console.log(
`Signaling State Change:${ chat.pc}`,
chat.pc.signalingState
);
if (!chat.pc) {
return;
}
console.log(`Signaling State Change:${chat.pc}`, chat.pc.signalingState);
switch (chat.pc.signalingState) {
case "have-remote-offer": {
case 'have-remote-offer': {
const answer = await chat.pc.createAnswer({
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
offerToReceiveVideo: 1,
});
chat.pc.setLocalDescription(answer);
chat.put('sdp', {time: new Date().toISOString(), data: answer});
chat.put('sdp', { time: new Date().toISOString(), data: answer });
break;
}
case "stable":
case 'stable':
this.stopCalling();
console.log('call answered by', pub);
iris.local().get('activeCall').put(pub);
break;
case "closed":
case 'closed':
console.log("Signalling state is 'closed'");
this.callClosed(pub);
break;
@ -293,22 +327,22 @@ class VideoCall extends Component {
chat.pc.onconnectionstatechange = () => {
console.log('iceConnectionState changed', chat.pc.iceConnectionState);
switch (chat.pc.iceConnectionState) {
case "connected":
case 'connected':
break;
case "disconnected":
case 'disconnected':
this.callClosed(pub);
break;
case "new":
case 'new':
//this.callClosed(pub);
break;
case "failed":
case 'failed':
this.callClosed(pub);
break;
case "closed":
case 'closed':
this.callClosed(pub);
break;
default:
console.log("Change of state", chat.pc.iceConnectionState);
console.log('Change of state', chat.pc.iceConnectionState);
break;
}
};
@ -317,7 +351,7 @@ class VideoCall extends Component {
const remoteVideo = $('#remotevideo');
if (remoteVideo[0].srcObject !== event.streams[0]) {
remoteVideo[0].srcObject = event.streams[0];
remoteVideo[0].onloadedmetadata = function() {
remoteVideo[0].onloadedmetadata = function () {
console.log('metadata loaded');
remoteVideo[0].play();
};
@ -327,39 +361,74 @@ class VideoCall extends Component {
}
render() {
const resizeButton = html`<span style="cursor:pointer;margin-left:15px;font-size:2em;position:absolute;left:0;top:0" onClick=${() => this.setState({maximized: !this.state.maximized})}>${this.state.maximized ? '-' : '+'}</span>`;
const resizeButton = html`<span
style="cursor:pointer;margin-left:15px;font-size:2em;position:absolute;left:0;top:0"
onClick=${() => this.setState({ maximized: !this.state.maximized })}
>${this.state.maximized ? '-' : '+'}</span
>`;
const width = this.state.maximized ? '100%' : '400px';
const height = this.state.maximized ? '100%' : '400px';
const bottom = this.state.maximized ? '0' : '70px';
const localVideo = html`<video id="localvideo" autoplay="true" playsinline="true" style="width:50%;max-height:60%" />`;
const remoteVideo = html`<video id="remotevideo" autoplay="true" playsinline="true" style="width:50%;max-height:60%" />`;
const localVideo = html`<video
id="localvideo"
autoplay="true"
playsinline="true"
style="width:50%;max-height:60%"
/>`;
const remoteVideo = html`<video
id="remotevideo"
autoplay="true"
playsinline="true"
style="width:50%;max-height:60%"
/>`;
if (this.state.activeCall) {
return html`
<div id="active-call" style="position: fixed; right:0; bottom: ${bottom}; height:${height}; width: ${width}; max-width: 100%; text-align: center; background: #000; color: #fff; padding: 15px 0">
return html` <div
id="active-call"
style="position: fixed; right:0; bottom: ${bottom}; height:${height}; width: ${width}; max-width: 100%; text-align: center; background: #000; color: #fff; padding: 15px 0"
>
<div style="margin-bottom:5px;position:relative;height:50px;">
${resizeButton}
${t('on_call_with')} ${iris.private(this.state.activeCall) && iris.private(this.state.activeCall).name}
${resizeButton} ${t('on_call_with')}
${iris.private(this.state.activeCall) && iris.private(this.state.activeCall).name}
</div>
${localVideo}
${remoteVideo}
<${Button} style="display:block;margin:15px auto" onClick=${() => this.endCall(this.state.activeCall)}>End call<//>
${localVideo} ${remoteVideo}
<${Button}
style="display:block;margin:15px auto"
onClick=${() => this.endCall(this.state.activeCall)}
>End call<//
>
</div>`;
} else if (this.state.outgoingCall) {
return html`<div id="outgoing-call" style="position:fixed; right:0; bottom: ${bottom}; height:${height}; width: ${width}; text-align: center; background: #000; color: #fff; padding: 15px">
return html`<div
id="outgoing-call"
style="position:fixed; right:0; bottom: ${bottom}; height:${height}; width: ${width}; text-align: center; background: #000; color: #fff; padding: 15px"
>
${t('calling')} ${iris.private(this.state.outgoingCall).name}
<${Button} onClick=${() => this.cancelCall(this.state.outgoingCall)} style="display:block; margin: 15px auto">
<${Button}
onClick=${() => this.cancelCall(this.state.outgoingCall)}
style="display:block; margin: 15px auto"
>
${t('cancel')}
<//>
${localVideo}
${remoteVideo}
${localVideo} ${remoteVideo}
</div>`;
} else if (this.state.incomingCall) {
return html`
<div id="incoming-call" style="position:fixed; right:0; bottom: ${bottom}; height:${height}; width: ${width}; text-align: center; background: #000; color: #fff; padding: 15px 0">
<div
id="incoming-call"
style="position:fixed; right:0; bottom: ${bottom}; height:${height}; width: ${width}; text-align: center; background: #000; color: #fff; padding: 15px 0"
>
Incoming call from ${iris.private(this.state.incomingCall).name}
<${Button} style="display:block; margin: 15px auto" onClick=${() => this.answerCall(this.state.incomingCall)}>${t('answer')}<//>
<${Button} style="display:block; margin: 15px auto" onClick=${() => this.rejectCall(this.state.incomingCall)}>${t('reject')}<//>
<${Button}
style="display:block; margin: 15px auto"
onClick=${() => this.answerCall(this.state.incomingCall)}
>${t('answer')}<//
>
<${Button}
style="display:block; margin: 15px auto"
onClick=${() => this.rejectCall(this.state.incomingCall)}
>${t('reject')}<//
>
</div>
`;
}
@ -367,9 +436,4 @@ class VideoCall extends Component {
}
export default VideoCall;
export {
RTC_CONFIG,
DEFAULT_RTC_CONFIG,
setRTCConfig,
getRTCConfig
};
export { RTC_CONFIG, DEFAULT_RTC_CONFIG, setRTCConfig, getRTCConfig };

View File

@ -1,4 +1,4 @@
import styled from "styled-components";
import styled from 'styled-components';
export default styled.button`
background: var(--button-bg);
@ -6,9 +6,11 @@ export default styled.button`
border: var(--button-border);
cursor: pointer;
transition: all 0.25s ease;
&:hover, &:focus, &:active {
&:hover,
&:focus,
&:active {
background: var(--button-bg-hover);
color: var(--button-color-hover);
}
`;
`;

View File

@ -20,7 +20,7 @@ class ScrollWindow {
updateSubscriptions() {
this.unsubscribe();
const subscribe = params => {
const subscribe = (params) => {
// this.node.get({ '.': params}).map().on((val, key, a, eve) => { // TODO: broken in gun?
this.node.map().on((val, key, a, eve) => {
if (params['-']) {
@ -34,7 +34,7 @@ class ScrollWindow {
if (this.center) {
subscribe({ '>': this.center, '<': '\uffff' });
subscribe({'<': this.center, '>' : '', '-': true});
subscribe({ '<': this.center, '>': '', '-': true });
} else {
subscribe({ '<': '\uffff', '>': '', '-': this.opts.stickTo === 'top' });
}
@ -48,7 +48,7 @@ class ScrollWindow {
_upOrDown(n, up) {
this.opts.stickTo = null;
const keys = this._getSortedKeys();
n = n || (keys.length / 2);
n = n || keys.length / 2;
n = up ? n : -n;
const half = Math.floor(keys.length / 2);
const newMiddleIndex = Math.max(Math.min(half + n, keys.length - 1), 0);
@ -86,7 +86,7 @@ class ScrollWindow {
const add = () => {
this.elements.set(key, val);
this.sortedKeys = [...this.elements.keys()].sort();
const sortedElements = this.sortedKeys.map(k => this.elements.get(k));
const sortedElements = this.sortedKeys.map((k) => this.elements.get(k));
this.opts.onChange && this.opts.onChange(sortedElements, this.elements);
};
const keys = this._getSortedKeys();
@ -100,7 +100,7 @@ class ScrollWindow {
this.elements.delete(keys[keys.length - 1]);
add();
} else if (this.center) {
if (keys.indexOf(this.center) < (keys.length / 2)) {
if (keys.indexOf(this.center) < keys.length / 2) {
if (key < keys[keys.length - 1]) {
this.elements.delete(keys[keys.length - 1]);
add();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1519
src/js/lib/pica.min.js vendored

File diff suppressed because one or more lines are too long

1002
src/js/lib/qrcode.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,68 +1,121 @@
(function(){
function Store(opt){
(function () {
function Store(opt) {
opt = opt || {};
opt.file = String(opt.file || 'radata');
var db = null, u;
var db = null,
u;
try{opt.indexedDB = opt.indexedDB || indexedDB}catch(e){}
try{if(!opt.indexedDB || 'file:' == location.protocol){
var store = {}, s = {};
store.put = function(f, d, cb){ s[f] = d; cb(null, 1) };
store.get = function(f, cb){ cb(null, s[f] || u) };
console.log('Warning: No indexedDB exists to persist data to!');
return store;
}}catch(e){}
var store = function Store(){};
if(Store[opt.file]){
console.log("Warning: reusing same IndexedDB store and options as 1st.");
try {
opt.indexedDB = opt.indexedDB || indexedDB;
} catch (e) {}
try {
if (!opt.indexedDB || 'file:' == location.protocol) {
var store = {},
s = {};
store.put = function (f, d, cb) {
s[f] = d;
cb(null, 1);
};
store.get = function (f, cb) {
cb(null, s[f] || u);
};
console.log('Warning: No indexedDB exists to persist data to!');
return store;
}
} catch (e) {}
var store = function Store() {};
if (Store[opt.file]) {
console.log('Warning: reusing same IndexedDB store and options as 1st.');
return Store[opt.file];
}
Store[opt.file] = store;
store.start = function(){
store.start = function () {
var o = indexedDB.open(opt.file, 1);
o.onupgradeneeded = function(eve){ (eve.target.result).createObjectStore(opt.file) }
o.onsuccess = function(){ db = o.result }
o.onerror = function(eve){ console.log(eve||1); }
}; store.start();
o.onupgradeneeded = function (eve) {
eve.target.result.createObjectStore(opt.file);
};
o.onsuccess = function () {
db = o.result;
};
o.onerror = function (eve) {
console.log(eve || 1);
};
};
store.start();
store.put = function(key, data, cb){
if(!db){ setTimeout(function(){ store.put(key, data, cb) },1); return }
store.put = function (key, data, cb) {
if (!db) {
setTimeout(function () {
store.put(key, data, cb);
}, 1);
return;
}
var tx = db.transaction([opt.file], 'readwrite');
var obj = tx.objectStore(opt.file);
var req = obj.put(data, ''+key);
req.onsuccess = obj.onsuccess = tx.onsuccess = function(){ cb(null, 1) }
req.onabort = obj.onabort = tx.onabort = function(eve){ cb(eve||'put.tx.abort') }
req.onerror = obj.onerror = tx.onerror = function(eve){ cb(eve||'put.tx.error') }
}
var req = obj.put(data, '' + key);
req.onsuccess =
obj.onsuccess =
tx.onsuccess =
function () {
cb(null, 1);
};
req.onabort =
obj.onabort =
tx.onabort =
function (eve) {
cb(eve || 'put.tx.abort');
};
req.onerror =
obj.onerror =
tx.onerror =
function (eve) {
cb(eve || 'put.tx.error');
};
};
store.get = function(key, cb){
if(!db){ setTimeout(function(){ store.get(key, cb) },9); return }
store.get = function (key, cb) {
if (!db) {
setTimeout(function () {
store.get(key, cb);
}, 9);
return;
}
var tx = db.transaction([opt.file], 'readonly');
var obj = tx.objectStore(opt.file);
var req = obj.get(''+key);
req.onsuccess = function(){ cb(null, req.result) }
req.onabort = function(eve){ cb(eve||4) }
req.onerror = function(eve){ cb(eve||5) }
}
setInterval(function(){ db && db.close(); db = null; store.start() }, 1000 * 15); // reset webkit bug?
var req = obj.get('' + key);
req.onsuccess = function () {
cb(null, req.result);
};
req.onabort = function (eve) {
cb(eve || 4);
};
req.onerror = function (eve) {
cb(eve || 5);
};
};
setInterval(function () {
db && db.close();
db = null;
store.start();
}, 1000 * 15); // reset webkit bug?
return store;
}
if(typeof window !== "undefined"){
if (typeof window !== 'undefined') {
(Store.window = window).RindexedDB = Store;
} else {
try{ module.exports = Store }catch(e){}
try {
module.exports = Store;
} catch (e) {}
}
try{
try {
var Gun = Store.window.Gun || require('./gun');
Gun.on('create', function(root){
Gun.on('create', function (root) {
this.to.next(root);
root.opt.store = root.opt.store || Store(root.opt);
});
}catch(e){}
}());
} catch (e) {}
})();

View File

@ -1,106 +1,157 @@
(function(){
var Gun = (typeof window !== "undefined")? window.Gun : require('../gun');
(function () {
var Gun = typeof window !== 'undefined' ? window.Gun : require('../gun');
Gun.on('opt', function(root){
this.to.next(root);
var opt = root.opt;
if(root.once){ return }
if(!Gun.Mesh){ return }
if(false === opt.RTCPeerConnection){ return }
Gun.on('opt', function (root) {
this.to.next(root);
var opt = root.opt;
if (root.once) {
return;
}
if (!Gun.Mesh) {
return;
}
if (false === opt.RTCPeerConnection) {
return;
}
var env;
if(typeof window !== "undefined"){ env = window }
if(typeof global !== "undefined"){ env = global }
env = env || {};
var env;
if (typeof window !== 'undefined') {
env = window;
}
if (typeof global !== 'undefined') {
env = global;
}
env = env || {};
var rtcpc = opt.RTCPeerConnection || env.RTCPeerConnection || env.webkitRTCPeerConnection || env.mozRTCPeerConnection;
var rtcsd = opt.RTCSessionDescription || env.RTCSessionDescription || env.webkitRTCSessionDescription || env.mozRTCSessionDescription;
var rtcic = opt.RTCIceCandidate || env.RTCIceCandidate || env.webkitRTCIceCandidate || env.mozRTCIceCandidate;
if(!rtcpc || !rtcsd || !rtcic){ return }
opt.RTCPeerConnection = rtcpc;
opt.RTCSessionDescription = rtcsd;
opt.RTCIceCandidate = rtcic;
opt.rtc = opt.rtc || {'iceServers': [
{urls: 'stun:stun.l.google.com:19302'},
{urls: "stun:stun.sipgate.net:3478"}/*,
var rtcpc =
opt.RTCPeerConnection ||
env.RTCPeerConnection ||
env.webkitRTCPeerConnection ||
env.mozRTCPeerConnection;
var rtcsd =
opt.RTCSessionDescription ||
env.RTCSessionDescription ||
env.webkitRTCSessionDescription ||
env.mozRTCSessionDescription;
var rtcic =
opt.RTCIceCandidate ||
env.RTCIceCandidate ||
env.webkitRTCIceCandidate ||
env.mozRTCIceCandidate;
if (!rtcpc || !rtcsd || !rtcic) {
return;
}
opt.RTCPeerConnection = rtcpc;
opt.RTCSessionDescription = rtcsd;
opt.RTCIceCandidate = rtcic;
opt.rtc = opt.rtc || {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun.sipgate.net:3478' } /*,
{urls: "stun:stun.stunprotocol.org"},
{urls: "stun:stun.sipgate.net:10000"},
{urls: "stun:217.10.68.152:10000"},
{urls: 'stun:stun.services.mozilla.com'}*/
]};
// TODO: Select the most appropriate stuns.
// FIXME: Find the wire throwing ICE Failed
// The above change corrects at least firefox RTC Peer handler where it **throws** on over 6 ice servers, and updates url: to urls: removing deprecation warning
opt.rtc.dataChannel = opt.rtc.dataChannel || {ordered: false, maxRetransmits: 2};
opt.rtc.sdp = opt.rtc.sdp || {mandatory: {OfferToReceiveAudio: false, OfferToReceiveVideo: false}};
opt.announce = function(to){
root.on('out', {rtc: {id: opt.pid, to:to}}); // announce ourself
{urls: 'stun:stun.services.mozilla.com'}*/,
],
};
var mesh = opt.mesh = opt.mesh || Gun.Mesh(root);
root.on('create', function(at){
this.to.next(at);
setTimeout(opt.announce, 1);
});
root.on('in', function(msg){
if(msg.rtc){ open(msg) }
this.to.next(msg);
});
// TODO: Select the most appropriate stuns.
// FIXME: Find the wire throwing ICE Failed
// The above change corrects at least firefox RTC Peer handler where it **throws** on over 6 ice servers, and updates url: to urls: removing deprecation warning
opt.rtc.dataChannel = opt.rtc.dataChannel || { ordered: false, maxRetransmits: 2 };
opt.rtc.sdp = opt.rtc.sdp || {
mandatory: { OfferToReceiveAudio: false, OfferToReceiveVideo: false },
};
opt.announce = function (to) {
root.on('out', { rtc: { id: opt.pid, to: to } }); // announce ourself
};
var mesh = (opt.mesh = opt.mesh || Gun.Mesh(root));
root.on('create', function (at) {
this.to.next(at);
setTimeout(opt.announce, 1);
});
root.on('in', function (msg) {
if (msg.rtc) {
open(msg);
}
this.to.next(msg);
});
function open(msg){
var rtc = msg.rtc, peer, tmp;
if(!rtc || !rtc.id){ return }
delete opt.announce[rtc.id]; /// remove after connect
if(tmp = rtc.answer){
if(!(peer = opt.peers[rtc.id] || open[rtc.id]) || peer.remoteSet){ return }
return peer.setRemoteDescription(peer.remoteSet = new opt.RTCSessionDescription(tmp));
}
if(tmp = rtc.candidate){
peer = opt.peers[rtc.id] || open[rtc.id] || open({rtc: {id: rtc.id}});
return peer.addIceCandidate(new opt.RTCIceCandidate(tmp));
}
//if(opt.peers[rtc.id]){ return }
if(open[rtc.id]){ return }
(peer = new opt.RTCPeerConnection(opt.rtc)).id = rtc.id;
var wire = peer.wire = peer.createDataChannel('dc', opt.rtc.dataChannel);
open[rtc.id] = peer;
wire.onclose = function(){
delete open[rtc.id];
mesh.bye(peer);
//reconnect(peer);
};
wire.onerror = function(err){};
wire.onopen = function(e){
//delete open[rtc.id];
mesh.hi(peer);
}
wire.onmessage = function(msg){
if(!msg){ return }
mesh.hear(msg.data || msg, peer);
};
peer.onicecandidate = function(e){ // source: EasyRTC!
if(!e.candidate){ return }
root.on('out', {'@': msg['#'], rtc: {candidate: e.candidate, id: opt.pid}});
}
peer.ondatachannel = function(e){
var rc = e.channel;
rc.onmessage = wire.onmessage;
rc.onopen = wire.onopen;
rc.onclose = wire.onclose;
}
if(tmp = rtc.offer){
peer.setRemoteDescription(new opt.RTCSessionDescription(tmp));
peer.createAnswer(function(answer){
peer.setLocalDescription(answer);
root.on('out', {'@': msg['#'], rtc: {answer: answer, id: opt.pid}});
}, function(){}, opt.rtc.sdp);
return;
}
peer.createOffer(function(offer){
peer.setLocalDescription(offer);
root.on('out', {'@': msg['#'], rtc: {offer: offer, id: opt.pid}});
}, function(){}, opt.rtc.sdp);
return peer;
}
});
var noop = function(){};
}());
function open(msg) {
var rtc = msg.rtc,
peer,
tmp;
if (!rtc || !rtc.id) {
return;
}
delete opt.announce[rtc.id]; /// remove after connect
if ((tmp = rtc.answer)) {
if (!(peer = opt.peers[rtc.id] || open[rtc.id]) || peer.remoteSet) {
return;
}
return peer.setRemoteDescription((peer.remoteSet = new opt.RTCSessionDescription(tmp)));
}
if ((tmp = rtc.candidate)) {
peer = opt.peers[rtc.id] || open[rtc.id] || open({ rtc: { id: rtc.id } });
return peer.addIceCandidate(new opt.RTCIceCandidate(tmp));
}
//if(opt.peers[rtc.id]){ return }
if (open[rtc.id]) {
return;
}
(peer = new opt.RTCPeerConnection(opt.rtc)).id = rtc.id;
var wire = (peer.wire = peer.createDataChannel('dc', opt.rtc.dataChannel));
open[rtc.id] = peer;
wire.onclose = function () {
delete open[rtc.id];
mesh.bye(peer);
//reconnect(peer);
};
wire.onerror = function (err) {};
wire.onopen = function (e) {
//delete open[rtc.id];
mesh.hi(peer);
};
wire.onmessage = function (msg) {
if (!msg) {
return;
}
mesh.hear(msg.data || msg, peer);
};
peer.onicecandidate = function (e) {
// source: EasyRTC!
if (!e.candidate) {
return;
}
root.on('out', { '@': msg['#'], rtc: { candidate: e.candidate, id: opt.pid } });
};
peer.ondatachannel = function (e) {
var rc = e.channel;
rc.onmessage = wire.onmessage;
rc.onopen = wire.onopen;
rc.onclose = wire.onclose;
};
if ((tmp = rtc.offer)) {
peer.setRemoteDescription(new opt.RTCSessionDescription(tmp));
peer.createAnswer(
function (answer) {
peer.setLocalDescription(answer);
root.on('out', { '@': msg['#'], rtc: { answer: answer, id: opt.pid } });
},
function () {},
opt.rtc.sdp,
);
return;
}
peer.createOffer(
function (offer) {
peer.setLocalDescription(offer);
root.on('out', { '@': msg['#'], rtc: { offer: offer, id: opt.pid } });
},
function () {},
opt.rtc.sdp,
);
return peer;
}
});
var noop = function () {};
})();

View File

@ -1,17 +1,17 @@
import english from './en.mjs';
const AVAILABLE_LANGUAGES = {
"en": "English",
"es": "Español",
"fr": "Français",
"de": "Deutsch",
"it": "Italiano",
"ru": "Русский",
"pt-BR": "Português (Brasil)",
"fi": "Suomi",
"ur": "اردو",
"zh-cn": "简体中文",
"ko": "한국어",
en: 'English',
es: 'Español',
fr: 'Français',
de: 'Deutsch',
it: 'Italiano',
ru: 'Русский',
'pt-BR': 'Português (Brasil)',
fi: 'Suomi',
ur: 'اردو',
'zh-cn': '简体中文',
ko: '한국어',
};
let AVAILABLE_LANGUAGE_KEYS = Object.keys(AVAILABLE_LANGUAGES);
@ -24,17 +24,17 @@ let translationLoaded;
if (typeof module !== 'undefined') {
language = localStorage.getItem('language') || navigator.language || 'en';
if (AVAILABLE_LANGUAGE_KEYS.indexOf(language) === -1) {
const s = language.slice(0,2);
const s = language.slice(0, 2);
language = 'en';
for (let i = 0; i < AVAILABLE_LANGUAGE_KEYS.length; i++) {
if (AVAILABLE_LANGUAGE_KEYS[i].slice(0,2) === s) {
if (AVAILABLE_LANGUAGE_KEYS[i].slice(0, 2) === s) {
language = AVAILABLE_LANGUAGE_KEYS[i];
break;
}
}
}
translationLoaded = import(`./${language}.mjs`).then(module => {
translationLoaded = import(`./${language}.mjs`).then((module) => {
translation = module.default;
if (language !== 'en') {
translation = { ...english, ...translation };
@ -48,7 +48,9 @@ function capitalize(s) {
}
function translate(k, linkProps) {
return k && (translation[k] || capitalize(k.replace(/_/g, ' '))).replace('<a', `<a ${linkProps||''}`);
return (
k && (translation[k] || capitalize(k.replace(/_/g, ' '))).replace('<a', `<a ${linkProps || ''}`)
);
}
export {translate, translationLoaded, AVAILABLE_LANGUAGES, AVAILABLE_LANGUAGE_KEYS, language};
export { translate, translationLoaded, AVAILABLE_LANGUAGES, AVAILABLE_LANGUAGE_KEYS, language };

View File

@ -1,4 +1,9 @@
import {translate, translationLoaded, AVAILABLE_LANGUAGES, AVAILABLE_LANGUAGE_KEYS, language} from './Translation.mjs';
export {translate, translationLoaded, AVAILABLE_LANGUAGES, AVAILABLE_LANGUAGE_KEYS, language};
import {
AVAILABLE_LANGUAGE_KEYS,
AVAILABLE_LANGUAGES,
language,
translate,
translationLoaded,
} from './Translation.mjs';
export { translate, translationLoaded, AVAILABLE_LANGUAGES, AVAILABLE_LANGUAGE_KEYS, language };

View File

@ -1,105 +1,115 @@
export default {
whats_your_name: "Was ist Ihr Name?",
new_user_go: "Auf Gehts",
already_have_an_account: "Haben sie schon ein Konto eingerichtet?",
back: "zurück",
scan_private_key_qr_code: "Scan einen privaten Schlüssel QR code",
paste_private_key: "Füge einen privaten Schlüssel ein",
get_notified_new_messages: "Werde über neue Nachrichten notifiziert",
turn_on_desktop_notifications: "Stelle Desktop Notifikationen ein",
new_chat: "Neue Unterhaltung",
if_other_person_doesnt_see_message: "Sollte die andere Person die Nachricht nicht sehen, können Sie <b>Ihren</b> Unterhaltung's link über einen anderen Kanal senden:",
type_a_message: "Schreiben sie eine Nachricht",
beware_of_sharing_invite_link_publicly: "Vorsicht wenn sie ihren Einladungslink Öffentlich teilen: Sie können mit Nachricht Anfragen gespammt werden. Teilen sie lieber ihren <a>Profil link</a>.",
whats_your_name: 'Was ist Ihr Name?',
new_user_go: 'Auf Gehts',
already_have_an_account: 'Haben sie schon ein Konto eingerichtet?',
back: 'zurück',
scan_private_key_qr_code: 'Scan einen privaten Schlüssel QR code',
paste_private_key: 'Füge einen privaten Schlüssel ein',
get_notified_new_messages: 'Werde über neue Nachrichten notifiziert',
turn_on_desktop_notifications: 'Stelle Desktop Notifikationen ein',
new_chat: 'Neue Unterhaltung',
if_other_person_doesnt_see_message:
"Sollte die andere Person die Nachricht nicht sehen, können Sie <b>Ihren</b> Unterhaltung's link über einen anderen Kanal senden:",
type_a_message: 'Schreiben sie eine Nachricht',
beware_of_sharing_invite_link_publicly:
'Vorsicht wenn sie ihren Einladungslink Öffentlich teilen: Sie können mit Nachricht Anfragen gespammt werden. Teilen sie lieber ihren <a>Profil link</a>.',
your_invite_links: "Ihre Unterhaltung's links",
create_new_invite_link: "Erstellen Sie einen neuen Einladungslink",
copy_your_invite_link: "Kopieren Sie ihren Einladungslink",
have_someones_invite_link: "Haben Sie einen Einladungslink von jemandem?",
paste_their_invite_link: "Fügen Sie den Einladungslink hier ein",
give_your_invite_link: "Teilen Sie ihren Einladungslink:",
or_scan_qr_code: "Oder scannen Sie den QR code",
or_show_qr_code: "Oder zeigen Sie ihren QR code",
new_group: "Neue Gruppe",
group_name: "Gruppen Name",
create: "Erstellen",
settings: "Einstellungen",
contacts: "Kontakte",
market: "Handelsplatz",
messages: "Nachrichten",
media: "Medien",
home: "Start",
explorer: "Explorer",
profile: "Profile",
your_name: "Ihr Name",
profile_photo: "Profil Bild",
add_profile_photo: "Profil Bild hinzufügen",
profile_photo_too_big: "Profil Bild zu gross: maximale Grösse ist 200KB",
cancel: "Abbrechen",
use_photo: "Bild benutzen",
remove_photo: "Bild entfernen",
create_new_invite_link: 'Erstellen Sie einen neuen Einladungslink',
copy_your_invite_link: 'Kopieren Sie ihren Einladungslink',
have_someones_invite_link: 'Haben Sie einen Einladungslink von jemandem?',
paste_their_invite_link: 'Fügen Sie den Einladungslink hier ein',
give_your_invite_link: 'Teilen Sie ihren Einladungslink:',
or_scan_qr_code: 'Oder scannen Sie den QR code',
or_show_qr_code: 'Oder zeigen Sie ihren QR code',
new_group: 'Neue Gruppe',
group_name: 'Gruppen Name',
create: 'Erstellen',
settings: 'Einstellungen',
contacts: 'Kontakte',
market: 'Handelsplatz',
messages: 'Nachrichten',
media: 'Medien',
home: 'Start',
explorer: 'Explorer',
profile: 'Profile',
your_name: 'Ihr Name',
profile_photo: 'Profil Bild',
add_profile_photo: 'Profil Bild hinzufügen',
profile_photo_too_big: 'Profil Bild zu gross: maximale Grösse ist 200KB',
cancel: 'Abbrechen',
use_photo: 'Bild benutzen',
remove_photo: 'Bild entfernen',
about_text: "'Über uns' Text",
account: "Konto",
save_backup_of_privkey_first: "Speichern Sie ein Backup für Ihren privaten Schlüssel!",
otherwise_cant_log_in_again: "Sonst können Sie nicht mehr in Ihr Konto einloggen.",
log_out: "Ausloggen",
private_key: "Privater Schlüssel",
download_private_key: "Download Privaten Schlüssel",
private_key_warning: "<b>Warnung!</b> Der private Schlüssel ist benötigt um <b>in Ihr Konto einzuloggen</b>. Geben Sie niemals Ihren privaten Schlüssel an Andere weiter!",
copy_private_key: "Kopieren Sie den privaten Schlüssel",
show_privkey_qr: "Zeigen Sie den privaten Schlüssel QR code",
hide_privkey_qr: "Blended Sie den privaten Schlüssel QR code aus",
privkey_storage_recommendation: "Der sicherste Platz für Ihren privaten Schlüssel ist ein <b>Passwor Manager</b>.",
language: "Sprache",
peers: "Peers",
peer_url: "Peer url",
public: "Öffentlich",
remove: "Entfernen",
enable: "Einstellen",
disable: "Ausstellen",
from: "Von",
add: "Hinzufügen",
public_peer_info: "<i>Öffentliche</i> peers sind automatisch auffindbar für Leute in Ihren Unterhaltungen (und ausserhalb).",
peers_info: "Peers sind GunDB nodes die Sie einfach <a>hochfahren</a> können. Kommend: Direkte Verbindung zu Freunden über WebRTC.",
webrtc_connection_options: "WebRTC Verbindungsoptionen",
webrtc_info: "WebRTC ist für Videoanrufe benutzt. Wenn Sie hinter einem NAT sind, müssen Sie wahrscheinlich einen TURN server hier definieren, der dann Ihr Video Verkehr weiterleitet. Bandwidth ist nicht kostenfrei, darum gibt es nicht viele kostenfreie TURN servers.",
restore_defaults: "Einstellungen zurückstellen",
about: "Über uns",
application_security_warning: "Diese App ist ein ungeprüftes 'proof-of-concept' Implementation, also nicht für Sicherheit nötige Dinge benutzten.",
donate: "Spenden",
donate_info: "<b>Spenden</b> helfen dem Projekt weiter zu gehen und werden dankbar angenommen. Sie können via <a>Open Collective</a> oder <b>bitcoin</b> spenden.",
logout_confirmation_info: "Sie <b>können nicht wieder einloggen</b>, ausser Sie haben saved eine Kopie ihres privaten Schlüssels gespeichert.",
participants: "Teilnehmer",
admin: "admin",
add_participant: "Teilnehmer hinzufügen",
account: 'Konto',
save_backup_of_privkey_first: 'Speichern Sie ein Backup für Ihren privaten Schlüssel!',
otherwise_cant_log_in_again: 'Sonst können Sie nicht mehr in Ihr Konto einloggen.',
log_out: 'Ausloggen',
private_key: 'Privater Schlüssel',
download_private_key: 'Download Privaten Schlüssel',
private_key_warning:
'<b>Warnung!</b> Der private Schlüssel ist benötigt um <b>in Ihr Konto einzuloggen</b>. Geben Sie niemals Ihren privaten Schlüssel an Andere weiter!',
copy_private_key: 'Kopieren Sie den privaten Schlüssel',
show_privkey_qr: 'Zeigen Sie den privaten Schlüssel QR code',
hide_privkey_qr: 'Blended Sie den privaten Schlüssel QR code aus',
privkey_storage_recommendation:
'Der sicherste Platz für Ihren privaten Schlüssel ist ein <b>Passwor Manager</b>.',
language: 'Sprache',
peers: 'Peers',
peer_url: 'Peer url',
public: 'Öffentlich',
remove: 'Entfernen',
enable: 'Einstellen',
disable: 'Ausstellen',
from: 'Von',
add: 'Hinzufügen',
public_peer_info:
'<i>Öffentliche</i> peers sind automatisch auffindbar für Leute in Ihren Unterhaltungen (und ausserhalb).',
peers_info:
'Peers sind GunDB nodes die Sie einfach <a>hochfahren</a> können. Kommend: Direkte Verbindung zu Freunden über WebRTC.',
webrtc_connection_options: 'WebRTC Verbindungsoptionen',
webrtc_info:
'WebRTC ist für Videoanrufe benutzt. Wenn Sie hinter einem NAT sind, müssen Sie wahrscheinlich einen TURN server hier definieren, der dann Ihr Video Verkehr weiterleitet. Bandwidth ist nicht kostenfrei, darum gibt es nicht viele kostenfreie TURN servers.',
restore_defaults: 'Einstellungen zurückstellen',
about: 'Über uns',
application_security_warning:
"Diese App ist ein ungeprüftes 'proof-of-concept' Implementation, also nicht für Sicherheit nötige Dinge benutzten.",
donate: 'Spenden',
donate_info:
'<b>Spenden</b> helfen dem Projekt weiter zu gehen und werden dankbar angenommen. Sie können via <a>Open Collective</a> oder <b>bitcoin</b> spenden.',
logout_confirmation_info:
'Sie <b>können nicht wieder einloggen</b>, ausser Sie haben saved eine Kopie ihres privaten Schlüssels gespeichert.',
participants: 'Teilnehmer',
admin: 'admin',
add_participant: 'Teilnehmer hinzufügen',
new_participants_profile_link: "Neuer Teilnehmer's Unterhaltung's link",
add_friend: "Freund hinzufügen",
send_message: "Nachricht senden",
copy_link: "Link kopieren",
chat_settings: "Unterhaltunseinstellungen",
nicknames: "Nicknames",
nickname: "Nickname",
video_call: "Video Anruf",
online: "online",
last_active: "Letztmals aktiv",
their_nickname_for_you: "Ihr Nickname für die Anderen",
notifications: "Notifikationen",
all_messages: "Alle Nachrichten",
mentions_only: "Nur Erwähnungen",
nothing: "Nichts",
delete_chat: "Unterhaltung löschen",
block_user: "Benutzer blockieren",
typing: "Schreibt...",
attachment: "Anhang",
note_to_self: "Notiz zu Selbst",
add_friend: 'Freund hinzufügen',
send_message: 'Nachricht senden',
copy_link: 'Link kopieren',
chat_settings: 'Unterhaltunseinstellungen',
nicknames: 'Nicknames',
nickname: 'Nickname',
video_call: 'Video Anruf',
online: 'online',
last_active: 'Letztmals aktiv',
their_nickname_for_you: 'Ihr Nickname für die Anderen',
notifications: 'Notifikationen',
all_messages: 'Alle Nachrichten',
mentions_only: 'Nur Erwähnungen',
nothing: 'Nichts',
delete_chat: 'Unterhaltung löschen',
block_user: 'Benutzer blockieren',
typing: 'Schreibt...',
attachment: 'Anhang',
note_to_self: 'Notiz zu Selbst',
today: 'heute',
yesterday: 'gestern',
copied: "Kopiert",
answer: "antworten",
reject: "ablehnen",
incoming_call: "Einkommender Anruf",
call_rejected: "Anruf abgelehnt",
close: "Schliessen",
call_ended: "Anruf beendet",
calling: "Rufe an",
on_call_with: "Anruf mit",
copied: 'Kopiert',
answer: 'antworten',
reject: 'ablehnen',
incoming_call: 'Einkommender Anruf',
call_rejected: 'Anruf abgelehnt',
close: 'Schliessen',
call_ended: 'Anruf beendet',
calling: 'Rufe an',
on_call_with: 'Anruf mit',
};

View File

@ -1,177 +1,199 @@
export default {
whats_your_name: "What's your name?",
new_user_go: "Go",
already_have_an_account: "Already have an account?",
back: "Back",
scan_private_key_qr_code: "Scan private key QR code",
paste_private_key: "Paste a private key",
get_notified_new_messages: "Get notified of new messages",
turn_on_desktop_notifications: "Turn on desktop notifications",
new_chat: "New chat",
public_messages: "Public messages",
follow_someone_info: "Follow someone to see content from their network! Suggestion:",
creator_of_this_distribution: "Creator of this Iris distribution",
no_followers_yet: "Share your profile link so others can follow you:",
no_followers_yet_info: "Your posts, replies and likes are only shown to your followers and their network.",
alternatively: "Alternatively,",
alternatively_get_sms_verified: "Alternatively, get <a>SMS verified</a> so others can find you.",
give_your_profile_link_to_someone: "give your profile link to someone",
if_other_person_doesnt_see_message: "If the other person doesn't see your message, you can give them <b>your</b> invite link through some other channel:",
type_a_message: "Type a message",
beware_of_sharing_invite_link_publicly: "Beware of sharing your invite link publicly: you might get spammed with message requests. Publicly share your <a>profile link</a> instead.",
your_invite_links: "Your Invite Links",
create_new_invite_link: "Create new invite link",
copy_your_invite_link: "Copy your invite link",
have_someones_invite_link: "Have someone's invite link?",
paste_their_invite_link: "Paste their invite link",
give_your_invite_link: "Give your invite link:",
or_scan_qr_code: "Or scan their QR code",
or_show_qr_code: "Or show your QR code",
new_group: "New group",
group_name: "Group name",
create: "Create",
settings: "Settings",
profile: "Profile",
your_name: "Your name",
profile_photo: "Profile photo",
add_profile_photo: "Add profile photo",
profile_photo_too_big: "Profile photo too big: maximum size is 200KB",
cancel: "Cancel",
use_photo: "Use photo",
remove_photo: "Remove photo",
about_text: "About text",
account: "Account",
save_backup_of_privkey_first: "Save a backup of your private key first!",
otherwise_cant_log_in_again: "Otherwise you can't log in back to this account.",
log_out: "Log out",
private_key: "Private key",
download: "Download",
download_private_key: "Download private key",
private_key_warning: "<b>DANGER!</b> Private key <b>gives access to your account</b>. Don't give or show your private key to anyone else!",
copy_private_key: "Copy private key",
show_privkey_qr: "Show private key QR code",
hide_privkey_qr: "Hide private key QR code",
privkey_storage_recommendation: "The safest place to store your private key is a <b>password manager</b>.",
language: "Language",
peers: "Peers",
peer_url: "Peer url",
public: "Public",
remove: "Remove",
enable: "Enable",
disable: "Disable",
from: "from", // source of the peer url, from whom we got it
add: "Add",
public_peer_info: "<i>Public</i> peers are automatically discoverable by people you chat with (and others).",
peers_info: "Peers are GunDB nodes that you can easily <a>spin up</a>. Upcoming: direct connection with friends over WebRTC.",
webrtc_connection_options: "WebRTC connection options",
webrtc_info: "WebRTC is used for video calls. If you're behind a NAT, you might need to specify a TURN server here, which will relay your video traffic.",
restore_defaults: "Restore defaults",
about: "About", // About Iris
application_security_warning: "The application is an unaudited proof-of-concept implementation, so don't use it for security critical purposes.",
donate: "Donate",
donate_info: "<b>Donations</b> help keep the project going and are very much appreciated. You can donate via <a>Open Collective</a> or <b>bitcoin</b>",
logout_confirmation_info: "You <b>cannot log in again</b> unless you have saved a copy of your private key.",
participants: "Participants",
admin: "admin",
add_participant: "Add participant",
new_participants_profile_link: "New participant's profile link",
invite_links: "Invite links",
copy: "Copy",
follows_you: "Follows you",
follow: "Follow",
unfollow: "Unfollow",
following: "Following",
followers: "Followers",
add_friend: "Add friend",
send_message: "Send message",
copy_link: "Copy link",
show_qr_code: "Show QR code",
chat_settings: "Chat Settings",
nicknames: "Nicknames",
nickname: "Nickname",
video_call: "Video call",
online: "online",
last_active: "last active",
their_nickname_for_you: "Their nickname for you",
notifications: "Notifications",
all_messages: "All messages",
mentions_only: "Mentions only",
nothing: "Nothing",
delete_chat: "Delete chat",
block_user: "Block user",
typing: "Typing...",
attachment: "attachment",
note_to_self: "Note to Self",
today: 'today',
yesterday: 'yesterday',
copied: "Copied",
answer: "answer",
reject: "reject",
incoming_call: "Incoming call",
call_rejected: "Call rejected",
close: "Close",
call_ended: "Call ended",
calling: "Calling",
on_call_with: "On call with",
delete: 'Delete',
about: 'About',
about_text: 'About text',
account: 'Account',
// source of the peer url, from whom we got it
add: 'Add',
add_friend: 'Add friend',
add_hashtag: '+ Add hashtag',
add_item: 'Add item',
add_participant: 'Add participant',
add_profile_photo: 'Add profile photo',
add_to_cart: 'Add to cart',
address: 'Address',
admin: 'admin',
all: 'All',
all_messages: 'All messages',
already_have_an_account: 'Already have an account?',
also: 'Also',
alternatively: 'Alternatively,',
alternatively_get_sms_verified: 'Alternatively, get <a>SMS verified</a> so others can find you.',
answer: 'answer',
// About Iris
application_security_warning:
"The application is an unaudited proof-of-concept implementation, so don't use it for security critical purposes.",
attachment: 'attachment',
automatically_load_webtorrent_attachments: 'Automatically load webtorrent attachments',
autoplay_webtorrent_videos: 'Autoplay webtorrent videos',
back: 'Back',
beware_of_sharing_invite_link_publicly:
'Beware of sharing your invite link publicly: you might get spammed with message requests. Publicly share your <a>profile link</a> instead.',
block_user: 'Block user',
bluetooth_support_upcoming: 'Bluetooth support upcoming',
call_ended: 'Call ended',
call_rejected: 'Call rejected',
calling: 'Calling',
cancel: 'Cancel',
chat_settings: 'Chat Settings',
close: 'Close',
communicate_and_synchronize:
'Communicate and synchronize with local network peers without Internet access',
confirm_delete_msg: 'Delete message?',
search: 'Search',
confirmation: 'Confirmation',
connected_peers: 'Connected peers',
contacts: 'Contacts',
copied: 'Copied',
copy: 'Copy',
copy_link: 'Copy link',
copy_private_key: 'Copy private key',
copy_your_invite_link: 'Copy your invite link',
create: 'Create',
create_new_invite_link: 'Create new invite link',
creator_of_this_distribution: 'Creator of this Iris distribution',
delete: 'Delete',
delete_chat: 'Delete chat',
delete_item: 'Delete item',
delivery: 'Delivery',
direct_connect_to_the:
'Direct-connect to the people you have an open chat with (if port 8767 open or upnp enabled in router)',
disable: 'Disable',
donate: 'Donate',
donate_info:
'<b>Donations</b> help keep the project going and are very much appreciated. You can donate via <a>Open Collective</a> or <b>bitcoin</b>',
download: 'Download',
download_private_key: 'Download private key',
download_webtorrent:
'Download <a>Webtorrent Desktop</a> to host your media files and paste their magnet links below.',
email: 'Email',
email_optional: 'Email (optional)',
email_privkey_to_yourself: 'Email the private key to yourself',
email: "Email",
retype_email: "Retype email",
email_optional: "Email (optional)",
delivery: "Delivery",
address: "Address",
confirmation: "Confirmation",
payment_method: "Payment method",
summary: "Summary",
download_webtorrent: "Download <a>Webtorrent Desktop</a> to host your media files and paste their magnet links below.",
visibility: "Your posts, replies and likes are only shown to your followers and their network.",
iris_is_like: "Iris is like the social networking apps we're used to, but better.",
this_is_a_prototype_store: "This is a prototype store that shows items from merchants in your social network. Orders are sent via Iris private message. Your own store can be found <a>here</a>.",
add_to_cart: "Add to cart",
web_push_subscriptions: "Web push subscriptions",
enable_public_peer_discovery: "Enable public peer discovery",
set_up_your_own_peer: "Set up your own peer",
also: "Also",
or_small: "or",
automatically_load_webtorrent_attachments: "Automatically load webtorrent attachments",
autoplay_webtorrent_videos: "Autoplay webtorrent videos",
home: "Home",
media: "Media",
messages: "Messages",
market: "Market",
contacts: "Contacts",
explorer: "Explorer",
iris_is_accessible: "<b>Accessible.</b> No phone number or signup required. Just type in your name or alias and go!",
iris_is_secure: "<b>Secure.</b> It's open source. You can verify that your data stays safe.",
iris_is_always_available: "<b>Always available.</b> It works offline-first and is not dependent on any single centrally managed server. Users can even connect directly to each other.",
enable: 'Enable',
enable_public_peer_discovery: 'Enable public peer discovery',
everyone: 'Everyone',
explorer: 'Explorer',
follow: 'Follow',
follow_someone_info: 'Follow someone to see content from their network! Suggestion:',
followers: 'Followers',
following: 'Following',
follows: 'Follows',
follows_you: 'Follows you',
from: 'from',
get_notified_new_messages: 'Get notified of new messages',
get_the_desktop_application: 'Get the desktop application',
give_your_invite_link: 'Give your invite link:',
give_your_profile_link_to_someone: 'give your profile link to someone',
go: 'Go',
group_name: 'Group name',
have_someones_invite_link: "Have someone's invite link?",
hide_privkey_qr: 'Hide private key QR code',
home: 'Home',
if_other_person_doesnt_see_message:
"If the other person doesn't see your message, you can give them <b>your</b> invite link through some other channel:",
in_other_words: "In other words, you can't be deplatformed from Iris.",
get_the_desktop_application: "Get the desktop application",
communicate_and_synchronize: "Communicate and synchronize with local network peers without Internet access",
when_local_peers: "When local peers eventually connect to the Internet, your messages are relayed globally",
bluetooth_support_upcoming: "Bluetooth support upcoming",
opens_to_background: "Opens to background on login: stay online and get message notifications",
more_secure_and_available: "More secure and available: no need to open the browser application from a server",
direct_connect_to_the: "Direct-connect to the people you have an open chat with (if port 8767 open or upnp enabled in router)",
the_application_is_unaudited: "The application is an unaudited proof-of-concept implementation, so don't use it for security critical purposes.",
add_hashtag: "+ Add hashtag",
all: "All",
follows: "Follows",
everyone: "Everyone",
connected_peers: "Connected peers",
no_notifications_yet: "No notifications yet",
popular_hashtags: "Popular hashtags",
go: "Go",
none: "None",
no_items_to_show: "No items to show",
store_description: "Store description",
add_item: "Add item",
name: "Name",
store: "Store",
item_id: "Item ID",
item_description: "Item description",
price: "Price",
delete_item: "Delete item",
no_contacts_in_list: "No contacts in list",
incoming_call: 'Incoming call',
invite_links: 'Invite links',
iris_is_accessible:
'<b>Accessible.</b> No phone number or signup required. Just type in your name or alias and go!',
iris_is_always_available:
'<b>Always available.</b> It works offline-first and is not dependent on any single centrally managed server. Users can even connect directly to each other.',
iris_is_like: "Iris is like the social networking apps we're used to, but better.",
iris_is_secure: "<b>Secure.</b> It's open source. You can verify that your data stays safe.",
item_description: 'Item description',
item_id: 'Item ID',
language: 'Language',
last_active: 'last active',
log_out: 'Log out',
logout_confirmation_info:
'You <b>cannot log in again</b> unless you have saved a copy of your private key.',
market: 'Market',
media: 'Media',
mentions_only: 'Mentions only',
messages: 'Messages',
more_secure_and_available:
'More secure and available: no need to open the browser application from a server',
name: 'Name',
new_chat: 'New chat',
new_group: 'New group',
new_participants_profile_link: "New participant's profile link",
new_user_go: 'Go',
nickname: 'Nickname',
nicknames: 'Nicknames',
no_contacts_in_list: 'No contacts in list',
no_followers_yet: 'Share your profile link so others can follow you:',
no_followers_yet_info:
'Your posts, replies and likes are only shown to your followers and their network.',
no_items_to_show: 'No items to show',
no_notifications_yet: 'No notifications yet',
none: 'None',
note_to_self: 'Note to Self',
nothing: 'Nothing',
notifications: 'Notifications',
on_call_with: 'On call with',
online: 'online',
opens_to_background: 'Opens to background on login: stay online and get message notifications',
or_scan_qr_code: 'Or scan their QR code',
or_show_qr_code: 'Or show your QR code',
or_small: 'or',
otherwise_cant_log_in_again: "Otherwise you can't log in back to this account.",
participants: 'Participants',
paste_private_key: 'Paste a private key',
paste_their_invite_link: 'Paste their invite link',
payment_method: 'Payment method',
peer_url: 'Peer url',
peers: 'Peers',
peers_info:
'Peers are GunDB nodes that you can easily <a>spin up</a>. Upcoming: direct connection with friends over WebRTC.',
popular_hashtags: 'Popular hashtags',
price: 'Price',
private_key: 'Private key',
private_key_warning:
"<b>DANGER!</b> Private key <b>gives access to your account</b>. Don't give or show your private key to anyone else!",
privkey_storage_recommendation:
'The safest place to store your private key is a <b>password manager</b>.',
profile: 'Profile',
profile_photo: 'Profile photo',
profile_photo_too_big: 'Profile photo too big: maximum size is 200KB',
public: 'Public',
public_messages: 'Public messages',
public_peer_info:
'<i>Public</i> peers are automatically discoverable by people you chat with (and others).',
reject: 'reject',
remove: 'Remove',
remove_photo: 'Remove photo',
restore_defaults: 'Restore defaults',
retype_email: 'Retype email',
save_backup_of_privkey_first: 'Save a backup of your private key first!',
scan_private_key_qr_code: 'Scan private key QR code',
search: 'Search',
send_message: 'Send message',
set_up_your_own_peer: 'Set up your own peer',
settings: 'Settings',
show_privkey_qr: 'Show private key QR code',
show_qr_code: 'Show QR code',
store: 'Store',
store_description: 'Store description',
summary: 'Summary',
the_application_is_unaudited:
"The application is an unaudited proof-of-concept implementation, so don't use it for security critical purposes.",
their_nickname_for_you: 'Their nickname for you',
this_is_a_prototype_store:
'This is a prototype store that shows items from merchants in your social network. Orders are sent via Iris private message. Your own store can be found <a>here</a>.',
today: 'today',
turn_on_desktop_notifications: 'Turn on desktop notifications',
type_a_message: 'Type a message',
typing: 'Typing...',
unfollow: 'Unfollow',
use_photo: 'Use photo',
video_call: 'Video call',
visibility: 'Your posts, replies and likes are only shown to your followers and their network.',
web_push_subscriptions: 'Web push subscriptions',
webrtc_connection_options: 'WebRTC connection options',
webrtc_info:
"WebRTC is used for video calls. If you're behind a NAT, you might need to specify a TURN server here, which will relay your video traffic.",
whats_your_name: "What's your name?",
when_local_peers:
'When local peers eventually connect to the Internet, your messages are relayed globally',
yesterday: 'yesterday',
your_invite_links: 'Your Invite Links',
your_name: 'Your name',
};

View File

@ -1,100 +1,145 @@
export default {
whats_your_name: "¿Cuál es tu nombre?",
new_user_go: "Ir",
already_have_an_account: "¿Ya tienes una cuenta?",
back: "volver",
scan_private_key_qr_code: "Escanee el código QR de clave privada",
paste_private_key: "Pegar una clave privada",
get_notified_new_messages: "Recibe notificaciones de nuevos mensajes",
turn_on_desktop_notifications: "Activa las notificaciones de escritorio",
new_chat: "Nueva conversación",
if_other_person_doesnt_see_message: "Si la otra persona no ve su mensaje, puede darle <b> su </b> enlace de chat a través de otro canal:",
type_a_message: "Escribe un mensaje",
beware_of_sharing_invite_link_publicly: "Tenga cuidado de compartir su enlace de chat públicamente: puede recibir spam con solicitudes de mensajes. Comparta públicamente su <a> enlace de perfil </a> en su lugar.",
your_invite_links: "Tus enlaces de chat",
create_new_invite_link: "Crear nuevo enlace de chat",
copy_your_invite_link: "Copia tu enlace de chat",
have_someones_invite_link: "¿Tienes el enlace de chat de alguien?",
paste_their_invite_link: "Pegue su enlace de chat",
give_your_invite_link: "Pasa tu enlace de chat:",
or_scan_qr_code: "O escanee su código QR",
or_show_qr_code: "O muestra tu código QR",
new_group: "Nuevo grupo",
group_name: "Nombre del grupo",
create: "Crear",
settings: "Configuraciones",
profile: "Perfil",
your_name: "Tu nombre",
profile_photo: "Foto de perfil",
add_profile_photo: "Añadir foto de perfil",
profile_photo_too_big: "Foto de perfil demasiado grande: el tamaño máximo es de 200 KB",
cancel: "Cancelar",
use_photo: "Usar foto",
remove_photo: "Eliminar foto",
about_text: "Sobre el texto",
account: "Cuenta",
save_backup_of_privkey_first: "¡tenga una copia de seguridad de su clave privada primero!",
otherwise_cant_log_in_again: "De lo contrario, no puede volver a iniciar sesión en esta cuenta.",
log_out: "Cerrar sesión",
private_key: "Llave privada",
download_private_key: "Descargar clave privada",
private_key_warning: "<b> ¡PELIGRO! </b> La clave privada se utiliza para <b> iniciar sesión en su cuenta </b>. ¡No le dé ni muestre su clave privada a nadie más!",
copy_private_key: "Copiar clave privada",
show_privkey_qr: "Mostrar código QR de clave privada",
hide_privkey_qr: "Ocultar código QR de clave privada",
privkey_storage_recommendation: "El lugar más seguro para almacenar su clave privada es un <b> administrador de contraseñas </b>.",
language: "Idioma",
peers: "Pares",
peer_url: "Par url",
public: "Público",
remove: "Remove",
enable: "Eliminar",
disable: "Inhabilitar",
from: "desde",
add: "Añadir",
public_peer_info: "Las personas con las que chatea (y otras) pueden detectar automáticamente a los pares <i> públicos </i>.",
peers_info: "Los pares son nodos GunDB que puede <a> girar fácilmente </a>. Próximamente: conexión directa con amigos a través de WebRTC.",
webrtc_connection_options: "Opciones de conexión de WebRTC",
webrtc_info: "WebRTC se utiliza para videollamadas. Si está detrás de un NAT, es posible que deba especificar un servidor TURN aquí, que retransmitirá su tráfico de video. El ancho de banda no es gratuito, por lo que no hay servidores TURN gratuitos disponibles.",
restore_defaults: "Restaurar los valores predeterminados",
about: "Acerca de",
application_security_warning: "La aplicación es una implementación de prueba de concepto no auditada, así que no la use con fines críticos de seguridad.",
donate: "Donar",
donate_info: "<b> Donaciones </b> ayudan a mantener el proyecto en marcha y son muy apreciados. Puede donar a través de <a> Open Collective </a> o <b> bitcoin </b>",
logout_confirmation_info: "<b> no puede iniciar sesión de nuevo </b> a menos que haya guardado una copia de su clave privada.",
participants: "Participantes",
admin: "administración",
add_participant: "Añada participante",
new_participants_profile_link: "Enlace de chat del nuevo participante",
add_friend: "Añadir amigo",
send_message: "Enviar mensaje",
copy_link: "Copiar link",
chat_settings: "Configuraciones de chat",
nicknames: "Apodos",
nickname: "Apodo",
video_call: "Videollamada",
online: "en línea",
last_active: "Último Activo",
their_nickname_for_you: "Su apodo para ti",
notifications: "Notificaciones",
all_messages: "Todos los mensajes",
mentions_only: "Solo menciones",
nothing: "Nada",
delete_chat: "Eliminar chat",
block_user: "Bloquear usuario",
typing: "Escribiendo...",
attachment: "adjunto archivo",
note_to_self: "Nota personal",
today: 'hoy',
yesterday: 'ayer',
copied: "Copiado",
answer: "responder",
reject: "rechazar",
incoming_call: "Llamada entrante",
call_rejected: "Llamada rechazada",
close: "Cerrar",
call_ended: "Llamada finalizada",
calling: "Llamando",
on_call_with: "En comunicación con",
about: 'Acerca de',
about_text: 'Sobre el texto',
account: 'Cuenta',
add: 'Añadir',
add_contact_or_create_group: 'Agrega un contacto o crea un grupo',
add_friend: 'Añadir amigo',
add_new_contact_or_group: 'Agrega un nuevo contacto o grupo',
add_participant: 'Añada participante',
add_profile_photo: 'Añadir foto de perfil',
admin: 'administración',
all_messages: 'Todos los mensajes',
already_have_an_account: '¿Ya tienes una cuenta?',
alternatively_get_sms_verified:
'Alternativamente, puede <a>verificar su cuenta por SMS</a> para que otros puedan encontrarlo.',
answer: 'responder',
application_security_warning:
'La aplicación es una implementación de prueba de concepto no auditada, así que no la use con fines críticos de seguridad.',
attachment: 'adjunto archivo',
autoplay_webtorrent_videos: 'Reproducir webtorrent videos automáticamente',
back: 'Volver',
connect_Ethereum_account: 'Conectar cuenta de Ethereum',
beware_of_sharing_invite_link_publicly:
'Tenga cuidado de compartir su enlace de chat públicamente: puede recibir spam con solicitudes de mensajes. Comparta públicamente su <a> enlace de perfil </a> en su lugar.',
block: 'Bloquear',
unfollow: 'Dejar de seguir',
block_user: 'Bloquear usuario',
blocked_users: 'Usuarios bloqueados',
call_ended: 'Llamada finalizada',
call_rejected: 'Llamada rechazada',
calling: 'Llamando',
cancel: 'Cancelar',
chat_settings: 'Configuraciones de chat',
close: 'Cerrar',
contacts: 'Contactos',
copied: 'Copiado',
copy: 'Copiar',
copy_link: 'Copiar link',
copy_private_key: 'Copiar clave privada',
copy_your_invite_link: 'Copia tu enlace de chat',
create: 'Crear',
create_new_invite_link: 'Crear nuevo enlace de chat',
delete_chat: 'Eliminar chat',
disable: 'Inhabilitar',
donate: 'Donar',
donate_info:
'<b> Donaciones </b> ayudan a mantener el proyecto en marcha y son muy apreciados. Puede donar a través de <a> Open Collective </a> o <b> bitcoin </b>',
download_private_key: 'Descargar clave privada',
email_privkey_to_yourself: 'Envíese la clave privada por correo electrónico a usted mismo',
enable: 'Eliminar',
enable_webtorrent: 'Habilitar webtorrent',
everyone: 'Todos',
follow: 'Seguir',
followers: 'Seguidores',
following: 'Seguidos',
follows: 'Seguido',
from: 'desde',
get_notified_new_messages: 'Recibe notificaciones de nuevos mensajes',
give_your_invite_link: 'Pasa tu enlace de chat:',
group_name: 'Nombre del grupo',
have_someones_invite_link: '¿Tienes el enlace de chat de alguien?',
hide_privkey_qr: 'Ocultar código QR de clave privada',
home: 'Inicio',
if_other_person_doesnt_see_message:
'Si la otra persona no ve su mensaje, puede darle <b> su </b> enlace de chat a través de otro canal:',
incoming_call: 'Llamada entrante',
invite_people_or_create_group: 'Invita a personas o crea un grupo',
language: 'Idioma',
last_active: 'Último Activo',
log_out: 'Cerrar sesión',
logout_confirmation_info:
'<b> no puede iniciar sesión de nuevo </b> a menos que haya guardado una copia de su clave privada.',
mentions_only: 'Solo menciones',
messages: 'Mensajes',
new_chat: 'Nueva conversación',
new_group: 'Nuevo grupo',
new_participants_profile_link: 'Enlace de chat del nuevo participante',
new_user_go: 'Ir',
nickname: 'Apodo',
nicknames: 'Apodos',
no_contacts_in_list: 'No tienes ningún contacto en tu lista.',
no_followers_yet: 'Comparte el link de tu perfil para que otros puedan seguirte:',
no_followers_yet_info:
'Tus publicaciones, respuestas y Me gusta solo se muestran a tus seguidores y su red.',
none: 'No se han encontrado datos.',
note_to_self: 'Nota personal',
nothing: 'Nada',
notifications: 'Notificaciones',
on_call_with: 'En comunicación con',
online: 'en línea',
or_scan_qr_code: 'O escanee su código QR',
or_show_qr_code: 'O muestra tu código QR',
otherwise_cant_log_in_again: 'De lo contrario, no puede volver a iniciar sesión en esta cuenta.',
participants: 'Participantes',
paste_private_key: 'Pegar una clave privada',
paste_their_invite_link: 'Pegue su enlace de chat',
peer_url: 'URL del par',
peers: 'Pares',
peers_info:
'Los pares son nodos GunDB que puede <a> girar fácilmente </a>. Próximamente: conexión directa con amigos a través de WebRTC.',
private_key: 'Llave privada',
private_key_warning:
'<b> ¡PELIGRO! </b> La clave privada se utiliza para <b> iniciar sesión en su cuenta </b>. ¡No le dé ni muestre su clave privada a nadie más!',
privkey_storage_recommendation:
'El lugar más seguro para almacenar su clave privada es un <b> administrador de contraseñas </b>.',
profile: 'Perfil',
show_more: 'Ver mas...',
show_less: 'Ver menos...',
profile_photo: 'Foto de perfil',
profile_photo_too_big: 'Foto de perfil demasiado grande: el tamaño máximo es de 200 KB',
public: 'Público',
public_peer_info:
'Las personas con las que chatea (y otras) pueden detectar automáticamente a los pares <i> públicos </i>.',
reject: 'rechazar',
remove: 'Eliminar',
posts: 'Posteos',
replies: 'Repuestas',
likes: 'Me gusta',
remove_photo: 'Eliminar foto',
restore_defaults: 'Restaurar los valores predeterminados',
retype_email: 'Reingrese su email',
save_backup_of_privkey_first: '¡tenga una copia de seguridad de su clave privada primero!',
scan_private_key_qr_code: 'Escanee el código QR de clave privada',
search: 'Buscar',
send_message: 'Enviar mensaje',
settings: 'Configuraciones',
show_beta_features: 'Ver funcionalidades en beta',
show_privkey_qr: 'Mostrar código QR de clave privada',
their_nickname_for_you: 'Su apodo para ti',
today: 'hoy',
turn_on_desktop_notifications: 'Activa las notificaciones de escritorio',
type_a_message: 'Escribe un mensaje',
typing: 'Escribiendo...',
use_photo: 'Usar foto',
video_call: 'Videollamada',
webrtc_connection_options: 'Opciones de conexión de WebRTC',
webrtc_info:
'WebRTC se utiliza para videollamadas. Si está detrás de un NAT, es posible que deba especificar un servidor TURN aquí, que retransmitirá su tráfico de video. El ancho de banda no es gratuito, por lo que no hay servidores TURN gratuitos disponibles.',
whats_your_name: '¿Cuál es tu nombre?',
yesterday: 'ayer',
your_invite_links: 'Tus enlaces de chat',
your_name: 'Tu nombre',
show_qr_code: 'Ver código QR',
send_email: 'Enviar email',
};

View File

@ -1,184 +1,206 @@
export default {
whats_your_name: "Mikä on nimesi?",
new_user_go: "Aloita",
already_have_an_account: "Onko sinulla jo tili?",
back: "Takaisin",
download: "Lataa",
scan_private_key_qr_code: "Skannaa salaisen avaimen QR-koodi",
paste_private_key: "Liitä salainen avain",
get_notified_new_messages: "Tilaa ilmoitukset uusista viesteistä",
turn_on_desktop_notifications: "Ota käyttöön työpöytäilmoitukset",
new_chat: "Uusi keskustelu",
if_other_person_doesnt_see_message: "Jos toinen käyttäjä ei näe viestiäsi, voit antaa hänelle <b>sinun</b> kutsulinkkisi jotain muuta kautta:",
type_a_message: "Kirjoita viesti",
beware_of_sharing_invite_link_publicly: "Jos jaat kutsulinkkisi julkisesti: saatat saada viestipyyntö-spämmiä. Sen sijaan voit turvallisesti jakaa <a>profiililinkkisi</a> julkisesti.",
your_invite_links: "Kutsulinkkisi",
create_new_invite_link: "Luo uusi kutsulinkki",
copy_your_invite_link: "Kopioi kutsulinkkisi",
have_someones_invite_link: "Saitko toisen käyttäjän kutsulinkin?",
paste_their_invite_link: "Liitä kutsulinkki",
give_your_invite_link: "Anna oma kutsulinkkisi:",
or_scan_qr_code: "Tai skannaa toisen QR-koodi",
or_show_qr_code: "Tai näytä oma QR-koodisi",
new_group: "Uusi ryhmä",
group_name: "Ryhmän nimi",
create: "Luo",
settings: "Asetukset",
profile: "Profiili",
your_name: "Nimesi",
profile_photo: "Profiilikuva",
add_profile_photo: "Lisää profiilikuva",
profile_photo_too_big: "Profiilikuva on liian suuri: maksimikoko on 200KB",
cancel: "Peruuta",
use_photo: "Käytä kuvaa",
remove_photo: "Poista kuva",
about_text: "Kuvausteksti",
account: "Tili",
save_backup_of_privkey_first: "Tallenna ensin varmuuskopio salaisesta avaimestasi!",
otherwise_cant_log_in_again: "Muuten et voi kirjautua takaisin tälle tilille.",
log_out: "Kirjaudu ulos",
private_key: "Salainen avain",
download_private_key: "Lataa salainen avain tiedostona",
private_key_warning: "<b>VAROITUS!</b> Salainen avain antaa <b>pääsyn tilillesi</b>. Älä anna tai näytä salaista avaintasi kenellekään.",
copy_private_key: "Kopioi salainen avain",
show_privkey_qr: "Näytä salaisen avaimen QR-koodi",
hide_privkey_qr: "Piilota salaisen avaimen QR-koodi",
privkey_storage_recommendation: "Turvallisin säilytyspaikka salaiselle avaimellesi on <b>salasananhallintaohjelma (password manager)</b>.",
language: "Kieli",
peers: "Yhteyspisteet (peers)",
peer_url: "Peer url",
public: "Julkinen",
remove: "Poista",
enable: "Käytä",
disable: "Poista käytöstä",
from: "lähde",
add: "Lisää",
public_peer_info: "<b>Julkiset</b> yhteyspisteet näkyvät automaattisesti käyttäjille, joiden kanssa keskustelet (ja muille).",
peers_info: "Yhteyspisteet ovat GunDB-solmuja, joiden pystyttämisestä tietoa <a>täällä</a>. Tulossa: suorat yhteydet kavereiden kesken WebRTC:llä.",
webrtc_connection_options: "WebRTC-yhteysasetukset",
webrtc_info: "WebRTC:tä käytetään videopuheluihin. Jos olet NATin takana, sinun tarvitsee ehkä määrittää tähän TURN-palvelin, joka välittää videoliikenteesi. Tiedonsiirto ei ole ilmaista, joten ilmaisia TURN-palvelimia ei ole saatavilla.",
restore_defaults: "Palauta oletusarvot",
about: "Tietoa",
application_security_warning: "Sovellus on auditoimaton konseptitoteutus, joten älä käytä sitä tarkoituksiin joissa turvallisuus on oleellisen tärkeää.",
donate: "Lahjoita",
donate_info: "<b>Lahjoitukset</b> pitävät projektin pystyssä ja arvostamme niitä suuresti. Voit lahjoittaa <a>Open Collectiven</a> kautta tai <b>bitcoinilla</b>",
logout_confirmation_info: "<b>Et voi kirjautua takaisin sisään</b> ellet ole tallentanut varmuuskopiota salaisesta avaimestasi.",
participants: "osallistujat",
admin: "ylläpitäjä",
add_participant: "Lisää osallistuja",
new_participants_profile_link: "Uuden osallistujan kutsulinkki",
add_friend: "Lisää kaveriksi",
send_message: "Lähetä viesti",
copy_link: "Kopioi linkki",
chat_settings: "Keskustelun asetukset",
nicknames: "Lempinimet",
nickname: "Lempinimi",
video_call: "Videopuhelu",
online: "paikalla",
last_active: "viimeksi aktiivisena",
their_nickname_for_you: "Hänen lempinimensä sinulle",
notifications: "Ilmoitukset",
all_messages: "Kaikki viestit",
mentions_only: "Vain maininnat",
nothing: "Ei mitään",
delete_chat: "Poista keskustelu",
block_user: "Estä käyttäjä",
typing: "Kirjoittaa...",
attachment: "liite",
note_to_self: "Muistiinpanot",
whats_your_name: 'Mikä on nimesi?',
new_user_go: 'Aloita',
already_have_an_account: 'Onko sinulla jo tili?',
back: 'Takaisin',
download: 'Lataa',
scan_private_key_qr_code: 'Skannaa salaisen avaimen QR-koodi',
paste_private_key: 'Liitä salainen avain',
get_notified_new_messages: 'Tilaa ilmoitukset uusista viesteistä',
turn_on_desktop_notifications: 'Ota käyttöön työpöytäilmoitukset',
new_chat: 'Uusi keskustelu',
if_other_person_doesnt_see_message:
'Jos toinen käyttäjä ei näe viestiäsi, voit antaa hänelle <b>sinun</b> kutsulinkkisi jotain muuta kautta:',
type_a_message: 'Kirjoita viesti',
beware_of_sharing_invite_link_publicly:
'Jos jaat kutsulinkkisi julkisesti: saatat saada viestipyyntö-spämmiä. Sen sijaan voit turvallisesti jakaa <a>profiililinkkisi</a> julkisesti.',
your_invite_links: 'Kutsulinkkisi',
create_new_invite_link: 'Luo uusi kutsulinkki',
copy_your_invite_link: 'Kopioi kutsulinkkisi',
have_someones_invite_link: 'Saitko toisen käyttäjän kutsulinkin?',
paste_their_invite_link: 'Liitä kutsulinkki',
give_your_invite_link: 'Anna oma kutsulinkkisi:',
or_scan_qr_code: 'Tai skannaa toisen QR-koodi',
or_show_qr_code: 'Tai näytä oma QR-koodisi',
new_group: 'Uusi ryhmä',
group_name: 'Ryhmän nimi',
create: 'Luo',
settings: 'Asetukset',
profile: 'Profiili',
your_name: 'Nimesi',
profile_photo: 'Profiilikuva',
add_profile_photo: 'Lisää profiilikuva',
profile_photo_too_big: 'Profiilikuva on liian suuri: maksimikoko on 200KB',
cancel: 'Peruuta',
use_photo: 'Käytä kuvaa',
remove_photo: 'Poista kuva',
about_text: 'Kuvausteksti',
account: 'Tili',
save_backup_of_privkey_first: 'Tallenna ensin varmuuskopio salaisesta avaimestasi!',
otherwise_cant_log_in_again: 'Muuten et voi kirjautua takaisin tälle tilille.',
log_out: 'Kirjaudu ulos',
private_key: 'Salainen avain',
download_private_key: 'Lataa salainen avain tiedostona',
private_key_warning:
'<b>VAROITUS!</b> Salainen avain antaa <b>pääsyn tilillesi</b>. Älä anna tai näytä salaista avaintasi kenellekään.',
copy_private_key: 'Kopioi salainen avain',
show_privkey_qr: 'Näytä salaisen avaimen QR-koodi',
hide_privkey_qr: 'Piilota salaisen avaimen QR-koodi',
privkey_storage_recommendation:
'Turvallisin säilytyspaikka salaiselle avaimellesi on <b>salasananhallintaohjelma (password manager)</b>.',
language: 'Kieli',
peers: 'Yhteyspisteet (peers)',
peer_url: 'Peer url',
public: 'Julkinen',
remove: 'Poista',
enable: 'Käytä',
disable: 'Poista käytöstä',
from: 'lähde',
add: 'Lisää',
public_peer_info:
'<b>Julkiset</b> yhteyspisteet näkyvät automaattisesti käyttäjille, joiden kanssa keskustelet (ja muille).',
peers_info:
'Yhteyspisteet ovat GunDB-solmuja, joiden pystyttämisestä tietoa <a>täällä</a>. Tulossa: suorat yhteydet kavereiden kesken WebRTC:llä.',
webrtc_connection_options: 'WebRTC-yhteysasetukset',
webrtc_info:
'WebRTC:tä käytetään videopuheluihin. Jos olet NATin takana, sinun tarvitsee ehkä määrittää tähän TURN-palvelin, joka välittää videoliikenteesi. Tiedonsiirto ei ole ilmaista, joten ilmaisia TURN-palvelimia ei ole saatavilla.',
restore_defaults: 'Palauta oletusarvot',
about: 'Tietoa',
application_security_warning:
'Sovellus on auditoimaton konseptitoteutus, joten älä käytä sitä tarkoituksiin joissa turvallisuus on oleellisen tärkeää.',
donate: 'Lahjoita',
donate_info:
'<b>Lahjoitukset</b> pitävät projektin pystyssä ja arvostamme niitä suuresti. Voit lahjoittaa <a>Open Collectiven</a> kautta tai <b>bitcoinilla</b>',
logout_confirmation_info:
'<b>Et voi kirjautua takaisin sisään</b> ellet ole tallentanut varmuuskopiota salaisesta avaimestasi.',
participants: 'osallistujat',
admin: 'ylläpitäjä',
add_participant: 'Lisää osallistuja',
new_participants_profile_link: 'Uuden osallistujan kutsulinkki',
add_friend: 'Lisää kaveriksi',
send_message: 'Lähetä viesti',
copy_link: 'Kopioi linkki',
chat_settings: 'Keskustelun asetukset',
nicknames: 'Lempinimet',
nickname: 'Lempinimi',
video_call: 'Videopuhelu',
online: 'paikalla',
last_active: 'viimeksi aktiivisena',
their_nickname_for_you: 'Hänen lempinimensä sinulle',
notifications: 'Ilmoitukset',
all_messages: 'Kaikki viestit',
mentions_only: 'Vain maininnat',
nothing: 'Ei mitään',
delete_chat: 'Poista keskustelu',
block_user: 'Estä käyttäjä',
typing: 'Kirjoittaa...',
attachment: 'liite',
note_to_self: 'Muistiinpanot',
today: 'tänään',
yesterday: 'eilen',
copied: "Kopioitu",
answer: "vastaa",
reject: "hylkää",
incoming_call: "Saapuva puhelu",
call_rejected: "Puhelu hylätty",
close: "Sulje",
call_ended: "Puhelu päättyi",
calling: "Soitetaan",
on_call_with: "Puhelu käyttäjälle",
total: "Yhteensä",
home: "Koti",
messages: "Viestit",
market: "Tori",
contacts: "Yhteystiedot",
explorer: "Hakemisto",
no_followers_yet: "Jaa linkkisi, niin muut voivat seurata sinua:",
next: "Seuraava",
shopping_carts: "Ostoskorit",
shopping_cart: "Ostoskori",
cart: "Kori",
delivery: "Yhteystiedot",
payment: "Maksutapa",
payment_method: "Maksutapa",
confirm: "Yhteenveto",
address: "Osoite",
email_optional: "Sähköposti (valinnainen)",
name: "Nimi",
confirm_button: "Vahvista",
summary: "Yhteenveto",
following: "Seurataan",
followers: "Seuraajat",
follow: "Seuraa",
likes: "Tykkäykset",
replies: "Vastaukset",
posts: "Julkaisut",
type_a_message_or_paste_a_magnet_link: "Kirjoita viesti tai liitä magnet-linkki",
download_webtorrent: "Lataa <a>Webtorrent Desktop</a> mediatiedostojen jakamiseen ja liitä niiden magnet-linkki alle.",
alternatively: "Vaihtoehtoisesti",
alternatively_get_sms_verified: "Vaihtoehtoisesti <a>tunnistaudu tekstiviestillä</a>",
visibility: "Julkaisut, vastaukset ja tykkäykset näkyvät vain seuraajillesi ja heidän verkostoillensa.",
no_followers_yet_info: "Julkaisut, vastaukset ja tykkäykset näkyvät vain seuraajillesi ja heidän verkostoillensa.",
iris_is_like: "Iiris on kuin tuntemamme sosiaalisen median sovellukset, mutta parempi.",
copy: "Kopioi",
this_is_a_prototype_store: "Tori on prototyyppi, ja se näyttää myynnissä olevat tavarat sosiaalisessa verkostossasi. Iriksessä tuotteet tilataan yksityisviesteillä. <a>Tästä </a> pääset omaan kauppaasi.",
add_to_cart: "Lisää koriin",
switch_account: "Vaihda tiliä",
web_push_subscriptions: "Selaimen ilmoitustilaukset (web push subscriptions)",
enable_public_peer_discovery: "Salli julkisten yhteyspisteiden hakeminen",
maximum_number_of_peer_connections: "Maksimimäärä yhteyspisteitä",
set_up_your_own_peer: "Pystytä oma yhteyspisteesi",
also: "Myös",
or_small: "tai",
automatically_load_webtorrent_attachments: "Lataa webtorrentin liitteet automaattisesti",
autoplay_webtorrent_videos: "Käynnistä webtorrentin videot automaattisesti",
search: "Etsi",
iris_is_accessible: "<b>Iris on helppo ottaa käyttöön.</b> Puhelinnumeroa tai erillistä tunnuksen luomista ei vaadita. Riittää, kun kirjoitat nimesi tai nimimerkkisi ja painat enteriä!",
iris_is_secure: "<b>Iris on turvallinen.</b> Se toimii avoimella lähdekoodilla. Voit olla varma, että yksityisviestisi säilyvät yksityisinä",
iris_is_always_available: "<b>Iris on aina käytettävissä.</b> Se toimii offline-tilassa, eikä ole riippuvainen keskuspalvelimesta. Käyttäjät voivat jopa muodostaa vertaisverkkoja keskenään",
in_other_words: "Toisin sanoen, tiliäsi ei voida sulkea Iriksestä",
get_the_desktop_application: "Hanki työpöytäsovellus",
communicate_and_synchronize: "Kommunikoi ja lähiverkossa olevien käyttäjien kanssa ilman Internet-yhteyttä",
when_local_peers: "Viestisi näkyvät globaalisti, kun käyttäjät kirjautuvat verkkoon",
bluetooth_support_upcoming: "Bluetooth-yhteys tulossa",
opens_to_background: "Sovellus pysyy käynnissä taustalla: olet online-tilassa ja saat ilmoitukset viesteistä",
more_secure_and_available: "Turvallisempi ja helpommin saavutettavissa: selainsovellusta ei tarvitse avata palvelimelta",
direct_connect_to_the: "Yhdistä suoraan henkilöille, joiden kanssa keskustelet (jos portti 8767 on auki tai upnp on käytössä reitittimessä)",
the_application_is_unaudited: "Sovellus on auditoimaton konseptitoteutus, joten älä käytä sitä tarkoituksiin joissa turvallisuus on oleellisen tärkeää.",
follow_someone_info: "Ala seurata jonkun profiilia nähdäksesi päivitykset! Ehdotus:",
add_hashtag: "+ Lisää aihetunniste",
all: "Kaikki",
give_your_profile_link_to_someone: "jaa profiililinkkisi",
follows: "Seurattavat",
everyone: "Kaikki",
connected_peers: "Yhdistetyt vertaisverkot",
no_notifications_yet: "Ei vielä ilmoituksia",
popular_hashtags: "Suosittuja aihetunnisteita",
email_privkey_to_yourself: "Lähetä yksityinen avain itsellesi sähköpostilla",
email: "Sähköposti",
retype_email: "Sähköposti (uudelleen)",
go: "Lähetä",
blocked_users: "Estetyt käyttäjät",
none: "Ei yhtään",
no_items_to_show: "Ei näytettäviä tuotteita",
store_description: "Kuvaus kaupasta",
profile_name: "Profiili/nimi",
store: "Kauppa",
item_id: "Tuotteen ID",
item_description: "Tuotteen kuvaus",
price: "Hinta",
delete_item: "Poista tuote",
add_item: "Lisää tuote",
no_contacts_in_list: "Ei yhteystietoja listalla",
show_qr_code: "Näytä QR-koodi",
copied: 'Kopioitu',
answer: 'vastaa',
reject: 'hylkää',
incoming_call: 'Saapuva puhelu',
call_rejected: 'Puhelu hylätty',
close: 'Sulje',
call_ended: 'Puhelu päättyi',
calling: 'Soitetaan',
on_call_with: 'Puhelu käyttäjälle',
total: 'Yhteensä',
home: 'Koti',
messages: 'Viestit',
market: 'Tori',
contacts: 'Yhteystiedot',
explorer: 'Hakemisto',
no_followers_yet: 'Jaa linkkisi, niin muut voivat seurata sinua:',
next: 'Seuraava',
shopping_carts: 'Ostoskorit',
shopping_cart: 'Ostoskori',
cart: 'Kori',
delivery: 'Yhteystiedot',
payment: 'Maksutapa',
payment_method: 'Maksutapa',
confirm: 'Yhteenveto',
address: 'Osoite',
email_optional: 'Sähköposti (valinnainen)',
name: 'Nimi',
confirm_button: 'Vahvista',
summary: 'Yhteenveto',
following: 'Seurataan',
followers: 'Seuraajat',
follow: 'Seuraa',
likes: 'Tykkäykset',
replies: 'Vastaukset',
posts: 'Julkaisut',
type_a_message_or_paste_a_magnet_link: 'Kirjoita viesti tai liitä magnet-linkki',
download_webtorrent:
'Lataa <a>Webtorrent Desktop</a> mediatiedostojen jakamiseen ja liitä niiden magnet-linkki alle.',
alternatively: 'Vaihtoehtoisesti',
alternatively_get_sms_verified: 'Vaihtoehtoisesti <a>tunnistaudu tekstiviestillä</a>',
visibility:
'Julkaisut, vastaukset ja tykkäykset näkyvät vain seuraajillesi ja heidän verkostoillensa.',
no_followers_yet_info:
'Julkaisut, vastaukset ja tykkäykset näkyvät vain seuraajillesi ja heidän verkostoillensa.',
iris_is_like: 'Iiris on kuin tuntemamme sosiaalisen median sovellukset, mutta parempi.',
copy: 'Kopioi',
this_is_a_prototype_store:
'Tori on prototyyppi, ja se näyttää myynnissä olevat tavarat sosiaalisessa verkostossasi. Iriksessä tuotteet tilataan yksityisviesteillä. <a>Tästä </a> pääset omaan kauppaasi.',
add_to_cart: 'Lisää koriin',
switch_account: 'Vaihda tiliä',
web_push_subscriptions: 'Selaimen ilmoitustilaukset (web push subscriptions)',
enable_public_peer_discovery: 'Salli julkisten yhteyspisteiden hakeminen',
maximum_number_of_peer_connections: 'Maksimimäärä yhteyspisteitä',
set_up_your_own_peer: 'Pystytä oma yhteyspisteesi',
also: 'Myös',
or_small: 'tai',
automatically_load_webtorrent_attachments: 'Lataa webtorrentin liitteet automaattisesti',
autoplay_webtorrent_videos: 'Käynnistä webtorrentin videot automaattisesti',
search: 'Etsi',
iris_is_accessible:
'<b>Iris on helppo ottaa käyttöön.</b> Puhelinnumeroa tai erillistä tunnuksen luomista ei vaadita. Riittää, kun kirjoitat nimesi tai nimimerkkisi ja painat enteriä!',
iris_is_secure:
'<b>Iris on turvallinen.</b> Se toimii avoimella lähdekoodilla. Voit olla varma, että yksityisviestisi säilyvät yksityisinä',
iris_is_always_available:
'<b>Iris on aina käytettävissä.</b> Se toimii offline-tilassa, eikä ole riippuvainen keskuspalvelimesta. Käyttäjät voivat jopa muodostaa vertaisverkkoja keskenään',
in_other_words: 'Toisin sanoen, tiliäsi ei voida sulkea Iriksestä',
get_the_desktop_application: 'Hanki työpöytäsovellus',
communicate_and_synchronize:
'Kommunikoi ja lähiverkossa olevien käyttäjien kanssa ilman Internet-yhteyttä',
when_local_peers: 'Viestisi näkyvät globaalisti, kun käyttäjät kirjautuvat verkkoon',
bluetooth_support_upcoming: 'Bluetooth-yhteys tulossa',
opens_to_background:
'Sovellus pysyy käynnissä taustalla: olet online-tilassa ja saat ilmoitukset viesteistä',
more_secure_and_available:
'Turvallisempi ja helpommin saavutettavissa: selainsovellusta ei tarvitse avata palvelimelta',
direct_connect_to_the:
'Yhdistä suoraan henkilöille, joiden kanssa keskustelet (jos portti 8767 on auki tai upnp on käytössä reitittimessä)',
the_application_is_unaudited:
'Sovellus on auditoimaton konseptitoteutus, joten älä käytä sitä tarkoituksiin joissa turvallisuus on oleellisen tärkeää.',
follow_someone_info: 'Ala seurata jonkun profiilia nähdäksesi päivitykset! Ehdotus:',
add_hashtag: '+ Lisää aihetunniste',
all: 'Kaikki',
give_your_profile_link_to_someone: 'jaa profiililinkkisi',
follows: 'Seurattavat',
everyone: 'Kaikki',
connected_peers: 'Yhdistetyt vertaisverkot',
no_notifications_yet: 'Ei vielä ilmoituksia',
popular_hashtags: 'Suosittuja aihetunnisteita',
email_privkey_to_yourself: 'Lähetä yksityinen avain itsellesi sähköpostilla',
email: 'Sähköposti',
retype_email: 'Sähköposti (uudelleen)',
go: 'Lähetä',
blocked_users: 'Estetyt käyttäjät',
none: 'Ei yhtään',
no_items_to_show: 'Ei näytettäviä tuotteita',
store_description: 'Kuvaus kaupasta',
profile_name: 'Profiili/nimi',
store: 'Kauppa',
item_id: 'Tuotteen ID',
item_description: 'Tuotteen kuvaus',
price: 'Hinta',
delete_item: 'Poista tuote',
add_item: 'Lisää tuote',
no_contacts_in_list: 'Ei yhteystietoja listalla',
show_qr_code: 'Näytä QR-koodi',
};

View File

@ -1,148 +1,164 @@
export default {
whats_your_name: "Quel est votre nom ?",
whats_your_name: 'Quel est votre nom ?',
new_user_go: "C'est parti",
already_have_an_account: "Vous avez déjà un compte ?",
back: "Retour",
scan_private_key_qr_code: "Scannez le code QR de la clé privée",
paste_private_key: "Coller une clé privée",
get_notified_new_messages: "Être notifié des nouveaux messages",
turn_on_desktop_notifications: "Activer les notifications de bureau",
new_chat: "Nouveau chat",
public_messages: "Messages publics",
already_have_an_account: 'Vous avez déjà un compte ?',
back: 'Retour',
scan_private_key_qr_code: 'Scannez le code QR de la clé privée',
paste_private_key: 'Coller une clé privée',
get_notified_new_messages: 'Être notifié des nouveaux messages',
turn_on_desktop_notifications: 'Activer les notifications de bureau',
new_chat: 'Nouveau chat',
public_messages: 'Messages publics',
follow_someone_info: "Suivez quelqu'un pour voir le contenu de leur réseau ! Suggestions : ",
creator_of_this_distribution: "Créateur de cette distribution d'Iris",
no_followers_yet: "Partagez votre lien de profil pour que les autres puissent vous suivre :",
no_followers_yet_info: "Vos posts, réponses et likes sont seulement visibles par vos followers et leurs réseaux.",
alternatively: "Sinon,",
alternatively_get_sms_verified: "Vous pouvez aussi, activer <a>SMS verified</a> pour que les autres puissent vous trouver.",
no_followers_yet: 'Partagez votre lien de profil pour que les autres puissent vous suivre :',
no_followers_yet_info:
'Vos posts, réponses et likes sont seulement visibles par vos followers et leurs réseaux.',
alternatively: 'Sinon,',
alternatively_get_sms_verified:
'Vous pouvez aussi, activer <a>SMS verified</a> pour que les autres puissent vous trouver.',
give_your_profile_link_to_someone: "donner votre lien de profil à quelqu'un",
if_other_person_doesnt_see_message: "Si les autres ne voient pas votre message, vous pouvez leur donner <b>votre</b> lien d'invitation depuis un autre canal :",
type_a_message: "Entrez un message",
type_a_message_or_paste_a_magnet_link: "Entrez un message ou collez un lien magnet",
beware_of_sharing_invite_link_publicly: "Attention à ne pas partager votre lien d'invitation publiquement : vous pourriez être spammé de demandes de contacts. Partagez publiquement plutôt votre <a>lien de profil</a> à la place.",
if_other_person_doesnt_see_message:
"Si les autres ne voient pas votre message, vous pouvez leur donner <b>votre</b> lien d'invitation depuis un autre canal :",
type_a_message: 'Entrez un message',
type_a_message_or_paste_a_magnet_link: 'Entrez un message ou collez un lien magnet',
beware_of_sharing_invite_link_publicly:
"Attention à ne pas partager votre lien d'invitation publiquement : vous pourriez être spammé de demandes de contacts. Partagez publiquement plutôt votre <a>lien de profil</a> à la place.",
your_invite_links: "Votre lien d'invitation",
create_new_invite_link: "Créer un nouveau lien d'invitation",
copy_your_invite_link: "Copier votre lien d'invitation",
have_someones_invite_link: "Vous avez le lien d'invitation de quelqu'un ?",
paste_their_invite_link: "Collez un lien d'invitation",
give_your_invite_link: "Donnez votre lien d'invitation:",
or_scan_qr_code: "Ou scannez leur Code QR",
or_show_qr_code: "Ou visualisez votre Code QR",
new_group: "Nouveau groupe",
group_name: "Nom du groupe",
create: "Créer",
settings: "Réglages",
profile: "Profil",
your_name: "Votre nom",
profile_photo: "Photo de profil",
add_profile_photo: "Ajoutez une photo de profil",
profile_photo_too_big: "Photo de profil trop grosse : la taille maximale est de 200Ko",
cancel: "Annulez",
use_photo: "Utiliser votre photo",
remove_photo: "Supprimer photo",
about_text: "À propos",
account: "Compte",
or_scan_qr_code: 'Ou scannez leur Code QR',
or_show_qr_code: 'Ou visualisez votre Code QR',
new_group: 'Nouveau groupe',
group_name: 'Nom du groupe',
create: 'Créer',
settings: 'Réglages',
profile: 'Profil',
your_name: 'Votre nom',
profile_photo: 'Photo de profil',
add_profile_photo: 'Ajoutez une photo de profil',
profile_photo_too_big: 'Photo de profil trop grosse : la taille maximale est de 200Ko',
cancel: 'Annulez',
use_photo: 'Utiliser votre photo',
remove_photo: 'Supprimer photo',
about_text: 'À propos',
account: 'Compte',
save_backup_of_privkey_first: "Sauvegardez d'abord une copie de votre clé privée",
otherwise_cant_log_in_again: "Sinon vous ne pourrez plus vous connecter de nouveau sur ce compte",
log_out: "Se déconnecter",
private_key: "Clé privée",
download_private_key: "Télécharger la clé privée",
private_key_warning: "<b>DANGER !</b> La clé privée <b>donne accès à votre compte</b>. Ne la transmettez ou la montrez à personne d'autre !",
copy_private_key: "Copier la clé privée",
show_privkey_qr: "Visualiser le Code QR de la clé privée",
hide_privkey_qr: "Cacher le code QR de la clé privée",
privkey_storage_recommendation: "L'endroit le plus sécurisé pour votre clé privée est un <b>gestionnaire de mots de passe</b>.",
language: "Langage",
peers: "Pairs",
peer_url: "Url des pairs",
public: "Public",
remove: "Supprimer",
enable: "Activer",
disable: "Désactiver",
from: "de", // source of the peer url, from whom we got it
add: "Ajouter",
public_peer_info: "Les pairs <i>publics</i> sont automatiquement visibles par les personnes avec qui vous discutez.",
peers_info: "Les pairs sont des noeuds GunDB que vous pouvez facilement <a>monter</a>. A venir : la connexion directe avec vos amis via WebRTC.",
webrtc_connection_options: "Options de connection WebRTC",
webrtc_info: "WebRTC est utilisé pour les communications audio/vidéo. Si vous êtes derrière un routeur, vous devrez probablement spécifier un serveur de rebond TURN ici, qui devrait relayer votre communication ici.",
restore_defaults: "Remettre par défaut",
about: "À propos", // About Iris
application_security_warning: "L'application est un POC d'implémentation non audité, ne l'utilisez pas pour un quelconque motif à sécurité critique.",
donate: "Faites un don",
donate_info: "<b>Les donnations</b> aident à garder le projet en œuvre et sont grandement appréciés. Vous pouvez participer via <a>Open Collective</a> ou <b>bitcoin</b>",
logout_confirmation_info: "Vous <b>ne pouvez pas vous réauthentifier</b> sauf si vous avez une copie de votre clé privée.",
participants: "Participants",
admin: "admin",
add_participant: "Ajouter participant",
new_participants_profile_link: "Lien de profil du nouveau participant",
otherwise_cant_log_in_again: 'Sinon vous ne pourrez plus vous connecter de nouveau sur ce compte',
log_out: 'Se déconnecter',
private_key: 'Clé privée',
download_private_key: 'Télécharger la clé privée',
private_key_warning:
"<b>DANGER !</b> La clé privée <b>donne accès à votre compte</b>. Ne la transmettez ou la montrez à personne d'autre !",
copy_private_key: 'Copier la clé privée',
show_privkey_qr: 'Visualiser le Code QR de la clé privée',
hide_privkey_qr: 'Cacher le code QR de la clé privée',
privkey_storage_recommendation:
"L'endroit le plus sécurisé pour votre clé privée est un <b>gestionnaire de mots de passe</b>.",
language: 'Langage',
peers: 'Pairs',
peer_url: 'Url des pairs',
public: 'Public',
remove: 'Supprimer',
enable: 'Activer',
disable: 'Désactiver',
from: 'de', // source of the peer url, from whom we got it
add: 'Ajouter',
public_peer_info:
'Les pairs <i>publics</i> sont automatiquement visibles par les personnes avec qui vous discutez.',
peers_info:
'Les pairs sont des noeuds GunDB que vous pouvez facilement <a>monter</a>. A venir : la connexion directe avec vos amis via WebRTC.',
webrtc_connection_options: 'Options de connection WebRTC',
webrtc_info:
'WebRTC est utilisé pour les communications audio/vidéo. Si vous êtes derrière un routeur, vous devrez probablement spécifier un serveur de rebond TURN ici, qui devrait relayer votre communication ici.',
restore_defaults: 'Remettre par défaut',
about: 'À propos', // About Iris
application_security_warning:
"L'application est un POC d'implémentation non audité, ne l'utilisez pas pour un quelconque motif à sécurité critique.",
donate: 'Faites un don',
donate_info:
'<b>Les donnations</b> aident à garder le projet en œuvre et sont grandement appréciés. Vous pouvez participer via <a>Open Collective</a> ou <b>bitcoin</b>',
logout_confirmation_info:
'Vous <b>ne pouvez pas vous réauthentifier</b> sauf si vous avez une copie de votre clé privée.',
participants: 'Participants',
admin: 'admin',
add_participant: 'Ajouter participant',
new_participants_profile_link: 'Lien de profil du nouveau participant',
invite_links: "Liens d'invitation",
copy: "Copier",
switch_account: "Changer de compte",
follows_you: "Vous suit",
follow: "Suivre",
unfollow: "Ne plus suivre",
following: "Suivi",
followers: "Followers",
copy: 'Copier',
switch_account: 'Changer de compte',
follows_you: 'Vous suit',
follow: 'Suivre',
unfollow: 'Ne plus suivre',
following: 'Suivi',
followers: 'Followers',
add_friend: "Demande d'ami",
send_message: "Envoyer un message",
copy_link: "Copier le lien",
show_qr_code: "Voir Code QR",
chat_settings: "Paramètres de messages",
nicknames: "Surnoms",
nickname: "Surnom",
video_call: "Appel vidéo",
online: "en ligne",
last_active: "dernière activité",
send_message: 'Envoyer un message',
copy_link: 'Copier le lien',
show_qr_code: 'Voir Code QR',
chat_settings: 'Paramètres de messages',
nicknames: 'Surnoms',
nickname: 'Surnom',
video_call: 'Appel vidéo',
online: 'en ligne',
last_active: 'dernière activité',
their_nickname_for_you: "Le surnom qu'il/elle vous donne :",
notifications: "Notifications",
all_messages: "Tous les messages",
mentions_only: "Mentions seulement",
nothing: "Rien",
delete_chat: "Supprimer chat",
notifications: 'Notifications',
all_messages: 'Tous les messages',
mentions_only: 'Mentions seulement',
nothing: 'Rien',
delete_chat: 'Supprimer chat',
block_user: "Bloquer l'utilisateur",
typing: "En train d'écrire...",
attachment: "pièces jointes",
note_to_self: "Notes pour soi",
attachment: 'pièces jointes',
note_to_self: 'Notes pour soi',
today: "aujourd'hui",
yesterday: 'hier',
copied: "Copié",
answer: "Répondre",
reject: "Rejeter",
incoming_call: "Appel entrant",
call_rejected: "Appel rejeté",
close: "Raccrocher",
call_ended: "Appel terminé",
calling: "Appel en cours",
on_call_with: "En appel avec : ",
copied: 'Copié',
answer: 'Répondre',
reject: 'Rejeter',
incoming_call: 'Appel entrant',
call_rejected: 'Appel rejeté',
close: 'Raccrocher',
call_ended: 'Appel terminé',
calling: 'Appel en cours',
on_call_with: 'En appel avec : ',
delete: 'Supprimer',
confirm_delete_msg: 'Supprimer message ?',
search: 'Recherche',
email_privkey_to_yourself: 'Vous envoyer votre clé privée ',
email: "Email",
email: 'Email',
retype_email: "Saisir l'email à nouveau",
email_optional: "Email (optionnel)",
delivery: "Délivré",
address: "Adresse",
confirmation: "Confirmation",
payment_method: "Méthode de paiement",
summary: "Résumé",
download_webtorrent: "Téléchargez <a>Webtorrent Desktop</a> pour héberger vos fichiers multimédias et collez leurs liens magnet ci-dessous.",
visibility: "Vos posts, réponses et likes sont seulement visibles par vos followers et leur réseau.",
iris_is_like: "Iris, c'est comme les réseaux sociaux auxquels nous sommes habitués ... mais c'est mieux !",
this_is_a_prototype_store: "Ceci est un prototype de boutique qui permet de visualiser des produits de marchands dans votre réseau social. Les commandes sont envoyées via messages privés Iris. Votre propre boutique est disponible <a>ici</a>.",
add_to_cart: "Ajouter au panier",
web_push_subscriptions: "Souscription push web",
enable_public_peer_discovery: "Activer la découverte de pairs publics",
set_up_your_own_peer: "Paramétrez votre propre pair",
also: "Également",
or_small: "ou",
automatically_load_webtorrent_attachments: "Lire automatiquement les médias webtorrent",
autoplay_webtorrent_videos: "Lire automatiquement les vidéos webtorrent",
home: "Accueil",
media: "Médias",
messages: "Messages",
market: "Market",
contacts: "Contacts",
explorer: "Explorer",
no_contacts_in_list: 'Vous n\'avez aucun contact dans votre liste.',
email_optional: 'Email (optionnel)',
delivery: 'Délivré',
address: 'Adresse',
confirmation: 'Confirmation',
payment_method: 'Méthode de paiement',
summary: 'Résumé',
download_webtorrent:
'Téléchargez <a>Webtorrent Desktop</a> pour héberger vos fichiers multimédias et collez leurs liens magnet ci-dessous.',
visibility:
'Vos posts, réponses et likes sont seulement visibles par vos followers et leur réseau.',
iris_is_like:
"Iris, c'est comme les réseaux sociaux auxquels nous sommes habitués ... mais c'est mieux !",
this_is_a_prototype_store:
'Ceci est un prototype de boutique qui permet de visualiser des produits de marchands dans votre réseau social. Les commandes sont envoyées via messages privés Iris. Votre propre boutique est disponible <a>ici</a>.',
add_to_cart: 'Ajouter au panier',
web_push_subscriptions: 'Souscription push web',
enable_public_peer_discovery: 'Activer la découverte de pairs publics',
set_up_your_own_peer: 'Paramétrez votre propre pair',
also: 'Également',
or_small: 'ou',
automatically_load_webtorrent_attachments: 'Lire automatiquement les médias webtorrent',
autoplay_webtorrent_videos: 'Lire automatiquement les vidéos webtorrent',
home: 'Accueil',
media: 'Médias',
messages: 'Messages',
market: 'Market',
contacts: 'Contacts',
explorer: 'Explorer',
no_contacts_in_list: "Vous n'avez aucun contact dans votre liste.",
};

View File

@ -1,100 +1,110 @@
export default {
whats_your_name: "Quale è il tuo nome??",
new_user_go: "Inizia",
already_have_an_account: "Hai già un account?",
back: "Indietro",
scan_private_key_qr_code: "Scansione codice QR di chiave privata",
paste_private_key: "Incolla una chiave privata",
get_notified_new_messages: "Ricevi una notifica per i nuovi messaggi",
turn_on_desktop_notifications: "Attiva le notifiche desktop",
new_chat: "Nuova chat",
if_other_person_doesnt_see_message: "Se l'altra persona non vede il tuo messaggio, puoi dargli il <b>tuo</b> invite link attraverso qualche altro canale:",
type_a_message: "Scrivi un messaggio",
beware_of_sharing_invite_link_publicly: "Fai attenzione a condividere pubblicamente il tuo invite link: potresti ricevere spam con richieste di messaggi. Condividi pubblicamente invece il tuo <a>link di profilo</a>.",
your_invite_links: "I tuoi invite link",
create_new_invite_link: "Crea nuovo invite link",
copy_your_invite_link: "Copia il tuo invite link",
have_someones_invite_link: "Hai il invite link di quakcuno?",
paste_their_invite_link: "Incolla il loro invite link",
give_your_invite_link: "Dai il tuo invite link:",
or_scan_qr_code: "O scansiona il loro codice QR",
or_show_qr_code: "O mostra il tuo codice QR",
new_group: "Nuovo gruppo",
group_name: "Nome gruppo",
create: "Crea",
profile: "Profilo",
settings: "Impostazioni",
your_name: "Il tuo nome",
profile_photo: "Foto di profilo",
add_profile_photo: "Aggiungi foto di profilo",
profile_photo_too_big: "Foto di profilo troppo grande: dimensione massima di 200KB",
cancel: "Cancella",
use_photo: "Usa foto",
remove_photo: "Rimuovi foto",
about_text: "Riguardo a te",
account: "Account",
save_backup_of_privkey_first: "Salva prima un backup della tua chiave privata!",
otherwise_cant_log_in_again: "Altrimenti non potrai riautenticarti a questo account.",
log_out: "Scollegati",
private_key: "Chiave privata",
download_private_key: "Scarica chiave privata",
private_key_warning: "<b>PERICOLO!</b> La chiave privata è usata per <b>autenticarti al tuo account</b>. Non dare o mostrare la tua chiave privata a nessun altro!",
copy_private_key: "Copia chiave privata",
show_privkey_qr: "Mostra il codice QR della chiave privata",
hide_privkey_qr: "Nascondi il codice QR della chaive privata",
privkey_storage_recommendation: "Il posto più sicuro dove archiviare la chiave privata è un <b>gestore password</b>.",
language: "Lingua",
peers: "Peer",
peer_url: "Peer url",
public: "Pubblico",
remove: "Rimuovi",
enable: "Abilita",
disable: "Disabilita",
from: "da", // source of the peer url, from whom we got it
add: "Aggiungi",
public_peer_info: "Peer <i>pubblici</i> sono automaticamente rilevabili dalle persone con cui scrivi (e altri).",
peers_info: "I peer sono nodi GunDB che puoi facilmente <a>montare</a>. In arrivo: connessione diretta con gli amici attraverso WebRTC.",
webrtc_connection_options: "Opzioni di connessione WebRTC",
webrtc_info: "WebRTC è usato per le video chiamate. Se sei dietro ad una NAT, potresti aver bisogno di specificare un server TURN qui, che inoltrerà il tuo traffico video. La banda di trasferimento non è gratuita, non ci sono server TURN gratuiti in giro.",
restore_defaults: "Ripristina le impostazioni predifinite",
about: "A proposito", // About Iris
application_security_warning: "L'applicazione è un'implementazione di prova del concetto non certificata, quindi non utilizzarla per fini che richiedano un uso sicuro in situazioni critiche.",
donate: "Dona",
donate_info: "<b>Le donazioni</b> aiutano a mantenere il progetto e sono molto apprezzate. Puoi donare attraverso <a>Open Collective</a> o <b>bitcoin</b>",
logout_confirmation_info: "<b>Non puoi autenticarti</b> a meno che tu non abbia salvato una copia della tua chiave privata.",
participants: "partecipanti",
admin: "admin",
add_participant: "Aggiungi partecipante",
new_participants_profile_link: "Nuovi invite link del participante",
add_friend: "Aggiungi amico",
send_message: "Invia messaggio",
copy_link: "Copia link",
chat_settings: "Impostazioni della Chat",
nicknames: "Soprannome",
nickname: "Soprannome",
video_call: "Chiamata video",
online: "online",
last_active: "ultimo attivo",
their_nickname_for_you: "Il loro soprannome per te",
notifications: "Notifiche",
all_messages: "Tutti i messaggi",
mentions_only: "Solo menzioni",
nothing: "Nulla",
delete_chat: "Cancella chat",
block_user: "Blocca utente",
typing: "Sta scrivendo...",
attachment: "allegato",
note_to_self: "Nota a te stesso",
whats_your_name: 'Quale è il tuo nome??',
new_user_go: 'Inizia',
already_have_an_account: 'Hai già un account?',
back: 'Indietro',
scan_private_key_qr_code: 'Scansione codice QR di chiave privata',
paste_private_key: 'Incolla una chiave privata',
get_notified_new_messages: 'Ricevi una notifica per i nuovi messaggi',
turn_on_desktop_notifications: 'Attiva le notifiche desktop',
new_chat: 'Nuova chat',
if_other_person_doesnt_see_message:
"Se l'altra persona non vede il tuo messaggio, puoi dargli il <b>tuo</b> invite link attraverso qualche altro canale:",
type_a_message: 'Scrivi un messaggio',
beware_of_sharing_invite_link_publicly:
'Fai attenzione a condividere pubblicamente il tuo invite link: potresti ricevere spam con richieste di messaggi. Condividi pubblicamente invece il tuo <a>link di profilo</a>.',
your_invite_links: 'I tuoi invite link',
create_new_invite_link: 'Crea nuovo invite link',
copy_your_invite_link: 'Copia il tuo invite link',
have_someones_invite_link: 'Hai il invite link di quakcuno?',
paste_their_invite_link: 'Incolla il loro invite link',
give_your_invite_link: 'Dai il tuo invite link:',
or_scan_qr_code: 'O scansiona il loro codice QR',
or_show_qr_code: 'O mostra il tuo codice QR',
new_group: 'Nuovo gruppo',
group_name: 'Nome gruppo',
create: 'Crea',
profile: 'Profilo',
settings: 'Impostazioni',
your_name: 'Il tuo nome',
profile_photo: 'Foto di profilo',
add_profile_photo: 'Aggiungi foto di profilo',
profile_photo_too_big: 'Foto di profilo troppo grande: dimensione massima di 200KB',
cancel: 'Cancella',
use_photo: 'Usa foto',
remove_photo: 'Rimuovi foto',
about_text: 'Riguardo a te',
account: 'Account',
save_backup_of_privkey_first: 'Salva prima un backup della tua chiave privata!',
otherwise_cant_log_in_again: 'Altrimenti non potrai riautenticarti a questo account.',
log_out: 'Scollegati',
private_key: 'Chiave privata',
download_private_key: 'Scarica chiave privata',
private_key_warning:
'<b>PERICOLO!</b> La chiave privata è usata per <b>autenticarti al tuo account</b>. Non dare o mostrare la tua chiave privata a nessun altro!',
copy_private_key: 'Copia chiave privata',
show_privkey_qr: 'Mostra il codice QR della chiave privata',
hide_privkey_qr: 'Nascondi il codice QR della chaive privata',
privkey_storage_recommendation:
'Il posto più sicuro dove archiviare la chiave privata è un <b>gestore password</b>.',
language: 'Lingua',
peers: 'Peer',
peer_url: 'Peer url',
public: 'Pubblico',
remove: 'Rimuovi',
enable: 'Abilita',
disable: 'Disabilita',
from: 'da', // source of the peer url, from whom we got it
add: 'Aggiungi',
public_peer_info:
'Peer <i>pubblici</i> sono automaticamente rilevabili dalle persone con cui scrivi (e altri).',
peers_info:
'I peer sono nodi GunDB che puoi facilmente <a>montare</a>. In arrivo: connessione diretta con gli amici attraverso WebRTC.',
webrtc_connection_options: 'Opzioni di connessione WebRTC',
webrtc_info:
'WebRTC è usato per le video chiamate. Se sei dietro ad una NAT, potresti aver bisogno di specificare un server TURN qui, che inoltrerà il tuo traffico video. La banda di trasferimento non è gratuita, non ci sono server TURN gratuiti in giro.',
restore_defaults: 'Ripristina le impostazioni predifinite',
about: 'A proposito', // About Iris
application_security_warning:
"L'applicazione è un'implementazione di prova del concetto non certificata, quindi non utilizzarla per fini che richiedano un uso sicuro in situazioni critiche.",
donate: 'Dona',
donate_info:
'<b>Le donazioni</b> aiutano a mantenere il progetto e sono molto apprezzate. Puoi donare attraverso <a>Open Collective</a> o <b>bitcoin</b>',
logout_confirmation_info:
'<b>Non puoi autenticarti</b> a meno che tu non abbia salvato una copia della tua chiave privata.',
participants: 'partecipanti',
admin: 'admin',
add_participant: 'Aggiungi partecipante',
new_participants_profile_link: 'Nuovi invite link del participante',
add_friend: 'Aggiungi amico',
send_message: 'Invia messaggio',
copy_link: 'Copia link',
chat_settings: 'Impostazioni della Chat',
nicknames: 'Soprannome',
nickname: 'Soprannome',
video_call: 'Chiamata video',
online: 'online',
last_active: 'ultimo attivo',
their_nickname_for_you: 'Il loro soprannome per te',
notifications: 'Notifiche',
all_messages: 'Tutti i messaggi',
mentions_only: 'Solo menzioni',
nothing: 'Nulla',
delete_chat: 'Cancella chat',
block_user: 'Blocca utente',
typing: 'Sta scrivendo...',
attachment: 'allegato',
note_to_self: 'Nota a te stesso',
today: 'oggi',
yesterday: 'ieri',
copied: "Copiato",
answer: "rispondere",
reject: "rifiutare",
incoming_call: "Chiamata in arrivo",
call_rejected: "Chiamata respinta",
close: "Chiudere",
call_ended: "Chiamata conclusa",
calling: "Chiamata in corso",
on_call_with: "In chiamata con",
copied: 'Copiato',
answer: 'rispondere',
reject: 'rifiutare',
incoming_call: 'Chiamata in arrivo',
call_rejected: 'Chiamata respinta',
close: 'Chiudere',
call_ended: 'Chiamata conclusa',
calling: 'Chiamata in corso',
on_call_with: 'In chiamata con',
no_contacts_in_list: 'Non hai contatti nella tua lista.',
};

View File

@ -1,137 +1,148 @@
export default {
whats_your_name: "이름?",
new_user_go: "진행",
already_have_an_account: "기존 계정이 있나요?",
back: "뒤로",
scan_private_key_qr_code: "프라이빗 키 스캔 QR 코드",
paste_private_key: "프라이빗 키 붙여넣기",
get_notified_new_messages: "신규 메시지 통보",
turn_on_desktop_notifications: "데스크탑 통보 시작",
new_chat: "신규 채팅",
public_messages: "공개 메시지",
follow_someone_info: "제안 그들의 네트워크의 내용을 보기위하여 펄로우 하기:",
creator_of_this_distribution: "본Iris 배분을 위한 저작권자",
no_followers_yet: "약력 링크를 소개하여 타인들이 펄로우 하게 함:",
no_followers_yet_info: "당신의 게시, 응답, 좋아요는 당신의 펄로워 및 네트워크에 보여짐.",
alternatively: "대안으로,",
alternatively_get_sms_verified: "대안으로, 타인이 당신을 찾을 수 있도록 하기 <a>SMS 확인 </a>.",
give_your_profile_link_to_someone: "당신의 약력 링크를 타인에게 전달",
if_other_person_doesnt_see_message: "타인이 당신의 메시지를 보지 못하면 다른 채널의 링크로 초대 할 수 있음 <b>당신</b>:",
type_a_message: "메시지 입력",
beware_of_sharing_invite_link_publicly: "경고 초대링크를 공개적으로 사용하는 것은: 스팸을 받을수 있으니 공개 할 시는 당신의 <a>profile link</a>를 사용하세요.",
your_invite_links: "당신의 초대 링크",
create_new_invite_link: "새 초대링크 생성",
copy_your_invite_link: "초대링크 복사",
have_someones_invite_link: "타인의 초대링크 보유?",
paste_their_invite_link: "그들의 초대링크 붙이기",
give_your_invite_link: "당신의 초대링크 제공:",
or_scan_qr_code: "아니면 그들의 QR코드 스캔",
or_show_qr_code: "아니면 당신의 QR코드 공개",
new_group: "신규 그룹",
group_name: "그룹 이름",
create: "생성",
settings: "환경",
profile: "약력",
your_name: "성명",
profile_photo: "약력 사진",
add_profile_photo: "약력 사진 추가",
profile_photo_too_big: "약력 사진 크기 초과: 최대 사이즈 200KB",
cancel: "취소",
use_photo: "사진 사용",
remove_photo: "사진 제거",
about_text: " 원문",
account: "계정",
save_backup_of_privkey_first: "먼저 당신의 프라이빗 키 백업 저장!",
otherwise_cant_log_in_again: "아니면 이 계정으로 다시 로그인 할 수 없음.",
log_out: "로그 아웃",
private_key: "프라이빗 키",
download_private_key: "프라이빗 키 내려받기",
private_key_warning: "<b>위험!</b> 프라이빗키<b> 는 당신 계정을 사용할 수 있음</b>. 타인에게 당신의 프라이빗 키를 보여주거나 공유하지 말것!",
copy_private_key: "프라이빗 키 복사",
show_privkey_qr: "프라이빗 키 QR 코드 공개",
hide_privkey_qr: "프라이빗 코 QR 코드 숭기기",
privkey_storage_recommendation: "안전하게 당신의 프라이빗 키를 저장하는 장소는 <b>패스워드 관리자</b>.",
language: "언어",
peers: "친구",
peer_url: "친구 url",
public: "공개",
remove: "제거",
enable: "동작",
disable: "비동작",
from: "전송자", // source of the peer url, from whom we got it
add: "추가",
public_peer_info: "<i>공개</i> 친구는 자동적으로 당신이 채팅하고 있는(타인들도)사람들로부터 보여질 수 있음.",
peers_info: "친구는 GunDB 노드이며 쉽게 <a>공유</a>. 향후 계획: 친구들과 직접 접속 WebRTC.",
webrtc_connection_options: "WebRTC 접속 옵션",
webrtc_info: "WebRTC 는 화상 통신에 사용. 당신이 NAT에 속해 있으면, TURN 서버를 여기에 추가 해야 할 수도 있음, 화상 트래픽 전송. 통신량은 무료가 아니라 무료 TURN 서버를 찾기가 쉽지 않음.",
restore_defaults: "조건 원상회복",
about: "About", // Iris 란?
application_security_warning: "본 응용은 검증되지 않은 아이디어 차원의 도구이어서 비밀을 요하는 중요한 목적으로 사용 할 수 없음.",
donate: "기부",
donate_info: "<b>기부</b> 는 본 프로젝트를 진행 하는데 도움이 되고 감사 드립니다. 기부 하려면 여기서 하면 됩니다 <a>Open Collective</a> or <b>bitcoin</b>",
logout_confirmation_info: "당신은 <b>로그인 다시 할 수 없음</b>만약 당신이 프라이빗 키를 저장해 두자 않았으ㅁ.",
participants: "참여자",
admin: "관리자",
add_participant: "참여자 추가",
new_participants_profile_link: "신규 참여자 약력 링크",
invite_links: "링크 초대",
copy: "복사",
follows_you: "당신 펄로우",
follow: "펄로우",
unfollow: "펄로우 취소",
following: "펄러우 진행중",
followers: "펄로워들",
add_friend: "친구 추가",
send_message: "메시지 전송",
copy_link: "링크 복사",
show_qr_code: "QR코드 공유",
chat_settings: "채팅 환경",
nicknames: "닉네임들",
nickname: "닉네임",
video_call: "화상채팅",
online: "온라인",
last_active: "마지막 활동",
their_nickname_for_you: "당신을 위한 그들의 닉네임",
notifications: "통보",
all_messages: "모든 메시지",
mentions_only: "단지 멘션",
nothing: "존대하지 않음",
delete_chat: "책 삭제",
block_user: "사용자 거부",
typing: "타이핑 중...",
attachment: "첨부",
note_to_self: "자신에게 메모",
today: '오늘',
yesterday: '어제',
copied: "복사완료",
answer: "대답",
reject: "거부",
incoming_call: "전화 수신",
call_rejected: "응답 거부",
close: "닫기",
call_ended: "통화 종료",
calling: "통화",
on_call_with: "통화 상대",
delete: '삭제',
confirm_delete_msg: '메시지 삭제?',
search: '검색',
email_optional: "이메일 (옵션)",
delivery: "전달",
address: "주소",
confirmation: "확인",
payment_method: "지불 방법",
summary: "요약",
download_webtorrent: "다운로드 <a>Webtorrent Desktop</a> 미디어 파일을호스트 하고 붙여 넣기 위하여 마그넷링크 아래 제공.",
visibility: "당신의 게시물, 답변과 좋아요는 당신의 펄로워들과 네트워크에만 보여짐.",
iris_is_like: "Iris는 소셜네트워킹 앱이지만 더 향상된 기능임.",
this_is_a_prototype_store: "ㅇ본 화면은 시범 상점이며 당신의 소셜네트워크의 판매점을 보여 줌. 주문은 Iris 개인 메시지로 함. 당신의 상점은 여기에 볼 수 있음. <a>여기</a>.",
add_to_cart: "카트에 추가",
web_push_subscriptions: "웹 푸시 구독",
enable_public_peer_discovery: "공개 친구 찾기 동작",
set_up_your_own_peer: "당신의 자체 친구 설정",
also: "그리고",
or_small: "또는",
automatically_load_webtorrent_attachments: "자동적으로 webtorrent 첨부 파일 로드",
autoplay_webtorrent_videos: "자동으로 webtorrent 비디오 시작",
whats_your_name: '이름?',
new_user_go: '진행',
already_have_an_account: '기존 계정이 있나요?',
back: '뒤로',
scan_private_key_qr_code: '프라이빗 키 스캔 QR 코드',
paste_private_key: '프라이빗 키 붙여넣기',
get_notified_new_messages: '신규 메시지 통보',
turn_on_desktop_notifications: '데스크탑 통보 시작',
new_chat: '신규 채팅',
public_messages: '공개 메시지',
follow_someone_info: '제안 그들의 네트워크의 내용을 보기위하여 펄로우 하기:',
creator_of_this_distribution: '본Iris 배분을 위한 저작권자',
no_followers_yet: '약력 링크를 소개하여 타인들이 펄로우 하게 함:',
no_followers_yet_info: '당신의 게시, 응답, 좋아요는 당신의 펄로워 및 네트워크에 보여짐.',
alternatively: '대안으로,',
alternatively_get_sms_verified: '대안으로, 타인이 당신을 찾을 수 있도록 하기 <a>SMS 확인 </a>.',
give_your_profile_link_to_someone: '당신의 약력 링크를 타인에게 전달',
if_other_person_doesnt_see_message:
'타인이 당신의 메시지를 보지 못하면 다른 채널의 링크로 초대 할 수 있음 <b>당신</b>:',
type_a_message: '메시지 입력',
beware_of_sharing_invite_link_publicly:
'경고 초대링크를 공개적으로 사용하는 것은: 스팸을 받을수 있으니 공개 할 시는 당신의 <a>profile link</a>를 사용하세요.',
your_invite_links: '당신의 초대 링크',
create_new_invite_link: '새 초대링크 생성',
copy_your_invite_link: '초대링크 복사',
have_someones_invite_link: '타인의 초대링크 보유?',
paste_their_invite_link: '그들의 초대링크 붙이기',
give_your_invite_link: '당신의 초대링크 제공:',
or_scan_qr_code: '아니면 그들의 QR코드 스캔',
or_show_qr_code: '아니면 당신의 QR코드 공개',
new_group: '신규 그룹',
group_name: '그룹 이름',
create: '생성',
settings: '환경',
profile: '약력',
your_name: '성명',
profile_photo: '약력 사진',
add_profile_photo: '약력 사진 추가',
profile_photo_too_big: '약력 사진 크기 초과: 최대 사이즈 200KB',
cancel: '취소',
use_photo: '사진 사용',
remove_photo: '사진 제거',
about_text: ' 원문',
account: '계정',
save_backup_of_privkey_first: '먼저 당신의 프라이빗 키 백업 저장!',
otherwise_cant_log_in_again: '아니면 이 계정으로 다시 로그인 할 수 없음.',
log_out: '로그 아웃',
private_key: '프라이빗 키',
download_private_key: '프라이빗 키 내려받기',
private_key_warning:
'<b>위험!</b> 프라이빗키<b> 는 당신 계정을 사용할 수 있음</b>. 타인에게 당신의 프라이빗 키를 보여주거나 공유하지 말것!',
copy_private_key: '프라이빗 키 복사',
show_privkey_qr: '프라이빗 키 QR 코드 공개',
hide_privkey_qr: '프라이빗 코 QR 코드 숭기기',
privkey_storage_recommendation:
'안전하게 당신의 프라이빗 키를 저장하는 장소는 <b>패스워드 관리자</b>.',
language: '언어',
peers: '친구',
peer_url: '친구 url',
public: '공개',
remove: '제거',
enable: '동작',
disable: '비동작',
from: '전송자', // source of the peer url, from whom we got it
add: '추가',
public_peer_info:
'<i>공개</i> 친구는 자동적으로 당신이 채팅하고 있는(타인들도)사람들로부터 보여질 수 있음.',
peers_info: '친구는 GunDB 노드이며 쉽게 <a>공유</a>. 향후 계획: 친구들과 직접 접속 WebRTC.',
webrtc_connection_options: 'WebRTC 접속 옵션',
webrtc_info:
'WebRTC 는 화상 통신에 사용. 당신이 NAT에 속해 있으면, TURN 서버를 여기에 추가 해야 할 수도 있음, 화상 트래픽 전송. 통신량은 무료가 아니라 무료 TURN 서버를 찾기가 쉽지 않음.',
restore_defaults: '조건 원상회복',
about: 'About', // Iris 란?
application_security_warning:
'본 응용은 검증되지 않은 아이디어 차원의 도구이어서 비밀을 요하는 중요한 목적으로 사용 할 수 없음.',
donate: '기부',
donate_info:
'<b>기부</b> 는 본 프로젝트를 진행 하는데 도움이 되고 감사 드립니다. 기부 하려면 여기서 하면 됩니다 <a>Open Collective</a> or <b>bitcoin</b>',
logout_confirmation_info:
'당신은 <b>로그인 다시 할 수 없음</b>만약 당신이 프라이빗 키를 저장해 두자 않았으ㅁ.',
participants: '참여자',
admin: '관리자',
add_participant: '참여자 추가',
new_participants_profile_link: '신규 참여자 약력 링크',
invite_links: '링크 초대',
copy: '복사',
follows_you: '당신 펄로우',
follow: '펄로우',
unfollow: '펄로우 취소',
following: '펄러우 진행중',
followers: '펄로워들',
add_friend: '친구 추가',
send_message: '메시지 전송',
copy_link: '링크 복사',
show_qr_code: 'QR코드 공유',
chat_settings: '채팅 환경',
nicknames: '닉네임들',
nickname: '닉네임',
video_call: '화상채팅',
online: '온라인',
last_active: '마지막 활동',
their_nickname_for_you: '당신을 위한 그들의 닉네임',
notifications: '통보',
all_messages: '모든 메시지',
mentions_only: '단지 멘션',
nothing: '존대하지 않음',
delete_chat: '책 삭제',
block_user: '사용자 거부',
typing: '타이핑 중...',
attachment: '첨부',
note_to_self: '자신에게 메모',
today: '오늘',
yesterday: '어제',
copied: '복사완료',
answer: '대답',
reject: '거부',
incoming_call: '전화 수신',
call_rejected: '응답 거부',
close: '닫기',
call_ended: '통화 종료',
calling: '통화',
on_call_with: '통화 상대',
delete: '삭제',
confirm_delete_msg: '메시지 삭제?',
search: '검색',
email_optional: '이메일 (옵션)',
delivery: '전달',
address: '주소',
confirmation: '확인',
payment_method: '지불 방법',
summary: '요약',
download_webtorrent:
'다운로드 <a>Webtorrent Desktop</a> 미디어 파일을호스트 하고 붙여 넣기 위하여 마그넷링크 아래 제공.',
visibility: '당신의 게시물, 답변과 좋아요는 당신의 펄로워들과 네트워크에만 보여짐.',
iris_is_like: 'Iris는 소셜네트워킹 앱이지만 더 향상된 기능임.',
this_is_a_prototype_store:
'ㅇ본 화면은 시범 상점이며 당신의 소셜네트워크의 판매점을 보여 줌. 주문은 Iris 개인 메시지로 함. 당신의 상점은 여기에 볼 수 있음. <a>여기</a>.',
add_to_cart: '카트에 추가',
web_push_subscriptions: '웹 푸시 구독',
enable_public_peer_discovery: '공개 친구 찾기 동작',
set_up_your_own_peer: '당신의 자체 친구 설정',
also: '그리고',
or_small: '또는',
automatically_load_webtorrent_attachments: '자동적으로 webtorrent 첨부 파일 로드',
autoplay_webtorrent_videos: '자동으로 webtorrent 비디오 시작',
no_contacts_in_list: '더 이상 친구 리스트가 없음.',
};

View File

@ -1,100 +1,110 @@
export default {
whats_your_name: "Qual o seu nome?",
new_user_go: "Vai",
already_have_an_account: "Já tem uma conta?",
back: "Voltar",
scan_private_key_qr_code: "Ler QR code da chave privada",
paste_private_key: "Cole a chave privada",
get_notified_new_messages: "Ser notificado de novas mensagens",
turn_on_desktop_notifications: "Ativar notificações no desktop",
new_chat: "Nova conversa",
if_other_person_doesnt_see_message: "Se a outra pessoa não vê sua mensagens, você pode passar pra ela o <b>seu</b> link do bate-papo através de outro canal.",
type_a_message: "Digite uma mensagem",
beware_of_sharing_invite_link_publicly: "Cuidado ao compartilhar seu link de bate-papo publicamente: você pode receber spam com solicitações de mensagem. Ao invés disso, compartilhe publicamente seu <a>Link do perfil</a>.",
your_invite_links: "Seus links de bate-papo",
create_new_invite_link: "Criar um novo link de bate-papo",
copy_your_invite_link: "Copiar seu link de bate-papo",
have_someones_invite_link: "Tem o link de bate-papo de alguém?",
paste_their_invite_link: "Cole o link da outra pessoa",
give_your_invite_link: "Forneça seu link de bate-papo:",
or_scan_qr_code: "Ou leia o QR code",
or_show_qr_code: "Ou mostre seu QR code",
new_group: "Novo grupo",
group_name: "Nome do grupo",
create: "Criar",
profile: "Perfil",
settings: "Configurações",
your_name: "Seu nome",
profile_photo: "Foto do perfil",
add_profile_photo: "Adicionar foto do perfil",
profile_photo_too_big: "Foto do perfil muito grande: tamanho máximo é 200KB",
cancel: "Cancelar",
use_photo: "Usar foto",
remove_photo: "Remover foto",
about_text: "Texto sobre",
account: "Conta",
save_backup_of_privkey_first: "Faça backup da sua chave privada primeiro!",
otherwise_cant_log_in_again: "Caso contrário você não vai conseguir acessar sua conta novamente.",
log_out: "Sair",
private_key: "Chave privada",
download_private_key: "Baixar chave privada",
private_key_warning: "<b>PERIGO!</b> A chave privada é usada para <b>acessar sua conta</b>. Não compartilhe ou mostre sua chave privada para ninguém!",
copy_private_key: "Copiar chave privada",
show_privkey_qr: "Mostrar QR code da chave privada",
hide_privkey_qr: "Esconder QR code da chave privada",
privkey_storage_recommendation: "O lugar mais seguro para armazenar sua chave privada é um <b>gerenciardor de senhas</b>.",
language: "Idioma",
peers: "Peers",
peer_url: "Peer url",
public: "Público",
remove: "Remover",
enable: "Ativar",
disable: "Desativar",
from: "de",
add: "Adicionar",
public_peer_info: "<i>Peers</i> públicos são encontrados automaticamente por pessoas que você bate-papo (e outros).",
peers_info: "Peers são nós do GunDB que você pode facilmente <a>trocar</a>. Novidade pela frente: conexão direta com seus amigos utilizando WebRTC.",
webrtc_connection_options: "Opções de conexão WebRTC",
webrtc_info: "WebRTC é utilizado para chamadas de vídeo. Se você está atrás de um NAT, você precisa especificar aqui um servidor TURN que vai retransmitir o tráfego de vídeo. A largura de banda não é gratuita, portanto, não há servidores TURN gratuitos disponíveis.",
restore_defaults: "Restaurar padrões",
about: "Sobre",
application_security_warning: "O aplicativo é uma implementação de prova de conceito não auditada; portanto, não o use para fins críticos de segurança.",
donate: "Doe",
donate_info: "Ajude a manter o projeto com <b>doações</b>, nós agrademos muito. Você pode doar por <a>Open Collective</a> ou <b>bitcoin</b>",
logout_confirmation_info: "Você <b>não pode logar novamente</b> ao menos que você tenha salvado uma cópia da sua chave primária.",
participants: "participantes",
admin: "administração",
add_participant: "Adicionar participante",
new_participants_profile_link: "Link do bate-papo do novo participante.",
add_friend: "Adicionar amigo",
send_message: "Enviar mensagem",
copy_link: "Copiar link",
chat_settings: "Configurações do bate-papo",
nicknames: "Apelidos",
nickname: "Apelido",
video_call: "Chamada de vídeo",
online: "online",
last_active: "última atividade",
their_nickname_for_you: "Apelido deles para você",
notifications: "Notificações",
all_messages: "Todas mensagens",
mentions_only: "Somente menções",
nothing: "Nada",
delete_chat: "Apagar bate-papo",
block_user: "Bloquear usuário",
typing: "Digitando...",
attachment: "anexo",
note_to_self: "Nota pessoal",
today: 'hoje',
yesterday: 'ontem',
copied: "Copiado",
answer: "resposta",
reject: "rejeitado",
incoming_call: "Chamada entrante",
call_rejected: "Chamada rejeitada",
close: "Fechar",
call_ended: "Chamada encerrada",
calling: "Chamando",
on_call_with: "Em chamada com",
no_contacts_in_list: 'Você não tem nenhum contato na sua lista.'
whats_your_name: 'Qual o seu nome?',
new_user_go: 'Vai',
already_have_an_account: 'Já tem uma conta?',
back: 'Voltar',
scan_private_key_qr_code: 'Ler QR code da chave privada',
paste_private_key: 'Cole a chave privada',
get_notified_new_messages: 'Ser notificado de novas mensagens',
turn_on_desktop_notifications: 'Ativar notificações no desktop',
new_chat: 'Nova conversa',
if_other_person_doesnt_see_message:
'Se a outra pessoa não vê sua mensagens, você pode passar pra ela o <b>seu</b> link do bate-papo através de outro canal.',
type_a_message: 'Digite uma mensagem',
beware_of_sharing_invite_link_publicly:
'Cuidado ao compartilhar seu link de bate-papo publicamente: você pode receber spam com solicitações de mensagem. Ao invés disso, compartilhe publicamente seu <a>Link do perfil</a>.',
your_invite_links: 'Seus links de bate-papo',
create_new_invite_link: 'Criar um novo link de bate-papo',
copy_your_invite_link: 'Copiar seu link de bate-papo',
have_someones_invite_link: 'Tem o link de bate-papo de alguém?',
paste_their_invite_link: 'Cole o link da outra pessoa',
give_your_invite_link: 'Forneça seu link de bate-papo:',
or_scan_qr_code: 'Ou leia o QR code',
or_show_qr_code: 'Ou mostre seu QR code',
new_group: 'Novo grupo',
group_name: 'Nome do grupo',
create: 'Criar',
profile: 'Perfil',
settings: 'Configurações',
your_name: 'Seu nome',
profile_photo: 'Foto do perfil',
add_profile_photo: 'Adicionar foto do perfil',
profile_photo_too_big: 'Foto do perfil muito grande: tamanho máximo é 200KB',
cancel: 'Cancelar',
use_photo: 'Usar foto',
remove_photo: 'Remover foto',
about_text: 'Texto sobre',
account: 'Conta',
save_backup_of_privkey_first: 'Faça backup da sua chave privada primeiro!',
otherwise_cant_log_in_again: 'Caso contrário você não vai conseguir acessar sua conta novamente.',
log_out: 'Sair',
private_key: 'Chave privada',
download_private_key: 'Baixar chave privada',
private_key_warning:
'<b>PERIGO!</b> A chave privada é usada para <b>acessar sua conta</b>. Não compartilhe ou mostre sua chave privada para ninguém!',
copy_private_key: 'Copiar chave privada',
show_privkey_qr: 'Mostrar QR code da chave privada',
hide_privkey_qr: 'Esconder QR code da chave privada',
privkey_storage_recommendation:
'O lugar mais seguro para armazenar sua chave privada é um <b>gerenciardor de senhas</b>.',
language: 'Idioma',
peers: 'Peers',
peer_url: 'Peer url',
public: 'Público',
remove: 'Remover',
enable: 'Ativar',
disable: 'Desativar',
from: 'de',
add: 'Adicionar',
public_peer_info:
'<i>Peers</i> públicos são encontrados automaticamente por pessoas que você bate-papo (e outros).',
peers_info:
'Peers são nós do GunDB que você pode facilmente <a>trocar</a>. Novidade pela frente: conexão direta com seus amigos utilizando WebRTC.',
webrtc_connection_options: 'Opções de conexão WebRTC',
webrtc_info:
'WebRTC é utilizado para chamadas de vídeo. Se você está atrás de um NAT, você precisa especificar aqui um servidor TURN que vai retransmitir o tráfego de vídeo. A largura de banda não é gratuita, portanto, não há servidores TURN gratuitos disponíveis.',
restore_defaults: 'Restaurar padrões',
about: 'Sobre',
application_security_warning:
'O aplicativo é uma implementação de prova de conceito não auditada; portanto, não o use para fins críticos de segurança.',
donate: 'Doe',
donate_info:
'Ajude a manter o projeto com <b>doações</b>, nós agrademos muito. Você pode doar por <a>Open Collective</a> ou <b>bitcoin</b>',
logout_confirmation_info:
'Você <b>não pode logar novamente</b> ao menos que você tenha salvado uma cópia da sua chave primária.',
participants: 'participantes',
admin: 'administração',
add_participant: 'Adicionar participante',
new_participants_profile_link: 'Link do bate-papo do novo participante.',
add_friend: 'Adicionar amigo',
send_message: 'Enviar mensagem',
copy_link: 'Copiar link',
chat_settings: 'Configurações do bate-papo',
nicknames: 'Apelidos',
nickname: 'Apelido',
video_call: 'Chamada de vídeo',
online: 'online',
last_active: 'última atividade',
their_nickname_for_you: 'Apelido deles para você',
notifications: 'Notificações',
all_messages: 'Todas mensagens',
mentions_only: 'Somente menções',
nothing: 'Nada',
delete_chat: 'Apagar bate-papo',
block_user: 'Bloquear usuário',
typing: 'Digitando...',
attachment: 'anexo',
note_to_self: 'Nota pessoal',
today: 'hoje',
yesterday: 'ontem',
copied: 'Copiado',
answer: 'resposta',
reject: 'rejeitado',
incoming_call: 'Chamada entrante',
call_rejected: 'Chamada rejeitada',
close: 'Fechar',
call_ended: 'Chamada encerrada',
calling: 'Chamando',
on_call_with: 'Em chamada com',
no_contacts_in_list: 'Você não tem nenhum contato na sua lista.',
};

View File

@ -1,104 +1,113 @@
export default {
whats_your_name: "Как вас зовут?",
new_user_go: "Поехали!",
already_have_an_account: "Уже есть аккаунт?",
back: "Назад",
scan_private_key_qr_code: "Отсканируйте QR-код приватного ключа",
paste_private_key: "Вставьте приватный ключ",
get_notified_new_messages: "Узнавайте о новых сообщениях",
turn_on_desktop_notifications: "Включить уведомления на рабочем столе",
new_chat: "Новый чат",
if_other_person_doesnt_see_message: "Если собеседник не видит сообщение, вы можете дать <b>вашу</b> ссылку на чат через другой канал связи:",
type_a_message: "Напишите сообщение",
beware_of_sharing_invite_link_publicly: "Аккуратно распространяйте вашу ссылку на чат: вас могут заспамить сообщениями. Лучше расшарьте ссылку на <a>ваш профиль</a>.",
your_invite_links: "Ссылки на ваши чаты",
create_new_invite_link: "Создать новую ссылку на чат",
copy_your_invite_link: "Скопировать вашу ссылку на чат",
have_someones_invite_link: "Есть чья-то ссылка на чат?",
paste_their_invite_link: "Вставьте свою ссылку на чат",
give_your_invite_link: "Дайте свою ссылку на чат:",
or_scan_qr_code: "Или отсканируйте их QR-код",
or_show_qr_code: "Или покажите свой QR-код",
new_group: "Новая группа",
group_name: "Название группы",
create: "Создать",
settings: "Настройки",
profile: "Профиль",
your_name: "Ваше имя",
profile_photo: "Аватар",
add_profile_photo: "Добавить аватар",
profile_photo_too_big: "Аватар слишком большой: макс. размер 200KB",
cancel: "Отмена",
use_photo: "Использовать фото",
remove_photo: "Убрать фото",
about_text: "Описание",
account: "Аккаунт",
save_backup_of_privkey_first: "Сначала сохраните копию приватного ключа!",
otherwise_cant_log_in_again: "Иначе вы не сможите войти в аккаунт.",
log_out: "Выйти",
private_key: "Приватный ключ",
download_private_key: "Скачать приватный ключ",
private_key_warning: "<b>ОПАСНО!</b> Приватный ключ используется для <b>входа в ваш аккаунт</b>. Никому не показывайте ключ!",
copy_private_key: "Скопировать приватный ключ",
show_privkey_qr: "Показать QR-код приватного ключа",
hide_privkey_qr: "Спрятать QR-код приватного ключа",
privkey_storage_recommendation: "Лучшее место для хранения приватного ключа — <b>менеджер паролей</b>.",
language: "Язык",
peers: "Пиры",
peer_url: "Ссылка пира",
public: "Публичный",
remove: "Убрать",
enable: "Включить",
disable: "Выключить",
from: "от кого", // source of the peer url, from whom we got it
add: "Добавить",
public_peer_info: "<i>Публичные</i> пиры автоматически обнаруживаются людьми, с которыми вы общаетесь (и другими).",
peers_info: "Пиры — это ноды GunDB, вы можете легко <a>развернуть ноду у себя</a>. Скоро будет возможно общаться с друзьями напрямую через WebRTC.",
webrtc_connection_options: "Опции подключения через WebRTC",
webrtc_info: "WebRTC используется для видеозвонков. Если вы используете NAT, возможно вам понадобится TURN-сервер, который будет обслуживать ваш видео-трафик. Передача данных стоит денег, поэтому бесплатных TURN-серверов мы не предоставляем.",
restore_defaults: "Восстановить исходные настройки",
about: "Про Iris", // About Iris
application_security_warning: "Приложение является экспериментальным, поэтому не используйте его для критичных процессов и не храните в нем секретные данные.",
donate: "Задонатить",
donate_info: "<b>Донаты</b> помогают проекту развиваться и приветствуются. Вы можете задонатить через via <a>Open Collective</a> или прислать <b>BTC</b>",
logout_confirmation_info: "Вы <b>не сможете заново войти</b> пока не скопируете приватный ключ.",
participants: "участники",
admin: "админ",
add_participant: "Добавить участника",
new_participants_profile_link: "Ссылка на чат для нового участника",
add_friend: "Добавить друга",
send_message: "Отправить сообщение",
copy_link: "Скопировать ссылку",
chat_settings: "Настройки чата",
nicknames: "Никнеймы",
nickname: "Никнейм",
video_call: "Видео-звонок",
online: "онлайн",
last_active: "последняя активность",
their_nickname_for_you: "Их никнейм для вас",
notifications: "Уведомления",
all_messages: "Все сообщения",
mentions_only: "Только упоминания меня",
nothing: "Ничего",
delete_chat: "Удалить чат",
block_user: "Заблокировать",
typing: "Печатает...",
attachment: "вложение",
note_to_self: "Заметка для себя",
whats_your_name: 'Как вас зовут?',
new_user_go: 'Поехали!',
already_have_an_account: 'Уже есть аккаунт?',
back: 'Назад',
scan_private_key_qr_code: 'Отсканируйте QR-код приватного ключа',
paste_private_key: 'Вставьте приватный ключ',
get_notified_new_messages: 'Узнавайте о новых сообщениях',
turn_on_desktop_notifications: 'Включить уведомления на рабочем столе',
new_chat: 'Новый чат',
if_other_person_doesnt_see_message:
'Если собеседник не видит сообщение, вы можете дать <b>вашу</b> ссылку на чат через другой канал связи:',
type_a_message: 'Напишите сообщение',
beware_of_sharing_invite_link_publicly:
'Аккуратно распространяйте вашу ссылку на чат: вас могут заспамить сообщениями. Лучше расшарьте ссылку на <a>ваш профиль</a>.',
your_invite_links: 'Ссылки на ваши чаты',
create_new_invite_link: 'Создать новую ссылку на чат',
copy_your_invite_link: 'Скопировать вашу ссылку на чат',
have_someones_invite_link: 'Есть чья-то ссылка на чат?',
paste_their_invite_link: 'Вставьте свою ссылку на чат',
give_your_invite_link: 'Дайте свою ссылку на чат:',
or_scan_qr_code: 'Или отсканируйте их QR-код',
or_show_qr_code: 'Или покажите свой QR-код',
new_group: 'Новая группа',
group_name: 'Название группы',
create: 'Создать',
settings: 'Настройки',
profile: 'Профиль',
your_name: 'Ваше имя',
profile_photo: 'Аватар',
add_profile_photo: 'Добавить аватар',
profile_photo_too_big: 'Аватар слишком большой: макс. размер 200KB',
cancel: 'Отмена',
use_photo: 'Использовать фото',
remove_photo: 'Убрать фото',
about_text: 'Описание',
account: 'Аккаунт',
save_backup_of_privkey_first: 'Сначала сохраните копию приватного ключа!',
otherwise_cant_log_in_again: 'Иначе вы не сможите войти в аккаунт.',
log_out: 'Выйти',
private_key: 'Приватный ключ',
download_private_key: 'Скачать приватный ключ',
private_key_warning:
'<b>ОПАСНО!</b> Приватный ключ используется для <b>входа в ваш аккаунт</b>. Никому не показывайте ключ!',
copy_private_key: 'Скопировать приватный ключ',
show_privkey_qr: 'Показать QR-код приватного ключа',
hide_privkey_qr: 'Спрятать QR-код приватного ключа',
privkey_storage_recommendation:
'Лучшее место для хранения приватного ключа — <b>менеджер паролей</b>.',
language: 'Язык',
peers: 'Пиры',
peer_url: 'Ссылка пира',
public: 'Публичный',
remove: 'Убрать',
enable: 'Включить',
disable: 'Выключить',
from: 'от кого', // source of the peer url, from whom we got it
add: 'Добавить',
public_peer_info:
'<i>Публичные</i> пиры автоматически обнаруживаются людьми, с которыми вы общаетесь (и другими).',
peers_info:
'Пиры — это ноды GunDB, вы можете легко <a>развернуть ноду у себя</a>. Скоро будет возможно общаться с друзьями напрямую через WebRTC.',
webrtc_connection_options: 'Опции подключения через WebRTC',
webrtc_info:
'WebRTC используется для видеозвонков. Если вы используете NAT, возможно вам понадобится TURN-сервер, который будет обслуживать ваш видео-трафик. Передача данных стоит денег, поэтому бесплатных TURN-серверов мы не предоставляем.',
restore_defaults: 'Восстановить исходные настройки',
about: 'Про Iris', // About Iris
application_security_warning:
'Приложение является экспериментальным, поэтому не используйте его для критичных процессов и не храните в нем секретные данные.',
donate: 'Задонатить',
donate_info:
'<b>Донаты</b> помогают проекту развиваться и приветствуются. Вы можете задонатить через via <a>Open Collective</a> или прислать <b>BTC</b>',
logout_confirmation_info: 'Вы <b>не сможете заново войти</b> пока не скопируете приватный ключ.',
participants: 'участники',
admin: 'админ',
add_participant: 'Добавить участника',
new_participants_profile_link: 'Ссылка на чат для нового участника',
add_friend: 'Добавить друга',
send_message: 'Отправить сообщение',
copy_link: 'Скопировать ссылку',
chat_settings: 'Настройки чата',
nicknames: 'Никнеймы',
nickname: 'Никнейм',
video_call: 'Видео-звонок',
online: 'онлайн',
last_active: 'последняя активность',
their_nickname_for_you: 'Их никнейм для вас',
notifications: 'Уведомления',
all_messages: 'Все сообщения',
mentions_only: 'Только упоминания меня',
nothing: 'Ничего',
delete_chat: 'Удалить чат',
block_user: 'Заблокировать',
typing: 'Печатает...',
attachment: 'вложение',
note_to_self: 'Заметка для себя',
today: 'сегодня',
yesterday: 'вчера',
copied: "Скопировано",
answer: "ответить",
reject: "отклонить",
incoming_call: "Входящий звонок",
call_rejected: "Звонок отклонен",
close: "Закрыть",
call_ended: "Звонок завершен",
calling: "Звоню",
on_call_with: "На звонке с",
home: "Дома",
media: "Медиа",
messages: "Сообщения",
contacts: "Контакты",
explorer: "Обзор"
copied: 'Скопировано',
answer: 'ответить',
reject: 'отклонить',
incoming_call: 'Входящий звонок',
call_rejected: 'Звонок отклонен',
close: 'Закрыть',
call_ended: 'Звонок завершен',
calling: 'Звоню',
on_call_with: 'На звонке с',
home: 'Дома',
media: 'Медиа',
messages: 'Сообщения',
contacts: 'Контакты',
explorer: 'Обзор',
};

View File

@ -1,137 +1,149 @@
export default {
whats_your_name: "آپکا کیا نام ہے؟",
new_user_go: "چلو",
already_have_an_account: "پہلے ہی اکاؤنٹ موجود ہے؟",
back: "واپس",
scan_private_key_qr_code: "خفیہ QR کوڈ سکین کریں",
paste_private_key: "خفیہ کوڈ لکھیں",
get_notified_new_messages: "نۓ پیغامات کی اطلاع حاصل کریں",
turn_on_desktop_notifications: "ڈیسک ٹاپ اطلاعات حاصل کریں",
new_chat: "نیا پیغام",
public_messages: "عام پیغامات",
follow_someone_info: "کسی کے حلقہ احباب کا مواد دیکھنے کے لیے اسے فالو کریں! سفارشات:",
creator_of_this_distribution: "Iris کو بنانے والے",
no_followers_yet: "اپنی پروفایٔل کو لنک شیر کریں تاکہ دوسرے آپکو فالو کر سکیں:",
no_followers_yet_info: "آپکی پوسٹ ، جوابات اور پسند صرف آپکے حلقہ احباب کو دکھایٔی جاتی ہے",
alternatively: "متبادل",
alternatively_get_sms_verified: "متبادل طور پر <a>ایس ایم ایس تصدیق</a> کریں تاکہ لوگ آپکو ڈھونڈ سکیں",
give_your_profile_link_to_someone: "اپنی پروفایٔل کو لنک شیر کریں",
if_other_person_doesnt_see_message: "اگر دوسرا شخص آپکا پیغام نہیں دیکھ رہا، تو آپ اسے <b>اپنا</b> پروفایٔل لنک کسی دوسرے پلیٹ فارم شیر کر سکتے ہیں",
type_a_message: "پیغام لکھیے",
beware_of_sharing_invite_link_publicly: "انوایٔٹ لنک سر عام رکھنے سے بعض رہیں لوگ آپکو پیغام بھیج کر تنگ کر سکتے ہیں! اسکی جگہ <a>پروفایٔل لنک</a> شیر کریں",
your_invite_links: "آپکا انوایٔٹ لنک",
create_new_invite_link: "نیا انوایٔٹ لنک بنایٔیں",
copy_your_invite_link: "اپنا انوایٔٹ لنک بنایٔیں کاپی کریں",
have_someones_invite_link: "کیا آپکے پاس کسی کا انوایٔٹ لنک ہے؟",
paste_their_invite_link: "انکا انوایٔٹ لنک لکھیں",
give_your_invite_link: "اپنا انوایٔٹ لنک دیں:",
or_scan_qr_code: "انکا QR کوڈ سکین کریں",
or_show_qr_code: "یا پھر اپنا QR کوڈ دیکھیں",
new_group: "نیا گروپ",
group_name: "گروپ کا نام",
create: "بنایٔیں",
settings: "مرمت",
profile: "پروفایٔل",
your_name: "آپکا نام",
profile_photo: "پروفایٔل تصویر",
add_profile_photo: "پروفایٔل تصویر ڈالیں",
profile_photo_too_big: "پروفایٔل تصویر بہت ہے ، سایٔز کی حد 200KB ہے ",
cancel: "ختم",
use_photo: "تصویر استعمال کریں",
remove_photo: "تصویر ہٹایٔیں",
about_text: "آپکی تعریف",
account: "اکاؤنٹ",
save_backup_of_privkey_first: "پہلے اپنا پریٔیوٹ کوڈ محفوظ کرلیں!",
otherwise_cant_log_in_again: "ورنہ آپ دوبارہ اکاؤنٹ استعمال نہیں کر سکیں گے",
log_out: "بند کریں",
private_key: "پرایٔویٹ کوڈ",
download_private_key: "پرایٔویٹ کوڈ ڈاؤن لوڈ کریں",
private_key_warning: "<b>خطرہ</b>پرایٔویٹ کوڈ <b>آپکے اکاؤنٹ کی رسایٔ دیتا ہے </b>اپنا پرایٔویٹ کوڈ کسی کو نہ دکھایٔیں",
copy_private_key: "پرایٔویٹ کوڈ کاپی کریں",
show_privkey_qr: "پرایٔویٹ کوڈ کا QR کوڈ دکھایٔیں",
hide_privkey_qr: "پرایٔویٹ کوڈ کا QR کوڈ چھپایٔیں",
privkey_storage_recommendation: "پرایٔویٹ کوڈ کو رکھنے کی محفوظ جگہ <b>پاس ورڈ مینیجر ہے</b>.",
language: "زبان",
peers: "دوست",
peer_url: "دوست کا لنک",
public: "عام",
remove: "ہٹایٔں",
enable: "چلایٔں",
disable: "روکیں",
from: "کی طرف سے", // source of the peer url, from whom we got it
add: "شامل کریں",
public_peer_info: "<i>عام</i> دوست خود بخود آپسے بات کرنے والوں اور دوسرے لوگوں کو نظر آ جاتے ہیں",
peers_info: "دوست GunDB کے نقتے ہیں جن کو آپ آسانی سے <a>گھما</a> سکتے ہیں۔ جلد ہی WebRTC کے ذریعے دوستوں کے ساتھ براہ راست رابطہ ہوگا",
webrtc_connection_options: "WebRTC کے آپشن",
webrtc_info: "WebRTC ویڈیو کال کے لیے استعمال ہوتا ہے ۔ اگر آپ NAT استعمال کرتے ہیں تو آپکو ایک ٹرن سرور بنانا ہوگا جو کے آپکی ویڈیو ٹریفک کو چلاۓ گا۔ Bandwidth مفت نہیں ہے اور نہ ہی ٹرن سرور مفت ہیں",
restore_defaults: "سب پہلے جیسا کردیں",
about: "معلومات", // About Iris
application_security_warning: "یہ ایک نامحاسبہ پروف آف کانسیپٹ عمل ہے تواسے حساس معاملات کے لیے استعمال نہ کریں",
donate: "عطیہ کریں",
donate_info: "<b>عطیات</b> پروجیکٹ کو چلانے میں مدد کرتے ہیں اور انکی قدر کی جاتی ہے۔ آپ <a>Open Collective</a> اور <b>bitcoin</b> کے ذریعے عطیات کر سکتے ہیں",
logout_confirmation_info: "اگر آپ نے پرایٔیویٹ کوڈ محفوز نہیں کیا تو آپ <b>دوبارہ لاگن نہیں کر سکتے</b>",
participants: "شرکاء",
admin: "ایڈمن",
add_participant: "شریک کریں",
new_participants_profile_link: "نۓشریک کا لنک",
invite_links: "انوایٔٹ لنک",
copy: "کاپی",
follows_you: "آپکو فالو کرتے ہیں",
follow: "فالو",
unfollow: "ان فالو",
following: "فالو کرتے ہیں",
followers: "فالورز",
add_friend: "دوست بنایٔیں",
send_message: "پیغام بھیجیں",
copy_link: "لنک کاپی کریں",
show_qr_code: "QR کوڈ دیکھیں",
chat_settings: "پیغام کی سیٹنگ",
nicknames: "نک نام",
nickname: "نک نام",
video_call: "ویڈیو کال",
online: "آن لایٔن",
last_active: "لاسٹ ایکٹیو",
their_nickname_for_you: "آپ کے لیے انکا نک نام",
notifications: "نوٹیفیکیشن",
all_messages: "تمام پیغامات",
mentions_only: "صرف مینش",
nothing: "کچھ نہیں",
delete_chat: "پیغام ڈلیٹ کریں",
block_user: "بلاک کریں",
typing: "لکھ رہے ہیں...",
attachment: "اٹیچمنٹ",
note_to_self: "خود کےلیے پیغام",
whats_your_name: 'آپکا کیا نام ہے؟',
new_user_go: 'چلو',
already_have_an_account: 'پہلے ہی اکاؤنٹ موجود ہے؟',
back: 'واپس',
scan_private_key_qr_code: 'خفیہ QR کوڈ سکین کریں',
paste_private_key: 'خفیہ کوڈ لکھیں',
get_notified_new_messages: 'نۓ پیغامات کی اطلاع حاصل کریں',
turn_on_desktop_notifications: 'ڈیسک ٹاپ اطلاعات حاصل کریں',
new_chat: 'نیا پیغام',
public_messages: 'عام پیغامات',
follow_someone_info: 'کسی کے حلقہ احباب کا مواد دیکھنے کے لیے اسے فالو کریں! سفارشات:',
creator_of_this_distribution: 'Iris کو بنانے والے',
no_followers_yet: 'اپنی پروفایٔل کو لنک شیر کریں تاکہ دوسرے آپکو فالو کر سکیں:',
no_followers_yet_info: 'آپکی پوسٹ ، جوابات اور پسند صرف آپکے حلقہ احباب کو دکھایٔی جاتی ہے',
alternatively: 'متبادل',
alternatively_get_sms_verified:
'متبادل طور پر <a>ایس ایم ایس تصدیق</a> کریں تاکہ لوگ آپکو ڈھونڈ سکیں',
give_your_profile_link_to_someone: 'اپنی پروفایٔل کو لنک شیر کریں',
if_other_person_doesnt_see_message:
'اگر دوسرا شخص آپکا پیغام نہیں دیکھ رہا، تو آپ اسے <b>اپنا</b> پروفایٔل لنک کسی دوسرے پلیٹ فارم شیر کر سکتے ہیں',
type_a_message: 'پیغام لکھیے',
beware_of_sharing_invite_link_publicly:
'انوایٔٹ لنک سر عام رکھنے سے بعض رہیں لوگ آپکو پیغام بھیج کر تنگ کر سکتے ہیں! اسکی جگہ <a>پروفایٔل لنک</a> شیر کریں',
your_invite_links: 'آپکا انوایٔٹ لنک',
create_new_invite_link: 'نیا انوایٔٹ لنک بنایٔیں',
copy_your_invite_link: 'اپنا انوایٔٹ لنک بنایٔیں کاپی کریں',
have_someones_invite_link: 'کیا آپکے پاس کسی کا انوایٔٹ لنک ہے؟',
paste_their_invite_link: 'انکا انوایٔٹ لنک لکھیں',
give_your_invite_link: 'اپنا انوایٔٹ لنک دیں:',
or_scan_qr_code: 'انکا QR کوڈ سکین کریں',
or_show_qr_code: 'یا پھر اپنا QR کوڈ دیکھیں',
new_group: 'نیا گروپ',
group_name: 'گروپ کا نام',
create: 'بنایٔیں',
settings: 'مرمت',
profile: 'پروفایٔل',
your_name: 'آپکا نام',
profile_photo: 'پروفایٔل تصویر',
add_profile_photo: 'پروفایٔل تصویر ڈالیں',
profile_photo_too_big: 'پروفایٔل تصویر بہت ہے ، سایٔز کی حد 200KB ہے ',
cancel: 'ختم',
use_photo: 'تصویر استعمال کریں',
remove_photo: 'تصویر ہٹایٔیں',
about_text: 'آپکی تعریف',
account: 'اکاؤنٹ',
save_backup_of_privkey_first: 'پہلے اپنا پریٔیوٹ کوڈ محفوظ کرلیں!',
otherwise_cant_log_in_again: 'ورنہ آپ دوبارہ اکاؤنٹ استعمال نہیں کر سکیں گے',
log_out: 'بند کریں',
private_key: 'پرایٔویٹ کوڈ',
download_private_key: 'پرایٔویٹ کوڈ ڈاؤن لوڈ کریں',
private_key_warning:
'<b>خطرہ</b>پرایٔویٹ کوڈ <b>آپکے اکاؤنٹ کی رسایٔ دیتا ہے </b>اپنا پرایٔویٹ کوڈ کسی کو نہ دکھایٔیں',
copy_private_key: 'پرایٔویٹ کوڈ کاپی کریں',
show_privkey_qr: 'پرایٔویٹ کوڈ کا QR کوڈ دکھایٔیں',
hide_privkey_qr: 'پرایٔویٹ کوڈ کا QR کوڈ چھپایٔیں',
privkey_storage_recommendation: 'پرایٔویٹ کوڈ کو رکھنے کی محفوظ جگہ <b>پاس ورڈ مینیجر ہے</b>.',
language: 'زبان',
peers: 'دوست',
peer_url: 'دوست کا لنک',
public: 'عام',
remove: 'ہٹایٔں',
enable: 'چلایٔں',
disable: 'روکیں',
from: 'کی طرف سے', // source of the peer url, from whom we got it
add: 'شامل کریں',
public_peer_info:
'<i>عام</i> دوست خود بخود آپسے بات کرنے والوں اور دوسرے لوگوں کو نظر آ جاتے ہیں',
peers_info:
'دوست GunDB کے نقتے ہیں جن کو آپ آسانی سے <a>گھما</a> سکتے ہیں۔ جلد ہی WebRTC کے ذریعے دوستوں کے ساتھ براہ راست رابطہ ہوگا',
webrtc_connection_options: 'WebRTC کے آپشن',
webrtc_info:
'WebRTC ویڈیو کال کے لیے استعمال ہوتا ہے ۔ اگر آپ NAT استعمال کرتے ہیں تو آپکو ایک ٹرن سرور بنانا ہوگا جو کے آپکی ویڈیو ٹریفک کو چلاۓ گا۔ Bandwidth مفت نہیں ہے اور نہ ہی ٹرن سرور مفت ہیں',
restore_defaults: 'سب پہلے جیسا کردیں',
about: 'معلومات', // About Iris
application_security_warning:
'یہ ایک نامحاسبہ پروف آف کانسیپٹ عمل ہے تواسے حساس معاملات کے لیے استعمال نہ کریں',
donate: 'عطیہ کریں',
donate_info:
'<b>عطیات</b> پروجیکٹ کو چلانے میں مدد کرتے ہیں اور انکی قدر کی جاتی ہے۔ آپ <a>Open Collective</a> اور <b>bitcoin</b> کے ذریعے عطیات کر سکتے ہیں',
logout_confirmation_info:
'اگر آپ نے پرایٔیویٹ کوڈ محفوز نہیں کیا تو آپ <b>دوبارہ لاگن نہیں کر سکتے</b>',
participants: 'شرکاء',
admin: 'ایڈمن',
add_participant: 'شریک کریں',
new_participants_profile_link: 'نۓشریک کا لنک',
invite_links: 'انوایٔٹ لنک',
copy: 'کاپی',
follows_you: 'آپکو فالو کرتے ہیں',
follow: 'فالو',
unfollow: 'ان فالو',
following: 'فالو کرتے ہیں',
followers: 'فالورز',
add_friend: 'دوست بنایٔیں',
send_message: 'پیغام بھیجیں',
copy_link: 'لنک کاپی کریں',
show_qr_code: 'QR کوڈ دیکھیں',
chat_settings: 'پیغام کی سیٹنگ',
nicknames: 'نک نام',
nickname: 'نک نام',
video_call: 'ویڈیو کال',
online: 'آن لایٔن',
last_active: 'لاسٹ ایکٹیو',
their_nickname_for_you: 'آپ کے لیے انکا نک نام',
notifications: 'نوٹیفیکیشن',
all_messages: 'تمام پیغامات',
mentions_only: 'صرف مینش',
nothing: 'کچھ نہیں',
delete_chat: 'پیغام ڈلیٹ کریں',
block_user: 'بلاک کریں',
typing: 'لکھ رہے ہیں...',
attachment: 'اٹیچمنٹ',
note_to_self: 'خود کےلیے پیغام',
today: 'آج',
yesterday: 'کل',
copied: "کاپی شدہ",
answer: "جواب",
reject: "رد",
incoming_call: "کال آ رہی ہے",
call_rejected: "کال کاٹ دی",
close: "بند کریں",
call_ended: "کال ختم",
calling: "کال ہو رہی",
on_call_with: "کے ساتھ کال پر ہیں",
copied: 'کاپی شدہ',
answer: 'جواب',
reject: 'رد',
incoming_call: 'کال آ رہی ہے',
call_rejected: 'کال کاٹ دی',
close: 'بند کریں',
call_ended: 'کال ختم',
calling: 'کال ہو رہی',
on_call_with: 'کے ساتھ کال پر ہیں',
delete: 'ڈلیٹ کریں',
confirm_delete_msg: 'پیغام ڈلیٹ کریں؟',
search: 'تلاش کریں',
email_optional: "ای میل (مرضی ہے)",
delivery: "ترصیل",
address: "پتہ",
confirmation: "تصدیق",
payment_method: "پیمنٹ میتھڈ",
summary: "سمری",
download_webtorrent: "میڈیا فایٔلوں کو ہوسٹ کرنے اور انکے میڈیا لنک پیسٹ کرنے کے لیے <a>Webtorrent Desktop</a> ڈاؤنلوڈ کریں",
visibility: "آپکے پوسٹ ، جوابات اور لایٔک صرف آپکے فالورز اور انکے نیٹ ورک کو نظر آنی ہیں",
iris_is_like: "Iris ہماری پسندیدہ سوشل نیٹ ورک ایپس کی طرح ہے مگر ان سے بہترہے",
this_is_a_prototype_store: "یہ ایک پروٹوٹایٔپ سٹور ہے جو آپکے نیٹورک میں موجود تاجروں کی مصنوعات دکھاتا ہے۔ آرڈر پرایٔیویٹ پیغام کے ذریعے بھیجے جاتے ہیں۔آپ کے اپنے سٹور <a>یہاں</a>ملیں گے",
add_to_cart: "کارٹ میں ڈالیں",
web_push_subscriptions: "ویب پش کی سبسکرپشن",
enable_public_peer_discovery: "عام دوستوں کی معلومات چالو کریں",
set_up_your_own_peer: "اپنا peer سیٹ اپ کریں",
also: "اور",
or_small: "یا",
automatically_load_webtorrent_attachments: "خود بخود webtorrent attachments لوڈ کریں",
autoplay_webtorrent_videos: "خود بخود ویڈیو چلایٔیں",
email_optional: 'ای میل (مرضی ہے)',
delivery: 'ترصیل',
address: 'پتہ',
confirmation: 'تصدیق',
payment_method: 'پیمنٹ میتھڈ',
summary: 'سمری',
download_webtorrent:
'میڈیا فایٔلوں کو ہوسٹ کرنے اور انکے میڈیا لنک پیسٹ کرنے کے لیے <a>Webtorrent Desktop</a> ڈاؤنلوڈ کریں',
visibility: 'آپکے پوسٹ ، جوابات اور لایٔک صرف آپکے فالورز اور انکے نیٹ ورک کو نظر آنی ہیں',
iris_is_like: 'Iris ہماری پسندیدہ سوشل نیٹ ورک ایپس کی طرح ہے مگر ان سے بہترہے',
this_is_a_prototype_store:
'یہ ایک پروٹوٹایٔپ سٹور ہے جو آپکے نیٹورک میں موجود تاجروں کی مصنوعات دکھاتا ہے۔ آرڈر پرایٔیویٹ پیغام کے ذریعے بھیجے جاتے ہیں۔آپ کے اپنے سٹور <a>یہاں</a>ملیں گے',
add_to_cart: 'کارٹ میں ڈالیں',
web_push_subscriptions: 'ویب پش کی سبسکرپشن',
enable_public_peer_discovery: 'عام دوستوں کی معلومات چالو کریں',
set_up_your_own_peer: 'اپنا peer سیٹ اپ کریں',
also: 'اور',
or_small: 'یا',
automatically_load_webtorrent_attachments: 'خود بخود webtorrent attachments لوڈ کریں',
autoplay_webtorrent_videos: 'خود بخود ویڈیو چلایٔیں',
no_contacts_in_list: 'اپکے کو کونٹیکٹ نہیں ہیں',
};

View File

@ -1,103 +1,109 @@
export default {
whats_your_name: "你叫什么名字?",
new_user_go: "开始",
already_have_an_account: "已有账户?",
back: "后退",
scan_private_key_qr_code: "请扫描私钥的二维码",
paste_private_key: "请复制你的私钥",
get_notified_new_messages: "接收新消息的提醒",
turn_on_desktop_notifications: "打开桌面消息提醒",
new_chat: "新的聊天",
if_other_person_doesnt_see_message: "如果其他人无法看到你的消息,你可以把 <b>你的</b> 聊天链接在其他频道发送给他们:",
type_a_message: "请输入消息",
beware_of_sharing_invite_link_publicly: "请注意,公开分享你的聊天链接将有可能让你收到垃圾消息申请。推荐分享你的<a>账号链接</a>。",
your_invite_links: "你的聊天链接",
create_new_invite_link: "创建新的聊天链接",
copy_your_invite_link: "复制你的聊天链接",
have_someones_invite_link: "有其他人的聊天链接?",
paste_their_invite_link: "请将聊天链接粘贴在此",
give_your_invite_link: "你的聊天链接:",
or_scan_qr_code: "或扫描他们的二维码",
or_show_qr_code: "或展示你的二维码",
new_group: "新的聊天群组",
group_name: "群组名称",
create: "创建",
profile: "个人资料",
settings: "设定",
your_name: "你的名字",
profile_photo: "头像",
add_profile_photo: "添加头像",
profile_photo_too_big: "头像超过最大尺寸(200KB)",
cancel: "取消",
use_photo: "选取照片",
remove_photo: "移除照片",
about_text: "简介",
account: "账号",
save_backup_of_privkey_first: "首先请妥善保管你的私钥!",
otherwise_cant_log_in_again: "否则你将无法登录这个账号。",
log_out: "登出",
private_key: "私钥",
download_private_key: "下载私钥",
private_key_warning: "<b>危险!</b> 私钥可以用来 <b>登录你的账号</b>。 不要将你的私钥交给或展示给任何其他人!",
copy_private_key: "复制私钥",
show_privkey_qr: "显示私钥二维码",
hide_privkey_qr: "隐藏私钥二维码",
privkey_storage_recommendation: "<b>密码管理器</b>是保存私钥最安全的方式。",
language: "语言",
peers: "节点",
peer_url: "节点地址",
public: "公开",
remove: "移除",
enable: "启用",
disable: "禁用",
from: "来自于",
add: "添加",
public_peer_info: "<i>公开</i> 节点可被自动发现",
peers_info: "GunDB 的节点可以帮助<a>加速连接速度 </a>。即将支持:通过 WebRTC 直接连接聊天对象。",
webrtc_connection_options: "WebRTC 连接选项",
webrtc_info: "本应用的视频聊天使用了 WebRTC。 如果你在一个内部网络中,你可能需要手动指定一个用作转发视频流量的 Turn server。网络带宽是收费的所以我们不提供免费的 Turn server。",
restore_defaults: "恢复默认值",
about: "关于",
application_security_warning:"本应用的实现暂时还未进行代码审计,仍处在概念验证阶段,所以请大家不要在任何安全等级高的场景下使用。",
donate: "Donate",
donate_info: "<b>捐款</b>可以帮助我们继续更好地开发。 你可以通过 <a>Open Collective</a> 或<b>比特币(Bitcoin)</b>来完成捐款,非常感谢你的帮助。比特币地址",
logout_confirmation_info: "如果你不保存好你的私钥,你<b>将无法再次登录</b>。 ",
participants: "参与者",
admin: "管理员",
add_participant: "添加参与者",
new_participants_profile_link: "新加参与者的聊天链接",
add_friend: "添加好友",
send_message: "发送消息",
copy_link: "复制链接",
chat_settings: "聊天设置",
nicknames: "所有昵称",
nickname: "昵称",
video_call: "视频通话",
online: "在线",
last_active: "上次在线",
their_nickname_for_you: "其他人给你起的昵称",
notifications: "通知",
all_messages: "所有消息",
mentions_only: "只提到",
nothing: "无",
delete_chat: "删除聊天",
block_user: "屏蔽用户",
typing: "正在输入...",
attachment: "福建",
note_to_self: "个人备忘录",
whats_your_name: '你叫什么名字?',
new_user_go: '开始',
already_have_an_account: '已有账户?',
back: '后退',
scan_private_key_qr_code: '请扫描私钥的二维码',
paste_private_key: '请复制你的私钥',
get_notified_new_messages: '接收新消息的提醒',
turn_on_desktop_notifications: '打开桌面消息提醒',
new_chat: '新的聊天',
if_other_person_doesnt_see_message:
'如果其他人无法看到你的消息,你可以把 <b>你的</b> 聊天链接在其他频道发送给他们:',
type_a_message: '请输入消息',
beware_of_sharing_invite_link_publicly:
'请注意,公开分享你的聊天链接将有可能让你收到垃圾消息申请。推荐分享你的<a>账号链接</a>。',
your_invite_links: '你的聊天链接',
create_new_invite_link: '创建新的聊天链接',
copy_your_invite_link: '复制你的聊天链接',
have_someones_invite_link: '有其他人的聊天链接?',
paste_their_invite_link: '请将聊天链接粘贴在此',
give_your_invite_link: '你的聊天链接:',
or_scan_qr_code: '或扫描他们的二维码',
or_show_qr_code: '或展示你的二维码',
new_group: '新的聊天群组',
group_name: '群组名称',
create: '创建',
profile: '个人资料',
settings: '设定',
your_name: '你的名字',
profile_photo: '头像',
add_profile_photo: '添加头像',
profile_photo_too_big: '头像超过最大尺寸(200KB)',
cancel: '取消',
use_photo: '选取照片',
remove_photo: '移除照片',
about_text: '简介',
account: '账号',
save_backup_of_privkey_first: '首先请妥善保管你的私钥!',
otherwise_cant_log_in_again: '否则你将无法登录这个账号。',
log_out: '登出',
private_key: '私钥',
download_private_key: '下载私钥',
private_key_warning:
'<b>危险!</b> 私钥可以用来 <b>登录你的账号</b>。 不要将你的私钥交给或展示给任何其他人!',
copy_private_key: '复制私钥',
show_privkey_qr: '显示私钥二维码',
hide_privkey_qr: '隐藏私钥二维码',
privkey_storage_recommendation: '<b>密码管理器</b>是保存私钥最安全的方式。',
language: '语言',
peers: '节点',
peer_url: '节点地址',
public: '公开',
remove: '移除',
enable: '启用',
disable: '禁用',
from: '来自于',
add: '添加',
public_peer_info: '<i>公开</i> 节点可被自动发现',
peers_info: 'GunDB 的节点可以帮助<a>加速连接速度 </a>。即将支持:通过 WebRTC 直接连接聊天对象。',
webrtc_connection_options: 'WebRTC 连接选项',
webrtc_info:
'本应用的视频聊天使用了 WebRTC。 如果你在一个内部网络中,你可能需要手动指定一个用作转发视频流量的 Turn server。网络带宽是收费的所以我们不提供免费的 Turn server。',
restore_defaults: '恢复默认值',
about: '关于',
application_security_warning:
'本应用的实现暂时还未进行代码审计,仍处在概念验证阶段,所以请大家不要在任何安全等级高的场景下使用。',
donate: 'Donate',
donate_info:
'<b>捐款</b>可以帮助我们继续更好地开发。 你可以通过 <a>Open Collective</a> 或<b>比特币(Bitcoin)</b>来完成捐款,非常感谢你的帮助。比特币地址',
logout_confirmation_info: '如果你不保存好你的私钥,你<b>将无法再次登录</b>。 ',
participants: '参与者',
admin: '管理员',
add_participant: '添加参与者',
new_participants_profile_link: '新加参与者的聊天链接',
add_friend: '添加好友',
send_message: '发送消息',
copy_link: '复制链接',
chat_settings: '聊天设置',
nicknames: '所有昵称',
nickname: '昵称',
video_call: '视频通话',
online: '在线',
last_active: '上次在线',
their_nickname_for_you: '其他人给你起的昵称',
notifications: '通知',
all_messages: '所有消息',
mentions_only: '只提到',
nothing: '无',
delete_chat: '删除聊天',
block_user: '屏蔽用户',
typing: '正在输入...',
attachment: '福建',
note_to_self: '个人备忘录',
today: '今天',
yesterday: '昨天',
copied: "已复制",
answer: "接通",
reject: "拒绝",
incoming_call: "来电",
call_rejected: "通话被拒绝",
close: "关闭",
call_ended: "通话结束",
calling: "拨号中",
on_call_with: "通话对象:",
media: "媒体",
home: "主页",
messages: "消息",
contacts: "联系人",
copied: '已复制',
answer: '接通',
reject: '拒绝',
incoming_call: '来电',
call_rejected: '通话被拒绝',
close: '关闭',
call_ended: '通话结束',
calling: '拨号中',
on_call_with: '通话对象:',
media: '媒体',
home: '主页',
messages: '消息',
contacts: '联系人',
};

View File

@ -1,45 +1,56 @@
import {translate as t} from '../translations/Translation';
import Helpers from '../Helpers';
import Identicon from '../components/Identicon';
import FollowButton from '../components/FollowButton';
import Text from '../components/Text';
import Header from '../components/Header';
import Component from '../BaseComponent';
import FollowButton from '../components/FollowButton';
import Header from '../components/Header';
import Identicon from '../components/Identicon';
import Text from '../components/Text';
import Helpers from '../Helpers';
import { translate as t } from '../translations/Translation';
const DEVELOPER = 'hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU';
const DEVELOPER =
'hyECQHwSo7fgr2MVfPyakvayPeixxsaAWVtZ-vbaiSc.TXIp8MnCtrnW6n2MrYquWPcc-DTmZzMBmc2yaGv9gIU';
class About extends Component {
render() {
return (
<>
<>
<Header />
<div class="main-view" id="settings">
<div class="centered-container">
<h3>{t('about')}</h3>
<p>{t('iris_is_like')}</p>
<ul>
<li dangerouslySetInnerHTML={{ __html: t('iris_is_accessible')}} />
<li dangerouslySetInnerHTML={{ __html: t('iris_is_secure')}} />
<li dangerouslySetInnerHTML={{ __html: t('iris_is_always_available')}} />
<li dangerouslySetInnerHTML={{ __html: t('iris_is_accessible') }} />
<li dangerouslySetInnerHTML={{ __html: t('iris_is_secure') }} />
<li
dangerouslySetInnerHTML={{
__html: t('iris_is_always_available'),
}}
/>
</ul>
<p> {t('in_other_words')} </p>
<p>Released under MIT license. Code: <a href="https://github.com/irislib/iris-messenger">Github</a>.</p>
<p><small>Version 2.3.3</small></p>
<p>
Released under MIT license. Code:{' '}
<a href="https://github.com/irislib/iris-messenger">Github</a>.
</p>
<p>
<small>Version 2.3.3</small>
</p>
{Helpers.isElectron ? '' : (
{Helpers.isElectron ? (
''
) : (
<div id="desktop-application-about">
<h4> {t('get_the_desktop_application')} </h4>
<ul>
<li> {t('communicate_and_synchronize')} </li>
<ul>
<ul>
<li> {t('when_local_peers')} </li>
<li> {t('bluetooth_support_upcoming')} </li>
</ul>
</ul>
<li> {t('opens_to_background')} </li>
<li> {t('opens_to_background')} </li>
<li> {t('more_secure_and_available')} </li>
@ -51,12 +62,30 @@ class About extends Component {
<h4>Privacy</h4>
<p>{t('application_security_warning')}</p>
<p>Private messages are end-to-end encrypted, but message timestamps and the number of chats aren't. In a decentralized network this information is potentially available to anyone.</p>
<p>By looking at timestamps in chats, it is possible to guess who are chatting with each other. There are potential technical solutions to hiding the timestamps, but they are not implemented yet. It is also possible, if not trivial, to find out who are communicating with each other by monitoring data subscriptions on the decentralized database.</p>
<p>In that regard, Iris prioritizes decentralization and availability over perfect privacy.</p>
<p>Profile names, photos and online status are currently public. That can be changed when advanced group permissions are developed.</p>
<p>
Private messages are end-to-end encrypted, but message timestamps and the number of
chats aren't. In a decentralized network this information is potentially available to
anyone.
</p>
<p>
By looking at timestamps in chats, it is possible to guess who are chatting with each
other. There are potential technical solutions to hiding the timestamps, but they are
not implemented yet. It is also possible, if not trivial, to find out who are
communicating with each other by monitoring data subscriptions on the decentralized
database.
</p>
<p>
In that regard, Iris prioritizes decentralization and availability over perfect
privacy.
</p>
<p>
Profile names, photos and online status are currently public. That can be changed when
advanced group permissions are developed.
</p>
<p>Iris makes no guarantees of data persistence.</p>
<p>You can check your saved data in the <a href="/explorer">Explorer</a>.</p>
<p>
You can check your saved data in the <a href="/explorer">Explorer</a>.
</p>
<h4>Developer:</h4>
<div class="profile-link-container">
@ -67,14 +96,24 @@ class About extends Component {
<FollowButton id={DEVELOPER} />
</div>
<p>While we're working on Iris group chats, you're welcome to join our <a href="https://discord.gg/4CJc74JEUY">Discord</a> community.</p>
<p>
While we're working on Iris group chats, you're welcome to join our{' '}
<a href="https://discord.gg/4CJc74JEUY">Discord</a> community.
</p>
<h4>{t('donate')}</h4>
<p dangerouslySetInnerHTML={{ __html:`${t('donate_info', "href=\"https://opencollective.com/iris-social\"") }: 3GopC1ijpZktaGLXHb7atugPj9zPGyQeST` }}></p>
<p
dangerouslySetInnerHTML={{
__html: `${t(
'donate_info',
'href="https://opencollective.com/iris-social"',
)}: 3GopC1ijpZktaGLXHb7atugPj9zPGyQeST`,
}}
></p>
<p>Dogecoin: DEsgP4H1Sjp4461PugHDNnoGd6S8pTvrm1</p>
</div>
</div>
</>
</>
);
}
}

View File

@ -1,10 +1,12 @@
import { html } from 'htm/preact';
import iris from 'iris-lib';
import { route } from 'preact-router';
import SafeImg from '../components/SafeImg';
import Store from './Store';
import {translate as t} from '../translations/Translation';
import Text from '../components/Text';
import { translate as t } from '../translations/Translation';
import Store from './Store';
class Checkout extends Store {
constructor() {
@ -24,53 +26,67 @@ class Checkout extends Store {
const pub = this.props.store;
iris.session.newChannel(pub);
const cart = {};
Object.keys(this.cart).forEach(k => {
Object.keys(this.cart).forEach((k) => {
const v = this.cart[k];
v && (cart[k] = v);
});
iris.private(pub).send({
text: `New order: ${ JSON.stringify(cart) }, delivery: ${ JSON.stringify(this.state.delivery) }, payment: ${ this.state.paymentMethod}`,
order: true
text: `New order: ${JSON.stringify(cart)}, delivery: ${JSON.stringify(
this.state.delivery,
)}, payment: ${this.state.paymentMethod}`,
order: true,
});
iris.local().get('cart').get(pub).map((v, k) => {
!!v && iris.local().get('cart').get(pub).get(k).put(null);
});
route(`/chat/${ pub}`);
iris
.local()
.get('cart')
.get(pub)
.map((v, k) => {
!!v && iris.local().get('cart').get(pub).get(k).put(null);
});
route(`/chat/${pub}`);
}
renderCart() {
return html`
<h3 class="side-padding-xs">${t('shopping_cart')}</h3>
<div class="flex-table">
${Object.keys(this.cart).filter(k => !!this.cart[k] && !!this.state.items[k]).map(k => {
const i = this.state.items[k];
return html`
<div class="flex-row">
<div class="flex-cell">
<a href=${`/product/${ k }/${ this.props.store}`}>
<${SafeImg} src=${i.thumbnail}/>
${i.name || 'item'}
</a>
${Object.keys(this.cart)
.filter((k) => !!this.cart[k] && !!this.state.items[k])
.map((k) => {
const i = this.state.items[k];
return html`
<div class="flex-row">
<div class="flex-cell">
<a href=${`/product/${k}/${this.props.store}`}>
<${SafeImg} src=${i.thumbnail} />
${i.name || 'item'}
</a>
</div>
<div class="flex-cell no-flex price-cell">
<p>
<span class="unit-price">${parseInt(i.price)} </span>
<button onClick=${() => this.changeItemCount(k, -1)}>-</button>
<input
type="text"
value=${this.cart[k]}
onInput=${() => this.changeItemCount(k, null)}
/>
<button onClick=${() => this.changeItemCount(k, 1)}>+</button>
</p>
<span class="price">${parseInt(i.price) * this.cart[k]} </span>
</div>
</div>
<div class="flex-cell no-flex price-cell">
<p>
<span class="unit-price">${parseInt(i.price)} </span>
<button onClick=${() => this.changeItemCount(k, -1)}>-</button>
<input type="text" value=${this.cart[k]} onInput=${() => this.changeItemCount(k, null)}/>
<button onClick=${() => this.changeItemCount(k, 1)}>+</button>
</p>
<span class="price">${parseInt(i.price) * this.cart[k]} </span>
</div>
</div>
`;
})}
`;
})}
<div class="flex-row">
<div class="flex-cell"></div>
<div class="flex-cell no-flex"><b>${t('total')} ${this.state.totalPrice} </b></div>
<div class="flex-cell no-flex">
<b>${t('total')} ${this.state.totalPrice} </b>
</div>
</div>
</div>
<p class="side-padding-xs">
<button onClick=${() => this.setState({page:'delivery'})}>${t('next')}</button>
<button onClick=${() => this.setState({ page: 'delivery' })}>${t('next')}</button>
</p>
`;
}
@ -80,15 +96,30 @@ class Checkout extends Store {
<div class="side-padding-xs">
<h3>${t('delivery')}</h3>
<p>
<input type="text" placeholder=${t('name')} value=${this.state.delivery.name} onInput=${e => iris.local().get('delivery').get('name').put(e.target.value)}/>
<input
type="text"
placeholder=${t('name')}
value=${this.state.delivery.name}
onInput=${(e) => iris.local().get('delivery').get('name').put(e.target.value)}
/>
</p>
<p>
<input type="text" placeholder=${t('address')} value=${this.state.delivery.address} onInput=${e => iris.local().get('delivery').get('address').put(e.target.value)}/>
<input
type="text"
placeholder=${t('address')}
value=${this.state.delivery.address}
onInput=${(e) => iris.local().get('delivery').get('address').put(e.target.value)}
/>
</p>
<p>
<input type="text" placeholder=${t('email_optional')} value=${this.state.delivery.email} onInput=${e => iris.local().get('delivery').get('email').put(e.target.value)}/>
<input
type="text"
placeholder=${t('email_optional')}
value=${this.state.delivery.email}
onInput=${(e) => iris.local().get('delivery').get('email').put(e.target.value)}
/>
</p>
<button onClick=${() => this.setState({page:'payment'})}>${t('next')}</button>
<button onClick=${() => this.setState({ page: 'payment' })}>${t('next')}</button>
</div>
`;
}
@ -103,18 +134,30 @@ class Checkout extends Store {
<div class="side-padding-xs">
<h3>${t('payment_method')}:</h3>
<p>
<label for="bitcoin" onClick=${e => this.paymentMethodChanged(e)}>
<input type="radio" name="payment" id="bitcoin" value="bitcoin" checked=${this.state.paymentMethod === 'bitcoin'}/>
<label for="bitcoin" onClick=${(e) => this.paymentMethodChanged(e)}>
<input
type="radio"
name="payment"
id="bitcoin"
value="bitcoin"
checked=${this.state.paymentMethod === 'bitcoin'}
/>
Bitcoin
</label>
</p>
<p>
<label for="dogecoin" onClick=${e => this.paymentMethodChanged(e)}>
<input type="radio" name="payment" id="dogecoin" value="dogecoin" checked=${this.state.paymentMethod === 'dogecoin'}/>
<label for="dogecoin" onClick=${(e) => this.paymentMethodChanged(e)}>
<input
type="radio"
name="payment"
id="dogecoin"
value="dogecoin"
checked=${this.state.paymentMethod === 'dogecoin'}
/>
Dogecoin
</label>
</p>
<button onClick=${() => this.setState({page:'confirmation'})}>${t('next')}</button>
<button onClick=${() => this.setState({ page: 'confirmation' })}>${t('next')}</button>
</div>
`;
}
@ -123,51 +166,61 @@ class Checkout extends Store {
return html`
<h3 class="side-padding-xs">${t('confirm')}</h3>
<div class="flex-table">
${Object.keys(this.cart).filter(k => !!this.cart[k] && !!this.state.items[k]).map(k => {
const i = this.state.items[k];
return html`
<div class="flex-row">
<div class="flex-cell">
<${SafeImg} src=${i.thumbnail}/>
${i.name || 'item'}
${Object.keys(this.cart)
.filter((k) => !!this.cart[k] && !!this.state.items[k])
.map((k) => {
const i = this.state.items[k];
return html`
<div class="flex-row">
<div class="flex-cell">
<${SafeImg} src=${i.thumbnail} />
${i.name || 'item'}
</div>
<div class="flex-cell no-flex price-cell">
<p>${this.cart[k]} x ${parseInt(i.price)} </p>
<span class="price">${parseInt(i.price) * this.cart[k]} </span>
</div>
</div>
<div class="flex-cell no-flex price-cell">
<p>
${this.cart[k]} x ${parseInt(i.price)}
</p>
<span class="price">${parseInt(i.price) * this.cart[k]} </span>
</div>
</div>
`;
})}
`;
})}
<div class="flex-row">
<div class="flex-cell"></div>
<div class="flex-cell no-flex"><b>${t('total')} ${this.state.totalPrice} </b></div>
<div class="flex-cell no-flex">
<b>${t('total')} ${this.state.totalPrice} </b>
</div>
</div>
</div>
<p>
${t('delivery')}:<br/>
${this.state.delivery.name}<br/>
${this.state.delivery.address}<br/>
${t('delivery')}:<br />
${this.state.delivery.name}<br />
${this.state.delivery.address}<br />
${this.state.delivery.email}
</p>
<p>${t('payment_method')}: <b>${this.state.paymentMethod}</b></p>
<p class="side-padding-xs"><button onClick=${() => this.confirm()}>${t('confirm_button')}</button></p>
<p class="side-padding-xs">
<button onClick=${() => this.confirm()}>${t('confirm_button')}</button>
</p>
`;
}
renderCartList() {
return html`
<div class="main-view" id="profile">
return html` <div class="main-view" id="profile">
<div class="content">
<h2>${t('shopping_carts')}</h2>
${this.state.carts && Object.keys(this.state.carts).map(user => {
const cartTotalItems = Object.keys(this.state.carts[user]).reduce((sum, k) => sum + this.state.carts[user][k], 0);
if (!cartTotalItems) { return; }
${this.state.carts &&
Object.keys(this.state.carts).map((user) => {
const cartTotalItems = Object.keys(this.state.carts[user]).reduce(
(sum, k) => sum + this.state.carts[user][k],
0,
);
if (!cartTotalItems) {
return;
}
return html`
<p>
<a href="/checkout/${user}">
<${Text} path= ${t('profile_name')} user=${user} editable="false"/> (${cartTotalItems})
<${Text} path=${t('profile_name')} user=${user} editable="false" />
(${cartTotalItems})
</a>
</p>
`;
@ -191,17 +244,38 @@ class Checkout extends Store {
} else {
page = this.renderCart();
}
return html`
<div class="main-view" id="profile">
return html` <div class="main-view" id="profile">
<div class="content">
<p>
<a href="/store/${this.props.store}"><${Text} path="profile/name" user=${this.props.store}/></a>
<a href="/store/${this.props.store}"
><${Text} path="profile/name" user=${this.props.store}
/></a>
</p>
<div id="store-steps">
<div class=${p === 'cart' ? 'active' : ''} onClick=${() => this.setState({page:'cart'})}>${t('shopping_cart')}</div>
<div class=${p === 'delivery' ? 'active' : ''} onClick=${() => this.setState({page:'delivery'})}>${t('delivery')}</div>
<div class=${p === 'payment' ? 'active' : ''} onClick=${() => this.setState({page:'payment'})}>${t('payment')}</div>
<div class=${p === 'confirmation' ? 'active' : ''} onClick=${() => this.setState({page:'confirmation'})}>${t('confirm')}</div>
<div
class=${p === 'cart' ? 'active' : ''}
onClick=${() => this.setState({ page: 'cart' })}
>
${t('shopping_cart')}
</div>
<div
class=${p === 'delivery' ? 'active' : ''}
onClick=${() => this.setState({ page: 'delivery' })}
>
${t('delivery')}
</div>
<div
class=${p === 'payment' ? 'active' : ''}
onClick=${() => this.setState({ page: 'payment' })}
>
${t('payment')}
</div>
<div
class=${p === 'confirmation' ? 'active' : ''}
onClick=${() => this.setState({ page: 'confirmation' })}
>
${t('confirm')}
</div>
</div>
${page}
</div>
@ -216,18 +290,28 @@ class Checkout extends Store {
componentDidMount() {
Store.prototype.componentDidMount.call(this);
Object.values(this.eventListeners).forEach(e => e.off());
Object.values(this.eventListeners).forEach((e) => e.off());
this.eventListeners = [];
const pub = this.props.store;
this.carts = {};
if (pub) {
this.setState({page:'cart'})
iris.local().get('cart').get(pub).map((v, k) => {
this.cart[k] = v;
this.setState({cart: this.cart});
});
iris.local().get('paymentMethod').on(paymentMethod => this.setState({paymentMethod}));
iris.local().get('delivery').open(delivery => this.setState({delivery}));
this.setState({ page: 'cart' });
iris
.local()
.get('cart')
.get(pub)
.map((v, k) => {
this.cart[k] = v;
this.setState({ cart: this.cart });
});
iris
.local()
.get('paymentMethod')
.on((paymentMethod) => this.setState({ paymentMethod }));
iris
.local()
.get('delivery')
.open((delivery) => this.setState({ delivery }));
} else {
this.getAllCarts();
}

View File

@ -1,7 +1,7 @@
import iris from 'iris-lib';
import Identicon from '../components/Identicon';
import Filters from '../components/Filters';
import {translate as t} from '../translations/Translation';
import { translate as t } from '../translations/Translation';
import FollowButton from '../components/FollowButton';
import Name from '../components/Name';
import View from './View';
@ -9,13 +9,13 @@ import _ from 'lodash';
// TODO: add group selector
class Contacts extends View {
state = {sortedKeys: [], nearbyUsers: null, group: null, allContacts: {}};
id = "contacts-view";
state = { sortedKeys: [], nearbyUsers: null, group: null, allContacts: {} };
id = 'contacts-view';
shownContacts = {};
allContacts = {};
updateSortedKeys() {
const sortedKeys = Object.keys(this.shownContacts).sort((aK,bK) => {
const sortedKeys = Object.keys(this.shownContacts).sort((aK, bK) => {
const a = this.allContacts[aK];
const b = this.allContacts[bK];
if (!a && !b) return 0;
@ -32,7 +32,7 @@ class Contacts extends View {
if (!b.name) return -1;
return a.name.localeCompare(b.name);
});
this.setState({sortedKeys});
this.setState({ sortedKeys });
}
shouldComponentUpdate() {
@ -41,39 +41,60 @@ class Contacts extends View {
componentDidMount() {
this.contactsSub && this.contactsSub.off();
iris.local().get('contacts').map(this.sub((contact, k) => {
this.allContacts[k] = contact;
this.setState({allContacts: this.allContacts});
this.updateSortedKeys();
}));
iris.local().get('filters').get('group').on(this.sub(group => {
if (group === this.state.group) return;
this.shownContacts = {};
this.setState({group});
iris.local().get('groups').get(group).on(this.sub((contacts,k,x,e) => {
this.contactsSub && this.contactsSub.off();
this.contactsSub = e;
this.shownContacts = contacts;
for (let k in this.shownContacts) { // remove some invalid keys. TODO: why are they there?
if (!this.shownContacts[k] || (k.indexOf('~') === 0) || (k.length < 40)) {
delete this.shownContacts[k];
}
}
this.updateSortedKeys();
}));
}));
iris
.local()
.get('contacts')
.map(
this.sub((contact, k) => {
this.allContacts[k] = contact;
this.setState({ allContacts: this.allContacts });
this.updateSortedKeys();
}),
);
iris
.local()
.get('filters')
.get('group')
.on(
this.sub((group) => {
if (group === this.state.group) return;
this.shownContacts = {};
this.setState({ group });
iris
.local()
.get('groups')
.get(group)
.on(
this.sub((contacts, k, x, e) => {
this.contactsSub && this.contactsSub.off();
this.contactsSub = e;
this.shownContacts = contacts;
for (let k in this.shownContacts) {
// remove some invalid keys. TODO: why are they there?
if (!this.shownContacts[k] || k.indexOf('~') === 0 || k.length < 40) {
delete this.shownContacts[k];
}
}
this.updateSortedKeys();
}),
);
}),
);
iris.electron && iris.electron.get('bonjour').on(s => {
const nearbyUsers = JSON.parse(s);
console.log('nearbyUsers', nearbyUsers);
this.setState({nearbyUsers});
});
iris.electron &&
iris.electron.get('bonjour').on((s) => {
const nearbyUsers = JSON.parse(s);
console.log('nearbyUsers', nearbyUsers);
this.setState({ nearbyUsers });
});
}
renderNearbyUsers() {
return this.state.nearbyUsers.map(peer => {
return this.state.nearbyUsers.map((peer) => {
const k = peer.txt && peer.txt.user;
if (!k) { return (<p>{peer.name}</p>); }
if (!k) {
return <p>{peer.name}</p>;
}
return (
<div class="profile-link-container">
{k ? (
@ -81,16 +102,24 @@ class Contacts extends View {
<a href={`/profile/${k}`} class="profile-link">
<Identicon key="i{k}" str={k} width={49} />
<div>
<Name key="k{k}" pub={k} /><br />
<Name key="k{k}" pub={k} />
<br />
<small class="follower-count">
{peer.name}<br />
{this.shownContacts[k] && this.shownContacts[k].followers && this.shownContacts[k].followers.size || '0'} {t('followers')}
{peer.name}
<br />
{(this.shownContacts[k] &&
this.shownContacts[k].followers &&
this.shownContacts[k].followers.size) ||
'0'}{' '}
{t('followers')}
</small>
</div>
</a>
{(k !== iris.session.getPubKey()) ? (<FollowButton key="f{k}" id={k} />) : ''}
{k !== iris.session.getPubKey() ? <FollowButton key="f{k}" id={k} /> : ''}
</div>
):''}
) : (
''
)}
</div>
);
});
@ -100,39 +129,53 @@ class Contacts extends View {
const keys = this.state.sortedKeys;
if (keys.length === 0 && !this.state.nearbyUsers) {
return (
<div class="centered-container">
<Filters /><br />
{t('no_contacts_in_list')}
</div>)
<div class="centered-container">
<Filters />
<br />
{t('no_contacts_in_list')}
</div>
);
}
return (
<div class="centered-container">
<div id="contacts-list">
{(this.state.nearbyUsers) ? (
<>
<h3>Nearby users</h3>
{this.renderNearbyUsers()}
{this.state.nearbyUsers.length === 0 ? (<p></p>) : ''}
<hr /><br />
<h3>Others</h3>
</>
):''}
<Filters /><br />
{keys.map(k => {
{this.state.nearbyUsers ? (
<>
<h3>Nearby users</h3>
{this.renderNearbyUsers()}
{this.state.nearbyUsers.length === 0 ? <p></p> : ''}
<hr />
<br />
<h3>Others</h3>
</>
) : (
''
)}
<Filters />
<br />
{keys.map((k) => {
if (this.state.group !== 'everyone' && k === iris.session.getPubKey()) return;
const contact = this.state.allContacts[k] || {};
return (
<div key={k} class="profile-link-container">
<a href={`/profile/${k}`} class="profile-link">
<Identicon key={`i${k}`} str={k} width={49} />
<div>
<Name key={`k${k}`} pub={k} /><br />
<small class="follower-count">{contact.followerCount || '0'} {t('followers')}</small>
</div>
</a>
{(this.state.group !== 'follows' && k !== iris.session.getPubKey()) ? (<FollowButton key={`f${k}`} id={k} />) : ''}
</div>);
<div key={k} class="profile-link-container">
<a href={`/profile/${k}`} class="profile-link">
<Identicon key={`i${k}`} str={k} width={49} />
<div>
<Name key={`k${k}`} pub={k} />
<br />
<small class="follower-count">
{contact.followerCount || '0'} {t('followers')}
</small>
</div>
</a>
{this.state.group !== 'follows' && k !== iris.session.getPubKey() ? (
<FollowButton key={`f${k}`} id={k} />
) : (
''
)}
</div>
);
})}
{keys.length === 0 ? '—' : ''}
</div>

View File

@ -1,33 +1,50 @@
import { html } from 'htm/preact';
import iris from 'iris-lib';
import View from './View';
import ExplorerNode from '../components/ExplorerNode';
import { translate as t } from '../translations/Translation';
import Name from '../components/Name';
const pubKeyRegex = /^[A-Za-z0-9\-\_]{40,50}\.[A-Za-z0-9\_\-]{40,50}$/;
import ExplorerNode from '../components/ExplorerNode';
import Name from '../components/Name';
import { translate as t } from '../translations/Translation';
import View from './View';
const pubKeyRegex = /^[A-Za-z0-9\-_]{40,50}\.[A-Za-z0-9_-]{40,50}$/;
const chevronDown = html`
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-chevron-down" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
fill="currentColor"
class="bi bi-chevron-down"
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"
/>
</svg>
`;
const chevronRight = html`
<svg width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
<svg width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
<path
fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
/>
</svg>
`;
class Explorer extends View {
renderView() {
console.log('node', this.props.node);
const split = (this.props.node || '').split('/');
const scope = split.length && split[0];
let gun = iris.global();
if (scope === 'Local') {
gun = iris.local();
} else if (scope === 'Group') {
const group = (split.length >= 2) && split[1];
const group = split.length >= 2 && split[1];
gun = iris.group(group || undefined);
}
const isRootLevel = !split[0].length;
@ -36,40 +53,69 @@ class Explorer extends View {
const substr = k.substr(1);
const isPubKey = substr.match(pubKeyRegex);
return html`
${chevronRight} <a href="/explorer/${encodeURIComponent(split.slice(0,i+1).join('/'))}">
${isPubKey ? html`<${Name} key=${substr} pub=${substr} placeholder="profile name" />` : k}
${chevronRight}
<a href="/explorer/${encodeURIComponent(split.slice(0, i + 1).join('/'))}">
${isPubKey ? html`<${Name} key=${substr} pub=${substr} placeholder="profile name" />` : k}
</a>
${isPubKey ? html`<small> (<a href="/profile/${substr}">${t('profile')}</a>)</small>`: ''}
`
${isPubKey ? html`<small> (<a href="/profile/${substr}">${t('profile')}</a>)</small>` : ''}
`;
});
const s = this.state;
return html`
<p>
<a href="/explorer">All</a> ${split[0].length ? pathString : ''}
${isRootLevel ? html `<small class="mar-left5">Iris raw data.</small>` : ''}
${isRootLevel ? html`<small class="mar-left5">Iris raw data.</small>` : ''}
</p>
${isRootLevel ? html`
<div class="explorer-row">
<span onClick=${() => this.setState({publicOpen:!s.publicOpen})}>${s.publicOpen ? chevronDown : chevronRight}</span>
<a href="/explorer/Public"><b>Public</b></a>
<small class="mar-left5">(synced with peers)</small>
</div>
${s.publicOpen ? html`<${ExplorerNode} indent=${1} gun=${iris.global()} key='Public' path='Public'/>`:''}
<div class="explorer-row">
<span onClick=${() => this.setState({groupOpen:!s.groupOpen})}>${s.groupOpen ? chevronDown : chevronRight}</span>
<a href="/explorer/Group"><b>Group</b></a>
<small class="mar-left5">(public data, composite object of all the users in the <a href="/explorer/Local%2Fgroups">group</a>)</small>
</div>
${s.groupOpen ? html`<${ExplorerNode} indent=${1} gun=${iris.global()} key='Group' path='Group'/>`:''}
<div class="explorer-row">
<span onClick=${() => this.setState({localOpen:!s.localOpen})}>${s.localOpen ? chevronDown : chevronRight}</span>
<a href="/explorer/Local"><b>Local</b></a>
<small class="mar-left5">(only stored on your device)</small>
</div>
${s.localOpen ? html`<${ExplorerNode} indent=${1} gun=${iris.local()} key="Local" path='Local'/>`:''}
`: html`
<${ExplorerNode} indent=${0} showTools=${true} gun=${gun} key=${this.props.node} path=${this.props.node}/>
`}
${isRootLevel
? html`
<div class="explorer-row">
<span onClick=${() => this.setState({ publicOpen: !s.publicOpen })}
>${s.publicOpen ? chevronDown : chevronRight}</span
>
<a href="/explorer/Public"><b>Public</b></a>
<small class="mar-left5">(synced with peers)</small>
</div>
${s.publicOpen
? html`<${ExplorerNode}
indent=${1}
gun=${iris.global()}
key="Public"
path="Public"
/>`
: ''}
<div class="explorer-row">
<span onClick=${() => this.setState({ groupOpen: !s.groupOpen })}
>${s.groupOpen ? chevronDown : chevronRight}</span
>
<a href="/explorer/Group"><b>Group</b></a>
<small class="mar-left5"
>(public data, composite object of all the users in the
<a href="/explorer/Local%2Fgroups">group</a>)</small
>
</div>
${s.groupOpen
? html`<${ExplorerNode} indent=${1} gun=${iris.global()} key="Group" path="Group" />`
: ''}
<div class="explorer-row">
<span onClick=${() => this.setState({ localOpen: !s.localOpen })}
>${s.localOpen ? chevronDown : chevronRight}</span
>
<a href="/explorer/Local"><b>Local</b></a>
<small class="mar-left5">(only stored on your device)</small>
</div>
${s.localOpen
? html`<${ExplorerNode} indent=${1} gun=${iris.local()} key="Local" path="Local" />`
: ''}
`
: html`
<${ExplorerNode}
indent=${0}
showTools=${true}
gun=${gun}
key=${this.props.node}
path=${this.props.node}
/>
`}
`;
}
}

View File

@ -1,18 +1,20 @@
import Helmet from 'react-helmet';
import { html } from 'htm/preact';
import iris from 'iris-lib';
import FeedMessageForm from '../components/FeedMessageForm';
import MessageFeed from '../components/MessageFeed';
import Filters from '../components/Filters';
import MessageFeed from '../components/MessageFeed';
import OnboardingNotification from '../components/OnboardingNotification';
import SubscribeHashtagButton from '../components/SubscribeHashtagButton';
import View from './View';
import SubscribeHashtagButton from "../components/SubscribeHashtagButton";
import Helmet from 'react-helmet';
import OnboardingNotification from "../components/OnboardingNotification";
class Feed extends View {
constructor() {
super();
this.eventListeners = {};
this.state = {sortedMessages: [], group: "follows"};
this.state = { sortedMessages: [], group: 'follows' };
this.messages = {};
this.id = 'message-view';
this.class = 'public-messages-view';
@ -20,7 +22,7 @@ class Feed extends View {
search() {
const searchTerm = this.props.term && this.props.term.toLowerCase();
this.setState({searchTerm});
this.setState({ searchTerm });
}
componentDidUpdate(prevProps) {
@ -39,7 +41,7 @@ class Feed extends View {
filter(msg) {
if (this.state.searchTerm) {
return msg.text && (msg.text.toLowerCase().indexOf(this.state.searchTerm) > -1);
return msg.text && msg.text.toLowerCase().indexOf(this.state.searchTerm) > -1;
}
return true;
}
@ -56,28 +58,43 @@ class Feed extends View {
<div class="centered-container">
<div style="display:flex;flex-direction:row">
<div style="flex:3;width: 100%">
${hashtag ? html`
<${Helmet}>
<title>${hashtagText}</title>
<meta property="og:title" content="${hashtagText} | Iris" />
<//>
<h3>${hashtagText} <span style="float:right"><${SubscribeHashtagButton} key=${hashtag} id=${hashtag} /></span></h3>
` : ''}
${s.searchTerm ? '' : html`
<${FeedMessageForm} key="form${path}" index=${path} class="hidden-xs" autofocus=${false}/>
`}
${s.searchTerm ? html`<h2>Search results for "${s.searchTerm}"</h2>` : html`
<${OnboardingNotification} />
`}
${!s.noFollows ? html`<${Filters}/>` : ''}
${hashtag
? html`
<${Helmet}>
<title>${hashtagText}</title>
<meta property="og:title" content="${hashtagText} | Iris" />
<//>
<h3>
${hashtagText}
<span style="float:right"
><${SubscribeHashtagButton} key=${hashtag} id=${hashtag}
/></span>
</h3>
`
: ''}
${s.searchTerm
? ''
: html`
<${FeedMessageForm}
key="form${path}"
index=${path}
class="hidden-xs"
autofocus=${false}
/>
`}
${s.searchTerm
? html`<h2>Search results for "${s.searchTerm}"</h2>`
: html` <${OnboardingNotification} /> `}
${!s.noFollows ? html`<${Filters} />` : ''}
<${MessageFeed}
scrollElement=${this.scrollElement.current}
hashtag=${hashtag}
filter=${s.searchTerm && (m => this.filter(m))}
thumbnails=${this.props.thumbnails}
key=${hashtag || this.props.index || 'feed'}
group=${this.state.group}
path=${path} />
scrollElement=${this.scrollElement.current}
hashtag=${hashtag}
filter=${s.searchTerm && ((m) => this.filter(m))}
thumbnails=${this.props.thumbnails}
key=${hashtag || this.props.index || 'feed'}
group=${this.state.group}
path=${path}
/>
</div>
</div>
</div>

View File

@ -1,42 +1,59 @@
import { html } from 'htm/preact';
import iris from 'iris-lib';
import Identicon from '../components/Identicon';
import {translate as t} from '../translations/Translation';
import throttle from 'lodash/throttle';
import FollowButton from '../components/FollowButton';
import Identicon from '../components/Identicon';
import Name from '../components/Name';
import { translate as t } from '../translations/Translation';
import View from './View';
import {throttle} from 'lodash';
class Follows extends View {
constructor() {
super();
this.follows = new Set();
this.followNames = new Map();
this.id = "follows-view";
this.id = 'follows-view';
this.state = { follows: [], contacts: {} };
}
updateSortedFollows = throttle(() => {
const follows = Array.from(this.follows).sort((aK,bK) => {
const aName = this.followNames.get(aK);
const bName = this.followNames.get(bK);
if (!aName && !bName) { return aK.localeCompare(bK); }
if (!aName) { return 1; }
if (!bName) { return -1; }
return aName.localeCompare(bName);
});
this.setState({follows});
}, 1000, {leading: false});
updateSortedFollows = throttle(
() => {
const follows = Array.from(this.follows).sort((aK, bK) => {
const aName = this.followNames.get(aK);
const bName = this.followNames.get(bK);
if (!aName && !bName) {
return aK.localeCompare(bK);
}
if (!aName) {
return 1;
}
if (!bName) {
return -1;
}
return aName.localeCompare(bName);
});
this.setState({ follows });
},
1000,
{ leading: false },
);
getFollows() {
iris.public(this.props.id).get('follow').map().on(this.sub(
(follows, pub) => {
if (follows && !this.follows.has(pub)) {
this.follows.add(pub);
this.getNameForUser(pub);
}
this.updateSortedFollows();
}));
iris
.public(this.props.id)
.get('follow')
.map()
.on(
this.sub((follows, pub) => {
if (follows && !this.follows.has(pub)) {
this.follows.add(pub);
this.getNameForUser(pub);
}
this.updateSortedFollows();
}),
);
}
shouldComponentUpdate() {
@ -44,22 +61,31 @@ class Follows extends View {
}
getNameForUser(user) {
iris.public(user).get('profile').get('name').on(this.sub(name => {
if (!name) return;
this.followNames.set(user, name);
this.updateSortedFollows();
}));
iris
.public(user)
.get('profile')
.get('name')
.on(
this.sub((name) => {
if (!name) return;
this.followNames.set(user, name);
this.updateSortedFollows();
}),
);
}
getFollowers() {
iris.group().on(`follow/${this.props.id}`, this.sub((following, a, b, e, user) => {
if (following && !this.follows.has(user)) {
iris.group().on(
`follow/${this.props.id}`,
this.sub((following, a, b, e, user) => {
if (following && !this.follows.has(user)) {
if (!following) return;
this.follows.add(user);
this.getNameForUser(user);
this.updateSortedFollows();
}
}));
}
}),
);
}
componentDidMount() {
@ -72,21 +98,24 @@ class Follows extends View {
renderView() {
return html`
<div class="centered-container">
<h3><a href="/profile/${this.props.id}"><${Name} pub=${this.props.id} placeholder="—" /></a>:<i> </i>
${this.props.followers ? t('followers') : t('following')}</h3>
<h3>
<a href="/profile/${this.props.id}"><${Name} pub=${this.props.id} placeholder="—" /></a
>:<i> </i> ${this.props.followers ? t('followers') : t('following')}
</h3>
<div id="follows-list">
${this.state.follows.map(k => {
return html`
<div key=${k} class="profile-link-container">
${this.state.follows.map((k) => {
return html` <div key=${k} class="profile-link-container">
<a href="/profile/${k}" class="profile-link">
<${Identicon} str=${k} width=49/>
<${Identicon} str=${k} width="49" />
<div>
<${Name} pub=${k}/><br/>
<small class="follower-count">${this.state.contacts[k] && this.state.contacts[k].followerCount} followers</small>
<${Name} pub=${k} /><br />
<small class="follower-count"
>${this.state.contacts[k] && this.state.contacts[k].followerCount}
followers</small
>
</div>
</a>
${k !== iris.session.getPubKey() ? html`<${FollowButton} id=${k}/>` : ''}
${k !== iris.session.getPubKey() ? html`<${FollowButton} id=${k} />` : ''}
</div>`;
})}
${this.state.follows.length === 0 ? '—' : ''}

View File

@ -1,32 +1,34 @@
import { Helmet } from 'react-helmet';
import { html } from 'htm/preact';
import {translate as tr} from '../translations/Translation';
import iris from 'iris-lib';
import {Helmet} from "react-helmet";
import ProfilePhotoPicker from '../components/ProfilePhotoPicker';
import $ from 'jquery';
import { route } from 'preact-router';
import SafeImg from '../components/SafeImg';
import Button from '../components/basic/Button';
import CopyButton from '../components/CopyButton';
import Identicon from '../components/Identicon';
import Name from '../components/Name';
import View from './View';
import ProfilePhotoPicker from '../components/ProfilePhotoPicker';
import SafeImg from '../components/SafeImg';
import SearchBox from '../components/SearchBox';
import {SMS_VERIFIER_PUB} from '../SMS';
import $ from 'jquery';
import Button from '../components/basic/Button';
import { SMS_VERIFIER_PUB } from '../SMS';
import { translate as tr } from '../translations/Translation';
import View from './View';
function deleteChat(uuid) {
if (confirm("Delete chat?")) {
iris.Channel.deleteGroup(iris.session.getKey(), uuid);
iris.session.channelIds.delete(uuid);
iris.local().get('channels').get(uuid).put(null);
route('/chat');
if (confirm('Delete chat?')) {
iris.Channel.deleteGroup(iris.session.getKey(), uuid);
iris.session.channelIds.delete(uuid);
iris.local().get('channels').get(uuid).put(null);
route('/chat');
}
}
class Group extends View {
constructor() {
super();
this.id = "profile";
this.id = 'profile';
}
onProfilePhotoSet(src) {
@ -50,7 +52,7 @@ class Group extends View {
}
removeChatLink(id) {
if (confirm("Remove chat link?")) {
if (confirm('Remove chat link?')) {
iris.local().get('chatLinks').get(id).put(null);
iris.private(this.props.id).removeGroupChatLink(id);
}
@ -59,12 +61,12 @@ class Group extends View {
onAddParticipant(add = true) {
add && iris.private(this.props.id).addParticipant(this.state.memberCandidate);
// send invite msg
iris.private(this.state.memberCandidate).send({invite:{group:this.props.id}});
this.setState({memberCandidate:null});
iris.private(this.state.memberCandidate).send({ invite: { group: this.props.id } });
this.setState({ memberCandidate: null });
}
onRemoveParticipant(pub) {
if (confirm("Remove participant?")) {
if (confirm('Remove participant?')) {
iris.private(this.props.id).removeParticipant(pub);
}
}
@ -76,82 +78,109 @@ class Group extends View {
<div>
<p>${tr('participants')}:</p>
<div class="flex-table">
${
chat ? Object.keys(chat.participantProfiles).map(k => {
const profile = chat.participantProfiles[k];
if (!k || !profile) {
return;
}
if (!(profile.permissions && profile.permissions.read && profile.permissions.write)) { return; }
return html`
<div class="flex-row">
<div class="flex-cell">
<div class="profile-link-container">
<a class="profile-link" onClick=${() => route(`/profile/${ k}`)}>
<${Identicon} str=${k} width=40/>
<${Name} pub=${k}/>
${profile.permissions && profile.permissions.admin ? html`
<small style="margin-left:5px">${tr('admin')}</small>
`: ''}
</a>
${chat
? Object.keys(chat.participantProfiles).map((k) => {
const profile = chat.participantProfiles[k];
if (!k || !profile) {
return;
}
if (
!(profile.permissions && profile.permissions.read && profile.permissions.write)
) {
return;
}
return html`
<div class="flex-row">
<div class="flex-cell">
<div class="profile-link-container">
<a class="profile-link" onClick=${() => route(`/profile/${k}`)}>
<${Identicon} str=${k} width="40" />
<${Name} pub=${k} />
${profile.permissions && profile.permissions.admin
? html` <small style="margin-left:5px">${tr('admin')}</small> `
: ''}
</a>
</div>
</div>
${this.state.isAdmin
? html`
<div class="flex-cell no-flex">
<${Button} onClick=${() => this.onRemoveParticipant(k)}
>${tr('remove')}<//
>
</div>
`
: ''}
</div>
${this.state.isAdmin ? html`
<div class="flex-cell no-flex">
<${Button} onClick=${() => this.onRemoveParticipant(k)}>${tr('remove')}<//>
</div>
` : ''}
</div>
`;
}) : ''
}
`;
})
: ''}
</div>
${this.state.isAdmin ? html`
<div>
<p>${tr('add_participant')}:</p>
<p>
${this.state.memberCandidate ? html`
<div class="profile-link-container"><div class="profile-link">
<${Identicon} str=${this.state.memberCandidate} width=40/>
<${Name} pub=${this.state.memberCandidate}/>
${this.state.isAdmin
? html`
<div>
<p>${tr('add_participant')}:</p>
<p>
${this.state.memberCandidate
? html`
<div class="profile-link-container">
<div class="profile-link">
<${Identicon} str=${this.state.memberCandidate} width="40" />
<${Name} pub=${this.state.memberCandidate} />
</div>
<${Button} onClick=${() => this.onAddParticipant()}>Add<//>
<${Button} onClick=${() => this.onAddParticipant(false)}>Cancel<//>
</div>
`
: html`
<${SearchBox}
onSelect=${(item) => this.setState({ memberCandidate: item.key })}
/>
`}
</p>
</div>
<${Button} onClick=${() => this.onAddParticipant()}>Add<//>
<${Button} onClick=${() => this.onAddParticipant(false)}>Cancel<//>
</div>
`: html`
<${SearchBox} onSelect=${item => this.setState({memberCandidate: item.key})}/>
`}
</p>
</div>
`: ''}
${chat && chat.inviteLinks && Object.keys(chat.inviteLinks).length ? html`
<hr/>
<p>${tr('invite_links')}</p>
<div class="flex-table">
${Object.keys(chat.inviteLinks).map(id => {
const url = chat.inviteLinks[id];
if (!url) { return; }
return html`
<div class="flex-row">
<div class="flex-cell no-flex">
<${CopyButton} copyStr=${url}/>
</div>
<div class="flex-cell">
<input type="text" value=${url} onClick=${e => $(e.target).select()}/>
</div>
${this.state.isAdmin ? html`
<div class="flex-cell no-flex">
<${Button} onClick=${() => this.removeChatLink(id)}>${tr('remove')}<//>
`
: ''}
${chat && chat.inviteLinks && Object.keys(chat.inviteLinks).length
? html`
<hr />
<p>${tr('invite_links')}</p>
<div class="flex-table">
${Object.keys(chat.inviteLinks).map((id) => {
const url = chat.inviteLinks[id];
if (!url) {
return;
}
return html`
<div class="flex-row">
<div class="flex-cell no-flex">
<${CopyButton} copyStr=${url} />
</div>
<div class="flex-cell">
<input type="text" value=${url} onClick=${(e) => $(e.target).select()} />
</div>
${this.state.isAdmin
? html`
<div class="flex-cell no-flex">
<${Button} onClick=${() => this.removeChatLink(id)}
>${tr('remove')}<//
>
</div>
`
: ''}
</div>
`: ''}
</div>
`;
})}
</div>
`: ''}
${this.state.isAdmin ? html`
<p><${Button} onClick=${() => chat.createChatLink()}>Create new invite link<//></p>
`: ''}
`;
})}
</div>
`
: ''}
${this.state.isAdmin
? html`
<p>
<${Button} onClick=${() => chat.createChatLink()}>Create new invite link<//>
</p>
`
: ''}
</div>
`;
}
@ -162,59 +191,103 @@ class Group extends View {
const editable = this.state.isAdmin;
let profilePhoto;
if (editable) {
profilePhoto = html`<${ProfilePhotoPicker} currentPhoto=${this.state.photo} placeholder=${this.props.id} callback=${src => this.onProfilePhotoSet(src)}/>`;
profilePhoto = html`<${ProfilePhotoPicker}
currentPhoto=${this.state.photo}
placeholder=${this.props.id}
callback=${(src) => this.onProfilePhotoSet(src)}
/>`;
} else if (this.state.photo) {
profilePhoto = html`<${SafeImg} class="profile-photo" src=${this.state.photo}/>`
} else {
profilePhoto = html`<${Identicon} str=${this.props.id} width=250/>`
}
profilePhoto = html`<${SafeImg} class="profile-photo" src=${this.state.photo} />`;
} else {
profilePhoto = html`<${Identicon} str=${this.props.id} width="250" />`;
}
return html`
<div class="content">
<${Helmet}><title>${this.state.name || 'Group'}</title><//>
<div class="profile-top">
<div class="profile-header">
<div class="profile-photo-container">
${profilePhoto}
</div>
<div class="profile-photo-container">${profilePhoto}</div>
<div class="profile-header-stuff">
<h3 class="profile-name" placeholder=${editable ? tr('name') : ''} contenteditable=${editable} onInput=${e => this.onNameInput(e)}>${this.state.name}</h3>
<h3
class="profile-name"
placeholder=${editable ? tr('name') : ''}
contenteditable=${editable}
onInput=${(e) => this.onNameInput(e)}
>
${this.state.name}
</h3>
<div class="profile-about hidden-xs">
<p class="profile-about-content" placeholder=${editable ? tr('about') : ''}
contenteditable=${editable} onInput=${e => this.onAboutInput(e)}>
${this.state.about}</p>
<p
class="profile-about-content"
placeholder=${editable ? tr('about') : ''}
contenteditable=${editable}
onInput=${(e) => this.onAboutInput(e)}
>
${this.state.about}
</p>
</div>
<div class="profile-actions">
${this.followedUsers && this.followedUsers.has(iris.session.getPubKey()) ? html`
<p><small>${tr('follows_you')}</small></p>
`: this.props.id === SMS_VERIFIER_PUB ? html`
<p><a href="https://iris-sms-auth.herokuapp.com/?pub=${iris.session.getPubKey()}">${tr('ask_for_verification')}</a></p>
` : ''}
<${Button} onClick=${() => route(`/chat/${ this.props.id}`)}>${tr('send_message')}<//>
<${Button} class="show-settings" onClick=${() => this.onClickSettings()}>${tr('settings')}<//>
${this.followedUsers && this.followedUsers.has(iris.session.getPubKey())
? html` <p><small>${tr('follows_you')}</small></p> `
: this.props.id === SMS_VERIFIER_PUB
? html`
<p>
<a
href="https://iris-sms-auth.herokuapp.com/?pub=${iris.session.getPubKey()}"
>${tr('ask_for_verification')}</a
>
</p>
`
: ''}
<${Button} onClick=${() => route(`/chat/${this.props.id}`)}
>${tr('send_message')}<//
>
<${Button} class="show-settings" onClick=${() => this.onClickSettings()}
>${tr('settings')}<//
>
</div>
</div>
</div>
<div class="profile-about visible-xs-flex">
<p class="profile-about-content" placeholder=${editable ? tr('about') : ''} contenteditable=${editable} onInput=${e => this.onAboutInput(e)}>${this.state.about}</p>
<p
class="profile-about-content"
placeholder=${editable ? tr('about') : ''}
contenteditable=${editable}
onInput=${(e) => this.onAboutInput(e)}
>
${this.state.about}
</p>
</div>
<div id="chat-settings" style="display:none">
<hr/>
<hr />
<h3>${tr('chat_settings')}</h3>
<div class="notification-settings">
<h4>${tr('notifications')}</h4>
<input type="radio" id="notifyAll" name="notificationPreference" value="all"/>
<label for="notifyAll">${tr('all_messages')}</label><br/>
<input type="radio" id="notifyMentionsOnly" name="notificationPreference" value="mentions"/>
<label for="notifyMentionsOnly">${tr('mentions_only')}</label><br/>
<input type="radio" id="notifyNothing" name="notificationPreference" value="nothing"/>
<label for="notifyNothing">${tr('nothing')}</label><br/><br/>
<input type="radio" id="notifyAll" name="notificationPreference" value="all" />
<label for="notifyAll">${tr('all_messages')}</label><br />
<input
type="radio"
id="notifyMentionsOnly"
name="notificationPreference"
value="mentions"
/>
<label for="notifyMentionsOnly">${tr('mentions_only')}</label><br />
<input
type="radio"
id="notifyNothing"
name="notificationPreference"
value="nothing"
/>
<label for="notifyNothing">${tr('nothing')}</label><br /><br />
</div>
<hr/>
<hr />
<p>
<${Button} class="delete-chat" onClick=${() => deleteChat(this.props.id)}>${tr('delete_chat')}<//>
<${Button} class="delete-chat" onClick=${() => deleteChat(this.props.id)}
>${tr('delete_chat')}<//
>
</p>
<hr/>
<hr />
</div>
${this.renderGroupSettings()}
@ -225,22 +298,23 @@ class Group extends View {
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.setState({isAdmin:false,uuid:null, memberCandidate:null});
this.setState({ isAdmin: false, uuid: null, memberCandidate: null });
this.componentDidMount();
}
}
groupDidMount() {
const chat = iris.private(this.props.id);
chat.on('name', name => { // TODO: this really needs unsubscribe
chat.on('name', (name) => {
// TODO: this really needs unsubscribe
if (!$('#profile .profile-name:focus').length) {
this.setState({name});
this.setState({ name });
}
});
chat.on('photo', photo => this.setState({photo}));
chat.on('about', about => {
chat.on('photo', (photo) => this.setState({ photo }));
chat.on('about', (about) => {
if (!$('#profile .profile-about-content:focus').length) {
this.setState({about});
this.setState({ about });
} else {
$('#profile .profile-about-content:not(:focus)').text(about);
}
@ -250,7 +324,7 @@ class Group extends View {
componentDidMount() {
const pub = this.props.id;
console.log(this.props.id, 2);
this.setState({name: '', photo: '', about: ''});
this.setState({ name: '', photo: '', about: '' });
const chat = iris.private(pub);
if (pub.length < 40) {
if (!chat) {
@ -262,17 +336,30 @@ class Group extends View {
}, 1000);
}
}
iris.local().get('inviteLinksChanged').on(() => this.setState({inviteLinksChanged: !this.state.inviteLinksChanged}));
iris.local().get('channels').get(this.props.id).get('participants').on(participants => {
const isAdmin = areWeAdmin(pub);
this.setState({isAdmin, participants});
});
iris
.local()
.get('inviteLinksChanged')
.on(() => this.setState({ inviteLinksChanged: !this.state.inviteLinksChanged }));
iris
.local()
.get('channels')
.get(this.props.id)
.get('participants')
.on((participants) => {
const isAdmin = areWeAdmin(pub);
this.setState({ isAdmin, participants });
});
if (chat) {
this.groupDidMount();
$(`input[name=notificationPreference][value=${ chat.notificationSetting }]`).attr('checked', 'checked');
$('input:radio[name=notificationPreference]').off().on('change', (event) => {
chat.put('notificationSetting', event.target.value);
});
$(`input[name=notificationPreference][value=${chat.notificationSetting}]`).attr(
'checked',
'checked',
);
$('input:radio[name=notificationPreference]')
.off()
.on('change', (event) => {
chat.put('notificationSetting', event.target.value);
});
}
}
}

View File

@ -1,7 +1,9 @@
import { html } from 'htm/preact';
import iris from 'iris-lib';
import Filters from '../components/Filters';
import {translate as t} from '../translations/Translation';
import { translate as t } from '../translations/Translation';
import View from './View';
export default class Hashtags extends View {
@ -20,16 +22,16 @@ export default class Hashtags extends View {
<div class="centered-container">
<h3>${t('hashtags')}</h3>
<${Filters} />
${Object.keys(this.hashtags).length === 0 ? html`
<p>No hashtags yet</p>
`:''}
${Object.keys(this.hashtags).sort().map(hashtag => {
return html`
<p>
${Object.keys(this.hashtags).length === 0 ? html` <p>No hashtags yet</p> ` : ''}
${Object.keys(this.hashtags)
.sort()
.map((hashtag) => {
return html`
<p>
<a href="/hashtag/${hashtag}">#${hashtag}</a>
</p>
`;
})}
</p>
`;
})}
</div>
`;
}

View File

@ -5,10 +5,10 @@ import QRScanner from '../QRScanner';
import { Component } from 'preact';
import logo from '../../assets/img/android-chrome-192x192.png';
import Button from '../components/basic/Button';
import {ec as EC} from "elliptic";
import WalletConnectProvider from "@walletconnect/web3-provider";
import Web3Modal from "web3modal";
import Web3 from "web3";
import { ec as EC } from 'elliptic';
import WalletConnectProvider from '@walletconnect/web3-provider';
import Web3Modal from 'web3modal';
import Web3 from 'web3';
import iris from 'iris-lib';
import _ from 'lodash';
import { route } from 'preact-router';
@ -21,19 +21,22 @@ const hexToUint8Array = (hexString) => {
throw new Error('Not a hex string');
}
return Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
}
};
function arrayToBase64Url(array) {
return btoa(String.fromCharCode.apply(null, array)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
return btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
function keyPairFromHash(hash) {
const ec = new EC('p256');
const keyPair = ec.keyFromPrivate(new Uint8Array(hash));
let privKey = keyPair.getPrivate().toArray("be", 32);
let x = keyPair.getPublic().getX().toArray("be", 32);
let y = keyPair.getPublic().getY().toArray("be", 32);
let privKey = keyPair.getPrivate().toArray('be', 32);
let x = keyPair.getPublic().getX().toArray('be', 32);
let y = keyPair.getPublic().getY().toArray('be', 32);
privKey = arrayToBase64Url(privKey);
x = arrayToBase64Url(x);
@ -48,14 +51,14 @@ async function ethereumConnect() {
walletconnect: {
package: WalletConnectProvider,
options: {
infuraId: "4bd8d95876de48e0b17d56c0da31880a"
}
}
infuraId: '4bd8d95876de48e0b17d56c0da31880a',
},
},
};
const web3Modal = new Web3Modal({
network: "mainnet",
network: 'mainnet',
providerOptions,
theme: "dark"
theme: 'dark',
});
web3Modal.on('accountsChanged', (provider) => {
@ -73,8 +76,12 @@ function maybeGoToChat(key) {
if (inviter !== key.pub) {
iris.session.newChannel(chatId, window.location.href);
}
_.defer(() => route(`/chat/${ chatId}`)); // defer because router is only initialised after login // TODO fix
window.history.pushState({}, "Iris Chat", `/${window.location.href.substring(window.location.href.lastIndexOf('/') + 1).split("?")[0]}`); // remove param
_.defer(() => route(`/chat/${chatId}`)); // defer because router is only initialised after login // TODO fix
window.history.pushState(
{},
'Iris Chat',
`/${window.location.href.substring(window.location.href.lastIndexOf('/') + 1).split('?')[0]}`,
); // remove param
}
if (chatId) {
if (inviter) {
@ -95,7 +102,8 @@ async function ethereumLogin(name) {
const accounts = await web3.eth.getAccounts();
if (accounts.length > 0) {
const message = "I'm trusting this application with an irrevocable access key to my Iris account.";
const message =
"I'm trusting this application with an irrevocable access key to my Iris account.";
const signature = await web3.eth.personal.sign(message, accounts[0]);
const signatureBytes = hexToUint8Array(signature.substring(2));
const hash1 = await window.crypto.subtle.digest('SHA-256', signatureBytes);
@ -106,17 +114,21 @@ async function ethereumLogin(name) {
pub: signingKey.pub,
priv: signingKey.priv,
epub: encryptionKey.pub,
epriv: encryptionKey.priv
epriv: encryptionKey.priv,
};
login(k);
setTimeout(async () => {
iris.public().get('profile').get('name').once(existingName => {
if (typeof existingName !== 'string' || existingName === '') {
name = name || iris.util.generateName();
iris.public().get('profile').put({a:null});
iris.public().get('profile').get('name').put(name);
}
});
iris
.public()
.get('profile')
.get('name')
.once((existingName) => {
if (typeof existingName !== 'string' || existingName === '') {
name = name || iris.util.generateName();
iris.public().get('profile').put({ a: null });
iris.public().get('profile').get('name').put(name);
}
});
}, 2000);
}
}
@ -133,12 +145,14 @@ class Login extends Component {
} else {
QRScanner.startPrivKeyQRScanner().then(login);
}
this.setState({showScanPrivKey: !this.state.showScanPrivKey});
this.setState({ showScanPrivKey: !this.state.showScanPrivKey });
}
onPastePrivKey(event) {
const val = event.target.value;
if (!val.length) { return; }
if (!val.length) {
return;
}
try {
let k = JSON.parse(val);
login(k);
@ -152,13 +166,13 @@ class Login extends Component {
showCreateAccount(e) {
e.preventDefault();
QRScanner.cleanupScanner();
this.setState({showSwitchAccount: false});
this.setState({ showSwitchAccount: false });
}
onLoginFormSubmit(e) {
e.preventDefault();
let name = document.getElementById('login-form-name').value || iris.util.generateName();
iris.session.loginAsNewUser(name);
iris.session.loginAsNewUser({ name });
this.base.style = 'display:none';
}
@ -169,52 +183,95 @@ class Login extends Component {
event.target.value = '';
return;
}
this.setState({inputStyle: val.length ? "text-align: center" : ""})
this.setState({ inputStyle: val.length ? 'text-align: center' : '' });
}
renderExistingAccountLogin() {
return (
<>
<input id="paste-privkey" autoFocus onInput={e => this.onPastePrivKey(e)}
placeholder={t('paste_private_key')} />
<input
id="paste-privkey"
autoFocus
onInput={(e) => this.onPastePrivKey(e)}
placeholder={t('paste_private_key')}
/>
<p>
<Button id="scan-privkey-btn"
onClick={e => this.toggleScanPrivKey(e)}>{t('scan_private_key_qr_code')}</Button>
<Button id="scan-privkey-btn" onClick={(e) => this.toggleScanPrivKey(e)}>
{t('scan_private_key_qr_code')}
</Button>
</p>
<p>
<video id="privkey-qr-video" width="320" height="320" style="object-fit: cover;"
className={this.state.showScanPrivKey ? '' : 'hidden'} />
<video
id="privkey-qr-video"
width="320"
height="320"
style="object-fit: cover;"
className={this.state.showScanPrivKey ? '' : 'hidden'}
/>
</p>
</>
);
}
render() {
return (<section id="login">
<div id="login-content">
{!this.state.showSwitchAccount ? (
<form id="login-form" autocomplete="off" onSubmit={e => this.onLoginFormSubmit(e)}>
<div id="create-account">
<img width="86" height="86" src={logo} alt="iris" />
<h1>iris</h1>
<input style={this.state.inputStyle} onInput={e => this.onNameChange(e)} autocomplete="off" autocorrect="off" autocapitalize="sentences" spellcheck="off" id="login-form-name" type="text" name="name" placeholder={t('whats_your_name')} />
<p><Button id="sign-up" type="submit">{t('new_user_go')}</Button></p>
<br />
<p><a href="#" id="show-existing-account-login" onClick={() => this.setState({showSwitchAccount: true})}>{t('already_have_an_account')}</a></p>
<p><a href="#" onClick={() => ethereumLogin()}>{t('web3_login')}</a></p>
return (
<section id="login">
<div id="login-content">
{!this.state.showSwitchAccount ? (
<form id="login-form" autocomplete="off" onSubmit={(e) => this.onLoginFormSubmit(e)}>
<div id="create-account">
<img width="86" height="86" src={logo} alt="iris" />
<h1>iris</h1>
<input
style={this.state.inputStyle}
onInput={(e) => this.onNameChange(e)}
autocomplete="off"
autocorrect="off"
autocapitalize="sentences"
spellcheck="off"
id="login-form-name"
type="text"
name="name"
placeholder={t('whats_your_name')}
/>
<p>
<Button id="sign-up" type="submit">
{t('new_user_go')}
</Button>
</p>
<br />
<p>
<a
href="#"
id="show-existing-account-login"
onClick={() => this.setState({ showSwitchAccount: true })}
>
{t('already_have_an_account')}
</a>
</p>
<p>
<a href="#" onClick={() => ethereumLogin()}>
{t('web3_login')}
</a>
</p>
<p>
<LanguageSelector />
</p>
</div>
</form>
) : (
<div id="existing-account-login">
<p>
<LanguageSelector />
<a href="#" id="show-create-account" onClick={(e) => this.showCreateAccount(e)}>
> {t('back')}
</a>
</p>
{this.renderExistingAccountLogin()}
</div>
</form>
):(
<div id="existing-account-login">
<p><a href="#" id="show-create-account" onClick={e => this.showCreateAccount(e)}>> {t('back')}</a></p>
{this.renderExistingAccountLogin()}
</div>
)}
</div>
</section>);
)}
</div>
</section>
);
}
}
@ -224,5 +281,5 @@ class ExistingAccountLogin extends Login {
}
}
export {ExistingAccountLogin};
export { ExistingAccountLogin };
export default Login;

View File

@ -1,23 +1,26 @@
import {translate as t} from '../translations/Translation';
import { route } from 'preact-router';
import Button from '../components/basic/Button';
import Component from '../BaseComponent';
import iris from 'iris-lib';
import { route } from 'preact-router';
import Component from '../BaseComponent';
import Button from '../components/basic/Button';
import { translate as t } from '../translations/Translation';
export default class LogoutConfirmation extends Component {
render () {
render() {
return (
<div class="main-view" id="logout-confirmation">
<div class="main-view" id="logout-confirmation">
<div class="centered-container">
<p dangerouslySetInnerHTML={{__html: t('logout_confirmation_info')}}></p>
<p>
<Button onClick={() => route('/settings')}>{t('back')}</Button>
</p>
<p>
<Button className="logout-button" onClick={() => iris.session.logOut()}>{t('log_out')}</Button>
</p>
<p dangerouslySetInnerHTML={{ __html: t('logout_confirmation_info') }}></p>
<p>
<Button onClick={() => route('/settings')}>{t('back')}</Button>
</p>
<p>
<Button className="logout-button" onClick={() => iris.session.logOut()}>
{t('log_out')}
</Button>
</p>
</div>
</div>
);
</div>
);
}
}
}

View File

@ -1,31 +1,35 @@
import { html } from 'htm/preact';
import PublicMessage from '../components/PublicMessage';
import FeedMessageForm from '../components/FeedMessageForm';
import { route } from 'preact-router';
import FeedMessageForm from '../components/FeedMessageForm';
import PublicMessage from '../components/PublicMessage';
import View from './View';
class Message extends View {
constructor() {
super();
this.class = "public-messages-view";
this.class = 'public-messages-view';
}
renderView() {
let content;
if (this.props.hash === 'new') {
content = html`
<${FeedMessageForm} activeChat="public" autofocus=${true} onSubmit=${() => route('/')}/>
<${FeedMessageForm} activeChat="public" autofocus=${true} onSubmit=${() => route('/')} />
`;
} else {
content = html`
<${PublicMessage} key=${this.props.hash} standalone=${true} hash=${this.props.hash} showName=${true} showReplies=${true} />
<${PublicMessage}
key=${this.props.hash}
standalone=${true}
hash=${this.props.hash}
showName=${true}
showReplies=${true}
/>
`;
}
return html`
<div class="centered-container">
${content}
</div>
`;
return html` <div class="centered-container">${content}</div> `;
}
}

View File

@ -1,11 +1,13 @@
import { html } from 'htm/preact';
import iris from 'iris-lib';
import Identicon from '../components/Identicon';
import Button from '../components/basic/Button';
import {translate as t} from '../translations/Translation';
import Identicon from '../components/Identicon';
import Name from '../components/Name';
import PublicMessage from '../components/PublicMessage';
import { translate as t } from '../translations/Translation';
import View from './View';
import PublicMessage from "../components/PublicMessage";
const PAGE_SIZE = 10;
@ -13,25 +15,28 @@ export default class Notifications extends View {
notifications = {};
class = 'public-messages-view';
state = {
displayCount: PAGE_SIZE
}
displayCount: PAGE_SIZE,
};
componentDidMount() {
iris.notifications.changeUnseenNotificationCount(0);
iris.local().get('notifications').map(this.sub(
(notification, time) => {
if (notification) {
this.notifications[time] = notification;
iris.notifications.getNotificationText(notification).then(text => {
this.notifications[time].text = text;
this.setState({});
});
} else {
delete this.notifications[time];
}
this.setState({d:new Date().toISOString()});
}
));
iris
.local()
.get('notifications')
.map(
this.sub((notification, time) => {
if (notification) {
this.notifications[time] = notification;
iris.notifications.getNotificationText(notification).then((text) => {
this.notifications[time].text = text;
this.setState({});
});
} else {
delete this.notifications[time];
}
this.setState({ d: new Date().toISOString() });
}),
);
}
shouldComponentUpdate() {
@ -44,37 +49,47 @@ export default class Notifications extends View {
return html`
<div class="centered-container" style="margin-bottom: 15px;">
<h3>${t('notifications')}</h3>
${Object.keys(this.notifications).length === 0 ? html`
<p> ${t('no_notifications_yet')}</p>
`:''}
${notificationKeys.slice(0, this.state.displayCount).map(k => {
${Object.keys(this.notifications).length === 0
? html` <p>${t('no_notifications_yet')}</p> `
: ''}
${notificationKeys.slice(0, this.state.displayCount).map((k) => {
const notification = this.notifications[k];
return html`
<div class="msg" key=${(notification.time||'') + (notification.from||'') + (notification.target||'')}>
<div
class="msg"
key=${(notification.time || '') +
(notification.from || '') +
(notification.target || '')}
>
<div class="msg-content">
<div class="msg-sender">
<a class="msg-sender-link" href="/profile/${notification.from}">
<${Identicon} str=${notification.from} width=30 />${' '}
<${Identicon} str=${notification.from} width="30" />${' '}
<small class="msgSenderName"><${Name} pub=${notification.from} /></small>
</a>
</div>
${notification.text || ''}
${notification.target ? html`<${PublicMessage} hash=${notification.target}/>` :''}
${notification.target ? html`<${PublicMessage} hash=${notification.target} />` : ''}
<div class="below-text">
<div class="time">${iris.util.formatDate(new Date(notification.time))}</div><br/>
<div class="time">${iris.util.formatDate(new Date(notification.time))}</div>
<br />
</div>
</div>
</div>
`;
})}
${displayCount < notificationKeys.length ? html`
<div>
<${Button} onClick=${() => this.setState({displayCount: displayCount + PAGE_SIZE})}>
${t('show_more')}
<//>
</div>
` : ''}
${displayCount < notificationKeys.length
? html`
<div>
<${Button}
onClick=${() => this.setState({ displayCount: displayCount + PAGE_SIZE })}
>
${t('show_more')}
<//>
</div>
`
: ''}
</div>
`;
}

View File

@ -1,10 +1,11 @@
import { html } from 'htm/preact';
import iris from 'iris-lib';
import {translate as t} from '../translations/Translation';
import { route } from 'preact-router';
import StoreView from './Store';
import Text from '../components/Text';
import { translate as t } from '../translations/Translation';
import StoreView from './Store';
class Product extends StoreView {
constructor() {
@ -22,19 +23,34 @@ class Product extends StoreView {
return html`
<div class="main-view" id="profile">
<div class="content">
<a href="/store/${iris.session.getPubKey()}"><${Text} path="profile/name" placeholder=${t('name')} user=${iris.session.getPubKey()} /></a>
<h3> ${t('add_item')}</h3>
<h2 contenteditable placeholder=${t('item_id')} onInput=${e => this.newProductName = e.target.innerText} />
<textarea placeholder=${t('item_description')} onInput=${e => this.newProductDescription = e.target.value} style="resize: vertical"/>
<input type="number" placeholder=${t('price')} onInput=${e => this.newProductPrice = parseInt(e.target.value)}/>
<hr/>
<a href="/store/${iris.session.getPubKey()}"
><${Text} path="profile/name" placeholder=${t('name')} user=${iris.session.getPubKey()}
/></a>
<h3>${t('add_item')}</h3>
<h2
contenteditable
placeholder=${t('item_id')}
onInput=${(e) => (this.newProductName = e.target.innerText)}
/>
<textarea
placeholder=${t('item_description')}
onInput=${(e) => (this.newProductDescription = e.target.value)}
style="resize: vertical"
/>
<input
type="number"
placeholder=${t('price')}
onInput=${(e) => (this.newProductPrice = parseInt(e.target.value))}
/>
<hr />
<p>${t('item_id')}:</p>
<p>
${t('item_id')}:
<input
placeholder=${t('item_id')}
onInput=${(e) => (this.newProductId = e.target.value)}
/>
</p>
<p>
<input placeholder=${t('item_id')} onInput=${e => this.newProductId = e.target.value} />
</p>
<button onClick=${e => this.addItemClicked(e)}>${t('add_item')}</button>
<button onClick=${(e) => this.addItemClicked(e)}>${t('add_item')}</button>
</div>
</div>
`;
@ -43,7 +59,7 @@ class Product extends StoreView {
onClickDelete() {
if (confirm('Delete product? This cannot be undone.')) {
iris.public().get('store').get('products').get(this.props.product).put(null);
route(`/store/${ this.props.store}`);
route(`/store/${this.props.store}`);
}
}
@ -51,38 +67,64 @@ class Product extends StoreView {
const cartTotalItems = Object.values(this.cart).reduce((sum, current) => sum + current, 0);
const i = this.state.product;
if (!i) return html``;
return html`
<div class="main-view" id="profile">
return html` <div class="main-view" id="profile">
<div class="content">
<a href="/store/${this.props.store}"><${Text} editable="false" path="profile/name" user=${this.props.store}/></a>
${cartTotalItems ? html`
<p>
<button onClick=${() => route(`/checkout/${ this.props.store}`)}>${t('shopping_cart')} (${cartTotalItems})</button>
</p>
` : ''}
${this.state.product ? html`
<${Text} tag="h3" user=${this.props.store} path="store/products/${this.props.product}/name"/>
<iris-img btn-class="btn btn-primary" user=${this.props.store} path="store/products/${this.props.product}/photo"/>
<p class="description">
<${Text} user=${this.props.store} path="store/products/${this.props.product}/description"/>
</p>
<p class="price">
<${Text} placeholder=${t('price')} user=${this.props.store} path="store/products/${this.props.product}/price"/>
</p>
<button class="add" onClick=${() => this.addToCart()}>
${t('add_to_cart')}
${this.cart[this.props.product] ? ` (${this.cart[this.props.product]})` : ''}
</button>
${this.isMyProfile ? html`
<p><button onClick=${e => this.onClickDelete(e)}>${t('delete_item')}</button></p>
` : ''}
` : ''}
<a href="/store/${this.props.store}"
><${Text} editable="false" path="profile/name" user=${this.props.store}
/></a>
${cartTotalItems
? html`
<p>
<button onClick=${() => route(`/checkout/${this.props.store}`)}>
${t('shopping_cart')} (${cartTotalItems})
</button>
</p>
`
: ''}
${this.state.product
? html`
<${Text}
tag="h3"
user=${this.props.store}
path="store/products/${this.props.product}/name"
/>
<iris-img
btn-class="btn btn-primary"
user=${this.props.store}
path="store/products/${this.props.product}/photo"
/>
<p class="description">
<${Text}
user=${this.props.store}
path="store/products/${this.props.product}/description"
/>
</p>
<p class="price">
<${Text}
placeholder=${t('price')}
user=${this.props.store}
path="store/products/${this.props.product}/price"
/>
</p>
<button class="add" onClick=${() => this.addToCart()}>
${t('add_to_cart')}
${this.cart[this.props.product] ? ` (${this.cart[this.props.product]})` : ''}
</button>
${this.isMyProfile
? html`
<p>
<button onClick=${(e) => this.onClickDelete(e)}>${t('delete_item')}</button>
</p>
`
: ''}
`
: ''}
</div>
</div>`;
}
render() {
return (this.props.store && this.props.product ? this.showProduct() : this.newProduct());
return this.props.store && this.props.product ? this.showProduct() : this.newProduct();
}
componentDidUpdate(prevProps) {
@ -95,19 +137,35 @@ class Product extends StoreView {
const product = {
name: this.newProductName,
description: this.newProductDescription,
price: this.newProductPrice
price: this.newProductPrice,
};
iris.public().get('store').get('products').get(this.newProductId || this.newProductName).put(product);
route(`/store/${iris.session.getPubKey()}`)
iris
.public()
.get('store')
.get('products')
.get(this.newProductId || this.newProductName)
.put(product);
route(`/store/${iris.session.getPubKey()}`);
}
componentDidMount() {
StoreView.prototype.componentDidMount.call(this);
const pub = this.props.store;
this.setState({followedUserCount: 0, followerCount: 0, name: '', photo: '', about: ''});
this.setState({
followedUserCount: 0,
followerCount: 0,
name: '',
photo: '',
about: '',
});
this.isMyProfile = iris.session.getPubKey() === pub;
if (this.props.product && pub) {
iris.public(pub).get('store').get('products').get(this.props.product).on(product => this.setState({product}));
iris
.public(pub)
.get('store')
.get('products')
.get(this.props.product)
.on((product) => this.setState({ product }));
}
}
}

View File

@ -1,26 +1,28 @@
import Helpers from '../Helpers';
import { Helmet } from 'react-helmet';
import { html } from 'htm/preact';
import {translate as t} from '../translations/Translation';
import iris from 'iris-lib';
import FeedMessageForm from '../components/FeedMessageForm';
import ProfilePhotoPicker from '../components/ProfilePhotoPicker';
import { route } from 'preact-router';
import { createRef } from 'preact';
import CopyButton from '../components/CopyButton';
import FollowButton from '../components/FollowButton';
import BlockButton from '../components/BlockButton';
import MessageFeed from '../components/MessageFeed';
import Identicon from '../components/Identicon';
import View from './View';
import { Link } from 'preact-router/match';
import $ from 'jquery';
import QRCode from '../lib/qrcode.min';
import {Helmet} from "react-helmet";
import {SMS_VERIFIER_PUB} from '../SMS';
import ProfilePhoto from '../components/ProfilePhoto';
import Button from '../components/basic/Button';
import Web3 from 'web3';
import { createRef } from 'preact';
import { route } from 'preact-router';
import { Link } from 'preact-router/match';
import styled from 'styled-components';
import Web3 from 'web3';
import Button from '../components/basic/Button';
import BlockButton from '../components/BlockButton';
import CopyButton from '../components/CopyButton';
import FeedMessageForm from '../components/FeedMessageForm';
import FollowButton from '../components/FollowButton';
import Identicon from '../components/Identicon';
import MessageFeed from '../components/MessageFeed';
import ProfilePhoto from '../components/ProfilePhoto';
import ProfilePhotoPicker from '../components/ProfilePhotoPicker';
import Helpers from '../Helpers';
import QRCode from '../lib/qrcode.min';
import { SMS_VERIFIER_PUB } from '../SMS';
import { translate as t } from '../translations/Translation';
import View from './View';
const ImageGrid = styled.div`
display: grid;
@ -39,7 +41,7 @@ const GalleryImage = styled.a`
background-size: cover;
background-position: center;
background-color: #ccc;
background-image: url(${props => props.src});
background-image: url(${(props) => props.src});
& .dropdown {
position: absolute;
top: 0;
@ -49,7 +51,7 @@ const GalleryImage = styled.a`
& .dropbtn {
padding-top: 0px;
margin-top: -5px;
text-shadow: 0px 0px 5px rgba(0,0,0,0.5);
text-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5);
color: white;
user-select: none;
}
@ -69,7 +71,7 @@ class Profile extends View {
super();
this.followedUsers = new Set();
this.followers = new Set();
this.id = "profile";
this.id = 'profile';
this.qrRef = createRef();
}
@ -100,7 +102,13 @@ class Profile extends View {
<div class="msg">
<div class="msg-content">
<p>Share your profile link so ${this.state.name || 'this user'} can follow you:</p>
<p><${CopyButton} text=${t('copy_link')} title=${iris.session.getMyName()} copyStr=${Helpers.getProfileLink(iris.session.getPubKey())}/></p>
<p>
<${CopyButton}
text=${t('copy_link')}
title=${iris.session.getMyName()}
copyStr=${Helpers.getProfileLink(iris.session.getPubKey())}
/>
</p>
<small>${t('visibility')}</small>
</div>
</div>
@ -112,37 +120,47 @@ class Profile extends View {
const chat = iris.private(this.props.id);
return html`
<div id="chat-settings" style="display:none">
<hr/>
<h3>${t('chat_settings')}</h3>
<div class="profile-nicknames">
<h4>${t('nicknames')}</h4>
<div id="chat-settings" style="display:none">
<hr />
<h3>${t('chat_settings')}</h3>
<div class="profile-nicknames">
<h4>${t('nicknames')}</h4>
<p>
${t('nickname')}:
<input
value=${chat && chat.theirNickname}
onInput=${(e) => chat && chat.put('nickname', e.target.value)}
/>
</p>
<p>
${t('their_nickname_for_you')}:
<span>
${chat && chat.myNickname && chat.myNickname.length ? chat.myNickname : ''}
</span>
</p>
</div>
<div class="notification-settings">
<h4>${t('notifications')}</h4>
<input type="radio" id="notifyAll" name="notificationPreference" value="all" />
<label for="notifyAll">${t('all_messages')}</label><br />
<input
type="radio"
id="notifyMentionsOnly"
name="notificationPreference"
value="mentions"
/>
<label for="notifyMentionsOnly">${t('mentions_only')}</label><br />
<input type="radio" id="notifyNothing" name="notificationPreference" value="nothing" />
<label for="notifyNothing">${t('nothing')}</label><br />
</div>
<hr />
<p>
${t('nickname')}:
<input value=${chat && chat.theirNickname} onInput=${e => chat && chat.put('nickname', e.target.value)}/>
</p>
<p>
${t('their_nickname_for_you')}:
<span>
${chat && chat.myNickname && chat.myNickname.length ? chat.myNickname : ''}
</span>
<${Button} class="delete-chat" onClick=${() => deleteChat(this.props.id)}
>${t('delete_chat')}<//
>
</p>
<hr />
</div>
<div class="notification-settings">
<h4>${t('notifications')}</h4>
<input type="radio" id="notifyAll" name="notificationPreference" value="all"/>
<label for="notifyAll">${t('all_messages')}</label><br/>
<input type="radio" id="notifyMentionsOnly" name="notificationPreference" value="mentions"/>
<label for="notifyMentionsOnly">${t('mentions_only')}</label><br/>
<input type="radio" id="notifyNothing" name="notificationPreference" value="nothing"/>
<label for="notifyNothing">${t('nothing')}</label><br/>
</div>
<hr/>
<p>
<${Button} class="delete-chat" onClick=${() => deleteChat(this.props.id)}>${t('delete_chat')}<//>
</p>
<hr/>
</div>
`;
}
@ -157,7 +175,7 @@ class Profile extends View {
const proof = await web3.eth.personal.sign(this.getEthIrisProofString(), address);
iris.public().get('profile').get('eth').put({
address,
proof
proof,
});
}
@ -173,14 +191,17 @@ class Profile extends View {
if (this.state.eth && this.state.eth.address) {
return html`
<p>
Eth: <a href="https://etherscan.io/address/${this.state.eth.address}">
${this.state.eth.address.slice(0, 4)}...${this.state.eth.address.slice(-4)}
</a>
<i> </i>
${this.isMyProfile ? html`(<a href="#" onClick=${this.disconnectEthereumClicked}>${t('disconnect')}</a>)` : ''}
${this.state.nfts.totalCount ? html`
<br /><a href="/nfts/${this.props.id}">NFT (${this.state.nfts.totalCount})</a>
` : ''}
Eth:
<a href="https://etherscan.io/address/${this.state.eth.address}">
${this.state.eth.address.slice(0, 4)}...${this.state.eth.address.slice(-4)}
</a>
<i> </i>
${this.isMyProfile
? html`(<a href="#" onClick=${this.disconnectEthereumClicked}>${t('disconnect')}</a>)`
: ''}
${this.state.nfts.totalCount
? html` <br /><a href="/nfts/${this.props.id}">NFT (${this.state.nfts.totalCount})</a> `
: ''}
</p>
`;
}
@ -188,7 +209,9 @@ class Profile extends View {
if (this.isMyProfile) {
return html`
<p>
<a href="#" onClick=${e => this.connectEthereumClicked(e)}>${t('Connect_Ethereum_account')}</a>
<a href="#" onClick=${(e) => this.connectEthereumClicked(e)}
>${t('connect_Ethereum_account')}</a
>
</p>
`;
}
@ -198,92 +221,155 @@ class Profile extends View {
this.isMyProfile = iris.session.getPubKey() === this.props.id;
let profilePhoto;
if (this.isMyProfile) {
profilePhoto = html`<${ProfilePhotoPicker} currentPhoto=${this.state.photo} placeholder=${this.props.id} callback=${src => this.onProfilePhotoSet(src)}/>`;
} else if (this.state.photo && !this.state.blocked && this.state.photo.indexOf('data:image') === 0) {
profilePhoto = html`<${ProfilePhoto} photo=${this.state.photo}/>`;
} else {
profilePhoto = html`<${Identicon} str=${this.props.id} hidePhoto=${this.state.blocked} width=250/>`
}
profilePhoto = html`<${ProfilePhotoPicker}
currentPhoto=${this.state.photo}
placeholder=${this.props.id}
callback=${(src) => this.onProfilePhotoSet(src)}
/>`;
} else if (
this.state.photo &&
!this.state.blocked &&
this.state.photo.indexOf('data:image') === 0
) {
profilePhoto = html`<${ProfilePhoto} photo=${this.state.photo} />`;
} else {
profilePhoto = html`<${Identicon}
str=${this.props.id}
hidePhoto=${this.state.blocked}
width="250"
/>`;
}
return html`
<div class="profile-top">
<div class="profile-header">
<div class="profile-photo-container">
${profilePhoto}
</div>
<div class="profile-header-stuff">
<div style="display:flex; flex-direction:row;">
<h3 style="flex: 1" class="profile-name" placeholder=${this.isMyProfile ? t('name') : ''} contenteditable=${this.isMyProfile} onInput=${e => this.onNameInput(e)}>
<div class="profile-top">
<div class="profile-header">
<div class="profile-photo-container">${profilePhoto}</div>
<div class="profile-header-stuff">
<div style="display:flex; flex-direction:row;">
<h3
style="flex: 1"
class="profile-name"
placeholder=${this.isMyProfile ? t('name') : ''}
contenteditable=${this.isMyProfile}
onInput=${(e) => this.onNameInput(e)}
>
${this.state.name}
</h3>
<div class="dropdown profile-actions">
<div class="dropbtn">\u2026</div>
<div class="dropdown-content">
${this.isMyProfile ? '' : html`<${BlockButton} key=${`${this.props.id}block`} id=${this.props.id}/>`}
<${CopyButton} key=${`${this.props.id}copy`} text=${t('copy_link')} title=${this.state.name} copyStr=${window.location.href}/>
<${Button} onClick=${() => $(this.qrRef.current).toggle()}>${t('show_qr_code')}<//>
${this.isMyProfile ? '' : html`
<${Button} class="show-settings" onClick=${() => this.onClickSettings()}>${t('settings')}<//>
`}
</h3>
<div class="dropdown profile-actions">
<div class="dropbtn"></div>
<div class="dropdown-content">
${this.isMyProfile
? ''
: html`<${BlockButton} key=${`${this.props.id}block`} id=${this.props.id} />`}
<${CopyButton}
key=${`${this.props.id}copy`}
text=${t('copy_link')}
title=${this.state.name}
copyStr=${window.location.href}
/>
<${Button} onClick=${() => $(this.qrRef.current).toggle()}
>${t('show_qr_code')}<//
>
${this.isMyProfile
? ''
: html`
<${Button} class="show-settings" onClick=${() => this.onClickSettings()}
>${t('settings')}<//
>
`}
</div>
</div>
</div>
</div>
<div class="profile-about hidden-xs">
<p class="profile-about-content" placeholder=${this.isMyProfile ? t('about') : ''} contenteditable=${this.isMyProfile} onInput=${e => this.onAboutInput(e)}>${this.state.about}</p>
</div>
${this.renderEthereum()}
<div class="profile-actions">
<div class="follow-count">
<a href="/follows/${this.props.id}">
<span>${this.state.followedUserCount}</span> ${t('following')}
</a>
<a href="/followers/${this.props.id}">
<span>${this.state.followerCount}</span> ${t('followers')}
</a>
<div class="profile-about hidden-xs">
<p
class="profile-about-content"
placeholder=${this.isMyProfile ? t('about') : ''}
contenteditable=${this.isMyProfile}
onInput=${(e) => this.onAboutInput(e)}
>
${this.state.about}
</p>
</div>
${this.renderEthereum()}
<div class="profile-actions">
<div class="follow-count">
<a href="/follows/${this.props.id}">
<span>${this.state.followedUserCount}</span> ${t('following')}
</a>
<a href="/followers/${this.props.id}">
<span>${this.state.followerCount}</span> ${t('followers')}
</a>
</div>
${this.followedUsers.has(iris.session.getPubKey())
? html` <p><small>${t('follows_you')}</small></p> `
: this.props.id === SMS_VERIFIER_PUB
? html`
<p>
<a href="https://iris-sms-auth.herokuapp.com/?pub=${iris.session.getPubKey()}"
>${t('ask_for_verification')}</a
>
</p>
`
: ''}
${this.isMyProfile
? ''
: html`
<div class="hidden-xs">
<${FollowButton} key=${`${this.props.id}follow`} id=${this.props.id} />
<${Button} onClick=${() => route(`/chat/${this.props.id}`)}
>${t('send_message')}<//
>
</div>
`}
</div>
${this.followedUsers.has(iris.session.getPubKey()) ? html`
<p><small>${t('follows_you')}</small></p>
`: this.props.id === SMS_VERIFIER_PUB ? html`
<p><a href="https://iris-sms-auth.herokuapp.com/?pub=${iris.session.getPubKey()}">${t('ask_for_verification')}</a></p>
` : ''}
${this.isMyProfile ? '' : html`
<div class="hidden-xs">
<${FollowButton} key=${`${this.props.id}follow`} id=${this.props.id}/>
<${Button} onClick=${() => route(`/chat/${ this.props.id}`)}>${t('send_message')}<//>
</div>
`}
</div>
</div>
</div>
${this.isMyProfile ? '' : html`
<div class="visible-xs-flex profile-actions" style="justify-content: flex-end">
<${FollowButton} key=${`${this.props.id}follow`} id=${this.props.id}/>
<${Button} onClick=${() => route(`/chat/${ this.props.id}`)}>${t('send_message')}<//>
</div>
`}
${(this.isMyProfile || this.state.about) ? html`
<div class="profile-about visible-xs-flex">
<p class="profile-about-content" placeholder=${this.isMyProfile ? t('about') : ''} contenteditable=${this.isMyProfile} onInput=${e => this.onAboutInput(e)}>${this.state.about}</p>
</div>
` : ''}
<p ref=${this.qrRef} style="display:none" class="qr-container"></p>
${this.renderSettings()}
</div>
${this.isMyProfile
? ''
: html`
<div class="visible-xs-flex profile-actions" style="justify-content: flex-end">
<${FollowButton} key=${`${this.props.id}follow`} id=${this.props.id} />
<${Button} onClick=${() => route(`/chat/${this.props.id}`)}>${t('send_message')}<//>
</div>
`}
${this.isMyProfile || this.state.about
? html`
<div class="profile-about visible-xs-flex">
<p
class="profile-about-content"
placeholder=${this.isMyProfile ? t('about') : ''}
contenteditable=${this.isMyProfile}
onInput=${(e) => this.onAboutInput(e)}
>
${this.state.about}
</p>
</div>
`
: ''}
<p ref=${this.qrRef} style="display:none" class="qr-container"></p>
${this.renderSettings()}
</div>
`;
}
renderTabs() {
return html`
<div class="tabs">
<${Link} activeClassName="active" href="/profile/${this.props.id}">${t('posts')} ${this.state.noPosts ? '(0)' : ''}<//>
<${Link} activeClassName="active" href="/replies/${this.props.id}">${t('replies')} ${this.state.noReplies ? '(0)' : ''}<//>
<${Link} activeClassName="active" href="/likes/${this.props.id}">${t('likes')} ${this.state.noLikes ? '(0)' : ''}<//>
<${Link} activeClassName="active" href="/media/${this.props.id}">${t('media')} ${this.state.noMedia ? '(0)' : ''}<//>
</div>
<div class="tabs">
<${Link} activeClassName="active" href="/profile/${this.props.id}"
>${t('posts')} ${this.state.noPosts ? '(0)' : ''}<//
>
<${Link} activeClassName="active" href="/replies/${this.props.id}"
>${t('replies')} ${this.state.noReplies ? '(0)' : ''}<//
>
<${Link} activeClassName="active" href="/likes/${this.props.id}"
>${t('likes')} ${this.state.noLikes ? '(0)' : ''}<//
>
<${Link} activeClassName="active" href="/media/${this.props.id}"
>${t('media')} ${this.state.noMedia ? '(0)' : ''}<//
>
</div>
`;
}
@ -308,11 +394,14 @@ class Profile extends View {
ctx.drawImage(img, 0, 0);
const dataURL = canvas.toDataURL('image/png');
iris.public().get('profile').get('photo').put(dataURL);
}
};
}
$('.main-view').animate({
scrollTop: 0
}, 'slow');
$('.main-view').animate(
{
scrollTop: 0,
},
'slow',
);
}
getSrcForNft(nft, thumbnail = true) {
@ -331,7 +420,9 @@ class Profile extends View {
if (src && src.indexOf('ipfs://') === 0) {
src = `https://ipfs.io/ipfs/${src.substring(7)}`;
} else if (src && src.indexOf('https://ipfs.io/ipfs/') !== 0) {
src = `https://proxy.irismessengers.wtf/insecure/${thumbnail ? 'rs:fill:520:520/' : ''}plain/${src}`;
src = `https://proxy.irismessengers.wtf/insecure/${
thumbnail ? 'rs:fill:520:520/' : ''
}plain/${src}`;
}
}
return src;
@ -341,59 +432,87 @@ class Profile extends View {
if (this.props.tab === 'replies') {
return html`
<div class="public-messages-view">
<${MessageFeed} scrollElement=${this.scrollElement.current} key="replies${this.props.id}" node=${iris.public(this.props.id).get('replies')} keyIsMsgHash=${true} />
<${MessageFeed}
scrollElement=${this.scrollElement.current}
key="replies${this.props.id}"
node=${iris.public(this.props.id).get('replies')}
keyIsMsgHash=${true}
/>
</div>
`;
} else if (this.props.tab === 'likes') {
return html`
<div class="public-messages-view">
<${MessageFeed} scrollElement=${this.scrollElement.current} key="likes${this.props.id}" node=${iris.public(this.props.id).get('likes')} keyIsMsgHash=${true}/>
<${MessageFeed}
scrollElement=${this.scrollElement.current}
key="likes${this.props.id}"
node=${iris.public(this.props.id).get('likes')}
keyIsMsgHash=${true}
/>
</div>
`;
} else if (this.props.tab === 'media') {
return html`
<div class="public-messages-view">
${this.isMyProfile ? html`<${FeedMessageForm} index="media" class="hidden-xs" autofocus=${false}/>` : ''}
<${MessageFeed} scrollElement=${this.scrollElement.current} key="media${this.props.id}" node=${iris.public(this.props.id).get('media')}/>
${this.isMyProfile
? html`<${FeedMessageForm} index="media" class="hidden-xs" autofocus=${false} />`
: ''}
<${MessageFeed}
scrollElement=${this.scrollElement.current}
key="media${this.props.id}"
node=${iris.public(this.props.id).get('media')}
/>
</div>
`;
} else if (this.props.tab === 'nfts') {
return html`
<div class="public-messages-view">
<${ImageGrid}>
${this.state.nfts && this.state.nfts.ownedNfts && this.state.nfts.ownedNfts.map(nft => {
${this.state.nfts &&
this.state.nfts.ownedNfts &&
this.state.nfts.ownedNfts.map((nft) => {
let src = this.getSrcForNft(nft, true);
return html`
<${GalleryImage}
href="https://etherscan.io/address/${nft.contract.address}"
target="_blank"
src=${src}>
${this.isMyProfile ? html`
<div class="dropdown">
<div class="dropbtn">\u2026</div>
<div class="dropdown-content">
<a href="#" onClick=${e => this.useAsPfp(nft, e)}>${t('use_as_PFP')}</a>
</div>
</div>
` : ''}
href="https://etherscan.io/address/${nft.contract.address}"
target="_blank"
src=${src}
>
${this.isMyProfile
? html`
<div class="dropdown">
<div class="dropbtn"></div>
<div class="dropdown-content">
<a href="#" onClick=${(e) => this.useAsPfp(nft, e)}
>${t('use_as_PFP')}</a
>
</div>
</div>
`
: ''}
<//>
`
`;
})}
<//>
</div>
`;
}
const messageForm = this.isMyProfile ? html`<${FeedMessageForm} class="hidden-xs" autofocus=${false}/>` : '';
return html`
const messageForm = this.isMyProfile
? html`<${FeedMessageForm} class="hidden-xs" autofocus=${false} />`
: '';
return html`
<div>
${messageForm}
<div class="public-messages-view">
${this.getNotification()}
<${MessageFeed} scrollElement=${this.scrollElement.current} key="posts${this.props.id}" node=${iris.public(this.props.id).get('msgs')} />
<${MessageFeed}
scrollElement=${this.scrollElement.current}
key="posts${this.props.id}"
node=${iris.public(this.props.id).get('msgs')}
/>
</div>
</div>
`;
`;
}
onNftImgError(e) {
@ -407,15 +526,16 @@ class Profile extends View {
return html`
<div class="content">
<${Helmet}>
<title>${title}</title>
<meta name="description" content=${description} />
<meta property="og:type" content="profile" />
${this.state.ogImageUrl ? html`<meta property="og:image" content=${this.state.ogImageUrl} />` : ''}
<meta property="og:title" content=${ogTitle} />
<meta property="og:description" content=${description} />
<title>${title}</title>
<meta name="description" content=${description} />
<meta property="og:type" content="profile" />
${this.state.ogImageUrl
? html`<meta property="og:image" content=${this.state.ogImageUrl} />`
: ''}
<meta property="og:title" content=${ogTitle} />
<meta property="og:description" content=${description} />
<//>
${this.renderDetails()}
${this.state.blocked ? '' : this.renderTabs()}
${this.renderDetails()} ${this.state.blocked ? '' : this.renderTabs()}
${this.state.blocked ? '' : this.renderTab()}
</div>
`;
@ -424,7 +544,7 @@ class Profile extends View {
async getNfts(address) {
const { Alchemy, Network } = await import('alchemy-sdk');
const config = {
apiKey: "DGLWKXjx7nRC5Dmz7mavP8CX1frKT1Ar",
apiKey: 'DGLWKXjx7nRC5Dmz7mavP8CX1frKT1Ar',
network: Network.ETH_MAINNET,
};
const alchemy = new Alchemy(config);
@ -457,54 +577,81 @@ class Profile extends View {
getProfileDetails() {
const pub = this.props.id;
iris.public(pub).get('follow').map().on(this.sub(
(following,key) => {
if (following) {
this.followedUsers.add(key);
} else {
this.followedUsers.delete(key);
}
this.setState({followedUserCount: this.followedUsers.size});
}
));
iris.group().count(`follow/${pub}`, this.sub((followerCount) => {
this.setState({followerCount});
}));
iris.public(pub).get('profile').get('eth').on(this.sub(eth => {
if (eth && eth.address && eth.proof) {
if (eth.address === (this.state.eth && this.state.eth.address)) {
return;
}
const web3 = new Web3();
const signer = web3.eth.accounts.recover(this.getEthIrisProofString(), eth.proof);
if (signer === eth.address) {
this.setState({eth});
this.getNfts(eth.address);
}
} else {
this.setState({eth: null});
}
}));
iris.public(pub).get('profile').get('name').on(this.sub(
name => {
if (!$('#profile .profile-name:focus').length) {
this.setState({name});
}
}
));
iris.public(pub).get('profile').get('photo').on(this.sub(photo => {
this.setState({photo});
this.setOgImageUrl(photo);
}));
iris.public(pub).get('profile').get('about').on(this.sub(
about => {
if (!$('#profile .profile-about-content:focus').length) {
this.setState({about});
} else {
$('#profile .profile-about-content:not(:focus)').text(about);
}
}
));
iris
.public(pub)
.get('follow')
.map()
.on(
this.sub((following, key) => {
if (following) {
this.followedUsers.add(key);
} else {
this.followedUsers.delete(key);
}
this.setState({ followedUserCount: this.followedUsers.size });
}),
);
iris.group().count(
`follow/${pub}`,
this.sub((followerCount) => {
this.setState({ followerCount });
}),
);
iris
.public(pub)
.get('profile')
.get('eth')
.on(
this.sub((eth) => {
if (eth && eth.address && eth.proof) {
if (eth.address === (this.state.eth && this.state.eth.address)) {
return;
}
const web3 = new Web3();
const signer = web3.eth.accounts.recover(this.getEthIrisProofString(), eth.proof);
if (signer === eth.address) {
this.setState({ eth });
this.getNfts(eth.address);
}
} else {
this.setState({ eth: null });
}
}),
);
iris
.public(pub)
.get('profile')
.get('name')
.on(
this.sub((name) => {
if (!$('#profile .profile-name:focus').length) {
this.setState({ name });
}
}),
);
iris
.public(pub)
.get('profile')
.get('photo')
.on(
this.sub((photo) => {
this.setState({ photo });
this.setOgImageUrl(photo);
}),
);
iris
.public(pub)
.get('profile')
.get('about')
.on(
this.sub((about) => {
if (!$('#profile .profile-about-content:focus').length) {
this.setState({ about });
} else {
$('#profile .profile-about-content:not(:focus)').text(about);
}
}),
);
}
componentDidMount() {
@ -543,41 +690,52 @@ class Profile extends View {
iris.local().get('noFollowers').on(this.inject());
this.getProfileDetails();
if (chat) {
$(`input[name=notificationPreference][value=${ chat.notificationSetting }]`).attr('checked', 'checked');
$('input:radio[name=notificationPreference]').off().on('change', (event) => {
chat.put('notificationSetting', event.target.value);
});
$(`input[name=notificationPreference][value=${chat.notificationSetting}]`).attr(
'checked',
'checked',
);
$('input:radio[name=notificationPreference]')
.off()
.on('change', (event) => {
chat.put('notificationSetting', event.target.value);
});
}
qrCodeEl.empty();
new QRCode(qrCodeEl.get(0), {
text: window.location.href,
width: 300,
height: 300,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.H,
});
iris.public().get('block').get(this.props.id).on(this.sub(
blocked => {
this.setState({blocked});
}
));
iris.public(this.props.id).on(this.sub(
user => {
iris
.public()
.get('block')
.get(this.props.id)
.on(
this.sub((blocked) => {
this.setState({ blocked });
}),
);
iris.public(this.props.id).on(
this.sub((user) => {
this.setState({
noPosts: !user.msgs,
noMedia: !user.media,
noLikes: !user.likes,
noReplies: !user.replies,
});
}
));
}),
);
if (this.isUserAgentCrawler() && !this.state.ogImageUrl && !this.state.photo) {
new iris.Attribute({type: 'keyID', value: this.props.id}).identiconSrc({width: 300, showType: false}).then(src => {
if (!this.state.ogImageUrl && !this.state.photo) {
this.setOgImageUrl(src);
}
});
new iris.Attribute({ type: 'keyID', value: this.props.id })
.identiconSrc({ width: 300, showType: false })
.then((src) => {
if (!this.state.ogImageUrl && !this.state.photo) {
this.setOgImageUrl(src);
}
});
}
}
}

View File

@ -1,16 +1,18 @@
import { html } from 'htm/preact';
import {translate as t} from '../translations/Translation';
import iris from 'iris-lib';
import ProfilePhotoPicker from '../components/ProfilePhotoPicker';
import { route } from 'preact-router';
import SafeImg from '../components/SafeImg';
import Text from '../components/Text';
import Filters from '../components/Filters';
import CopyButton from '../components/CopyButton';
import Filters from '../components/Filters';
import FollowButton from '../components/FollowButton';
import Identicon from '../components/Identicon';
import OnboardingNotification from '../components/OnboardingNotification';
import ProfilePhotoPicker from '../components/ProfilePhotoPicker';
import SafeImg from '../components/SafeImg';
import Text from '../components/Text';
import { translate as t } from '../translations/Translation';
import View from './View';
import OnboardingNotification from "../components/OnboardingNotification";
class Store extends View {
constructor() {
@ -19,7 +21,7 @@ class Store extends View {
this.followers = new Set();
this.cart = {};
this.carts = {};
this.state = {items:{}};
this.state = { items: {} };
this.id = 'profile';
this.class = 'public-messages-view';
}
@ -36,24 +38,33 @@ class Store extends View {
const followable = !(this.isMyProfile || user.length < 40);
let profilePhoto;
if (this.isMyProfile) {
profilePhoto = html`<${ProfilePhotoPicker} currentPhoto=${this.state.photo} placeholder=${user} callback=${src => this.onProfilePhotoSet(src)}/>`;
profilePhoto = html`<${ProfilePhotoPicker}
currentPhoto=${this.state.photo}
placeholder=${user}
callback=${(src) => this.onProfilePhotoSet(src)}
/>`;
} else if (this.state.photo) {
profilePhoto = html`<${SafeImg} class="profile-photo" src=${this.state.photo}/>`
profilePhoto = html`<${SafeImg} class="profile-photo" src=${this.state.photo} />`;
} else {
profilePhoto = html`<${Identicon} str=${user} width=250/>`
profilePhoto = html`<${Identicon} str=${user} width="250" />`;
}
return html`
<div class="content">
<div class="profile-top">
<div class="profile-header">
<div class="profile-photo-container">
${profilePhoto}
</div>
<div class="profile-photo-container">${profilePhoto}</div>
<div class="profile-header-stuff">
<h3 class="profile-name"><${Text} path= ${t('profile_name')} placeholder= ${t('name')} user=${user}/></h3>
<h3 class="profile-name">
<${Text} path=${t('profile_name')} placeholder=${t('name')} user=${user} />
</h3>
<div class="profile-about hidden-xs">
<p class="profile-about-content">
<${Text} path="store/about" placeholder=${t('store_description')} attr="about" user=${user}/>
<${Text}
path="store/about"
placeholder=${t('store_description')}
attr="about"
user=${user}
/>
</p>
</div>
<div class="profile-actions">
@ -65,19 +76,32 @@ class Store extends View {
<span>${this.state.followerCount}</span> ${t('followers')}
</a>
</div>
${this.followedUsers.has(iris.session.getPubKey()) ? html`
<p><small>${t('follows_you')}</small></p>
`: ''}
${followable ? html`<${FollowButton} id=${user}/>` : ''}
<button onClick=${() => route(`/chat/${ user}`)}>${t('send_message')}</button>
${uuid ? '' : html`
<${CopyButton} text=${t('copy_link')} title=${this.state.name} copyStr=${window.location.href}/>
`}
${this.followedUsers.has(iris.session.getPubKey())
? html` <p><small>${t('follows_you')}</small></p> `
: ''}
${followable ? html`<${FollowButton} id=${user} />` : ''}
<button onClick=${() => route(`/chat/${user}`)}>${t('send_message')}</button>
${uuid
? ''
: html`
<${CopyButton}
text=${t('copy_link')}
title=${this.state.name}
copyStr=${window.location.href}
/>
`}
</div>
</div>
</div>
<div class="profile-about visible-xs-flex">
<p class="profile-about-content" placeholder=${this.isMyProfile ? t('about') : ''} contenteditable=${this.isMyProfile} onInput=${e => this.onAboutInput(e)}>${this.state.about}</p>
<p
class="profile-about-content"
placeholder=${this.isMyProfile ? t('about') : ''}
contenteditable=${this.isMyProfile}
onInput=${(e) => this.onAboutInput(e)}
>
${this.state.about}
</p>
</div>
</div>
@ -91,36 +115,53 @@ class Store extends View {
const cartTotalItems = Object.keys(this.cart).reduce((sum, k) => sum + this.cart[k], 0);
const keys = Object.keys(this.state.items);
return html`
${(this.props.store || this.state.noFollows) ? '' : html`<${Filters}/>`}
${cartTotalItems ? html`
<p>
<button onClick=${() => route('/checkout')}>${t('shopping_cart')}(${cartTotalItems})</button>
</p>
` : ''}
${this.props.store || this.state.noFollows ? '' : html`<${Filters} />`}
${cartTotalItems
? html`
<p>
<button onClick=${() => route('/checkout')}>
${t('shopping_cart')}(${cartTotalItems})
</button>
</p>
`
: ''}
<div class="thumbnail-items">
${this.isMyProfile ? html`
<div class="thumbnail-item store-item" onClick=${() => route(`/product/new`)}>
<a href="/product/new" class="name">${t('add_item')}</a>
</div>
` : ''}
${!keys.length ? html`<p> ${t('no_items_to_show')}</p>`:''}
${keys.map(k => {
${this.isMyProfile
? html`
<div class="thumbnail-item store-item" onClick=${() => route(`/product/new`)}>
<a href="/product/new" class="name">${t('add_item')}</a>
</div>
`
: ''}
${!keys.length ? html`<p>${t('no_items_to_show')}</p>` : ''}
${keys.map((k) => {
const i = this.state.items[k];
return html`
<div class="thumbnail-item store-item" onClick=${() => route(`/product/${k}/${i.from}`)}>
<${SafeImg} src=${i.photo || ''}/>
<div
class="thumbnail-item store-item"
onClick=${() => route(`/product/${k}/${i.from}`)}
>
<${SafeImg} src=${i.photo || ''} />
<a href="/product/${k}/${i.from || this.props.store}" class="name">${i.name}</a>
${this.props.store ? '':html`
<small>by <${Text} path="profile/name" editable="false" placeholder="Name" user=${i.from}/></small>
`}
${this.props.store
? ''
: html`
<small
>by
<${Text}
path="profile/name"
editable="false"
placeholder="Name"
user=${i.from}
/></small>
`}
<p class="description">${i.description}</p>
<p class="price">${i.price}</p>
<button class="add" onClick=${e => this.addToCart(k, i.from, e)}>
${t('add_to_cart')}
${this.cart[k] ? ` (${this.cart[k]})` : ''}
<button class="add" onClick=${(e) => this.addToCart(k, i.from, e)}>
${t('add_to_cart')} ${this.cart[k] ? ` (${this.cart[k]})` : ''}
</button>
</div>
`
`;
})}
</div>
`;
@ -131,8 +172,11 @@ class Store extends View {
return this.renderUserStore(this.props.store);
}
return html`
<p dangerouslySetInnerHTML=${{ __html: t('this_is_a_prototype_store', `href="/store/${iris.session.getPubKey()}"`
)}}></p>
<p
dangerouslySetInnerHTML=${{
__html: t('this_is_a_prototype_store', `href="/store/${iris.session.getPubKey()}"`),
}}
></p>
<${OnboardingNotification} />
${this.renderItems()}
`;
@ -141,10 +185,10 @@ class Store extends View {
updateTotalPrice() {
const totalPrice = Object.keys(this.cart).reduce((sum, currentKey) => {
const item = this.state.items[currentKey];
const price = item && parseInt(item.price) || 0;
const price = (item && parseInt(item.price)) || 0;
return sum + price * this.cart[currentKey];
}, 0);
this.setState({totalPrice});
this.setState({ totalPrice });
}
componentDidUpdate(prevProps) {
@ -154,22 +198,29 @@ class Store extends View {
}
getCartFromUser(user) {
iris.local().get('cart').get(user).map(this.sub(
(v, k) => {
if (k === '#') { return; } // blah
this.cart[k + user] = v;
this.carts[user] = this.carts[user] || {};
this.carts[user][k] = v;
this.setState({cart: this.cart, carts: this.carts});
this.updateTotalPrice();
}, `cart${ user}`
));
iris
.local()
.get('cart')
.get(user)
.map(
this.sub((v, k) => {
if (k === '#') {
return;
} // blah
this.cart[k + user] = v;
this.carts[user] = this.carts[user] || {};
this.carts[user][k] = v;
this.setState({ cart: this.cart, carts: this.carts });
this.updateTotalPrice();
}, `cart${user}`),
);
}
onProduct(p, id, a, e, from) {
this.eventListeners[`products${ from}`] = e;
this.eventListeners[`products${from}`] = e;
const items = this.state.items;
if (p && typeof p === "object") { // TODO gun returning bad data (typeof p === "string")?
if (p && typeof p === 'object') {
// TODO gun returning bad data (typeof p === "string")?
const o = {};
p.from = from;
o[id] = p;
@ -178,43 +229,54 @@ class Store extends View {
} else {
delete items[id];
}
this.setState({items});
this.setState({ items });
}
getProductsFromUser(user) {
iris.public(user).get('store').get('products').map().on(this.sub(
(...args) => {
return this.onProduct(...args, user);
}, `${user }products`
));
iris
.public(user)
.get('store')
.get('products')
.map()
.on(
this.sub((...args) => {
return this.onProduct(...args, user);
}, `${user}products`),
);
}
getAllCarts() {
const carts = {};
iris.local().get('cart').map(this.sub(
(o, user) => {
if (!user) {
delete carts[user];
return;
}
if (carts[user]) { return; }
carts[user] = true;
this.getCartFromUser(user);
}
));
iris
.local()
.get('cart')
.map(
this.sub((o, user) => {
if (!user) {
delete carts[user];
return;
}
if (carts[user]) {
return;
}
carts[user] = true;
this.getCartFromUser(user);
}),
);
}
getAllProducts(group) {
iris.group(group).map('store/products', this.sub(
(...args) => {
iris.group(group).map(
'store/products',
this.sub((...args) => {
this.onProduct(...args);
}
));
}),
);
}
componentDidMount() {
const user = this.props.store;
Object.values(this.eventListeners).forEach(e => e.off());
Object.values(this.eventListeners).forEach((e) => e.off());
this.cart = {};
this.isMyProfile = iris.session.getPubKey() === user;
@ -223,14 +285,18 @@ class Store extends View {
this.getProductsFromUser(user);
} else {
let prevGroup;
iris.local().get('filters').get('group').on(this.sub(
group => {
if (group && group !== prevGroup) {
prevGroup = group;
this.getAllProducts(group);
}
}
));
iris
.local()
.get('filters')
.get('group')
.on(
this.sub((group) => {
if (group && group !== prevGroup) {
prevGroup = group;
this.getAllProducts(group);
}
}),
);
this.getAllCarts();
}
}

View File

@ -1,17 +1,19 @@
import { html } from 'htm/preact';
import Torrent from '../components/Torrent';
import View from './View';
class TorrentView extends View {
constructor() {
super();
this.class = "public-messages-view";
this.class = 'public-messages-view';
}
renderView() {
return html`
<div id="message-list" class="centered-container">
<${Torrent} standalone=${true} showFiles=${true} torrentId=${this.props.id}/>
<${Torrent} standalone=${true} showFiles=${true} torrentId=${this.props.id} />
</div>
`;
}

View File

@ -1,5 +1,6 @@
import Component from '../BaseComponent';
import { createRef } from 'preact';
import Component from '../BaseComponent';
import Header from '../components/Header';
abstract class View extends Component {
@ -11,12 +12,12 @@ abstract class View extends Component {
render() {
return (
<>
<Header />
<div ref={this.scrollElement} class={`main-view ${this.class}`} id={this.id}>
{this.renderView()}
</div>
</>
<>
<Header />
<div ref={this.scrollElement} class={`main-view ${this.class}`} id={this.id}>
{this.renderView()}
</div>
</>
);
}
}

View File

@ -1,13 +1,15 @@
import { html } from 'htm/preact';
import View from '../View';
import ChatList from './ChatList';
import PrivateChat from './PrivateChat';
import HashtagChat from './HashtagChat';
import PrivateChat from './PrivateChat';
class Chat extends View {
constructor() {
super();
this.id = "chat-view";
this.id = 'chat-view';
}
renderView() {
@ -18,10 +20,12 @@ class Chat extends View {
chat = html`<${PrivateChat} id=${this.props.id} key=${this.props.id} />`;
}
return html`
<${ChatList} activeChat=${this.props.id} class=${this.props.id || this.props.hashtag ? 'hidden-xs' : ''}/>
<${ChatList}
activeChat=${this.props.id}
class=${this.props.id || this.props.hashtag ? 'hidden-xs' : ''}
/>
${chat}
`;
}
}

View File

@ -1,19 +1,20 @@
import Component from '../../BaseComponent';
import Helpers from '../../Helpers';
import { html } from 'htm/preact';
import { translate as t } from '../../translations/Translation';
import iris from 'iris-lib';
import ChatListItem from './ChatListItem';
import $ from 'jquery';
import orderBy from 'lodash/orderBy';
import { route } from 'preact-router';
import ScrollViewport from 'preact-scroll-viewport';
import _ from 'lodash';
import $ from 'jquery';
import Component from '../../BaseComponent';
import Helpers from '../../Helpers';
import { translate as t } from '../../translations/Translation';
import ChatListItem from './ChatListItem';
class ChatList extends Component {
constructor(props) {
super(props);
this.state = {chats: new Map(), hashtags: {}, latestTime: null};
this.state = { chats: new Map(), hashtags: {}, latestTime: null };
}
enableDesktopNotifications() {
@ -31,41 +32,58 @@ class ChatList extends Component {
componentDidMount() {
const hashtags = {};
iris.local().get('channels').map(this.sub(
(chat, id) => {
if (!chat || id === 'public' || chat.name == null) {
this.state.chats.has(id) && this.setState({chats: this.state.chats.delete(id)});
return;
}
chat.latestTime = chat.latestTime || '';
iris.local().get('channels').get(id).get('latest').on(this.sub(
(latest) => {
this.setState({latestTime : latest});
chat.latestTime = latest.time || '';
chat.latest = latest;
chat.id = id;
this.setState({chats: this.state.chats.set(id, chat)});
iris
.local()
.get('channels')
.map(
this.sub((chat, id) => {
if (!chat || id === 'public' || chat.name == null) {
this.state.chats.has(id) && this.setState({ chats: this.state.chats.delete(id) });
return;
}
));
chat.id = id;
this.setState({chats: this.state.chats.set(id, chat)});
}
));
iris.local().get('scrollUp').on(this.sub(
() => Helpers.animateScrollTop('.chat-list')
));
iris.public().get('hashtagSubscriptions').map().on(this.sub(
(isSubscribed, hashtag) => {
if (isSubscribed) {
hashtags[hashtag] = true;
} else {
delete hashtags[hashtag];
}
this.setState({hashtags});
}
));
chat.latestTime = chat.latestTime || '';
iris
.local()
.get('channels')
.get(id)
.get('latest')
.on(
this.sub((latest) => {
this.setState({ latestTime: latest });
chat.latestTime = latest.time || '';
chat.latest = latest;
chat.id = id;
this.setState({ chats: this.state.chats.set(id, chat) });
}),
);
chat.id = id;
this.setState({ chats: this.state.chats.set(id, chat) });
}),
);
iris
.local()
.get('scrollUp')
.on(this.sub(() => Helpers.animateScrollTop('.chat-list')));
iris
.public()
.get('hashtagSubscriptions')
.map()
.on(
this.sub((isSubscribed, hashtag) => {
if (isSubscribed) {
hashtags[hashtag] = true;
} else {
delete hashtags[hashtag];
}
this.setState({ hashtags });
}),
);
if (window.Notification && Notification.permission !== 'granted' && Notification.permission !== 'denied') {
if (
window.Notification &&
Notification.permission !== 'granted' &&
Notification.permission !== 'denied'
) {
setTimeout(() => {
$('#enable-notifications-prompt').slideDown();
}, 5000);
@ -74,14 +92,21 @@ class ChatList extends Component {
render() {
const activeChat = this.props.activeChat;
const sortedChats = _.orderBy(Array.from(this.state.chats.values()), ['latestTime', 'name'], ['desc', 'asc']);
const sortedChats = orderBy(
Array.from(this.state.chats.values()),
['latestTime', 'name'],
['desc', 'asc'],
);
return html`<section class="sidebar ${this.props.class || ''}">
<div id="enable-notifications-prompt" onClick=${() => iris.notifications.enableDesktopNotifications()}>
<div id="enable-notifications-prompt" onClick=${() =>
iris.notifications.enableDesktopNotifications()}>
<div class="title">${t('get_notified_new_messages')}</div>
<div><a>${t('turn_on_desktop_notifications')}</a></div>
</div>
<div class="chat-list">
<div tabindex="0" class="chat-item new ${activeChat === 'new' ? 'active-item' : ''}" onClick=${() => route('/chat/new')}>
<div tabindex="0" class="chat-item new ${
activeChat === 'new' ? 'active-item' : ''
}" onClick=${() => route('/chat/new')}>
<svg class="svg-inline--fa fa-smile fa-w-16" style="margin-right:10px;margin-top:3px" x="0px" y="0px"
viewBox="0 0 510 510">
<path fill="currentColor" d="M459,0H51C22.95,0,0,22.95,0,51v459l102-102h357c28.05,0,51-22.95,51-51V51C510,22.95,487.05,0,459,0z M102,178.5h306v51 H102V178.5z M306,306H102v-51h204V306z M408,153H102v-51h306V153z"/>
@ -89,18 +114,19 @@ class ChatList extends Component {
${t('new_chat')}
</div>
<${ScrollViewport}>
${sortedChats.map(chat =>
html`<${ChatListItem}
photo=${chat.photo}
active=${chat.id === activeChat}
key=${chat.id}
chat=${chat}
lates=${this.state.latestTime}/>`
)
}
${sortedChats.map(
(chat) =>
html`<${ChatListItem}
photo=${chat.photo}
active=${chat.id === activeChat}
key=${chat.id}
chat=${chat}
lates=${this.state.latestTime}
/>`,
)}
</${ScrollViewport}>
</div>
</section>`
</section>`;
}
}

View File

@ -1,17 +1,29 @@
import Component from '../../BaseComponent';
import Helpers from '../../Helpers';
import { html } from 'htm/preact';
import { route } from 'preact-router';
import { translate as t } from '../../translations/Translation';
import iris from 'iris-lib';
import Identicon from '../../components/Identicon';
import { route } from 'preact-router';
const seenIndicator = html`<span class="seen-indicator"><svg viewBox="0 0 59 42"><polygon fill="currentColor" points="40.6,12.1 17,35.7 7.4,26.1 4.6,29 17,41.3 43.4,14.9"></polygon><polygon class="iris-delivered-checkmark" fill="currentColor" points="55.6,12.1 32,35.7 29.4,33.1 26.6,36 32,41.3 58.4,14.9"></polygon></svg></span>`;
import Component from '../../BaseComponent';
import Identicon from '../../components/Identicon';
import Helpers from '../../Helpers';
import { translate as t } from '../../translations/Translation';
const seenIndicator = html`<span class="seen-indicator"
><svg viewBox="0 0 59 42">
<polygon
fill="currentColor"
points="40.6,12.1 17,35.7 7.4,26.1 4.6,29 17,41.3 43.4,14.9"
></polygon>
<polygon
class="iris-delivered-checkmark"
fill="currentColor"
points="55.6,12.1 32,35.7 29.4,33.1 26.6,36 32,41.3 58.4,14.9"
></polygon></svg
></span>`;
class ChatListItem extends Component {
constructor() {
super();
this.state = {latest: {}, unseen : {}};
this.state = { latest: {}, unseen: {} };
}
shouldComponentUpdate() {
@ -20,11 +32,16 @@ class ChatListItem extends Component {
componentDidMount() {
const chat = this.props.chat;
iris.local().get('channels').get(chat.id).get('unseen').on(this.sub(
(unseen) => {
this.setState({unseen});
}
));
iris
.local()
.get('channels')
.get(chat.id)
.get('unseen')
.on(
this.sub((unseen) => {
this.setState({ unseen });
}),
);
}
onKeyUp(e) {
@ -36,11 +53,13 @@ class ChatListItem extends Component {
render() {
const chat = this.props.chat;
const active = this.props.active ? "active-item" : "";
const seen = chat.theirMsgsLastSeenTime >= chat.latestTime ? "seen" : "";
const delivered = chat.theirLastActiveTime >= chat.latestTime ? "delivered" : "";
const active = this.props.active ? 'active-item' : '';
const seen = chat.theirMsgsLastSeenTime >= chat.latestTime ? 'seen' : '';
const delivered = chat.theirLastActiveTime >= chat.latestTime ? 'delivered' : '';
const hasUnseen = this.state.unseen ? 'has-unseen' : '';
const unseenEl = this.state.unseen ? html`<span class="unseen">${JSON.stringify(this.state.unseen)}</span>` : '';
const unseenEl = this.state.unseen
? html`<span class="unseen">${JSON.stringify(this.state.unseen)}</span>`
: '';
const activity = ['online', 'active'].indexOf(chat.activity) > -1 ? chat.activity : '';
const time = chat.latestTime && new Date(chat.latestTime);
let latestTimeText = Helpers.getRelativeTimeText(time);
@ -50,34 +69,43 @@ class ChatListItem extends Component {
name = html`📝 <b>${t('note_to_self')}</b>`;
}
let iconEl = chat.photo ?
html`<div class="identicon-container"><img src="${chat.photo}" class="round-borders" height=49 width=49 alt=""/></div>` :
html`<${Identicon} str=${chat.id} width=49/>`;
let iconEl = chat.photo
? html`<div class="identicon-container">
<img src="${chat.photo}" class="round-borders" height="49" width="49" alt="" />
</div>`
: html`<${Identicon} str=${chat.id} width="49" />`;
const latestEl = chat.isTyping || !chat.latest ? '' : html`<small class="latest">
${chat.latest.selfAuthored && seenIndicator}
${chat.latest.text}
</small>`;
const latestEl =
chat.isTyping || !chat.latest
? ''
: html`<small class="latest">
${chat.latest.selfAuthored && seenIndicator} ${chat.latest.text}
</small>`;
const typingIndicator = chat.isTyping ? html`<small class="typing-indicator">${t('typing')}</small>` : '';
const typingIndicator = chat.isTyping
? html`<small class="typing-indicator">${t('typing')}</small>`
: '';
const onlineIndicator = chat.id.length > 36 ? html`<div class="online-indicator"></div>` : '';
// TODO use button so we can use keyboard to navigate
return html`
<div onKeyUp=${e => this.onKeyUp(e)} role="button" tabindex="0" class="chat-item ${activity} ${hasUnseen} ${active} ${seen} ${delivered}" onClick=${() => route(`/chat/${ this.props.chat.id}`)}>
${iconEl}
${onlineIndicator}
<div class="text">
<div>
<span class="name">${name}</span>
<small class="latest-time">${latestTimeText}</small>
<div
onKeyUp=${(e) => this.onKeyUp(e)}
role="button"
tabindex="0"
class="chat-item ${activity} ${hasUnseen} ${active} ${seen} ${delivered}"
onClick=${() => route(`/chat/${this.props.chat.id}`)}
>
${iconEl} ${onlineIndicator}
<div class="text">
<div>
<span class="name">${name}</span>
<small class="latest-time">${latestTimeText}</small>
</div>
${typingIndicator} ${latestEl} ${unseenEl}
</div>
${typingIndicator}
${latestEl}
${unseenEl}
</div>
</div>
`;
}
}

View File

@ -1,43 +1,61 @@
import Helpers from '../../Helpers';
import { html } from 'htm/preact';
import { translate as t } from '../../translations/Translation';
import Torrent from '../../components/Torrent';
import iris from 'iris-lib';
import _ from 'lodash';
import $ from 'jquery';
import EmojiButton from '../../lib/emoji-button';
import MessageForm from '../../components/MessageForm';
import throttle from 'lodash/throttle';
const submitButton = html`
<button type="submit">
<svg class="svg-inline--fa fa-w-16" x="0px" y="0px" viewBox="0 0 486.736 486.736" style="enable-background:new 0 0 486.736 486.736;" width="100px" height="100px" fill="currentColor" stroke="#000000" stroke-width="0"><path fill="currentColor" d="M481.883,61.238l-474.3,171.4c-8.8,3.2-10.3,15-2.6,20.2l70.9,48.4l321.8-169.7l-272.4,203.4v82.4c0,5.6,6.3,9,11,5.9 l60-39.8l59.1,40.3c5.4,3.7,12.8,2.1,16.3-3.5l214.5-353.7C487.983,63.638,485.083,60.038,481.883,61.238z"></path></svg>
</button>`;
import MessageForm from '../../components/MessageForm';
import Torrent from '../../components/Torrent';
import Helpers from '../../Helpers';
import EmojiButton from '../../lib/emoji-button';
import { translate as t } from '../../translations/Translation';
const submitButton = html` <button type="submit">
<svg
class="svg-inline--fa fa-w-16"
x="0px"
y="0px"
viewBox="0 0 486.736 486.736"
style="enable-background:new 0 0 486.736 486.736;"
width="100px"
height="100px"
fill="currentColor"
stroke="#000000"
stroke-width="0"
>
<path
fill="currentColor"
d="M481.883,61.238l-474.3,171.4c-8.8,3.2-10.3,15-2.6,20.2l70.9,48.4l321.8-169.7l-272.4,203.4v82.4c0,5.6,6.3,9,11,5.9 l60-39.8l59.1,40.3c5.4,3.7,12.8,2.1,16.3-3.5l214.5-353.7C487.983,63.638,485.083,60.038,481.883,61.238z"
></path>
</svg>
</button>`;
class ChatMessageForm extends MessageForm {
componentDidMount() {
this.picker = new EmojiButton({position: 'top-start'});
this.picker.on('emoji', emoji => {
this.picker = new EmojiButton({ position: 'top-start' });
this.picker.on('emoji', (emoji) => {
const textEl = $(this.base).find('.new-msg');
textEl.val(textEl.val() + emoji);
textEl.focus();
});
if (!iris.util.isMobile && this.props.autofocus !== false) {
$(this.base).find(".new-msg").focus();
$(this.base).find('.new-msg').focus();
}
}
sendToPrivateOrGroup() {
const chat = iris.private(this.props.activeChat);
if (!chat) {
console.error("no chat", this.props.activeChat, "found");
console.error('no chat', this.props.activeChat, 'found');
return;
}
iris.local().get('channels').get(this.props.activeChat).get('msgDraft').put(null);
const textEl = $(this.base).find('.new-msg');
const text = textEl.val();
if (!text.length && !chat.attachments) { return; }
if (!text.length && !chat.attachments) {
return;
}
chat.setTyping && chat.setTyping(false);
const msg = {text};
const msg = { text };
if (this.props.replyingTo) {
msg.replyingTo = this.props.replyingTo;
}
@ -57,8 +75,10 @@ class ChatMessageForm extends MessageForm {
iris.local().get('channels').get(this.props.activeChat).get('msgDraft').put(null);
const textEl = $(this.base).find('.new-msg');
const text = textEl.val();
if (!text.length) { return; }
const msg = {text};
if (!text.length) {
return;
}
const msg = { text };
if (this.state.torrentId) {
msg.torrentId = this.state.torrentId;
}
@ -69,7 +89,7 @@ class ChatMessageForm extends MessageForm {
componentDidUpdate() {
if (!iris.util.isMobile && this.props.autofocus !== false) {
$(this.base).find(".new-msg").focus();
$(this.base).find('.new-msg').focus();
}
if ($('#attachment-preview:visible').length) {
$('#attachment-preview').append($('#webtorrent'));
@ -94,28 +114,38 @@ class ChatMessageForm extends MessageForm {
onMsgTextPaste(event) {
const pasted = (event.clipboardData || window.clipboardData).getData('text');
const magnetRegex = /^magnet:\?xt=urn:btih:*/;
if (pasted !== this.state.torrentId && pasted.indexOf('.torrent') > -1 || pasted.match(magnetRegex)) {
if (
(pasted !== this.state.torrentId && pasted.indexOf('.torrent') > -1) ||
pasted.match(magnetRegex)
) {
event.preventDefault();
this.setState({torrentId: pasted});
this.setState({ torrentId: pasted });
this.openAttachmentsPreview();
}
}
onMsgTextInput(event) {
const channel = iris.private(this.props.activeChat);
if (!channel) {return;}
if (!channel) {
return;
}
const val = $(event.target).val();
this.isTyping = this.isTyping !== undefined ? this.isTyping : false;
const getIsTyping = () => val.length > 0;
const setTyping = () => channel.setTyping(getIsTyping());
const setTypingThrottled = _.throttle(setTyping, 1000);
const setTypingThrottled = throttle(setTyping, 1000);
if (this.isTyping === getIsTyping()) {
setTypingThrottled();
} else {
setTyping();
}
this.isTyping = getIsTyping();
iris.local().get('channels').get(this.props.activeChat).get('msgDraft').put($(event.target).val());
iris
.local()
.get('channels')
.get(this.props.activeChat)
.get('msgDraft')
.put($(event.target).val());
}
attachFileClicked(event) {
@ -123,39 +153,45 @@ class ChatMessageForm extends MessageForm {
$(this.base).find('.attachment-input').click();
}
openAttachmentsPreview() { // TODO: this should be done using state, but we're editing an element in another component
openAttachmentsPreview() {
// TODO: this should be done using state, but we're editing an element in another component
$('#floating-day-separator').remove();
const attachmentsPreview = $('#attachment-preview');
attachmentsPreview.removeClass('gallery');
attachmentsPreview.empty();
let closeBtn = $('<button>').text(t('cancel')).click(() => this.closeAttachmentsPreview());
let closeBtn = $('<button>')
.text(t('cancel'))
.click(() => this.closeAttachmentsPreview());
attachmentsPreview.append(closeBtn);
let files = $(this.base).find('.attachment-input')[0].files;
if (files) {
attachmentsPreview.show();
$('#message-list').hide();
for (let i = 0;i < files.length;i++) {
Helpers.getBase64(files[i]).then(base64 => {
for (let i = 0; i < files.length; i++) {
Helpers.getBase64(files[i]).then((base64) => {
const channel = iris.private(this.props.activeChat);
channel.attachments = channel.attachments || [];
channel.attachments.push({type: 'image', data: base64});
channel.attachments.push({ type: 'image', data: base64 });
let preview = Helpers.setImgSrc($('<img>'), base64);
attachmentsPreview.append(preview);
});
}
$(this.base).find('.attachment-input').val(null)
$(this.base).find('.attachment-input').val(null);
$(this.base).find('.new-msg').focus();
}
$(document).off('keyup').on('keyup', e => {
if (e.key === "Escape") { // escape key maps to keycode `27`
$(document).off('keyup');
if ($('#attachment-preview:visible').length) {
this.closeAttachmentsPreview();
$(document)
.off('keyup')
.on('keyup', (e) => {
if (e.key === 'Escape') {
// escape key maps to keycode `27`
$(document).off('keyup');
if ($('#attachment-preview:visible').length) {
this.closeAttachmentsPreview();
}
}
}
});
});
}
closeAttachmentsPreview() {
@ -167,41 +203,92 @@ class ChatMessageForm extends MessageForm {
if (!this.props.hashtag) {
Helpers.scrollToMessageListBottom();
}
this.setState({torrentId:null});
this.setState({ torrentId: null });
}
webPush(msg) {
const chat = iris.private(this.props.activeChat);
const myKey = iris.session.getKey();
const shouldWebPush = (this.props.activeChat === myKey.pub) || !(chat.activity && chat.activity.isActive);
const shouldWebPush =
this.props.activeChat === myKey.pub || !(chat.activity && chat.activity.isActive);
if (shouldWebPush) {
const myName = iris.session.getMyName();
const title = chat.uuid ? chat.name : myName;
const body = chat.uuid ? `${myName}: ${msg.text}` : msg.text;
iris.notifications.sendWebPushNotification(this.props.activeChat, {title, body});
iris.notifications.sendWebPushNotification(this.props.activeChat, {
title,
body,
});
}
}
render() {
const contentBtns = html`
<button type="button" class="attach-file-btn" onClick=${e => this.attachFileClicked(e)}>
<svg width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M21.586 10.461l-10.05 10.075c-1.95 1.949-5.122 1.949-7.071 0s-1.95-5.122 0-7.072l10.628-10.585c1.17-1.17 3.073-1.17 4.243 0 1.169 1.17 1.17 3.072 0 4.242l-8.507 8.464c-.39.39-1.024.39-1.414 0s-.39-1.024 0-1.414l7.093-7.05-1.415-1.414-7.093 7.049c-1.172 1.172-1.171 3.073 0 4.244s3.071 1.171 4.242 0l8.507-8.464c.977-.977 1.464-2.256 1.464-3.536 0-2.769-2.246-4.999-5-4.999-1.28 0-2.559.488-3.536 1.465l-10.627 10.583c-1.366 1.368-2.05 3.159-2.05 4.951 0 3.863 3.13 7 7 7 1.792 0 3.583-.684 4.95-2.05l10.05-10.075-1.414-1.414z"/></svg>
const contentBtns = html` <button
type="button"
class="attach-file-btn"
onClick=${(e) => this.attachFileClicked(e)}
>
<svg width="24" height="24" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M21.586 10.461l-10.05 10.075c-1.95 1.949-5.122 1.949-7.071 0s-1.95-5.122 0-7.072l10.628-10.585c1.17-1.17 3.073-1.17 4.243 0 1.169 1.17 1.17 3.072 0 4.242l-8.507 8.464c-.39.39-1.024.39-1.414 0s-.39-1.024 0-1.414l7.093-7.05-1.415-1.414-7.093 7.049c-1.172 1.172-1.171 3.073 0 4.244s3.071 1.171 4.242 0l8.507-8.464c.977-.977 1.464-2.256 1.464-3.536 0-2.769-2.246-4.999-5-4.999-1.28 0-2.559.488-3.536 1.465l-10.627 10.583c-1.366 1.368-2.05 3.159-2.05 4.951 0 3.863 3.13 7 7 7 1.792 0 3.583-.684 4.95-2.05l10.05-10.075-1.414-1.414z"
/>
</svg>
</button>
<button class="emoji-picker-btn hidden-xs" type="button" onClick=${e => this.onEmojiButtonClick(e)}>
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="smile" class="svg-inline--fa fa-smile fa-w-16" role="img" viewBox="0 0 496 512"><path fill="currentColor" d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm4 72.6c-20.8 25-51.5 39.4-84 39.4s-63.2-14.3-84-39.4c-8.5-10.2-23.7-11.5-33.8-3.1-10.2 8.5-11.5 23.6-3.1 33.8 30 36 74.1 56.6 120.9 56.6s90.9-20.6 120.9-56.6c8.5-10.2 7.1-25.3-3.1-33.8-10.1-8.4-25.3-7.1-33.8 3.1z"></path></svg>
<button
class="emoji-picker-btn hidden-xs"
type="button"
onClick=${(e) => this.onEmojiButtonClick(e)}
>
<svg
aria-hidden="true"
focusable="false"
data-prefix="far"
data-icon="smile"
class="svg-inline--fa fa-smile fa-w-16"
role="img"
viewBox="0 0 496 512"
>
<path
fill="currentColor"
d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm4 72.6c-20.8 25-51.5 39.4-84 39.4s-63.2-14.3-84-39.4c-8.5-10.2-23.7-11.5-33.8-3.1-10.2 8.5-11.5 23.6-3.1 33.8 30 36 74.1 56.6 120.9 56.6s90.9-20.6 120.9-56.6c8.5-10.2 7.1-25.3-3.1-33.8-10.1-8.4-25.3-7.1-33.8 3.1z"
></path>
</svg>
</button>`;
return html`<form autocomplete="off" class="message-form ${this.props.class || ''}" onSubmit=${e => this.onMsgFormSubmit(e)}>
return html`<form
autocomplete="off"
class="message-form ${this.props.class || ''}"
onSubmit=${(e) => this.onMsgFormSubmit(e)}
>
${contentBtns}
<input name="attachment-input" type="file" class="hidden attachment-input" accept="image/*" multiple onChange=${() => this.openAttachmentsPreview()}/>
<input onPaste=${e => this.onMsgTextPaste(e)} onInput=${e => this.onMsgTextInput(e)} class="new-msg" type="text" placeholder="${t('type_a_message')}" autocomplete="off" autocorrect="off" autocapitalize="sentences" spellcheck="off"/>
<input
name="attachment-input"
type="file"
class="hidden attachment-input"
accept="image/*"
multiple
onChange=${() => this.openAttachmentsPreview()}
/>
<input
onPaste=${(e) => this.onMsgTextPaste(e)}
onInput=${(e) => this.onMsgTextInput(e)}
class="new-msg"
type="text"
placeholder="${t('type_a_message')}"
autocomplete="off"
autocorrect="off"
autocapitalize="sentences"
spellcheck="off"
/>
${submitButton}
<div id="webtorrent">
${this.state.torrentId ? html`<${Torrent} preview=${true} torrentId=${this.state.torrentId}/>` : ''}
${this.state.torrentId
? html`<${Torrent} preview=${true} torrentId=${this.state.torrentId} />`
: ''}
</div>
</form>`;
}
}
export default ChatMessageForm;

View File

@ -1,26 +1,37 @@
import Helpers from '../../Helpers';
import { Helmet } from 'react-helmet';
import { html } from 'htm/preact';
import {createRef} from 'preact';
import { translate as t } from '../../translations/Translation';
import iris from 'iris-lib';
import Identicon from '../../components/Identicon';
import ChatMessageForm from './ChatMessageForm';
import Name from '../../components/Name';
import $ from 'jquery';
import {Helmet} from 'react-helmet';
import { createRef } from 'preact';
import Component from '../../BaseComponent';
import Identicon from '../../components/Identicon';
import MessageFeed from '../../components/MessageFeed';
import OnboardingNotification from "../../components/OnboardingNotification";
import Name from '../../components/Name';
import OnboardingNotification from '../../components/OnboardingNotification';
import Helpers from '../../Helpers';
import { translate as t } from '../../translations/Translation';
import ChatMessageForm from './ChatMessageForm';
const caretDownSvg = html`
<svg x="0px" y="0px"
width="451.847px" height="451.847px" viewBox="0 0 451.847 451.847" style="enable-background:new 0 0 451.847 451.847;">
<g>
<path fill="currentColor" d="M225.923,354.706c-8.098,0-16.195-3.092-22.369-9.263L9.27,151.157c-12.359-12.359-12.359-32.397,0-44.751
<svg
x="0px"
y="0px"
width="451.847px"
height="451.847px"
viewBox="0 0 451.847 451.847"
style="enable-background:new 0 0 451.847 451.847;"
>
<g>
<path
fill="currentColor"
d="M225.923,354.706c-8.098,0-16.195-3.092-22.369-9.263L9.27,151.157c-12.359-12.359-12.359-32.397,0-44.751
c12.354-12.354,32.388-12.354,44.748,0l171.905,171.915l171.906-171.909c12.359-12.354,32.391-12.354,44.744,0
c12.365,12.354,12.365,32.392,0,44.751L248.292,345.449C242.115,351.621,234.018,354.706,225.923,354.706z"/>
</g>
</svg>
c12.365,12.354,12.365,32.392,0,44.751L248.292,345.449C242.115,351.621,234.018,354.706,225.923,354.706z"
/>
</g>
</svg>
`;
function copyMyChatLinkClicked(e) {
@ -41,7 +52,11 @@ export default class ChatMain extends Component {
super();
this.hashtagChatRef = createRef();
this.participants = {};
this.state = {sortedParticipants: [], showParticipants: true, stickToBottom: true};
this.state = {
sortedParticipants: [],
showParticipants: true,
stickToBottom: true,
};
}
shouldComponentUpdate() {
@ -51,98 +66,134 @@ export default class ChatMain extends Component {
componentDidMount() {
iris.local().get('showParticipants').put(true);
iris.local().get('showParticipants').on(this.inject());
iris.group().on(`hashtagSubscriptions/${this.props.hashtag}`, this.sub(
(isSubscribing, k, b, c, from) => {
iris.group().on(
`hashtagSubscriptions/${this.props.hashtag}`,
this.sub((isSubscribing, k, b, c, from) => {
if (isSubscribing && !this.participants[from]) {
this.participants[from] = {};
iris.public(from).get('activity').on(this.sub(
(activity) => {
if (this.participants[from]) { this.participants[from].activity = activity; }
this.setSortedParticipants();
}
));
iris
.public(from)
.get('activity')
.on(
this.sub((activity) => {
if (this.participants[from]) {
this.participants[from].activity = activity;
}
this.setSortedParticipants();
}),
);
} else {
delete this.participants[from];
}
this.setSortedParticipants();
}
));
iris.local().get('hashtags').get(this.props.hashtag).get('msgDraft').once(m => $('.new-msg').val(m));
const el = $("#message-view");
}),
);
iris
.local()
.get('hashtags')
.get(this.props.hashtag)
.get('msgDraft')
.once((m) => $('.new-msg').val(m));
const el = $('#message-view');
el.off('scroll').on('scroll', () => {
const scrolledToBottom = (el[0].scrollHeight - el.scrollTop() == el.outerHeight());
const scrolledToBottom = el[0].scrollHeight - el.scrollTop() == el.outerHeight();
if (this.state.stickToBottom && !scrolledToBottom) {
this.setState({stickToBottom: false});
this.setState({ stickToBottom: false });
} else if (!this.state.stickToBottom && scrolledToBottom) {
this.setState({stickToBottom: true});
this.setState({ stickToBottom: true });
}
});
}
setSortedParticipants() {
const sortedParticipants = Object.keys(this.participants)
.sort((a, b) => {
const sortedParticipants = Object.keys(this.participants).sort((a, b) => {
const aO = this.participants[a];
const bO = this.participants[b];
const aActive = new Date(aO && aO.activity && aO.activity.time || 0);
const bActive = new Date(bO && bO.activity && bO.activity.time || 0);
const aActive = new Date((aO && aO.activity && aO.activity.time) || 0);
const bActive = new Date((bO && bO.activity && bO.activity.time) || 0);
if (Math.abs(aActive - bActive) < 10000) {
return a > b ? -1 : 1;
}
if (aActive > bActive) { return -1; }
else if (aActive < bActive) { return 1; }
return 0;
if (aActive > bActive) {
return -1;
} else if (aActive < bActive) {
return 1;
}
return 0;
});
this.setState({sortedParticipants});
this.setState({ sortedParticipants });
}
componentDidUpdate() {
if (this.state.stickToBottom) {
Helpers.scrollToMessageListBottom();
}
$('.msg-content img').off('load').on('load', () => this.state.stickToBottom && Helpers.scrollToMessageListBottom());
$('.msg-content img')
.off('load')
.on('load', () => this.state.stickToBottom && Helpers.scrollToMessageListBottom());
}
scrollDown() {
Helpers.scrollToMessageListBottom();
const el = document.getElementById("message-list");
const el = document.getElementById('message-list');
el && (el.style.paddingBottom = 0);
}
render() {
return html`
<${Helmet}><title>${this.chat && this.chat.name || 'Messages'}</title><//>
<${Helmet}><title>${(this.chat && this.chat.name) || 'Messages'}</title><//>
<div id="chat-main">
<div class="main-view public-messages-view" id="message-view" ref=${this.hashtagChatRef}>
<${MessageFeed} reverse=${true} key=${this.props.hashtag} scrollElement=${this.hashtagChatRef.current} group="everyone" path="hashtags/${this.props.hashtag}"/>
<${MessageFeed}
reverse=${true}
key=${this.props.hashtag}
scrollElement=${this.hashtagChatRef.current}
group="everyone"
path="hashtags/${this.props.hashtag}"
/>
<${OnboardingNotification} />
<div id="attachment-preview" class="attachment-preview" style="display:none"></div>
</div>
<div id="scroll-down-btn" style="display:none;" onClick=${() => this.scrollDown()}>${caretDownSvg}</div>
<div id="scroll-down-btn" style="display:none;" onClick=${() => this.scrollDown()}>
${caretDownSvg}
</div>
<div id="not-seen-by-them" style="display: none">
<p dangerouslySetInnerHTML=${{ __html: t('if_other_person_doesnt_see_message') }}></p>
<p><button onClick=${e => copyMyChatLinkClicked(e)}>${t('copy_your_invite_link')}</button></p>
<p
dangerouslySetInnerHTML=${{
__html: t('if_other_person_doesnt_see_message'),
}}
></p>
<p>
<button onClick=${(e) => copyMyChatLinkClicked(e)}>
${t('copy_your_invite_link')}
</button>
</p>
</div>
<div class="chat-message-form">
<${ChatMessageForm} key=${this.props.hashtag} hashtag=${this.props.hashtag} onSubmit=${() => this.scrollDown()} />
<${ChatMessageForm}
key=${this.props.hashtag}
hashtag=${this.props.hashtag}
onSubmit=${() => this.scrollDown()}
/>
</div>
</div>
<div class="participant-list ${this.state.showParticipants ? 'open' : ''}">
${this.state.sortedParticipants.length ? html`
<small>${this.state.sortedParticipants.length} ${t('subscribers')}</small>
` : ''}
${this.state.sortedParticipants.map(k =>
html`
<a href="/profile/${k}">
<span class="text">
<${Identicon} key="i${k}" str=${k} width=30 activity=${true}/>
<${Name} pub=${k} key="t${k}" />
</span>
</a>
`
${this.state.sortedParticipants.length
? html` <small>${this.state.sortedParticipants.length} ${t('subscribers')}</small> `
: ''}
${this.state.sortedParticipants.map(
(k) =>
html`
<a href="/profile/${k}">
<span class="text">
<${Identicon} key="i${k}" str=${k} width="30" activity=${true} />
<${Name} pub=${k} key="t${k}" />
</span>
</a>
`,
)}
</div>
`;

View File

@ -1,30 +1,40 @@
import Helpers from '../../Helpers';
import { Helmet } from 'react-helmet';
import { html } from 'htm/preact';
import {createRef} from 'preact';
import { translate as t } from '../../translations/Translation';
import iris from 'iris-lib';
import Identicon from '../../components/Identicon';
import Message from '../../components/Message';
import ChatMessageForm from './ChatMessageForm';
import Name from '../../components/Name';
import NewChat from './newchat/NewChat';
import _ from 'lodash';
import $ from 'jquery';
import {Helmet} from 'react-helmet';
import Component from '../../BaseComponent';
import Button from '../../components/basic/Button';
import throttle from 'lodash/throttle';
import { createRef } from 'preact';
import { Router } from 'preact-router';
import Component from '../../BaseComponent';
import Button from '../../components/basic/Button';
import Identicon from '../../components/Identicon';
import Message from '../../components/Message';
import Name from '../../components/Name';
import Helpers from '../../Helpers';
import { translate as t } from '../../translations/Translation';
import NewChat from './newchat/NewChat';
import ChatMessageForm from './ChatMessageForm';
const caretDownSvg = html`
<svg x="0px" y="0px"
width="451.847px" height="451.847px" viewBox="0 0 451.847 451.847" style="enable-background:new 0 0 451.847 451.847;">
<g>
<path fill="currentColor" d="M225.923,354.706c-8.098,0-16.195-3.092-22.369-9.263L9.27,151.157c-12.359-12.359-12.359-32.397,0-44.751
<svg
x="0px"
y="0px"
width="451.847px"
height="451.847px"
viewBox="0 0 451.847 451.847"
style="enable-background:new 0 0 451.847 451.847;"
>
<g>
<path
fill="currentColor"
d="M225.923,354.706c-8.098,0-16.195-3.092-22.369-9.263L9.27,151.157c-12.359-12.359-12.359-32.397,0-44.751
c12.354-12.354,32.388-12.354,44.748,0l171.905,171.915l171.906-171.909c12.359-12.354,32.391-12.354,44.744,0
c12.365,12.354,12.365,32.392,0,44.751L248.292,345.449C242.115,351.621,234.018,354.706,225.923,354.706z"/>
</g>
</svg>
c12.365,12.354,12.365,32.392,0,44.751L248.292,345.449C242.115,351.621,234.018,354.706,225.923,354.706z"
/>
</g>
</svg>
`;
function copyMyChatLinkClicked(e) {
@ -49,7 +59,7 @@ export default class PrivateChat extends Component {
sortedParticipants: [],
showParticipants: true,
stickToBottom: true,
noLongerParticipant: false
noLongerParticipant: false,
};
}
@ -68,102 +78,132 @@ export default class PrivateChat extends Component {
this.chat = iris.session.newChannel(this.props.id);
}
if (this.chat) {
clearInterval(this.iv)
clearInterval(this.iv);
iris.session.subscribeToMsgs(this.props.id);
iris.notifications.changeChatUnseenCount(this.props.id, 0);
this.chat.setMyMsgsLastSeenTime();
Helpers.scrollToMessageListBottom();
this.chat.setMyMsgsLastSeenTime();
}
}
};
this.iv = setInterval(go, 3000);
go();
iris.local().get('showParticipants').put(true);
iris.local().get('showParticipants').on(this.inject());
iris.local().get('channels').get(this.props.id).get('participants').map(this.sub(
(v, k) => {
const hasAlready = !!this.participants[k];
this.participants[k] = v;
if (!!v && !hasAlready) {
iris.public(k).get('activity').on(this.sub(
(activity) => {
if (this.participants[k]) { this.participants[k].activity = activity; }
this.setSortedParticipants();
}
));
}
this.setSortedParticipants();
}
));
iris.local().get('channels').get(this.props.id).get('msgDraft').once(m => $('.new-msg').val(m));
iris
.local()
.get('channels')
.get(this.props.id)
.get('participants')
.map(
this.sub((v, k) => {
const hasAlready = !!this.participants[k];
this.participants[k] = v;
if (!!v && !hasAlready) {
iris
.public(k)
.get('activity')
.on(
this.sub((activity) => {
if (this.participants[k]) {
this.participants[k].activity = activity;
}
this.setSortedParticipants();
}),
);
}
this.setSortedParticipants();
}),
);
iris
.local()
.get('channels')
.get(this.props.id)
.get('msgDraft')
.once((m) => $('.new-msg').val(m));
const node = iris.local().get('channels').get(this.props.id).get('msgs');
const limitedUpdate = _.throttle(() => this.setState({
sortedMessages: Object.keys(this.msgs).sort().map(k => this.msgs[k])
}), 100); // TODO: this is jumpy, as if reverse sorting is broken? why isn't MessageFeed the same?
const limitedUpdate = throttle(
() =>
this.setState({
sortedMessages: Object.keys(this.msgs)
.sort()
.map((k) => this.msgs[k]),
}),
100,
); // TODO: this is jumpy, as if reverse sorting is broken? why isn't MessageFeed the same?
this.msgs = {};
node.map(this.sub(
(msg, time) => {
node.map(
this.sub((msg, time) => {
this.msgs[time] = msg;
limitedUpdate();
}
));
const container = document.getElementById("message-list");
}),
);
const container = document.getElementById('message-list');
container.style.paddingBottom = 0;
container.style.paddingTop = 0;
const el = $("#message-view");
const el = $('#message-view');
el.off('scroll').on('scroll', () => {
const scrolledToBottom = (el[0].scrollHeight - el.scrollTop() == el.outerHeight());
const scrolledToBottom = el[0].scrollHeight - el.scrollTop() == el.outerHeight();
if (this.state.stickToBottom && !scrolledToBottom) {
this.setState({stickToBottom: false});
this.setState({ stickToBottom: false });
} else if (!this.state.stickToBottom && scrolledToBottom) {
this.setState({stickToBottom: true});
this.setState({ stickToBottom: true });
}
});
}
setSortedParticipants = _.throttle(() => {
let noLongerParticipant = true;
const sortedParticipants = Object.keys(this.participants)
.filter(k => {
if (k === 'undefined') return false;
const p = this.participants[k];
const hasPermissions = p && p.read && p.write;
if (noLongerParticipant && hasPermissions && k === iris.session.getPubKey()) {
noLongerParticipant = false;
}
return hasPermissions;
})
.sort((a, b) => {
const aO = this.participants[a];
const bO = this.participants[b];
const aActive = new Date(aO && aO.activity && aO.activity.time || 0);
const bActive = new Date(bO && bO.activity && bO.activity.time || 0);
if (Math.abs(aActive - bActive) < 10000) {
return a > b ? -1 : 1;
}
if (aActive > bActive) { return -1; }
else if (aActive < bActive) { return 1; }
return 0;
});
this.setState({sortedParticipants, noLongerParticipant});
}, 2000, {leading: true});
setSortedParticipants = throttle(
() => {
let noLongerParticipant = true;
const sortedParticipants = Object.keys(this.participants)
.filter((k) => {
if (k === 'undefined') return false;
const p = this.participants[k];
const hasPermissions = p && p.read && p.write;
if (noLongerParticipant && hasPermissions && k === iris.session.getPubKey()) {
noLongerParticipant = false;
}
return hasPermissions;
})
.sort((a, b) => {
const aO = this.participants[a];
const bO = this.participants[b];
const aActive = new Date((aO && aO.activity && aO.activity.time) || 0);
const bActive = new Date((bO && bO.activity && bO.activity.time) || 0);
if (Math.abs(aActive - bActive) < 10000) {
return a > b ? -1 : 1;
}
if (aActive > bActive) {
return -1;
} else if (aActive < bActive) {
return 1;
}
return 0;
});
this.setState({ sortedParticipants, noLongerParticipant });
},
2000,
{ leading: true },
);
componentDidUpdate() {
if (this.state.stickToBottom) {
Helpers.scrollToMessageListBottom();
}
$('.msg-content img').off('load').on('load', () => this.state.stickToBottom && Helpers.scrollToMessageListBottom());
setTimeout(() => {
if (this.chat && !this.chat.uuid && this.props.id !== iris.session.getPubKey()) {
if ($('.msg.our').length && !$('.msg.their').length && !this.chat.theirMsgsLastSeenTime) {
$('#not-seen-by-them').slideDown();
} else {
$('#not-seen-by-them').slideUp();
}
$('.msg-content img')
.off('load')
.on('load', () => this.state.stickToBottom && Helpers.scrollToMessageListBottom());
setTimeout(() => {
if (this.chat && !this.chat.uuid && this.props.id !== iris.session.getPubKey()) {
if ($('.msg.our').length && !$('.msg.their').length && !this.chat.theirMsgsLastSeenTime) {
$('#not-seen-by-them').slideDown();
} else {
$('#not-seen-by-them').slideUp();
}
}, 2000);
}
}, 2000);
}
componentWillUnmount() {
@ -179,7 +219,11 @@ export default class PrivateChat extends Component {
pos = currentDaySeparator.position();
}
let s = currentDaySeparator.clone();
let center = $('<div>').css({position: 'fixed', top: 70, 'text-align': 'center'}).attr('id', 'floating-day-separator').width($('#message-view').width()).append(s);
let center = $('<div>')
.css({ position: 'fixed', top: 70, 'text-align': 'center' })
.attr('id', 'floating-day-separator')
.width($('#message-view').width())
.append(s);
$('#floating-day-separator').remove();
setTimeout(() => s.fadeOut(), 2000);
$('#message-view').prepend(center);
@ -196,17 +240,21 @@ export default class PrivateChat extends Component {
}
onMessageViewScroll() {
this.messageViewScrollHandler = this.messageViewScrollHandler || _.throttle(() => {
if ($('#attachment-preview:visible').length) { return; }
this.addFloatingDaySeparator();
this.toggleScrollDownBtn();
}, 200);
this.messageViewScrollHandler =
this.messageViewScrollHandler ||
throttle(() => {
if ($('#attachment-preview:visible').length) {
return;
}
this.addFloatingDaySeparator();
this.toggleScrollDownBtn();
}, 200);
this.messageViewScrollHandler();
}
scrollDown() {
Helpers.scrollToMessageListBottom();
const el = document.getElementById("message-list");
const el = document.getElementById('message-list');
el && (el.style.paddingBottom = 0);
}
@ -218,47 +266,54 @@ export default class PrivateChat extends Component {
let previousDateStr;
let previousFrom;
const msgListContent = [];
this.state.sortedMessages && Object.values(this.state.sortedMessages).forEach(msg => {
if (typeof msg !== 'object') {
try {
msg = JSON.parse(msg);
} catch (e) {
console.error('JSON.parse(msg) failed', e);
return;
this.state.sortedMessages &&
Object.values(this.state.sortedMessages).forEach((msg) => {
if (typeof msg !== 'object') {
try {
msg = JSON.parse(msg);
} catch (e) {
console.error('JSON.parse(msg) failed', e);
return;
}
}
}
const date = typeof msg.time === 'string' ? new Date(msg.time) : msg.time;
let isDifferentDay;
if (date) {
const dateStr = date.toLocaleDateString();
if (dateStr !== previousDateStr) {
isDifferentDay = true;
let separatorText = iris.util.getDaySeparatorText(date, dateStr, now, nowStr);
msgListContent.push(html`<div class="day-separator">${t(separatorText)}</div>`);
const date = typeof msg.time === 'string' ? new Date(msg.time) : msg.time;
let isDifferentDay;
if (date) {
const dateStr = date.toLocaleDateString();
if (dateStr !== previousDateStr) {
isDifferentDay = true;
let separatorText = iris.util.getDaySeparatorText(date, dateStr, now, nowStr);
msgListContent.push(html`<div class="day-separator">${t(separatorText)}</div>`);
}
previousDateStr = dateStr;
}
previousDateStr = dateStr;
}
let showName = false;
if (isDifferentDay || (previousFrom && (msg.from !== previousFrom))) {
msgListContent.push(html`<div class="from-separator"/>`);
showName = true;
}
previousFrom = msg.from;
msgListContent.push(html`
<${Message} ...${msg} showName=${showName} key=${msg.time} chatId=${this.props.id}/>
`);
});
let showName = false;
if (isDifferentDay || (previousFrom && msg.from !== previousFrom)) {
msgListContent.push(html`<div class="from-separator" />`);
showName = true;
}
previousFrom = msg.from;
msgListContent.push(html`
<${Message} ...${msg} showName=${showName} key=${msg.time} chatId=${this.props.id} />
`);
});
mainView = html`
<div class="main-view" id="message-view" onScroll=${e => this.onMessageViewScroll(e)}>
<div id="message-list">
${msgListContent}
</div>
<div id="attachment-preview" class="attachment-preview" style="display:none"></div>
</div>`;
mainView = html` <div
class="main-view"
id="message-view"
onScroll=${(e) => this.onMessageViewScroll(e)}
>
<div id="message-list">${msgListContent}</div>
<div id="attachment-preview" class="attachment-preview" style="display:none"></div>
</div>`;
} else {
mainView = <Router><NewChat path="/chat/new/:view?" /><NewChat path="/chat" /></Router>;
mainView = (
<Router>
<NewChat path="/chat/new/:view?" />
<NewChat path="/chat" />
</Router>
);
}
return mainView;
}
@ -266,45 +321,70 @@ export default class PrivateChat extends Component {
renderParticipantList() {
const participants = this.state.sortedParticipants;
return this.props.id && this.props.id !== 'new' && this.props.id !== 'InviteView' && this.props.id !== 'QRView' && this.props.id.length < 40 ? html`
<div class="participant-list ${this.state.showParticipants ? 'open' : ''}">
${participants.length ? html`
<small>${participants.length} ${t('participants')}</small>
` : ''}
${participants.map(k =>
html`
<a href="/profile/${k}">
<span class="text">
<${Identicon} key="i${k}" str=${k} width=30 activity=${true}/>
<${Name} pub=${k} key="t${k}" />
</span>
</a>
`
)}
</div>
`: '';
return this.props.id &&
this.props.id !== 'new' &&
this.props.id !== 'InviteView' &&
this.props.id !== 'QRView' &&
this.props.id.length < 40
? html`
<div class="participant-list ${this.state.showParticipants ? 'open' : ''}">
${participants.length
? html` <small>${participants.length} ${t('participants')}</small> `
: ''}
${participants.map(
(k) =>
html`
<a href="/profile/${k}">
<span class="text">
<${Identicon} key="i${k}" str=${k} width="30" activity=${true} />
<${Name} pub=${k} key="t${k}" />
</span>
</a>
`,
)}
</div>
`
: '';
}
renderMsgForm() {
return this.props.id && this.props.id.length > 20 ? html`
<div id="scroll-down-btn" style="display:none;" onClick=${() => this.scrollDown()}>${caretDownSvg}</div>
<div id="not-seen-by-them" style="display: none">
<p dangerouslySetInnerHTML=${{ __html: t('if_other_person_doesnt_see_message') }}></p>
<p><${Button} onClick=${e => copyMyChatLinkClicked(e)}>${t('copy_your_invite_link')}<//></p>
</div>
<div class="chat-message-form">
${this.state.noLongerParticipant ? html`<div style="text-align:center">You can't send messages to this group because you're no longer a participant.</div>` :
html`<${ChatMessageForm} key=${this.props.id} activeChat=${this.props.id} onSubmit=${() => this.scrollDown()}/>`}
</div>
`: '';
return this.props.id && this.props.id.length > 20
? html`
<div id="scroll-down-btn" style="display:none;" onClick=${() => this.scrollDown()}>
${caretDownSvg}
</div>
<div id="not-seen-by-them" style="display: none">
<p
dangerouslySetInnerHTML=${{
__html: t('if_other_person_doesnt_see_message'),
}}
></p>
<p>
<${Button} onClick=${(e) => copyMyChatLinkClicked(e)}
>${t('copy_your_invite_link')}<//
>
</p>
</div>
<div class="chat-message-form">
${this.state.noLongerParticipant
? html`<div style="text-align:center">
You can't send messages to this group because you're no longer a participant.
</div>`
: html`<${ChatMessageForm}
key=${this.props.id}
activeChat=${this.props.id}
onSubmit=${() => this.scrollDown()}
/>`}
</div>
`
: '';
}
render() {
return html`
<${Helmet}><title>${this.chat && this.chat.name || 'Messages'}</title><//>
<${Helmet}><title>${(this.chat && this.chat.name) || 'Messages'}</title><//>
<div id="chat-main" class="${this.props.id ? '' : 'hidden-xs'}">
${this.renderMainView()}
${this.renderMsgForm()}
${this.renderMainView()} ${this.renderMsgForm()}
</div>
${this.renderParticipantList()}
`;

View File

@ -1,66 +1,69 @@
import { translate as t } from '../../../translations/Translation';
import iris from 'iris-lib';
import $ from 'jquery';
import { route } from 'preact-router';
import Component from '../../../BaseComponent';
import Button from '../../../components/basic/Button';
import { route } from 'preact-router';
import iris from 'iris-lib';
import Helpers from '../../../Helpers';
import { translate as t } from '../../../translations/Translation';
class InviteView extends Component {
constructor() {
super();
this.chatLinks = {};
this.state = {chatLinks: {}};
}
constructor() {
super();
this.chatLinks = {};
this.state = { chatLinks: {} };
}
componentDidMount() {
this.chatLinks = this.props.chatLinks;
this.setState({chatLinks: this.props.chatLinks});
}
onPasteChatLink(e) {
const val = $(e.target).val();
Helpers.followChatLink(val);
$(e.target).val('');
}
onCreateGroupSubmit(e) {
e.preventDefault();
if ($('#new-group-name').val().length) {
let c = new iris.Channel({
gun: iris.global(),
key: iris.session.getKey(),
participants: [],
});
c.put('name', $('#new-group-name').val());
$('#new-group-name').val('');
iris.session.addChannel(c);
route(`/group/${ c.uuid}`);
}
}
render(){
return(
<>
<div>
<h2>{t('add_contact_or_create_group')}</h2>
<h3>{t('have_someones_invite_link')}</h3>
<div class="btn-group">
<input id="paste-chat-link" onInput={e => this.onPasteChatLink(e)} type="text" placeholder={t('paste_their_invite_link')} />
</div>
<h3>{t('new_group')}</h3>
<p>
<form onSubmit={e => this.onCreateGroupSubmit(e)}>
<input id="new-group-name" type="text" placeholder={t('group_name')} />
<Button type="submit">{t('create')}</Button>
</form>
</p>
</div>
</>
);
componentDidMount() {
this.chatLinks = this.props.chatLinks;
this.setState({ chatLinks: this.props.chatLinks });
}
onPasteChatLink(e) {
const val = $(e.target).val();
Helpers.followChatLink(val);
$(e.target).val('');
}
onCreateGroupSubmit(e) {
e.preventDefault();
if ($('#new-group-name').val().length) {
let c = new iris.Channel({
gun: iris.global(),
key: iris.session.getKey(),
participants: [],
});
c.put('name', $('#new-group-name').val());
$('#new-group-name').val('');
iris.session.addChannel(c);
route(`/group/${c.uuid}`);
}
}
render() {
return (
<>
<div>
<h2>{t('add_contact_or_create_group')}</h2>
<h3>{t('have_someones_invite_link')}</h3>
<div class="btn-group">
<input
id="paste-chat-link"
onInput={(e) => this.onPasteChatLink(e)}
type="text"
placeholder={t('paste_their_invite_link')}
/>
</div>
<h3>{t('new_group')}</h3>
<p>
<form onSubmit={(e) => this.onCreateGroupSubmit(e)}>
<input id="new-group-name" type="text" placeholder={t('group_name')} />
<Button type="submit">{t('create')}</Button>
</form>
</p>
</div>
</>
);
}
}
export default InviteView;
export default InviteView;

View File

@ -1,83 +1,121 @@
import { html } from 'htm/preact';
import { translate as t } from '../../../translations/Translation';
import iris from 'iris-lib';
import CopyButton from '../../../components/CopyButton';
import Button from '../../../components/basic/Button';
import $ from 'jquery';
import Component from '../../../BaseComponent';
import { route } from 'preact-router';
import Component from '../../../BaseComponent';
import Button from '../../../components/basic/Button';
import CopyButton from '../../../components/CopyButton';
import { translate as t } from '../../../translations/Translation';
class MainView extends Component {
constructor() {
super();
this.state = {chatLinks: {}};
this.removeChatLink = this.removeChatLink.bind(this);
}
constructor() {
super();
this.state = { chatLinks: {} };
this.removeChatLink = this.removeChatLink.bind(this);
}
removeChatLink(id) {
iris.local().get('chatLinks').get(id).put(null);
this.props.chatLinks[id] = null;
this.setState({chatLinks: this.props.chatLinks});
this.forceUpdate();
return iris.Channel.removePrivateChatLink(iris.global(), iris.session.getKey(), id);
}
componentDidMount() {
this.setState({chatLinks: this.props.chatLinks});
}
removeChatLink(id) {
iris.local().get('chatLinks').get(id).put(null);
this.props.chatLinks[id] = null;
this.setState({ chatLinks: this.props.chatLinks });
this.forceUpdate();
return iris.Channel.removePrivateChatLink(iris.global(), iris.session.getKey(), id);
}
componentDidMount() {
this.setState({ chatLinks: this.props.chatLinks });
}
render(){
return(
<>
<h2>{t("invite_people_or_create_group")}</h2>
<div class="chat-new-item" >
<div class="chat-new-item-inner" style="flex-grow: 9;" onClick={() => route("/chat/new/InviteView")}>
<svg class="svg-inline--fa fa-smile fa-w-16" style="margin-right:10px;margin-top:3px; " x="0px" y="0px" viewBox="0 0 510 510">
<path fill="currentColor" d="M459,0H51C22.95,0,0,22.95,0,51v459l102-102h357c28.05,0,51-22.95,51-51V51C510,22.95,487.05,0,459,0z M102,178.5h306v51 H102V178.5z M306,306H102v-51h204V306z M408,153H102v-51h306V153z" />
</svg>
<p>{t("add_new_contact_or_group")}</p>
</div>
<div class="chat-new-item-inner" style="flex-grow: 1;" onClick={() => route("/chat/new/QRView")}>
<svg fill="currentColor" x="0px" y="0px" viewBox="0 0 122.88 122.7" style="enable-background:new 0 0 122.88 122.7; flex-grow: 1;" width="24px" height="24px">
<g>
<path class="st0" d="M0.18,0h44.63v44.45H0.18V0L0.18,0z M111.5,111.5h11.38v11.2H111.5V111.5L111.5,111.5z M89.63,111.48h11.38 v10.67H89.63h-0.01H78.25v-21.82h11.02V89.27h11.21V67.22h11.38v10.84h10.84v11.2h-10.84v11.2h-11.21h-0.17H89.63V111.48 L89.63,111.48z M55.84,89.09h11.02v-11.2H56.2v-11.2h10.66v-11.2H56.02v11.2H44.63v-11.2h11.2V22.23h11.38v33.25h11.02v11.2h10.84 v-11.2h11.38v11.2H89.63v11.2H78.25v22.05H67.22v22.23H55.84V89.09L55.84,89.09z M111.31,55.48h11.38v11.2h-11.38V55.48 L111.31,55.48z M22.41,55.48h11.38v11.2H22.41V55.48L22.41,55.48z M0.18,55.48h11.38v11.2H0.18V55.48L0.18,55.48z M55.84,0h11.38 v11.2H55.84V0L55.84,0z M0,78.06h44.63v44.45H0V78.06L0,78.06z M10.84,88.86h22.95v22.86H10.84V88.86L10.84,88.86z M78.06,0h44.63 v44.45H78.06V0L78.06,0z M88.91,10.8h22.95v22.86H88.91V10.8L88.91,10.8z M11.02,10.8h22.95v22.86H11.02V10.8L11.02,10.8z" />
</g>
</svg>
</div>
</div>
<h3>{t("your_invite_links")}</h3>
<div id="my-chat-links" class="flex-table">
{Object.keys(this.state.chatLinks).map(id => {
const url = this.state.chatLinks[id];
if(url == null){
return html``;
}
return html`
render() {
return (
<>
<h2>{t('invite_people_or_create_group')}</h2>
<div class="chat-new-item">
<div
class="chat-new-item-inner"
style="flex-grow: 9;"
onClick={() => route('/chat/new/InviteView')}
>
<svg
class="svg-inline--fa fa-smile fa-w-16"
style="margin-right:10px;margin-top:3px; "
x="0px"
y="0px"
viewBox="0 0 510 510"
>
<path
fill="currentColor"
d="M459,0H51C22.95,0,0,22.95,0,51v459l102-102h357c28.05,0,51-22.95,51-51V51C510,22.95,487.05,0,459,0z M102,178.5h306v51 H102V178.5z M306,306H102v-51h204V306z M408,153H102v-51h306V153z"
/>
</svg>
<p>{t('add_new_contact_or_group')}</p>
</div>
<div
class="chat-new-item-inner"
style="flex-grow: 1;"
onClick={() => route('/chat/new/QRView')}
>
<svg
fill="currentColor"
x="0px"
y="0px"
viewBox="0 0 122.88 122.7"
style="enable-background:new 0 0 122.88 122.7; flex-grow: 1;"
width="24px"
height="24px"
>
<g>
<path
class="st0"
d="M0.18,0h44.63v44.45H0.18V0L0.18,0z M111.5,111.5h11.38v11.2H111.5V111.5L111.5,111.5z M89.63,111.48h11.38 v10.67H89.63h-0.01H78.25v-21.82h11.02V89.27h11.21V67.22h11.38v10.84h10.84v11.2h-10.84v11.2h-11.21h-0.17H89.63V111.48 L89.63,111.48z M55.84,89.09h11.02v-11.2H56.2v-11.2h10.66v-11.2H56.02v11.2H44.63v-11.2h11.2V22.23h11.38v33.25h11.02v11.2h10.84 v-11.2h11.38v11.2H89.63v11.2H78.25v22.05H67.22v22.23H55.84V89.09L55.84,89.09z M111.31,55.48h11.38v11.2h-11.38V55.48 L111.31,55.48z M22.41,55.48h11.38v11.2H22.41V55.48L22.41,55.48z M0.18,55.48h11.38v11.2H0.18V55.48L0.18,55.48z M55.84,0h11.38 v11.2H55.84V0L55.84,0z M0,78.06h44.63v44.45H0V78.06L0,78.06z M10.84,88.86h22.95v22.86H10.84V88.86L10.84,88.86z M78.06,0h44.63 v44.45H78.06V0L78.06,0z M88.91,10.8h22.95v22.86H88.91V10.8L88.91,10.8z M11.02,10.8h22.95v22.86H11.02V10.8L11.02,10.8z"
/>
</g>
</svg>
</div>
</div>
<h3>{t('your_invite_links')}</h3>
<div id="my-chat-links" class="flex-table">
{Object.keys(this.state.chatLinks).map((id) => {
const url = this.state.chatLinks[id];
if (url == null) {
return html``;
}
return html`
<div class="flex-row">
<div class="flex-cell no-flex">
<${CopyButton} copyStr=${url}/>
</div>
<div class="flex-cell">
<input type="text" value=${url} onClick=${e => $(e.target).select()}/>
<input type="text" value=${url} onClick=${(e) =>
$(e.target).select()}/>
</div>
<div class="flex-cell no-flex">
<${Button} onClick=${() => this.removeChatLink(id)}>${t('remove')}</${Button}>
<${Button} onClick=${() => this.removeChatLink(id)}>${t(
'remove',
)}</${Button}>
</div>
</div>
`;
})}
<p>
<Button onClick={() => iris.Channel.createChatLink()}>
{t('create_new_invite_link')}
</Button>
</p>
<p><small dangerouslySetInnerHTML={{ __html: t('beware_of_sharing_invite_link_publicly', `href="/profile/${iris.session.getPubKey()}"`) }} /></p>
</div>
</>
);
}
})}
<p>
<Button onClick={() => iris.Channel.createChatLink()}>
{t('create_new_invite_link')}
</Button>
</p>
<p>
<small
dangerouslySetInnerHTML={{
__html: t(
'beware_of_sharing_invite_link_publicly',
`href="/profile/${iris.session.getPubKey()}"`,
),
}}
/>
</p>
</div>
</>
);
}
}
export default MainView;
export default MainView;

View File

@ -1,24 +1,28 @@
import { translate as t } from '../../../translations/Translation';
import Component from '../../../BaseComponent';
import {Helmet} from 'react-helmet';
import InviteView from './InviteView';
import QRView from './QRView';
import MainView from './MainView';
import { Helmet } from 'react-helmet';
import iris from 'iris-lib';
type Props = { view?: string; chatLinks: {URL?: string , ID?: string}};
type State = {chatLinks : {}};
import Component from '../../../BaseComponent';
import { translate as t } from '../../../translations/Translation';
import InviteView from './InviteView';
import MainView from './MainView';
import QRView from './QRView';
type Props = { view?: string; chatLinks: { URL?: string; ID?: string } };
type State = { chatLinks: Record<string, string> };
const chatlinks = {};
let ownqrurl = '';
class NewChat extends Component<Props,State> {
class NewChat extends Component<Props, State> {
constructor() {
super();
}
componentDidMount() {
iris.local().get('chatLinks').map(this.sub(
(url, id) => {
componentDidMount() {
iris
.local()
.get('chatLinks')
.map(
this.sub((url, id) => {
if (url) {
if (typeof url !== 'string' || url.indexOf('http') !== 0) return;
chatlinks[id] = url;
@ -26,30 +30,30 @@ class NewChat extends Component<Props,State> {
} else {
delete chatlinks[id];
}
this.setState({chatLinks: chatlinks});
}
));;
this.setState({ chatLinks: chatlinks });
}),
);
}
render() {
return (
<>
<Helmet>
<title>{t('new_chat')}</title>
</Helmet>
<div class="main-view" id="new-chat">
{(() => {
switch (this.props.view) {
case 'InviteView':
return <InviteView chatLinks={chatlinks} />;
case 'QRView':
return <QRView url={ownqrurl} />;
default:
return <MainView chatLinks={chatlinks} />;
}
})()}
</div>
</>
);
}
render(){
return(
<>
<Helmet><title>{t('new_chat')}</title></Helmet>
<div class="main-view" id="new-chat">
{(() => {
switch (this.props.view) {
case "InviteView":
return (<InviteView chatLinks={chatlinks}/>);
case "QRView":
return (<QRView url={ownqrurl}/>);
default:
return (<MainView chatLinks={chatlinks} />);
}
})()}
</div>
</>
);
}
}
export default NewChat;
export default NewChat;

View File

@ -1,52 +1,58 @@
import { translate as t } from '../../../translations/Translation';
import QRScanner from '../../../QRScanner';
import Button from '../../../components/basic/Button';
import $ from 'jquery';
import Component from '../../../BaseComponent';
import QRCode from '../../../lib/qrcode.min';
import Helpers from '../../../Helpers';
import iris from 'iris-lib';
import $ from 'jquery';
import Component from '../../../BaseComponent';
import Button from '../../../components/basic/Button';
import Helpers from '../../../Helpers';
import QRCode from '../../../lib/qrcode.min';
import QRScanner from '../../../QRScanner';
import { translate as t } from '../../../translations/Translation';
function setChatLinkQrCode(link) {
let qrCodeEl = $('#my-qr-code');
if (qrCodeEl.length === 0) { return; }
qrCodeEl.empty();
new QRCode(qrCodeEl[0], {
text: link || iris.session.getMyChatLink(),
width: 320,
height: 320,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
});
let qrCodeEl = $('#my-qr-code');
if (qrCodeEl.length === 0) {
return;
}
class QRView extends Component {
qrCodeEl.empty();
new QRCode(qrCodeEl[0], {
text: link || iris.session.getMyChatLink(),
width: 320,
height: 320,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.H,
});
}
componentDidMount() {
setChatLinkQrCode(this.props.url);
}
class QRView extends Component {
componentDidMount() {
setChatLinkQrCode(this.props.url);
}
scanChatLinkQr() {
if ($('#chatlink-qr-video:visible').length) {
$('#chatlink-qr-video').hide();
QRScanner.cleanupScanner();
} else {
$('#chatlink-qr-video').show();
QRScanner.startChatLinkQRScanner(result => result.text && Helpers.followChatLink(result.text));
}
}
render(){
return(
<>
<h2>{t('Show_or_scan_QR_code')}</h2>
<Button id="scan-chatlink-qr-btn" onClick={() => this.scanChatLinkQr()}>{t('scan_qr_code')}</Button>
<video id="chatlink-qr-video" width="320" height="320" style="object-fit: cover;" />
<h3>{t('your_qr_code')}</h3>
<p id="my-qr-code" class="qr-container" />
</>
);
scanChatLinkQr() {
if ($('#chatlink-qr-video:visible').length) {
$('#chatlink-qr-video').hide();
QRScanner.cleanupScanner();
} else {
$('#chatlink-qr-video').show();
QRScanner.startChatLinkQRScanner(
(result) => result.text && Helpers.followChatLink(result.text),
);
}
}
export default QRView;
render() {
return (
<>
<h2>{t('Show_or_scan_QR_code')}</h2>
<Button id="scan-chatlink-qr-btn" onClick={() => this.scanChatLinkQr()}>
{t('scan_qr_code')}
</Button>
<video id="chatlink-qr-video" width="320" height="320" style="object-fit: cover;" />
<h3>{t('your_qr_code')}</h3>
<p id="my-qr-code" class="qr-container" />
</>
);
}
}
export default QRView;

View File

@ -1,27 +1,35 @@
import { html } from 'htm/preact';
import Component from '../../BaseComponent';
import {ExistingAccountLogin} from '../Login';
import {translate as t} from '../../translations/Translation';
import { route } from 'preact-router';
import Component from '../../BaseComponent';
import Button from '../../components/basic/Button';
import { translate as t } from '../../translations/Translation';
import { ExistingAccountLogin } from '../Login';
export default class AccountSettings extends Component {
render() {
return (
<>
<>
<div class="centered-container">
<h3>{t('account')}</h3>
<p>
<b>{t('save_backup_of_privkey_first')}</b> {t('otherwise_cant_log_in_again')}
</p>
<div style="">
<Button onClick={() => route('/logout')}>{t('log_out')}</Button>
<Button onClick={() => this.setState({showSwitchAccount: !this.state.showSwitchAccount})}>{t('switch_account')}</Button>
<h3>{t('account')}</h3>
<p>
<b>{t('save_backup_of_privkey_first')}</b> {t('otherwise_cant_log_in_again')}
</p>
<div style="">
<Button onClick={() => route('/logout')}>{t('log_out')}</Button>
<Button
onClick={() =>
this.setState({
showSwitchAccount: !this.state.showSwitchAccount,
})
}
>
{t('switch_account')}
</Button>
</div>
{this.state.showSwitchAccount ? html`<${ExistingAccountLogin} />` : ''}
</div>
{this.state.showSwitchAccount ? html`<${ExistingAccountLogin}/>` : ''}
</div>
</>
</>
);
}
}

View File

@ -1,7 +1,7 @@
import iris from 'iris-lib';
import Component from '../../BaseComponent';
import {translate as t} from '../../translations/Translation';
import iris from 'iris-lib';
import { translate as t } from '../../translations/Translation';
export default class BetaSettings extends Component {
constructor() {
@ -9,26 +9,45 @@ export default class BetaSettings extends Component {
this.state = iris.session.DEFAULT_SETTINGS;
this.state.webPushSubscriptions = {};
this.state.blockedUsers = {};
this.id = "settings";
this.id = 'settings';
}
render() {
return (
<>
<>
<div class="centered-container">
<h3>Show beta features</h3>
<p><input type="checkbox" checked={this.state.local.showBetaFeatures} onChange={() => iris.local().get('settings').get('showBetaFeatures').put(!this.state.local.showBetaFeatures)} id="showBetaFeatures" /><label for="showBetaFeatures">{t('show_beta_features')}</label></p>
<h3>{t('show_beta_features')}</h3>
<p>
<input
type="checkbox"
checked={this.state.local.showBetaFeatures}
onChange={() =>
iris
.local()
.get('settings')
.get('showBetaFeatures')
.put(!this.state.local.showBetaFeatures)
}
id="showBetaFeatures"
/>
<label for="showBetaFeatures">{t('show_beta_features')}</label>
</p>
</div>
</>
</>
);
}
componentDidMount() {
iris.electron && iris.electron.get('settings').on(this.inject('electron', 'electron'));
iris.local().get('settings').on(this.sub(local => {
console.log('local settings', local);
if (local) {
this.setState({local});
}
}));
iris
.local()
.get('settings')
.on(
this.sub((local) => {
console.log('local settings', local);
if (local) {
this.setState({ local });
}
}),
);
}
}

View File

@ -1,8 +1,9 @@
import _ from 'lodash';
import Component from '../../BaseComponent';
import {translate as t} from '../../translations/Translation';
import Text from '../../components/Text';
import iris from 'iris-lib';
import filter from 'lodash/filter';
import Component from '../../BaseComponent';
import Text from '../../components/Text';
import { translate as t } from '../../translations/Translation';
export default class BlockedSettings extends Component {
constructor() {
@ -10,45 +11,58 @@ export default class BlockedSettings extends Component {
this.state = iris.session.DEFAULT_SETTINGS;
this.state.webPushSubscriptions = {};
this.state.blockedUsers = {};
this.id = "settings";
this.id = 'settings';
}
render() {
const blockedUsers = _.filter(Object.keys(this.state.blockedUsers), user => this.state.blockedUsers[user]);
const blockedUsers = filter(
Object.keys(this.state.blockedUsers),
(user) => this.state.blockedUsers[user],
);
return (
<>
<>
<div class="centered-container">
<h3>{t('blocked_users')}</h3>
{blockedUsers.map(user => {
if (this.state.blockedUsers[user]) {
return (<p key={user}><a href={`/profile/${ encodeURIComponent(user)}`} ><Text user={user} path="profile/name" placeholder="User" /></a></p>);
}
<h3>{t('blocked_users')}</h3>
{blockedUsers.map((user) => {
if (this.state.blockedUsers[user]) {
return (
<p key={user}>
<a href={`/profile/${encodeURIComponent(user)}`}>
<Text user={user} path="profile/name" placeholder="User" />
</a>
</p>
);
}
)
}
{blockedUsers.length === 0 ? t('none') : ''}
})}
{blockedUsers.length === 0 ? t('none') : ''}
</div>
</>
</>
);
}
componentDidMount() {
const blockedUsers = {};
iris.electron && iris.electron.get('settings').on(this.inject('electron', 'electron'));
iris.local().get('settings').on(this.sub(local => {
console.log('local settings', local);
if (local) {
this.setState({local});
}
}));
iris.public().get('block').map().on(this.sub(
(v,k) => {
blockedUsers[k] = v;
this.setState({blockedUsers});
}
));
iris
.local()
.get('settings')
.on(
this.sub((local) => {
console.log('local settings', local);
if (local) {
this.setState({ local });
}
}),
);
iris
.public()
.get('block')
.map()
.on(
this.sub((v, k) => {
blockedUsers[k] = v;
this.setState({ blockedUsers });
}),
);
}
}

View File

@ -1,59 +1,77 @@
import Helpers from '../../Helpers';
import Component from '../../BaseComponent';
import {translate as t} from '../../translations/Translation';
import CopyButton from '../../components/CopyButton';
import iris from 'iris-lib';
import QRCode from '../../lib/qrcode.min';
import $ from 'jquery';
import Component from '../../BaseComponent';
import Button from '../../components/basic/Button';
import CopyButton from '../../components/CopyButton';
import Helpers from '../../Helpers';
import QRCode from '../../lib/qrcode.min';
import { translate as t } from '../../translations/Translation';
export default class KeySettings extends Component {
constructor() {
super();
this.state = iris.session.DEFAULT_SETTINGS;
}
constructor(){
super();
this.state = iris.session.DEFAULT_SETTINGS;
mailtoSubmit(e) {
e.preventDefault();
if (this.state.email && this.state.email === this.state.retypeEmail) {
window.location.href = `mailto:${
this.state.email
}?&subject=Iris%20private%20key&body=${JSON.stringify(iris.session.getKey())}`;
}
mailtoSubmit(e) {
e.preventDefault();
if (this.state.email && this.state.email === this.state.retypeEmail) {
window.location.href = `mailto:${this.state.email}?&subject=Iris%20private%20key&body=${JSON.stringify(iris.session.getKey())}`;
}
}
}
render() {
return (
<>
<>
<div class="centered-container">
<h3>{t('private_key')}</h3>
<p dangerouslySetInnerHTML={{ __html: t('private_key_warning') }} />
<p>
<Button onClick={() => downloadKey()}>{t('download_private_key')}</Button>
<CopyButton notShareable={true} text={t('copy_private_key')} copyStr={JSON.stringify(iris.session.getKey())} />
</p>
<p>
<Button onClick={e => togglePrivateKeyQR(e)}>{t('show_privkey_qr')}</Button>
</p>
<div id="private-key-qr" class="qr-container" />
<hr />
<p>
{t('email_privkey_to_yourself')}:
</p>
<p>
<form onSubmit={e => this.mailtoSubmit(e)}>
<input name="email" type="email" onChange={e => this.setState({email:e.target.value.trim()})} placeholder={t('email')} />
<input name="verify_email" type="email" onChange={e => this.setState({retypeEmail:e.target.value.trim()})} placeholder={t('retype_email')} />
<Button type="submit">{t('send email')}</Button>
</form>
</p>
<hr />
<p><small dangerouslySetInnerHTML={{ __html: t('privkey_storage_recommendation')}} /></p>
<h3>{t('private_key')}</h3>
<p dangerouslySetInnerHTML={{ __html: t('private_key_warning') }} />
<p>
<Button onClick={() => downloadKey()}>{t('download_private_key')}</Button>
<CopyButton
notShareable={true}
text={t('copy_private_key')}
copyStr={JSON.stringify(iris.session.getKey())}
/>
</p>
<p>
<Button onClick={(e) => togglePrivateKeyQR(e)}>{t('show_privkey_qr')}</Button>
</p>
<div id="private-key-qr" class="qr-container" />
<hr />
<p>{t('email_privkey_to_yourself')}:</p>
<p>
<form onSubmit={(e) => this.mailtoSubmit(e)}>
<input
name="email"
type="email"
onChange={(e) => this.setState({ email: e.target.value.trim() })}
placeholder={t('email')}
/>
<input
name="verify_email"
type="email"
onChange={(e) => this.setState({ retypeEmail: e.target.value.trim() })}
placeholder={t('retype_email')}
/>
<Button type="submit">{t('send_email')}</Button>
</form>
</p>
<hr />
<p>
<small
dangerouslySetInnerHTML={{
__html: t('privkey_storage_recommendation'),
}}
/>
</p>
</div>
</>
</>
);
}
}
function downloadKey() {
@ -72,13 +90,16 @@ function togglePrivateKeyQR(e) {
$('#private-key-qr').empty();
btn.text(SHOW_TEXT);
}
function hideText(s) { return `${t('hide_privkey_qr') } (${ s })`; }
function hideText(s) {
return `${t('hide_privkey_qr')} (${s})`;
}
if (show) {
let showPrivateKeySecondsRemaining = 20;
btn.text(hideText(showPrivateKeySecondsRemaining));
hidePrivateKeyInterval = setInterval(() => {
if ($('#private-key-qr img').length === 0) {
clearInterval(hidePrivateKeyInterval);return;
clearInterval(hidePrivateKeyInterval);
return;
}
showPrivateKeySecondsRemaining -= 1;
if (showPrivateKeySecondsRemaining === 0) {
@ -92,9 +113,9 @@ function togglePrivateKeyQR(e) {
text: JSON.stringify(iris.session.getKey()),
width: 300,
height: 300,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.H,
});
} else {
reset();

View File

@ -1,38 +1,56 @@
import {AVAILABLE_LANGUAGES, AVAILABLE_LANGUAGE_KEYS, language, translate as t} from '../../translations/Translation';
import $ from 'jquery';
import { Fragment } from 'preact';
const LanguageSettings = () => {
return (
<>
<div class="centered-container">
import {
AVAILABLE_LANGUAGE_KEYS,
AVAILABLE_LANGUAGES,
language,
translate as t,
} from '../../translations/Translation';
const LanguageSettings = () => {
return (
<>
<div class="centered-container">
<h3>{t('language')}</h3>
<div class="centered-container">
{
Object.keys(AVAILABLE_LANGUAGES).map(l => {
let inputl = "";
if(l == language){
inputl = <input type="radio" name="language" id={l} onChange={e => onLanguageChange(e)} value={l} checked />;
}else{
inputl = <input type="radio" name="language" id={l} onChange={e => onLanguageChange(e)} value={l} />;
{Object.keys(AVAILABLE_LANGUAGES).map((l) => {
let inputl = '';
if (l == language) {
inputl = (
<input
type="radio"
name="language"
id={l}
onChange={(e) => onLanguageChange(e)}
value={l}
checked
/>
);
} else {
inputl = (
<input
type="radio"
name="language"
id={l}
onChange={(e) => onLanguageChange(e)}
value={l}
/>
);
}
return(
<Fragment key={l} >
return (
<Fragment key={l}>
{inputl}
<label for={l}>{AVAILABLE_LANGUAGES[l]}</label>
<br />
</Fragment >
);
}
)
}
</Fragment>
);
})}
</div>
</div>
</>
);
}
</div>
</>
);
};
function onLanguageChange(e) {
const l = $(e.target).val();
@ -41,4 +59,4 @@ function onLanguageChange(e) {
location.reload();
}
}
export default LanguageSettings;
export default LanguageSettings;

View File

@ -1,24 +1,31 @@
import Component from "../../BaseComponent";
import Name from "../../components/Name";
import Component from '../../BaseComponent';
import Name from '../../components/Name';
import iris from 'iris-lib';
import {translate as t} from "../../translations/Translation";
import Helpers from "../../Helpers";
import Icons from "../../Icons";
import {route} from "preact-router";
import $ from "jquery";
import Button from "../../components/basic/Button";
import { translate as t } from '../../translations/Translation';
import Helpers from '../../Helpers';
import Icons from '../../Icons';
import { route } from 'preact-router';
import $ from 'jquery';
import { filter } from 'lodash';
import Button from '../../components/basic/Button';
import { forEach } from 'lodash';
export default class PeerSettings extends Component {
updatePeersFromGunInterval = 0;
state = iris.session.DEFAULT_SETTINGS;
componentDidMount() {
iris.local().get('settings').on(this.sub(local => {
console.log('settings', local);
if (local) {
this.setState(local);
}
}));
iris
.local()
.get('settings')
.on(
this.sub((local) => {
console.log('settings', local);
if (local) {
this.setState(local);
}
}),
);
this.updatePeersFromGun();
this.updatePeersFromGunInterval = window.setInterval(() => this.updatePeersFromGun(), 1000);
}
@ -30,39 +37,73 @@ export default class PeerSettings extends Component {
render() {
return (
<>
<>
<div class="centered-container">
<h3>{t('peers')}</h3>
{this.renderPeerList()}
<p><input type="checkbox" checked={this.state.local.enablePublicPeerDiscovery}
onChange={() => iris.local().get('settings').get('enablePublicPeerDiscovery').put(!this.state.local.enablePublicPeerDiscovery)}
id="enablePublicPeerDiscovery" />
<label htmlFor="enablePublicPeerDiscovery">{t('enable_public_peer_discovery')}</label></p>
<h4>{t('maximum_number_of_peer_connections')}</h4>
<p>
<input type="number" value={this.state.local.maxConnectedPeers} onChange={e => {
const target = e.target;
iris.local().get('settings').get('maxConnectedPeers').put(target.value || 0);
}}/>
</p>
{Helpers.isElectron ? (
<>
<h4>{t('your_public_address')}</h4>
<p>http://{this.state.electron.publicIp || '-'}:8767/gun</p>
<p><small>If you're behind NAT (likely) and want to accept incoming connections, you need to configure your router to forward the port 8767 to this computer.</small></p>
</>
) : ''}
<h4>{t('set_up_your_own_peer')}</h4>
<p>
<small
dangerouslySetInnerHTML={{__html: t('peers_info', "href=\"https://github.com/amark/gun#deploy\"")}}></small>
</p>
<p><a href="https://heroku.com/deploy?template=https://github.com/mmalmi/rod">
{Icons.herokuButton}
</a></p>
<p>{t('also')} <a href="https://github.com/amark/gun#docker">Docker</a> {t('or_small')} <a href="https://github.com/irislib/iris-electron">Iris-electron</a>.</p>
</div>
</>
<h3>{t('peers')}</h3>
{this.renderPeerList()}
<p>
<input
type="checkbox"
checked={this.state.local.enablePublicPeerDiscovery}
onChange={() =>
iris
.local()
.get('settings')
.get('enablePublicPeerDiscovery')
.put(!this.state.local.enablePublicPeerDiscovery)
}
id="enablePublicPeerDiscovery"
/>
<label htmlFor="enablePublicPeerDiscovery">{t('enable_public_peer_discovery')}</label>
</p>
<h4>{t('maximum_number_of_peer_connections')}</h4>
<p>
<input
type="number"
value={this.state.local.maxConnectedPeers}
onChange={(e) => {
const target = e.target;
iris
.local()
.get('settings')
.get('maxConnectedPeers')
.put(target.value || 0);
}}
/>
</p>
{Helpers.isElectron ? (
<>
<h4>{t('your_public_address')}</h4>
<p>http://{this.state.electron.publicIp || '-'}:8767/gun</p>
<p>
<small>
If you're behind NAT (likely) and want to accept incoming connections, you need to
configure your router to forward the port 8767 to this computer.
</small>
</p>
</>
) : (
''
)}
<h4>{t('set_up_your_own_peer')}</h4>
<p>
<small
dangerouslySetInnerHTML={{
__html: t('peers_info', 'href="https://github.com/amark/gun#deploy"'),
}}
></small>
</p>
<p>
<a href="https://heroku.com/deploy?template=https://github.com/mmalmi/rod">
{Icons.herokuButton}
</a>
</p>
<p>
{t('also')} <a href="https://github.com/amark/gun#docker">Docker</a> {t('or_small')}{' '}
<a href="https://github.com/irislib/iris-electron">Iris-electron</a>.
</p>
</div>
</>
);
}
@ -77,67 +118,102 @@ export default class PeerSettings extends Component {
}
enablePeerClicked(url, peerFromGun, peer) {
peer.enabled ? iris.peers.disable(url,peerFromGun) : iris.peers.connect(url);
peer.enabled ? iris.peers.disable(url, peerFromGun) : iris.peers.connect(url);
}
renderPeerList() {
let urls = Object.keys(iris.peers.known);
if (this.state.peersFromGun) {
Object.keys(this.state.peersFromGun).forEach(url => urls.indexOf(url) === -1 && urls.push(url));
Object.keys(this.state.peersFromGun).forEach(
(url) => urls.indexOf(url) === -1 && urls.push(url),
);
}
return (
<div id="peers" class="flex-table">
{urls.length === 0 ?
<Button id="reset-peers" style="margin-bottom: 15px" onClick={() => this.resetPeersClicked()}>{t('restore_defaults')}</Button>
: ''}
{urls.map(url => {
const peer = iris.peers.known[url] || {};
const peerFromGun = this.state.peersFromGun && this.state.peersFromGun[url];
const connected = peerFromGun && peerFromGun.wire && peerFromGun.wire.readyState == 1 && peerFromGun.wire.bufferedAmount === 0;
return (
<div class="flex-row peer">
<div class="flex-cell">
{connected ? (
<span class="tooltip" style="color: var(--positive-color);margin-right:15px">
<span class="tooltiptext">Connected</span>
<svg height="14" width="14" x="0px" y="0px" viewBox="0 0 191.667 191.667"><path fill="currentColor" d="M95.833,0C42.991,0,0,42.99,0,95.833s42.991,95.834,95.833,95.834s95.833-42.991,95.833-95.834S148.676,0,95.833,0z M150.862,79.646l-60.207,60.207c-2.56,2.56-5.963,3.969-9.583,3.969c-3.62,0-7.023-1.409-9.583-3.969l-30.685-30.685 c-2.56-2.56-3.97-5.963-3.97-9.583c0-3.621,1.41-7.024,3.97-9.584c2.559-2.56,5.962-3.97,9.583-3.97c3.62,0,7.024,1.41,9.583,3.971 l21.101,21.1l50.623-50.623c2.56-2.56,5.963-3.969,9.583-3.969c3.62,0,7.023,1.409,9.583,3.969 C156.146,65.765,156.146,74.362,150.862,79.646z"/></svg>
</span>
) : (
<small class="tooltip" style="margin-right:15px">
<span class="tooltiptext">Disconnected</span>
<svg width="14" height="14" x="0px" y="0px" viewBox="0 0 512 512" fill="currentColor"><path d="M257,0C116.39,0,0,114.39,0,255s116.39,257,257,257s255-116.39,255-257S397.61,0,257,0z M383.22,338.79 c11.7,11.7,11.7,30.73,0,42.44c-11.61,11.6-30.64,11.79-42.44,0L257,297.42l-85.79,83.82c-11.7,11.7-30.73,11.7-42.44,0 c-11.7-11.7-11.7-30.73,0-42.44l83.8-83.8l-83.8-83.8c-11.7-11.71-11.7-30.74,0-42.44c11.71-11.7,30.74-11.7,42.44,0L257,212.58 l83.78-83.82c11.68-11.68,30.71-11.72,42.44,0c11.7,11.7,11.7,30.73,0,42.44l-83.8,83.8L383.22,338.79z"/></svg>
{urls.length === 0 ? (
<Button
id="reset-peers"
style="margin-bottom: 15px"
onClick={() => this.resetPeersClicked()}
>
{t('restore_defaults')}
</Button>
) : (
''
)}
{urls.map((url) => {
const peer = iris.peers.known[url] || {};
const peerFromGun = this.state.peersFromGun && this.state.peersFromGun[url];
const connected =
peerFromGun &&
peerFromGun.wire &&
peerFromGun.wire.readyState == 1 &&
peerFromGun.wire.bufferedAmount === 0;
return (
<div class="flex-row peer">
<div class="flex-cell">
{connected ? (
<span class="tooltip" style="color: var(--positive-color);margin-right:15px">
<span class="tooltiptext">Connected</span>
<svg height="14" width="14" x="0px" y="0px" viewBox="0 0 191.667 191.667">
<path
fill="currentColor"
d="M95.833,0C42.991,0,0,42.99,0,95.833s42.991,95.834,95.833,95.834s95.833-42.991,95.833-95.834S148.676,0,95.833,0z M150.862,79.646l-60.207,60.207c-2.56,2.56-5.963,3.969-9.583,3.969c-3.62,0-7.023-1.409-9.583-3.969l-30.685-30.685 c-2.56-2.56-3.97-5.963-3.97-9.583c0-3.621,1.41-7.024,3.97-9.584c2.559-2.56,5.962-3.97,9.583-3.97c3.62,0,7.024,1.41,9.583,3.971 l21.101,21.1l50.623-50.623c2.56-2.56,5.963-3.969,9.583-3.969c3.62,0,7.023,1.409,9.583,3.969 C156.146,65.765,156.146,74.362,150.862,79.646z"
/>
</svg>
</span>
) : (
<small class="tooltip" style="margin-right:15px">
<span class="tooltiptext">Disconnected</span>
<svg
width="14"
height="14"
x="0px"
y="0px"
viewBox="0 0 512 512"
fill="currentColor"
>
<path d="M257,0C116.39,0,0,114.39,0,255s116.39,257,257,257s255-116.39,255-257S397.61,0,257,0z M383.22,338.79 c11.7,11.7,11.7,30.73,0,42.44c-11.61,11.6-30.64,11.79-42.44,0L257,297.42l-85.79,83.82c-11.7,11.7-30.73,11.7-42.44,0 c-11.7-11.7-11.7-30.73,0-42.44l83.8-83.8l-83.8-83.8c-11.7-11.71-11.7-30.74,0-42.44c11.71-11.7,30.74-11.7,42.44,0L257,212.58 l83.78-83.82c11.68-11.68,30.71-11.72,42.44,0c11.7,11.7,11.7,30.73,0,42.44l-83.8,83.8L383.22,338.79z" />
</svg>
</small>
)}
{url}
{peer.from ? (
<>
<br />
<small style="cursor:pointer" onClick={() => route(`/profile/{peer.from}`)}>
{t('from')} <Name pub={peer.from} placeholder={peer.from.slice(0, 6)} />
</small>
)}
{url}
{peer.from ? (
<>
<br />
<small style="cursor:pointer" onClick={() => route(`/profile/{peer.from}`)}>
{t('from')} <Name pub={peer.from} placeholder={peer.from.slice(0,6)} />
</small>
</>
) : ''}
</div>
<div class="flex-cell no-flex">
<Button onClick={() => this.removePeerClicked(url, peerFromGun)}>{t('remove')}</Button>
<Button onClick={() => this.enablePeerClicked(url, peerFromGun, peer)}>{peer.enabled ? t('disable') : t('enable')}</Button>
</div>
</>
) : (
''
)}
</div>
);
})
}
<div class="flex-cell no-flex">
<Button onClick={() => this.removePeerClicked(url, peerFromGun)}>
{t('remove')}
</Button>
<Button onClick={() => this.enablePeerClicked(url, peerFromGun, peer)}>
{peer.enabled ? t('disable') : t('enable')}
</Button>
</div>
</div>
);
})}
<div class="flex-row" id="add-peer-row">
<div class="flex-cell">
<input type="url" id="add-peer-url" placeholder={t('peer_url')} />
<input type="checkbox" id="add-peer-public" />
<label for="add-peer-public">{t('public')}</label>
<Button id="add-peer-btn" onClick={() => this.addPeerClicked()}>{t('add')}</Button>
<Button id="add-peer-btn" onClick={() => this.addPeerClicked()}>
{t('add')}
</Button>
</div>
</div>
<p>
<small dangerouslySetInnerHTML={{ __html:t('public_peer_info') }}></small>
<small dangerouslySetInnerHTML={{ __html: t('public_peer_info') }}></small>
</p>
</div>
);
@ -148,15 +224,26 @@ export default class PeerSettings extends Component {
}
updatePeersFromGun() {
const peersFromGun = iris.global().back('opt.peers') || {};
// @ts-ignore
this.setState({peersFromGun});
const peers = {};
const peersFromGun = iris.global().back('opt.peers');
forEach(peersFromGun, (obj) => {
if (!obj.id || !obj.url) {
return;
}
peers[obj.id] = obj;
});
// @ts-ignore
this.setState({ peersFromGun: peers });
}
addPeerClicked() {
let url = $('#add-peer-url').val();
if (!url) {
return;
}
let visibility = $('#add-peer-public').is(':checked') ? 'public' : undefined;
iris.peers.add({url, visibility});
iris.peers.add({ url, visibility });
$('#add-peer-url').val('');
}
}
}

View File

@ -1,61 +1,70 @@
import { Component, html } from 'htm/preact';
import SettingsMenu from './SettingsMenu';
import SettingsContent from './SettingsContent';
import Header from '../../components/Header';
import Icons from '../../Icons';
import { Component } from 'htm/preact';
import iris from 'iris-lib';
import $ from 'jquery';
import { route } from 'preact-router';
type Props = { page?: string;};
import Header from '../../components/Header';
import Icons from '../../Icons';
import SettingsContent from './SettingsContent';
import SettingsMenu from './SettingsMenu';
type Props = { page?: string };
type State = {
toggleSettingsMenu: boolean;
showSettingsMenu: boolean;
platform: string;
}
};
class Settings extends Component<Props,State> {
class Settings extends Component<Props, State> {
componentDidMount() {
iris.local().get('toggleSettingsMenu').on((show: boolean) => this.toggleMenu(show));
iris
.local()
.get('toggleSettingsMenu')
.on((show: boolean) => this.toggleMenu(show));
}
toggleMenu(show: boolean): void {
this.setState({showSettingsMenu: typeof show === 'undefined' ? !this.state.toggleSettingsMenu : show});
this.setState({
showSettingsMenu: typeof show === 'undefined' ? !this.state.toggleSettingsMenu : show,
});
}
render() {
const isDesktopNonMac = this.state.platform && this.state.platform !== 'darwin';
// ? const isDesktopNonMac = this.state.platform && this.state.platform !== 'darwin'
return (
<>
<Header />
<div class="main-view" id="settings">
<div style="flex-direction: row;" id="settings">
<div class='logo' className={this.props.page ? 'visible-xs-flex' : 'hidden' }>
<div href="/settings/" onClick={e => this.onLogoClick(e) } style="margin: 1em; display:flex;" >
<div>{Icons.backArrow}</div>
<Header />
<div class="main-view" id="settings">
<div style="flex-direction: row;" id="settings">
<div class="logo" className={this.props.page ? 'visible-xs-flex' : 'hidden'}>
<div
href="/settings/"
onClick={(e) => this.onLogoClick(e)}
style="margin: 1em; display:flex;"
>
<div>{Icons.backArrow}</div>
</div>
</div>
<SettingsMenu activePage={this.props.page} />
<div className={this.props.page ? '' : 'hidden-xs'} style="padding: 0px 15px;">
<SettingsContent id={this.props.page} />
</div>
</div>
<SettingsMenu activePage={this.props.page} />
<div className={this.props.page ? '' : 'hidden-xs' } style="padding: 0px 15px;">
<SettingsContent id={this.props.page} />
</div>
</div>
</div>
</>
);
}
onLogoClick(e) {
console.log("test open" + ($(window).width() > 625));
console.log('test open' + ($(window).width() > 625));
e.preventDefault();
e.stopPropagation();
$('a.logo').blur();
($(window).width() > 625);
$(window).width() > 625;
iris.local().get('toggleSettingsMenu').put(true);
route('/settings/')
route('/settings/');
}
}
export default Settings;
export default Settings;

View File

@ -1,36 +1,33 @@
import Component from '../../BaseComponent';
import AccountSettings from './AccountSettings';
import KeySettings from './KeySettings';
import PeerSettings from './PeerSettings';
import LanguageSettings from './LanguageSettings';
import WebtorrentSettings from './WebtorrentSettings';
import WebRTCSettings from './WebRTCSettings';
import BetaSettings from './BetaSettings';
import BlockedSettings from './BlockedSettings';
import KeySettings from './KeySettings';
import LanguageSettings from './LanguageSettings';
import PeerSettings from './PeerSettings';
import WebRTCSettings from './WebRTCSettings';
import WebtorrentSettings from './WebtorrentSettings';
export default class SettingsContent extends Component {
content ="";
pages = {
account: AccountSettings,
key: KeySettings,
peer: PeerSettings,
language: LanguageSettings,
webtorrent: WebtorrentSettings,
webrtc: WebRTCSettings,
beta: BetaSettings,
blocked: BlockedSettings,
};
content = '';
pages = {
account: AccountSettings,
key: KeySettings,
peer: PeerSettings,
language: LanguageSettings,
webtorrent: WebtorrentSettings,
webrtc: WebRTCSettings,
beta: BetaSettings,
blocked: BlockedSettings,
};
constructor() {
super();
this.content = "home";
this.content = 'home';
}
render() {
const Content = this.pages[this.props.id] || this.pages.account;
return (
<Content />
);
return <Content />;
}
}

View File

@ -1,14 +1,15 @@
import Component from '../../BaseComponent';
import {translate as t} from '../../translations/Translation';
import { route } from 'preact-router';
import { html } from 'htm/preact';
import iris from 'iris-lib';
import Helpers from "../../Helpers";
import {html} from "htm/preact";
import { route } from 'preact-router';
import Component from '../../BaseComponent';
import Helpers from '../../Helpers';
import { translate as t } from '../../translations/Translation';
const SETTINGS = {
account: 'account',
key: 'private_key',
peer: 'peer',
peer: 'peers',
language: 'language',
webtorrent: 'webtorrent',
webrtc: 'webRTC',
@ -16,9 +17,8 @@ const SETTINGS = {
blocked: 'blocked_users',
};
export default class SettingsMenu extends Component{
menuLinkClicked(url,e) {
export default class SettingsMenu extends Component {
menuLinkClicked(url, e) {
e.preventDefault();
iris.local().get('toggleSettingsMenu').put(false);
iris.local().get('scrollUp').put(true);
@ -28,21 +28,25 @@ export default class SettingsMenu extends Component{
render() {
const activePage = this.props.activePage || 'account';
return (
<>
<div className={!this.props.activePage ? 'settings-list' : 'settings-list hidden-xs' }>
{Helpers.isElectron ? html`<div class="electron-padding"/>` : html`
<h3 class="visible-xs-block" style="padding: 0px 15px;">${t('settings')}</h3>
`}
{Object.keys(SETTINGS).map(page => {
return (
<a href="#" class={(activePage === page && window.innerWidth > 624) ? 'active' : ''} onClick={e => this.menuLinkClicked(page,e)} key={page}>
<span class="text">{t(SETTINGS[page])}</span>
</a>
);
}
)}
</div>
</>
);
<>
<div className={!this.props.activePage ? 'settings-list' : 'settings-list hidden-xs'}>
{Helpers.isElectron
? html`<div class="electron-padding" />`
: html` <h3 class="visible-xs-block" style="padding: 0px 15px;">${t('settings')}</h3> `}
{Object.keys(SETTINGS).map((page) => {
return (
<a
href="#"
class={activePage === page && window.innerWidth > 624 ? 'active' : ''}
onClick={(e) => this.menuLinkClicked(page, e)}
key={page}
>
<span class="text">{t(SETTINGS[page])}</span>
</a>
);
})}
</div>
</>
);
}
}

Some files were not shown because too many files have changed in this diff Show More